Path: blob/main/pkg/metrics/instance/configstore/api_test.go
5317 views
package configstore12import (3"bytes"4"context"5"encoding/json"6"fmt"7"io"8"net/http"9"net/http/httptest"10"strings"11"testing"12"time"1314"github.com/go-kit/log"15"github.com/gorilla/mux"16"github.com/grafana/agent/pkg/client"17"github.com/grafana/agent/pkg/metrics/cluster/configapi"18"github.com/grafana/agent/pkg/metrics/instance"19"github.com/stretchr/testify/assert"20"github.com/stretchr/testify/require"21)2223func TestAPI_ListConfigurations(t *testing.T) {24s := &Mock{25ListFunc: func(ctx context.Context) ([]string, error) {26return []string{"a", "b", "c"}, nil27},28}2930api := NewAPI(log.NewNopLogger(), s, nil, true)31env := newAPITestEnvironment(t, api)3233resp, err := http.Get(env.srv.URL + "/agent/api/v1/configs")34require.NoError(t, err)35require.Equal(t, http.StatusOK, resp.StatusCode)3637expect := `{38"status": "success",39"data": {40"configs": ["a", "b", "c"]41}42}`43body, err := io.ReadAll(resp.Body)44require.NoError(t, err)45require.JSONEq(t, expect, string(body))4647t.Run("With Client", func(t *testing.T) {48cli := client.New(env.srv.URL)49apiResp, err := cli.ListConfigs(context.Background())50require.NoError(t, err)5152expect := &configapi.ListConfigurationsResponse{Configs: []string{"a", "b", "c"}}53require.Equal(t, expect, apiResp)54})55}5657func TestAPI_GetConfiguration_Invalid(t *testing.T) {58s := &Mock{59GetFunc: func(ctx context.Context, key string) (instance.Config, error) {60return instance.Config{}, NotExistError{Key: key}61},62}6364api := NewAPI(log.NewNopLogger(), s, nil, true)65env := newAPITestEnvironment(t, api)6667resp, err := http.Get(env.srv.URL + "/agent/api/v1/configs/does-not-exist")68require.NoError(t, err)69require.Equal(t, http.StatusNotFound, resp.StatusCode)7071expect := `{72"status": "error",73"data": {74"error": "configuration does-not-exist does not exist"75}76}`77body, err := io.ReadAll(resp.Body)78require.NoError(t, err)79require.JSONEq(t, expect, string(body))8081t.Run("With Client", func(t *testing.T) {82cli := client.New(env.srv.URL)83_, err := cli.GetConfiguration(context.Background(), "does-not-exist")84require.NotNil(t, err)85require.Equal(t, "configuration does-not-exist does not exist", err.Error())86})87}8889func TestAPI_GetConfiguration(t *testing.T) {90s := &Mock{91GetFunc: func(ctx context.Context, key string) (instance.Config, error) {92return instance.Config{93Name: key,94HostFilter: true,95RemoteFlushDeadline: 10 * time.Minute,96}, nil97},98}99100api := NewAPI(log.NewNopLogger(), s, nil, true)101env := newAPITestEnvironment(t, api)102103resp, err := http.Get(env.srv.URL + "/agent/api/v1/configs/exists")104require.NoError(t, err)105require.Equal(t, http.StatusOK, resp.StatusCode)106107expect := `{108"status": "success",109"data": {110"value": "name: exists\nhost_filter: true\nremote_flush_deadline: 10m0s\n"111}112}`113body, err := io.ReadAll(resp.Body)114require.NoError(t, err)115require.JSONEq(t, expect, string(body))116117t.Run("With Client", func(t *testing.T) {118cli := client.New(env.srv.URL)119actual, err := cli.GetConfiguration(context.Background(), "exists")120require.NoError(t, err)121122// The client will apply defaults, so we need to start with the DefaultConfig123// as a base here.124expect := instance.DefaultConfig125expect.Name = "exists"126expect.HostFilter = true127expect.RemoteFlushDeadline = 10 * time.Minute128require.Equal(t, &expect, actual)129})130}131132func TestAPI_GetConfiguration_ScrubSecrets(t *testing.T) {133rawConfig := `name: exists134scrape_configs:135- job_name: local_scrape136follow_redirects: true137enable_http2: true138honor_timestamps: true139metrics_path: /metrics140scheme: http141static_configs:142- targets:143- 127.0.0.1:12345144labels:145cluster: localhost146basic_auth:147username: admin148password: SCRUBME149remote_write:150- url: http://localhost:9009/api/prom/push151remote_timeout: 30s152name: test-d0f32c153send_exemplars: true154basic_auth:155username: admin156password: SCRUBME157queue_config:158capacity: 500159max_shards: 1000160min_shards: 1161max_samples_per_send: 100162batch_send_deadline: 5s163min_backoff: 30ms164max_backoff: 100ms165follow_redirects: true166enable_http2: true167metadata_config:168send: true169send_interval: 1m170max_samples_per_send: 500171wal_truncate_frequency: 1m0s172min_wal_time: 5m0s173max_wal_time: 4h0m0s174remote_flush_deadline: 1m0s175`176scrubbedConfig := strings.ReplaceAll(rawConfig, "SCRUBME", "<secret>")177178s := &Mock{179GetFunc: func(ctx context.Context, key string) (instance.Config, error) {180c, err := instance.UnmarshalConfig(strings.NewReader(rawConfig))181if err != nil {182return instance.Config{}, err183}184return *c, nil185},186}187188api := NewAPI(log.NewNopLogger(), s, nil, true)189env := newAPITestEnvironment(t, api)190191resp, err := http.Get(env.srv.URL + "/agent/api/v1/configs/exists")192require.NoError(t, err)193require.Equal(t, http.StatusOK, resp.StatusCode)194respBytes, err := io.ReadAll(resp.Body)195require.NoError(t, err)196197var apiResp struct {198Status string `json:"status"`199Data struct {200Value string `json:"value"`201} `json:"data"`202}203err = json.Unmarshal(respBytes, &apiResp)204require.NoError(t, err)205require.Equal(t, "success", apiResp.Status)206require.YAMLEq(t, scrubbedConfig, apiResp.Data.Value)207208t.Run("With Client", func(t *testing.T) {209cli := client.New(env.srv.URL)210actual, err := cli.GetConfiguration(context.Background(), "exists")211require.NoError(t, err)212213// Marshal the retrieved config _without_ scrubbing. This means214// that if the secrets weren't scrubbed from GetConfiguration, something215// bad happened at the API level.216actualBytes, err := instance.MarshalConfig(actual, false)217require.NoError(t, err)218require.YAMLEq(t, scrubbedConfig, string(actualBytes))219})220}221222func TestServer_GetConfiguration_Disabled(t *testing.T) {223api := NewAPI(log.NewNopLogger(), nil, nil, false)224env := newAPITestEnvironment(t, api)225resp, err := http.Get(env.srv.URL + "/agent/api/v1/configs/exists")226require.NoError(t, err)227require.Equal(t, http.StatusNotFound, resp.StatusCode)228body, err := io.ReadAll(resp.Body)229require.NoError(t, err)230require.Equal(t, []byte("404 - config endpoint is disabled"), body)231}232233func TestServer_PutConfiguration(t *testing.T) {234var s Mock235236api := NewAPI(log.NewNopLogger(), &s, nil, true)237env := newAPITestEnvironment(t, api)238239cfg := instance.Config{Name: "newconfig"}240bb, err := instance.MarshalConfig(&cfg, false)241require.NoError(t, err)242243t.Run("Created", func(t *testing.T) {244// Created configs should return http.StatusCreated245s.PutFunc = func(ctx context.Context, c instance.Config) (created bool, err error) {246return true, nil247}248249resp, err := http.Post(env.srv.URL+"/agent/api/v1/config/newconfig", "", bytes.NewReader(bb))250require.NoError(t, err)251require.Equal(t, http.StatusCreated, resp.StatusCode)252})253254t.Run("Updated", func(t *testing.T) {255// Updated configs should return http.StatusOK256s.PutFunc = func(ctx context.Context, c instance.Config) (created bool, err error) {257return false, nil258}259260resp, err := http.Post(env.srv.URL+"/agent/api/v1/config/newconfig", "", bytes.NewReader(bb))261require.NoError(t, err)262require.Equal(t, http.StatusOK, resp.StatusCode)263})264}265266func TestServer_PutConfiguration_Invalid(t *testing.T) {267var s Mock268269api := NewAPI(log.NewNopLogger(), &s, func(c *instance.Config) error {270return fmt.Errorf("custom validation error")271}, true)272env := newAPITestEnvironment(t, api)273274cfg := instance.Config{Name: "newconfig"}275bb, err := instance.MarshalConfig(&cfg, false)276require.NoError(t, err)277278resp, err := http.Post(env.srv.URL+"/agent/api/v1/config/newconfig", "", bytes.NewReader(bb))279require.NoError(t, err)280require.Equal(t, http.StatusBadRequest, resp.StatusCode)281282expect := `{283"status": "error",284"data": {285"error": "failed to validate config: custom validation error"286}287}`288body, err := io.ReadAll(resp.Body)289require.NoError(t, err)290require.JSONEq(t, expect, string(body))291}292293func TestServer_PutConfiguration_WithClient(t *testing.T) {294var s Mock295api := NewAPI(log.NewNopLogger(), &s, nil, true)296env := newAPITestEnvironment(t, api)297298cfg := instance.DefaultConfig299cfg.Name = "newconfig-withclient"300cfg.HostFilter = true301cfg.RemoteFlushDeadline = 10 * time.Minute302303s.PutFunc = func(ctx context.Context, c instance.Config) (created bool, err error) {304assert.Equal(t, cfg, c)305return true, nil306}307308cli := client.New(env.srv.URL)309err := cli.PutConfiguration(context.Background(), "newconfig-withclient", &cfg)310require.NoError(t, err)311}312313func TestServer_DeleteConfiguration(t *testing.T) {314s := &Mock{315DeleteFunc: func(ctx context.Context, key string) error {316assert.Equal(t, "deleteme", key)317return nil318},319}320321api := NewAPI(log.NewNopLogger(), s, nil, true)322env := newAPITestEnvironment(t, api)323324req, err := http.NewRequest(http.MethodDelete, env.srv.URL+"/agent/api/v1/config/deleteme", nil)325require.NoError(t, err)326resp, err := http.DefaultClient.Do(req)327require.NoError(t, err)328require.Equal(t, http.StatusOK, resp.StatusCode)329330t.Run("With Client", func(t *testing.T) {331cli := client.New(env.srv.URL)332err := cli.DeleteConfiguration(context.Background(), "deleteme")333require.NoError(t, err)334})335}336337func TestServer_DeleteConfiguration_Invalid(t *testing.T) {338s := &Mock{339DeleteFunc: func(ctx context.Context, key string) error {340assert.Equal(t, "deleteme", key)341return NotExistError{Key: key}342},343}344345api := NewAPI(log.NewNopLogger(), s, nil, true)346env := newAPITestEnvironment(t, api)347348req, err := http.NewRequest(http.MethodDelete, env.srv.URL+"/agent/api/v1/config/deleteme", nil)349require.NoError(t, err)350resp, err := http.DefaultClient.Do(req)351require.NoError(t, err)352require.Equal(t, http.StatusNotFound, resp.StatusCode)353354t.Run("With Client", func(t *testing.T) {355cli := client.New(env.srv.URL)356err := cli.DeleteConfiguration(context.Background(), "deleteme")357require.Error(t, err)358})359}360361func TestServer_URLEncoded(t *testing.T) {362var s Mock363364api := NewAPI(log.NewNopLogger(), &s, nil, true)365env := newAPITestEnvironment(t, api)366367var cfg instance.Config368bb, err := instance.MarshalConfig(&cfg, false)369require.NoError(t, err)370371s.PutFunc = func(ctx context.Context, c instance.Config) (created bool, err error) {372assert.Equal(t, "url/encoded", c.Name)373return true, nil374}375376resp, err := http.Post(env.srv.URL+"/agent/api/v1/config/url%2Fencoded", "", bytes.NewReader(bb))377require.NoError(t, err)378require.Equal(t, http.StatusCreated, resp.StatusCode)379380s.GetFunc = func(ctx context.Context, key string) (instance.Config, error) {381assert.Equal(t, "url/encoded", key)382return instance.Config{Name: "url/encoded"}, nil383}384385resp, err = http.Get(env.srv.URL + "/agent/api/v1/configs/url%2Fencoded")386require.NoError(t, err)387require.Equal(t, http.StatusOK, resp.StatusCode)388}389390type apiTestEnvironment struct {391srv *httptest.Server392router *mux.Router393}394395func newAPITestEnvironment(t *testing.T, api *API) apiTestEnvironment {396t.Helper()397398router := mux.NewRouter()399srv := httptest.NewServer(router)400t.Cleanup(srv.Close)401402api.WireAPI(router)403404return apiTestEnvironment{srv: srv, router: router}405}406407408