// Copyright 2021 The Prometheus Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package xds import ( "context" "errors" "fmt" "testing" "time" v3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" "github.com/prometheus/common/model" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" "gopkg.in/yaml.v2" "github.com/prometheus/prometheus/discovery/targetgroup" ) var ( kumaConf = sdConf testKumaMadsV1Resources = []*MonitoringAssignment{ { Mesh: "metrics", Service: "prometheus", Targets: []*MonitoringAssignment_Target{ { Name: "prometheus-01", Scheme: "http", Address: "10.1.4.32:9090", MetricsPath: "/custom-metrics", Labels: map[string]string{ "commit_hash": "620506a88", }, }, { Name: "prometheus-02", Scheme: "http", Address: "10.1.4.33:9090", Labels: map[string]string{ "commit_hash": "3513bba00", }, }, }, Labels: map[string]string{ "kuma.io/zone": "us-east-1", "team": "infra", }, }, { Mesh: "metrics", Service: "grafana", Targets: []*MonitoringAssignment_Target{}, Labels: map[string]string{ "kuma.io/zone": "us-east-1", "team": "infra", }, }, { Mesh: "data", Service: "elasticsearch", Targets: []*MonitoringAssignment_Target{ { Name: "elasticsearch-01", Scheme: "http", Address: "10.1.1.1", Labels: map[string]string{ "role": "ml", }, }, }, }, } ) func getKumaMadsV1DiscoveryResponse(resources ...*MonitoringAssignment) (*v3.DiscoveryResponse, error) { serialized := make([]*anypb.Any, len(resources)) for i, res := range resources { data, err := proto.Marshal(res) if err != nil { return nil, err } serialized[i] = &anypb.Any{ TypeUrl: KumaMadsV1ResourceTypeURL, Value: data, } } return &v3.DiscoveryResponse{ TypeUrl: KumaMadsV1ResourceTypeURL, Resources: serialized, }, nil } func newKumaTestHTTPDiscovery(c KumaSDConfig) (*fetchDiscovery, error) { kd, err := NewKumaHTTPDiscovery(&c, nopLogger) if err != nil { return nil, err } pd, ok := kd.(*fetchDiscovery) if !ok { return nil, errors.New("not a fetchDiscovery") } return pd, nil } func TestKumaMadsV1ResourceParserInvalidTypeURL(t *testing.T) { resources := make([]*anypb.Any, 0) groups, err := kumaMadsV1ResourceParser(resources, "type.googleapis.com/some.api.v1.Monitoring") require.Nil(t, groups) require.Error(t, err) } func TestKumaMadsV1ResourceParserEmptySlice(t *testing.T) { resources := make([]*anypb.Any, 0) groups, err := kumaMadsV1ResourceParser(resources, KumaMadsV1ResourceTypeURL) require.Len(t, groups, 0) require.NoError(t, err) } func TestKumaMadsV1ResourceParserValidResources(t *testing.T) { res, err := getKumaMadsV1DiscoveryResponse(testKumaMadsV1Resources...) require.NoError(t, err) targets, err := kumaMadsV1ResourceParser(res.Resources, KumaMadsV1ResourceTypeURL) require.NoError(t, err) require.Len(t, targets, 3) expectedTargets := []model.LabelSet{ { "__address__": "10.1.4.32:9090", "__metrics_path__": "/custom-metrics", "__scheme__": "http", "instance": "prometheus-01", "__meta_kuma_mesh": "metrics", "__meta_kuma_service": "prometheus", "__meta_kuma_label_team": "infra", "__meta_kuma_label_kuma_io_zone": "us-east-1", "__meta_kuma_label_commit_hash": "620506a88", "__meta_kuma_dataplane": "prometheus-01", }, { "__address__": "10.1.4.33:9090", "__metrics_path__": "", "__scheme__": "http", "instance": "prometheus-02", "__meta_kuma_mesh": "metrics", "__meta_kuma_service": "prometheus", "__meta_kuma_label_team": "infra", "__meta_kuma_label_kuma_io_zone": "us-east-1", "__meta_kuma_label_commit_hash": "3513bba00", "__meta_kuma_dataplane": "prometheus-02", }, { "__address__": "10.1.1.1", "__metrics_path__": "", "__scheme__": "http", "instance": "elasticsearch-01", "__meta_kuma_mesh": "data", "__meta_kuma_service": "elasticsearch", "__meta_kuma_label_role": "ml", "__meta_kuma_dataplane": "elasticsearch-01", }, } require.Equal(t, expectedTargets, targets) } func TestKumaMadsV1ResourceParserInvalidResources(t *testing.T) { data, err := protoJSONMarshalOptions.Marshal(&MonitoringAssignment_Target{}) require.NoError(t, err) resources := []*anypb.Any{{ TypeUrl: KumaMadsV1ResourceTypeURL, Value: data, }} groups, err := kumaMadsV1ResourceParser(resources, KumaMadsV1ResourceTypeURL) require.Nil(t, groups) require.Error(t, err) require.Contains(t, err.Error(), "cannot parse") } func TestNewKumaHTTPDiscovery(t *testing.T) { kd, err := newKumaTestHTTPDiscovery(kumaConf) require.NoError(t, err) require.NotNil(t, kd) resClient, ok := kd.client.(*HTTPResourceClient) require.True(t, ok) require.Equal(t, kumaConf.Server, resClient.Server()) require.Equal(t, KumaMadsV1ResourceTypeURL, resClient.ResourceTypeURL()) require.NotEmpty(t, resClient.ID()) require.Equal(t, KumaMadsV1ResourceType, resClient.config.ResourceType) } func TestKumaHTTPDiscoveryRefresh(t *testing.T) { s := createTestHTTPServer(t, func(request *v3.DiscoveryRequest) (*v3.DiscoveryResponse, error) { if request.VersionInfo == "1" { return nil, nil } res, err := getKumaMadsV1DiscoveryResponse(testKumaMadsV1Resources...) require.NoError(t, err) res.VersionInfo = "1" res.Nonce = "abc" return res, nil }) defer s.Close() cfgString := fmt.Sprintf(` --- server: %s refresh_interval: 10s tls_config: insecure_skip_verify: true `, s.URL) var cfg KumaSDConfig require.NoError(t, yaml.Unmarshal([]byte(cfgString), &cfg)) kd, err := newKumaTestHTTPDiscovery(cfg) require.NoError(t, err) require.NotNil(t, kd) ch := make(chan []*targetgroup.Group, 1) kd.poll(context.Background(), ch) groups := <-ch require.Len(t, groups, 1) targets := groups[0].Targets require.Len(t, targets, 3) expectedTargets := []model.LabelSet{ { "__address__": "10.1.4.32:9090", "__metrics_path__": "/custom-metrics", "__scheme__": "http", "instance": "prometheus-01", "__meta_kuma_mesh": "metrics", "__meta_kuma_service": "prometheus", "__meta_kuma_label_team": "infra", "__meta_kuma_label_kuma_io_zone": "us-east-1", "__meta_kuma_label_commit_hash": "620506a88", "__meta_kuma_dataplane": "prometheus-01", }, { "__address__": "10.1.4.33:9090", "__metrics_path__": "", "__scheme__": "http", "instance": "prometheus-02", "__meta_kuma_mesh": "metrics", "__meta_kuma_service": "prometheus", "__meta_kuma_label_team": "infra", "__meta_kuma_label_kuma_io_zone": "us-east-1", "__meta_kuma_label_commit_hash": "3513bba00", "__meta_kuma_dataplane": "prometheus-02", }, { "__address__": "10.1.1.1", "__metrics_path__": "", "__scheme__": "http", "instance": "elasticsearch-01", "__meta_kuma_mesh": "data", "__meta_kuma_service": "elasticsearch", "__meta_kuma_label_role": "ml", "__meta_kuma_dataplane": "elasticsearch-01", }, } require.Equal(t, expectedTargets, targets) // Should skip the next update. ctx, cancel := context.WithCancel(context.Background()) go func() { time.Sleep(1 * time.Second) cancel() }() kd.poll(ctx, ch) select { case <-ctx.Done(): return case <-ch: require.Fail(t, "no update expected") } }