diff --git a/datasource.go b/datasource.go index 72fff526..f697c91b 100644 --- a/datasource.go +++ b/datasource.go @@ -3,6 +3,7 @@ package gapi import ( "encoding/json" "fmt" + "strings" ) // DataSource represents a Grafana data source. @@ -19,16 +20,12 @@ type DataSource struct { Database string `json:"database,omitempty"` User string `json:"user,omitempty"` - // Deprecated: Use secureJsonData.password instead. - Password string `json:"password,omitempty"` OrgID int64 `json:"orgId,omitempty"` IsDefault bool `json:"isDefault"` BasicAuth bool `json:"basicAuth"` BasicAuthUser string `json:"basicAuthUser,omitempty"` - // Deprecated: Use secureJsonData.basicAuthPassword instead. - BasicAuthPassword string `json:"basicAuthPassword,omitempty"` JSONData map[string]interface{} `json:"jsonData,omitempty"` SecureJSONData map[string]interface{} `json:"secureJsonData,omitempty"` @@ -138,3 +135,49 @@ func (c *Client) DeleteDataSourceByName(name string) error { return c.request("DELETE", path, nil, nil, nil) } + +func cloneMap(m map[string]interface{}) map[string]interface{} { + clone := make(map[string]interface{}) + for k, v := range m { + clone[k] = v + } + return clone +} + +func JSONDataWithHeaders(jsonData, secureJSONData map[string]interface{}, headers map[string]string) (map[string]interface{}, map[string]interface{}) { + // Clone the maps so we don't modify the original + jsonData = cloneMap(jsonData) + secureJSONData = cloneMap(secureJSONData) + + idx := 1 + for name, value := range headers { + jsonData[fmt.Sprintf("httpHeaderName%d", idx)] = name + secureJSONData[fmt.Sprintf("httpHeaderValue%d", idx)] = value + idx += 1 + } + + return jsonData, secureJSONData +} + +func ExtractHeadersFromJSONData(jsonData, secureJSONData map[string]interface{}) (map[string]interface{}, map[string]interface{}, map[string]string) { + // Clone the maps so we don't modify the original + jsonData = cloneMap(jsonData) + secureJSONData = cloneMap(secureJSONData) + headers := make(map[string]string) + + for dataName, dataValue := range jsonData { + if strings.HasPrefix(dataName, "httpHeaderName") { + // Remove the header name from JSON data + delete(jsonData, dataName) + + // Remove the header value from secure JSON data + secureDataName := strings.Replace(dataName, "httpHeaderName", "httpHeaderValue", 1) + delete(secureJSONData, secureDataName) + + headerName := dataValue.(string) + headers[headerName] = "true" // We can't retrieve the headers, so we just set a dummy value + } + } + + return jsonData, secureJSONData, headers +} diff --git a/datasource_cache.go b/datasource_cache.go new file mode 100644 index 00000000..f091ebd6 --- /dev/null +++ b/datasource_cache.go @@ -0,0 +1,72 @@ +package gapi + +import ( + "encoding/json" + "fmt" +) + +type DatasourceCache struct { + Message string `json:"message"` + DatasourceID int64 `json:"dataSourceID"` + DatasourceUID string `json:"dataSourceUID"` + Enabled bool `json:"enabled"` + TTLQueriesMs int64 `json:"ttlQueriesMs"` + TTLResourcesMs int64 `json:"ttlResourcesMs"` + UseDefaultTLS bool `json:"useDefaultTTL"` + DefaultTTLMs int64 `json:"defaultTTLMs"` + Created string `json:"created"` + Updated string `json:"updated"` +} + +type DatasourceCachePayload struct { + DatasourceID int64 `json:"dataSourceID"` + DatasourceUID string `json:"dataSourceUID"` + Enabled bool `json:"enabled"` + UseDefaultTLS bool `json:"useDefaultTTL"` + TTLQueriesMs int64 `json:"ttlQueriesMs"` + TTLResourcesMs int64 `json:"ttlResourcesMs"` +} + +// EnableDatasourceCache enables the datasource cache (this is a datasource setting) +func (c *Client) EnableDatasourceCache(id int64) error { + path := fmt.Sprintf("/api/datasources/%d/cache/enable", id) + if err := c.request("POST", path, nil, nil, nil); err != nil { + return fmt.Errorf("error enabling cache at %s: %w", path, err) + } + return nil +} + +// DisableDatasourceCache disables the datasource cache (this is a datasource setting) +func (c *Client) DisableDatasourceCache(id int64) error { + path := fmt.Sprintf("/api/datasources/%d/cache/disable", id) + if err := c.request("POST", path, nil, nil, nil); err != nil { + return fmt.Errorf("error disabling cache at %s: %w", path, err) + } + return nil +} + +// UpdateDatasourceCache updates the cache configurations +func (c *Client) UpdateDatasourceCache(id int64, payload *DatasourceCachePayload) error { + path := fmt.Sprintf("/api/datasources/%d/cache", id) + data, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("marshal err: %w", err) + } + + if err = c.request("POST", path, nil, data, nil); err != nil { + return fmt.Errorf("error updating cache at %s: %w", path, err) + } + + return nil +} + +// DatasourceCache fetches datasource cache configuration +func (c *Client) DatasourceCache(id int64) (*DatasourceCache, error) { + path := fmt.Sprintf("/api/datasources/%d/cache", id) + cache := &DatasourceCache{} + err := c.request("GET", path, nil, nil, cache) + if err != nil { + return cache, fmt.Errorf("error getting cache at %s: %w", path, err) + } + return cache, nil +} diff --git a/datasource_cache_test.go b/datasource_cache_test.go new file mode 100644 index 00000000..377b4338 --- /dev/null +++ b/datasource_cache_test.go @@ -0,0 +1,84 @@ +package gapi + +import ( + "testing" + + "github.com/gobs/pretty" +) + +const ( + getDatasourceCacheJSON = ` + { + "message": "Data source cache settings loaded", + "dataSourceID": 1, + "dataSourceUID": "jZrmlLCGka", + "enabled": true, + "useDefaultTTL": false, + "ttlQueriesMs": 60000, + "ttlResourcesMs": 300000, + "defaultTTLMs": 300000, + "created": "2023-04-21T11:49:22-04:00", + "updated": "2023-04-24T17:03:40-04:00" + }` + updateDatasourceCacheJSON = ` + { + "message": "Data source cache settings updated", + "dataSourceID": 1, + "dataSourceUID": "jZrmlLCGka", + "enabled": true, + "useDefaultTTL": false, + "ttlQueriesMs": 60000, + "ttlResourcesMs": 300000, + "defaultTTLMs": 300000, + "created": "2023-04-21T11:49:22-04:00", + "updated": "2023-04-24T17:03:40-04:00" + }` +) + +func TestDatasourceCache(t *testing.T) { + client := gapiTestTools(t, 200, getDatasourceCacheJSON) + resp, err := client.DatasourceCache(1) + if err != nil { + t.Fatal(err) + } + + t.Log(pretty.PrettyFormat(resp)) + + expects := DatasourceCache{ + Message: "Data source cache settings loaded", + DatasourceID: 1, + DatasourceUID: "jZrmlLCGka", + Enabled: true, + UseDefaultTLS: false, + TTLQueriesMs: 60000, + TTLResourcesMs: 300000, + DefaultTTLMs: 300000, + Created: "2023-04-21T11:49:22-04:00", + Updated: "2023-04-24T17:03:40-04:00", + } + + if resp.Enabled != expects.Enabled || + resp.DatasourceUID != expects.DatasourceUID || + resp.UseDefaultTLS != expects.UseDefaultTLS || + resp.TTLQueriesMs != expects.TTLQueriesMs || + resp.TTLResourcesMs != expects.TTLResourcesMs || + resp.DefaultTTLMs != expects.DefaultTTLMs { + t.Error("Not correctly parsing returned datasource cache") + } +} + +func TestUpdateDatasourceCache(t *testing.T) { + client := gapiTestTools(t, 200, updateDatasourceCacheJSON) + payload := &DatasourceCachePayload{ + DatasourceID: 1, + DatasourceUID: "jZrmlLCGka", + Enabled: true, + UseDefaultTLS: true, + TTLQueriesMs: 6000, + TTLResourcesMs: 30000, + } + err := client.UpdateDatasourceCache(1, payload) + if err != nil { + t.Error(err) + } +} diff --git a/datasource_json_data.go b/datasource_json_data.go deleted file mode 100644 index 385777c2..00000000 --- a/datasource_json_data.go +++ /dev/null @@ -1,224 +0,0 @@ -package gapi - -import ( - "encoding/json" - "fmt" - "strings" -) - -type LokiDerivedField struct { - Name string `json:"name"` - MatcherRegex string `json:"matcherRegex"` - URL string `json:"url"` - DatasourceUID string `json:"datasourceUid,omitempty"` -} - -// JSONData is a representation of the datasource `jsonData` property -type JSONData struct { - // Used by all datasources - TLSAuth bool `json:"tlsAuth,omitempty"` - TLSAuthWithCACert bool `json:"tlsAuthWithCACert,omitempty"` - TLSConfigurationMethod string `json:"tlsConfigurationMethod,omitempty"` - TLSSkipVerify bool `json:"tlsSkipVerify,omitempty"` - - // Used by Athena - Catalog string `json:"catalog,omitempty"` - Database string `json:"database,omitempty"` - OutputLocation string `json:"outputLocation,omitempty"` - Workgroup string `json:"workgroup,omitempty"` - - // Used by Github - GitHubURL string `json:"githubUrl,omitempty"` - - // Used by Graphite - GraphiteVersion string `json:"graphiteVersion,omitempty"` - - // Used by Prometheus, Elasticsearch, InfluxDB, MySQL, PostgreSQL and MSSQL - TimeInterval string `json:"timeInterval,omitempty"` - - // Used by Elasticsearch - // From Grafana 8.x esVersion is the semantic version of Elasticsearch. - EsVersion string `json:"esVersion,omitempty"` - TimeField string `json:"timeField,omitempty"` - Interval string `json:"interval,omitempty"` - LogMessageField string `json:"logMessageField,omitempty"` - LogLevelField string `json:"logLevelField,omitempty"` - MaxConcurrentShardRequests int64 `json:"maxConcurrentShardRequests,omitempty"` - XpackEnabled bool `json:"xpack"` - - // Used by Cloudwatch - CustomMetricsNamespaces string `json:"customMetricsNamespaces,omitempty"` - TracingDatasourceUID string `json:"tracingDatasourceUid,omitempty"` - - // Used by Cloudwatch, Athena - AuthType string `json:"authType,omitempty"` - AssumeRoleArn string `json:"assumeRoleArn,omitempty"` - DefaultRegion string `json:"defaultRegion,omitempty"` - Endpoint string `json:"endpoint,omitempty"` - ExternalID string `json:"externalId,omitempty"` - Profile string `json:"profile,omitempty"` - - // Used by Loki - DerivedFields []LokiDerivedField `json:"derivedFields,omitempty"` - MaxLines int `json:"maxLines,omitempty"` - - // Used by OpenTSDB - TsdbVersion int64 `json:"tsdbVersion,omitempty"` - TsdbResolution int64 `json:"tsdbResolution,omitempty"` - - // Used by MSSQL - Encrypt string `json:"encrypt,omitempty"` - - // Used by PostgreSQL - Sslmode string `json:"sslmode,omitempty"` - PostgresVersion int64 `json:"postgresVersion,omitempty"` - Timescaledb bool `json:"timescaledb"` - - // Used by MySQL, PostgreSQL and MSSQL - MaxOpenConns int64 `json:"maxOpenConns,omitempty"` - MaxIdleConns int64 `json:"maxIdleConns,omitempty"` - ConnMaxLifetime int64 `json:"connMaxLifetime,omitempty"` - - // Used by Prometheus - HTTPMethod string `json:"httpMethod,omitempty"` - QueryTimeout string `json:"queryTimeout,omitempty"` - - // Used by Stackdriver - AuthenticationType string `json:"authenticationType,omitempty"` - ClientEmail string `json:"clientEmail,omitempty"` - DefaultProject string `json:"defaultProject,omitempty"` - TokenURI string `json:"tokenUri,omitempty"` - - // Used by Prometheus and Elasticsearch - SigV4AssumeRoleArn string `json:"sigV4AssumeRoleArn,omitempty"` - SigV4Auth bool `json:"sigV4Auth"` - SigV4AuthType string `json:"sigV4AuthType,omitempty"` - SigV4ExternalID string `json:"sigV4ExternalID,omitempty"` - SigV4Profile string `json:"sigV4Profile,omitempty"` - SigV4Region string `json:"sigV4Region,omitempty"` - - // Used by Prometheus and Loki - ManageAlerts bool `json:"manageAlerts"` - AlertmanagerUID string `json:"alertmanagerUid,omitempty"` - - // Used by Alertmanager - Implementation string `json:"implementation,omitempty"` - - // Used by Sentry - OrgSlug string `json:"orgSlug,omitempty"` - URL string `json:"url,omitempty"` // Sentry is not using the datasource URL attribute - - // Used by InfluxDB - DefaultBucket string `json:"defaultBucket,omitempty"` - Organization string `json:"organization,omitempty"` - Version string `json:"version,omitempty"` - - // Used by Azure Monitor - ClientID string `json:"clientId,omitempty"` - CloudName string `json:"cloudName,omitempty"` - SubscriptionID string `json:"subscriptionId,omitempty"` - TenantID string `json:"tenantId,omitempty"` -} - -// Marshal JSONData -func (d JSONData) Map() (map[string]interface{}, error) { - b, err := json.Marshal(d) - if err != nil { - return nil, err - } - fields := make(map[string]interface{}) - if err = json.Unmarshal(b, &fields); err != nil { - return nil, err - } - - return fields, nil -} - -// SecureJSONData is a representation of the datasource `secureJsonData` property -type SecureJSONData struct { - // Used by all datasources - TLSCACert string `json:"tlsCACert,omitempty"` - TLSClientCert string `json:"tlsClientCert,omitempty"` - TLSClientKey string `json:"tlsClientKey,omitempty"` - Password string `json:"password,omitempty"` - BasicAuthPassword string `json:"basicAuthPassword,omitempty"` - - // Used by Cloudwatch, Athena - AccessKey string `json:"accessKey,omitempty"` - SecretKey string `json:"secretKey,omitempty"` - - // Used by Stackdriver - PrivateKey string `json:"privateKey,omitempty"` - - // Used by Prometheus and Elasticsearch - SigV4AccessKey string `json:"sigV4AccessKey,omitempty"` - SigV4SecretKey string `json:"sigV4SecretKey,omitempty"` - - // Used by GitHub - AccessToken string `json:"accessToken,omitempty"` - - // Used by Sentry - AuthToken string `json:"authToken,omitempty"` - - // Used by Azure Monitor - ClientSecret string `json:"clientSecret,omitempty"` -} - -func (d SecureJSONData) Map() (map[string]interface{}, error) { - b, err := json.Marshal(d) - if err != nil { - return nil, err - } - fields := make(map[string]interface{}) - if err = json.Unmarshal(b, &fields); err != nil { - return nil, err - } - - return fields, nil -} - -func cloneMap(m map[string]interface{}) map[string]interface{} { - clone := make(map[string]interface{}) - for k, v := range m { - clone[k] = v - } - return clone -} - -func JSONDataWithHeaders(jsonData, secureJSONData map[string]interface{}, headers map[string]string) (map[string]interface{}, map[string]interface{}) { - // Clone the maps so we don't modify the original - jsonData = cloneMap(jsonData) - secureJSONData = cloneMap(secureJSONData) - - idx := 1 - for name, value := range headers { - jsonData[fmt.Sprintf("httpHeaderName%d", idx)] = name - secureJSONData[fmt.Sprintf("httpHeaderValue%d", idx)] = value - idx += 1 - } - - return jsonData, secureJSONData -} - -func ExtractHeadersFromJSONData(jsonData, secureJSONData map[string]interface{}) (map[string]interface{}, map[string]interface{}, map[string]string) { - // Clone the maps so we don't modify the original - jsonData = cloneMap(jsonData) - secureJSONData = cloneMap(secureJSONData) - headers := make(map[string]string) - - for dataName, dataValue := range jsonData { - if strings.HasPrefix(dataName, "httpHeaderName") { - // Remove the header name from JSON data - delete(jsonData, dataName) - - // Remove the header value from secure JSON data - secureDataName := strings.Replace(dataName, "httpHeaderName", "httpHeaderValue", 1) - delete(secureJSONData, secureDataName) - - headerName := dataValue.(string) - headers[headerName] = "true" // We can't retrieve the headers, so we just set a dummy value - } - } - - return jsonData, secureJSONData, headers -} diff --git a/datasource_test.go b/datasource_test.go index 448c3833..fd83fab9 100644 --- a/datasource_test.go +++ b/datasource_test.go @@ -15,22 +15,16 @@ const ( func TestNewDataSource(t *testing.T) { client := gapiTestTools(t, 200, createdDataSourceJSON) - jd, err := JSONData{ - AssumeRoleArn: "arn:aws:iam::123:role/some-role", - AuthType: "keys", - CustomMetricsNamespaces: "SomeNamespace", - DefaultRegion: "us-east-1", - TLSSkipVerify: true, - }.Map() - if err != nil { - t.Fatal(err) + jd := map[string]interface{}{ + "assumeRoleArn": "arn:aws:iam::123:role/some-role", + "authType": "keys", + "customMetricsNamespaces": "SomeNamespace", + "defaultRegion": "us-east-1", + "tlsSkipVerify": true, } - sjd, err := SecureJSONData{ - AccessKey: "123", - SecretKey: "456", - }.Map() - if err != nil { - t.Fatal(err) + sjd := map[string]interface{}{ + "accessKey": "123", + "secretKey": "456", } ds := &DataSource{ @@ -58,13 +52,10 @@ func TestNewDataSource(t *testing.T) { func TestNewPrometheusDataSource(t *testing.T) { client := gapiTestTools(t, 200, createdDataSourceJSON) - jd, err := JSONData{ - HTTPMethod: "POST", - QueryTimeout: "60s", - TimeInterval: "1m", - }.Map() - if err != nil { - t.Fatal(err) + jd := map[string]interface{}{ + "httpMethod": "POST", + "queryTimeout": "60s", + "timeInterval": "1m", } ds := &DataSource{ @@ -91,21 +82,15 @@ func TestNewPrometheusDataSource(t *testing.T) { func TestNewPrometheusSigV4DataSource(t *testing.T) { client := gapiTestTools(t, 200, createdDataSourceJSON) - jd, err := JSONData{ - HTTPMethod: "POST", - SigV4Auth: true, - SigV4AuthType: "keys", - SigV4Region: "us-east-1", - }.Map() - if err != nil { - t.Fatal(err) + jd := map[string]interface{}{ + "httpMethod": "POST", + "sigV4Auth": true, + "sigV4AuthType": "keys", + "sigV4Region": "us-east-1", } - sjd, err := SecureJSONData{ - SigV4AccessKey: "123", - SigV4SecretKey: "456", - }.Map() - if err != nil { - t.Fatal(err) + sjd := map[string]interface{}{ + "sigV4AccessKey": "123", + "sigV4SecretKey": "456", } ds := &DataSource{ @@ -133,16 +118,13 @@ func TestNewPrometheusSigV4DataSource(t *testing.T) { func TestNewElasticsearchDataSource(t *testing.T) { client := gapiTestTools(t, 200, createdDataSourceJSON) - jd, err := JSONData{ - EsVersion: "7.0.0", - TimeField: "time", - Interval: "1m", - LogMessageField: "message", - LogLevelField: "field", - MaxConcurrentShardRequests: 8, - }.Map() - if err != nil { - t.Fatal(err) + jd := map[string]interface{}{ + "esVersion": "7.0.0", + "timeField": "time", + "interval": "1m", + "logMessageField": "message", + "logLevelField": "field", + "maxConcurrentShardRequests": 8, } ds := &DataSource{ @@ -168,21 +150,16 @@ func TestNewElasticsearchDataSource(t *testing.T) { func TestNewInfluxDBDataSource(t *testing.T) { client := gapiTestTools(t, 200, createdDataSourceJSON) - jd, err := JSONData{ - DefaultBucket: "telegraf", - Organization: "acme", - Version: "Flux", - }.Map() - if err != nil { - t.Fatal(err) - } - sjd, err := SecureJSONData{}.Map() - if err != nil { - t.Fatal(err) - } - jd, sjd = JSONDataWithHeaders(jd, sjd, map[string]string{ - "Authorization": "Token alksdjaslkdjkslajdkj.asdlkjaksdjlkajsdlkjsaldj==", - }) + jd, sjd := JSONDataWithHeaders( + map[string]interface{}{ + "defaultBucket": "telegraf", + "organization": "acme", + "version": "Flux", + }, + map[string]interface{}{}, + map[string]string{ + "Authorization": "Token alksdjaslkdjkslajdkj.asdlkjaksdjlkajsdlkjsaldj==", + }) ds := &DataSource{ Name: "foo_influxdb", @@ -208,12 +185,9 @@ func TestNewInfluxDBDataSource(t *testing.T) { func TestNewOpenTSDBDataSource(t *testing.T) { client := gapiTestTools(t, 200, createdDataSourceJSON) - jd, err := JSONData{ - TsdbResolution: 1, - TsdbVersion: 3, - }.Map() - if err != nil { - t.Fatal(err) + jd := map[string]interface{}{ + "tsdbResolution": 1, + "tsdbVersion": 3, } ds := &DataSource{ @@ -240,20 +214,14 @@ func TestNewOpenTSDBDataSource(t *testing.T) { func TestNewAzureDataSource(t *testing.T) { client := gapiTestTools(t, 200, createdDataSourceJSON) - jd, err := JSONData{ - ClientID: "lorem-ipsum", - CloudName: "azuremonitor", - SubscriptionID: "lorem-ipsum", - TenantID: "lorem-ipsum", - }.Map() - if err != nil { - t.Fatal(err) + jd := map[string]interface{}{ + "clientId": "lorem-ipsum", + "cloudName": "azuremonitor", + "subscriptionId": "lorem-ipsum", + "tenantId": "lorem-ipsum", } - sjd, err := SecureJSONData{ - ClientSecret: "alksdjaslkdjkslajdkj.asdlkjaksdjlkajsdlkjsaldj==", - }.Map() - if err != nil { - t.Fatal(err) + sjd := map[string]interface{}{ + "clientSecret": "alksdjaslkdjkslajdkj.asdlkjaksdjlkajsdlkjsaldj==", } ds := &DataSource{