diff --git a/.github/workflows/build-and-upload-release.yml b/.github/workflows/build-and-upload-release.yml index f31e61374..38fc1ecce 100644 --- a/.github/workflows/build-and-upload-release.yml +++ b/.github/workflows/build-and-upload-release.yml @@ -6,7 +6,9 @@ env: # Use terraform assume role for uploading to ecr AWS_ASSUME_ROLE: ${{ secrets.TERRAFORM_AWS_ASSUME_ROLE }} ECR_OPERATOR_STAGING_REPO: ${{ vars.ECR_OPERATOR_STAGING_REPO }} - ECR_OPERATOR_RELEASE_IMAGE: ${{ secrets.ECR_OPERATOR_RELEASE_IMAGE }} + ECR_TARGET_ALLOCATOR_STAGING_REPO: ${{ vars.ECR_TARGET_ALLOCATOR_STAGING_REPO}} + ECR_OPERATOR_RELEASE_IMAGE: ${{ vars.ECR_TARGET_ALLOCATOR_TEST_OPERATOR_REPO}} + ECR_TARGET_ALLOCATOR_RELEASE_REPO: ${{ vars.ECR_TARGET_ALLOCATOR_RELEASE_REPO}} on: workflow_dispatch: @@ -81,9 +83,60 @@ jobs: tags: ${{ env.ECR_OPERATOR_STAGING_REPO }}:${{ inputs.tag }} platforms: linux/amd64, linux/arm64 + MakeTABinary: + name: 'MakeTargetAllocatorImage' + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Go 1.x + uses: actions/setup-go@v4 + with: + go-version: '>1.22' + cache: true + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + role-to-assume: ${{ env.AWS_ASSUME_ROLE }} + aws-region: us-west-2 + + - name: Login to ECR + if: steps.cached_binaries.outputs.cache-hit == false + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + + - name: Set up Docker Buildx + if: steps.cached_binaries.outputs.cache-hit == false + uses: docker/setup-buildx-action@v1 + + - name: Set up QEMU + if: steps.cached_binaries.outputs.cache-hit == false + uses: docker/setup-qemu-action@v1 + + - name: Build Binaries + run: | + go mod download + export GOARCH=arm64 && make targetallocator + export GOARCH=amd64 && make targetallocator + - name: Build Cloudwatch Agent Target Allocator Image and push to ECR + uses: docker/build-push-action@v4 + if: steps.cached_binaries.outputs.cache-hit == false + with: + file: ./cmd/amazon-cloudwatch-agent-target-allocator/Dockerfile + context: ./cmd/amazon-cloudwatch-agent-target-allocator + push: true + tags: ${{ env.ECR_TARGET_ALLOCATOR_STAGING_REPO }}:${{ inputs.tag }} + platforms: linux/amd64, linux/arm64 + e2e-test: name: "Application Signals E2E Test" - needs: MakeBinary + needs: [MakeBinary] uses: ./.github/workflows/application-signals-e2e-test.yml secrets: inherit permissions: @@ -119,4 +172,10 @@ jobs: run: | docker buildx imagetools create \ -t ${{ env.ECR_OPERATOR_RELEASE_IMAGE }} \ - ${{ env.ECR_OPERATOR_STAGING_REPO }}:${{ inputs.tag }} \ No newline at end of file + ${{ env.ECR_OPERATOR_STAGING_REPO }}:${{ inputs.tag }} + + - name: Push image to TA release ECR + run: | + docker buildx imagetools create \ + -t ${{ env.ECR_TARGET_ALLOCATOR_RELEASE_REPO}} \ + ${{ env.ECR_TARGET_ALLOCATOR_STAGING_REPO }}:${{ inputs.tag }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index e3bca8565..c99c02890 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM golang:1.20 as builder +FROM golang:1.21 as builder # set goproxy=direct ENV GOPROXY direct @@ -30,9 +30,10 @@ ARG AUTO_INSTRUMENTATION_DOTNET_VERSION ARG AUTO_INSTRUMENTATION_NODEJS_VERSION ARG DCMG_EXPORTER_VERSION ARG NEURON_MONITOR_VERSION +ARG TARGET_ALLOCATOR_VERSION # Build -RUN CGO_ENABLED=0 GOOS=linux GO111MODULE=on go build -ldflags="-X ${VERSION_PKG}.version=${VERSION} -X ${VERSION_PKG}.buildDate=${VERSION_DATE} -X ${VERSION_PKG}.agent=${AGENT_VERSION} -X ${VERSION_PKG}.autoInstrumentationJava=${AUTO_INSTRUMENTATION_JAVA_VERSION} -X ${VERSION_PKG}.autoInstrumentationPython=${AUTO_INSTRUMENTATION_PYTHON_VERSION} -X ${VERSION_PKG}.autoInstrumentationDotNet=${AUTO_INSTRUMENTATION_DOTNET_VERSION} -X ${VERSION_PKG}.autoInstrumentationNodeJS=${AUTO_INSTRUMENTATION_NODEJS_VERSION} -X ${VERSION_PKG}.dcgmExporter=${DCMG_EXPORTER_VERSION} -X ${VERSION_PKG}.neuronMonitor=${NEURON_MONITOR_VERSION}" -a -o manager main.go +RUN CGO_ENABLED=0 GOOS=linux GO111MODULE=on go build -ldflags="-X ${VERSION_PKG}.version=${VERSION} -X ${VERSION_PKG}.buildDate=${VERSION_DATE} -X ${VERSION_PKG}.agent=${AGENT_VERSION} -X ${VERSION_PKG}.autoInstrumentationJava=${AUTO_INSTRUMENTATION_JAVA_VERSION} -X ${VERSION_PKG}.autoInstrumentationPython=${AUTO_INSTRUMENTATION_PYTHON_VERSION} -X ${VERSION_PKG}.autoInstrumentationDotNet=${AUTO_INSTRUMENTATION_DOTNET_VERSION} -X ${VERSION_PKG}.autoInstrumentationNodeJS=${AUTO_INSTRUMENTATION_NODEJS_VERSION} -X ${VERSION_PKG}.dcgmExporter=${DCMG_EXPORTER_VERSION} -X ${VERSION_PKG}.neuronMonitor=${NEURON_MONITOR_VERSION} -X ${VERSION_PKG}.targetAllocator=${TARGET_ALLOCATOR_VERSION}" -a -o manager main.go # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details @@ -41,4 +42,4 @@ WORKDIR / COPY --from=builder /workspace/manager . USER 65532:65532 -ENTRYPOINT ["/manager"] +ENTRYPOINT ["/manager"] \ No newline at end of file diff --git a/Makefile b/Makefile index 4e3777b38..7532691c3 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,7 @@ AUTO_INSTRUMENTATION_DOTNET_VERSION ?= "$(shell grep -v '\#' versions.txt | grep AUTO_INSTRUMENTATION_NODEJS_VERSION ?= "$(shell grep -v '\#' versions.txt | grep aws-otel-nodejs-instrumentation | awk -F= '{print $$2}')" DCGM_EXPORTER_VERSION ?= "$(shell grep -v '\#' versions.txt | grep dcgm-exporter | awk -F= '{print $$2}')" NEURON_MONITOR_VERSION ?= "$(shell grep -v '\#' versions.txt | grep neuron-monitor | awk -F= '{print $$2}')" +TARGET_ALLOCATOR_VERSION ?= "$(shell grep -v '\#' versions.txt | grep target-allocator | awk -F= '{print $$2}')" # Image URL to use all building/pushing image targets IMG_PREFIX ?= aws @@ -16,6 +17,9 @@ IMG_REPO ?= cloudwatch-agent-operator IMG ?= ${IMG_PREFIX}/${IMG_REPO}:${VERSION} ARCH ?= $(shell go env GOARCH) +TARGET_ALLOCATOR_IMG_REPO ?= target-allocator +TARGET_ALLOCATOR_IMG ?= ${IMG_PREFIX}/${TARGET_ALLOCATOR_IMG_REPO}:${TARGET_ALLOCATOR_VERSION} + # Options for 'bundle-build' ifneq ($(origin CHANNELS), undefined) BUNDLE_CHANNELS := --channels=$(CHANNELS) @@ -96,6 +100,10 @@ test: generate fmt vet envtest .PHONY: manager manager: generate fmt vet go build -o bin/manager main.go +# Build target allocator binary +.PHONY: targetallocator +targetallocator: + cd cmd/amazon-cloudwatch-agent-target-allocator && CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(ARCH) go build -installsuffix cgo -o bin/targetallocator_${ARCH} -ldflags "${LDFLAGS}" . # Run against the configured Kubernetes cluster in ~/.kube/config .PHONY: run @@ -155,13 +163,26 @@ generate: controller-gen api-docs # buildx is used to ensure same results for arm based systems (m1/2 chips) .PHONY: container container: - docker buildx build --load --platform linux/${ARCH} -t ${IMG} --build-arg VERSION_PKG=${VERSION_PKG} --build-arg VERSION=${VERSION} --build-arg VERSION_DATE=${VERSION_DATE} --build-arg AGENT_VERSION=${AGENT_VERSION} --build-arg AUTO_INSTRUMENTATION_JAVA_VERSION=${AUTO_INSTRUMENTATION_JAVA_VERSION} --build-arg AUTO_INSTRUMENTATION_PYTHON_VERSION=${AUTO_INSTRUMENTATION_PYTHON_VERSION} --build-arg AUTO_INSTRUMENTATION_DOTNET_VERSION=${AUTO_INSTRUMENTATION_DOTNET_VERSION} --build-arg AUTO_INSTRUMENTATION_NODEJS_VERSION=${AUTO_INSTRUMENTATION_NODEJS_VERSION} --build-arg DCGM_EXPORTER_VERSION=${DCGM_EXPORTER_VERSION} --build-arg NEURON_MONITOR_VERSION=${NEURON_MONITOR_VERSION} . + docker buildx build --load --platform linux/${ARCH} -t ${IMG} --build-arg VERSION_PKG=${VERSION_PKG} --build-arg VERSION=${VERSION} --build-arg VERSION_DATE=${VERSION_DATE} --build-arg AGENT_VERSION=${AGENT_VERSION} --build-arg AUTO_INSTRUMENTATION_JAVA_VERSION=${AUTO_INSTRUMENTATION_JAVA_VERSION} --build-arg AUTO_INSTRUMENTATION_PYTHON_VERSION=${AUTO_INSTRUMENTATION_PYTHON_VERSION} --build-arg AUTO_INSTRUMENTATION_DOTNET_VERSION=${AUTO_INSTRUMENTATION_DOTNET_VERSION} --build-arg AUTO_INSTRUMENTATION_NODEJS_VERSION=${AUTO_INSTRUMENTATION_NODEJS_VERSION} --build-arg DCGM_EXPORTER_VERSION=${DCGM_EXPORTER_VERSION} --build-arg NEURON_MONITOR_VERSION=${NEURON_MONITOR_VERSION} --build-arg TARGET_ALLOCATOR_VERSION=${TARGET_ALLOCATOR_VERSION} . # Push the container image, used only for local dev purposes .PHONY: container-push container-push: docker push ${IMG} +.PHONY: container-target-allocator-push +container-target-allocator-push: + docker push ${TARGET_ALLOCATOR_IMG} + +.PHONY: container-target-allocator +container-target-allocator: GOOS = linux +container-target-allocator: targetallocator + docker buildx build --load --platform linux/${ARCH} -t ${TARGET_ALLOCATOR_IMG} cmd/amazon-cloudwatch-agent-target-allocator + +.PHONY: ta-build-and-push +ta-build-and-push: container-target-allocator +ta-build-and-push: container-target-allocator-push + .PHONY: kustomize kustomize: ## Download kustomize locally if necessary. $(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION)) diff --git a/apis/v1alpha1/allocation_strategy.go b/apis/v1alpha1/allocation_strategy.go new file mode 100644 index 000000000..f3e3be140 --- /dev/null +++ b/apis/v1alpha1/allocation_strategy.go @@ -0,0 +1,15 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +type ( + // AmazonCloudWatchAgentTargetAllocatorAllocationStrategy represent which strategy to distribute target to each collector + // +kubebuilder:validation:Enum=consistent-hashing + AmazonCloudWatchAgentTargetAllocatorAllocationStrategy string +) + +const ( + // AmazonCloudWatchAgentTargetAllocatorAllocationStrategyConsistentHashing targets will be consistently added to collectors, which allows a high-availability setup. + AmazonCloudWatchAgentTargetAllocatorAllocationStrategyConsistentHashing AmazonCloudWatchAgentTargetAllocatorAllocationStrategy = "consistent-hashing" +) diff --git a/apis/v1alpha1/amazoncloudwatchagent_types.go b/apis/v1alpha1/amazoncloudwatchagent_types.go index bc034966c..7e3cd06bc 100644 --- a/apis/v1alpha1/amazoncloudwatchagent_types.go +++ b/apis/v1alpha1/amazoncloudwatchagent_types.go @@ -143,6 +143,9 @@ type AmazonCloudWatchAgentSpec struct { // Collector and Target Allocator pods. // +optional PodAnnotations map[string]string `json:"podAnnotations,omitempty"` + // TargetAllocator indicates a value which determines whether to spawn a target allocation resource or not. + // +optional + TargetAllocator AmazonCloudWatchAgentTargetAllocator `json:"targetAllocator,omitempty"` // Mode represents how the collector should be deployed (deployment, daemonset, statefulset or sidecar) // +optional Mode Mode `json:"mode,omitempty"` @@ -164,6 +167,9 @@ type AmazonCloudWatchAgentSpec struct { // ImagePullPolicy indicates the pull policy to be used for retrieving the container image (Always, Never, IfNotPresent) // +optional ImagePullPolicy v1.PullPolicy `json:"imagePullPolicy,omitempty"` + // Prometheus is the raw YAML to be used as the collector's prometheus configuration. + // +optional + Prometheus PrometheusConfig `json:"prometheus,omitempty"` // Config is the raw JSON to be used as the collector's configuration. Refer to the OpenTelemetry Collector documentation for details. // +required Config string `json:"config,omitempty"` @@ -276,6 +282,87 @@ type AmazonCloudWatchAgentSpec struct { UpdateStrategy appsv1.DaemonSetUpdateStrategy `json:"updateStrategy,omitempty"` } +// AmazonCloudWatchAgentTargetAllocator defines the configurations for the Prometheus target allocator. +type AmazonCloudWatchAgentTargetAllocator struct { + // Replicas is the number of pod instances for the underlying TargetAllocator. This should only be set to a value + // other than 1 if a strategy that allows for high availability is chosen. Currently, the only allocation strategy + // that can be run in a high availability mode is consistent-hashing. + // +optional + Replicas *int32 `json:"replicas,omitempty"` + // NodeSelector to schedule OpenTelemetry TargetAllocator pods. + // +optional + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + // Resources to set on the OpenTelemetryTargetAllocator containers. + // +optional + Resources v1.ResourceRequirements `json:"resources,omitempty"` + // AllocationStrategy determines which strategy the target allocator should use for allocation. + // The current option is consistent-hashing. + // +optional + AllocationStrategy AmazonCloudWatchAgentTargetAllocatorAllocationStrategy `json:"allocationStrategy,omitempty"` + // FilterStrategy determines how to filter targets before allocating them among the collectors. + // The only current option is relabel-config (drops targets based on prom relabel_config). + // Filtering is disabled by default. + // +optional + FilterStrategy string `json:"filterStrategy,omitempty"` + // ServiceAccount indicates the name of an existing service account to use with this instance. When set, + // the operator will not automatically create a ServiceAccount for the TargetAllocator. + // +optional + ServiceAccount string `json:"serviceAccount,omitempty"` + // Image indicates the container image to use for the OpenTelemetry TargetAllocator. + // +optional + Image string `json:"image,omitempty"` + // Enabled indicates whether to use a target allocation mechanism for Prometheus targets or not. + // +optional + Enabled bool `json:"enabled,omitempty"` + // If specified, indicates the pod's scheduling constraints + // +optional + Affinity *v1.Affinity `json:"affinity,omitempty"` + // PrometheusCR defines the configuration for the retrieval of PrometheusOperator CRDs ( servicemonitor.monitoring.coreos.com/v1 and podmonitor.monitoring.coreos.com/v1 ) retrieval. + // All CR instances which the ServiceAccount has access to will be retrieved. This includes other namespaces. + // +optional + PrometheusCR AmazonCloudWatchAgentTargetAllocatorPrometheusCR `json:"prometheusCR,omitempty"` + // SecurityContext configures the container security context for + // the target-allocator. + // +optional + SecurityContext *v1.PodSecurityContext `json:"securityContext,omitempty"` + // TopologySpreadConstraints embedded kubernetes pod configuration option, + // controls how pods are spread across your cluster among failure-domains + // such as regions, zones, nodes, and other user-defined topology domains + // https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/ + // +optional + TopologySpreadConstraints []v1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"` + // Toleration embedded kubernetes pod configuration option, + // controls how pods can be scheduled with matching taints + // +optional + Tolerations []v1.Toleration `json:"tolerations,omitempty"` + // ENV vars to set on the OpenTelemetry TargetAllocator's Pods. These can then in certain cases be + // consumed in the config file for the TargetAllocator. + // +optional + Env []v1.EnvVar `json:"env,omitempty"` +} + +type AmazonCloudWatchAgentTargetAllocatorPrometheusCR struct { + // Enabled indicates whether to use a PrometheusOperator custom resources as targets or not. + // +optional + Enabled bool `json:"enabled,omitempty"` + // Interval between consecutive scrapes. Equivalent to the same setting on the Prometheus CRD. + // + // Default: "30s" + // +kubebuilder:default:="30s" + // +kubebuilder:validation:Format:=duration + ScrapeInterval *metav1.Duration `json:"scrapeInterval,omitempty"` + // PodMonitors to be selected for target discovery. + // This is a map of {key,value} pairs. Each {key,value} in the map is going to exactly match a label in a + // PodMonitor's meta labels. The requirements are ANDed. + // +optional + PodMonitorSelector map[string]string `json:"podMonitorSelector,omitempty"` + // ServiceMonitors to be selected for target discovery. + // This is a map of {key,value} pairs. Each {key,value} in the map is going to exactly match a label in a + // ServiceMonitor's meta labels. The requirements are ANDed. + // +optional + ServiceMonitorSelector map[string]string `json:"serviceMonitorSelector,omitempty"` +} + // ScaleSubresourceStatus defines the observed state of the AmazonCloudWatchAgent's // scale subresource. type ScaleSubresourceStatus struct { diff --git a/apis/v1alpha1/collector_webhook.go b/apis/v1alpha1/collector_webhook.go index 3ce5de731..b893ff8df 100644 --- a/apis/v1alpha1/collector_webhook.go +++ b/apis/v1alpha1/collector_webhook.go @@ -16,6 +16,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" "github.com/aws/amazon-cloudwatch-agent-operator/internal/config" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/collector/adapters" + ta "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/targetallocator/adapters" + "github.com/aws/amazon-cloudwatch-agent-operator/pkg/featuregate" ) var ( @@ -87,6 +90,9 @@ func (c CollectorWebhook) defaulter(r *AmazonCloudWatchAgent) error { if r.Spec.Replicas == nil { r.Spec.Replicas = &one } + if r.Spec.TargetAllocator.Enabled && r.Spec.TargetAllocator.Replicas == nil { + r.Spec.TargetAllocator.Replicas = &one + } if r.Spec.MaxReplicas != nil || (r.Spec.Autoscaler != nil && r.Spec.Autoscaler.MaxReplicas != nil) { if r.Spec.Autoscaler == nil { @@ -163,6 +169,32 @@ func (c CollectorWebhook) validate(r *AmazonCloudWatchAgent) (admission.Warnings return warnings, fmt.Errorf("the OpenTelemetry Collector mode is set to %s, which does not support the attribute 'AdditionalContainers'", r.Spec.Mode) } + // validate target allocation + if r.Spec.TargetAllocator.Enabled && r.Spec.Mode != ModeStatefulSet { + warnings = append(warnings, fmt.Sprintf("The Amazon CloudWatch Agent mode is set to %s, we do not recommend enabling Target Allocator when not running as a StatefulSet", r.Spec.Mode)) + } + + // validate Prometheus config for target allocation + if r.Spec.TargetAllocator.Enabled { + promConfigYaml, err := r.Spec.Prometheus.Yaml() + if err != nil { + return warnings, fmt.Errorf("%s could not convert json to yaml", err) + } + + promCfg, err := adapters.ConfigFromString(promConfigYaml) + if err != nil { + return warnings, fmt.Errorf("the OpenTelemetry Spec Prometheus configuration is incorrect, %w", err) + } + err = ta.ValidatePromConfig(promCfg, r.Spec.TargetAllocator.Enabled, featuregate.EnableTargetAllocatorRewrite.IsEnabled()) + if err != nil { + return warnings, fmt.Errorf("the OpenTelemetry Spec Prometheus configuration is incorrect, %w", err) + } + err = ta.ValidateTargetAllocatorConfig(r.Spec.TargetAllocator.PrometheusCR.Enabled, promCfg) + if err != nil { + return warnings, fmt.Errorf("the OpenTelemetry Spec Prometheus configuration is incorrect, %w", err) + } + } + // validator port config for _, p := range r.Spec.Ports { nameErrs := validation.IsValidPortName(p.Name) diff --git a/apis/v1alpha1/collector_webhook_test.go b/apis/v1alpha1/collector_webhook_test.go index 55dc99932..9f1a7fde0 100644 --- a/apis/v1alpha1/collector_webhook_test.go +++ b/apis/v1alpha1/collector_webhook_test.go @@ -11,6 +11,8 @@ import ( "github.com/go-logr/logr" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" v1 "k8s.io/api/core/v1" @@ -273,6 +275,7 @@ func TestOTELColDefaultingWebhook(t *testing.T) { scheme: testScheme, cfg: config.New( config.WithCollectorImage("collector:v0.0.0"), + config.WithTargetAllocatorImage("ta:v0.0.0"), ), } ctx := context.Background() @@ -283,6 +286,12 @@ func TestOTELColDefaultingWebhook(t *testing.T) { } } +var promCfgYaml = `config: + scrape_configs: + - job_name: otel-collector + scrape_interval: 10s +` + // TODO: a lot of these tests use .Spec.MaxReplicas and .Spec.MinReplicas. These fields are // deprecated and moved to .Spec.Autoscaler. Fine to use these fields to test that old CRD is // still supported but should eventually be updated. @@ -294,6 +303,10 @@ func TestOTELColValidatingWebhook(t *testing.T) { three := int32(3) five := int32(5) + promCfg := PrometheusConfig{} + err := yaml.Unmarshal([]byte(promCfgYaml), &promCfg) + require.NoError(t, err) + tests := []struct { //nolint:govet name string otelcol AmazonCloudWatchAgent @@ -313,21 +326,10 @@ func TestOTELColValidatingWebhook(t *testing.T) { Replicas: &three, MaxReplicas: &five, UpgradeStrategy: "adhoc", - Config: `receivers: - examplereceiver: - endpoint: "0.0.0.0:12345" - examplereceiver/settings: - endpoint: "0.0.0.0:12346" - prometheus: - config: - scrape_configs: - - job_name: otel-collector - scrape_interval: 10s - jaeger/custom: - protocols: - thrift_http: - endpoint: 0.0.0.0:15268 -`, + TargetAllocator: AmazonCloudWatchAgentTargetAllocator{ + Enabled: true, + }, + Prometheus: promCfg, Ports: []v1.ServicePort{ { Name: "port1", @@ -373,6 +375,18 @@ func TestOTELColValidatingWebhook(t *testing.T) { }, expectedErr: "does not support the attribute 'tolerations'", }, + { + name: "invalid target allocator config", + otelcol: AmazonCloudWatchAgent{ + Spec: AmazonCloudWatchAgentSpec{ + Mode: ModeStatefulSet, + TargetAllocator: AmazonCloudWatchAgentTargetAllocator{ + Enabled: true, + }, + }, + }, + expectedErr: "the OpenTelemetry Spec Prometheus configuration is incorrect", + }, { name: "invalid port name", otelcol: AmazonCloudWatchAgent{ @@ -755,6 +769,7 @@ func TestOTELColValidatingWebhook(t *testing.T) { scheme: testScheme, cfg: config.New( config.WithCollectorImage("collector:v0.0.0"), + config.WithTargetAllocatorImage("ta:v0.0.0"), ), } ctx := context.Background() diff --git a/apis/v1alpha1/prometheus_config.go b/apis/v1alpha1/prometheus_config.go new file mode 100644 index 000000000..8d5635546 --- /dev/null +++ b/apis/v1alpha1/prometheus_config.go @@ -0,0 +1,96 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + "bytes" + "encoding/json" + + "gopkg.in/yaml.v3" +) + +// AnyConfig represent parts of the config. +type AnyConfig struct { + Object map[string]interface{} `json:"-" yaml:",inline"` +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (ac *AnyConfig) DeepCopyInto(out *AnyConfig) { + *out = *ac + if ac.Object != nil { + in, out := &ac.Object, &out.Object + *out = make(map[string]interface{}, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnyConfig. +func (ac *AnyConfig) DeepCopy() *AnyConfig { + if ac == nil { + return nil + } + out := new(AnyConfig) + ac.DeepCopyInto(out) + return out +} + +var _ json.Marshaler = &AnyConfig{} +var _ json.Unmarshaler = &AnyConfig{} + +// UnmarshalJSON implements an alternative parser for this field. +func (ac *AnyConfig) UnmarshalJSON(b []byte) error { + vals := map[string]interface{}{} + if err := json.Unmarshal(b, &vals); err != nil { + return err + } + ac.Object = vals + return nil +} + +// MarshalJSON specifies how to convert this object into JSON. +func (ac *AnyConfig) MarshalJSON() ([]byte, error) { + if ac == nil { + return []byte("{}"), nil + } + return json.Marshal(ac.Object) +} + +// PrometheusConfig encapsulates prometheus config. +type PrometheusConfig struct { + // +kubebuilder:pruning:PreserveUnknownFields + Config *AnyConfig `json:"config,omitempty" yaml:"config,omitempty"` + // +kubebuilder:pruning:PreserveUnknownFields + TrimMetricSuffixes bool `json:"trim_metric_suffixes,omitempty" yaml:"trim_metric_suffixes,omitempty"` + // +kubebuilder:pruning:PreserveUnknownFields + UseStartTimeMetric bool `json:"use_start_time_metric,omitempty" yaml:"use_start_time_metric,omitempty"` + // +kubebuilder:pruning:PreserveUnknownFields + StartTimeMetricRegex string `json:"start_time_metric_regex,omitempty" yaml:"start_time_metric_regex,omitempty"` + // +kubebuilder:pruning:PreserveUnknownFields + ReportExtraScrapeMetrics bool `json:"report_extra_scrape_metrics,omitempty" yaml:"report_extra_scrape_metrics,omitempty"` + // +kubebuilder:pruning:PreserveUnknownFields + TargetAllocator *AnyConfig `json:"target_allocator,omitempty" yaml:"target_allocator,omitempty"` +} + +// Yaml encodes the current object and returns it as a string. +func (pc *PrometheusConfig) Yaml() (string, error) { + var buf bytes.Buffer + yamlEncoder := yaml.NewEncoder(&buf) + yamlEncoder.SetIndent(2) + if err := yamlEncoder.Encode(&pc); err != nil { + return "", err + } + return buf.String(), nil +} + +// IsEmpty checks if the prometheus config is empty. +func (pc *PrometheusConfig) IsEmpty() bool { + return pc.Config == nil && + !pc.TrimMetricSuffixes && + !pc.UseStartTimeMetric && + pc.StartTimeMetricRegex == "" && + !pc.ReportExtraScrapeMetrics && + pc.TargetAllocator == nil +} diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index f11b337ae..4d7355472 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -10,6 +10,7 @@ import ( v2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" v1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" ) @@ -133,6 +134,8 @@ func (in *AmazonCloudWatchAgentSpec) DeepCopyInto(out *AmazonCloudWatchAgentSpec (*out)[key] = val } } + in.TargetAllocator.DeepCopyInto(&out.TargetAllocator) + in.Prometheus.DeepCopyInto(&out.Prometheus) if in.VolumeMounts != nil { in, out := &in.VolumeMounts, &out.VolumeMounts *out = make([]corev1.VolumeMount, len(*in)) @@ -264,6 +267,100 @@ func (in *AmazonCloudWatchAgentStatus) DeepCopy() *AmazonCloudWatchAgentStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AmazonCloudWatchAgentTargetAllocator) DeepCopyInto(out *AmazonCloudWatchAgentTargetAllocator) { + *out = *in + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.Resources.DeepCopyInto(&out.Resources) + if in.Affinity != nil { + in, out := &in.Affinity, &out.Affinity + *out = new(corev1.Affinity) + (*in).DeepCopyInto(*out) + } + in.PrometheusCR.DeepCopyInto(&out.PrometheusCR) + if in.SecurityContext != nil { + in, out := &in.SecurityContext, &out.SecurityContext + *out = new(corev1.PodSecurityContext) + (*in).DeepCopyInto(*out) + } + if in.TopologySpreadConstraints != nil { + in, out := &in.TopologySpreadConstraints, &out.TopologySpreadConstraints + *out = make([]corev1.TopologySpreadConstraint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]corev1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]corev1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AmazonCloudWatchAgentTargetAllocator. +func (in *AmazonCloudWatchAgentTargetAllocator) DeepCopy() *AmazonCloudWatchAgentTargetAllocator { + if in == nil { + return nil + } + out := new(AmazonCloudWatchAgentTargetAllocator) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AmazonCloudWatchAgentTargetAllocatorPrometheusCR) DeepCopyInto(out *AmazonCloudWatchAgentTargetAllocatorPrometheusCR) { + *out = *in + if in.ScrapeInterval != nil { + in, out := &in.ScrapeInterval, &out.ScrapeInterval + *out = new(metav1.Duration) + **out = **in + } + if in.PodMonitorSelector != nil { + in, out := &in.PodMonitorSelector, &out.PodMonitorSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ServiceMonitorSelector != nil { + in, out := &in.ServiceMonitorSelector, &out.ServiceMonitorSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AmazonCloudWatchAgentTargetAllocatorPrometheusCR. +func (in *AmazonCloudWatchAgentTargetAllocatorPrometheusCR) DeepCopy() *AmazonCloudWatchAgentTargetAllocatorPrometheusCR { + if in == nil { + return nil + } + out := new(AmazonCloudWatchAgentTargetAllocatorPrometheusCR) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ApacheHttpd) DeepCopyInto(out *ApacheHttpd) { *out = *in @@ -1115,6 +1212,29 @@ func (in *Probe) DeepCopy() *Probe { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PrometheusConfig) DeepCopyInto(out *PrometheusConfig) { + *out = *in + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = (*in).DeepCopy() + } + if in.TargetAllocator != nil { + in, out := &in.TargetAllocator, &out.TargetAllocator + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrometheusConfig. +func (in *PrometheusConfig) DeepCopy() *PrometheusConfig { + if in == nil { + return nil + } + out := new(PrometheusConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Python) DeepCopyInto(out *Python) { *out = *in diff --git a/apis/v1alpha2/amazoncloudwatchagent_types.go b/apis/v1alpha2/amazoncloudwatchagent_types.go index 61edcfb55..267fa0cf8 100644 --- a/apis/v1alpha2/amazoncloudwatchagent_types.go +++ b/apis/v1alpha2/amazoncloudwatchagent_types.go @@ -68,6 +68,9 @@ type AmazonCloudWatchAgentSpec struct { // Collector and Target Allocator pods. // +optional PodAnnotations map[string]string `json:"podAnnotations,omitempty"` + // TargetAllocator indicates a value which determines whether to spawn a target allocation resource or not. + // +optional + TargetAllocator v1alpha1.AmazonCloudWatchAgentTargetAllocator `json:"targetAllocator,omitempty"` // Mode represents how the collector should be deployed (deployment, daemonset, statefulset or sidecar) // +optional Mode v1alpha1.Mode `json:"mode,omitempty"` @@ -89,6 +92,9 @@ type AmazonCloudWatchAgentSpec struct { // ImagePullPolicy indicates the pull policy to be used for retrieving the container image (Always, Never, IfNotPresent) // +optional ImagePullPolicy v1.PullPolicy `json:"imagePullPolicy,omitempty"` + // Prometheus is the raw YAML to be used as the collector's prometheus configuration. + // +optional + Prometheus v1alpha1.PrometheusConfig `json:"prometheus,omitempty"` // Config is the raw JSON to be used as the collector's configuration. Refer to the OpenTelemetry Collector documentation for details. // +required Config string `json:"config,omitempty"` diff --git a/apis/v1alpha2/zz_generated.deepcopy.go b/apis/v1alpha2/zz_generated.deepcopy.go index 2833a1325..06959cc6b 100644 --- a/apis/v1alpha2/zz_generated.deepcopy.go +++ b/apis/v1alpha2/zz_generated.deepcopy.go @@ -122,6 +122,8 @@ func (in *AmazonCloudWatchAgentSpec) DeepCopyInto(out *AmazonCloudWatchAgentSpec (*out)[key] = val } } + in.TargetAllocator.DeepCopyInto(&out.TargetAllocator) + in.Prometheus.DeepCopyInto(&out.Prometheus) if in.VolumeMounts != nil { in, out := &in.VolumeMounts, &out.VolumeMounts *out = make([]v1.VolumeMount, len(*in)) diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/Dockerfile b/cmd/amazon-cloudwatch-agent-target-allocator/Dockerfile new file mode 100644 index 000000000..1a2b9b7a2 --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/Dockerfile @@ -0,0 +1,19 @@ +# Get CA certificates from the Alpine package repo +FROM alpine:3.18 as certificates + +RUN apk --no-cache add ca-certificates + +# Start a new stage from scratch +FROM scratch + +ARG TARGETARCH + +WORKDIR /root/ + +# Copy the certs +COPY --from=certificates /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt + +# Copy binary built on the host +COPY bin/targetallocator_${TARGETARCH} ./main + +ENTRYPOINT ["./main"] diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/allocation/allocatortest.go b/cmd/amazon-cloudwatch-agent-target-allocator/allocation/allocatortest.go new file mode 100644 index 000000000..ad55f7e4e --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/allocation/allocatortest.go @@ -0,0 +1,60 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package allocation + +import ( + "fmt" + "strconv" + + "github.com/prometheus/common/model" + + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/target" +) + +func colIndex(index, numCols int) int { + if numCols == 0 { + return -1 + } + return index % numCols +} + +func MakeNNewTargets(n int, numCollectors int, startingIndex int) map[string]*target.Item { + toReturn := map[string]*target.Item{} + for i := startingIndex; i < n+startingIndex; i++ { + collector := fmt.Sprintf("collector-%d", colIndex(i, numCollectors)) + label := model.LabelSet{ + "collector": model.LabelValue(collector), + "i": model.LabelValue(strconv.Itoa(i)), + "total": model.LabelValue(strconv.Itoa(n + startingIndex)), + } + newTarget := target.NewItem(fmt.Sprintf("test-job-%d", i), fmt.Sprintf("test-url-%d", i), label, collector) + toReturn[newTarget.Hash()] = newTarget + } + return toReturn +} + +func MakeNCollectors(n int, startingIndex int) map[string]*Collector { + toReturn := map[string]*Collector{} + for i := startingIndex; i < n+startingIndex; i++ { + collector := fmt.Sprintf("collector-%d", i) + toReturn[collector] = &Collector{ + Name: collector, + NumTargets: 0, + } + } + return toReturn +} + +func MakeNNewTargetsWithEmptyCollectors(n int, startingIndex int) map[string]*target.Item { + toReturn := map[string]*target.Item{} + for i := startingIndex; i < n+startingIndex; i++ { + label := model.LabelSet{ + "i": model.LabelValue(strconv.Itoa(i)), + "total": model.LabelValue(strconv.Itoa(n + startingIndex)), + } + newTarget := target.NewItem(fmt.Sprintf("test-job-%d", i), fmt.Sprintf("test-url-%d", i), label, "") + toReturn[newTarget.Hash()] = newTarget + } + return toReturn +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/allocation/consistent_hashing.go b/cmd/amazon-cloudwatch-agent-target-allocator/allocation/consistent_hashing.go new file mode 100644 index 000000000..4bff39eee --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/allocation/consistent_hashing.go @@ -0,0 +1,284 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package allocation + +import ( + "strings" + "sync" + + "github.com/buraksezer/consistent" + "github.com/cespare/xxhash/v2" + "github.com/go-logr/logr" + "github.com/prometheus/client_golang/prometheus" + + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/diff" + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/target" +) + +var _ Allocator = &consistentHashingAllocator{} + +const consistentHashingStrategyName = "consistent-hashing" + +type hasher struct{} + +func (h hasher) Sum64(data []byte) uint64 { + return xxhash.Sum64(data) +} + +type consistentHashingAllocator struct { + // m protects consistentHasher, collectors and targetItems for concurrent use. + m sync.RWMutex + + consistentHasher *consistent.Consistent + + // collectors is a map from a Collector's name to a Collector instance + // collectorKey -> collector pointer + collectors map[string]*Collector + + // targetItems is a map from a target item's hash to the target items allocated state + // targetItem hash -> target item pointer + targetItems map[string]*target.Item + + // collectorKey -> job -> target item hash -> true + targetItemsPerJobPerCollector map[string]map[string]map[string]bool + + log logr.Logger + + filter Filter +} + +func newConsistentHashingAllocator(log logr.Logger, opts ...AllocationOption) Allocator { + config := consistent.Config{ + PartitionCount: 1061, + ReplicationFactor: 5, + Load: 1.1, + Hasher: hasher{}, + } + consistentHasher := consistent.New(nil, config) + chAllocator := &consistentHashingAllocator{ + consistentHasher: consistentHasher, + collectors: make(map[string]*Collector), + targetItems: make(map[string]*target.Item), + targetItemsPerJobPerCollector: make(map[string]map[string]map[string]bool), + log: log, + } + for _, opt := range opts { + opt(chAllocator) + } + + return chAllocator +} + +// SetFilter sets the filtering hook to use. +func (c *consistentHashingAllocator) SetFilter(filter Filter) { + c.filter = filter +} + +// addCollectorTargetItemMapping keeps track of which collector has which jobs and targets +// this allows the allocator to respond without any extra allocations to http calls. The caller of this method +// has to acquire a lock. +func (c *consistentHashingAllocator) addCollectorTargetItemMapping(tg *target.Item) { + if c.targetItemsPerJobPerCollector[tg.CollectorName] == nil { + c.targetItemsPerJobPerCollector[tg.CollectorName] = make(map[string]map[string]bool) + } + if c.targetItemsPerJobPerCollector[tg.CollectorName][tg.JobName] == nil { + c.targetItemsPerJobPerCollector[tg.CollectorName][tg.JobName] = make(map[string]bool) + } + c.targetItemsPerJobPerCollector[tg.CollectorName][tg.JobName][tg.Hash()] = true +} + +// addTargetToTargetItems assigns a target to the collector based on its hash and adds it to the allocator's targetItems +// This method is called from within SetTargets and SetCollectors, which acquire the needed lock. +// This is only called after the collectors are cleared or when a new target has been found in the tempTargetMap. +// INVARIANT: c.collectors must have at least 1 collector set. +// NOTE: by not creating a new target item, there is the potential for a race condition where we modify this target +// item while it's being encoded by the server JSON handler. +func (c *consistentHashingAllocator) addTargetToTargetItems(tg *target.Item) { + // Check if this is a reassignment, if so, decrement the previous collector's NumTargets + if previousColName, ok := c.collectors[tg.CollectorName]; ok { + previousColName.NumTargets-- + delete(c.targetItemsPerJobPerCollector[tg.CollectorName][tg.JobName], tg.Hash()) + TargetsPerCollector.WithLabelValues(previousColName.String(), consistentHashingStrategyName).Set(float64(c.collectors[previousColName.String()].NumTargets)) + } + colOwner := c.consistentHasher.LocateKey([]byte(strings.Join(tg.TargetURL, ""))) + tg.CollectorName = colOwner.String() + c.targetItems[tg.Hash()] = tg + c.addCollectorTargetItemMapping(tg) + c.collectors[colOwner.String()].NumTargets++ + TargetsPerCollector.WithLabelValues(colOwner.String(), consistentHashingStrategyName).Set(float64(c.collectors[colOwner.String()].NumTargets)) +} + +// handleTargets receives the new and removed targets and reconciles the current state. +// Any removals are removed from the allocator's targetItems and unassigned from the corresponding collector. +// Any net-new additions are assigned to the next available collector. +func (c *consistentHashingAllocator) handleTargets(diff diff.Changes[*target.Item]) { + // Check for removals + for k, item := range c.targetItems { + // if the current item is in the removals list + if _, ok := diff.Removals()[k]; ok { + col := c.collectors[item.CollectorName] + col.NumTargets-- + delete(c.targetItems, k) + delete(c.targetItemsPerJobPerCollector[item.CollectorName][item.JobName], item.Hash()) + TargetsPerCollector.WithLabelValues(item.CollectorName, consistentHashingStrategyName).Set(float64(col.NumTargets)) + } + } + + // Check for additions + for k, item := range diff.Additions() { + // Do nothing if the item is already there + if _, ok := c.targetItems[k]; ok { + continue + } else { + // Add item to item pool and assign a collector + c.addTargetToTargetItems(item) + } + } +} + +// handleCollectors receives the new and removed collectors and reconciles the current state. +// Any removals are removed from the allocator's collectors. New collectors are added to the allocator's collector map. +// Finally, update all targets' collectors to match the consistent hashing. +func (c *consistentHashingAllocator) handleCollectors(diff diff.Changes[*Collector]) { + // Clear removed collectors + for _, k := range diff.Removals() { + delete(c.collectors, k.Name) + delete(c.targetItemsPerJobPerCollector, k.Name) + c.consistentHasher.Remove(k.Name) + TargetsPerCollector.WithLabelValues(k.Name, consistentHashingStrategyName).Set(0) + } + // Insert the new collectors + for _, i := range diff.Additions() { + c.collectors[i.Name] = NewCollector(i.Name) + c.consistentHasher.Add(c.collectors[i.Name]) + } + + // Re-Allocate all targets + for _, item := range c.targetItems { + c.addTargetToTargetItems(item) + } +} + +// SetTargets accepts a list of targets that will be used to make +// load balancing decisions. This method should be called when there are +// new targets discovered or existing targets are shutdown. +func (c *consistentHashingAllocator) SetTargets(targets map[string]*target.Item) { + timer := prometheus.NewTimer(TimeToAssign.WithLabelValues("SetTargets", consistentHashingStrategyName)) + defer timer.ObserveDuration() + + if c.filter != nil { + targets = c.filter.Apply(targets) + } + RecordTargetsKept(targets) + + c.m.Lock() + defer c.m.Unlock() + + if len(c.collectors) == 0 { + c.log.Info("No collector instances present, saving targets to allocate to collector(s)") + // If there were no targets discovered previously, assign this as the new set of target items + if len(c.targetItems) == 0 { + c.log.Info("Not discovered any targets previously, saving targets found to the targetItems set") + for k, item := range targets { + c.targetItems[k] = item + } + } else { + // If there were previously discovered targets, add or remove accordingly + targetsDiffEmptyCollectorSet := diff.Maps(c.targetItems, targets) + + // Check for additions + if len(targetsDiffEmptyCollectorSet.Additions()) > 0 { + c.log.Info("New targets discovered, adding new targets to the targetItems set") + for k, item := range targetsDiffEmptyCollectorSet.Additions() { + // Do nothing if the item is already there + if _, ok := c.targetItems[k]; ok { + continue + } else { + // Add item to item pool + c.targetItems[k] = item + } + } + } + + // Check for deletions + if len(targetsDiffEmptyCollectorSet.Removals()) > 0 { + c.log.Info("Targets removed, Removing targets from the targetItems set") + for k := range targetsDiffEmptyCollectorSet.Removals() { + // Delete item from target items + delete(c.targetItems, k) + } + } + } + return + } + // Check for target changes + targetsDiff := diff.Maps(c.targetItems, targets) + // If there are any additions or removals + if len(targetsDiff.Additions()) != 0 || len(targetsDiff.Removals()) != 0 { + c.handleTargets(targetsDiff) + } +} + +// SetCollectors sets the set of collectors with key=collectorName, value=Collector object. +// This method is called when Collectors are added or removed. +func (c *consistentHashingAllocator) SetCollectors(collectors map[string]*Collector) { + timer := prometheus.NewTimer(TimeToAssign.WithLabelValues("SetCollectors", consistentHashingStrategyName)) + defer timer.ObserveDuration() + + CollectorsAllocatable.WithLabelValues(consistentHashingStrategyName).Set(float64(len(collectors))) + if len(collectors) == 0 { + c.log.Info("No collector instances present") + return + } + + c.m.Lock() + defer c.m.Unlock() + + // Check for collector changes + collectorsDiff := diff.Maps(c.collectors, collectors) + if len(collectorsDiff.Additions()) != 0 || len(collectorsDiff.Removals()) != 0 { + c.handleCollectors(collectorsDiff) + } + c.log.Info("Setting collector completed") +} + +func (c *consistentHashingAllocator) GetTargetsForCollectorAndJob(collector string, job string) []*target.Item { + c.m.RLock() + defer c.m.RUnlock() + if _, ok := c.targetItemsPerJobPerCollector[collector]; !ok { + return []*target.Item{} + } + if _, ok := c.targetItemsPerJobPerCollector[collector][job]; !ok { + return []*target.Item{} + } + targetItemsCopy := make([]*target.Item, len(c.targetItemsPerJobPerCollector[collector][job])) + index := 0 + for targetHash := range c.targetItemsPerJobPerCollector[collector][job] { + targetItemsCopy[index] = c.targetItems[targetHash] + index++ + } + return targetItemsCopy +} + +// TargetItems returns a shallow copy of the targetItems map. +func (c *consistentHashingAllocator) TargetItems() map[string]*target.Item { + c.m.RLock() + defer c.m.RUnlock() + targetItemsCopy := make(map[string]*target.Item) + for k, v := range c.targetItems { + targetItemsCopy[k] = v + } + return targetItemsCopy +} + +// Collectors returns a shallow copy of the collectors map. +func (c *consistentHashingAllocator) Collectors() map[string]*Collector { + c.m.RLock() + defer c.m.RUnlock() + collectorsCopy := make(map[string]*Collector) + for k, v := range c.collectors { + collectorsCopy[k] = v + } + return collectorsCopy +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/allocation/consistent_hashing_test.go b/cmd/amazon-cloudwatch-agent-target-allocator/allocation/consistent_hashing_test.go new file mode 100644 index 000000000..8070c0ed2 --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/allocation/consistent_hashing_test.go @@ -0,0 +1,136 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package allocation + +import ( + "testing" + + "github.com/stretchr/testify/assert" + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +var logger = logf.Log.WithName("unit-tests") + +func TestCanSetSingleTarget(t *testing.T) { + cols := MakeNCollectors(3, 0) + c := newConsistentHashingAllocator(logger) + c.SetCollectors(cols) + c.SetTargets(MakeNNewTargets(1, 3, 0)) + actualTargetItems := c.TargetItems() + assert.Len(t, actualTargetItems, 1) + for _, item := range actualTargetItems { + assert.Equal(t, "collector-0", item.CollectorName) + } +} + +func TestRelativelyEvenDistribution(t *testing.T) { + numCols := 15 + numItems := 10000 + cols := MakeNCollectors(numCols, 0) + var expectedPerCollector = float64(numItems / numCols) + expectedDelta := (expectedPerCollector * 1.5) - expectedPerCollector + c := newConsistentHashingAllocator(logger) + c.SetCollectors(cols) + c.SetTargets(MakeNNewTargets(numItems, 0, 0)) + actualTargetItems := c.TargetItems() + assert.Len(t, actualTargetItems, numItems) + actualCollectors := c.Collectors() + assert.Len(t, actualCollectors, numCols) + for _, col := range actualCollectors { + assert.InDelta(t, col.NumTargets, expectedPerCollector, expectedDelta) + } +} + +func TestFullReallocation(t *testing.T) { + cols := MakeNCollectors(10, 0) + c := newConsistentHashingAllocator(logger) + c.SetCollectors(cols) + c.SetTargets(MakeNNewTargets(10000, 10, 0)) + actualTargetItems := c.TargetItems() + assert.Len(t, actualTargetItems, 10000) + actualCollectors := c.Collectors() + assert.Len(t, actualCollectors, 10) + newCols := MakeNCollectors(10, 10) + c.SetCollectors(newCols) + updatedTargetItems := c.TargetItems() + assert.Len(t, updatedTargetItems, 10000) + updatedCollectors := c.Collectors() + assert.Len(t, updatedCollectors, 10) + for _, item := range updatedTargetItems { + _, ok := updatedCollectors[item.CollectorName] + assert.True(t, ok, "Some items weren't reallocated correctly") + } +} + +func TestNumRemapped(t *testing.T) { + numItems := 10_000 + numInitialCols := 15 + numFinalCols := 16 + expectedDelta := float64((numFinalCols - numInitialCols) * (numItems / numFinalCols)) + cols := MakeNCollectors(numInitialCols, 0) + c := newConsistentHashingAllocator(logger) + c.SetCollectors(cols) + c.SetTargets(MakeNNewTargets(numItems, numInitialCols, 0)) + actualTargetItems := c.TargetItems() + assert.Len(t, actualTargetItems, numItems) + actualCollectors := c.Collectors() + assert.Len(t, actualCollectors, numInitialCols) + newCols := MakeNCollectors(numFinalCols, 0) + c.SetCollectors(newCols) + updatedTargetItems := c.TargetItems() + assert.Len(t, updatedTargetItems, numItems) + updatedCollectors := c.Collectors() + assert.Len(t, updatedCollectors, numFinalCols) + countRemapped := 0 + countNotRemapped := 0 + for _, item := range updatedTargetItems { + previousItem, ok := actualTargetItems[item.Hash()] + assert.True(t, ok) + if previousItem.CollectorName != item.CollectorName { + countRemapped++ + } else { + countNotRemapped++ + } + } + assert.InDelta(t, numItems/numFinalCols, countRemapped, expectedDelta) +} + +func TestTargetsWithNoCollectorsConsistentHashing(t *testing.T) { + + c := newConsistentHashingAllocator(logger) + + // Adding 10 new targets + numItems := 10 + c.SetTargets(MakeNNewTargetsWithEmptyCollectors(numItems, 0)) + actualTargetItems := c.TargetItems() + assert.Len(t, actualTargetItems, numItems) + + // Adding 5 new targets, and removing the old 10 targets + numItemsUpdate := 5 + c.SetTargets(MakeNNewTargetsWithEmptyCollectors(numItemsUpdate, 10)) + actualTargetItemsUpdated := c.TargetItems() + assert.Len(t, actualTargetItemsUpdated, numItemsUpdate) + + // Adding 5 new targets, and one existing target + numItemsUpdate = 6 + c.SetTargets(MakeNNewTargetsWithEmptyCollectors(numItemsUpdate, 14)) + actualTargetItemsUpdated = c.TargetItems() + assert.Len(t, actualTargetItemsUpdated, numItemsUpdate) + + // Adding collectors to test allocation + numCols := 2 + cols := MakeNCollectors(2, 0) + c.SetCollectors(cols) + var expectedPerCollector = float64(numItemsUpdate / numCols) + expectedDelta := (expectedPerCollector * 1.5) - expectedPerCollector + // Checking to see that there is no change to number of targets + actualTargetItems = c.TargetItems() + assert.Len(t, actualTargetItems, numItemsUpdate) + // Checking to see collectors are added correctly + actualCollectors := c.Collectors() + assert.Len(t, actualCollectors, numCols) + for _, col := range actualCollectors { + assert.InDelta(t, col.NumTargets, expectedPerCollector, expectedDelta) + } +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/allocation/strategy.go b/cmd/amazon-cloudwatch-agent-target-allocator/allocation/strategy.go new file mode 100644 index 000000000..5f5285ece --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/allocation/strategy.go @@ -0,0 +1,118 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package allocation + +import ( + "errors" + "fmt" + + "github.com/buraksezer/consistent" + "github.com/go-logr/logr" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/target" +) + +type AllocatorProvider func(log logr.Logger, opts ...AllocationOption) Allocator + +var ( + registry = map[string]AllocatorProvider{} + + // TargetsPerCollector records how many targets have been assigned to each collector. + // It is currently the responsibility of the strategy to track this information. + TargetsPerCollector = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "cloudwatch_agent_allocator_targets_per_collector", + Help: "The number of targets for each collector.", + }, []string{"collector_name", "strategy"}) + CollectorsAllocatable = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "cloudwatch_agent_allocator_collectors_allocatable", + Help: "Number of collectors the allocator is able to allocate to.", + }, []string{"strategy"}) + TimeToAssign = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "cloudwatch_agent_allocator_time_to_allocate", + Help: "The time it takes to allocate", + }, []string{"method", "strategy"}) + targetsRemaining = promauto.NewCounter(prometheus.CounterOpts{ + Name: "cloudwatch_agent_allocator_targets_remaining", + Help: "Number of targets kept after filtering.", + }) +) + +type AllocationOption func(Allocator) + +type Filter interface { + Apply(map[string]*target.Item) map[string]*target.Item +} + +func WithFilter(filter Filter) AllocationOption { + return func(allocator Allocator) { + allocator.SetFilter(filter) + } +} + +func RecordTargetsKept(targets map[string]*target.Item) { + targetsRemaining.Add(float64(len(targets))) +} + +func New(name string, log logr.Logger, opts ...AllocationOption) (Allocator, error) { + if p, ok := registry[name]; ok { + return p(log.WithValues("allocator", name), opts...), nil + } + return nil, fmt.Errorf("unregistered strategy: %s", name) +} + +func Register(name string, provider AllocatorProvider) error { + if _, ok := registry[name]; ok { + return errors.New("already registered") + } + registry[name] = provider + return nil +} + +func GetRegisteredAllocatorNames() []string { + var names []string + for s := range registry { + names = append(names, s) + } + return names +} + +type Allocator interface { + SetCollectors(collectors map[string]*Collector) + SetTargets(targets map[string]*target.Item) + TargetItems() map[string]*target.Item + Collectors() map[string]*Collector + GetTargetsForCollectorAndJob(collector string, job string) []*target.Item + SetFilter(filter Filter) +} + +var _ consistent.Member = Collector{} + +// Collector Creates a struct that holds Collector information. +// This struct will be parsed into endpoint with Collector and jobs info. +// This struct can be extended with information like annotations and labels in the future. +type Collector struct { + Name string + NumTargets int +} + +func (c Collector) Hash() string { + return c.Name +} + +func (c Collector) String() string { + return c.Name +} + +func NewCollector(name string) *Collector { + return &Collector{Name: name} +} + +func init() { + err := Register(consistentHashingStrategyName, newConsistentHashingAllocator) + if err != nil { + panic(err) + } +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/allocation/strategy_test.go b/cmd/amazon-cloudwatch-agent-target-allocator/allocation/strategy_test.go new file mode 100644 index 000000000..0437ed021 --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/allocation/strategy_test.go @@ -0,0 +1,125 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package allocation + +import ( + "fmt" + "reflect" + "testing" + + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/diff" +) + +func BenchmarkGetAllTargetsByCollectorAndJob(b *testing.B) { + var table = []struct { + numCollectors int + numJobs int + }{ + {numCollectors: 100, numJobs: 100}, + {numCollectors: 100, numJobs: 1000}, + {numCollectors: 100, numJobs: 10000}, + {numCollectors: 100, numJobs: 100000}, + {numCollectors: 1000, numJobs: 100}, + {numCollectors: 1000, numJobs: 1000}, + {numCollectors: 1000, numJobs: 10000}, + {numCollectors: 1000, numJobs: 100000}, + } + for _, s := range GetRegisteredAllocatorNames() { + for _, v := range table { + a, err := New(s, logger) + if err != nil { + b.Log(err) + b.Fail() + } + cols := MakeNCollectors(v.numCollectors, 0) + jobs := MakeNNewTargets(v.numJobs, v.numCollectors, 0) + a.SetCollectors(cols) + a.SetTargets(jobs) + b.Run(fmt.Sprintf("%s_num_cols_%d_num_jobs_%d", s, v.numCollectors, v.numJobs), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + a.GetTargetsForCollectorAndJob(fmt.Sprintf("collector-%d", v.numCollectors/2), fmt.Sprintf("test-job-%d", v.numJobs/2)) + } + }) + } + } +} + +func Benchmark_Setting(b *testing.B) { + var table = []struct { + numCollectors int + numTargets int + }{ + {numCollectors: 100, numTargets: 100}, + {numCollectors: 100, numTargets: 1000}, + {numCollectors: 100, numTargets: 10000}, + {numCollectors: 100, numTargets: 100000}, + {numCollectors: 1000, numTargets: 100}, + {numCollectors: 1000, numTargets: 1000}, + {numCollectors: 1000, numTargets: 10000}, + {numCollectors: 1000, numTargets: 100000}, + } + + for _, s := range GetRegisteredAllocatorNames() { + for _, v := range table { + a, _ := New(s, logger) + cols := MakeNCollectors(v.numCollectors, 0) + targets := MakeNNewTargets(v.numTargets, v.numCollectors, 0) + b.Run(fmt.Sprintf("%s_num_cols_%d_num_jobs_%d", s, v.numCollectors, v.numTargets), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + a.SetCollectors(cols) + a.SetTargets(targets) + } + }) + } + } +} + +func TestCollectorDiff(t *testing.T) { + collector0 := NewCollector("collector-0") + collector1 := NewCollector("collector-1") + collector2 := NewCollector("collector-2") + collector3 := NewCollector("collector-3") + collector4 := NewCollector("collector-4") + type args struct { + current map[string]*Collector + new map[string]*Collector + } + tests := []struct { + name string + args args + want diff.Changes[*Collector] + }{ + { + name: "diff two collector maps", + args: args{ + current: map[string]*Collector{ + "collector-0": collector0, + "collector-1": collector1, + "collector-2": collector2, + "collector-3": collector3, + }, + new: map[string]*Collector{ + "collector-0": collector0, + "collector-1": collector1, + "collector-2": collector2, + "collector-4": collector4, + }, + }, + want: diff.NewChanges(map[string]*Collector{ + "collector-4": collector4, + }, map[string]*Collector{ + "collector-3": collector3, + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := diff.Maps(tt.args.current, tt.args.new); !reflect.DeepEqual(got, tt.want) { + t.Errorf("DiffMaps() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/collector/collector.go b/cmd/amazon-cloudwatch-agent-target-allocator/collector/collector.go new file mode 100644 index 000000000..60bda6bca --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/collector/collector.go @@ -0,0 +1,132 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package collector + +import ( + "context" + "os" + "time" + + "github.com/go-logr/logr" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/allocation" +) + +const ( + watcherTimeout = 15 * time.Minute +) + +var ( + ns = os.Getenv("OTELCOL_NAMESPACE") + collectorsDiscovered = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "amazon_cloudwatch_agent_allocator_collectors_discovered", + Help: "Number of collectors discovered.", + }) +) + +type Client struct { + log logr.Logger + k8sClient kubernetes.Interface + close chan struct{} +} + +func NewClient(logger logr.Logger, kubeConfig *rest.Config) (*Client, error) { + clientset, err := kubernetes.NewForConfig(kubeConfig) + if err != nil { + return &Client{}, err + } + + return &Client{ + log: logger.WithValues("component", "amazon-cloudwatch-agent-target-allocator"), + k8sClient: clientset, + close: make(chan struct{}), + }, nil +} + +func (k *Client) Watch(ctx context.Context, labelMap map[string]string, fn func(collectors map[string]*allocation.Collector)) error { + collectorMap := map[string]*allocation.Collector{} + + opts := metav1.ListOptions{ + LabelSelector: labels.SelectorFromSet(labelMap).String(), + } + pods, err := k.k8sClient.CoreV1().Pods(ns).List(ctx, opts) + if err != nil { + k.log.Error(err, "Pod failure") + os.Exit(1) + } + for i := range pods.Items { + pod := pods.Items[i] + if pod.GetObjectMeta().GetDeletionTimestamp() == nil { + collectorMap[pod.Name] = allocation.NewCollector(pod.Name) + } + } + fn(collectorMap) + + for { + if !k.restartWatch(ctx, opts, collectorMap, fn) { + return nil + } + } +} + +func (k *Client) restartWatch(ctx context.Context, opts metav1.ListOptions, collectorMap map[string]*allocation.Collector, fn func(collectors map[string]*allocation.Collector)) bool { + // add timeout to the context before calling Watch + ctx, cancel := context.WithTimeout(ctx, watcherTimeout) + defer cancel() + watcher, err := k.k8sClient.CoreV1().Pods(ns).Watch(ctx, opts) + if err != nil { + k.log.Error(err, "unable to create collector pod watcher") + return false + } + k.log.Info("Successfully started a collector pod watcher") + if msg := runWatch(ctx, k, watcher.ResultChan(), collectorMap, fn); msg != "" { + k.log.Info("Collector pod watch event stopped " + msg) + return false + } + + return true +} + +func runWatch(ctx context.Context, k *Client, c <-chan watch.Event, collectorMap map[string]*allocation.Collector, fn func(collectors map[string]*allocation.Collector)) string { + for { + collectorsDiscovered.Set(float64(len(collectorMap))) + select { + case <-k.close: + return "kubernetes client closed" + case <-ctx.Done(): + return "" // this means that the watcher most likely timed out + case event, ok := <-c: + if !ok { + k.log.Info("No event found. Restarting watch routine") + return "" + } + + pod, ok := event.Object.(*v1.Pod) + if !ok { + k.log.Info("No pod found in event Object. Restarting watch routine") + return "" + } + + switch event.Type { //nolint:exhaustive + case watch.Added: + collectorMap[pod.Name] = allocation.NewCollector(pod.Name) + case watch.Deleted: + delete(collectorMap, pod.Name) + } + fn(collectorMap) + } + } +} + +func (k *Client) Close() { + close(k.close) +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/collector/collector_test.go b/cmd/amazon-cloudwatch-agent-target-allocator/collector/collector_test.go new file mode 100644 index 000000000..9268ea38d --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/collector/collector_test.go @@ -0,0 +1,209 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package collector + +import ( + "context" + "fmt" + "os" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/kubernetes/fake" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/allocation" +) + +var logger = logf.Log.WithName("collector-unit-tests") + +func getTestClient() (Client, watch.Interface) { + kubeClient := Client{ + k8sClient: fake.NewSimpleClientset(), + close: make(chan struct{}), + log: logger, + } + + labelMap := map[string]string{ + "app.kubernetes.io/instance": "default.test", + "app.kubernetes.io/managed-by": "amazon-cloudwatch-agent-operator", + } + + opts := metav1.ListOptions{ + LabelSelector: labels.SelectorFromSet(labelMap).String(), + } + watcher, err := kubeClient.k8sClient.CoreV1().Pods("test-ns").Watch(context.Background(), opts) + if err != nil { + fmt.Printf("failed to setup a Collector Pod watcher: %v", err) + os.Exit(1) + } + return kubeClient, watcher +} + +func pod(name string) *v1.Pod { + labelSet := make(map[string]string) + labelSet["app.kubernetes.io/instance"] = "default.test" + labelSet["app.kubernetes.io/managed-by"] = "amazon-cloudwatch-agent-operator" + + return &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "test-ns", + Labels: labelSet, + }, + } +} + +func Test_runWatch(t *testing.T) { + type args struct { + kubeFn func(t *testing.T, client Client, group *sync.WaitGroup) + collectorMap map[string]*allocation.Collector + } + tests := []struct { + name string + args args + want map[string]*allocation.Collector + }{ + { + name: "pod add", + args: args{ + kubeFn: func(t *testing.T, client Client, group *sync.WaitGroup) { + for _, k := range []string{"test-pod1", "test-pod2", "test-pod3"} { + p := pod(k) + group.Add(1) + _, err := client.k8sClient.CoreV1().Pods("test-ns").Create(context.Background(), p, metav1.CreateOptions{}) + assert.NoError(t, err) + } + }, + collectorMap: map[string]*allocation.Collector{}, + }, + want: map[string]*allocation.Collector{ + "test-pod1": { + Name: "test-pod1", + }, + "test-pod2": { + Name: "test-pod2", + }, + "test-pod3": { + Name: "test-pod3", + }, + }, + }, + { + name: "pod delete", + args: args{ + kubeFn: func(t *testing.T, client Client, group *sync.WaitGroup) { + for _, k := range []string{"test-pod2", "test-pod3"} { + group.Add(1) + err := client.k8sClient.CoreV1().Pods("test-ns").Delete(context.Background(), k, metav1.DeleteOptions{}) + assert.NoError(t, err) + } + }, + collectorMap: map[string]*allocation.Collector{ + "test-pod1": { + Name: "test-pod1", + }, + "test-pod2": { + Name: "test-pod2", + }, + "test-pod3": { + Name: "test-pod3", + }, + }, + }, + want: map[string]*allocation.Collector{ + "test-pod1": { + Name: "test-pod1", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + kubeClient, watcher := getTestClient() + defer func() { + close(kubeClient.close) + watcher.Stop() + }() + var wg sync.WaitGroup + actual := make(map[string]*allocation.Collector) + for _, k := range tt.args.collectorMap { + p := pod(k.Name) + _, err := kubeClient.k8sClient.CoreV1().Pods("test-ns").Create(context.Background(), p, metav1.CreateOptions{}) + wg.Add(1) + assert.NoError(t, err) + } + go runWatch(context.Background(), &kubeClient, watcher.ResultChan(), map[string]*allocation.Collector{}, func(colMap map[string]*allocation.Collector) { + actual = colMap + wg.Done() + }) + + tt.args.kubeFn(t, kubeClient, &wg) + wg.Wait() + + assert.Len(t, actual, len(tt.want)) + assert.Equal(t, actual, tt.want) + }) + } +} + +// this tests runWatch in the case of watcher channel closing and watcher timing out. +func Test_closeChannel(t *testing.T) { + tests := []struct { + description string + isCloseChannel bool + timeoutSeconds time.Duration + }{ + { + // event is triggered by channel closing. + description: "close_channel", + isCloseChannel: true, + // channel should be closed before this timeout occurs + timeoutSeconds: 10 * time.Second, + }, + { + // event triggered by timeout. + description: "watcher_timeout", + isCloseChannel: false, + timeoutSeconds: 0 * time.Second, + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + kubeClient, watcher := getTestClient() + + defer func() { + close(kubeClient.close) + watcher.Stop() + }() + var wg sync.WaitGroup + wg.Add(1) + terminated := false + + go func(watcher watch.Interface) { + defer wg.Done() + ctx, cancel := context.WithTimeout(context.Background(), tc.timeoutSeconds) + defer cancel() + if msg := runWatch(ctx, &kubeClient, watcher.ResultChan(), map[string]*allocation.Collector{}, func(colMap map[string]*allocation.Collector) {}); msg != "" { + terminated = true + return + } + }(watcher) + + if tc.isCloseChannel { + // stop pod watcher to trigger event. + watcher.Stop() + } + wg.Wait() + assert.False(t, terminated) + }) + } +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/config/config.go b/cmd/amazon-cloudwatch-agent-target-allocator/config/config.go new file mode 100644 index 000000000..265929464 --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/config/config.go @@ -0,0 +1,248 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + "io/fs" + "os" + "time" + + "sigs.k8s.io/controller-runtime/pkg/certwatcher" + + "github.com/go-logr/logr" + "github.com/prometheus/common/model" + promconfig "github.com/prometheus/prometheus/config" + _ "github.com/prometheus/prometheus/discovery/install" + "github.com/spf13/pflag" + "gopkg.in/yaml.v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + tamanifest "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/targetallocator" +) + +const ( + DefaultResyncTime = 5 * time.Minute + DefaultConfigFilePath string = "/conf/targetallocator.yaml" + DefaultCRScrapeInterval model.Duration = model.Duration(time.Second * 30) + DefaultAllocationStrategy = "consistent-hashing" + DefaultFilterStrategy = "relabel-config" + DefaultListenAddr = ":8443" + DefaultCertMountPath = tamanifest.TACertMountPath + DefaultTLSKeyPath = DefaultCertMountPath + "/server.key" + DefaultTLSCertPath = DefaultCertMountPath + "/server.crt" + DefaultCABundlePath = "" +) + +type Config struct { + ListenAddr string `yaml:"listen_addr,omitempty"` + KubeConfigFilePath string `yaml:"kube_config_file_path,omitempty"` + ClusterConfig *rest.Config `yaml:"-"` + RootLogger logr.Logger `yaml:"-"` + ReloadConfig bool `yaml:"-"` + LabelSelector map[string]string `yaml:"label_selector,omitempty"` + PromConfig *promconfig.Config `yaml:"config"` + AllocationStrategy *string `yaml:"allocation_strategy,omitempty"` + FilterStrategy *string `yaml:"filter_strategy,omitempty"` + PrometheusCR PrometheusCRConfig `yaml:"prometheus_cr,omitempty"` + PodMonitorSelector map[string]string `yaml:"pod_monitor_selector,omitempty"` + ServiceMonitorSelector map[string]string `yaml:"service_monitor_selector,omitempty"` + CollectorSelector *metav1.LabelSelector `yaml:"collector_selector,omitempty"` + HTTPS HTTPSServerConfig `yaml:"https,omitempty"` +} + +type PrometheusCRConfig struct { + Enabled bool `yaml:"enabled,omitempty"` + ScrapeInterval model.Duration `yaml:"scrape_interval,omitempty"` +} + +type HTTPSServerConfig struct { + Enabled bool `yaml:"enabled,omitempty"` + ListenAddr string `yaml:"listen_addr,omitempty"` + CAFilePath string `yaml:"ca_file_path,omitempty"` + TLSCertFilePath string `yaml:"tls_cert_file_path,omitempty"` + TLSKeyFilePath string `yaml:"tls_key_file_path,omitempty"` +} + +func (c Config) GetAllocationStrategy() string { + if c.AllocationStrategy != nil { + return *c.AllocationStrategy + } + return DefaultAllocationStrategy +} + +func (c Config) GetTargetsFilterStrategy() string { + if c.FilterStrategy != nil { + return *c.FilterStrategy + } + return "" +} + +func LoadFromFile(file string, target *Config) error { + return unmarshal(target, file) +} + +func LoadFromCLI(target *Config, flagSet *pflag.FlagSet) error { + var err error + // set the rest of the config attributes based on command-line flag values + target.RootLogger = zap.New(zap.UseFlagOptions(&zapCmdLineOpts)) + klog.SetLogger(target.RootLogger) + ctrl.SetLogger(target.RootLogger) + + target.KubeConfigFilePath, err = getKubeConfigFilePath(flagSet) + if err != nil { + return err + } + clusterConfig, err := clientcmd.BuildConfigFromFlags("", target.KubeConfigFilePath) + if err != nil { + pathError := &fs.PathError{} + if ok := errors.As(err, &pathError); !ok { + return err + } + clusterConfig, err = rest.InClusterConfig() + if err != nil { + return err + } + target.KubeConfigFilePath = "" + } + target.ClusterConfig = clusterConfig + + target.ReloadConfig, err = getConfigReloadEnabled(flagSet) + if err != nil { + return err + } + + target.HTTPS.Enabled, err = getHttpsEnabled(flagSet) + if err != nil { + return err + } + + target.HTTPS.ListenAddr, err = getHttpsListenAddr(flagSet) + if err != nil { + return err + } + + target.HTTPS.CAFilePath, err = getHttpsCAFilePath(flagSet) + if err != nil { + return err + } + + target.HTTPS.TLSCertFilePath, err = getHttpsTLSCertFilePath(flagSet) + if err != nil { + return err + } + + target.HTTPS.TLSKeyFilePath, err = getHttpsTLSKeyFilePath(flagSet) + if err != nil { + return err + } + + return nil +} + +func unmarshal(cfg *Config, configFile string) error { + + yamlFile, err := os.ReadFile(configFile) + if err != nil { + return err + } + if err = yaml.UnmarshalStrict(yamlFile, cfg); err != nil { + return fmt.Errorf("error unmarshaling YAML: %w", err) + } + return nil +} + +func CreateDefaultConfig() Config { + var allocation_strategy = DefaultAllocationStrategy + return Config{ + PrometheusCR: PrometheusCRConfig{ + ScrapeInterval: DefaultCRScrapeInterval, + }, + AllocationStrategy: &allocation_strategy, + HTTPS: HTTPSServerConfig{ + Enabled: true, + ListenAddr: DefaultListenAddr, + CAFilePath: DefaultCABundlePath, + TLSCertFilePath: DefaultTLSCertPath, + TLSKeyFilePath: DefaultTLSKeyPath, + }, + } +} + +func Load() (*Config, string, error) { + var err error + + flagSet := getFlagSet(pflag.ExitOnError) + err = flagSet.Parse(os.Args) + if err != nil { + return nil, "", err + } + + config := CreateDefaultConfig() + + // load the config from the config file + configFilePath, err := getConfigFilePath(flagSet) + if err != nil { + return nil, "", err + } + err = LoadFromFile(configFilePath, &config) + if err != nil { + return nil, "", err + } + + err = LoadFromCLI(&config, flagSet) + if err != nil { + return nil, "", err + } + + return &config, configFilePath, nil +} + +// ValidateConfig validates the cli and file configs together. +func ValidateConfig(config *Config) error { + scrapeConfigsPresent := config.PromConfig != nil && len(config.PromConfig.ScrapeConfigs) > 0 + if !(config.PrometheusCR.Enabled || scrapeConfigsPresent) { + return fmt.Errorf("at least one scrape config must be defined, or Prometheus CR watching must be enabled") + } + return nil +} + +func (c HTTPSServerConfig) NewTLSConfig(ctx context.Context) (*tls.Config, error) { + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS13, + } + + certWatcher, err := certwatcher.New(c.TLSCertFilePath, c.TLSKeyFilePath) + if err != nil { + return nil, err + } + tlsConfig.GetCertificate = certWatcher.GetCertificate + go func() { + _ = certWatcher.Start(ctx) + }() + + if c.CAFilePath == "" { + return tlsConfig, nil + } + + caCert, err := os.ReadFile(c.CAFilePath) + caCertPool := x509.NewCertPool() + if err != nil { + return nil, err + } + caCertPool.AppendCertsFromPEM(caCert) + tlsConfig.ClientCAs = caCertPool + tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert + + return tlsConfig, nil +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/config/config_test.go b/cmd/amazon-cloudwatch-agent-target-allocator/config/config_test.go new file mode 100644 index 000000000..d46b1705e --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/config/config_test.go @@ -0,0 +1,225 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "fmt" + "testing" + "time" + + commonconfig "github.com/prometheus/common/config" + "github.com/prometheus/common/model" + promconfig "github.com/prometheus/prometheus/config" + "github.com/prometheus/prometheus/discovery" + "github.com/prometheus/prometheus/discovery/file" + "github.com/stretchr/testify/assert" +) + +func TestLoad(t *testing.T) { + var defaulAllocationStrategy = DefaultAllocationStrategy + type args struct { + file string + } + tests := []struct { + name string + args args + want Config + wantErr assert.ErrorAssertionFunc + }{ + { + name: "file sd load", + args: args{ + file: "./testdata/config_test.yaml", + }, + want: Config{ + AllocationStrategy: &defaulAllocationStrategy, + LabelSelector: map[string]string{ + "app.kubernetes.io/instance": "default.test", + "app.kubernetes.io/managed-by": "amazon-cloudwatch-agent-operator", + }, + PrometheusCR: PrometheusCRConfig{ + ScrapeInterval: model.Duration(time.Second * 60), + }, + HTTPS: HTTPSServerConfig{ + Enabled: true, + ListenAddr: DefaultListenAddr, + CAFilePath: "/path/to/ca.pem", + TLSCertFilePath: "/path/to/cert.pem", + TLSKeyFilePath: "/path/to/key.pem", + }, + PromConfig: &promconfig.Config{ + GlobalConfig: promconfig.GlobalConfig{ + ScrapeInterval: model.Duration(60 * time.Second), + ScrapeTimeout: model.Duration(10 * time.Second), + EvaluationInterval: model.Duration(60 * time.Second), + }, + ScrapeConfigs: []*promconfig.ScrapeConfig{ + { + JobName: "prometheus", + HonorTimestamps: true, + ScrapeInterval: model.Duration(60 * time.Second), + ScrapeTimeout: model.Duration(10 * time.Second), + MetricsPath: "/metrics", + Scheme: "http", + HTTPClientConfig: commonconfig.HTTPClientConfig{ + FollowRedirects: true, + EnableHTTP2: true, + }, + ServiceDiscoveryConfigs: []discovery.Config{ + &file.SDConfig{ + Files: []string{"./file_sd_test.json"}, + RefreshInterval: model.Duration(5 * time.Minute), + }, + discovery.StaticConfig{ + { + Targets: []model.LabelSet{ + {model.AddressLabel: "prom.domain:9001"}, + {model.AddressLabel: "prom.domain:9002"}, + {model.AddressLabel: "prom.domain:9003"}, + }, + Labels: model.LabelSet{ + "my": "label", + }, + Source: "0", + }, + }, + }, + }, + }, + }, + }, + wantErr: assert.NoError, + }, + { + name: "no config", + args: args{ + file: "./testdata/no_config.yaml", + }, + want: CreateDefaultConfig(), + wantErr: assert.NoError, + }, + { + name: "service monitor pod monitor selector", + args: args{ + file: "./testdata/pod_service_selector_test.yaml", + }, + want: Config{ + AllocationStrategy: &defaulAllocationStrategy, + LabelSelector: map[string]string{ + "app.kubernetes.io/instance": "default.test", + "app.kubernetes.io/managed-by": "amazon-cloudwatch-agent-operator", + }, + PrometheusCR: PrometheusCRConfig{ + ScrapeInterval: DefaultCRScrapeInterval, + }, + HTTPS: HTTPSServerConfig{ + Enabled: true, + ListenAddr: DefaultListenAddr, + CAFilePath: DefaultCABundlePath, + TLSCertFilePath: DefaultTLSCertPath, + TLSKeyFilePath: DefaultTLSKeyPath, + }, + PromConfig: &promconfig.Config{ + GlobalConfig: promconfig.GlobalConfig{ + ScrapeInterval: model.Duration(60 * time.Second), + ScrapeTimeout: model.Duration(10 * time.Second), + EvaluationInterval: model.Duration(60 * time.Second), + }, + ScrapeConfigs: []*promconfig.ScrapeConfig{ + { + JobName: "prometheus", + HonorTimestamps: true, + ScrapeInterval: model.Duration(60 * time.Second), + ScrapeTimeout: model.Duration(10 * time.Second), + MetricsPath: "/metrics", + Scheme: "http", + HTTPClientConfig: commonconfig.HTTPClientConfig{ + FollowRedirects: true, + EnableHTTP2: true, + }, + ServiceDiscoveryConfigs: []discovery.Config{ + discovery.StaticConfig{ + { + Targets: []model.LabelSet{ + {model.AddressLabel: "prom.domain:9001"}, + {model.AddressLabel: "prom.domain:9002"}, + {model.AddressLabel: "prom.domain:9003"}, + }, + Labels: model.LabelSet{ + "my": "label", + }, + Source: "0", + }, + }, + }, + }, + }, + }, + PodMonitorSelector: map[string]string{ + "release": "test", + }, + ServiceMonitorSelector: map[string]string{ + "release": "test", + }, + }, + wantErr: assert.NoError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := CreateDefaultConfig() + err := LoadFromFile(tt.args.file, &got) + if !tt.wantErr(t, err, fmt.Sprintf("Load(%v)", tt.args.file)) { + return + } + assert.Equalf(t, tt.want, got, "Load(%v)", tt.args.file) + }) + } +} + +func TestValidateConfig(t *testing.T) { + testCases := []struct { + name string + fileConfig Config + expectedErr error + }{ + { + name: "promCR enabled, no Prometheus config", + fileConfig: Config{PromConfig: nil, PrometheusCR: PrometheusCRConfig{Enabled: true}}, + expectedErr: nil, + }, + { + name: "promCR disabled, no Prometheus config", + fileConfig: Config{PromConfig: nil}, + expectedErr: fmt.Errorf("at least one scrape config must be defined, or Prometheus CR watching must be enabled"), + }, + { + name: "promCR disabled, Prometheus config present, no scrapeConfigs", + fileConfig: Config{PromConfig: &promconfig.Config{}}, + expectedErr: fmt.Errorf("at least one scrape config must be defined, or Prometheus CR watching must be enabled"), + }, + { + name: "promCR disabled, Prometheus config present, scrapeConfigs present", + fileConfig: Config{ + PromConfig: &promconfig.Config{ScrapeConfigs: []*promconfig.ScrapeConfig{{}}}, + }, + expectedErr: nil, + }, + { + name: "promCR enabled, Prometheus config present, scrapeConfigs present", + fileConfig: Config{ + PromConfig: &promconfig.Config{ScrapeConfigs: []*promconfig.ScrapeConfig{{}}}, + PrometheusCR: PrometheusCRConfig{Enabled: true}, + }, + expectedErr: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := ValidateConfig(&tc.fileConfig) + assert.Equal(t, tc.expectedErr, err) + }) + } +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/config/flags.go b/cmd/amazon-cloudwatch-agent-target-allocator/config/flags.go new file mode 100644 index 000000000..caf639ed9 --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/config/flags.go @@ -0,0 +1,79 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "flag" + "path/filepath" + + "github.com/spf13/pflag" + "k8s.io/client-go/util/homedir" + "sigs.k8s.io/controller-runtime/pkg/log/zap" +) + +// Flag names. +const ( + targetAllocatorName = "target-allocator" + configFilePathFlagName = "config-file" + listenAddrFlagName = "listen-addr" + prometheusCREnabledFlagName = "enable-prometheus-cr-watcher" + kubeConfigPathFlagName = "kubeconfig-path" + reloadConfigFlagName = "reload-config" + httpsEnabledFlagName = "enable-https-server" + listenAddrHttpsFlagName = "listen-addr-https" + httpsCAFilePathFlagName = "https-ca-file" + httpsTLSCertFilePathFlagName = "https-tls-cert-file" + httpsTLSKeyFilePathFlagName = "https-tls-key-file" +) + +// We can't bind this flag to our FlagSet, so we need to handle it separately. +var zapCmdLineOpts zap.Options + +func getFlagSet(errorHandling pflag.ErrorHandling) *pflag.FlagSet { + flagSet := pflag.NewFlagSet(targetAllocatorName, errorHandling) + flagSet.String(configFilePathFlagName, DefaultConfigFilePath, "The path to the config file.") + flagSet.String(kubeConfigPathFlagName, filepath.Join(homedir.HomeDir(), ".kube", "config"), "absolute path to the KubeconfigPath file") + flagSet.Bool(reloadConfigFlagName, false, "Enable automatic configuration reloading. This functionality is deprecated and will be removed in a future release.") + flagSet.Bool(httpsEnabledFlagName, true, "Enable HTTPS additional server") + flagSet.String(listenAddrHttpsFlagName, ":8443", "The address where this service serves over HTTPS.") + flagSet.String(httpsCAFilePathFlagName, DefaultCABundlePath, "The path to the HTTPS server TLS CA file.") + flagSet.String(httpsTLSCertFilePathFlagName, DefaultTLSCertPath, "The path to the HTTPS server TLS certificate file.") + flagSet.String(httpsTLSKeyFilePathFlagName, DefaultTLSKeyPath, "The path to the HTTPS server TLS key file.") + zapFlagSet := flag.NewFlagSet("", flag.ErrorHandling(errorHandling)) + zapCmdLineOpts.BindFlags(zapFlagSet) + flagSet.AddGoFlagSet(zapFlagSet) + return flagSet +} + +func getConfigFilePath(flagSet *pflag.FlagSet) (string, error) { + return flagSet.GetString(configFilePathFlagName) +} + +func getKubeConfigFilePath(flagSet *pflag.FlagSet) (string, error) { + return flagSet.GetString(kubeConfigPathFlagName) +} + +func getConfigReloadEnabled(flagSet *pflag.FlagSet) (bool, error) { + return flagSet.GetBool(reloadConfigFlagName) +} + +func getHttpsListenAddr(flagSet *pflag.FlagSet) (string, error) { + return flagSet.GetString(listenAddrHttpsFlagName) +} + +func getHttpsEnabled(flagSet *pflag.FlagSet) (bool, error) { + return flagSet.GetBool(httpsEnabledFlagName) +} + +func getHttpsCAFilePath(flagSet *pflag.FlagSet) (string, error) { + return flagSet.GetString(httpsCAFilePathFlagName) +} + +func getHttpsTLSCertFilePath(flagSet *pflag.FlagSet) (string, error) { + return flagSet.GetString(httpsTLSCertFilePathFlagName) +} + +func getHttpsTLSKeyFilePath(flagSet *pflag.FlagSet) (string, error) { + return flagSet.GetString(httpsTLSKeyFilePathFlagName) +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/config/flags_test.go b/cmd/amazon-cloudwatch-agent-target-allocator/config/flags_test.go new file mode 100644 index 000000000..8beed29d0 --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/config/flags_test.go @@ -0,0 +1,84 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "path/filepath" + "testing" + + "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" +) + +func TestGetFlagSet(t *testing.T) { + fs := getFlagSet(pflag.ExitOnError) + + // Check if each flag exists + assert.NotNil(t, fs.Lookup(configFilePathFlagName), "Flag %s not found", configFilePathFlagName) + assert.NotNil(t, fs.Lookup(kubeConfigPathFlagName), "Flag %s not found", kubeConfigPathFlagName) +} + +func TestFlagGetters(t *testing.T) { + tests := []struct { + name string + flagArgs []string + expectedValue interface{} + expectedErr bool + getterFunc func(*pflag.FlagSet) (interface{}, error) + }{ + { + name: "GetConfigFilePath", + flagArgs: []string{"--" + configFilePathFlagName, "/path/to/config"}, + expectedValue: "/path/to/config", + getterFunc: func(fs *pflag.FlagSet) (interface{}, error) { return getConfigFilePath(fs) }, + }, + { + name: "GetKubeConfigFilePath", + flagArgs: []string{"--" + kubeConfigPathFlagName, filepath.Join("~", ".kube", "config")}, + expectedValue: filepath.Join("~", ".kube", "config"), + getterFunc: func(fs *pflag.FlagSet) (interface{}, error) { return getKubeConfigFilePath(fs) }, + }, + { + name: "GetConfigReloadEnabled", + flagArgs: []string{"--" + reloadConfigFlagName, "true"}, + expectedValue: true, + getterFunc: func(fs *pflag.FlagSet) (interface{}, error) { return getConfigReloadEnabled(fs) }, + }, + { + name: "InvalidFlag", + flagArgs: []string{"--invalid-flag", "value"}, + expectedErr: true, + getterFunc: func(fs *pflag.FlagSet) (interface{}, error) { return getConfigFilePath(fs) }, + }, + { + name: "HttpsServer", + flagArgs: []string{"--" + httpsEnabledFlagName, "true"}, + expectedValue: true, + getterFunc: func(fs *pflag.FlagSet) (interface{}, error) { return getHttpsEnabled(fs) }, + }, + { + name: "HttpsServerKey", + flagArgs: []string{"--" + httpsTLSKeyFilePathFlagName, "/path/to/tls.key"}, + expectedValue: "/path/to/tls.key", + getterFunc: func(fs *pflag.FlagSet) (interface{}, error) { return getHttpsTLSKeyFilePath(fs) }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fs := getFlagSet(pflag.ContinueOnError) + err := fs.Parse(tt.flagArgs) + + // If an error is expected during parsing, we check it here. + if tt.expectedErr { + assert.Error(t, err) + return + } + + got, err := tt.getterFunc(fs) + assert.NoError(t, err) + assert.Equal(t, tt.expectedValue, got) + }) + } +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/config/testdata/config_test.yaml b/cmd/amazon-cloudwatch-agent-target-allocator/config/testdata/config_test.yaml new file mode 100644 index 000000000..b3f080ae0 --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/config/testdata/config_test.yaml @@ -0,0 +1,22 @@ +label_selector: + app.kubernetes.io/instance: default.test + app.kubernetes.io/managed-by: amazon-cloudwatch-agent-operator +prometheus_cr: + scrape_interval: 60s +https: + enabled: true + ca_file_path: /path/to/ca.pem + tls_cert_file_path: /path/to/cert.pem + tls_key_file_path: /path/to/key.pem +config: + scrape_configs: + - job_name: prometheus + + file_sd_configs: + - files: + - ./file_sd_test.json + + static_configs: + - targets: ["prom.domain:9001", "prom.domain:9002", "prom.domain:9003"] + labels: + my: label diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/config/testdata/file_sd_test.json b/cmd/amazon-cloudwatch-agent-target-allocator/config/testdata/file_sd_test.json new file mode 100644 index 000000000..7114e6246 --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/config/testdata/file_sd_test.json @@ -0,0 +1,18 @@ +[ + { + "labels": { + "job": "node" + }, + "targets": [ + "promfile.domain:1001" + ] + }, + { + "labels": { + "foo1": "bar1" + }, + "targets": [ + "promfile.domain:3000" + ] + } +] diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/config/testdata/no_config.yaml b/cmd/amazon-cloudwatch-agent-target-allocator/config/testdata/no_config.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/config/testdata/pod_service_selector_test.yaml b/cmd/amazon-cloudwatch-agent-target-allocator/config/testdata/pod_service_selector_test.yaml new file mode 100644 index 000000000..298ce5639 --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/config/testdata/pod_service_selector_test.yaml @@ -0,0 +1,14 @@ +label_selector: + app.kubernetes.io/instance: default.test + app.kubernetes.io/managed-by: amazon-cloudwatch-agent-operator +pod_monitor_selector: + release: test +service_monitor_selector: + release: test +config: + scrape_configs: + - job_name: prometheus + static_configs: + - targets: ["prom.domain:9001", "prom.domain:9002", "prom.domain:9003"] + labels: + my: label \ No newline at end of file diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/diff/diff.go b/cmd/amazon-cloudwatch-agent-target-allocator/diff/diff.go new file mode 100644 index 000000000..daac98724 --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/diff/diff.go @@ -0,0 +1,52 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package diff + +// Changes is the result of the difference between two maps – items that are added and items that are removed +// This map is used to reconcile state differences. +type Changes[T Hasher] struct { + additions map[string]T + removals map[string]T +} + +type Hasher interface { + Hash() string +} + +func NewChanges[T Hasher](additions map[string]T, removals map[string]T) Changes[T] { + return Changes[T]{additions: additions, removals: removals} +} + +func (c Changes[T]) Additions() map[string]T { + return c.additions +} + +func (c Changes[T]) Removals() map[string]T { + return c.removals +} + +// Maps generates Changes for two maps with the same type signature by checking for any removals and then checking for +// additions. +// TODO: This doesn't need to create maps, it can return slices only. This function doesn't need to insert the values. +func Maps[T Hasher](current, new map[string]T) Changes[T] { + additions := map[string]T{} + removals := map[string]T{} + for key, newValue := range new { + if currentValue, found := current[key]; !found { + additions[key] = newValue + } else if currentValue.Hash() != newValue.Hash() { + additions[key] = newValue + removals[key] = currentValue + } + } + for key, value := range current { + if _, found := new[key]; !found { + removals[key] = value + } + } + return Changes[T]{ + additions: additions, + removals: removals, + } +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/diff/diff_test.go b/cmd/amazon-cloudwatch-agent-target-allocator/diff/diff_test.go new file mode 100644 index 000000000..d64c1c743 --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/diff/diff_test.go @@ -0,0 +1,97 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package diff + +import ( + "reflect" + "testing" +) + +type HasherString string + +func (s HasherString) Hash() string { + return string(s) +} + +func TestDiffMaps(t *testing.T) { + type args struct { + current map[string]Hasher + new map[string]Hasher + } + tests := []struct { + name string + args args + want Changes[Hasher] + }{ + { + name: "basic replacement", + args: args{ + current: map[string]Hasher{ + "current": HasherString("one"), + }, + new: map[string]Hasher{ + "new": HasherString("another"), + }, + }, + want: Changes[Hasher]{ + additions: map[string]Hasher{ + "new": HasherString("another"), + }, + removals: map[string]Hasher{ + "current": HasherString("one"), + }, + }, + }, + { + name: "single addition", + args: args{ + current: map[string]Hasher{ + "current": HasherString("one"), + }, + new: map[string]Hasher{ + "current": HasherString("one"), + "new": HasherString("another"), + }, + }, + want: Changes[Hasher]{ + additions: map[string]Hasher{ + "new": HasherString("another"), + }, + removals: map[string]Hasher{}, + }, + }, + { + name: "value change", + args: args{ + current: map[string]Hasher{ + "k1": HasherString("v1"), + "k2": HasherString("v2"), + "change": HasherString("before"), + }, + new: map[string]Hasher{ + "k1": HasherString("v1"), + "k3": HasherString("v3"), + "change": HasherString("after"), + }, + }, + want: Changes[Hasher]{ + additions: map[string]Hasher{ + "k3": HasherString("v3"), + "change": HasherString("after"), + }, + removals: map[string]Hasher{ + "k2": HasherString("v2"), + "change": HasherString("before"), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Maps(tt.args.current, tt.args.new); !reflect.DeepEqual(got, tt.want) { + t.Errorf("DiffMaps() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/main.go b/cmd/amazon-cloudwatch-agent-target-allocator/main.go new file mode 100644 index 000000000..33eb1ae6e --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/main.go @@ -0,0 +1,240 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + + gokitlog "github.com/go-kit/log" + "github.com/oklog/run" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/prometheus/discovery" + _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" + ctrl "sigs.k8s.io/controller-runtime" + + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/allocation" + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/collector" + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/config" + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/prehook" + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/server" + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/target" + allocatorWatcher "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/watcher" +) + +var ( + setupLog = ctrl.Log.WithName("setup") + eventsMetric = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "cloudwatch_agent_allocator_events", + Help: "Number of events in the channel.", + }, []string{"source"}) +) + +func main() { + var ( + // allocatorPrehook will be nil if filterStrategy is not set or + // unrecognized. No filtering will be used in this case. + allocatorPrehook prehook.Hook + allocator allocation.Allocator + discoveryManager *discovery.Manager + collectorWatcher *collector.Client + fileWatcher allocatorWatcher.Watcher + promWatcher allocatorWatcher.Watcher + targetDiscoverer *target.Discoverer + + discoveryCancel context.CancelFunc + runGroup run.Group + eventChan = make(chan allocatorWatcher.Event) + eventCloser = make(chan bool, 1) + interrupts = make(chan os.Signal, 1) + errChan = make(chan error) + ) + cfg, configFilePath, err := config.Load() + + if err != nil { + fmt.Printf("Failed to load config: %v", err) + os.Exit(1) + } + setupLog.Info("init config", "Config-Http", cfg.HTTPS) + ctrl.SetLogger(cfg.RootLogger) + + if validationErr := config.ValidateConfig(cfg); validationErr != nil { + setupLog.Error(validationErr, "Invalid configuration") + os.Exit(1) + } + + cfg.RootLogger.Info("Starting the Target Allocator") + ctx := context.Background() + log := ctrl.Log.WithName("allocator") + + allocatorPrehook = prehook.New(cfg.GetTargetsFilterStrategy(), log) + allocator, err = allocation.New(cfg.GetAllocationStrategy(), log, allocation.WithFilter(allocatorPrehook)) + if err != nil { + setupLog.Error(err, "Unable to initialize allocation strategy") + os.Exit(1) + } + + httpOptions := []server.Option{} + tlsConfig, confErr := cfg.HTTPS.NewTLSConfig(ctx) + if confErr != nil { + setupLog.Error(confErr, "Unable to initialize TLS configuration", "Config", cfg.HTTPS) + os.Exit(1) + } + httpOptions = append(httpOptions, server.WithTLSConfig(tlsConfig, cfg.HTTPS.ListenAddr)) + srv := server.NewServer(log, allocator, cfg.ListenAddr, httpOptions...) + + discoveryCtx, discoveryCancel := context.WithCancel(ctx) + discoveryManager = discovery.NewManager(discoveryCtx, gokitlog.NewNopLogger()) + discovery.RegisterMetrics() // discovery manager metrics need to be enabled explicitly + + targetDiscoverer = target.NewDiscoverer(log, discoveryManager, allocatorPrehook, srv) + collectorWatcher, collectorWatcherErr := collector.NewClient(log, cfg.ClusterConfig) + if collectorWatcherErr != nil { + setupLog.Error(collectorWatcherErr, "Unable to initialize collector watcher") + os.Exit(1) + } + if cfg.ReloadConfig { + fileWatcher, err = allocatorWatcher.NewFileWatcher(setupLog.WithName("file-watcher"), configFilePath) + if err != nil { + setupLog.Error(err, "Can't start the file watcher") + os.Exit(1) + } + } + signal.Notify(interrupts, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + defer close(interrupts) + + if cfg.PrometheusCR.Enabled { + promWatcher, err = allocatorWatcher.NewPrometheusCRWatcher(setupLog.WithName("prometheus-cr-watcher"), *cfg) + if err != nil { + setupLog.Error(err, "Can't start the prometheus watcher") + os.Exit(1) + } + runGroup.Add( + func() error { + promWatcherErr := promWatcher.Watch(eventChan, errChan) + setupLog.Info("Prometheus watcher exited") + return promWatcherErr + }, + func(_ error) { + setupLog.Info("Closing prometheus watcher") + promWatcherErr := promWatcher.Close() + if promWatcherErr != nil { + setupLog.Error(promWatcherErr, "prometheus watcher failed to close") + } + }) + } + if cfg.ReloadConfig { + runGroup.Add( + func() error { + fileWatcherErr := fileWatcher.Watch(eventChan, errChan) + setupLog.Info("File watcher exited") + return fileWatcherErr + }, + func(_ error) { + setupLog.Info("Closing file watcher") + fileWatcherErr := fileWatcher.Close() + if fileWatcherErr != nil { + setupLog.Error(fileWatcherErr, "file watcher failed to close") + } + }) + } + runGroup.Add( + func() error { + discoveryManagerErr := discoveryManager.Run() + setupLog.Info("Discovery manager exited") + return discoveryManagerErr + }, + func(_ error) { + setupLog.Info("Closing discovery manager") + discoveryCancel() + }) + runGroup.Add( + func() error { + // Initial loading of the config file's scrape config + err = targetDiscoverer.ApplyConfig(allocatorWatcher.EventSourceConfigMap, cfg.PromConfig) + if err != nil { + setupLog.Error(err, "Unable to apply initial configuration") + return err + } + err := targetDiscoverer.Watch(allocator.SetTargets) + setupLog.Info("Target discoverer exited") + return err + }, + func(_ error) { + setupLog.Info("Closing target discoverer") + targetDiscoverer.Close() + }) + runGroup.Add( + func() error { + err := collectorWatcher.Watch(ctx, cfg.LabelSelector, allocator.SetCollectors) + setupLog.Info("Collector watcher exited") + return err + }, + func(_ error) { + setupLog.Info("Closing collector watcher") + collectorWatcher.Close() + }) + runGroup.Add( + func() error { + err := srv.StartHTTPS() + setupLog.Info("HTTPS Server failed to start", "error", err) + return err + }, + func(intrpError error) { + setupLog.Info("Closing HTTPS server", "intrp", intrpError) + if shutdownErr := srv.ShutdownHTTPS(ctx); shutdownErr != nil { + setupLog.Error(shutdownErr, "Error on HTTPS server shutdown") + } + }) + runGroup.Add( + func() error { + for { + select { + case event := <-eventChan: + eventsMetric.WithLabelValues(event.Source.String()).Inc() + loadConfig, err := event.Watcher.LoadConfig(ctx) + if err != nil { + setupLog.Error(err, "Unable to load configuration") + continue + } + err = targetDiscoverer.ApplyConfig(event.Source, loadConfig) + if err != nil { + setupLog.Error(err, "Unable to apply configuration") + continue + } + case err := <-errChan: + setupLog.Error(err, "Watcher error") + case <-eventCloser: + return nil + } + } + }, + func(_ error) { + setupLog.Info("Closing watcher loop") + close(eventCloser) + }) + runGroup.Add( + func() error { + for { + select { + case <-interrupts: + setupLog.Info("Received interrupt") + return nil + case <-eventCloser: + return nil + } + } + }, + func(_ error) { + setupLog.Info("Closing interrupt loop") + }) + if runErr := runGroup.Run(); runErr != nil { + setupLog.Error(runErr, "run group exited") + } + setupLog.Info("Target allocator exited.") +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/prehook/prehook.go b/cmd/amazon-cloudwatch-agent-target-allocator/prehook/prehook.go new file mode 100644 index 000000000..a1f1b4c35 --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/prehook/prehook.go @@ -0,0 +1,53 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package prehook + +import ( + "errors" + + "github.com/go-logr/logr" + "github.com/prometheus/prometheus/model/relabel" + + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/target" +) + +const ( + relabelConfigTargetFilterName = "relabel-config" +) + +type Hook interface { + Apply(map[string]*target.Item) map[string]*target.Item + SetConfig(map[string][]*relabel.Config) + GetConfig() map[string][]*relabel.Config +} + +type HookProvider func(log logr.Logger) Hook + +var ( + registry = map[string]HookProvider{} +) + +func New(name string, log logr.Logger) Hook { + if p, ok := registry[name]; ok { + return p(log.WithName("Prehook").WithName(name)) + } + + log.Info("Unrecognized filter strategy; filtering disabled") + return nil +} + +func Register(name string, provider HookProvider) error { + if _, ok := registry[name]; ok { + return errors.New("already registered") + } + registry[name] = provider + return nil +} + +func init() { + err := Register(relabelConfigTargetFilterName, NewRelabelConfigTargetFilter) + if err != nil { + panic(err) + } +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/prehook/relabel.go b/cmd/amazon-cloudwatch-agent-target-allocator/prehook/relabel.go new file mode 100644 index 000000000..c8d7b20f8 --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/prehook/relabel.go @@ -0,0 +1,99 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package prehook + +import ( + "github.com/go-logr/logr" + "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/model/labels" + "github.com/prometheus/prometheus/model/relabel" + + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/target" +) + +type RelabelConfigTargetFilter struct { + log logr.Logger + relabelCfg map[string][]*relabel.Config +} + +func NewRelabelConfigTargetFilter(log logr.Logger) Hook { + return &RelabelConfigTargetFilter{ + log: log, + relabelCfg: make(map[string][]*relabel.Config), + } +} + +// helper function converts from model.LabelSet to []labels.Label. +func convertLabelToPromLabelSet(lbls model.LabelSet) []labels.Label { + newLabels := make([]labels.Label, len(lbls)) + index := 0 + for k, v := range lbls { + newLabels[index].Name = string(k) + newLabels[index].Value = string(v) + index++ + } + return newLabels +} + +func (tf *RelabelConfigTargetFilter) Apply(targets map[string]*target.Item) map[string]*target.Item { + numTargets := len(targets) + + // need to wait until relabelCfg is set + if len(tf.relabelCfg) == 0 { + return targets + } + + // Note: jobNameKey != tItem.JobName (jobNameKey is hashed) + for jobNameKey, tItem := range targets { + keepTarget := true + lset := convertLabelToPromLabelSet(tItem.Labels) + for _, cfg := range tf.relabelCfg[tItem.JobName] { + if newLset, keep := relabel.Process(lset, cfg); !keep { + keepTarget = false + break // inner loop + } else { + lset = newLset + } + } + + if !keepTarget { + delete(targets, jobNameKey) + } + } + + tf.log.V(2).Info("Filtering complete", "seen", numTargets, "kept", len(targets)) + return targets +} + +func (tf *RelabelConfigTargetFilter) SetConfig(cfgs map[string][]*relabel.Config) { + relabelCfgCopy := make(map[string][]*relabel.Config) + for key, val := range cfgs { + relabelCfgCopy[key] = tf.replaceRelabelConfig(val) + } + + tf.relabelCfg = relabelCfgCopy +} + +// See this thread [https://github.com/open-telemetry/opentelemetry-operator/pull/1124/files#r983145795] +// for why SHARD == 0 is a necessary substitution. Otherwise the keep action that uses this env variable, +// would not match the regex and all targets end up dropped. Also note, $(SHARD) will always be 0 and it +// does not make sense to read from the environment because it is never set in the allocator. +func (tf *RelabelConfigTargetFilter) replaceRelabelConfig(cfg []*relabel.Config) []*relabel.Config { + for i := range cfg { + str := cfg[i].Regex.String() + if str == "$(SHARD)" { + cfg[i].Regex = relabel.MustNewRegexp("0") + } + } + + return cfg +} + +func (tf *RelabelConfigTargetFilter) GetConfig() map[string][]*relabel.Config { + relabelCfgCopy := make(map[string][]*relabel.Config) + for k, v := range tf.relabelCfg { + relabelCfgCopy[k] = v + } + return relabelCfgCopy +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/prehook/relabel_test.go b/cmd/amazon-cloudwatch-agent-target-allocator/prehook/relabel_test.go new file mode 100644 index 000000000..a1e14b03d --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/prehook/relabel_test.go @@ -0,0 +1,259 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package prehook + +import ( + "crypto/rand" + "fmt" + "math/big" + "strconv" + "testing" + + "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/model/relabel" + "github.com/stretchr/testify/assert" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/target" +) + +var ( + logger = logf.Log.WithName("unit-tests") + defaultNumTargets = 100 + defaultNumCollectors = 3 + defaultStartIndex = 0 + + relabelConfigs = []relabelConfigObj{ + { + cfg: []*relabel.Config{ + { + SourceLabels: model.LabelNames{"i"}, + Action: "replace", + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + Replacement: "$1", + TargetLabel: "foo", + }, + }, + isDrop: false, + }, + { + cfg: []*relabel.Config{ + { + SourceLabels: model.LabelNames{"i"}, + Regex: relabel.MustNewRegexp("(.*)"), + Separator: ";", + Action: "keep", + Replacement: "$1", + }, + }, + isDrop: false, + }, + { + cfg: []*relabel.Config{ + { + SourceLabels: model.LabelNames{"i"}, + Regex: relabel.MustNewRegexp("bad.*match"), + Action: "drop", + Separator: ";", + Replacement: "$1", + }, + }, + isDrop: false, + }, + { + cfg: []*relabel.Config{ + { + SourceLabels: model.LabelNames{"label_not_present"}, + Regex: relabel.MustNewRegexp("(.*)"), + Separator: ";", + Action: "keep", + Replacement: "$1", + }, + }, + isDrop: false, + }, + { + cfg: []*relabel.Config{ + { + SourceLabels: model.LabelNames{"i"}, + Regex: relabel.MustNewRegexp("(.*)"), + Separator: ";", + Action: "drop", + Replacement: "$1", + }, + }, + isDrop: true, + }, + { + cfg: []*relabel.Config{ + { + SourceLabels: model.LabelNames{"collector"}, + Regex: relabel.MustNewRegexp("(collector.*)"), + Separator: ";", + Action: "drop", + Replacement: "$1", + }, + }, + isDrop: true, + }, + { + cfg: []*relabel.Config{ + { + SourceLabels: model.LabelNames{"i"}, + Regex: relabel.MustNewRegexp("bad.*match"), + Separator: ";", + Action: "keep", + Replacement: "$1", + }, + }, + isDrop: true, + }, + { + cfg: []*relabel.Config{ + { + SourceLabels: model.LabelNames{"collector"}, + Regex: relabel.MustNewRegexp("collectors-n"), + Separator: ";", + Action: "keep", + Replacement: "$1", + }, + }, + isDrop: true, + }, + } + + HashmodConfig = relabelConfigObj{ + cfg: []*relabel.Config{ + { + SourceLabels: model.LabelNames{"i"}, + Regex: relabel.MustNewRegexp("(.*)"), + Separator: ";", + Modulus: 1, + TargetLabel: "tmp-0", + Action: "hashmod", + Replacement: "$1", + }, + + { + SourceLabels: model.LabelNames{"tmp-$(SHARD)"}, + Regex: relabel.MustNewRegexp("$(SHARD)"), + Separator: ";", + Action: "keep", + Replacement: "$1", + }, + }, + isDrop: false, + } + + DefaultDropRelabelConfig = relabel.Config{ + SourceLabels: model.LabelNames{"i"}, + Regex: relabel.MustNewRegexp("(.*)"), + Action: "drop", + } +) + +type relabelConfigObj struct { + cfg []*relabel.Config + isDrop bool +} + +func colIndex(index, numCols int) int { + if numCols == 0 { + return -1 + } + return index % numCols +} + +func makeNNewTargets(rCfgs []relabelConfigObj, n int, numCollectors int, startingIndex int) (map[string]*target.Item, int, map[string]*target.Item, map[string][]*relabel.Config) { + toReturn := map[string]*target.Item{} + expectedMap := make(map[string]*target.Item) + numItemsRemaining := n + relabelConfig := make(map[string][]*relabel.Config) + for i := startingIndex; i < n+startingIndex; i++ { + collector := fmt.Sprintf("collector-%d", colIndex(i, numCollectors)) + label := model.LabelSet{ + "collector": model.LabelValue(collector), + "i": model.LabelValue(strconv.Itoa(i)), + "total": model.LabelValue(strconv.Itoa(n + startingIndex)), + } + jobName := fmt.Sprintf("test-job-%d", i) + newTarget := target.NewItem(jobName, "test-url", label, collector) + // add a single replace, drop, or keep action as relabel_config for targets + var index int + ind, _ := rand.Int(rand.Reader, big.NewInt(int64(len(relabelConfigs)))) + + index = int(ind.Int64()) + + relabelConfig[jobName] = rCfgs[index].cfg + + targetKey := newTarget.Hash() + if relabelConfigs[index].isDrop { + numItemsRemaining-- + } else { + expectedMap[targetKey] = newTarget + } + toReturn[targetKey] = newTarget + } + return toReturn, numItemsRemaining, expectedMap, relabelConfig +} + +func TestApply(t *testing.T) { + allocatorPrehook := New("relabel-config", logger) + assert.NotNil(t, allocatorPrehook) + + targets, numRemaining, expectedTargetMap, relabelCfg := makeNNewTargets(relabelConfigs, defaultNumTargets, defaultNumCollectors, defaultStartIndex) + allocatorPrehook.SetConfig(relabelCfg) + remainingItems := allocatorPrehook.Apply(targets) + assert.Len(t, remainingItems, numRemaining) + assert.Equal(t, remainingItems, expectedTargetMap) + + // clear out relabelCfg to test with empty values + for key := range relabelCfg { + relabelCfg[key] = nil + } + + // cfg = createMockConfig(relabelCfg) + allocatorPrehook.SetConfig(relabelCfg) + remainingItems = allocatorPrehook.Apply(targets) + // relabelCfg is empty so targets should be unfiltered + assert.Len(t, remainingItems, len(targets)) + assert.Equal(t, remainingItems, targets) +} + +func TestApplyHashmodAction(t *testing.T) { + allocatorPrehook := New("relabel-config", logger) + assert.NotNil(t, allocatorPrehook) + + hashRelabelConfigs := append(relabelConfigs, HashmodConfig) + targets, numRemaining, expectedTargetMap, relabelCfg := makeNNewTargets(hashRelabelConfigs, defaultNumTargets, defaultNumCollectors, defaultStartIndex) + allocatorPrehook.SetConfig(relabelCfg) + remainingItems := allocatorPrehook.Apply(targets) + assert.Len(t, remainingItems, numRemaining) + assert.Equal(t, remainingItems, expectedTargetMap) +} + +func TestApplyEmptyRelabelCfg(t *testing.T) { + + allocatorPrehook := New("relabel-config", logger) + assert.NotNil(t, allocatorPrehook) + + targets, _, _, _ := makeNNewTargets(relabelConfigs, defaultNumTargets, defaultNumCollectors, defaultStartIndex) + + relabelCfg := map[string][]*relabel.Config{} + allocatorPrehook.SetConfig(relabelCfg) + remainingItems := allocatorPrehook.Apply(targets) + // relabelCfg is empty so targets should be unfiltered + assert.Len(t, remainingItems, len(targets)) + assert.Equal(t, remainingItems, targets) +} + +func TestSetConfig(t *testing.T) { + allocatorPrehook := New("relabel-config", logger) + assert.NotNil(t, allocatorPrehook) + + _, _, _, relabelCfg := makeNNewTargets(relabelConfigs, defaultNumTargets, defaultNumCollectors, defaultStartIndex) + allocatorPrehook.SetConfig(relabelCfg) + assert.Equal(t, relabelCfg, allocatorPrehook.GetConfig()) +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/server/bench_test.go b/cmd/amazon-cloudwatch-agent-target-allocator/server/bench_test.go new file mode 100644 index 000000000..e209d6775 --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/server/bench_test.go @@ -0,0 +1,259 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package server + +import ( + "fmt" + "math/rand" + "net/http/httptest" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/prometheus/common/model" + promconfig "github.com/prometheus/prometheus/config" + "github.com/stretchr/testify/assert" + + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/allocation" + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/target" +) + +func BenchmarkServerTargetsHandler(b *testing.B) { + random := rand.New(rand.NewSource(time.Now().UnixNano())) // nolint: gosec + var table = []struct { + numCollectors int + numJobs int + }{ + {numCollectors: 100, numJobs: 100}, + {numCollectors: 100, numJobs: 1000}, + {numCollectors: 100, numJobs: 10000}, + {numCollectors: 100, numJobs: 100000}, + {numCollectors: 1000, numJobs: 100}, + {numCollectors: 1000, numJobs: 1000}, + {numCollectors: 1000, numJobs: 10000}, + {numCollectors: 1000, numJobs: 100000}, + } + + for _, allocatorName := range allocation.GetRegisteredAllocatorNames() { + for _, v := range table { + a, _ := allocation.New(allocatorName, logger) + cols := allocation.MakeNCollectors(v.numCollectors, 0) + targets := allocation.MakeNNewTargets(v.numJobs, v.numCollectors, 0) + listenAddr := ":8080" + a.SetCollectors(cols) + a.SetTargets(targets) + s := NewServer(logger, a, listenAddr) + b.Run(fmt.Sprintf("%s_num_cols_%d_num_jobs_%d", allocatorName, v.numCollectors, v.numJobs), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + randomJob := random.Intn(v.numJobs) //nolint: gosec + randomCol := random.Intn(v.numCollectors) //nolint: gosec + request := httptest.NewRequest("GET", fmt.Sprintf("/jobs/test-job-%d/targets?collector_id=collector-%d", randomJob, randomCol), nil) + w := httptest.NewRecorder() + s.server.Handler.ServeHTTP(w, request) + } + }) + } + } +} + +func BenchmarkScrapeConfigsHandler(b *testing.B) { + random := rand.New(rand.NewSource(time.Now().UnixNano())) // nolint: gosec + s := &Server{ + logger: logger, + } + + tests := []int{0, 5, 10, 50, 100, 500} + for _, n := range tests { + data := makeNScrapeConfigs(*random, n) + assert.NoError(b, s.UpdateScrapeConfigResponse(data)) + + b.Run(fmt.Sprintf("%d_targets", n), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + gin.SetMode(gin.ReleaseMode) + c.Request = httptest.NewRequest("GET", "/scrape_configs", nil) + + s.ScrapeConfigsHandler(c) + } + }) + } +} + +func BenchmarkCollectorMapJSONHandler(b *testing.B) { + random := rand.New(rand.NewSource(time.Now().UnixNano())) // nolint: gosec + s := &Server{ + logger: logger, + jsonMarshaller: jsonConfig, + } + + tests := []struct { + numCollectors int + numTargets int + }{ + { + numCollectors: 0, + numTargets: 0, + }, + { + numCollectors: 5, + numTargets: 5, + }, + { + numCollectors: 5, + numTargets: 50, + }, + { + numCollectors: 5, + numTargets: 500, + }, + { + numCollectors: 50, + numTargets: 5, + }, + { + numCollectors: 50, + numTargets: 50, + }, + { + numCollectors: 50, + numTargets: 500, + }, + { + numCollectors: 50, + numTargets: 5000, + }, + } + for _, tc := range tests { + data := makeNCollectorJSON(*random, tc.numCollectors, tc.numTargets) + b.Run(fmt.Sprintf("%d_collectors_%d_targets", tc.numCollectors, tc.numTargets), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + resp := httptest.NewRecorder() + s.jsonHandler(resp, data) + } + }) + } +} + +func BenchmarkTargetItemsJSONHandler(b *testing.B) { + random := rand.New(rand.NewSource(time.Now().UnixNano())) // nolint: gosec + s := &Server{ + logger: logger, + jsonMarshaller: jsonConfig, + } + + tests := []struct { + numTargets int + numLabels int + }{ + { + numTargets: 0, + numLabels: 0, + }, + { + numTargets: 5, + numLabels: 5, + }, + { + numTargets: 5, + numLabels: 50, + }, + { + numTargets: 50, + numLabels: 5, + }, + { + numTargets: 50, + numLabels: 50, + }, + { + numTargets: 500, + numLabels: 50, + }, + { + numTargets: 500, + numLabels: 500, + }, + { + numTargets: 5000, + numLabels: 50, + }, + { + numTargets: 5000, + numLabels: 500, + }, + } + for _, tc := range tests { + data := makeNTargetItems(*random, tc.numTargets, tc.numLabels) + b.Run(fmt.Sprintf("%d_targets_%d_labels", tc.numTargets, tc.numLabels), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + resp := httptest.NewRecorder() + s.jsonHandler(resp, data) + } + }) + } +} + +var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_/") + +func randSeq(random rand.Rand, n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letters[random.Intn(len(letters))] //nolint:gosec + } + return string(b) +} + +func makeNScrapeConfigs(random rand.Rand, n int) map[string]*promconfig.ScrapeConfig { + items := make(map[string]*promconfig.ScrapeConfig, n) + for i := 0; i < n; i++ { + items[randSeq(random, 20)] = &promconfig.ScrapeConfig{ + JobName: randSeq(random, 20), + ScrapeInterval: model.Duration(30 * time.Second), + ScrapeTimeout: model.Duration(time.Minute), + MetricsPath: randSeq(random, 50), + SampleLimit: 5, + TargetLimit: 200, + LabelLimit: 20, + LabelNameLengthLimit: 50, + LabelValueLengthLimit: 100, + } + } + return items +} + +func makeNCollectorJSON(random rand.Rand, numCollectors, numItems int) map[string]collectorJSON { + items := make(map[string]collectorJSON, numCollectors) + for i := 0; i < numCollectors; i++ { + items[randSeq(random, 20)] = collectorJSON{ + Link: randSeq(random, 120), + Jobs: makeNTargetItems(random, numItems, 50), + } + } + return items +} + +func makeNTargetItems(random rand.Rand, numItems, numLabels int) []*target.Item { + items := make([]*target.Item, 0, numItems) + for i := 0; i < numItems; i++ { + items = append(items, target.NewItem( + randSeq(random, 80), + randSeq(random, 150), + makeNNewLabels(random, numLabels), + randSeq(random, 30), + )) + } + return items +} + +func makeNNewLabels(random rand.Rand, n int) model.LabelSet { + labels := make(map[model.LabelName]model.LabelValue, n) + for i := 0; i < n; i++ { + labels[model.LabelName(randSeq(random, 20))] = model.LabelValue(randSeq(random, 20)) + } + return labels +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/server/mocks_test.go b/cmd/amazon-cloudwatch-agent-target-allocator/server/mocks_test.go new file mode 100644 index 000000000..160a8e316 --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/server/mocks_test.go @@ -0,0 +1,27 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package server + +import ( + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/allocation" + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/target" +) + +var _ allocation.Allocator = &mockAllocator{} + +// mockAllocator implements the Allocator interface, but all funcs other than +// TargetItems() are a no-op. +type mockAllocator struct { + targetItems map[string]*target.Item +} + +func (m *mockAllocator) SetCollectors(_ map[string]*allocation.Collector) {} +func (m *mockAllocator) SetTargets(_ map[string]*target.Item) {} +func (m *mockAllocator) Collectors() map[string]*allocation.Collector { return nil } +func (m *mockAllocator) GetTargetsForCollectorAndJob(_ string, _ string) []*target.Item { return nil } +func (m *mockAllocator) SetFilter(_ allocation.Filter) {} + +func (m *mockAllocator) TargetItems() map[string]*target.Item { + return m.targetItems +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/server/server.go b/cmd/amazon-cloudwatch-agent-target-allocator/server/server.go new file mode 100644 index 000000000..222c9f73f --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/server/server.go @@ -0,0 +1,324 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package server + +import ( + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "sync" + "time" + + yaml2 "github.com/ghodss/yaml" + "github.com/gin-gonic/gin" + "github.com/go-logr/logr" + jsoniter "github.com/json-iterator/go" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/client_golang/prometheus/promhttp" + promcommconfig "github.com/prometheus/common/config" + promconfig "github.com/prometheus/prometheus/config" + "gopkg.in/yaml.v2" + + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/allocation" + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/target" +) + +var ( + httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "amazon_cloudwatch_agent_allocator_http_duration_seconds", + Help: "Duration of received HTTP requests.", + }, []string{"path"}) +) + +var ( + jsonConfig = jsoniter.Config{ + EscapeHTML: false, + MarshalFloatWith6Digits: true, + ObjectFieldMustBeSimpleString: true, + }.Froze() +) + +type collectorJSON struct { + Link string `json:"_link"` + Jobs []*target.Item `json:"targets"` +} + +type Server struct { + logger logr.Logger + allocator allocation.Allocator + server *http.Server + httpsServer *http.Server + jsonMarshaller jsoniter.API + + // Use RWMutex to protect scrapeConfigResponse, since it + // will be predominantly read and only written when config + // is applied. + mtx sync.RWMutex + scrapeConfigResponse []byte + ScrapeConfigMarshalledSecretResponse []byte +} + +type Option func(*Server) + +// Option to create an additional https server with mTLS configuration. +// Used for getting the scrape config with real secret values. +func WithTLSConfig(tlsConfig *tls.Config, httpsListenAddr string) Option { + return func(s *Server) { + httpsRouter := gin.New() + s.setRouter(httpsRouter) + + s.httpsServer = &http.Server{Addr: httpsListenAddr, Handler: httpsRouter, ReadHeaderTimeout: 90 * time.Second, TLSConfig: tlsConfig} + err := s.server.Shutdown(context.Background()) + if err != nil { + s.logger.Error(err, "Failed to shutdown http server") + } + if errors.Is(err, http.ErrServerClosed) { + s.logger.Info("Http server is already closed") + } + s.server = s.httpsServer + } +} + +func (s *Server) setRouter(router *gin.Engine) { + router.Use(gin.Recovery()) + router.UseRawPath = true + router.UnescapePathValues = false + router.Use(s.PrometheusMiddleware) + + router.GET("/scrape_configs", s.ScrapeConfigsHandler) + router.GET("/jobs", s.JobHandler) + router.GET("/jobs/:job_id/targets", s.TargetsHandler) + router.GET("/metrics", gin.WrapH(promhttp.Handler())) + router.GET("/livez", s.LivenessProbeHandler) + router.GET("/readyz", s.ReadinessProbeHandler) +} + +func NewServer(log logr.Logger, allocator allocation.Allocator, listenAddr string, options ...Option) *Server { + s := &Server{ + logger: log, + allocator: allocator, + jsonMarshaller: jsonConfig, + } + + gin.SetMode(gin.ReleaseMode) + router := gin.New() + s.setRouter(router) + + s.server = &http.Server{Addr: listenAddr, Handler: router, ReadHeaderTimeout: 90 * time.Second} + + for _, opt := range options { + opt(s) + } + + return s +} + +func (s *Server) Start() error { + s.logger.Info("Starting server...") + return s.server.ListenAndServe() +} + +func (s *Server) Shutdown(ctx context.Context) error { + s.logger.Info("Shutting down server...") + return s.server.Shutdown(ctx) +} + +func (s *Server) StartHTTPS() error { + s.logger.Info("Starting HTTPS server...") + return s.httpsServer.ListenAndServeTLS("", "") +} + +func (s *Server) ShutdownHTTPS(ctx context.Context) error { + s.logger.Info("Shutting down HTTPS server...") + return s.httpsServer.Shutdown(ctx) +} + +// RemoveRegexFromRelabelAction is needed specifically for keepequal/dropequal actions because even though the user doesn't specify the +// regex field for these actions the unmarshalling implementations of prometheus adds back the default regex fields +// which in turn causes the receiver to error out since the unmarshaling of the json response doesn't expect anything in the regex fields +// for these actions. Adding this as a fix until the original issue with prometheus unmarshaling is fixed - +// https://github.com/prometheus/prometheus/issues/12534 +func RemoveRegexFromRelabelAction(jsonConfig []byte) ([]byte, error) { + var jobToScrapeConfig map[string]interface{} + err := json.Unmarshal(jsonConfig, &jobToScrapeConfig) + if err != nil { + return nil, err + } + for _, scrapeConfig := range jobToScrapeConfig { + scrapeConfig := scrapeConfig.(map[string]interface{}) + if scrapeConfig["relabel_configs"] != nil { + relabelConfigs := scrapeConfig["relabel_configs"].([]interface{}) + for _, relabelConfig := range relabelConfigs { + relabelConfig := relabelConfig.(map[string]interface{}) + // Dropping regex key from the map since unmarshalling this on the client(metrics_receiver.go) results in error + // because of the bug here - https://github.com/prometheus/prometheus/issues/12534 + if relabelConfig["action"] == "keepequal" || relabelConfig["action"] == "dropequal" { + delete(relabelConfig, "regex") + } + } + } + if scrapeConfig["metric_relabel_configs"] != nil { + metricRelabelConfigs := scrapeConfig["metric_relabel_configs"].([]interface{}) + for _, metricRelabelConfig := range metricRelabelConfigs { + metricRelabelConfig := metricRelabelConfig.(map[string]interface{}) + // Dropping regex key from the map since unmarshalling this on the client(metrics_receiver.go) results in error + // because of the bug here - https://github.com/prometheus/prometheus/issues/12534 + if metricRelabelConfig["action"] == "keepequal" || metricRelabelConfig["action"] == "dropequal" { + delete(metricRelabelConfig, "regex") + } + } + } + } + + jsonConfigNew, err := json.Marshal(jobToScrapeConfig) + if err != nil { + return nil, err + } + return jsonConfigNew, nil +} + +func (s *Server) MarshalScrapeConfig(configs map[string]*promconfig.ScrapeConfig, marshalSecretValue bool) error { + var configBytes []byte + promcommconfig.MarshalSecretValue = marshalSecretValue + configBytes, err := yaml.Marshal(configs) + if err != nil { + return err + } + + var jsonConfig []byte + jsonConfig, err = yaml2.YAMLToJSON(configBytes) + if err != nil { + return err + } + + jsonConfigNew, err := RemoveRegexFromRelabelAction(jsonConfig) + if err != nil { + return err + } + s.mtx.Lock() + if marshalSecretValue { + s.ScrapeConfigMarshalledSecretResponse = jsonConfigNew + } else { + s.scrapeConfigResponse = jsonConfigNew + } + s.mtx.Unlock() + + return nil +} + +// UpdateScrapeConfigResponse updates the scrape config response. The target allocator first marshals these +// configurations such that the underlying prometheus marshaling is used. After that, the YAML is converted +// in to a JSON format for consumers to use. +func (s *Server) UpdateScrapeConfigResponse(configs map[string]*promconfig.ScrapeConfig) error { + err := s.MarshalScrapeConfig(configs, false) + if err != nil { + return err + } + err = s.MarshalScrapeConfig(configs, true) + if err != nil { + return err + } + return nil +} + +// ScrapeConfigsHandler returns the available scrape configuration discovered by the target allocator. +func (s *Server) ScrapeConfigsHandler(c *gin.Context) { + s.mtx.RLock() + result := s.scrapeConfigResponse + if c.Request.TLS != nil { + result = s.ScrapeConfigMarshalledSecretResponse + } + s.mtx.RUnlock() + + // We don't use the jsonHandler method because we don't want our bytes to be re-encoded + c.Writer.Header().Set("Content-Type", "application/json") + _, err := c.Writer.Write(result) + if err != nil { + s.errorHandler(c.Writer, err) + } +} + +func (s *Server) ReadinessProbeHandler(c *gin.Context) { + s.mtx.RLock() + result := s.scrapeConfigResponse + s.mtx.RUnlock() + + if result != nil { + c.Status(http.StatusOK) + } else { + c.Status(http.StatusServiceUnavailable) + } +} + +func (s *Server) JobHandler(c *gin.Context) { + displayData := make(map[string]target.LinkJSON) + for _, v := range s.allocator.TargetItems() { + displayData[v.JobName] = target.LinkJSON{Link: v.Link.Link} + } + s.jsonHandler(c.Writer, displayData) +} + +func (s *Server) LivenessProbeHandler(c *gin.Context) { + c.Status(http.StatusOK) +} + +func (s *Server) PrometheusMiddleware(c *gin.Context) { + path := c.FullPath() + timer := prometheus.NewTimer(httpDuration.WithLabelValues(path)) + c.Next() + timer.ObserveDuration() +} + +func (s *Server) TargetsHandler(c *gin.Context) { + q := c.Request.URL.Query()["collector_id"] + + jobIdParam := c.Params.ByName("job_id") + jobId, err := url.QueryUnescape(jobIdParam) + if err != nil { + s.errorHandler(c.Writer, err) + return + } + + if len(q) == 0 { + displayData := GetAllTargetsByJob(s.allocator, jobId) + s.jsonHandler(c.Writer, displayData) + + } else { + tgs := s.allocator.GetTargetsForCollectorAndJob(q[0], jobId) + // Displays empty list if nothing matches + if len(tgs) == 0 { + s.jsonHandler(c.Writer, []interface{}{}) + return + } + s.jsonHandler(c.Writer, tgs) + } +} + +func (s *Server) errorHandler(w http.ResponseWriter, err error) { + w.WriteHeader(http.StatusInternalServerError) + s.jsonHandler(w, err) +} + +func (s *Server) jsonHandler(w http.ResponseWriter, data interface{}) { + w.Header().Set("Content-Type", "application/json") + err := s.jsonMarshaller.NewEncoder(w).Encode(data) + if err != nil { + s.logger.Error(err, "failed to encode data for http response") + } +} + +// GetAllTargetsByJob is a relatively expensive call that is usually only used for debugging purposes. +func GetAllTargetsByJob(allocator allocation.Allocator, job string) map[string]collectorJSON { + displayData := make(map[string]collectorJSON) + for _, col := range allocator.Collectors() { + items := allocator.GetTargetsForCollectorAndJob(col.Name, job) + displayData[col.Name] = collectorJSON{Link: fmt.Sprintf("/jobs/%s/targets?collector_id=%s", url.QueryEscape(job), col.Name), Jobs: items} + } + return displayData +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/server/server_test.go b/cmd/amazon-cloudwatch-agent-target-allocator/server/server_test.go new file mode 100644 index 000000000..2f41e2798 --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/server/server_test.go @@ -0,0 +1,947 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package server + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "math/big" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + "time" + + "github.com/prometheus/common/config" + "github.com/prometheus/common/model" + promconfig "github.com/prometheus/prometheus/config" + "github.com/prometheus/prometheus/model/relabel" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/allocation" + allocatorconfig "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/config" + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/target" +) + +var ( + logger = logf.Log.WithName("server-unit-tests") + baseLabelSet = model.LabelSet{ + "test_label": "test-value", + } + testJobLabelSetTwo = model.LabelSet{ + "test_label": "test-value2", + } + baseTargetItem = target.NewItem("test-job", "test-url", baseLabelSet, "test-collector") + secondTargetItem = target.NewItem("test-job", "test-url", baseLabelSet, "test-collector") + testJobTargetItemTwo = target.NewItem("test-job", "test-url2", testJobLabelSetTwo, "test-collector2") +) + +func TestServer_LivenessProbeHandler(t *testing.T) { + consistentHashing, _ := allocation.New("consistent-hashing", logger) + listenAddr := ":8080" + s := NewServer(logger, consistentHashing, listenAddr) + request := httptest.NewRequest("GET", "/livez", nil) + w := httptest.NewRecorder() + + s.server.Handler.ServeHTTP(w, request) + result := w.Result() + + assert.Equal(t, http.StatusOK, result.StatusCode) +} + +func TestServer_TargetsHandler(t *testing.T) { + consistentHashing, _ := allocation.New("consistent-hashing", logger) + type args struct { + collector string + job string + cMap map[string]*target.Item + allocator allocation.Allocator + } + type want struct { + items []*target.Item + errString string + } + tests := []struct { + name string + args args + want want + }{ + { + name: "Empty target map", + args: args{ + collector: "test-collector", + job: "test-job", + cMap: map[string]*target.Item{}, + allocator: consistentHashing, + }, + want: want{ + items: []*target.Item{}, + }, + }, + { + name: "Single entry target map", + args: args{ + collector: "test-collector", + job: "test-job", + cMap: map[string]*target.Item{ + baseTargetItem.Hash(): baseTargetItem, + }, + allocator: consistentHashing, + }, + want: want{ + items: []*target.Item{ + { + TargetURL: []string{"test-url"}, + Labels: map[model.LabelName]model.LabelValue{ + "test_label": "test-value", + }, + }, + }, + }, + }, + { + name: "Multiple entry target map", + args: args{ + collector: "test-collector", + job: "test-job", + cMap: map[string]*target.Item{ + baseTargetItem.Hash(): baseTargetItem, + secondTargetItem.Hash(): secondTargetItem, + }, + allocator: consistentHashing, + }, + want: want{ + items: []*target.Item{ + { + TargetURL: []string{"test-url"}, + Labels: map[model.LabelName]model.LabelValue{ + "test_label": "test-value", + }, + }, + }, + }, + }, + { + name: "Multiple entry target map of same job with label merge", + args: args{ + collector: "test-collector", + job: "test-job", + cMap: map[string]*target.Item{ + baseTargetItem.Hash(): baseTargetItem, + testJobTargetItemTwo.Hash(): testJobTargetItemTwo, + }, + allocator: consistentHashing, + }, + want: want{ + items: []*target.Item{ + { + TargetURL: []string{"test-url"}, + Labels: map[model.LabelName]model.LabelValue{ + "test_label": "test-value", + }, + }, + { + TargetURL: []string{"test-url2"}, + Labels: map[model.LabelName]model.LabelValue{ + "test_label": "test-value2", + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + listenAddr := ":8080" + s := NewServer(logger, tt.args.allocator, listenAddr) + tt.args.allocator.SetCollectors(map[string]*allocation.Collector{"test-collector": {Name: "test-collector"}}) + tt.args.allocator.SetTargets(tt.args.cMap) + request := httptest.NewRequest("GET", fmt.Sprintf("/jobs/%s/targets?collector_id=%s", tt.args.job, tt.args.collector), nil) + w := httptest.NewRecorder() + + s.server.Handler.ServeHTTP(w, request) + result := w.Result() + + assert.Equal(t, http.StatusOK, result.StatusCode) + body := result.Body + bodyBytes, err := io.ReadAll(body) + assert.NoError(t, err) + if len(tt.want.errString) != 0 { + assert.EqualError(t, err, tt.want.errString) + return + } + var itemResponse []*target.Item + err = json.Unmarshal(bodyBytes, &itemResponse) + assert.NoError(t, err) + assert.ElementsMatch(t, tt.want.items, itemResponse) + }) + } +} + +func TestServer_ScrapeConfigsHandler(t *testing.T) { + svrConfig := allocatorconfig.HTTPSServerConfig{} + tlsConfig, _ := svrConfig.NewTLSConfig(context.TODO()) + tests := []struct { + description string + scrapeConfigs map[string]*promconfig.ScrapeConfig + expectedCode int + expectedBody []byte + serverOptions []Option + }{ + { + description: "nil scrape config", + scrapeConfigs: nil, + expectedCode: http.StatusOK, + expectedBody: []byte("{}"), + }, + { + description: "empty scrape config", + scrapeConfigs: map[string]*promconfig.ScrapeConfig{}, + expectedCode: http.StatusOK, + expectedBody: []byte("{}"), + }, + { + description: "single entry", + scrapeConfigs: map[string]*promconfig.ScrapeConfig{ + "serviceMonitor/testapp/testapp/0": { + JobName: "serviceMonitor/testapp/testapp/0", + HonorTimestamps: true, + ScrapeInterval: model.Duration(30 * time.Second), + ScrapeTimeout: model.Duration(30 * time.Second), + MetricsPath: "/metrics", + Scheme: "http", + HTTPClientConfig: config.HTTPClientConfig{ + FollowRedirects: true, + }, + RelabelConfigs: []*relabel.Config{ + { + SourceLabels: model.LabelNames{model.LabelName("job")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "__tmp_prometheus_job_name", + Replacement: "$$1", + Action: relabel.Replace, + }, + }, + }, + }, + expectedCode: http.StatusOK, + }, + { + description: "multiple entries", + scrapeConfigs: map[string]*promconfig.ScrapeConfig{ + "serviceMonitor/testapp/testapp/0": { + JobName: "serviceMonitor/testapp/testapp/0", + HonorTimestamps: true, + ScrapeInterval: model.Duration(30 * time.Second), + ScrapeTimeout: model.Duration(30 * time.Second), + MetricsPath: "/metrics", + Scheme: "http", + HTTPClientConfig: config.HTTPClientConfig{ + FollowRedirects: true, + }, + RelabelConfigs: []*relabel.Config{ + { + SourceLabels: model.LabelNames{model.LabelName("job")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "__tmp_prometheus_job_name", + Replacement: "$$1", + Action: relabel.Replace, + }, + { + SourceLabels: model.LabelNames{ + model.LabelName("__meta_kubernetes_service_label_app_kubernetes_io_name"), + model.LabelName("__meta_kubernetes_service_labelpresent_app_kubernetes_io_name"), + }, + Separator: ";", + Regex: relabel.MustNewRegexp("(testapp);true"), + Replacement: "$$1", + Action: relabel.Keep, + }, + { + SourceLabels: model.LabelNames{model.LabelName("__meta_kubernetes_endpoint_port_name")}, + Separator: ";", + Regex: relabel.MustNewRegexp("http"), + Replacement: "$$1", + Action: relabel.Keep, + }, + { + SourceLabels: model.LabelNames{model.LabelName("__meta_kubernetes_namespace")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "namespace", + Replacement: "$$1", + Action: relabel.Replace, + }, + { + SourceLabels: model.LabelNames{model.LabelName("__meta_kubernetes_service_name")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "service", + Replacement: "$$1", + Action: relabel.Replace, + }, + { + SourceLabels: model.LabelNames{model.LabelName("__meta_kubernetes_pod_name")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "pod", + Replacement: "$$1", + Action: relabel.Replace, + }, + { + SourceLabels: model.LabelNames{model.LabelName("__meta_kubernetes_pod_container_name")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "container", + Replacement: "$$1", + Action: relabel.Replace, + }, + }, + }, + "serviceMonitor/testapp/testapp1/0": { + JobName: "serviceMonitor/testapp/testapp1/0", + HonorTimestamps: true, + ScrapeInterval: model.Duration(5 * time.Minute), + ScrapeTimeout: model.Duration(10 * time.Second), + MetricsPath: "/v2/metrics", + Scheme: "http", + HTTPClientConfig: config.HTTPClientConfig{ + FollowRedirects: true, + }, + RelabelConfigs: []*relabel.Config{ + { + SourceLabels: model.LabelNames{model.LabelName("job")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "__tmp_prometheus_job_name", + Replacement: "$$1", + Action: relabel.Replace, + }, + { + SourceLabels: model.LabelNames{ + model.LabelName("__meta_kubernetes_service_label_app_kubernetes_io_name"), + model.LabelName("__meta_kubernetes_service_labelpresent_app_kubernetes_io_name"), + }, + Separator: ";", + Regex: relabel.MustNewRegexp("(testapp);true"), + Replacement: "$$1", + Action: relabel.Keep, + }, + { + SourceLabels: model.LabelNames{model.LabelName("__meta_kubernetes_endpoint_port_name")}, + Separator: ";", + Regex: relabel.MustNewRegexp("http"), + Replacement: "$$1", + Action: relabel.Keep, + }, + { + SourceLabels: model.LabelNames{model.LabelName("__meta_kubernetes_namespace")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "namespace", + Replacement: "$$1", + Action: relabel.Replace, + }, + { + SourceLabels: model.LabelNames{model.LabelName("__meta_kubernetes_service_name")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "service", + Replacement: "$$1", + Action: relabel.Replace, + }, + { + SourceLabels: model.LabelNames{model.LabelName("__meta_kubernetes_pod_name")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "pod", + Replacement: "$$1", + Action: relabel.Replace, + }, + { + SourceLabels: model.LabelNames{model.LabelName("__meta_kubernetes_pod_container_name")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "container", + Replacement: "$$1", + Action: relabel.Replace, + }, + }, + }, + "serviceMonitor/testapp/testapp2/0": { + JobName: "serviceMonitor/testapp/testapp2/0", + HonorTimestamps: true, + ScrapeInterval: model.Duration(30 * time.Minute), + ScrapeTimeout: model.Duration(2 * time.Minute), + MetricsPath: "/metrics", + Scheme: "http", + HTTPClientConfig: config.HTTPClientConfig{ + FollowRedirects: true, + }, + RelabelConfigs: []*relabel.Config{ + { + SourceLabels: model.LabelNames{model.LabelName("job")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "__tmp_prometheus_job_name", + Replacement: "$$1", + Action: relabel.Replace, + }, + { + SourceLabels: model.LabelNames{ + model.LabelName("__meta_kubernetes_service_label_app_kubernetes_io_name"), + model.LabelName("__meta_kubernetes_service_labelpresent_app_kubernetes_io_name"), + }, + Separator: ";", + Regex: relabel.MustNewRegexp("(testapp);true"), + Replacement: "$$1", + Action: relabel.Keep, + }, + { + SourceLabels: model.LabelNames{model.LabelName("__meta_kubernetes_endpoint_port_name")}, + Separator: ";", + Regex: relabel.MustNewRegexp("http"), + Replacement: "$$1", + Action: relabel.Keep, + }, + { + SourceLabels: model.LabelNames{model.LabelName("__meta_kubernetes_namespace")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "namespace", + Replacement: "$$1", + Action: relabel.Replace, + }, + { + SourceLabels: model.LabelNames{model.LabelName("__meta_kubernetes_service_name")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "service", + Replacement: "$$1", + Action: relabel.Replace, + }, + { + SourceLabels: model.LabelNames{model.LabelName("__meta_kubernetes_pod_name")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "pod", + Replacement: "$$1", + Action: relabel.Replace, + }, + { + SourceLabels: model.LabelNames{model.LabelName("__meta_kubernetes_pod_container_name")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "container", + Replacement: "$$1", + Action: relabel.Replace, + }, + }, + }, + }, + expectedCode: http.StatusOK, + }, + { + description: "https secret handling", + scrapeConfigs: map[string]*promconfig.ScrapeConfig{ + "serviceMonitor/testapp/testapp3/0": { + JobName: "serviceMonitor/testapp/testapp3/0", + HonorTimestamps: true, + ScrapeInterval: model.Duration(30 * time.Second), + ScrapeTimeout: model.Duration(30 * time.Second), + MetricsPath: "/metrics", + Scheme: "http", + HTTPClientConfig: config.HTTPClientConfig{ + FollowRedirects: true, + BasicAuth: &config.BasicAuth{ + Username: "test", + Password: "P@$$w0rd1!?", + }, + }, + }, + }, + expectedCode: http.StatusOK, + serverOptions: []Option{ + WithTLSConfig(tlsConfig, ""), + }, + }, + { + description: "http secret handling", + scrapeConfigs: map[string]*promconfig.ScrapeConfig{ + "serviceMonitor/testapp/testapp3/0": { + JobName: "serviceMonitor/testapp/testapp3/0", + HonorTimestamps: true, + ScrapeInterval: model.Duration(30 * time.Second), + ScrapeTimeout: model.Duration(30 * time.Second), + MetricsPath: "/metrics", + Scheme: "http", + HTTPClientConfig: config.HTTPClientConfig{ + FollowRedirects: true, + BasicAuth: &config.BasicAuth{ + Username: "test", + Password: "P@$$w0rd1!?", + }, + }, + }, + }, + expectedCode: http.StatusOK, + }, + } + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + listenAddr := ":8080" + s := NewServer(logger, nil, listenAddr, tc.serverOptions...) + assert.NoError(t, s.UpdateScrapeConfigResponse(tc.scrapeConfigs)) + + request := httptest.NewRequest("GET", "/scrape_configs", nil) + w := httptest.NewRecorder() + + if s.httpsServer != nil { + request.TLS = &tls.ConnectionState{} + s.httpsServer.Handler.ServeHTTP(w, request) + } else { + s.server.Handler.ServeHTTP(w, request) + } + result := w.Result() + + assert.Equal(t, tc.expectedCode, result.StatusCode) + bodyBytes, err := io.ReadAll(result.Body) + require.NoError(t, err) + if tc.expectedBody != nil { + assert.Equal(t, tc.expectedBody, bodyBytes) + return + } + scrapeConfigs := map[string]*promconfig.ScrapeConfig{} + err = yaml.Unmarshal(bodyBytes, scrapeConfigs) + require.NoError(t, err) + + for _, c := range scrapeConfigs { + if s.httpsServer == nil && c.HTTPClientConfig.BasicAuth != nil { + assert.Equal(t, c.HTTPClientConfig.BasicAuth.Password, config.Secret("")) + } + } + + for _, c := range tc.scrapeConfigs { + if s.httpsServer == nil && c.HTTPClientConfig.BasicAuth != nil { + c.HTTPClientConfig.BasicAuth.Password = "" + } + } + + assert.Equal(t, tc.scrapeConfigs, scrapeConfigs) + }) + } +} + +func TestServer_JobHandler(t *testing.T) { + tests := []struct { + description string + targetItems map[string]*target.Item + expectedCode int + expectedJobs map[string]target.LinkJSON + }{ + { + description: "nil jobs", + targetItems: nil, + expectedCode: http.StatusOK, + expectedJobs: make(map[string]target.LinkJSON), + }, + { + description: "empty jobs", + targetItems: map[string]*target.Item{}, + expectedCode: http.StatusOK, + expectedJobs: make(map[string]target.LinkJSON), + }, + { + description: "one job", + targetItems: map[string]*target.Item{ + "targetitem": target.NewItem("job1", "", model.LabelSet{}, ""), + }, + expectedCode: http.StatusOK, + expectedJobs: map[string]target.LinkJSON{ + "job1": newLink("job1"), + }, + }, + { + description: "multiple jobs", + targetItems: map[string]*target.Item{ + "a": target.NewItem("job1", "", model.LabelSet{}, ""), + "b": target.NewItem("job2", "", model.LabelSet{}, ""), + "c": target.NewItem("job3", "", model.LabelSet{}, ""), + "d": target.NewItem("job3", "", model.LabelSet{}, ""), + "e": target.NewItem("job3", "", model.LabelSet{}, "")}, + expectedCode: http.StatusOK, + expectedJobs: map[string]target.LinkJSON{ + "job1": newLink("job1"), + "job2": newLink("job2"), + "job3": newLink("job3"), + }, + }, + } + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + listenAddr := ":8080" + a := &mockAllocator{targetItems: tc.targetItems} + s := NewServer(logger, a, listenAddr) + request := httptest.NewRequest("GET", "/jobs", nil) + w := httptest.NewRecorder() + + s.server.Handler.ServeHTTP(w, request) + result := w.Result() + + assert.Equal(t, tc.expectedCode, result.StatusCode) + bodyBytes, err := io.ReadAll(result.Body) + require.NoError(t, err) + jobs := map[string]target.LinkJSON{} + err = json.Unmarshal(bodyBytes, &jobs) + require.NoError(t, err) + assert.Equal(t, tc.expectedJobs, jobs) + }) + } +} + +func TestServer_Readiness(t *testing.T) { + tests := []struct { + description string + scrapeConfigs map[string]*promconfig.ScrapeConfig + expectedCode int + expectedBody []byte + }{ + { + description: "nil scrape config", + scrapeConfigs: nil, + expectedCode: http.StatusServiceUnavailable, + }, + { + description: "empty scrape config", + scrapeConfigs: map[string]*promconfig.ScrapeConfig{}, + expectedCode: http.StatusOK, + }, + { + description: "single entry", + scrapeConfigs: map[string]*promconfig.ScrapeConfig{ + "serviceMonitor/testapp/testapp/0": { + JobName: "serviceMonitor/testapp/testapp/0", + HonorTimestamps: true, + ScrapeInterval: model.Duration(30 * time.Second), + ScrapeTimeout: model.Duration(30 * time.Second), + MetricsPath: "/metrics", + Scheme: "http", + HTTPClientConfig: config.HTTPClientConfig{ + FollowRedirects: true, + }, + RelabelConfigs: []*relabel.Config{ + { + SourceLabels: model.LabelNames{model.LabelName("job")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "__tmp_prometheus_job_name", + Replacement: "$$1", + Action: relabel.Replace, + }, + }, + }, + }, + expectedCode: http.StatusOK, + }, + } + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + listenAddr := ":8080" + s := NewServer(logger, nil, listenAddr) + if tc.scrapeConfigs != nil { + assert.NoError(t, s.UpdateScrapeConfigResponse(tc.scrapeConfigs)) + } + + request := httptest.NewRequest("GET", "/readyz", nil) + w := httptest.NewRecorder() + + s.server.Handler.ServeHTTP(w, request) + result := w.Result() + + assert.Equal(t, tc.expectedCode, result.StatusCode) + }) + } +} + +func TestServer_ValidCAonTLS(t *testing.T) { + listenAddr := ":8443" + server, clientTlsConfig, err := createTestTLSServer(listenAddr) + assert.NoError(t, err) + go func() { + assert.ErrorIs(t, server.StartHTTPS(), http.ErrServerClosed) + }() + time.Sleep(100 * time.Millisecond) // wait for server to launch + defer func() { + err := server.ShutdownHTTPS(context.Background()) + if err != nil { + assert.NoError(t, err) + } + }() + tests := []struct { + description string + endpoint string + expectedCode int + }{ + { + description: "with tls test for scrape config", + endpoint: "scrape_configs", + expectedCode: http.StatusOK, + }, + { + description: "with tls test for jobs", + endpoint: "jobs", + expectedCode: http.StatusOK, + }, + } + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + // Create a custom HTTP client with TLS transport + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: clientTlsConfig, + }, + } + + // Make the GET request + request, err := client.Get(fmt.Sprintf("https://localhost%s/%s", listenAddr, tc.endpoint)) + + // Verify if a certificate verification error occurred + require.NoError(t, err) + + // Only check the status code if there was no error + if err == nil { + assert.Equal(t, tc.expectedCode, request.StatusCode) + } else { + t.Log(err) + } + }) + } +} + +func TestServer_MissingCAonTLS(t *testing.T) { + listenAddr := ":8443" + server, _, err := createTestTLSServer(listenAddr) + assert.NoError(t, err) + go func() { + assert.ErrorIs(t, server.StartHTTPS(), http.ErrServerClosed) + }() + time.Sleep(100 * time.Millisecond) // wait for server to launch + defer func() { + err := server.ShutdownHTTPS(context.Background()) + if err != nil { + assert.NoError(t, err) + } + }() + tests := []struct { + description string + endpoint string + expectedCode int + }{ + { + description: "no tls test for scrape config", + endpoint: "scrape_configs", + expectedCode: http.StatusBadRequest, + }, + { + description: "no tls test for jobs", + endpoint: "jobs", + expectedCode: http.StatusBadRequest, + }, + } + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + request, err := http.Get(fmt.Sprintf("https://localhost%s/%s", listenAddr, tc.endpoint)) + + // Verify if a certificate verification error occurred + require.Error(t, err) + + // Only check the status code if there was no error + if err == nil { + assert.Equal(t, tc.expectedCode, request.StatusCode) + } + }) + } +} + +func TestServer_HTTPOnTLS(t *testing.T) { + listenAddr := ":8443" + server, _, err := createTestTLSServer(listenAddr) + assert.NoError(t, err) + go func() { + assert.ErrorIs(t, server.StartHTTPS(), http.ErrServerClosed) + }() + time.Sleep(100 * time.Millisecond) // wait for server to launch + + defer func(s *Server, ctx context.Context) { + err := s.Shutdown(ctx) + if err != nil { + assert.NoError(t, err) + } + }(server, context.Background()) + tests := []struct { + description string + endpoint string + expectedCode int + }{ + { + description: "no tls test for scrape config", + endpoint: "scrape_configs", + expectedCode: http.StatusBadRequest, + }, + { + description: "no tls test for jobs", + endpoint: "jobs", + expectedCode: http.StatusBadRequest, + }, + } + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + request, err := http.Get(fmt.Sprintf("http://localhost%s/%s", listenAddr, tc.endpoint)) + + // Only check the status code if there was no error + if err == nil { + assert.Equal(t, tc.expectedCode, request.StatusCode) + } + }) + } +} + +func createTestTLSServer(listenAddr string) (*Server, *tls.Config, error) { + //testing using this function replicates customer environment + svrConfig := allocatorconfig.HTTPSServerConfig{} + caBundle, caCert, caKey, err := generateTestingCerts() + if err != nil { + return nil, nil, err + } + svrConfig.TLSKeyFilePath = caKey + svrConfig.TLSCertFilePath = caCert + tlsConfig, err := svrConfig.NewTLSConfig(context.TODO()) + if err != nil { + return nil, nil, err + } + //generate ca bundle + bundle, err := readCABundle(caBundle) + if err != nil { + return nil, nil, err + } + httpOptions := []Option{} + httpOptions = append(httpOptions, WithTLSConfig(tlsConfig, listenAddr)) + + allocator := &mockAllocator{targetItems: map[string]*target.Item{ + "a": target.NewItem("job1", "", model.LabelSet{}, ""), + }} + + return NewServer(logger, allocator, listenAddr, httpOptions...), bundle, nil +} + +func newLink(jobName string) target.LinkJSON { + return target.LinkJSON{Link: fmt.Sprintf("/jobs/%s/targets", url.QueryEscape(jobName))} +} + +func readCABundle(caBundlePath string) (*tls.Config, error) { + // Load the CA bundle + caCert, err := os.ReadFile(caBundlePath) + if err != nil { + return nil, fmt.Errorf("failed to read CA bundle: %w", err) + } + + // Create a CA pool and add the CA certificate(s) + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(caCert) { + return nil, fmt.Errorf("failed to add CA certificates to pool") + } + + // Set up TLS configuration with the CA pool + tlsConfig := &tls.Config{ + RootCAs: caCertPool, + } + return tlsConfig, nil +} + +func generateTestingCerts() (caBundlePath, caCertPath, caKeyPath string, err error) { + // Generate private key + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return "", "", "", fmt.Errorf("error generating private key: %w", err) + } + + // Set up certificate template + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: "localhost", + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(365 * 24 * time.Hour), // 1 year validity + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{ + x509.ExtKeyUsageServerAuth, + }, + DNSNames: []string{"localhost"}, + } + + // Self-sign the certificate + certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey) + if err != nil { + return "", "", "", fmt.Errorf("error creating certificate: %w", err) + } + + // Create temporary files + tempDir := os.TempDir() + + caCertFile, err := os.CreateTemp(tempDir, "ca-cert-*.crt") + if err != nil { + return "", "", "", fmt.Errorf("error creating temp CA cert file: %w", err) + } + defer caCertFile.Close() + + caKeyFile, err := os.CreateTemp(tempDir, "ca-key-*.key") + if err != nil { + return "", "", "", fmt.Errorf("error creating temp CA key file: %w", err) + } + defer caKeyFile.Close() + + caBundleFile, err := os.CreateTemp(tempDir, "ca-bundle-*.crt") + if err != nil { + return "", "", "", fmt.Errorf("error creating temp CA bundle file: %w", err) + } + defer caBundleFile.Close() + + // Write the private key to the key file + privateKeyBytes, err := x509.MarshalECPrivateKey(privateKey) + if err != nil { + return "", "", "", fmt.Errorf("error writing private key: %w", err) + } + err = pem.Encode(caKeyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: privateKeyBytes}) + if err != nil { + return "", "", "", fmt.Errorf("error writing private key: %w", err) + } + + // Write the certificate to the certificate and bundle files + certPEM := &pem.Block{Type: "CERTIFICATE", Bytes: certBytes} + if err = pem.Encode(caCertFile, certPEM); err != nil { + return "", "", "", fmt.Errorf("error writing certificate: %w", err) + } + if err = pem.Encode(caBundleFile, certPEM); err != nil { + return "", "", "", fmt.Errorf("error writing bundle certificate: %w", err) + } + + // Return the file paths + return caBundleFile.Name(), caCertFile.Name(), caKeyFile.Name(), nil +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/target/discovery.go b/cmd/amazon-cloudwatch-agent-target-allocator/target/discovery.go new file mode 100644 index 000000000..5f9924c34 --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/target/discovery.go @@ -0,0 +1,145 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package target + +import ( + "hash" + "hash/fnv" + + "github.com/go-logr/logr" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/config" + "github.com/prometheus/prometheus/discovery" + "github.com/prometheus/prometheus/model/relabel" + "gopkg.in/yaml.v3" + + allocatorWatcher "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/watcher" +) + +var ( + targetsDiscovered = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "amazon_cloudwatch_agent_allocator_targets", + Help: "Number of targets discovered.", + }, []string{"job_name"}) +) + +type Discoverer struct { + log logr.Logger + manager *discovery.Manager + close chan struct{} + configsMap map[allocatorWatcher.EventSource]*config.Config + hook discoveryHook + scrapeConfigsHash hash.Hash + scrapeConfigsUpdater scrapeConfigsUpdater +} + +type discoveryHook interface { + SetConfig(map[string][]*relabel.Config) +} + +type scrapeConfigsUpdater interface { + UpdateScrapeConfigResponse(map[string]*config.ScrapeConfig) error +} + +func NewDiscoverer(log logr.Logger, manager *discovery.Manager, hook discoveryHook, scrapeConfigsUpdater scrapeConfigsUpdater) *Discoverer { + return &Discoverer{ + log: log, + manager: manager, + close: make(chan struct{}), + configsMap: make(map[allocatorWatcher.EventSource]*config.Config), + hook: hook, + scrapeConfigsUpdater: scrapeConfigsUpdater, + } +} + +func (m *Discoverer) ApplyConfig(source allocatorWatcher.EventSource, cfg *config.Config) error { + if cfg == nil { + m.log.Info("Service Discovery got empty Prometheus config", "source", source.String()) + return nil + } + m.configsMap[source] = cfg + jobToScrapeConfig := make(map[string]*config.ScrapeConfig) + + discoveryCfg := make(map[string]discovery.Configs) + relabelCfg := make(map[string][]*relabel.Config) + + for _, value := range m.configsMap { + for _, scrapeConfig := range value.ScrapeConfigs { + jobToScrapeConfig[scrapeConfig.JobName] = scrapeConfig + discoveryCfg[scrapeConfig.JobName] = scrapeConfig.ServiceDiscoveryConfigs + relabelCfg[scrapeConfig.JobName] = scrapeConfig.RelabelConfigs + } + } + + hash, err := getScrapeConfigHash(jobToScrapeConfig) + if err != nil { + return err + } + // If the hash has changed, updated stored hash and send the new config. + // Otherwise skip updating scrape configs. + if m.scrapeConfigsUpdater != nil && m.scrapeConfigsHash != hash { + err := m.scrapeConfigsUpdater.UpdateScrapeConfigResponse(jobToScrapeConfig) + if err != nil { + return err + } + + m.scrapeConfigsHash = hash + } + + if m.hook != nil { + m.hook.SetConfig(relabelCfg) + } + return m.manager.ApplyConfig(discoveryCfg) +} + +func (m *Discoverer) Watch(fn func(targets map[string]*Item)) error { + for { + select { + case <-m.close: + m.log.Info("Service Discovery watch event stopped: discovery manager closed") + return nil + case tsets := <-m.manager.SyncCh(): + targets := map[string]*Item{} + + for jobName, tgs := range tsets { + var count float64 = 0 + for _, tg := range tgs { + for _, t := range tg.Targets { + count++ + item := NewItem(jobName, string(t[model.AddressLabel]), t.Merge(tg.Labels), "") + targets[item.Hash()] = item + } + } + targetsDiscovered.WithLabelValues(jobName).Set(count) + } + fn(targets) + } + } +} + +func (m *Discoverer) Close() { + close(m.close) +} + +// Calculate a hash for a scrape config map. +// This is done by marshaling to YAML because it's the most straightforward and doesn't run into problems with unexported fields. +func getScrapeConfigHash(jobToScrapeConfig map[string]*config.ScrapeConfig) (hash.Hash64, error) { + var err error + hash := fnv.New64() + yamlEncoder := yaml.NewEncoder(hash) + for jobName, scrapeConfig := range jobToScrapeConfig { + _, err = hash.Write([]byte(jobName)) + if err != nil { + return nil, err + } + err = yamlEncoder.Encode(scrapeConfig) + if err != nil { + return nil, err + } + } + yamlEncoder.Close() + return hash, err +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/target/discovery_test.go b/cmd/amazon-cloudwatch-agent-target-allocator/target/discovery_test.go new file mode 100644 index 000000000..bd75d7155 --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/target/discovery_test.go @@ -0,0 +1,402 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package target + +import ( + "context" + "errors" + "hash" + "sort" + "testing" + "time" + + gokitlog "github.com/go-kit/log" + commonconfig "github.com/prometheus/common/config" + "github.com/prometheus/common/model" + promconfig "github.com/prometheus/prometheus/config" + "github.com/prometheus/prometheus/discovery" + "github.com/prometheus/prometheus/model/relabel" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + ctrl "sigs.k8s.io/controller-runtime" + + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/config" + allocatorWatcher "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/watcher" +) + +func TestDiscovery(t *testing.T) { + type args struct { + file string + } + tests := []struct { + name string + args args + want []string + }{ + { + name: "base case", + args: args{ + file: "./testdata/test.yaml", + }, + want: []string{"prom.domain:9001", "prom.domain:9002", "prom.domain:9003", "prom.domain:8001", "promfile.domain:1001", "promfile.domain:3000"}, + }, + { + name: "update", + args: args{ + file: "./testdata/test_update.yaml", + }, + want: []string{"prom.domain:9004", "prom.domain:9005", "promfile.domain:1001", "promfile.domain:3000"}, + }, + } + scu := &mockScrapeConfigUpdater{} + ctx, cancelFunc := context.WithCancel(context.Background()) + d := discovery.NewManager(ctx, gokitlog.NewNopLogger()) + manager := NewDiscoverer(ctrl.Log.WithName("test"), d, nil, scu) + + defer func() { manager.Close() }() + defer cancelFunc() + + results := make(chan []string) + go func() { + err := d.Run() + assert.Error(t, err) + }() + go func() { + err := manager.Watch(func(targets map[string]*Item) { + var result []string + for _, t := range targets { + result = append(result, t.TargetURL[0]) + } + results <- result + }) + assert.NoError(t, err) + }() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := config.CreateDefaultConfig() + err := config.LoadFromFile(tt.args.file, &cfg) + assert.NoError(t, err) + assert.True(t, len(cfg.PromConfig.ScrapeConfigs) > 0) + err = manager.ApplyConfig(allocatorWatcher.EventSourcePrometheusCR, cfg.PromConfig) + assert.NoError(t, err) + + gotTargets := <-results + sort.Strings(gotTargets) + sort.Strings(tt.want) + assert.Equal(t, tt.want, gotTargets) + + // check the updated scrape configs + expectedScrapeConfigs := map[string]*promconfig.ScrapeConfig{} + for _, scrapeConfig := range cfg.PromConfig.ScrapeConfigs { + expectedScrapeConfigs[scrapeConfig.JobName] = scrapeConfig + } + assert.Equal(t, expectedScrapeConfigs, scu.mockCfg) + }) + } +} + +func TestDiscovery_ScrapeConfigHashing(t *testing.T) { + // these tests are meant to be run sequentially in this order, to test + // that hashing doesn't cause us to send the wrong information. + tests := []struct { + description string + cfg *promconfig.Config + expectErr bool + }{ + { + description: "base config", + cfg: &promconfig.Config{ + ScrapeConfigs: []*promconfig.ScrapeConfig{ + { + JobName: "serviceMonitor/testapp/testapp/0", + HonorTimestamps: true, + ScrapeInterval: model.Duration(30 * time.Second), + ScrapeTimeout: model.Duration(30 * time.Second), + MetricsPath: "/metrics", + Scheme: "http", + HTTPClientConfig: commonconfig.HTTPClientConfig{ + FollowRedirects: true, + }, + RelabelConfigs: []*relabel.Config{ + { + SourceLabels: model.LabelNames{model.LabelName("job")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "__tmp_prometheus_job_name", + Replacement: "$$1", + Action: relabel.Replace, + }, + }, + }, + }, + }, + }, + { + description: "different bool", + cfg: &promconfig.Config{ + ScrapeConfigs: []*promconfig.ScrapeConfig{ + { + JobName: "serviceMonitor/testapp/testapp/0", + HonorTimestamps: false, + ScrapeInterval: model.Duration(30 * time.Second), + ScrapeTimeout: model.Duration(30 * time.Second), + MetricsPath: "/metrics", + Scheme: "http", + HTTPClientConfig: commonconfig.HTTPClientConfig{ + FollowRedirects: true, + }, + RelabelConfigs: []*relabel.Config{ + { + SourceLabels: model.LabelNames{model.LabelName("job")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "__tmp_prometheus_job_name", + Replacement: "$$1", + Action: relabel.Replace, + }, + }, + }, + }, + }, + }, + { + description: "different job name", + cfg: &promconfig.Config{ + ScrapeConfigs: []*promconfig.ScrapeConfig{ + { + JobName: "serviceMonitor/testapp/testapp/1", + HonorTimestamps: false, + ScrapeInterval: model.Duration(30 * time.Second), + ScrapeTimeout: model.Duration(30 * time.Second), + MetricsPath: "/metrics", + Scheme: "http", + HTTPClientConfig: commonconfig.HTTPClientConfig{ + FollowRedirects: true, + }, + RelabelConfigs: []*relabel.Config{ + { + SourceLabels: model.LabelNames{model.LabelName("job")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "__tmp_prometheus_job_name", + Replacement: "$$1", + Action: relabel.Replace, + }, + }, + }, + }, + }, + }, + { + description: "different key", + cfg: &promconfig.Config{ + ScrapeConfigs: []*promconfig.ScrapeConfig{ + { + JobName: "serviceMonitor/testapp/testapp/1", + HonorTimestamps: false, + ScrapeInterval: model.Duration(30 * time.Second), + ScrapeTimeout: model.Duration(30 * time.Second), + MetricsPath: "/metrics", + Scheme: "http", + HTTPClientConfig: commonconfig.HTTPClientConfig{ + FollowRedirects: true, + }, + RelabelConfigs: []*relabel.Config{ + { + SourceLabels: model.LabelNames{model.LabelName("job")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "__tmp_prometheus_job_name", + Replacement: "$$1", + Action: relabel.Replace, + }, + }, + }, + }, + }, + }, + { + description: "unset scrape interval", + cfg: &promconfig.Config{ + ScrapeConfigs: []*promconfig.ScrapeConfig{ + { + JobName: "serviceMonitor/testapp/testapp/1", + HonorTimestamps: false, + ScrapeTimeout: model.Duration(30 * time.Second), + MetricsPath: "/metrics", + Scheme: "http", + HTTPClientConfig: commonconfig.HTTPClientConfig{ + FollowRedirects: true, + }, + RelabelConfigs: []*relabel.Config{ + { + SourceLabels: model.LabelNames{model.LabelName("job")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "__tmp_prometheus_job_name", + Replacement: "$$1", + Action: relabel.Replace, + }, + }, + }, + }, + }, + }, + { + description: "different regex", + cfg: &promconfig.Config{ + ScrapeConfigs: []*promconfig.ScrapeConfig{ + { + JobName: "serviceMonitor/testapp/testapp/1", + HonorTimestamps: false, + ScrapeTimeout: model.Duration(30 * time.Second), + MetricsPath: "/metrics", + Scheme: "http", + HTTPClientConfig: commonconfig.HTTPClientConfig{ + FollowRedirects: true, + }, + RelabelConfigs: []*relabel.Config{ + { + SourceLabels: model.LabelNames{model.LabelName("job")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.+)"), + TargetLabel: "__tmp_prometheus_job_name", + Replacement: "$$1", + Action: relabel.Replace, + }, + }, + }, + }, + }, + }, + { + description: "mock error on update - no hash update", + cfg: &promconfig.Config{ + ScrapeConfigs: []*promconfig.ScrapeConfig{ + { + JobName: "error", + }, + }, + }, + expectErr: true, + }, + } + var ( + lastValidHash hash.Hash + expectedConfig map[string]*promconfig.ScrapeConfig + lastValidConfig map[string]*promconfig.ScrapeConfig + ) + + scu := &mockScrapeConfigUpdater{} + ctx := context.Background() + d := discovery.NewManager(ctx, gokitlog.NewNopLogger()) + manager := NewDiscoverer(ctrl.Log.WithName("test"), d, nil, scu) + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + err := manager.ApplyConfig(allocatorWatcher.EventSourcePrometheusCR, tc.cfg) + if !tc.expectErr { + expectedConfig = make(map[string]*promconfig.ScrapeConfig) + for _, value := range manager.configsMap { + for _, scrapeConfig := range value.ScrapeConfigs { + expectedConfig[scrapeConfig.JobName] = scrapeConfig + } + } + assert.NoError(t, err) + assert.NotZero(t, manager.scrapeConfigsHash) + // Assert that scrape configs in manager are correctly + // reflected in the scrape job updater. + assert.Equal(t, expectedConfig, scu.mockCfg) + + lastValidHash = manager.scrapeConfigsHash + lastValidConfig = expectedConfig + } else { + // In case of error, assert that we retain the last + // known valid config. + assert.Error(t, err) + assert.Equal(t, lastValidHash, manager.scrapeConfigsHash) + assert.Equal(t, lastValidConfig, scu.mockCfg) + } + + }) + } +} + +func TestDiscovery_NoConfig(t *testing.T) { + scu := &mockScrapeConfigUpdater{mockCfg: map[string]*promconfig.ScrapeConfig{}} + ctx, cancelFunc := context.WithCancel(context.Background()) + d := discovery.NewManager(ctx, gokitlog.NewNopLogger()) + manager := NewDiscoverer(ctrl.Log.WithName("test"), d, nil, scu) + defer close(manager.close) + defer cancelFunc() + + go func() { + err := d.Run() + assert.Error(t, err) + }() + // check the updated scrape configs + expectedScrapeConfigs := map[string]*promconfig.ScrapeConfig{} + assert.Equal(t, expectedScrapeConfigs, scu.mockCfg) +} + +func BenchmarkApplyScrapeConfig(b *testing.B) { + numConfigs := 1000 + scrapeConfig := promconfig.ScrapeConfig{ + JobName: "serviceMonitor/testapp/testapp/0", + HonorTimestamps: true, + ScrapeInterval: model.Duration(30 * time.Second), + ScrapeTimeout: model.Duration(30 * time.Second), + MetricsPath: "/metrics", + Scheme: "http", + HTTPClientConfig: commonconfig.HTTPClientConfig{ + FollowRedirects: true, + }, + RelabelConfigs: []*relabel.Config{ + { + SourceLabels: model.LabelNames{model.LabelName("job")}, + Separator: ";", + Regex: relabel.MustNewRegexp("(.*)"), + TargetLabel: "__tmp_prometheus_job_name", + Replacement: "$$1", + Action: relabel.Replace, + }, + }, + } + cfg := &promconfig.Config{ + ScrapeConfigs: make([]*promconfig.ScrapeConfig, numConfigs), + } + + for i := 0; i < numConfigs; i++ { + cfg.ScrapeConfigs[i] = &scrapeConfig + } + + scu := &mockScrapeConfigUpdater{} + ctx := context.Background() + d := discovery.NewManager(ctx, gokitlog.NewNopLogger()) + manager := NewDiscoverer(ctrl.Log.WithName("test"), d, nil, scu) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + err := manager.ApplyConfig(allocatorWatcher.EventSourcePrometheusCR, cfg) + require.NoError(b, err) + } +} + +var _ scrapeConfigsUpdater = &mockScrapeConfigUpdater{} + +// mockScrapeConfigUpdater is a mock implementation of the scrapeConfigsUpdater. +// If a job with name "error" is provided to the UpdateScrapeConfigResponse, +// it will return an error for testing purposes. +type mockScrapeConfigUpdater struct { + mockCfg map[string]*promconfig.ScrapeConfig +} + +func (m *mockScrapeConfigUpdater) UpdateScrapeConfigResponse(cfg map[string]*promconfig.ScrapeConfig) error { + if _, ok := cfg["error"]; ok { + return errors.New("error") + } + + m.mockCfg = cfg + return nil +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/target/target.go b/cmd/amazon-cloudwatch-agent-target-allocator/target/target.go new file mode 100644 index 000000000..fb49e96ec --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/target/target.go @@ -0,0 +1,44 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package target + +import ( + "fmt" + "net/url" + + "github.com/prometheus/common/model" +) + +// LinkJSON This package contains common structs and methods that relate to scrape targets. +type LinkJSON struct { + Link string `json:"_link"` +} + +type Item struct { + JobName string `json:"-"` + Link LinkJSON `json:"-"` + TargetURL []string `json:"targets"` + Labels model.LabelSet `json:"labels"` + CollectorName string `json:"-"` + hash string +} + +func (t *Item) Hash() string { + return t.hash +} + +// NewItem Creates a new target item. +// INVARIANTS: +// * Item fields must not be modified after creation. +// * Item should only be made via its constructor, never directly. +func NewItem(jobName string, targetURL string, label model.LabelSet, collectorName string) *Item { + return &Item{ + JobName: jobName, + Link: LinkJSON{Link: fmt.Sprintf("/jobs/%s/targets", url.QueryEscape(jobName))}, + hash: jobName + targetURL + label.Fingerprint().String(), + TargetURL: []string{targetURL}, + Labels: label, + CollectorName: collectorName, + } +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/target/testdata/test.yaml b/cmd/amazon-cloudwatch-agent-target-allocator/target/testdata/test.yaml new file mode 100644 index 000000000..f9582037f --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/target/testdata/test.yaml @@ -0,0 +1,17 @@ +label_selector: + app.kubernetes.io/instance: default.test + app.kubernetes.io/managed-by: amazon-cloudwatch-agent-operator +config: + scrape_configs: + - job_name: prometheus + + file_sd_configs: + - files: + - ../config/testdata/file_sd_test.json + static_configs: + - targets: ["prom.domain:9001", "prom.domain:9002", "prom.domain:9003"] + labels: + my: label + - job_name: prometheus2 + static_configs: + - targets: ["prom.domain:8001"] diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/target/testdata/test_update.yaml b/cmd/amazon-cloudwatch-agent-target-allocator/target/testdata/test_update.yaml new file mode 100644 index 000000000..826b0b410 --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/target/testdata/test_update.yaml @@ -0,0 +1,14 @@ +label_selector: + app.kubernetes.io/instance: default.test + app.kubernetes.io/managed-by: amazon-cloudwatch-agent-operator +config: + scrape_configs: + - job_name: prometheus + + file_sd_configs: + - files: + - ../config/testdata/file_sd_test.json + static_configs: + - targets: ["prom.domain:9004", "prom.domain:9005"] + labels: + my: other-label diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/watcher/file.go b/cmd/amazon-cloudwatch-agent-target-allocator/watcher/file.go new file mode 100644 index 000000000..d3fa77271 --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/watcher/file.go @@ -0,0 +1,79 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package watcher + +import ( + "context" + "path/filepath" + + "github.com/fsnotify/fsnotify" + "github.com/go-logr/logr" + promconfig "github.com/prometheus/prometheus/config" + + "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/config" +) + +var _ Watcher = &FileWatcher{} + +type FileWatcher struct { + logger logr.Logger + configFilePath string + watcher *fsnotify.Watcher + closer chan bool +} + +func NewFileWatcher(logger logr.Logger, configFilePath string) (*FileWatcher, error) { + fileWatcher, err := fsnotify.NewWatcher() + if err != nil { + logger.Error(err, "Can't start the watcher") + return &FileWatcher{}, err + } + + return &FileWatcher{ + logger: logger, + configFilePath: configFilePath, + watcher: fileWatcher, + closer: make(chan bool), + }, nil +} + +func (f *FileWatcher) LoadConfig(_ context.Context) (*promconfig.Config, error) { + cfg := config.CreateDefaultConfig() + err := config.LoadFromFile(f.configFilePath, &cfg) + if err != nil { + f.logger.Error(err, "Unable to load configuration") + return nil, err + } + return cfg.PromConfig, nil +} + +func (f *FileWatcher) Watch(upstreamEvents chan Event, upstreamErrors chan error) error { + err := f.watcher.Add(filepath.Dir(f.configFilePath)) + if err != nil { + return err + } + + for { + select { + case <-f.closer: + return nil + case fileEvent := <-f.watcher.Events: + // Using Op.Has as per this doc - https://github.com/fsnotify/fsnotify/blob/9342b6df577910c6eac718dc62845d8c95f8548b/fsnotify.go#L30 + if fileEvent.Op.Has(fsnotify.Create) || fileEvent.Op.Has(fsnotify.Write) { + f.logger.Info("File change detected", "event", fileEvent.Op.String()) + upstreamEvents <- Event{ + Source: EventSourceConfigMap, + Watcher: Watcher(f), + } + } + case err := <-f.watcher.Errors: + upstreamErrors <- err + } + } +} + +func (f *FileWatcher) Close() error { + f.closer <- true + return f.watcher.Close() +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/watcher/promOperator.go b/cmd/amazon-cloudwatch-agent-target-allocator/watcher/promOperator.go new file mode 100644 index 000000000..b85e809fd --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/watcher/promOperator.go @@ -0,0 +1,359 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package watcher + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/go-logr/logr" + monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + promv1alpha1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1alpha1" + "github.com/prometheus-operator/prometheus-operator/pkg/assets" + monitoringclient "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned" + "github.com/prometheus-operator/prometheus-operator/pkg/informers" + "github.com/prometheus-operator/prometheus-operator/pkg/prometheus" + promconfig "github.com/prometheus/prometheus/config" + kubeDiscovery "github.com/prometheus/prometheus/discovery/kubernetes" + "gopkg.in/yaml.v2" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/cache" + + allocatorconfig "github.com/aws/amazon-cloudwatch-agent-operator/cmd/amazon-cloudwatch-agent-target-allocator/config" +) + +const minEventInterval = time.Second * 5 + +func NewPrometheusCRWatcher(logger logr.Logger, cfg allocatorconfig.Config) (*PrometheusCRWatcher, error) { + mClient, err := monitoringclient.NewForConfig(cfg.ClusterConfig) + if err != nil { + return nil, err + } + + clientset, err := kubernetes.NewForConfig(cfg.ClusterConfig) + if err != nil { + return nil, err + } + + factory := informers.NewMonitoringInformerFactories(map[string]struct{}{v1.NamespaceAll: {}}, map[string]struct{}{}, mClient, allocatorconfig.DefaultResyncTime, nil) //TODO decide what strategy to use regarding namespaces + + monitoringInformers, err := getInformers(factory) + if err != nil { + return nil, err + } + + // TODO: We should make these durations configurable + prom := &monitoringv1.Prometheus{ + Spec: monitoringv1.PrometheusSpec{ + CommonPrometheusFields: monitoringv1.CommonPrometheusFields{ + ScrapeInterval: monitoringv1.Duration(cfg.PrometheusCR.ScrapeInterval.String()), + }, + }, + } + + promOperatorLogger := level.NewFilter(log.NewLogfmtLogger(os.Stderr), level.AllowWarn()) + generator, err := prometheus.NewConfigGenerator(promOperatorLogger, prom, true) + + if err != nil { + return nil, err + } + + servMonSelector := getSelector(cfg.ServiceMonitorSelector) + + podMonSelector := getSelector(cfg.PodMonitorSelector) + + return &PrometheusCRWatcher{ + logger: logger, + kubeMonitoringClient: mClient, + k8sClient: clientset, + informers: monitoringInformers, + stopChannel: make(chan struct{}), + eventInterval: minEventInterval, + configGenerator: generator, + kubeConfigPath: cfg.KubeConfigFilePath, + serviceMonitorSelector: servMonSelector, + podMonitorSelector: podMonSelector, + }, nil +} + +type PrometheusCRWatcher struct { + logger logr.Logger + kubeMonitoringClient monitoringclient.Interface + k8sClient kubernetes.Interface + informers map[string]*informers.ForResource + eventInterval time.Duration + stopChannel chan struct{} + configGenerator *prometheus.ConfigGenerator + kubeConfigPath string + + serviceMonitorSelector labels.Selector + podMonitorSelector labels.Selector +} + +func getSelector(s map[string]string) labels.Selector { + if s == nil { + return labels.NewSelector() + } + return labels.SelectorFromSet(s) +} + +// getInformers returns a map of informers for the given resources. +func getInformers(factory informers.FactoriesForNamespaces) (map[string]*informers.ForResource, error) { + serviceMonitorInformers, err := informers.NewInformersForResource(factory, monitoringv1.SchemeGroupVersion.WithResource(monitoringv1.ServiceMonitorName)) + if err != nil { + return nil, err + } + + podMonitorInformers, err := informers.NewInformersForResource(factory, monitoringv1.SchemeGroupVersion.WithResource(monitoringv1.PodMonitorName)) + if err != nil { + return nil, err + } + + return map[string]*informers.ForResource{ + monitoringv1.ServiceMonitorName: serviceMonitorInformers, + monitoringv1.PodMonitorName: podMonitorInformers, + }, nil +} + +// Watch wrapped informers and wait for an initial sync. +func (w *PrometheusCRWatcher) Watch(upstreamEvents chan Event, upstreamErrors chan error) error { + success := true + // this channel needs to be buffered because notifications are asynchronous and neither producers nor consumers wait + notifyEvents := make(chan struct{}, 1) + + for name, resource := range w.informers { + resource.Start(w.stopChannel) + + if ok := cache.WaitForNamedCacheSync(name, w.stopChannel, resource.HasSynced); !ok { + success = false + } + + // only send an event notification if there isn't one already + resource.AddEventHandler(cache.ResourceEventHandlerFuncs{ + // these functions only write to the notification channel if it's empty to avoid blocking + // if scrape config updates are being rate-limited + AddFunc: func(obj interface{}) { + select { + case notifyEvents <- struct{}{}: + default: + } + }, + UpdateFunc: func(oldObj, newObj interface{}) { + select { + case notifyEvents <- struct{}{}: + default: + } + }, + DeleteFunc: func(obj interface{}) { + select { + case notifyEvents <- struct{}{}: + default: + } + }, + }) + } + if !success { + return fmt.Errorf("failed to sync cache") + } + + // limit the rate of outgoing events + w.rateLimitedEventSender(upstreamEvents, notifyEvents) + + <-w.stopChannel + return nil +} + +// rateLimitedEventSender sends events to the upstreamEvents channel whenever it gets a notification on the notifyEvents channel, +// but not more frequently than once per w.eventPeriod. +func (w *PrometheusCRWatcher) rateLimitedEventSender(upstreamEvents chan Event, notifyEvents chan struct{}) { + ticker := time.NewTicker(w.eventInterval) + defer ticker.Stop() + + event := Event{ + Source: EventSourcePrometheusCR, + Watcher: Watcher(w), + } + + for { + select { + case <-w.stopChannel: + return + case <-ticker.C: // throttle events to avoid excessive updates + select { + case <-notifyEvents: + select { + case upstreamEvents <- event: + default: // put the notification back in the queue if we can't send it upstream + select { + case notifyEvents <- struct{}{}: + default: + } + } + default: + } + } + } +} + +func (w *PrometheusCRWatcher) Close() error { + close(w.stopChannel) + return nil +} + +func (w *PrometheusCRWatcher) LoadConfig(ctx context.Context) (*promconfig.Config, error) { + store := assets.NewStore(w.k8sClient.CoreV1(), w.k8sClient.CoreV1()) + serviceMonitorInstances := make(map[string]*monitoringv1.ServiceMonitor) + smRetrieveErr := w.informers[monitoringv1.ServiceMonitorName].ListAll(w.serviceMonitorSelector, func(sm interface{}) { + monitor := sm.(*monitoringv1.ServiceMonitor) + key, _ := cache.DeletionHandlingMetaNamespaceKeyFunc(monitor) + w.addStoreAssetsForServiceMonitor(ctx, monitor.Name, monitor.Namespace, monitor.Spec.Endpoints, store) + serviceMonitorInstances[key] = monitor + }) + if smRetrieveErr != nil { + return nil, smRetrieveErr + } + + podMonitorInstances := make(map[string]*monitoringv1.PodMonitor) + pmRetrieveErr := w.informers[monitoringv1.PodMonitorName].ListAll(w.podMonitorSelector, func(pm interface{}) { + monitor := pm.(*monitoringv1.PodMonitor) + key, _ := cache.DeletionHandlingMetaNamespaceKeyFunc(monitor) + w.addStoreAssetsForPodMonitor(ctx, monitor.Name, monitor.Namespace, monitor.Spec.PodMetricsEndpoints, store) + podMonitorInstances[key] = monitor + }) + if pmRetrieveErr != nil { + return nil, pmRetrieveErr + } + + generatedConfig, err := w.configGenerator.GenerateServerConfiguration( + ctx, + "30s", + "", + nil, + nil, + monitoringv1.TSDBSpec{}, + nil, + nil, + serviceMonitorInstances, + podMonitorInstances, + map[string]*monitoringv1.Probe{}, + map[string]*promv1alpha1.ScrapeConfig{}, + store, + nil, + nil, + nil, + []string{}) + if err != nil { + return nil, err + } + + promCfg := &promconfig.Config{} + unmarshalErr := yaml.Unmarshal(generatedConfig, promCfg) + if unmarshalErr != nil { + return nil, unmarshalErr + } + + // set kubeconfig path to service discovery configs, else kubernetes_sd will always attempt in-cluster + // authentication even if running with a detected kubeconfig + for _, scrapeConfig := range promCfg.ScrapeConfigs { + for _, serviceDiscoveryConfig := range scrapeConfig.ServiceDiscoveryConfigs { + if serviceDiscoveryConfig.Name() == "kubernetes" { + sdConfig := interface{}(serviceDiscoveryConfig).(*kubeDiscovery.SDConfig) + sdConfig.KubeConfig = w.kubeConfigPath + } + } + } + return promCfg, nil +} + +// addStoreAssetsForServiceMonitor adds authentication / authorization related information to the assets store, +// based on the service monitor and endpoints specs. +// This code borrows from +// https://github.com/prometheus-operator/prometheus-operator/blob/06b5c4189f3f72737766d86103d049115c3aff48/pkg/prometheus/resource_selector.go#L73. +func (w *PrometheusCRWatcher) addStoreAssetsForServiceMonitor( + ctx context.Context, + smName, smNamespace string, + endps []monitoringv1.Endpoint, + store *assets.Store, +) { + var err error + for i, endp := range endps { + objKey := fmt.Sprintf("serviceMonitor/%s/%s/%d", smNamespace, smName, i) + + if err = store.AddBearerToken(ctx, smNamespace, endp.BearerTokenSecret, objKey); err != nil { + break + } + + if err = store.AddBasicAuth(ctx, smNamespace, endp.BasicAuth, objKey); err != nil { + break + } + + if endp.TLSConfig != nil { + if err = store.AddTLSConfig(ctx, smNamespace, endp.TLSConfig); err != nil { + break + } + } + + if err = store.AddOAuth2(ctx, smNamespace, endp.OAuth2, objKey); err != nil { + break + } + + smAuthKey := fmt.Sprintf("serviceMonitor/auth/%s/%s/%d", smNamespace, smName, i) + if err = store.AddSafeAuthorizationCredentials(ctx, smNamespace, endp.Authorization, smAuthKey); err != nil { + break + } + } + + if err != nil { + w.logger.Error(err, "Failed to obtain credentials for a ServiceMonitor", "serviceMonitor", smName) + } +} + +// addStoreAssetsForServiceMonitor adds authentication / authorization related information to the assets store, +// based on the service monitor and pod metrics endpoints specs. +// This code borrows from +// https://github.com/prometheus-operator/prometheus-operator/blob/06b5c4189f3f72737766d86103d049115c3aff48/pkg/prometheus/resource_selector.go#L314. +func (w *PrometheusCRWatcher) addStoreAssetsForPodMonitor( + ctx context.Context, + pmName, pmNamespace string, + podMetricsEndps []monitoringv1.PodMetricsEndpoint, + store *assets.Store, +) { + var err error + for i, endp := range podMetricsEndps { + objKey := fmt.Sprintf("podMonitor/%s/%s/%d", pmNamespace, pmName, i) + + if err = store.AddBearerToken(ctx, pmNamespace, &endp.BearerTokenSecret, objKey); err != nil { + break + } + + if err = store.AddBasicAuth(ctx, pmNamespace, endp.BasicAuth, objKey); err != nil { + break + } + + if endp.TLSConfig != nil { + if err = store.AddSafeTLSConfig(ctx, pmNamespace, &endp.TLSConfig.SafeTLSConfig); err != nil { + break + } + } + + if err = store.AddOAuth2(ctx, pmNamespace, endp.OAuth2, objKey); err != nil { + break + } + + smAuthKey := fmt.Sprintf("podMonitor/auth/%s/%s/%d", pmNamespace, pmName, i) + if err = store.AddSafeAuthorizationCredentials(ctx, pmNamespace, endp.Authorization, smAuthKey); err != nil { + break + } + } + + if err != nil { + w.logger.Error(err, "Failed to obtain credentials for a PodMonitor", "podMonitor", pmName) + } +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/watcher/promOperator_test.go b/cmd/amazon-cloudwatch-agent-target-allocator/watcher/promOperator_test.go new file mode 100644 index 000000000..17a4ed46a --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/watcher/promOperator_test.go @@ -0,0 +1,416 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package watcher + +import ( + "context" + "testing" + "time" + + "github.com/go-kit/log" + monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + fakemonitoringclient "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned/fake" + "github.com/prometheus-operator/prometheus-operator/pkg/informers" + "github.com/prometheus-operator/prometheus-operator/pkg/prometheus" + "github.com/prometheus/common/config" + "github.com/prometheus/common/model" + promconfig "github.com/prometheus/prometheus/config" + "github.com/prometheus/prometheus/discovery" + kubeDiscovery "github.com/prometheus/prometheus/discovery/kubernetes" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/tools/cache" +) + +func TestLoadConfig(t *testing.T) { + tests := []struct { + name string + serviceMonitor *monitoringv1.ServiceMonitor + podMonitor *monitoringv1.PodMonitor + want *promconfig.Config + wantErr bool + }{ + { + name: "simple test", + serviceMonitor: &monitoringv1.ServiceMonitor{ + ObjectMeta: metav1.ObjectMeta{ + Name: "simple", + Namespace: "test", + }, + Spec: monitoringv1.ServiceMonitorSpec{ + JobLabel: "test", + Endpoints: []monitoringv1.Endpoint{ + { + Port: "web", + }, + }, + }, + }, + podMonitor: &monitoringv1.PodMonitor{ + ObjectMeta: metav1.ObjectMeta{ + Name: "simple", + Namespace: "test", + }, + Spec: monitoringv1.PodMonitorSpec{ + JobLabel: "test", + PodMetricsEndpoints: []monitoringv1.PodMetricsEndpoint{ + { + Port: "web", + }, + }, + }, + }, + want: &promconfig.Config{ + ScrapeConfigs: []*promconfig.ScrapeConfig{ + { + JobName: "serviceMonitor/test/simple/0", + ScrapeInterval: model.Duration(30 * time.Second), + ScrapeTimeout: model.Duration(10 * time.Second), + HonorTimestamps: true, + HonorLabels: false, + Scheme: "http", + MetricsPath: "/metrics", + ServiceDiscoveryConfigs: []discovery.Config{ + &kubeDiscovery.SDConfig{ + Role: "endpointslice", + NamespaceDiscovery: kubeDiscovery.NamespaceDiscovery{ + Names: []string{"test"}, + IncludeOwnNamespace: false, + }, + HTTPClientConfig: config.DefaultHTTPClientConfig, + }, + }, + HTTPClientConfig: config.DefaultHTTPClientConfig, + }, + { + JobName: "podMonitor/test/simple/0", + ScrapeInterval: model.Duration(30 * time.Second), + ScrapeTimeout: model.Duration(10 * time.Second), + HonorTimestamps: true, + HonorLabels: false, + Scheme: "http", + MetricsPath: "/metrics", + ServiceDiscoveryConfigs: []discovery.Config{ + &kubeDiscovery.SDConfig{ + Role: "pod", + NamespaceDiscovery: kubeDiscovery.NamespaceDiscovery{ + Names: []string{"test"}, + IncludeOwnNamespace: false, + }, + HTTPClientConfig: config.DefaultHTTPClientConfig, + }, + }, + HTTPClientConfig: config.DefaultHTTPClientConfig, + }, + }, + }, + }, + { + name: "basic auth (serviceMonitor)", + serviceMonitor: &monitoringv1.ServiceMonitor{ + ObjectMeta: metav1.ObjectMeta{ + Name: "auth", + Namespace: "test", + }, + Spec: monitoringv1.ServiceMonitorSpec{ + JobLabel: "auth", + Endpoints: []monitoringv1.Endpoint{ + { + Port: "web", + BasicAuth: &monitoringv1.BasicAuth{ + Username: v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{ + Name: "basic-auth", + }, + Key: "username", + }, + Password: v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{ + Name: "basic-auth", + }, + Key: "password", + }, + }, + }, + }, + Selector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "auth", + }, + }, + }, + }, + want: &promconfig.Config{ + GlobalConfig: promconfig.GlobalConfig{}, + ScrapeConfigs: []*promconfig.ScrapeConfig{ + { + JobName: "serviceMonitor/test/auth/0", + ScrapeInterval: model.Duration(30 * time.Second), + ScrapeTimeout: model.Duration(10 * time.Second), + HonorTimestamps: true, + HonorLabels: false, + Scheme: "http", + MetricsPath: "/metrics", + ServiceDiscoveryConfigs: []discovery.Config{ + &kubeDiscovery.SDConfig{ + Role: "endpointslice", + NamespaceDiscovery: kubeDiscovery.NamespaceDiscovery{ + Names: []string{"test"}, + IncludeOwnNamespace: false, + }, + HTTPClientConfig: config.DefaultHTTPClientConfig, + }, + }, + HTTPClientConfig: config.HTTPClientConfig{ + FollowRedirects: true, + EnableHTTP2: true, + BasicAuth: &config.BasicAuth{ + Username: "admin", + Password: "password", + }, + }, + }, + }, + }, + }, + { + name: "bearer token (podMonitor)", + podMonitor: &monitoringv1.PodMonitor{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bearer", + Namespace: "test", + }, + Spec: monitoringv1.PodMonitorSpec{ + JobLabel: "bearer", + PodMetricsEndpoints: []monitoringv1.PodMetricsEndpoint{ + { + Port: "web", + BearerTokenSecret: v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{ + Name: "bearer", + }, + Key: "token", + }, + }, + }, + }, + }, + want: &promconfig.Config{ + GlobalConfig: promconfig.GlobalConfig{}, + ScrapeConfigs: []*promconfig.ScrapeConfig{ + { + JobName: "podMonitor/test/bearer/0", + ScrapeInterval: model.Duration(30 * time.Second), + ScrapeTimeout: model.Duration(10 * time.Second), + HonorTimestamps: true, + HonorLabels: false, + Scheme: "http", + MetricsPath: "/metrics", + ServiceDiscoveryConfigs: []discovery.Config{ + &kubeDiscovery.SDConfig{ + Role: "pod", + NamespaceDiscovery: kubeDiscovery.NamespaceDiscovery{ + Names: []string{"test"}, + IncludeOwnNamespace: false, + }, + HTTPClientConfig: config.DefaultHTTPClientConfig, + }, + }, + HTTPClientConfig: config.HTTPClientConfig{ + FollowRedirects: true, + EnableHTTP2: true, + Authorization: &config.Authorization{ + Type: "Bearer", + Credentials: "bearer-token", + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := getTestPrometheusCRWatcher(t, tt.serviceMonitor, tt.podMonitor) + for _, informer := range w.informers { + // Start informers in order to populate cache. + informer.Start(w.stopChannel) + } + + // Wait for informers to sync. + for _, informer := range w.informers { + for !informer.HasSynced() { + time.Sleep(50 * time.Millisecond) + } + } + + got, err := w.LoadConfig(context.Background()) + assert.NoError(t, err) + + sanitizeScrapeConfigsForTest(got.ScrapeConfigs) + assert.Equal(t, tt.want.ScrapeConfigs, got.ScrapeConfigs) + }) + } +} + +func TestRateLimit(t *testing.T) { + var err error + serviceMonitor := &monitoringv1.ServiceMonitor{ + ObjectMeta: metav1.ObjectMeta{ + Name: "simple", + Namespace: "test", + }, + Spec: monitoringv1.ServiceMonitorSpec{ + JobLabel: "test", + Endpoints: []monitoringv1.Endpoint{ + { + Port: "web", + }, + }, + }, + } + events := make(chan Event, 1) + eventInterval := 5 * time.Millisecond + + w := getTestPrometheusCRWatcher(t, nil, nil) + defer w.Close() + w.eventInterval = eventInterval + + go func() { + watchErr := w.Watch(events, make(chan error)) + require.NoError(t, watchErr) + }() + // we don't have a simple way to wait for the watch to actually add event handlers to the informer, + // instead, we just update a ServiceMonitor periodically and wait until we get a notification + _, err = w.kubeMonitoringClient.MonitoringV1().ServiceMonitors("test").Create(context.Background(), serviceMonitor, metav1.CreateOptions{}) + require.NoError(t, err) + + // wait for cache sync first + for _, informer := range w.informers { + success := cache.WaitForCacheSync(w.stopChannel, informer.HasSynced) + require.True(t, success) + } + + require.Eventually(t, func() bool { + _, createErr := w.kubeMonitoringClient.MonitoringV1().ServiceMonitors("test").Update(context.Background(), serviceMonitor, metav1.UpdateOptions{}) + if createErr != nil { + return false + } + select { + case <-events: + return true + default: + return false + } + }, eventInterval*2, time.Millisecond) + + // it's difficult to measure the rate precisely + // what we do, is send two updates, and then assert that the elapsed time is between eventInterval and 3*eventInterval + startTime := time.Now() + _, err = w.kubeMonitoringClient.MonitoringV1().ServiceMonitors("test").Update(context.Background(), serviceMonitor, metav1.UpdateOptions{}) + require.NoError(t, err) + require.Eventually(t, func() bool { + select { + case <-events: + return true + default: + return false + } + }, eventInterval*2, time.Millisecond) + _, err = w.kubeMonitoringClient.MonitoringV1().ServiceMonitors("test").Update(context.Background(), serviceMonitor, metav1.UpdateOptions{}) + require.NoError(t, err) + require.Eventually(t, func() bool { + select { + case <-events: + return true + default: + return false + } + }, eventInterval*2, time.Millisecond) + elapsedTime := time.Since(startTime) + assert.Less(t, eventInterval, elapsedTime) + assert.GreaterOrEqual(t, eventInterval*3, elapsedTime) + +} + +// getTestPrometheuCRWatcher creates a test instance of PrometheusCRWatcher with fake clients +// and test secrets. +func getTestPrometheusCRWatcher(t *testing.T, sm *monitoringv1.ServiceMonitor, pm *monitoringv1.PodMonitor) *PrometheusCRWatcher { + mClient := fakemonitoringclient.NewSimpleClientset() + if sm != nil { + _, err := mClient.MonitoringV1().ServiceMonitors("test").Create(context.Background(), sm, metav1.CreateOptions{}) + if err != nil { + t.Fatal(t, err) + } + } + if pm != nil { + _, err := mClient.MonitoringV1().PodMonitors("test").Create(context.Background(), pm, metav1.CreateOptions{}) + if err != nil { + t.Fatal(t, err) + } + } + + k8sClient := fake.NewSimpleClientset() + _, err := k8sClient.CoreV1().Secrets("test").Create(context.Background(), &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "basic-auth", + Namespace: "test", + }, + Data: map[string][]byte{"username": []byte("admin"), "password": []byte("password")}, + }, metav1.CreateOptions{}) + if err != nil { + t.Fatal(t, err) + } + _, err = k8sClient.CoreV1().Secrets("test").Create(context.Background(), &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bearer", + Namespace: "test", + }, + Data: map[string][]byte{"token": []byte("bearer-token")}, + }, metav1.CreateOptions{}) + if err != nil { + t.Fatal(t, err) + } + + factory := informers.NewMonitoringInformerFactories(map[string]struct{}{v1.NamespaceAll: {}}, map[string]struct{}{}, mClient, 0, nil) + informers, err := getInformers(factory) + if err != nil { + t.Fatal(t, err) + } + + prom := &monitoringv1.Prometheus{ + Spec: monitoringv1.PrometheusSpec{ + CommonPrometheusFields: monitoringv1.CommonPrometheusFields{ + ScrapeInterval: monitoringv1.Duration("30s"), + }, + }, + } + + generator, err := prometheus.NewConfigGenerator(log.NewNopLogger(), prom, true) + if err != nil { + t.Fatal(t, err) + } + + return &PrometheusCRWatcher{ + kubeMonitoringClient: mClient, + k8sClient: k8sClient, + informers: informers, + configGenerator: generator, + serviceMonitorSelector: getSelector(nil), + podMonitorSelector: getSelector(nil), + stopChannel: make(chan struct{}), + } +} + +// Remove relable configs fields from scrape configs for testing, +// since these are mutated and tested down the line with the hook(s). +func sanitizeScrapeConfigsForTest(scs []*promconfig.ScrapeConfig) { + for _, sc := range scs { + sc.RelabelConfigs = nil + sc.MetricRelabelConfigs = nil + } +} diff --git a/cmd/amazon-cloudwatch-agent-target-allocator/watcher/watcher.go b/cmd/amazon-cloudwatch-agent-target-allocator/watcher/watcher.go new file mode 100644 index 000000000..49b2cef87 --- /dev/null +++ b/cmd/amazon-cloudwatch-agent-target-allocator/watcher/watcher.go @@ -0,0 +1,40 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package watcher + +import ( + "context" + + promconfig "github.com/prometheus/prometheus/config" +) + +type Watcher interface { + // Watch watcher and supply channels which will receive change events + Watch(upstreamEvents chan Event, upstreamErrors chan error) error + LoadConfig(ctx context.Context) (*promconfig.Config, error) + Close() error +} + +type Event struct { + Source EventSource + Watcher Watcher +} + +type EventSource int + +const ( + EventSourceConfigMap EventSource = iota + EventSourcePrometheusCR +) + +var ( + eventSourceToString = map[EventSource]string{ + EventSourceConfigMap: "EventSourceConfigMap", + EventSourcePrometheusCR: "EventSourcePrometheusCR", + } +) + +func (e EventSource) String() string { + return eventSourceToString[e] +} diff --git a/config/crd/bases/cloudwatch.aws.amazon.com_amazoncloudwatchagents.yaml b/config/crd/bases/cloudwatch.aws.amazon.com_amazoncloudwatchagents.yaml index 4acbceebe..bf6c09215 100644 --- a/config/crd/bases/cloudwatch.aws.amazon.com_amazoncloudwatchagents.yaml +++ b/config/crd/bases/cloudwatch.aws.amazon.com_amazoncloudwatchagents.yaml @@ -4805,6 +4805,31 @@ spec: If not specified, the pod priority will be default or zero if there is no default. type: string + prometheus: + description: Prometheus is the raw YAML to be used as the collector's + prometheus configuration. + properties: + config: + description: AnyConfig represent parts of the config. + type: object + x-kubernetes-preserve-unknown-fields: true + report_extra_scrape_metrics: + type: boolean + x-kubernetes-preserve-unknown-fields: true + start_time_metric_regex: + type: string + x-kubernetes-preserve-unknown-fields: true + target_allocator: + description: AnyConfig represent parts of the config. + type: object + x-kubernetes-preserve-unknown-fields: true + trim_metric_suffixes: + type: boolean + x-kubernetes-preserve-unknown-fields: true + use_start_time_metric: + type: boolean + x-kubernetes-preserve-unknown-fields: true + type: object replicas: description: Replicas is the number of pod instances for the underlying OpenTelemetry Collector. Set this if your are not using autoscaling @@ -5046,6 +5071,1552 @@ spec: ServiceAccount indicates the name of an existing service account to use with this instance. When set, the operator will not automatically create a ServiceAccount for the collector. type: string + targetAllocator: + description: TargetAllocator indicates a value which determines whether + to spawn a target allocation resource or not. + properties: + affinity: + description: If specified, indicates the pod's scheduling constraints + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for + the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated with + the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the + corresponding nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + x-kubernetes-map-type: atomic + type: array + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, etc. + as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the anti-affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + Also, MatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `LabelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both MismatchLabelKeys and LabelSelector. + Also, MismatchLabelKeys cannot be set when LabelSelector isn't set. + This is an alpha field and requires enabling MatchLabelKeysInPodAffinity feature gate. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + allocationStrategy: + description: |- + AllocationStrategy determines which strategy the target allocator should use for allocation. + The current option is consistent-hashing. + enum: + - consistent-hashing + type: string + enabled: + description: Enabled indicates whether to use a target allocation + mechanism for Prometheus targets or not. + type: boolean + env: + description: |- + ENV vars to set on the OpenTelemetry TargetAllocator's Pods. These can then in certain cases be + consumed in the config file for the TargetAllocator. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid? + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + filterStrategy: + description: |- + FilterStrategy determines how to filter targets before allocating them among the collectors. + The only current option is relabel-config (drops targets based on prom relabel_config). + Filtering is disabled by default. + type: string + image: + description: Image indicates the container image to use for the + OpenTelemetry TargetAllocator. + type: string + nodeSelector: + additionalProperties: + type: string + description: NodeSelector to schedule OpenTelemetry TargetAllocator + pods. + type: object + prometheusCR: + description: |- + PrometheusCR defines the configuration for the retrieval of PrometheusOperator CRDs ( servicemonitor.monitoring.coreos.com/v1 and podmonitor.monitoring.coreos.com/v1 ) retrieval. + All CR instances which the ServiceAccount has access to will be retrieved. This includes other namespaces. + properties: + enabled: + description: Enabled indicates whether to use a PrometheusOperator + custom resources as targets or not. + type: boolean + podMonitorSelector: + additionalProperties: + type: string + description: |- + PodMonitors to be selected for target discovery. + This is a map of {key,value} pairs. Each {key,value} in the map is going to exactly match a label in a + PodMonitor's meta labels. The requirements are ANDed. + type: object + scrapeInterval: + default: 30s + description: |- + Interval between consecutive scrapes. Equivalent to the same setting on the Prometheus CRD. + + + Default: "30s" + format: duration + type: string + serviceMonitorSelector: + additionalProperties: + type: string + description: |- + ServiceMonitors to be selected for target discovery. + This is a map of {key,value} pairs. Each {key,value} in the map is going to exactly match a label in a + ServiceMonitor's meta labels. The requirements are ANDed. + type: object + type: object + replicas: + description: |- + Replicas is the number of pod instances for the underlying TargetAllocator. This should only be set to a value + other than 1 if a strategy that allows for high availability is chosen. Currently, the only allocation strategy + that can be run in a high availability mode is consistent-hashing. + format: int32 + type: integer + resources: + description: Resources to set on the OpenTelemetryTargetAllocator + containers. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + securityContext: + description: |- + SecurityContext configures the container security context for + the target-allocator. + properties: + fsGroup: + description: |- + A special supplemental group that applies to all containers in a pod. + Some volume types allow the Kubelet to change the ownership of that volume + to be owned by the pod: + + + 1. The owning GID will be the FSGroup + 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) + 3. The permission bits are OR'd with rw-rw---- + + + If unset, the Kubelet will not modify the ownership and permissions of any volume. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + fsGroupChangePolicy: + description: |- + fsGroupChangePolicy defines behavior of changing ownership and permission of the volume + before being exposed inside Pod. This field will only apply to + volume types which support fsGroup based ownership(and permissions). + It will have no effect on ephemeral volume types such as: secret, configmaps + and emptydir. + Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used. + Note that this field cannot be set when spec.os.name is windows. + type: string + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in SecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence + for that container. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to all containers. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in SecurityContext. If set in + both SecurityContext and PodSecurityContext, the value specified in SecurityContext + takes precedence for that container. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by the containers in this pod. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + supplementalGroups: + description: |- + A list of groups applied to the first process run in each container, in addition + to the container's primary GID, the fsGroup (if specified), and group memberships + defined in the container image for the uid of the container process. If unspecified, + no additional groups are added to any container. Note that group memberships + defined in the container image for the uid of the container process are still effective, + even if they are not included in this list. + Note that this field cannot be set when spec.os.name is windows. + items: + format: int64 + type: integer + type: array + sysctls: + description: |- + Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported + sysctls (by the container runtime) might fail to launch. + Note that this field cannot be set when spec.os.name is windows. + items: + description: Sysctl defines a kernel parameter to be set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options within a container's SecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + serviceAccount: + description: |- + ServiceAccount indicates the name of an existing service account to use with this instance. When set, + the operator will not automatically create a ServiceAccount for the TargetAllocator. + type: string + tolerations: + description: |- + Toleration embedded kubernetes pod configuration option, + controls how pods can be scheduled with matching taints + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + topologySpreadConstraints: + description: |- + TopologySpreadConstraints embedded kubernetes pod configuration option, + controls how pods are spread across your cluster among failure-domains + such as regions, zones, nodes, and other user-defined topology domains + https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/ + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: |- + LabelSelector is used to find matching pods. + Pods that match this label selector are counted to determine the number of pods + in their corresponding topology domain. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select the pods over which + spreading will be calculated. The keys are used to lookup values from the + incoming pod labels, those key-value labels are ANDed with labelSelector + to select the group of existing pods over which spreading will be calculated + for the incoming pod. The same key is forbidden to exist in both MatchLabelKeys and LabelSelector. + MatchLabelKeys cannot be set when LabelSelector isn't set. + Keys that don't exist in the incoming pod labels will + be ignored. A null or empty list means only match against labelSelector. + + + This is a beta field and requires the MatchLabelKeysInPodTopologySpread feature gate to be enabled (enabled by default). + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: |- + MaxSkew describes the degree to which pods may be unevenly distributed. + When `whenUnsatisfiable=DoNotSchedule`, it is the maximum permitted difference + between the number of matching pods in the target topology and the global minimum. + The global minimum is the minimum number of matching pods in an eligible domain + or zero if the number of eligible domains is less than MinDomains. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 2/2/1: + In this case, the global minimum is 1. + | zone1 | zone2 | zone3 | + | P P | P P | P | + - if MaxSkew is 1, incoming pod can only be scheduled to zone3 to become 2/2/2; + scheduling it onto zone1(zone2) would make the ActualSkew(3-1) on zone1(zone2) + violate MaxSkew(1). + - if MaxSkew is 2, incoming pod can be scheduled onto any zone. + When `whenUnsatisfiable=ScheduleAnyway`, it is used to give higher precedence + to topologies that satisfy it. + It's a required field. Default value is 1 and 0 is not allowed. + format: int32 + type: integer + minDomains: + description: |- + MinDomains indicates a minimum number of eligible domains. + When the number of eligible domains with matching topology keys is less than minDomains, + Pod Topology Spread treats "global minimum" as 0, and then the calculation of Skew is performed. + And when the number of eligible domains with matching topology keys equals or greater than minDomains, + this value has no effect on scheduling. + As a result, when the number of eligible domains is less than minDomains, + scheduler won't schedule more than maxSkew Pods to those domains. + If value is nil, the constraint behaves as if MinDomains is equal to 1. + Valid values are integers greater than 0. + When value is not nil, WhenUnsatisfiable must be DoNotSchedule. + + + For example, in a 3-zone cluster, MaxSkew is set to 2, MinDomains is set to 5 and pods with the same + labelSelector spread as 2/2/2: + | zone1 | zone2 | zone3 | + | P P | P P | P P | + The number of domains is less than 5(MinDomains), so "global minimum" is treated as 0. + In this situation, new pod with the same labelSelector cannot be scheduled, + because computed skew will be 3(3 - 0) if new Pod is scheduled to any of the three zones, + it will violate MaxSkew. + + + This is a beta field and requires the MinDomainsInPodTopologySpread feature gate to be enabled (enabled by default). + format: int32 + type: integer + nodeAffinityPolicy: + description: |- + NodeAffinityPolicy indicates how we will treat Pod's nodeAffinity/nodeSelector + when calculating pod topology spread skew. Options are: + - Honor: only nodes matching nodeAffinity/nodeSelector are included in the calculations. + - Ignore: nodeAffinity/nodeSelector are ignored. All nodes are included in the calculations. + + + If this value is nil, the behavior is equivalent to the Honor policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string + nodeTaintsPolicy: + description: |- + NodeTaintsPolicy indicates how we will treat node taints when calculating + pod topology spread skew. Options are: + - Honor: nodes without taints, along with tainted nodes for which the incoming pod + has a toleration, are included. + - Ignore: node taints are ignored. All nodes are included. + + + If this value is nil, the behavior is equivalent to the Ignore policy. + This is a beta-level feature default enabled by the NodeInclusionPolicyInPodTopologySpread feature flag. + type: string + topologyKey: + description: |- + TopologyKey is the key of node labels. Nodes that have a label with this key + and identical values are considered to be in the same topology. + We consider each as a "bucket", and try to put balanced number + of pods into each bucket. + We define a domain as a particular instance of a topology. + Also, we define an eligible domain as a domain whose nodes meet the requirements of + nodeAffinityPolicy and nodeTaintsPolicy. + e.g. If TopologyKey is "kubernetes.io/hostname", each Node is a domain of that topology. + And, if TopologyKey is "topology.kubernetes.io/zone", each zone is a domain of that topology. + It's a required field. + type: string + whenUnsatisfiable: + description: |- + WhenUnsatisfiable indicates how to deal with a pod if it doesn't satisfy + the spread constraint. + - DoNotSchedule (default) tells the scheduler not to schedule it. + - ScheduleAnyway tells the scheduler to schedule the pod in any location, + but giving higher precedence to topologies that would help reduce the + skew. + A constraint is considered "Unsatisfiable" for an incoming pod + if and only if every possible node assignment for that pod would violate + "MaxSkew" on some topology. + For example, in a 3-zone cluster, MaxSkew is set to 1, and pods with the same + labelSelector spread as 3/1/1: + | zone1 | zone2 | zone3 | + | P P P | P | P | + If WhenUnsatisfiable is set to DoNotSchedule, incoming pod can only be scheduled + to zone2(zone3) to become 3/2/1(3/1/2) as ActualSkew(2-1) on zone2(zone3) satisfies + MaxSkew(1). In other words, the cluster can still be imbalanced, but scheduler + won't make it *more* imbalanced. + It's a required field. + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + type: object terminationGracePeriodSeconds: description: Duration in seconds the pod needs to terminate gracefully upon probe failure. diff --git a/controllers/amazoncloudwatchagent_controller.go b/controllers/amazoncloudwatchagent_controller.go index 99721c3d9..ca20663c8 100644 --- a/controllers/amazoncloudwatchagent_controller.go +++ b/controllers/amazoncloudwatchagent_controller.go @@ -11,7 +11,9 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -19,6 +21,7 @@ import ( "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" "github.com/aws/amazon-cloudwatch-agent-operator/internal/config" "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/manifestutils" collectorStatus "github.com/aws/amazon-cloudwatch-agent-operator/internal/status/collector" ) @@ -40,6 +43,79 @@ type Params struct { Config config.Config } +func (r *AmazonCloudWatchAgentReconciler) findCloudWatchAgentOwnedObjects(ctx context.Context, owner v1alpha1.AmazonCloudWatchAgent) (map[types.UID]client.Object, error) { + // Define a map to store the owned objects + ownedObjects := make(map[types.UID]client.Object) + selector := manifestutils.SelectorLabelsForAllOperatorManaged(owner.ObjectMeta) + listOps := &client.ListOptions{ + Namespace: owner.Namespace, + LabelSelector: labels.SelectorFromSet(selector), + } + // Define lists for different Kubernetes resources + configMapList := &corev1.ConfigMapList{} + serviceList := &corev1.ServiceList{} + serviceAccountList := &corev1.ServiceAccountList{} + deploymentList := &appsv1.DeploymentList{} + statefulSetList := &appsv1.StatefulSetList{} + daemonSetList := &appsv1.DaemonSetList{} + var err error + + // List ConfigMaps + err = r.List(ctx, configMapList, listOps) + if err != nil { + return nil, err + } + for i := range configMapList.Items { + ownedObjects[configMapList.Items[i].GetUID()] = &configMapList.Items[i] + } + + // List Services + err = r.List(ctx, serviceList, listOps) + if err != nil { + return nil, err + } + for i := range serviceList.Items { + ownedObjects[serviceList.Items[i].GetUID()] = &serviceList.Items[i] + } + // List ServiceAccounts + err = r.List(ctx, serviceAccountList, listOps) + if err != nil { + return nil, err + } + for i := range serviceAccountList.Items { + ownedObjects[serviceAccountList.Items[i].GetUID()] = &serviceAccountList.Items[i] + } + + // List Deployments + err = r.List(ctx, deploymentList, listOps) + if err != nil { + return nil, err + } + for i := range deploymentList.Items { + ownedObjects[deploymentList.Items[i].GetUID()] = &deploymentList.Items[i] + } + + // List StatefulSets + err = r.List(ctx, statefulSetList, listOps) + if err != nil { + return nil, err + } + for i := range statefulSetList.Items { + ownedObjects[statefulSetList.Items[i].GetUID()] = &statefulSetList.Items[i] + } + + // List DaemonSets + err = r.List(ctx, daemonSetList, listOps) + if err != nil { + return nil, err + } + for i := range daemonSetList.Items { + ownedObjects[daemonSetList.Items[i].GetUID()] = &daemonSetList.Items[i] + } + + return ownedObjects, nil + +} func (r *AmazonCloudWatchAgentReconciler) getParams(instance v1alpha1.AmazonCloudWatchAgent) manifests.Params { return manifests.Params{ Config: r.config, @@ -108,7 +184,8 @@ func (r *AmazonCloudWatchAgentReconciler) Reconcile(ctx context.Context, req ctr if buildErr != nil { return ctrl.Result{}, buildErr } - err := reconcileDesiredObjects(ctx, r.Client, log, ¶ms.OtelCol, params.Scheme, desiredObjects...) + + err := reconcileDesiredObjectsWPrune(ctx, r.Client, log, params.OtelCol, params.Scheme, desiredObjects, r.findCloudWatchAgentOwnedObjects) return collectorStatus.HandleReconcileStatus(ctx, log, params, err) } diff --git a/controllers/common.go b/controllers/common.go index c3b844ca1..72f4849d8 100644 --- a/controllers/common.go +++ b/controllers/common.go @@ -13,16 +13,17 @@ import ( rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/util/retry" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" - "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/collector/adapters" - "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/collector" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/collector/adapters" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/targetallocator" ) const ( @@ -44,6 +45,7 @@ func isNamespaceScoped(obj client.Object) bool { func BuildCollector(params manifests.Params) ([]client.Object, error) { builders := []manifests.Builder{ collector.Build, + targetallocator.Build, } var resources []client.Object for _, builder := range builders { @@ -55,10 +57,12 @@ func BuildCollector(params manifests.Params) ([]client.Object, error) { } return resources, nil } - -// reconcileDesiredObjects runs the reconcile process using the mutateFn over the given list of objects. -func reconcileDesiredObjects(ctx context.Context, kubeClient client.Client, logger logr.Logger, owner metav1.Object, scheme *runtime.Scheme, desiredObjects ...client.Object) error { +func reconcileDesiredObjectUIDs(ctx context.Context, kubeClient client.Client, logger logr.Logger, + owner metav1.Object, scheme *runtime.Scheme, desiredObjects ...client.Object) (map[types.UID]client.Object, error) { var errs []error + existingObjectMap := make(map[types.UID]client.Object) + var existingObjectList []client.Object + for _, desired := range desiredObjects { l := logger.WithValues( "object_name", desired.GetName(), @@ -75,6 +79,8 @@ func reconcileDesiredObjects(ctx context.Context, kubeClient client.Client, logg // existing is an object the controller runtime will hydrate for us // we obtain the existing object by deep copying the desired object because it's the most convenient way existing := desired.DeepCopyObject().(client.Object) + existingObjectList = append(existingObjectList, existing) //uid are not assigned yet + mutateFn := manifests.MutateFuncFor(existing, desired) var op controllerutil.OperationResult crudErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { @@ -86,7 +92,7 @@ func reconcileDesiredObjects(ctx context.Context, kubeClient client.Client, logg l.Error(crudErr, "detected immutable field change, trying to delete, new object will be created on next reconcile", "existing", existing.GetName()) delErr := kubeClient.Delete(ctx, existing) if delErr != nil { - return delErr + return nil, delErr } continue } else if crudErr != nil { @@ -98,11 +104,61 @@ func reconcileDesiredObjects(ctx context.Context, kubeClient client.Client, logg l.V(1).Info(fmt.Sprintf("desired has been %s", op)) } if len(errs) > 0 { - return fmt.Errorf("failed to create objects for %s: %w", owner.GetName(), errors.Join(errs...)) + return nil, fmt.Errorf("failed to create objects for %s: %w", owner.GetName(), errors.Join(errs...)) + } + for _, obj := range existingObjectList { + existingObjectMap[obj.GetUID()] = obj + } + return existingObjectMap, nil +} + +func reconcileDesiredObjectsWPrune(ctx context.Context, kubeClient client.Client, logger logr.Logger, owner v1alpha1.AmazonCloudWatchAgent, scheme *runtime.Scheme, + desiredObjects []client.Object, + searchOwnedObjectsFunc func(ctx context.Context, owner v1alpha1.AmazonCloudWatchAgent) (map[types.UID]client.Object, error), +) error { + previouslyOwnedObjects, err := searchOwnedObjectsFunc(ctx, owner) + if err != nil { + return fmt.Errorf("failed to search owned objects: %w", err) + } + + desiredObjectMap, err := reconcileDesiredObjectUIDs(ctx, kubeClient, logger, &owner, scheme, desiredObjects...) + + // Pruning owned objects in the cluster which are not should not be present after the reconciliation. + err = pruneStaleObjects(ctx, kubeClient, logger, previouslyOwnedObjects, desiredObjectMap) + if err != nil { + return fmt.Errorf("failed to prune objects for %s: %w", owner.GetName(), err) } return nil } +// reconcileDesiredObjects runs the reconcile process using the mutateFn over the given list of objects. +func reconcileDesiredObjects(ctx context.Context, kubeClient client.Client, logger logr.Logger, owner metav1.Object, scheme *runtime.Scheme, desiredObjects ...client.Object) error { + _, err := reconcileDesiredObjectUIDs(ctx, kubeClient, logger, owner, scheme, desiredObjects...) + return err +} + +func pruneStaleObjects(ctx context.Context, kubeClient client.Client, logger logr.Logger, previouslyOwnedMap, desiredMap map[types.UID]client.Object) error { + // Pruning owned objects in the cluster which should not be present after the reconciliation. + var pruneErrs []error + for uid, obj := range previouslyOwnedMap { + l := logger.WithValues( + "object_name", obj.GetName(), + "object_kind", obj.GetObjectKind().GroupVersionKind().Kind, + ) + if _, found := desiredMap[uid]; found { + continue + } + + l.Info("pruning unmanaged resource") + err := kubeClient.Delete(ctx, obj) + if err != nil { + l.Error(err, "failed to delete resource") + pruneErrs = append(pruneErrs, err) + } + } + return errors.Join(pruneErrs...) +} + func enabledAcceleratedComputeByAgentConfig(ctx context.Context, c client.Client, log logr.Logger) bool { agentResource := getAmazonCloudWatchAgentResource(ctx, c) // missing feature flag means it's on by default diff --git a/go.mod b/go.mod index feb43eded..b408da5b2 100644 --- a/go.mod +++ b/go.mod @@ -9,23 +9,38 @@ replace github.com/openshift/api v3.9.0+incompatible => github.com/openshift/api require ( dario.cat/mergo v1.0.0 + github.com/buraksezer/consistent v0.10.0 + github.com/cespare/xxhash/v2 v2.2.0 + github.com/fsnotify/fsnotify v1.7.0 + github.com/ghodss/yaml v1.0.0 + github.com/gin-gonic/gin v1.10.0 + github.com/go-kit/log v0.2.1 github.com/go-logr/logr v1.4.1 github.com/google/uuid v1.6.0 + github.com/json-iterator/go v1.1.12 github.com/mitchellh/mapstructure v1.5.0 + github.com/oklog/run v1.1.0 github.com/openshift/api v3.9.0+incompatible + github.com/prometheus-operator/prometheus-operator v0.70.0 github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.70.0 + github.com/prometheus-operator/prometheus-operator/pkg/client v0.70.0 + github.com/prometheus/client_golang v1.19.0 + github.com/prometheus/common v0.53.0 github.com/prometheus/prometheus v0.48.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/collector/confmap v0.101.0 go.opentelemetry.io/collector/featuregate v0.77.0 go.opentelemetry.io/otel v1.21.0 - go.uber.org/zap v1.25.0 - golang.org/x/exp v0.0.0-20231006140011-7918f672742d + go.uber.org/zap v1.27.0 + golang.org/x/exp v0.0.0-20231127185646-65229373498e gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.29.0 k8s.io/apimachinery v0.29.0 k8s.io/client-go v0.29.0 k8s.io/component-base v0.29.0 + k8s.io/klog/v2 v2.110.1 k8s.io/kubectl v0.29.0 k8s.io/utils v0.0.0-20231127182322-b307cd553661 sigs.k8s.io/controller-runtime v0.16.3 @@ -41,39 +56,61 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v2 v2.2.1 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect github.com/armon/go-metrics v0.4.1 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/aws/aws-sdk-go v1.45.25 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 // indirect github.com/containerd/log v0.1.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dennwc/varint v1.0.0 // indirect github.com/digitalocean/godo v1.104.1 // indirect github.com/distribution/reference v0.5.0 // indirect github.com/docker/docker v25.0.6+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/edsrzf/mmap-go v1.1.0 // indirect + github.com/efficientgo/core v1.0.0-rc.2 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/envoyproxy/go-control-plane v0.11.1 // indirect github.com/envoyproxy/protoc-gen-validate v1.0.2 // indirect - github.com/evanphx/json-patch v5.6.0+incompatible // indirect - github.com/evanphx/json-patch/v5 v5.6.0 // indirect + github.com/evanphx/json-patch v5.7.0+incompatible // indirect + github.com/evanphx/json-patch/v5 v5.7.0 // indirect github.com/fatih/color v1.16.0 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/go-kit/log v0.2.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/zapr v1.2.4 // indirect + github.com/go-openapi/analysis v0.21.4 // indirect + github.com/go-openapi/errors v0.20.4 // indirect github.com/go-openapi/jsonpointer v0.20.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/loads v0.21.2 // indirect + github.com/go-openapi/runtime v0.26.0 // indirect + github.com/go-openapi/spec v0.20.9 // indirect + github.com/go-openapi/strfmt v0.21.7 // indirect github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-openapi/validate v0.22.1 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/go-resty/resty/v2 v2.7.0 // indirect + github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect github.com/go-zookeeper/zk v1.0.3 // indirect + github.com/goccy/go-json v0.10.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/go-querystring v1.1.0 // indirect @@ -93,7 +130,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect - github.com/hashicorp/go-version v1.6.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/golang-lru v0.6.0 // indirect github.com/hashicorp/nomad/api v0.0.0-20230721134942-515895c7690c // indirect github.com/hashicorp/serf v0.10.1 // indirect @@ -104,64 +141,79 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.1 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect + github.com/knadh/koanf/providers/confmap v0.1.0 // indirect + github.com/knadh/koanf/v2 v2.1.1 // indirect github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/linode/linodego v1.23.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/metalmatze/signal v0.0.0-20210307161603-1c9aa721a97a // indirect github.com/miekg/dns v1.1.56 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect + github.com/oklog/ulid v1.3.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/ovh/go-ovh v1.4.3 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.17.0 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.44.0 // indirect - github.com/prometheus/procfs v0.11.1 // indirect + github.com/prometheus-community/prom-label-proxy v0.7.0 // indirect + github.com/prometheus/alertmanager v0.26.0 // indirect + github.com/prometheus/client_model v0.6.0 // indirect + github.com/prometheus/common/sigv4 v0.1.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect github.com/scaleway/scaleway-sdk-go v1.0.0-beta.21 // indirect github.com/spf13/cobra v1.7.0 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect github.com/vultr/govultr/v2 v2.17.2 // indirect + go.mongodb.org/mongo-driver v1.12.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect go.opentelemetry.io/otel/metric v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.21.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/goleak v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect + golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.24.0 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/oauth2 v0.13.0 // indirect + golang.org/x/oauth2 v0.18.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/term v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect - golang.org/x/time v0.3.0 // indirect + golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/api v0.147.0 // indirect - google.golang.org/appengine v1.6.7 // indirect + google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20231012201019-e917dd12ba7a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231009173412-8bfb1ae86b6c // indirect google.golang.org/grpc v1.58.3 // indirect - google.golang.org/protobuf v1.33.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.29.0 // indirect - k8s.io/klog/v2 v2.110.1 // indirect - k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect + k8s.io/kube-openapi v0.0.0-20231129212854-f0671cc7e66a // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 4b4041235..5bf9a7e00 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,42 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY= cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 h1:U2rTu3Ef+7w9FHKIAXM6ZyqF3UOWJZ12zIm8zECAFfg= @@ -26,38 +58,62 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg6 github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/DATA-DOG/go-sqlmock v1.4.1/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= +github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go v1.38.35/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.45.25 h1:c4fLlh5sLdK2DCRTY1z0hyuJZU4ygxX8m1FswL6/nF4= github.com/aws/aws-sdk-go v1.45.25/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= -github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/buraksezer/consistent v0.10.0 h1:hqBgz1PvNLC5rkWcEBVAL9dFMBWz6I0VgUCW25rrZlU= +github.com/buraksezer/consistent v0.10.0/go.mod h1:6BrVajWq7wbKZlTOUPs/XVfR8c0maujuPowduSpZqmw= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= @@ -69,6 +125,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= +github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= github.com/digitalocean/godo v1.104.1 h1:SZNxjAsskM/su0YW9P8Wx3gU0W1Z13b6tZlYNpl5BnA= github.com/digitalocean/godo v1.104.1/go.mod h1:VAI/L5YDzMuPRU01lEEUSQ/sp5Z//1HnnFv/RBTEdbg= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= @@ -81,6 +139,10 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= +github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= +github.com/efficientgo/core v1.0.0-rc.2 h1:7j62qHLnrZqO3V3UA0AqOGd5d5aXV3AX6m/NZBHp78I= +github.com/efficientgo/core v1.0.0-rc.2/go.mod h1:FfGdkzWarkuzOlY04VY+bGfb1lWrjaL6x/GLcQ4vJps= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -91,10 +153,10 @@ github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= -github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= -github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= -github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= +github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc= +github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -104,12 +166,25 @@ github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBd github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -121,45 +196,129 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= +github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY= +github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= +github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= +github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.4 h1:unTcVm6PispJsMECE3zWgvG4xTiKda1LIR5rCRWLG6M= +github.com/go-openapi/errors v0.20.4/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/loads v0.21.1/go.mod h1:/DtAMXXneXFjbQMGEtbamCZb+4x7eGwkvZCvBmwUG+g= +github.com/go-openapi/loads v0.21.2 h1:r2a/xFIYeZ4Qd2TnGpWDIQNcP80dIaZgf704za8enro= +github.com/go-openapi/loads v0.21.2/go.mod h1:Jq58Os6SSGz0rzh62ptiu8Z31I+OTHqmULx5e/gJbNw= +github.com/go-openapi/runtime v0.26.0 h1:HYOFtG00FM1UvqrcxbEJg/SwvDRvYLQKGhw2zaQjTcc= +github.com/go-openapi/runtime v0.26.0/go.mod h1:QgRGeZwrUcSHdeh4Ka9Glvo0ug1LC5WyE+EV88plZrQ= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= +github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg= +github.com/go-openapi/strfmt v0.21.1/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= +github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= +github.com/go-openapi/strfmt v0.21.7 h1:rspiXgNWgeUzhjo1YU01do6qsahtJNByjLVbPLNHb8k= +github.com/go-openapi/strfmt v0.21.7/go.mod h1:adeGTkxE44sPyLk0JV235VQAO/ZXUr8KAzYjclFs3ew= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/validate v0.22.1 h1:G+c2ub6q47kfX1sOBLwIQwzBVt8qmOAARyo/9Fqs9NU= +github.com/go-openapi/validate v0.22.1/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-zookeeper/zk v1.0.3 h1:7M2kwOsc//9VeeFiPtf+uSJlVpU66x9Ba5+8XK7/TDg= github.com/go-zookeeper/zk v1.0.3/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw= +github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= +github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= +github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= +github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= +github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= +github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= +github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= +github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= +github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= +github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= +github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= +github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= +github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= +github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= +github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= +github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= @@ -168,9 +327,12 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -180,15 +342,28 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 h1:pUa4ghanp6q4IJHwE9RwLgmVFfReJN+KbQ8ExNEUUoQ= github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.1 h1:SBWmZhjUDRorQxrN0nwzf+AHBxnbFjViHQS4P0yVpmQ= github.com/googleapis/enterprise-certificate-proxy v0.3.1/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/gophercloud/gophercloud v1.7.0 h1:fyJGKh0LBvIZKLvBWvQdIgkaV5yTM3Jh9EYUh+UNCAs= @@ -237,9 +412,10 @@ github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= -github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4= github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= @@ -252,33 +428,57 @@ github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= github.com/hetznercloud/hcloud-go/v2 v2.4.0 h1:MqlAE+w125PLvJRCpAJmEwrIxoVdUdOyuFUhE/Ukbok= github.com/hetznercloud/hcloud-go/v2 v2.4.0/go.mod h1:l7fA5xsncFBzQTyw29/dw5Yr88yEGKKdc6BHf24ONS0= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/ionos-cloud/sdk-go/v6 v6.1.9 h1:Iq3VIXzeEbc8EbButuACgfLMiY5TPVWUPNrF+Vsddo4= github.com/ionos-cloud/sdk-go/v6 v6.1.9/go.mod h1:EzEgRIDxBELvfoa/uBN0kOQaqovLjUWEB7iW4/Q+t4k= github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= +github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.17.1 h1:NE3C767s2ak2bweCZo3+rdP4U/HoyVXLv/X9f2gPS5g= +github.com/klauspost/compress v1.17.1/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU= +github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU= +github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM= +github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -290,10 +490,17 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/linode/linodego v1.23.0 h1:s0ReCZtuN9Z1IoUN9w1RLeYO1dMZUGPwOQ/IBFsBHtU= github.com/linode/linodego v1.23.0/go.mod h1:0U7wj/UQOqBNbKv1FYTXiBUXueR8DY4HvIotwE0ENgg= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -310,22 +517,28 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= +github.com/metalmatze/signal v0.0.0-20210307161603-1c9aa721a97a h1:0usWxe5SGXKQovz3p+BiQ81Jy845xSMu2CWKuXsXuUM= +github.com/metalmatze/signal v0.0.0-20210307161603-1c9aa721a97a/go.mod h1:3OETvrxfELvGsU2RoGGWercfeZ4bCL3+SOwzIWtJH/Q= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE= github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -335,6 +548,7 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= @@ -342,6 +556,11 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= @@ -352,11 +571,16 @@ github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrB github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/openshift/api v0.0.0-20180801171038-322a19404e37 h1:05irGU4HK4IauGGDbsk+ZHrm1wOzMLYjMlfaiqMrBYc= github.com/openshift/api v0.0.0-20180801171038-322a19404e37/go.mod h1:dh9o4Fs58gpFXGSYfnVxGR9PnV53I8TW84pQaJDdGiY= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/ovh/go-ovh v1.4.3 h1:Gs3V823zwTFpzgGLZNI6ILS4rmxZgJwJCz54Er9LwD0= github.com/ovh/go-ovh v1.4.3/go.mod h1:AkPXVtgwB6xlKblMjRKJJmjRp+ogrE7fz2lVgcQY8SY= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -368,30 +592,51 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus-community/prom-label-proxy v0.7.0 h1:1iNHXF7V8z2iOCinEyxKDUHu2jppPAAd6PmBCi3naok= +github.com/prometheus-community/prom-label-proxy v0.7.0/go.mod h1:wR9C/Mwp5aBbiqM6gQ+FZdFRwL8pCzzhsje8lTAx/aA= +github.com/prometheus-operator/prometheus-operator v0.70.0 h1:kMufKWvqJl08Kh0oue3VLmTsowYKKqCQJa7tqXo+DJI= +github.com/prometheus-operator/prometheus-operator v0.70.0/go.mod h1:a/P8ufM+z7gkt4QpWEHWPARs/8bhRBkyZN5pjUl5RMk= github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.70.0 h1:CFTvpkpVP4EXXZuaZuxpikAoma8xVha/IZKMDc9lw+Y= github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.70.0/go.mod h1:npfc20mPOAu7ViOVnATVMbI7PoXvW99EzgJVqkAomIQ= +github.com/prometheus-operator/prometheus-operator/pkg/client v0.70.0 h1:PpdpJDS1MyMSLILG+Y0hgzVQ3tu6qEkRD0gR/UuvSZk= +github.com/prometheus-operator/prometheus-operator/pkg/client v0.70.0/go.mod h1:4I5Rt6iIu95JBYYaDYA+Er+YBfUwIq9Pwh5TEoBmawg= +github.com/prometheus/alertmanager v0.26.0 h1:uOMJWfIwJguc3NaM3appWNbbrh6G/OjvaHMk22aBBYc= +github.com/prometheus/alertmanager v0.26.0/go.mod h1:rVcnARltVjavgVaNnmevxK7kOn7IZavyf0KNgHkbEpU= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= -github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= +github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= +github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= -github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE= +github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= +github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= +github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= -github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/prometheus v0.48.1 h1:CTszphSNTXkuCG6O0IfpKdHcJkvvnAAE1GbELKS+NFk= github.com/prometheus/prometheus v0.48.1/go.mod h1:SRw624aMAxTfryAcP8rOjg4S/sHHaetx2lyJJ2nM83g= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -403,11 +648,16 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg github.com/shoenig/test v0.6.6 h1:Oe8TPH9wAbv++YPNDKJWUnI8Q4PPWCx3UbOfH+FxiMU= github.com/shoenig/test v0.6.6/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -419,22 +669,52 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZQs= github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= +go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= +go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= +go.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE= +go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/collector/confmap v0.101.0 h1:pGXZRBKnZqys1HgNECGSi8Pec5RBGa9vVCfrpcvW+kA= +go.opentelemetry.io/collector/confmap v0.101.0/go.mod h1:BWKPIpYeUzSG6ZgCJMjF7xsLvyrvJCfYURl57E5vhiQ= go.opentelemetry.io/collector/featuregate v0.77.0 h1:m1/IzaXoQh6SgF6CM80vrBOCf5zSJ2GVISfA27fYzGU= go.opentelemetry.io/collector/featuregate v0.77.0/go.mod h1:/kVAsGUCyJXIDSgHftCN63QiwAEVHRLX2Kh/S+dqgHY= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg= @@ -454,31 +734,64 @@ go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+ go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= -go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= -go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= -go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= +golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -488,19 +801,39 @@ golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -508,14 +841,24 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= -golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= +golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= @@ -526,20 +869,49 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -549,6 +921,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -557,24 +930,70 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -586,15 +1005,61 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.147.0 h1:Can3FaQo9LlVqxJCodNmeZW/ib3/qKAY3rFeXiHo5gc= google.golang.org/api v0.147.0/go.mod h1:pQ/9j83DcmPd/5C9e2nFOdjjNkDZ1G+zkbK2uvdkJMs= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97 h1:SeZZZx0cP0fqUyA+oRzP9k7cSwJlvDFiROO72uwD6i0= google.golang.org/genproto v0.0.0-20231002182017-d307bd883b97/go.mod h1:t1VqOqqvce95G3hIDCT5FeO3YUc6Q4Oe24L/+rNMxRk= google.golang.org/genproto/googleapis/api v0.0.0-20231012201019-e917dd12ba7a h1:myvhA4is3vrit1a6NZCWBIwN0kNEnX21DJOJX/NvIfI= @@ -602,9 +1067,17 @@ google.golang.org/genproto/googleapis/api v0.0.0-20231012201019-e917dd12ba7a/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20231009173412-8bfb1ae86b6c h1:jHkCUWkseRf+W+edG5hMzr/Uh1xkDREY4caybAq4dpY= google.golang.org/genproto/googleapis/rpc v0.0.0-20231009173412-8bfb1ae86b6c/go.mod h1:4cYg8o5yUbm77w8ZX00LhMVNl/YVBFJRYWDc0uYWMs0= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= @@ -616,17 +1089,20 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= @@ -636,15 +1112,24 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= -gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A= k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA= k8s.io/apiextensions-apiserver v0.29.0 h1:0VuspFG7Hj+SxyF/Z/2T0uFbI5gb5LRgEyUVE3Q4lV0= @@ -657,17 +1142,22 @@ k8s.io/component-base v0.29.0 h1:T7rjd5wvLnPBV1vC4zWd/iWRbV8Mdxs+nGaoaFzGw3s= k8s.io/component-base v0.29.0/go.mod h1:sADonFTQ9Zc9yFLghpDpmNXEdHyQmFIGbiuZbqAXQ1M= k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= -k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= -k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/kube-openapi v0.0.0-20231129212854-f0671cc7e66a h1:ZeIPbyHHqahGIbeyLJJjAUhnxCKqXaDY+n89Ms8szyA= +k8s.io/kube-openapi v0.0.0-20231129212854-f0671cc7e66a/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= k8s.io/kubectl v0.29.0 h1:Oqi48gXjikDhrBF67AYuZRTcJV4lg2l42GmvsP7FmYI= k8s.io/kubectl v0.29.0/go.mod h1:0jMjGWIcMIQzmUaMgAzhSELv5WtHo2a8pq67DtviAJs= k8s.io/utils v0.0.0-20231127182322-b307cd553661 h1:FepOBzJ0GXm8t0su67ln2wAZjbQ6RxQGZDnzuLcrUTI= k8s.io/utils v0.0.0-20231127182322-b307cd553661/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/integration-tests/eks/validateResources_test.go b/integration-tests/eks/validateResources_test.go index 96a935af3..83fc46eaf 100644 --- a/integration-tests/eks/validateResources_test.go +++ b/integration-tests/eks/validateResources_test.go @@ -15,7 +15,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - arv1 "k8s.io/api/admissionregistration/v1" appsV1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" diff --git a/integration-tests/manifests/annotations/validate_annotation_methods.go b/integration-tests/manifests/annotations/validate_annotation_methods.go index b9f8da0e1..9e95ef950 100644 --- a/integration-tests/manifests/annotations/validate_annotation_methods.go +++ b/integration-tests/manifests/annotations/validate_annotation_methods.go @@ -6,25 +6,23 @@ package annotations import ( "context" "fmt" - "strconv" - "testing" - - "github.com/google/uuid" - - "github.com/aws/amazon-cloudwatch-agent-operator/integration-tests/util" - "os" "os/exec" "path/filepath" + "strconv" "strings" + "testing" "time" + "github.com/google/uuid" appsV1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" + + "github.com/aws/amazon-cloudwatch-agent-operator/integration-tests/util" ) const ( diff --git a/integration-tests/manifests/cmd/validate_instrumentation_vars.go b/integration-tests/manifests/cmd/validate_instrumentation_vars.go index 96943f68a..9bb4a6b9b 100644 --- a/integration-tests/manifests/cmd/validate_instrumentation_vars.go +++ b/integration-tests/manifests/cmd/validate_instrumentation_vars.go @@ -13,9 +13,8 @@ import ( "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/tools/clientcmd" - "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" ) var appSignalsEnvVarKeys = []string{ diff --git a/internal/config/main.go b/internal/config/main.go index 07c3df91b..0ebfe4f28 100644 --- a/internal/config/main.go +++ b/internal/config/main.go @@ -12,8 +12,10 @@ import ( ) const ( - defaultCollectorConfigMapEntry = "cwagentconfig.json" - defaultOtelCollectorConfigMapEntry = "cwagentotelconfig.yaml" + defaultCollectorConfigMapEntry = "cwagentconfig.json" + defaultOtelCollectorConfigMapEntry = "cwagentotelconfig.yaml" + defaultTargetAllocatorConfigMapEntry = "targetallocator.yaml" + defaultPrometheusConfigMapEntry = "prometheus.yaml" ) // Config holds the static configuration for this operator. @@ -31,6 +33,9 @@ type Config struct { autoInstrumentationJavaImage string dcgmExporterImage string neuronMonitorImage string + targetAllocatorImage string + targetAllocatorConfigMapEntry string + prometheusConfigMapEntry string labelsFilter []string } @@ -38,10 +43,12 @@ type Config struct { func New(opts ...Option) Config { // initialize with the default values o := options{ - collectorConfigMapEntry: defaultCollectorConfigMapEntry, - otelCollectorConfigMapEntry: defaultOtelCollectorConfigMapEntry, - logger: logf.Log.WithName("config"), - version: version.Get(), + collectorConfigMapEntry: defaultCollectorConfigMapEntry, + otelCollectorConfigMapEntry: defaultOtelCollectorConfigMapEntry, + targetAllocatorConfigMapEntry: defaultTargetAllocatorConfigMapEntry, + prometheusConfigMapEntry: defaultPrometheusConfigMapEntry, + logger: logf.Log.WithName("config"), + version: version.Get(), } for _, opt := range opts { opt(&o) @@ -61,6 +68,9 @@ func New(opts ...Option) Config { autoInstrumentationNginxImage: o.autoInstrumentationNginxImage, dcgmExporterImage: o.dcgmExporterImage, neuronMonitorImage: o.neuronMonitorImage, + targetAllocatorImage: o.targetAllocatorImage, + targetAllocatorConfigMapEntry: o.targetAllocatorConfigMapEntry, + prometheusConfigMapEntry: o.prometheusConfigMapEntry, labelsFilter: o.labelsFilter, } } @@ -125,6 +135,19 @@ func (c *Config) NeuronMonitorImage() string { return c.neuronMonitorImage } +// TargetAllocatorImage represents the flag to override the OpenTelemetry TargetAllocator container image. +func (c *Config) TargetAllocatorImage() string { + return c.targetAllocatorImage +} + +// TargetAllocatorConfigMapEntry represents the configuration file name for the TargetAllocator. Immutable. +func (c *Config) TargetAllocatorConfigMapEntry() string { + return c.targetAllocatorConfigMapEntry +} + +// PrometheusConfigMapEntry represents the configuration file name for Prometheus. +func (c *Config) PrometheusConfigMapEntry() string { return c.prometheusConfigMapEntry } + // LabelsFilter Returns the filters converted to regex strings used to filter out unwanted labels from propagations. func (c *Config) LabelsFilter() []string { return c.labelsFilter diff --git a/internal/config/main_test.go b/internal/config/main_test.go index 5fc30e0a2..c3847b45d 100644 --- a/internal/config/main_test.go +++ b/internal/config/main_test.go @@ -17,10 +17,14 @@ func TestNewConfig(t *testing.T) { config.WithCollectorImage("some-image"), config.WithCollectorConfigMapEntry("some-config.json"), config.WithOtelCollectorConfigMapEntry("some-otel-config.yaml"), + config.WithTargetAllocatorConfigMapEntry("some-ta-config.yaml"), + config.WithPrometheusConfigMapEntry("some-prom-config.yaml"), ) // test assert.Equal(t, "some-image", cfg.CollectorImage()) assert.Equal(t, "some-config.json", cfg.CollectorConfigMapEntry()) assert.Equal(t, "some-otel-config.yaml", cfg.OtelCollectorConfigMapEntry()) + assert.Equal(t, "some-ta-config.yaml", cfg.TargetAllocatorConfigMapEntry()) + assert.Equal(t, "some-prom-config.yaml", cfg.PrometheusConfigMapEntry()) } diff --git a/internal/config/options.go b/internal/config/options.go index 8c862e4af..1bbc7430a 100644 --- a/internal/config/options.go +++ b/internal/config/options.go @@ -30,6 +30,9 @@ type options struct { otelCollectorConfigMapEntry string dcgmExporterImage string neuronMonitorImage string + targetAllocatorImage string + targetAllocatorConfigMapEntry string + prometheusConfigMapEntry string labelsFilter []string } @@ -48,6 +51,16 @@ func WithOtelCollectorConfigMapEntry(s string) Option { o.otelCollectorConfigMapEntry = s } } +func WithTargetAllocatorConfigMapEntry(s string) Option { + return func(o *options) { + o.targetAllocatorConfigMapEntry = s + } +} +func WithPrometheusConfigMapEntry(s string) Option { + return func(o *options) { + o.prometheusConfigMapEntry = s + } +} func WithLogger(logger logr.Logger) Option { return func(o *options) { o.logger = logger @@ -113,6 +126,12 @@ func WithNeuronMonitorImage(s string) Option { } } +func WithTargetAllocatorImage(s string) Option { + return func(o *options) { + o.targetAllocatorImage = s + } +} + func WithLabelFilters(labelFilters []string) Option { return func(o *options) { diff --git a/internal/manifests/collector/collector.go b/internal/manifests/collector/collector.go index 34df8b56a..4e6a9f334 100644 --- a/internal/manifests/collector/collector.go +++ b/internal/manifests/collector/collector.go @@ -32,7 +32,6 @@ func Build(params manifests.Params) ([]client.Object, error) { params.Log.V(5).Info("not building sidecar...") } manifestFactories = append(manifestFactories, []manifests.K8sManifestFactory{ - manifests.Factory(ConfigMap), manifests.FactoryWithoutError(HorizontalPodAutoscaler), manifests.FactoryWithoutError(ServiceAccount), manifests.Factory(Service), @@ -55,6 +54,13 @@ func Build(params manifests.Params) ([]client.Object, error) { resourceManifests = append(resourceManifests, res) } } + configmaps, err := ConfigMaps(params) + if err != nil { + return nil, err + } + for _, configmap := range configmaps { + resourceManifests = append(resourceManifests, configmap) + } routes, err := Routes(params) if err != nil { return nil, err diff --git a/internal/manifests/collector/config_replace.go b/internal/manifests/collector/config_replace.go index 4ca3fb049..e2f31aafa 100644 --- a/internal/manifests/collector/config_replace.go +++ b/internal/manifests/collector/config_replace.go @@ -5,22 +5,87 @@ package collector import ( "encoding/json" + "fmt" + "time" + promconfig "github.com/prometheus/prometheus/config" + _ "github.com/prometheus/prometheus/discovery/install" // Package install has the side-effect of registering all builtin. "gopkg.in/yaml.v2" - _ "github.com/prometheus/prometheus/discovery/install" // Package install has the side-effect of registering all builtin. + "go.opentelemetry.io/collector/confmap" "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/collector/adapters" + ta "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/targetallocator/adapters" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/naming" + "github.com/aws/amazon-cloudwatch-agent-operator/pkg/featuregate" ) +type targetAllocator struct { + Endpoint string `yaml:"endpoint"` + Interval time.Duration `yaml:"interval"` + // HTTPSDConfig is a preference that can be set for the collector's target allocator, but the operator doesn't + // care about what the value is set to. We just need this for validation when unmarshalling the configmap. + HTTPSDConfig interface{} `yaml:"http_sd_config,omitempty"` +} + +type Config struct { + PromConfig *promconfig.Config `yaml:"config"` + TargetAllocConfig *targetAllocator `yaml:"target_allocator,omitempty"` +} + func ReplaceConfig(instance v1alpha1.AmazonCloudWatchAgent) (string, error) { + // Parse the original configuration from instance.Spec.Config config, err := adapters.ConfigFromJSONString(instance.Spec.Config) if err != nil { return "", err } - out, err := json.Marshal(config) + conf := confmap.NewFromStringMap(config) + + prometheusFilePath := conf.Get("logs::metrics_collected::prometheus::prometheus_config_path") + if prometheusFilePath == nil { + prometheusFilePath = "/etc/prometheusconfig/prometheus.yaml" + } + if conf.IsSet("logs::metrics_collected::prometheus") && !instance.Spec.Prometheus.IsEmpty() { + prometheusConfig := confmap.NewFromStringMap(map[string]interface{}{ + "logs": map[string]interface{}{ + "metrics_collected": map[string]interface{}{ + "prometheus": map[string]interface{}{ + "prometheus_config_path": prometheusFilePath, + }, + }, + }, + }) + + err = conf.Merge(prometheusConfig) + if err != nil { + return "", err + } + } + prometheusFilePath = conf.Get("metrics::metrics_collected::prometheus::prometheus_config_path") + if prometheusFilePath == nil { + prometheusFilePath = "/etc/prometheusconfig/prometheus.yaml" + } + if conf.IsSet("metrics::metrics_collected::prometheus") && !instance.Spec.Prometheus.IsEmpty() { + prometheusConfig := confmap.NewFromStringMap(map[string]interface{}{ + "metrics": map[string]interface{}{ + "metrics_collected": map[string]interface{}{ + "prometheus": map[string]interface{}{ + "prometheus_config_path": prometheusFilePath, + }, + }, + }, + }) + + err = conf.Merge(prometheusConfig) + if err != nil { + return "", err + } + } + + finalConfig := conf.ToStringMap() + out, err := json.Marshal(finalConfig) if err != nil { return "", err } @@ -40,3 +105,64 @@ func ReplaceOtelConfig(instance v1alpha1.AmazonCloudWatchAgent) (string, error) } return string(out), nil } + +// ReplacePrometheusConfig replaces the prometheus configuration that the customer provides with itself (if the +// target-allocator isn't enabled) or the target_allocator configuration (if the target-allocator is enabled) +// and populates it into the prometheus.yaml file, which is seen in its ConfigMap. +func ReplacePrometheusConfig(instance v1alpha1.AmazonCloudWatchAgent) (string, error) { + promConfigYaml, err := instance.Spec.Prometheus.Yaml() + if err != nil { + return "", fmt.Errorf("%s could not convert json to yaml", err) + } + + // Check if TargetAllocator is enabled, if not, return the original config + if !instance.Spec.TargetAllocator.Enabled { + prometheusConfig, err := adapters.ConfigFromString(promConfigYaml) + if err != nil { + return "", err + } + + prometheusConfigYAML, err := yaml.Marshal(prometheusConfig) + if err != nil { + return "", err + } + + return string(prometheusConfigYAML), nil + } + + promCfgMap, getCfgPromErr := adapters.ConfigFromString(promConfigYaml) + if getCfgPromErr != nil { + return "", getCfgPromErr + } + + validateCfgPromErr := ta.ValidatePromConfig(promCfgMap, instance.Spec.TargetAllocator.Enabled, featuregate.EnableTargetAllocatorRewrite.IsEnabled()) + if validateCfgPromErr != nil { + return "", validateCfgPromErr + } + + if featuregate.EnableTargetAllocatorRewrite.IsEnabled() { + updPromCfgMap, getCfgPromErr := ta.AddTAConfigToPromConfig(promCfgMap, naming.TAService(instance.Name)) + if getCfgPromErr != nil { + return "", getCfgPromErr + } + + out, updCfgMarshalErr := yaml.Marshal(updPromCfgMap) + if updCfgMarshalErr != nil { + return "", updCfgMarshalErr + } + + return string(out), nil + } + + updPromCfgMap, err := ta.AddHTTPSDConfigToPromConfig(promCfgMap, naming.TAService(instance.Name)) + if err != nil { + return "", err + } + + out, err := yaml.Marshal(updPromCfgMap) + if err != nil { + return "", err + } + + return string(out), nil +} diff --git a/internal/manifests/collector/config_replace_test.go b/internal/manifests/collector/config_replace_test.go new file mode 100644 index 000000000..45bfe528c --- /dev/null +++ b/internal/manifests/collector/config_replace_test.go @@ -0,0 +1,628 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package collector + +import ( + "encoding/json" + "fmt" + "os" + "testing" + + "github.com/prometheus/prometheus/discovery/http" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + colfeaturegate "go.opentelemetry.io/collector/featuregate" + "gopkg.in/yaml.v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/collector/adapters" + "github.com/aws/amazon-cloudwatch-agent-operator/pkg/featuregate" +) + +func TestPrometheusParser(t *testing.T) { + httpConfigYAML, err := os.ReadFile("testdata/http_sd_config_test.yaml") + if err != nil { + fmt.Printf("Error getting yaml file: %v", err) + } + promCfg := v1alpha1.PrometheusConfig{} + err = yaml.Unmarshal(httpConfigYAML, &promCfg) + if err != nil { + fmt.Printf("failed to unmarshal config: %v", err) + } + param := manifests.Params{ + OtelCol: v1alpha1.AmazonCloudWatchAgent{ + TypeMeta: metav1.TypeMeta{ + Kind: "cloudwatch.aws.amazon.com", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + UID: instanceUID, + }, + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + Image: "public.ecr.aws/cloudwatch-agent/cloudwatch-agent:0.0.0", + Prometheus: promCfg, + TargetAllocator: v1alpha1.AmazonCloudWatchAgentTargetAllocator{ + Enabled: true, + Image: "test/test-img", + }, + }, + }, + } + assert.NoError(t, err) + + t.Run("should update config with http_sd_config", func(t *testing.T) { + err := colfeaturegate.GlobalRegistry().Set(featuregate.EnableTargetAllocatorRewrite.ID(), false) + require.NoError(t, err) + t.Cleanup(func() { + _ = colfeaturegate.GlobalRegistry().Set(featuregate.EnableTargetAllocatorRewrite.ID(), true) + }) + actualConfig, err := ReplacePrometheusConfig(param.OtelCol) + assert.NoError(t, err) + + // prepare + var cfg Config + promCfgMap, err := adapters.ConfigFromString(actualConfig) + assert.NoError(t, err) + + promCfg, err := yaml.Marshal(promCfgMap) + assert.NoError(t, err) + + err = yaml.UnmarshalStrict(promCfg, &cfg) + assert.NoError(t, err) + + // test + expectedMap := map[string]bool{ + "prometheus": false, + "service-x": false, + } + for _, scrapeConfig := range cfg.PromConfig.ScrapeConfigs { + assert.Len(t, scrapeConfig.ServiceDiscoveryConfigs, 1) + assert.Equal(t, scrapeConfig.ServiceDiscoveryConfigs[0].Name(), "http") + assert.Equal(t, scrapeConfig.ServiceDiscoveryConfigs[0].(*http.SDConfig).URL, fmt.Sprintf("https://%s-target-allocator-service:80/jobs/", param.OtelCol.Name)+scrapeConfig.JobName+"/targets") + expectedMap[scrapeConfig.JobName] = true + } + for k := range expectedMap { + assert.True(t, expectedMap[k], k) + } + assert.True(t, cfg.TargetAllocConfig == nil) + }) + + t.Run("should update config with targetAllocator block if block not present", func(t *testing.T) { + // Set up the test scenario + param.OtelCol.Spec.TargetAllocator.Enabled = true + actualConfig, err := ReplacePrometheusConfig(param.OtelCol) + assert.NoError(t, err) + + // Verify the expected changes in the config + promCfgMap, err := adapters.ConfigFromString(actualConfig) + assert.NoError(t, err) + + prometheusConfig := promCfgMap["config"].(map[interface{}]interface{}) + + assert.NotContains(t, prometheusConfig, "scrape_configs") + + expectedTAConfig := map[interface{}]interface{}{ + "endpoint": fmt.Sprintf("https://%s-target-allocator-service:80", param.OtelCol.Name), + "interval": "30s", + } + assert.Equal(t, expectedTAConfig, promCfgMap["target_allocator"]) + assert.NoError(t, err) + }) + + t.Run("should update config with targetAllocator block if block already present", func(t *testing.T) { + // Set up the test scenario + httpTAConfigYAML, err := os.ReadFile("testdata/http_sd_config_ta_test.yaml") + if err != nil { + fmt.Printf("Error getting yaml file: %v", err) + } + promCfg := v1alpha1.PrometheusConfig{} + err = yaml.Unmarshal(httpTAConfigYAML, &promCfg) + if err != nil { + fmt.Printf("failed to unmarshal config: %v", err) + } + paramTa := manifests.Params{ + OtelCol: v1alpha1.AmazonCloudWatchAgent{ + TypeMeta: metav1.TypeMeta{ + Kind: "cloudwatch.aws.amazon.com", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + UID: instanceUID, + }, + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + Image: "public.ecr.aws/cloudwatch-agent/cloudwatch-agent:0.0.0", + Prometheus: promCfg, + TargetAllocator: v1alpha1.AmazonCloudWatchAgentTargetAllocator{ + Enabled: true, + Image: "test/test-img", + }, + }, + }, + } + require.NoError(t, err) + paramTa.OtelCol.Spec.TargetAllocator.Enabled = true + + actualConfig, err := ReplacePrometheusConfig(paramTa.OtelCol) + assert.NoError(t, err) + + // Verify the expected changes in the config + promCfgMap, err := adapters.ConfigFromString(actualConfig) + assert.NoError(t, err) + + prometheusConfig := promCfgMap["config"].(map[interface{}]interface{}) + + assert.NotContains(t, prometheusConfig, "scrape_configs") + + expectedTAConfig := map[interface{}]interface{}{ + "endpoint": fmt.Sprintf("https://%s-target-allocator-service:80", param.OtelCol.Name), + "interval": "30s", + } + assert.Equal(t, expectedTAConfig, promCfgMap["target_allocator"]) + assert.NoError(t, err) + }) + + t.Run("should not update config with http_sd_config", func(t *testing.T) { + param.OtelCol.Spec.TargetAllocator.Enabled = false + actualConfig, err := ReplacePrometheusConfig(param.OtelCol) + assert.NoError(t, err) + + // prepare + var cfg Config + promCfgMap, err := adapters.ConfigFromString(actualConfig) + assert.NoError(t, err) + + promCfg, err := yaml.Marshal(promCfgMap) + assert.NoError(t, err) + + err = yaml.UnmarshalStrict(promCfg, &cfg) + assert.NoError(t, err) + + // test + expectedMap := map[string]bool{ + "prometheus": false, + "service-x": false, + } + for _, scrapeConfig := range cfg.PromConfig.ScrapeConfigs { + assert.Len(t, scrapeConfig.ServiceDiscoveryConfigs, 2) + assert.Equal(t, scrapeConfig.ServiceDiscoveryConfigs[0].Name(), "file") + assert.Equal(t, scrapeConfig.ServiceDiscoveryConfigs[1].Name(), "static") + expectedMap[scrapeConfig.JobName] = true + } + for k := range expectedMap { + assert.True(t, expectedMap[k], k) + } + assert.True(t, cfg.TargetAllocConfig == nil) + }) + +} + +func TestReplacePrometheusConfig(t *testing.T) { + relabelConfigYAML, err := os.ReadFile("testdata/relabel_config_original.yaml") + if err != nil { + fmt.Printf("Error getting yaml file: %v", err) + } + promCfg := v1alpha1.PrometheusConfig{} + err = yaml.Unmarshal(relabelConfigYAML, &promCfg) + if err != nil { + fmt.Printf("failed to unmarshal config: %v", err) + } + param := manifests.Params{ + OtelCol: v1alpha1.AmazonCloudWatchAgent{ + TypeMeta: metav1.TypeMeta{ + Kind: "cloudwatch.aws.amazon.com", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + UID: instanceUID, + }, + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + Image: "public.ecr.aws/cloudwatch-agent/cloudwatch-agent:0.0.0", + Prometheus: promCfg, + TargetAllocator: v1alpha1.AmazonCloudWatchAgentTargetAllocator{ + Enabled: true, + Image: "test/test-img", + }, + }, + }, + } + assert.NoError(t, err) + + t.Run("should not modify config when TargetAllocator is disabled", func(t *testing.T) { + param.OtelCol.Spec.TargetAllocator.Enabled = false + + expectedConfig := `config: + global: + evaluation_interval: 1m + scrape_interval: 1m + scrape_timeout: 10s + scrape_configs: + - honor_labels: true + job_name: service-x + metric_relabel_configs: + - action: keep + regex: (.*) + separator: ; + source_labels: + - label1 + - action: labelmap + regex: (.*) + separator: ; + source_labels: + - label4 + metrics_path: /metrics + relabel_configs: + - action: keep + regex: (.*) + source_labels: + - label1 + - action: replace + regex: (.*) + replacement: $1_$2 + separator: ; + source_labels: + - label2 + target_label: label3 + - action: labelmap + regex: (.*) + separator: ; + source_labels: + - label4 + - action: labeldrop + regex: foo_.* + scheme: http + scrape_interval: 1m + scrape_timeout: 10s +` + + actualConfig, err := ReplacePrometheusConfig(param.OtelCol) + assert.NoError(t, err) + + assert.Equal(t, expectedConfig, actualConfig) + }) + + t.Run("should rewrite scrape configs with SD config when TargetAllocator is enabled and feature flag is not set", func(t *testing.T) { + err := colfeaturegate.GlobalRegistry().Set(featuregate.EnableTargetAllocatorRewrite.ID(), false) + require.NoError(t, err) + t.Cleanup(func() { + _ = colfeaturegate.GlobalRegistry().Set(featuregate.EnableTargetAllocatorRewrite.ID(), true) + }) + + param.OtelCol.Spec.TargetAllocator.Enabled = true + + expectedConfig := `config: + global: + evaluation_interval: 1m + scrape_interval: 1m + scrape_timeout: 10s + scrape_configs: + - honor_labels: true + http_sd_configs: + - url: https://test-target-allocator-service:80/jobs/service-x/targets + job_name: service-x + metric_relabel_configs: + - action: keep + regex: (.*) + separator: ; + source_labels: + - label1 + - action: labelmap + regex: (.*) + separator: ; + source_labels: + - label4 + metrics_path: /metrics + relabel_configs: + - action: keep + regex: (.*) + source_labels: + - label1 + - action: replace + regex: (.*) + replacement: $1_$2 + separator: ; + source_labels: + - label2 + target_label: label3 + - action: labelmap + regex: (.*) + separator: ; + source_labels: + - label4 + - action: labeldrop + regex: foo_.* + scheme: http + scrape_interval: 1m + scrape_timeout: 10s +` + + actualConfig, err := ReplacePrometheusConfig(param.OtelCol) + assert.NoError(t, err) + + assert.Equal(t, expectedConfig, actualConfig) + }) + + t.Run("should remove scrape configs if TargetAllocator is enabled and feature flag is set", func(t *testing.T) { + param.OtelCol.Spec.TargetAllocator.Enabled = true + + expectedConfig := `config: + global: + evaluation_interval: 1m + scrape_interval: 1m + scrape_timeout: 10s +target_allocator: + endpoint: https://test-target-allocator-service:80 + interval: 30s +` + + actualConfig, err := ReplacePrometheusConfig(param.OtelCol) + assert.NoError(t, err) + + assert.Equal(t, expectedConfig, actualConfig) + }) +} + +// TestReplaceConfig tests the ReplaceConfig function when logs::metrics_collected::prometheus is present +func TestReplaceConfigLogsPrometheus(t *testing.T) { + jsonConfig := `{ + "logs": { + "metrics_collected": { + "prometheus": { + "prometheus_config_path": "/custom/path/logs_prometheus.yaml" + } + } + } + }` + + agent := v1alpha1.AmazonCloudWatchAgent{ + TypeMeta: metav1.TypeMeta{ + Kind: "AmazonCloudWatchAgent", + APIVersion: "v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-agent", + Namespace: "default", + }, + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + Config: jsonConfig, + Prometheus: v1alpha1.PrometheusConfig{ + Config: &v1alpha1.AnyConfig{}, + }, + }, + Status: v1alpha1.AmazonCloudWatchAgentStatus{}, + } + + result, err := ReplaceConfig(agent) + assert.NoError(t, err, "Expected no error while replacing config") + + expected := map[string]interface{}{ + "logs": map[string]interface{}{ + "metrics_collected": map[string]interface{}{ + "prometheus": map[string]interface{}{ + "prometheus_config_path": "/custom/path/logs_prometheus.yaml", + }, + }, + }, + } + + expectedJSON, err := json.Marshal(expected) + assert.NoError(t, err, "Expected no error while marshaling expected result") + + assert.JSONEq(t, string(expectedJSON), result, "The resulting JSON should match the expected JSON") +} + +// TestReplaceConfigMetricsPrometheus tests the ReplaceConfig function when metrics::metrics_collected::prometheus is present +func TestReplaceConfigMetricsPrometheus(t *testing.T) { + jsonConfig := `{ + "metrics": { + "metrics_collected": { + "prometheus": { + "prometheus_config_path": "/custom/path/metrics_prometheus.yaml" + } + } + } + }` + + agent := v1alpha1.AmazonCloudWatchAgent{ + TypeMeta: metav1.TypeMeta{ + Kind: "AmazonCloudWatchAgent", + APIVersion: "v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-agent", + Namespace: "default", + }, + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + Config: jsonConfig, + Prometheus: v1alpha1.PrometheusConfig{ + Config: &v1alpha1.AnyConfig{}, + }, + }, + Status: v1alpha1.AmazonCloudWatchAgentStatus{}, + } + + result, err := ReplaceConfig(agent) + assert.NoError(t, err, "Expected no error while replacing config") + + expected := map[string]interface{}{ + "metrics": map[string]interface{}{ + "metrics_collected": map[string]interface{}{ + "prometheus": map[string]interface{}{ + "prometheus_config_path": "/custom/path/metrics_prometheus.yaml", + }, + }, + }, + } + + // Marshal expected result to JSON + expectedJSON, err := json.Marshal(expected) + assert.NoError(t, err, "Expected no error while marshaling expected result") + + // Assert the output of ReplaceConfig matches the expected JSON + assert.JSONEq(t, string(expectedJSON), result, "The resulting JSON should match the expected JSON") +} + +// TestReplaceConfigWithDefaultPath tests the ReplaceConfig function when neither logs nor metrics prometheus path is set +func TestReplaceConfigWithDefaultPath(t *testing.T) { + jsonConfig := `{ + "logs": { + "metrics_collected": { + "prometheus": {} + } + }, + "metrics": { + "metrics_collected": { + "prometheus": {} + } + } + }` + + agent := v1alpha1.AmazonCloudWatchAgent{ + TypeMeta: metav1.TypeMeta{ + Kind: "AmazonCloudWatchAgent", + APIVersion: "v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-agent", + Namespace: "default", + }, + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + Config: jsonConfig, + Prometheus: v1alpha1.PrometheusConfig{ + Config: &v1alpha1.AnyConfig{}, + }, + }, + Status: v1alpha1.AmazonCloudWatchAgentStatus{}, + } + + result, err := ReplaceConfig(agent) + assert.NoError(t, err, "Expected no error while replacing config") + + expected := map[string]interface{}{ + "logs": map[string]interface{}{ + "metrics_collected": map[string]interface{}{ + "prometheus": map[string]interface{}{ + "prometheus_config_path": "/etc/prometheusconfig/prometheus.yaml", + }, + }, + }, + "metrics": map[string]interface{}{ + "metrics_collected": map[string]interface{}{ + "prometheus": map[string]interface{}{ + "prometheus_config_path": "/etc/prometheusconfig/prometheus.yaml", + }, + }, + }, + } + + expectedJSON, err := json.Marshal(expected) + assert.NoError(t, err, "Expected no error while marshaling expected result") + + assert.JSONEq(t, string(expectedJSON), result, "The resulting JSON should match the expected JSON") +} + +func TestReplaceConfigWithDefaultPathButNoConfigmap(t *testing.T) { + jsonConfig := `{ + "logs": { + "metrics_collected": { + "prometheus": {} + } + }, + "metrics": { + "metrics_collected": { + "prometheus": {} + } + } + }` + + agent := v1alpha1.AmazonCloudWatchAgent{ + TypeMeta: metav1.TypeMeta{ + Kind: "AmazonCloudWatchAgent", + APIVersion: "v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-agent", + Namespace: "default", + }, + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + Config: jsonConfig, + }, + Status: v1alpha1.AmazonCloudWatchAgentStatus{}, + } + + result, err := ReplaceConfig(agent) + assert.NoError(t, err, "Expected no error while replacing config") + + expected := map[string]interface{}{ + "logs": map[string]interface{}{ + "metrics_collected": map[string]interface{}{ + "prometheus": map[string]interface{}{}, + }, + }, + "metrics": map[string]interface{}{ + "metrics_collected": map[string]interface{}{ + "prometheus": map[string]interface{}{}, + }, + }, + } + + expectedJSON, err := json.Marshal(expected) + assert.NoError(t, err, "Expected no error while marshaling expected result") + + assert.JSONEq(t, string(expectedJSON), result, "The resulting JSON should match the expected JSON") +} + +// TestReplaceConfigNoPrometheusSection tests the ReplaceConfig function when neither `logs` nor `metrics` sections have `prometheus` defined +func TestReplaceConfigNoPrometheusSection(t *testing.T) { + jsonConfig := `{ + "logs": { + "metrics_collected": {} + }, + "metrics": { + "metrics_collected": {} + } + }` + + agent := v1alpha1.AmazonCloudWatchAgent{ + TypeMeta: metav1.TypeMeta{ + Kind: "AmazonCloudWatchAgent", + APIVersion: "v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-agent", + Namespace: "default", + }, + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + Config: jsonConfig, + Prometheus: v1alpha1.PrometheusConfig{ + Config: &v1alpha1.AnyConfig{}, + }, + }, + Status: v1alpha1.AmazonCloudWatchAgentStatus{}, + } + + result, err := ReplaceConfig(agent) + assert.NoError(t, err, "Expected no error while replacing config") + + expected := map[string]interface{}{ + "logs": map[string]interface{}{ + "metrics_collected": map[string]interface{}{}, + }, + "metrics": map[string]interface{}{ + "metrics_collected": map[string]interface{}{}, + }, + } + + expectedJSON, err := json.Marshal(expected) + assert.NoError(t, err, "Expected no error while marshaling expected result") + + assert.JSONEq(t, string(expectedJSON), result, "The resulting JSON should match the expected JSON") +} diff --git a/internal/manifests/collector/configmap.go b/internal/manifests/collector/configmap.go index c19e90bf6..c74f187d5 100644 --- a/internal/manifests/collector/configmap.go +++ b/internal/manifests/collector/configmap.go @@ -4,15 +4,21 @@ package collector import ( + "fmt" + + "gopkg.in/yaml.v2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/collector/adapters" "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/manifestutils" "github.com/aws/amazon-cloudwatch-agent-operator/internal/naming" ) -func ConfigMap(params manifests.Params) (*corev1.ConfigMap, error) { +func ConfigMaps(params manifests.Params) ([]*corev1.ConfigMap, error) { + var configmaps []*corev1.ConfigMap + name := naming.ConfigMap(params.OtelCol.Name) labels := manifestutils.Labels(params.OtelCol.ObjectMeta, name, params.OtelCol.Spec.Image, ComponentAmazonCloudWatchAgent, []string{}) @@ -35,7 +41,7 @@ func ConfigMap(params manifests.Params) (*corev1.ConfigMap, error) { sourceDataMap[params.Config.OtelCollectorConfigMapEntry()] = replacedOtelConfig } - return &corev1.ConfigMap{ + configmaps = append(configmaps, &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: params.OtelCol.Namespace, @@ -43,5 +49,49 @@ func ConfigMap(params manifests.Params) (*corev1.ConfigMap, error) { Annotations: params.OtelCol.Annotations, }, Data: sourceDataMap, - }, nil + }) + + if !params.OtelCol.Spec.Prometheus.IsEmpty() { + promName := naming.PrometheusConfigMap(params.OtelCol.Name) + promLabels := manifestutils.Labels(params.OtelCol.ObjectMeta, promName, "", ComponentAmazonCloudWatchAgent, []string{}) + + replacedPrometheusConf, err := ReplacePrometheusConfig(params.OtelCol) + if err != nil { + params.Log.V(2).Info("failed to update prometheus config to use sharded targets: ", "err", err) + return nil, err + } + + if !params.OtelCol.Spec.TargetAllocator.Enabled { + replacedPrometheusConfig, err := adapters.ConfigFromString(replacedPrometheusConf) + if err != nil { + return nil, err + } + + replacedPrometheusConfProp, ok := replacedPrometheusConfig["config"] + if !ok { + return nil, fmt.Errorf("no prometheusConfig available as part of the configuration") + } + + replacedPrometheusConfPropYAML, err := yaml.Marshal(replacedPrometheusConfProp) + if err != nil { + return nil, err + } + + replacedPrometheusConf = string(replacedPrometheusConfPropYAML) + } + + configmaps = append(configmaps, &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: promName, + Namespace: params.OtelCol.Namespace, + Labels: promLabels, + Annotations: params.OtelCol.Annotations, + }, + Data: map[string]string{ + params.Config.PrometheusConfigMapEntry(): replacedPrometheusConf, + }, + }) + } + + return configmaps, nil } diff --git a/internal/manifests/collector/configmap_test.go b/internal/manifests/collector/configmap_test.go index 8fb1b47fd..3e252c1f9 100644 --- a/internal/manifests/collector/configmap_test.go +++ b/internal/manifests/collector/configmap_test.go @@ -4,9 +4,19 @@ package collector import ( + "fmt" + "os" "testing" "github.com/stretchr/testify/assert" + colfeaturegate "go.opentelemetry.io/collector/featuregate" + "gopkg.in/yaml.v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/config" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" + "github.com/aws/amazon-cloudwatch-agent-operator/pkg/featuregate" ) func TestDesiredConfigMap(t *testing.T) { @@ -27,12 +37,244 @@ func TestDesiredConfigMap(t *testing.T) { } param := deploymentParams() - actual, err := ConfigMap(param) + actual, err := ConfigMaps(param) + + assert.NoError(t, err) + assert.Equal(t, "test", actual[0].Name) + assert.Equal(t, expectedLabels, actual[0].Labels) + assert.Equal(t, expectedData, actual[0].Data) + + }) +} + +func TestDesiredPrometheusConfigMap(t *testing.T) { + expectedLabels := map[string]string{ + "app.kubernetes.io/managed-by": "amazon-cloudwatch-agent-operator", + "app.kubernetes.io/instance": "default.test", + "app.kubernetes.io/part-of": "amazon-cloudwatch-agent", + } + + configYAML, err := os.ReadFile("testdata/prometheus_test.yaml") + if err != nil { + fmt.Printf("Error getting yaml file: %v", err) + } + promCfg := v1alpha1.PrometheusConfig{} + err = yaml.Unmarshal(configYAML, &promCfg) + if err != nil { + fmt.Printf("failed to unmarshal config: %v", err) + } + + httpConfigYAML, err := os.ReadFile("testdata/http_sd_config_servicemonitor_test.yaml") + if err != nil { + fmt.Printf("Error getting yaml file: %v", err) + } + httpPromCfg := v1alpha1.PrometheusConfig{} + err = yaml.Unmarshal(httpConfigYAML, &httpPromCfg) + if err != nil { + fmt.Printf("failed to unmarshal config: %v", err) + } + + httpTAConfigYAML, err := os.ReadFile("testdata/http_sd_config_servicemonitor_test_ta_set.yaml") + if err != nil { + fmt.Printf("Error getting yaml file: %v", err) + } + httpTAPromCfg := v1alpha1.PrometheusConfig{} + err = yaml.Unmarshal(httpTAConfigYAML, &httpTAPromCfg) + if err != nil { + fmt.Printf("failed to unmarshal config: %v", err) + } + + t.Run("should return expected prometheus config map with no target allocator", func(t *testing.T) { + expectedLabels["app.kubernetes.io/component"] = "amazon-cloudwatch-agent" + expectedLabels["app.kubernetes.io/name"] = "test-prometheus-config" + + expectedData := map[string]string{ + "prometheus.yaml": `scrape_configs: +- job_name: cloudwatch-agent + scrape_interval: 10s + static_configs: + - targets: + - 0.0.0.0:8888 +`, + } + + param := manifests.Params{ + Config: config.New(), + OtelCol: v1alpha1.AmazonCloudWatchAgent{ + TypeMeta: metav1.TypeMeta{ + Kind: "cloudwatch.aws.amazon.com", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + UID: instanceUID, + }, + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + Image: "public.ecr.aws/cloudwatch-agent/cloudwatch-agent:0.0.0", + Config: "{}", + Prometheus: promCfg, + }, + }, + } + actual, err := ConfigMaps(param) + + assert.NoError(t, err) + assert.Equal(t, "test-prometheus-config", actual[1].Name) + assert.Equal(t, expectedLabels, actual[1].Labels) + assert.Equal(t, expectedData, actual[1].Data) + + }) + + t.Run("should return expected prometheus config map with http_sd_config if rewrite flag disabled", func(t *testing.T) { + err := colfeaturegate.GlobalRegistry().Set(featuregate.EnableTargetAllocatorRewrite.ID(), false) + assert.NoError(t, err) + t.Cleanup(func() { + _ = colfeaturegate.GlobalRegistry().Set(featuregate.EnableTargetAllocatorRewrite.ID(), true) + }) + expectedLabels["app.kubernetes.io/component"] = "amazon-cloudwatch-agent" + expectedLabels["app.kubernetes.io/name"] = "test-prometheus-config" + + expectedData := map[string]string{ + "prometheus.yaml": `config: + scrape_configs: + - http_sd_configs: + - url: https://test-target-allocator-service:80/jobs/cloudwatch-agent/targets + job_name: cloudwatch-agent + scrape_interval: 10s +`, + } + + param := manifests.Params{ + Config: config.New(), + OtelCol: v1alpha1.AmazonCloudWatchAgent{ + TypeMeta: metav1.TypeMeta{ + Kind: "cloudwatch.aws.amazon.com", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + UID: instanceUID, + }, + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + Image: "public.ecr.aws/cloudwatch-agent/cloudwatch-agent:0.0.0", + Config: "{}", + Prometheus: promCfg, + }, + }, + } + param.OtelCol.Spec.TargetAllocator.Enabled = true + actual, err := ConfigMaps(param) + + assert.NoError(t, err) + assert.Equal(t, "test-prometheus-config", actual[1].GetName()) + assert.Equal(t, expectedLabels, actual[1].GetLabels()) + assert.Equal(t, expectedData, actual[1].Data) + + }) + + t.Run("should return expected escaped prometheus config map with http_sd_config if rewrite flag disabled", func(t *testing.T) { + err := colfeaturegate.GlobalRegistry().Set(featuregate.EnableTargetAllocatorRewrite.ID(), false) + assert.NoError(t, err) + t.Cleanup(func() { + _ = colfeaturegate.GlobalRegistry().Set(featuregate.EnableTargetAllocatorRewrite.ID(), true) + }) + + expectedLabels["app.kubernetes.io/component"] = "amazon-cloudwatch-agent" + expectedLabels["app.kubernetes.io/name"] = "test-prometheus-config" + + expectedData := map[string]string{ + "prometheus.yaml": `config: + scrape_configs: + - http_sd_configs: + - url: https://test-target-allocator-service:80/jobs/serviceMonitor%2Ftest%2Ftest%2F0/targets + job_name: serviceMonitor/test/test/0 +target_allocator: + endpoint: https://test-target-allocator-service:80 + http_sd_config: + refresh_interval: 60s + interval: 30s +`, + } + + param := manifests.Params{ + Config: config.New(), + OtelCol: v1alpha1.AmazonCloudWatchAgent{ + TypeMeta: metav1.TypeMeta{ + Kind: "cloudwatch.aws.amazon.com", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + UID: instanceUID, + }, + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + Image: "public.ecr.aws/cloudwatch-agent/cloudwatch-agent:0.0.0", + Config: "{}", + Prometheus: httpTAPromCfg, + TargetAllocator: v1alpha1.AmazonCloudWatchAgentTargetAllocator{ + Enabled: true, + Image: "test/test-img", + }, + }, + }, + } + assert.NoError(t, err) + param.OtelCol.Spec.TargetAllocator.Enabled = true + actual, err := ConfigMaps(param) + + assert.NoError(t, err) + assert.Equal(t, "test-prometheus-config", actual[1].Name) + assert.Equal(t, expectedLabels, actual[1].Labels) + assert.Equal(t, expectedData, actual[1].Data) + + }) + + t.Run("should return expected escaped prometheus config map with target_allocator config block", func(t *testing.T) { + expectedLabels["app.kubernetes.io/component"] = "amazon-cloudwatch-agent" + expectedLabels["app.kubernetes.io/name"] = "test-prometheus-config" + + expectedData := map[string]string{ + "prometheus.yaml": `config: {} +target_allocator: + endpoint: https://test-target-allocator-service:80 + interval: 30s +`, + } + + param := manifests.Params{ + Config: config.New(), + OtelCol: v1alpha1.AmazonCloudWatchAgent{ + TypeMeta: metav1.TypeMeta{ + Kind: "cloudwatch.aws.amazon.com", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + UID: instanceUID, + }, + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + Image: "public.ecr.aws/cloudwatch-agent/cloudwatch-agent:0.0.0", + Config: "{}", + Prometheus: httpPromCfg, + TargetAllocator: v1alpha1.AmazonCloudWatchAgentTargetAllocator{ + Enabled: true, + Image: "test/test-img", + }, + }, + }, + } + assert.NoError(t, err) + param.OtelCol.Spec.TargetAllocator.Enabled = true + actual, err := ConfigMaps(param) assert.NoError(t, err) - assert.Equal(t, "test", actual.Name) - assert.Equal(t, expectedLabels, actual.Labels) - assert.Equal(t, expectedData, actual.Data) + assert.Equal(t, "test-prometheus-config", actual[1].Name) + assert.Equal(t, expectedLabels, actual[1].Labels) + assert.Equal(t, expectedData, actual[1].Data) }) } @@ -75,12 +317,12 @@ service: } param := otelConfigParams() - actual, err := ConfigMap(param) + actual, err := ConfigMaps(param) assert.NoError(t, err) - assert.Equal(t, "test", actual.Name) - assert.Equal(t, expectedLabels, actual.Labels) - assert.Equal(t, expectedData["cwagentconfig.json"], actual.Data["cwagentconfig.json"]) - assert.YAMLEq(t, expectedData["cwagentotelconfig.yaml"], actual.Data["cwagentotelconfig.yaml"]) + assert.Equal(t, "test", actual[0].Name) + assert.Equal(t, expectedLabels, actual[0].Labels) + assert.Equal(t, expectedData["cwagentconfig.json"], actual[0].Data["cwagentconfig.json"]) + assert.YAMLEq(t, expectedData["cwagentotelconfig.yaml"], actual[0].Data["cwagentotelconfig.yaml"]) }) } diff --git a/internal/manifests/collector/container.go b/internal/manifests/collector/container.go index 6f6d73252..8f468c614 100644 --- a/internal/manifests/collector/container.go +++ b/internal/manifests/collector/container.go @@ -46,6 +46,10 @@ func Container(cfg config.Config, logger logr.Logger, agent v1alpha1.AmazonCloud if addConfig { volumeMounts = append(volumeMounts, getVolumeMounts(agent.Spec.NodeSelector["kubernetes.io/os"])) + + if !agent.Spec.Prometheus.IsEmpty() { + volumeMounts = append(volumeMounts, getPrometheusVolumeMounts(agent.Spec.NodeSelector["kubernetes.io/os"])) + } } // ensure that the v1alpha1.AmazonCloudWatchAgentSpec.Args are ordered when moved to container.Args, @@ -76,6 +80,18 @@ func Container(cfg config.Config, logger logr.Logger, agent v1alpha1.AmazonCloud }, }) + if agent.Spec.TargetAllocator.Enabled { + // We need to add a SHARD here so the collector is able to keep targets after the hashmod operation which is + // added by default by the Prometheus operator's config generator. + // All collector instances use SHARD == 0 as they only receive targets + // allocated to them and should not use the Prometheus hashmod-based + // allocation. + envVars = append(envVars, corev1.EnvVar{ + Name: "SHARD", + Value: "0", + }) + } + if _, err := adapters.ConfigFromJSONString(agent.Spec.Config); err != nil { logger.Error(err, "error parsing config") } @@ -126,6 +142,22 @@ func getVolumeMounts(os string) corev1.VolumeMount { return volumeMount } +func getPrometheusVolumeMounts(os string) corev1.VolumeMount { + var volumeMount corev1.VolumeMount + if os == "windows" { + volumeMount = corev1.VolumeMount{ + Name: naming.PrometheusConfigMapVolume(), + MountPath: "C:\\Program Files\\Amazon\\AmazonCloudWatchAgent\\prometheusconfig", + } + } else { + volumeMount = corev1.VolumeMount{ + Name: naming.PrometheusConfigMapVolume(), + MountPath: "/etc/prometheusconfig", + } + } + return volumeMount +} + func portMapToContainerPortList(portMap map[string]corev1.ContainerPort) []corev1.ContainerPort { ports := make([]corev1.ContainerPort, 0, len(portMap)) for _, p := range portMap { diff --git a/internal/manifests/collector/container_test.go b/internal/manifests/collector/container_test.go index 26c08dd3f..32a6b9ae8 100644 --- a/internal/manifests/collector/container_test.go +++ b/internal/manifests/collector/container_test.go @@ -44,6 +44,17 @@ func TestGetVolumeMounts(t *testing.T) { assert.Equal(t, volumeMount.MountPath, "/etc/cwagentconfig") } +func TestGetPrometheusVolumeMounts(t *testing.T) { + volumeMount := getPrometheusVolumeMounts("windows") + assert.Equal(t, volumeMount.MountPath, "C:\\Program Files\\Amazon\\AmazonCloudWatchAgent\\prometheusconfig") + + volumeMount = getPrometheusVolumeMounts("linux") + assert.Equal(t, volumeMount.MountPath, "/etc/prometheusconfig") + + volumeMount = getPrometheusVolumeMounts("") + assert.Equal(t, volumeMount.MountPath, "/etc/prometheusconfig") +} + func TestContainerPorts(t *testing.T) { var sampleJSONConfig = `{ "logs": { diff --git a/internal/manifests/collector/ingress_test.go b/internal/manifests/collector/ingress_test.go index a219053d9..c87bb3eef 100644 --- a/internal/manifests/collector/ingress_test.go +++ b/internal/manifests/collector/ingress_test.go @@ -14,11 +14,10 @@ import ( networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" - "github.com/aws/amazon-cloudwatch-agent-operator/internal/naming" - "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" "github.com/aws/amazon-cloudwatch-agent-operator/internal/config" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/naming" ) const testFileIngress = "testdata/ingress_testdata.yaml" diff --git a/internal/manifests/collector/podmonitor_test.go b/internal/manifests/collector/podmonitor_test.go index c7614c061..5cca1f744 100644 --- a/internal/manifests/collector/podmonitor_test.go +++ b/internal/manifests/collector/podmonitor_test.go @@ -7,13 +7,12 @@ package collector import ( "fmt" - - "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" - "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" + "testing" "github.com/stretchr/testify/assert" - "testing" + "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" ) func sidecarParams() manifests.Params { diff --git a/internal/manifests/collector/ports.go b/internal/manifests/collector/ports.go index d4cab62b8..6a137c701 100644 --- a/internal/manifests/collector/ports.go +++ b/internal/manifests/collector/ports.go @@ -13,12 +13,11 @@ import ( "github.com/go-logr/logr" "go.uber.org/zap" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/validation" "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/collector/adapters" "github.com/aws/amazon-cloudwatch-agent-operator/internal/naming" - - corev1 "k8s.io/api/core/v1" ) const ( diff --git a/internal/manifests/collector/service_test.go b/internal/manifests/collector/service_test.go index 47c373d3a..153c5ebdc 100644 --- a/internal/manifests/collector/service_test.go +++ b/internal/manifests/collector/service_test.go @@ -12,11 +12,10 @@ import ( v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" - "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/manifestutils" - "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" "github.com/aws/amazon-cloudwatch-agent-operator/internal/config" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/manifestutils" ) func TestExtractPortNumbersAndNames(t *testing.T) { diff --git a/internal/manifests/collector/testdata/config_expected_targetallocator.yaml b/internal/manifests/collector/testdata/config_expected_targetallocator.yaml new file mode 100644 index 000000000..4cf98f290 --- /dev/null +++ b/internal/manifests/collector/testdata/config_expected_targetallocator.yaml @@ -0,0 +1,8 @@ +config: + global: + evaluation_interval: 1m + scrape_interval: 1m + scrape_timeout: 10s +target_allocator: + endpoint: https://test-target-allocator-service:80 + interval: 30s \ No newline at end of file diff --git a/internal/manifests/collector/testdata/http_sd_config_servicemonitor_test.yaml b/internal/manifests/collector/testdata/http_sd_config_servicemonitor_test.yaml new file mode 100644 index 000000000..e755e8165 --- /dev/null +++ b/internal/manifests/collector/testdata/http_sd_config_servicemonitor_test.yaml @@ -0,0 +1,12 @@ +config: + scrape_configs: + - job_name: serviceMonitor/test/test/0 + + static_configs: + - targets: ["prom.domain:1001", "prom.domain:1002", "prom.domain:1003"] + labels: + my: label + + file_sd_configs: + - files: + - file2.json \ No newline at end of file diff --git a/internal/manifests/collector/testdata/http_sd_config_servicemonitor_test_ta_set.yaml b/internal/manifests/collector/testdata/http_sd_config_servicemonitor_test_ta_set.yaml new file mode 100644 index 000000000..c3fbebba4 --- /dev/null +++ b/internal/manifests/collector/testdata/http_sd_config_servicemonitor_test_ta_set.yaml @@ -0,0 +1,17 @@ +config: + scrape_configs: + - job_name: serviceMonitor/test/test/0 + + static_configs: + - targets: ["prom.domain:1001", "prom.domain:1002", "prom.domain:1003"] + labels: + my: label + + file_sd_configs: + - files: + - file2.json +target_allocator: + endpoint: https://test-target-allocator-service:80 + interval: 30s + http_sd_config: + refresh_interval: 60s \ No newline at end of file diff --git a/internal/manifests/collector/testdata/http_sd_config_ta_test.yaml b/internal/manifests/collector/testdata/http_sd_config_ta_test.yaml new file mode 100644 index 000000000..68981a74b --- /dev/null +++ b/internal/manifests/collector/testdata/http_sd_config_ta_test.yaml @@ -0,0 +1,11 @@ +config: + scrape_configs: + - job_name: prometheus + + static_configs: + - targets: ["prom.domain:9001", "prom.domain:9002", "prom.domain:9003"] + labels: + my: label +target_allocator: + endpoint: https://test-sd-target-allocator:80 + interval: 60s \ No newline at end of file diff --git a/internal/manifests/collector/testdata/http_sd_config_test.yaml b/internal/manifests/collector/testdata/http_sd_config_test.yaml new file mode 100644 index 000000000..8b88fbeda --- /dev/null +++ b/internal/manifests/collector/testdata/http_sd_config_test.yaml @@ -0,0 +1,23 @@ +config: + scrape_configs: + - job_name: prometheus + + static_configs: + - targets: ["prom.domain:9001", "prom.domain:9002", "prom.domain:9003"] + labels: + my: label + + file_sd_configs: + - files: + - file1.json + + - job_name: service-x + + static_configs: + - targets: ["prom.domain:1001", "prom.domain:1002", "prom.domain:1003"] + labels: + my: label + + file_sd_configs: + - files: + - file2.json \ No newline at end of file diff --git a/internal/manifests/collector/testdata/prometheus_test.yaml b/internal/manifests/collector/testdata/prometheus_test.yaml new file mode 100644 index 000000000..bb8b3bfd6 --- /dev/null +++ b/internal/manifests/collector/testdata/prometheus_test.yaml @@ -0,0 +1,6 @@ +config: + scrape_configs: + - job_name: 'cloudwatch-agent' + scrape_interval: 10s + static_configs: + - targets: [ '0.0.0.0:8888' ] \ No newline at end of file diff --git a/internal/manifests/collector/testdata/relabel_config_expected_with_sd_config.yaml b/internal/manifests/collector/testdata/relabel_config_expected_with_sd_config.yaml new file mode 100644 index 000000000..17733e048 --- /dev/null +++ b/internal/manifests/collector/testdata/relabel_config_expected_with_sd_config.yaml @@ -0,0 +1,44 @@ +config: + global: + evaluation_interval: 1m + scrape_interval: 1m + scrape_timeout: 10s + scrape_configs: + - honor_labels: true + http_sd_configs: + - url: https://test-target-allocator-service:80/jobs/service-x/targets + job_name: service-x + metric_relabel_configs: + - action: keep + regex: (.*) + separator: ; + source_labels: + - label1 + - action: labelmap + regex: (.*) + separator: ; + source_labels: + - label4 + metrics_path: /metrics + relabel_configs: + - action: keep + regex: (.*) + source_labels: + - label1 + - action: replace + regex: (.*) + replacement: $1_$2 + separator: ; + source_labels: + - label2 + target_label: label3 + - action: labelmap + regex: (.*) + separator: ; + source_labels: + - label4 + - action: labeldrop + regex: foo_.* + scheme: http + scrape_interval: 1m + scrape_timeout: 10s \ No newline at end of file diff --git a/internal/manifests/collector/testdata/relabel_config_original.yaml b/internal/manifests/collector/testdata/relabel_config_original.yaml new file mode 100644 index 000000000..dc7a4f6de --- /dev/null +++ b/internal/manifests/collector/testdata/relabel_config_original.yaml @@ -0,0 +1,37 @@ +config: + global: + evaluation_interval: 1m + scrape_interval: 1m + scrape_timeout: 10s + scrape_configs: + - job_name: service-x + metrics_path: /metrics + scheme: http + scrape_interval: 1m + scrape_timeout: 10s + honor_labels: true + relabel_configs: + - source_labels: [label1] + action: keep + regex: (.*) + - target_label: label3 + source_labels: [label2] + action: replace + regex: (.*) + replacement: "$1_$2" + separator: ";" + - source_labels: [label4] + action: labelmap + regex: (.*) + separator: ";" + - regex: foo_.* + action: labeldrop + metric_relabel_configs: + - source_labels: [label1] + action: keep + regex: (.*) + separator: ";" + - regex: (.*) + action: labelmap + separator: ";" + source_labels: [label4] \ No newline at end of file diff --git a/internal/manifests/collector/volume.go b/internal/manifests/collector/volume.go index 50c2d1f21..40622d555 100644 --- a/internal/manifests/collector/volume.go +++ b/internal/manifests/collector/volume.go @@ -38,6 +38,23 @@ func Volumes(cfg config.Config, otelcol v1alpha1.AmazonCloudWatchAgent) []corev1 }, }} + if !otelcol.Spec.Prometheus.IsEmpty() { + volumes = append(volumes, corev1.Volume{ + Name: naming.PrometheusConfigMapVolume(), + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: naming.PrometheusConfigMap(otelcol.Name), + }, + Items: []corev1.KeyToPath{{ + Key: cfg.PrometheusConfigMapEntry(), + Path: cfg.PrometheusConfigMapEntry(), + }}, + }, + }, + }) + } + if len(otelcol.Spec.Volumes) > 0 { volumes = append(volumes, otelcol.Spec.Volumes...) } diff --git a/internal/manifests/collector/volume_test.go b/internal/manifests/collector/volume_test.go index a86459af5..54590aa23 100644 --- a/internal/manifests/collector/volume_test.go +++ b/internal/manifests/collector/volume_test.go @@ -76,3 +76,46 @@ func TestVolumeWithMoreConfigMaps(t *testing.T) { assert.Equal(t, "configmap-configmap-test", volumes[1].Name) assert.Equal(t, "configmap-configmap-test2", volumes[2].Name) } + +func TestVolumePrometheus(t *testing.T) { + // prepare + otelcol := v1alpha1.AmazonCloudWatchAgent{ + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + Prometheus: v1alpha1.PrometheusConfig{Config: &v1alpha1.AnyConfig{}}, + }, + } + + cfg := config.New() + + // test + volumes := Volumes(cfg, otelcol) + + // verify + assert.Len(t, volumes, 2) + + // check that it's the otc-internal volume, with the config map + assert.Equal(t, naming.ConfigMapVolume(), volumes[0].Name) + + // check that the second volume is prometheus-config, with the config map + assert.Equal(t, naming.PrometheusConfigMapVolume(), volumes[1].Name) +} + +func TestVolumeNoPrometheus(t *testing.T) { + // prepare + otelcol := v1alpha1.AmazonCloudWatchAgent{ + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + Prometheus: v1alpha1.PrometheusConfig{}, + }, + } + + cfg := config.New() + + // test + volumes := Volumes(cfg, otelcol) + + // verify + assert.Len(t, volumes, 1) + + // check that it's not the prometheus-config volume, with the config map + assert.NotEqual(t, naming.PrometheusConfigMapVolume(), volumes[0].Name) +} diff --git a/internal/manifests/dcgmexporter/service_test.go b/internal/manifests/dcgmexporter/service_test.go index 3dc189dc6..f17d4c349 100644 --- a/internal/manifests/dcgmexporter/service_test.go +++ b/internal/manifests/dcgmexporter/service_test.go @@ -13,11 +13,10 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" logf "sigs.k8s.io/controller-runtime/pkg/log" - "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" - "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/manifestutils" - "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" "github.com/aws/amazon-cloudwatch-agent-operator/internal/config" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/manifestutils" ) var logger = logf.Log.WithName("unit-tests") diff --git a/internal/manifests/manifestutils/labels.go b/internal/manifests/manifestutils/labels.go index 68fdf1fe6..7bc374664 100644 --- a/internal/manifests/manifestutils/labels.go +++ b/internal/manifests/manifestutils/labels.go @@ -37,19 +37,21 @@ func Labels(instance metav1.ObjectMeta, name string, image string, component str base[k] = v } - version := strings.Split(image, ":") - for _, v := range version { - if strings.HasSuffix(v, "@sha256") { - versionLabel = strings.TrimSuffix(v, "@sha256") + if len(image) > 0 { + version := strings.Split(image, ":") + for _, v := range version { + if strings.HasSuffix(v, "@sha256") { + versionLabel = strings.TrimSuffix(v, "@sha256") + } + } + switch lenVersion := len(version); lenVersion { + case 3: + base["app.kubernetes.io/version"] = versionLabel + case 2: + base["app.kubernetes.io/version"] = naming.Truncate("%s", 63, version[len(version)-1]) + default: + base["app.kubernetes.io/version"] = "latest" } - } - switch lenVersion := len(version); lenVersion { - case 3: - base["app.kubernetes.io/version"] = versionLabel - case 2: - base["app.kubernetes.io/version"] = naming.Truncate("%s", 63, version[len(version)-1]) - default: - base["app.kubernetes.io/version"] = "latest" } // Don't override the app name if it already exists @@ -70,3 +72,11 @@ func SelectorLabels(instance metav1.ObjectMeta, component string) map[string]str "app.kubernetes.io/component": component, } } + +func SelectorLabelsForAllOperatorManaged(instance metav1.ObjectMeta) map[string]string { + return map[string]string{ + "app.kubernetes.io/managed-by": "amazon-cloudwatch-agent-operator", + "app.kubernetes.io/instance": naming.Truncate("%s.%s", 63, instance.Namespace, instance.Name), + "app.kubernetes.io/part-of": "amazon-cloudwatch-agent", + } +} diff --git a/internal/manifests/neuronmonitor/service_test.go b/internal/manifests/neuronmonitor/service_test.go index ccabcb315..505c0ead8 100644 --- a/internal/manifests/neuronmonitor/service_test.go +++ b/internal/manifests/neuronmonitor/service_test.go @@ -13,11 +13,10 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" logf "sigs.k8s.io/controller-runtime/pkg/log" - "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" - "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/manifestutils" - "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" "github.com/aws/amazon-cloudwatch-agent-operator/internal/config" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/manifestutils" ) var logger = logf.Log.WithName("unit-tests") diff --git a/internal/manifests/targetallocator/adapters/config_to_prom_config.go b/internal/manifests/targetallocator/adapters/config_to_prom_config.go new file mode 100644 index 000000000..c1ef3101e --- /dev/null +++ b/internal/manifests/targetallocator/adapters/config_to_prom_config.go @@ -0,0 +1,284 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package adapters + +import ( + "errors" + "fmt" + "net/url" + "regexp" + + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/collector/adapters" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/naming" +) + +func errorNoComponent(component string) error { + return fmt.Errorf("no %s available as part of the configuration", component) +} + +func errorNotAMapAtIndex(component string, index int) error { + return fmt.Errorf("index %d: %s property in the configuration doesn't contain a valid map: %s", index, component, component) +} + +func errorNotAMap(component string) error { + return fmt.Errorf("%s property in the configuration doesn't contain valid %s", component, component) +} + +func errorNotAList(component string) error { + return fmt.Errorf("%s must be a list in the config", component) +} + +func errorNotAListAtIndex(component string, index int) error { + return fmt.Errorf("index %d: %s property in the configuration doesn't contain a valid index: %s", index, component, component) +} + +func errorNotAStringAtIndex(component string, index int) error { + return fmt.Errorf("index %d: %s property in the configuration doesn't contain a valid string: %s", index, component, component) +} + +// getScrapeConfigsFromPromConfig extracts the scrapeConfig array from prometheus config. +func getScrapeConfigsFromPromConfig(promConfig map[interface{}]interface{}) ([]interface{}, error) { + prometheusConfigProperty, ok := promConfig["config"] + if !ok { + return nil, errorNoComponent("prometheusConfig") + } + + prometheusConfig, ok := prometheusConfigProperty.(map[interface{}]interface{}) + if !ok { + return nil, errorNotAMap("prometheusConfig") + } + + scrapeConfigsProperty, ok := prometheusConfig["scrape_configs"] + if !ok { + return nil, errorNoComponent("scrape_configs") + } + + scrapeConfigs, ok := scrapeConfigsProperty.([]interface{}) + if !ok { + return nil, errorNotAList("scrape_configs") + } + + return scrapeConfigs, nil +} + +// GetPromConfig returns a Prometheus configuration file. +func GetPromConfig(cfg string) (map[interface{}]interface{}, error) { + prometheus, err := adapters.ConfigFromString(cfg) + if err != nil { + return nil, err + } + + scrapeConfigs, err := getScrapeConfigsFromPromConfig(prometheus) + if err != nil { + return nil, err + } + + for i, config := range scrapeConfigs { + scrapeConfig, ok := config.(map[interface{}]interface{}) + if !ok { + return nil, errorNotAMapAtIndex("scrape_config", i) + } + + relabelConfigsProperty, ok := scrapeConfig["relabel_configs"] + if !ok { + continue + } + + relabelConfigs, ok := relabelConfigsProperty.([]interface{}) + if !ok { + return nil, errorNotAListAtIndex("relabel_configs", i) + } + + for i, rc := range relabelConfigs { + relabelConfig, rcErr := rc.(map[interface{}]interface{}) + if !rcErr { + return nil, errorNotAMapAtIndex("relabel_config", i) + } + + replacementProperty, rcErr := relabelConfig["replacement"] + if !rcErr { + continue + } + + _, rcErr = replacementProperty.(string) + if !rcErr { + return nil, errorNotAStringAtIndex("replacement", i) + } + } + + metricRelabelConfigsProperty, ok := scrapeConfig["metric_relabel_configs"] + if !ok { + continue + } + + metricRelabelConfigs, ok := metricRelabelConfigsProperty.([]interface{}) + if !ok { + return nil, errorNotAListAtIndex("metric_relabel_configs", i) + } + + for i, rc := range metricRelabelConfigs { + relabelConfig, ok := rc.(map[interface{}]interface{}) + if !ok { + return nil, errorNotAMapAtIndex("metric_relabel_config", i) + } + + replacementProperty, ok := relabelConfig["replacement"] + if !ok { + continue + } + + _, ok = replacementProperty.(string) + if !ok { + return nil, errorNotAStringAtIndex("replacement", i) + } + } + } + + return prometheus, nil +} + +// AddHTTPSDConfigToPromConfig adds HTTP SD (Service Discovery) configuration to the Prometheus configuration. +// This function removes any existing service discovery configurations (e.g., `sd_configs`, `dns_sd_configs`, `file_sd_configs`, etc.) +// from the `scrape_configs` section and adds a single `http_sd_configs` configuration. +// The `http_sd_configs` points to the TA (Target Allocator) endpoint that provides the list of targets for the given job. +func AddHTTPSDConfigToPromConfig(prometheus map[interface{}]interface{}, taServiceName string) (map[interface{}]interface{}, error) { + prometheusConfigProperty, ok := prometheus["config"] + if !ok { + return nil, errorNoComponent("prometheusConfig") + } + + prometheusConfig, ok := prometheusConfigProperty.(map[interface{}]interface{}) + if !ok { + return nil, errorNotAMap("prometheusConfig") + } + + scrapeConfigsProperty, ok := prometheusConfig["scrape_configs"] + if !ok { + return nil, errorNoComponent("scrape_configs") + } + + scrapeConfigs, ok := scrapeConfigsProperty.([]interface{}) + if !ok { + return nil, errorNotAList("scrape_configs") + } + + sdRegex := regexp.MustCompile(`^.*(sd|static)_configs$`) + + for i, config := range scrapeConfigs { + scrapeConfig, ok := config.(map[interface{}]interface{}) + if !ok { + return nil, errorNotAMapAtIndex("scrape_config", i) + } + + // Check for other types of service discovery configs (e.g. dns_sd_configs, file_sd_configs, etc.) + for key := range scrapeConfig { + keyStr, keyErr := key.(string) + if !keyErr { + continue + } + if sdRegex.MatchString(keyStr) { + delete(scrapeConfig, key) + } + } + + jobNameProperty, ok := scrapeConfig["job_name"] + if !ok { + return nil, errorNotAStringAtIndex("job_name", i) + } + + jobName, ok := jobNameProperty.(string) + if !ok { + return nil, errorNotAStringAtIndex("job_name is not a string", i) + } + + escapedJob := url.QueryEscape(jobName) + scrapeConfig["http_sd_configs"] = []interface{}{ + map[string]interface{}{ + "url": fmt.Sprintf("https://%s:%d/jobs/%s/targets", taServiceName, naming.TargetAllocatorServicePort, escapedJob), + }, + } + } + + return prometheus, nil +} + +// AddTAConfigToPromConfig adds or updates the target_allocator configuration in the Prometheus configuration. +// If the `EnableTargetAllocatorRewrite` feature flag for the target allocator is enabled, this function +// removes the existing scrape_configs from the collector's Prometheus configuration as it's not required. +func AddTAConfigToPromConfig(prometheus map[interface{}]interface{}, taServiceName string) (map[interface{}]interface{}, error) { + prometheusConfigProperty, ok := prometheus["config"] + if !ok { + return nil, errorNoComponent("prometheusConfig") + } + + prometheusCfg, ok := prometheusConfigProperty.(map[interface{}]interface{}) + if !ok { + return nil, errorNotAMap("prometheusConfig") + } + + // Create the TargetAllocConfig dynamically if it doesn't exist + if prometheus["target_allocator"] == nil { + prometheus["target_allocator"] = make(map[interface{}]interface{}) + } + + targetAllocatorCfg, ok := prometheus["target_allocator"].(map[interface{}]interface{}) + if !ok { + return nil, errorNotAMap("target_allocator") + } + + targetAllocatorCfg["endpoint"] = fmt.Sprintf("https://%s:%d", taServiceName, naming.TargetAllocatorServicePort) + targetAllocatorCfg["interval"] = "30s" + + // Remove the scrape_configs key from the map + delete(prometheusCfg, "scrape_configs") + + return prometheus, nil +} + +// ValidatePromConfig checks if the prometheus config is valid given other collector-level settings. +func ValidatePromConfig(config map[interface{}]interface{}, targetAllocatorEnabled bool, targetAllocatorRewriteEnabled bool) error { + _, promConfigExists := config["config"] + + if targetAllocatorEnabled { + if targetAllocatorRewriteEnabled { // if rewrite is enabled, we will add a target_allocator section during rewrite + return nil + } + _, targetAllocatorExists := config["target_allocator"] + + // otherwise, either the target_allocator or config section needs to be here + if !(promConfigExists || targetAllocatorExists) { + return errors.New("either target allocator or prometheus config needs to be present") + } + + return nil + } + // if target allocator isn't enabled, we need a config section + if !promConfigExists { + return errorNoComponent("prometheusConfig") + } + + return nil +} + +// ValidateTargetAllocatorConfig checks if the Target Allocator config is valid +// In order for Target Allocator to do anything useful, at least one of the following has to be true: +// - at least one scrape config has to be defined in Prometheus configuration +// - PrometheusCR has to be enabled in target allocator settings +func ValidateTargetAllocatorConfig(targetAllocatorPrometheusCR bool, promConfig map[interface{}]interface{}) error { + + if targetAllocatorPrometheusCR { + return nil + } + // if PrometheusCR isn't enabled, we need at least one scrape config + scrapeConfigs, err := getScrapeConfigsFromPromConfig(promConfig) + if err != nil { + return err + } + + if len(scrapeConfigs) == 0 { + return fmt.Errorf("either at least one scrape config needs to be defined or PrometheusCR needs to be enabled") + } + + return nil +} diff --git a/internal/manifests/targetallocator/adapters/config_to_prom_config_test.go b/internal/manifests/targetallocator/adapters/config_to_prom_config_test.go new file mode 100644 index 000000000..3948c6f60 --- /dev/null +++ b/internal/manifests/targetallocator/adapters/config_to_prom_config_test.go @@ -0,0 +1,395 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package adapters_test + +import ( + "errors" + "fmt" + "net/url" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/collector/adapters" + ta "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/targetallocator/adapters" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/naming" +) + +func TestExtractPromConfigFromConfig(t *testing.T) { + configStr := ` +config: + scrape_config: + job_name: otel-collector + scrape_interval: 10s +` + expectedData := map[interface{}]interface{}{ + "config": map[interface{}]interface{}{ + "scrape_config": map[interface{}]interface{}{ + "job_name": "otel-collector", + "scrape_interval": "10s", + }, + }, + } + + // test + promConfig, err := adapters.ConfigFromString(configStr) + assert.NoError(t, err) + + // verify + assert.Equal(t, expectedData, promConfig) +} + +func TestExtractPromConfigWithTAConfigFromConfig(t *testing.T) { + configStr := ` +config: + scrape_config: + job_name: otel-collector + scrape_interval: 10s +target_allocator: + endpoint: "test:80" +` + expectedData := map[interface{}]interface{}{ + "config": map[interface{}]interface{}{ + "scrape_config": map[interface{}]interface{}{ + "job_name": "otel-collector", + "scrape_interval": "10s", + }, + }, + "target_allocator": map[interface{}]interface{}{ + "endpoint": "test:80", + }, + } + + // test + promConfig, err := adapters.ConfigFromString(configStr) + assert.NoError(t, err) + + // verify + assert.Equal(t, expectedData, promConfig) +} + +func TestGetPromConfig(t *testing.T) { + actual := ` +config: + scrape_configs: + - job_name: 'example' + relabel_configs: + - source_labels: ['__meta_service_id'] + target_label: 'job' + replacement: 'my_service_$1' + - source_labels: ['__meta_service_name'] + target_label: 'instance' + replacement: '$1' + metric_relabel_configs: + - source_labels: ['job'] + target_label: 'job' + replacement: '$1_$2' +` + expected := ` +config: + scrape_configs: + - job_name: 'example' + relabel_configs: + - source_labels: ['__meta_service_id'] + target_label: 'job' + replacement: 'my_service_$1' + - source_labels: ['__meta_service_name'] + target_label: 'instance' + replacement: '$1' + metric_relabel_configs: + - source_labels: ['job'] + target_label: 'job' + replacement: '$1_$2' +` + + config, err := ta.GetPromConfig(actual) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + expectedConfig, err := ta.GetPromConfig(expected) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if !reflect.DeepEqual(config, expectedConfig) { + t.Errorf("unexpected config: got %v, want %v", config, expectedConfig) + } +} + +func TestAddHTTPSDConfigToPromConfig(t *testing.T) { + t.Run("ValidConfiguration, add http_sd_config", func(t *testing.T) { + cfg := map[interface{}]interface{}{ + "config": map[interface{}]interface{}{ + "scrape_configs": []interface{}{ + map[interface{}]interface{}{ + "job_name": "test_job", + "static_configs": []interface{}{ + map[interface{}]interface{}{ + "targets": []interface{}{ + "localhost:9090", + }, + }, + }, + }, + }, + }, + } + taServiceName := "test-service" + expectedCfg := map[interface{}]interface{}{ + "config": map[interface{}]interface{}{ + "scrape_configs": []interface{}{ + map[interface{}]interface{}{ + "job_name": "test_job", + "http_sd_configs": []interface{}{ + map[string]interface{}{ + "url": fmt.Sprintf("https://%s:80/jobs/%s/targets", taServiceName, url.QueryEscape("test_job")), + }, + }, + }, + }, + }, + } + + actualCfg, err := ta.AddHTTPSDConfigToPromConfig(cfg, taServiceName) + assert.NoError(t, err) + assert.Equal(t, expectedCfg, actualCfg) + }) + + t.Run("invalid config property, returns error", func(t *testing.T) { + cfg := map[interface{}]interface{}{ + "config": map[interface{}]interface{}{ + "job_name": "test_job", + "static_configs": []interface{}{ + map[interface{}]interface{}{ + "targets": []interface{}{ + "localhost:9090", + }, + }, + }, + }, + } + + taServiceName := "test-service" + + _, err := ta.AddHTTPSDConfigToPromConfig(cfg, taServiceName) + assert.Error(t, err) + assert.EqualError(t, err, "no scrape_configs available as part of the configuration") + }) +} + +func TestAddTAConfigToPromConfig(t *testing.T) { + collectorName := "test-collector" + t.Run("should return expected prom config map with TA config", func(t *testing.T) { + cfg := map[interface{}]interface{}{ + "config": map[interface{}]interface{}{ + "scrape_configs": []interface{}{ + map[interface{}]interface{}{ + "job_name": "test_job", + "static_configs": []interface{}{ + map[interface{}]interface{}{ + "targets": []interface{}{ + "localhost:9090", + }, + }, + }, + }, + }, + }, + } + + expectedResult := map[interface{}]interface{}{ + "config": map[interface{}]interface{}{}, + "target_allocator": map[interface{}]interface{}{ + "endpoint": fmt.Sprintf("https://%s-target-allocator-service:80", collectorName), + "interval": "30s", + }, + } + + result, err := ta.AddTAConfigToPromConfig(cfg, naming.TAService(collectorName)) + + assert.NoError(t, err) + assert.Equal(t, expectedResult, result) + }) + + t.Run("missing or invalid prometheusConfig property, returns error", func(t *testing.T) { + testCases := []struct { + name string + cfg map[interface{}]interface{} + errText string + }{ + { + name: "missing config property", + cfg: map[interface{}]interface{}{}, + errText: "no prometheusConfig available as part of the configuration", + }, + { + name: "invalid config property", + cfg: map[interface{}]interface{}{ + "config": "invalid", + }, + errText: "prometheusConfig property in the configuration doesn't contain valid prometheusConfig", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err := ta.AddTAConfigToPromConfig(tc.cfg, naming.TAService(collectorName)) + + assert.Error(t, err) + assert.EqualError(t, err, tc.errText) + }) + } + }) +} + +func TestValidatePromConfig(t *testing.T) { + testCases := []struct { + description string + config map[interface{}]interface{} + targetAllocatorEnabled bool + targetAllocatorRewriteEnabled bool + expectedError error + }{ + { + description: "target_allocator and rewrite enabled", + config: map[interface{}]interface{}{}, + targetAllocatorEnabled: true, + targetAllocatorRewriteEnabled: true, + expectedError: nil, + }, + { + description: "target_allocator enabled, target_allocator section present", + config: map[interface{}]interface{}{ + "target_allocator": map[interface{}]interface{}{}, + }, + targetAllocatorEnabled: true, + targetAllocatorRewriteEnabled: false, + expectedError: nil, + }, + { + description: "target_allocator enabled, config section present", + config: map[interface{}]interface{}{ + "config": map[interface{}]interface{}{}, + }, + targetAllocatorEnabled: true, + targetAllocatorRewriteEnabled: false, + expectedError: nil, + }, + { + description: "target_allocator enabled, neither section present", + config: map[interface{}]interface{}{}, + targetAllocatorEnabled: true, + targetAllocatorRewriteEnabled: false, + expectedError: errors.New("either target allocator or prometheus config needs to be present"), + }, + { + description: "target_allocator disabled, config section present", + config: map[interface{}]interface{}{ + "config": map[interface{}]interface{}{}, + }, + targetAllocatorEnabled: false, + targetAllocatorRewriteEnabled: false, + expectedError: nil, + }, + { + description: "target_allocator disabled, config section not present", + config: map[interface{}]interface{}{}, + targetAllocatorEnabled: false, + targetAllocatorRewriteEnabled: false, + expectedError: fmt.Errorf("no %s available as part of the configuration", "prometheusConfig"), + }, + } + + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.description, func(t *testing.T) { + err := ta.ValidatePromConfig(testCase.config, testCase.targetAllocatorEnabled, testCase.targetAllocatorRewriteEnabled) + assert.Equal(t, testCase.expectedError, err) + }) + } +} + +func TestValidateTargetAllocatorConfig(t *testing.T) { + testCases := []struct { + description string + config map[interface{}]interface{} + targetAllocatorPrometheusCR bool + expectedError error + }{ + { + description: "scrape configs present and PrometheusCR enabled", + config: map[interface{}]interface{}{ + "config": map[interface{}]interface{}{ + "scrape_configs": []interface{}{ + map[interface{}]interface{}{ + "job_name": "test_job", + "static_configs": []interface{}{ + map[interface{}]interface{}{ + "targets": []interface{}{ + "localhost:9090", + }, + }, + }, + }, + }, + }, + }, + targetAllocatorPrometheusCR: true, + expectedError: nil, + }, + { + description: "scrape configs present and PrometheusCR disabled", + config: map[interface{}]interface{}{ + "config": map[interface{}]interface{}{ + "scrape_configs": []interface{}{ + map[interface{}]interface{}{ + "job_name": "test_job", + "static_configs": []interface{}{ + map[interface{}]interface{}{ + "targets": []interface{}{ + "localhost:9090", + }, + }, + }, + }, + }, + }, + }, + targetAllocatorPrometheusCR: false, + expectedError: nil, + }, + { + description: "config empty and PrometheusCR enabled", + config: map[interface{}]interface{}{}, + targetAllocatorPrometheusCR: true, + expectedError: nil, + }, + { + description: "config empty and PrometheusCR disabled", + config: map[interface{}]interface{}{}, + targetAllocatorPrometheusCR: false, + expectedError: fmt.Errorf("no %s available as part of the configuration", "prometheusConfig"), + }, + { + description: "scrape configs empty and PrometheusCR disabled", + config: map[interface{}]interface{}{ + "config": map[interface{}]interface{}{ + "scrape_configs": []interface{}{}, + }, + }, + targetAllocatorPrometheusCR: false, + expectedError: fmt.Errorf("either at least one scrape config needs to be defined or PrometheusCR needs to be enabled"), + }, + } + + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.description, func(t *testing.T) { + err := ta.ValidateTargetAllocatorConfig(testCase.targetAllocatorPrometheusCR, testCase.config) + assert.Equal(t, testCase.expectedError, err) + }) + } +} diff --git a/internal/manifests/targetallocator/annotations.go b/internal/manifests/targetallocator/annotations.go new file mode 100644 index 000000000..07e483f66 --- /dev/null +++ b/internal/manifests/targetallocator/annotations.go @@ -0,0 +1,43 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package targetallocator + +import ( + "crypto/sha256" + "fmt" + + v1 "k8s.io/api/core/v1" + + "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" +) + +const configMapHashAnnotationKey = "amazon-cloudwatch-agent-target-allocator-config/hash" + +// Annotations returns the annotations for the TargetAllocator Pod. +func Annotations(instance v1alpha1.AmazonCloudWatchAgent, configMap *v1.ConfigMap) map[string]string { + // Make a copy of PodAnnotations to be safe + annotations := make(map[string]string, len(instance.Spec.PodAnnotations)) + for key, value := range instance.Spec.PodAnnotations { + annotations[key] = value + } + + if configMap != nil { + cmHash := getConfigMapSHA(configMap) + if cmHash != "" { + annotations[configMapHashAnnotationKey] = getConfigMapSHA(configMap) + } + } + + return annotations +} + +// getConfigMapSHA returns the hash of the content of the TA ConfigMap. +func getConfigMapSHA(configMap *v1.ConfigMap) string { + configString, ok := configMap.Data[targetAllocatorFilename] + if !ok { + return "" + } + h := sha256.Sum256([]byte(configString)) + return fmt.Sprintf("%x", h) +} diff --git a/internal/manifests/targetallocator/annotations_test.go b/internal/manifests/targetallocator/annotations_test.go new file mode 100644 index 000000000..58b998791 --- /dev/null +++ b/internal/manifests/targetallocator/annotations_test.go @@ -0,0 +1,52 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package targetallocator + +import ( + "crypto/sha256" + "fmt" + "testing" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aws/amazon-cloudwatch-agent-operator/internal/config" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" +) + +func TestPodAnnotations(t *testing.T) { + instance := collectorInstance() + instance.Spec.PodAnnotations = map[string]string{ + "key": "value", + } + annotations := Annotations(instance, nil) + assert.Subset(t, annotations, instance.Spec.PodAnnotations) +} + +func TestConfigMapHash(t *testing.T) { + cfg := config.New() + instance := collectorInstance() + params := manifests.Params{ + OtelCol: instance, + Config: cfg, + Log: logr.Discard(), + } + expectedConfigMap, err := ConfigMap(params) + require.NoError(t, err) + expectedConfig := expectedConfigMap.Data[targetAllocatorFilename] + require.NotEmpty(t, expectedConfig) + expectedHash := sha256.Sum256([]byte(expectedConfig)) + annotations := Annotations(instance, expectedConfigMap) + require.Contains(t, annotations, configMapHashAnnotationKey) + cmHash := annotations[configMapHashAnnotationKey] + assert.Equal(t, fmt.Sprintf("%x", expectedHash), cmHash) +} + +func TestInvalidConfigNoHash(t *testing.T) { + instance := collectorInstance() + instance.Spec.Config = "" + annotations := Annotations(instance, nil) + require.NotContains(t, annotations, configMapHashAnnotationKey) +} diff --git a/internal/manifests/targetallocator/configmap.go b/internal/manifests/targetallocator/configmap.go new file mode 100644 index 000000000..ff5160f53 --- /dev/null +++ b/internal/manifests/targetallocator/configmap.go @@ -0,0 +1,92 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package targetallocator + +import ( + "fmt" + "strings" + + "gopkg.in/yaml.v2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/collector" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/manifestutils" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests/targetallocator/adapters" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/naming" +) + +const ( + targetAllocatorFilename = "targetallocator.yaml" +) + +func ConfigMap(params manifests.Params) (*corev1.ConfigMap, error) { + name := naming.TAConfigMap(params.OtelCol.Name) + version := strings.Split(params.OtelCol.Spec.TargetAllocator.Image, ":") + labels := Labels(params.OtelCol, name) + if len(version) > 1 { + labels["app.kubernetes.io/version"] = version[len(version)-1] + } else { + labels["app.kubernetes.io/version"] = "latest" + } + + promConfigYaml, err := params.OtelCol.Spec.Prometheus.Yaml() + if err != nil { + return &corev1.ConfigMap{}, fmt.Errorf("%s could not convert json to yaml", err) + } + + prometheusConfig, err := adapters.GetPromConfig(promConfigYaml) + if err != nil { + return &corev1.ConfigMap{}, err + } + + taConfig := make(map[interface{}]interface{}) + prometheusCRConfig := make(map[interface{}]interface{}) + taConfig["label_selector"] = manifestutils.SelectorLabels(params.OtelCol.ObjectMeta, collector.ComponentAmazonCloudWatchAgent) + // We only take the "config" from the returned object, if it's present + if prometheusConfig, ok := prometheusConfig["config"]; ok { + taConfig["config"] = prometheusConfig + } + + taConfig["allocation_strategy"] = v1alpha1.AmazonCloudWatchAgentTargetAllocatorAllocationStrategyConsistentHashing + + if len(params.OtelCol.Spec.TargetAllocator.FilterStrategy) > 0 { + taConfig["filter_strategy"] = params.OtelCol.Spec.TargetAllocator.FilterStrategy + } + + if params.OtelCol.Spec.TargetAllocator.PrometheusCR.ScrapeInterval.Size() > 0 { + prometheusCRConfig["scrape_interval"] = params.OtelCol.Spec.TargetAllocator.PrometheusCR.ScrapeInterval.Duration + } + + if params.OtelCol.Spec.TargetAllocator.PrometheusCR.ServiceMonitorSelector != nil { + taConfig["service_monitor_selector"] = ¶ms.OtelCol.Spec.TargetAllocator.PrometheusCR.ServiceMonitorSelector + } + + if params.OtelCol.Spec.TargetAllocator.PrometheusCR.PodMonitorSelector != nil { + taConfig["pod_monitor_selector"] = ¶ms.OtelCol.Spec.TargetAllocator.PrometheusCR.PodMonitorSelector + } + + if len(prometheusCRConfig) > 0 { + taConfig["prometheus_cr"] = prometheusCRConfig + } + + taConfigYAML, err := yaml.Marshal(taConfig) + if err != nil { + return &corev1.ConfigMap{}, err + } + + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: params.OtelCol.Namespace, + Labels: labels, + Annotations: params.OtelCol.Annotations, + }, + Data: map[string]string{ + targetAllocatorFilename: string(taConfigYAML), + }, + }, nil +} diff --git a/internal/manifests/targetallocator/configmap_test.go b/internal/manifests/targetallocator/configmap_test.go new file mode 100644 index 000000000..90f76948b --- /dev/null +++ b/internal/manifests/targetallocator/configmap_test.go @@ -0,0 +1,149 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package targetallocator + +import ( + "testing" + "time" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/aws/amazon-cloudwatch-agent-operator/internal/config" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" +) + +func TestDesiredConfigMap(t *testing.T) { + expectedLables := map[string]string{ + "app.kubernetes.io/managed-by": "amazon-cloudwatch-agent-operator", + "app.kubernetes.io/instance": "default.my-instance", + "app.kubernetes.io/part-of": "amazon-cloudwatch-agent", + "app.kubernetes.io/version": "latest", + } + + t.Run("should return expected target allocator config map", func(t *testing.T) { + expectedLables["app.kubernetes.io/component"] = "amazon-cloudwatch-agent-target-allocator" + expectedLables["app.kubernetes.io/name"] = "my-instance-target-allocator" + + expectedData := map[string]string{ + "targetallocator.yaml": `allocation_strategy: consistent-hashing +config: + scrape_configs: + - job_name: otel-collector + scrape_interval: 10s + static_configs: + - targets: + - 0.0.0.0:8888 + - 0.0.0.0:9999 +label_selector: + app.kubernetes.io/component: amazon-cloudwatch-agent + app.kubernetes.io/instance: default.my-instance + app.kubernetes.io/managed-by: amazon-cloudwatch-agent-operator + app.kubernetes.io/part-of: amazon-cloudwatch-agent +`, + } + instance := collectorInstance() + cfg := config.New() + params := manifests.Params{ + OtelCol: instance, + Config: cfg, + Log: logr.Discard(), + } + actual, err := ConfigMap(params) + assert.NoError(t, err) + + assert.Equal(t, "my-instance-target-allocator", actual.Name) + assert.Equal(t, expectedLables, actual.Labels) + assert.Equal(t, expectedData, actual.Data) + + }) + t.Run("should return expected target allocator config map with label selectors", func(t *testing.T) { + expectedLables["app.kubernetes.io/component"] = "amazon-cloudwatch-agent-target-allocator" + expectedLables["app.kubernetes.io/name"] = "my-instance-target-allocator" + + expectedData := map[string]string{ + "targetallocator.yaml": `allocation_strategy: consistent-hashing +config: + scrape_configs: + - job_name: otel-collector + scrape_interval: 10s + static_configs: + - targets: + - 0.0.0.0:8888 + - 0.0.0.0:9999 +label_selector: + app.kubernetes.io/component: amazon-cloudwatch-agent + app.kubernetes.io/instance: default.my-instance + app.kubernetes.io/managed-by: amazon-cloudwatch-agent-operator + app.kubernetes.io/part-of: amazon-cloudwatch-agent +pod_monitor_selector: + release: my-instance +service_monitor_selector: + release: my-instance +`, + } + instance := collectorInstance() + instance.Spec.TargetAllocator.PrometheusCR.PodMonitorSelector = map[string]string{ + "release": "my-instance", + } + instance.Spec.TargetAllocator.PrometheusCR.ServiceMonitorSelector = map[string]string{ + "release": "my-instance", + } + cfg := config.New() + params := manifests.Params{ + OtelCol: instance, + Config: cfg, + Log: logr.Discard(), + } + actual, err := ConfigMap(params) + assert.NoError(t, err) + + assert.Equal(t, "my-instance-target-allocator", actual.Name) + assert.Equal(t, expectedLables, actual.Labels) + assert.Equal(t, expectedData, actual.Data) + + }) + t.Run("should return expected target allocator config map with scrape interval set", func(t *testing.T) { + expectedLables["app.kubernetes.io/component"] = "amazon-cloudwatch-agent-target-allocator" + expectedLables["app.kubernetes.io/name"] = "my-instance-target-allocator" + + expectedData := map[string]string{ + "targetallocator.yaml": `allocation_strategy: consistent-hashing +config: + scrape_configs: + - job_name: otel-collector + scrape_interval: 10s + static_configs: + - targets: + - 0.0.0.0:8888 + - 0.0.0.0:9999 +label_selector: + app.kubernetes.io/component: amazon-cloudwatch-agent + app.kubernetes.io/instance: default.my-instance + app.kubernetes.io/managed-by: amazon-cloudwatch-agent-operator + app.kubernetes.io/part-of: amazon-cloudwatch-agent +prometheus_cr: + scrape_interval: 30s +`, + } + + collector := collectorInstance() + collector.Spec.TargetAllocator.PrometheusCR.ScrapeInterval = &metav1.Duration{Duration: time.Second * 30} + cfg := config.New() + params := manifests.Params{ + OtelCol: collector, + Config: cfg, + Log: logr.Discard(), + } + actual, err := ConfigMap(params) + assert.NoError(t, err) + + assert.Equal(t, "my-instance-target-allocator", actual.Name) + assert.Equal(t, expectedLables, actual.Labels) + assert.Equal(t, expectedData, actual.Data) + + }) + +} diff --git a/internal/manifests/targetallocator/container.go b/internal/manifests/targetallocator/container.go new file mode 100644 index 000000000..3d757d0ab --- /dev/null +++ b/internal/manifests/targetallocator/container.go @@ -0,0 +1,78 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package targetallocator + +import ( + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + + "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/config" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/naming" +) + +// This has to be a constant so that TA container code can access it as well +const TACertMountPath = "/etc/amazon-cloudwatch-target-allocator-cert" + +// Container builds a container for the given TargetAllocator. +func Container(cfg config.Config, logger logr.Logger, otelcol v1alpha1.AmazonCloudWatchAgent) corev1.Container { + image := otelcol.Spec.TargetAllocator.Image + if len(image) == 0 { + image = cfg.TargetAllocatorImage() + } + + ports := make([]corev1.ContainerPort, 0) + ports = append(ports, corev1.ContainerPort{ + Name: "https", + ContainerPort: naming.TargetAllocatorContainerPort, + Protocol: corev1.ProtocolTCP, + }) + + volumeMounts := []corev1.VolumeMount{{ + Name: naming.TAConfigMapVolume(), + MountPath: "/conf", + }, { + Name: naming.TASecretVolume(), + MountPath: TACertMountPath, + ReadOnly: true, + }, + } + + var envVars = otelcol.Spec.TargetAllocator.Env + if otelcol.Spec.TargetAllocator.Env == nil { + envVars = []corev1.EnvVar{} + } + + idx := -1 + for i := range envVars { + if envVars[i].Name == "OTELCOL_NAMESPACE" { + idx = i + } + } + if idx == -1 { + envVars = append(envVars, corev1.EnvVar{ + Name: "OTELCOL_NAMESPACE", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.namespace", + }, + }, + }) + } + + var args []string + if otelcol.Spec.TargetAllocator.PrometheusCR.Enabled { + args = append(args, "--enable-prometheus-cr-watcher") + } + + return corev1.Container{ + Name: naming.TAContainer(), + Image: image, + Ports: ports, + Env: envVars, + VolumeMounts: volumeMounts, + Resources: otelcol.Spec.TargetAllocator.Resources, + Args: args, + } +} diff --git a/internal/manifests/targetallocator/container_test.go b/internal/manifests/targetallocator/container_test.go new file mode 100644 index 000000000..2cc545bdb --- /dev/null +++ b/internal/manifests/targetallocator/container_test.go @@ -0,0 +1,255 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package targetallocator + +import ( + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/config" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/naming" +) + +var logger = logf.Log.WithName("unit-tests") + +func TestContainerNewDefault(t *testing.T) { + // prepare + otelcol := v1alpha1.AmazonCloudWatchAgent{} + cfg := config.New(config.WithTargetAllocatorImage("default-image")) + + // test + c := Container(cfg, logger, otelcol) + + // verify + assert.Equal(t, "default-image", c.Image) +} + +func TestContainerWithImageOverridden(t *testing.T) { + // prepare + otelcol := v1alpha1.AmazonCloudWatchAgent{ + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + TargetAllocator: v1alpha1.AmazonCloudWatchAgentTargetAllocator{ + Enabled: true, + Image: "overridden-image", + }, + }, + } + cfg := config.New(config.WithTargetAllocatorImage("default-image")) + + // test + c := Container(cfg, logger, otelcol) + + // verify + assert.Equal(t, "overridden-image", c.Image) +} + +func TestContainerPorts(t *testing.T) { + // prepare + otelcol := v1alpha1.AmazonCloudWatchAgent{ + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + TargetAllocator: v1alpha1.AmazonCloudWatchAgentTargetAllocator{ + Enabled: true, + Image: "default-image", + }, + }, + } + cfg := config.New() + + // test + c := Container(cfg, logger, otelcol) + + // verify + assert.Len(t, c.Ports, 1) + assert.Equal(t, "https", c.Ports[0].Name) + assert.Equal(t, int32(naming.TargetAllocatorContainerPort), c.Ports[0].ContainerPort) +} + +func TestContainerVolumes(t *testing.T) { + // prepare + otelcol := v1alpha1.AmazonCloudWatchAgent{ + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + TargetAllocator: v1alpha1.AmazonCloudWatchAgentTargetAllocator{ + Enabled: true, + Image: "default-image", + }, + }, + } + cfg := config.New() + + // test + c := Container(cfg, logger, otelcol) + + // verify + assert.Len(t, c.VolumeMounts, 2) + assert.Equal(t, naming.TAConfigMapVolume(), c.VolumeMounts[0].Name) +} + +func TestContainerResourceRequirements(t *testing.T) { + otelcol := v1alpha1.AmazonCloudWatchAgent{ + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + TargetAllocator: v1alpha1.AmazonCloudWatchAgentTargetAllocator{ + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("128M"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("200m"), + corev1.ResourceMemory: resource.MustParse("256M"), + }, + }, + }, + }, + } + + cfg := config.New() + resourceTest := corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("128M"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("200m"), + corev1.ResourceMemory: resource.MustParse("256M"), + }, + } + // test + c := Container(cfg, logger, otelcol) + resourcesValues := c.Resources + + // verify + assert.Equal(t, resourceTest, resourcesValues) +} + +func TestContainerHasEnvVars(t *testing.T) { + // prepare + otelcol := v1alpha1.AmazonCloudWatchAgent{ + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + TargetAllocator: v1alpha1.AmazonCloudWatchAgentTargetAllocator{ + Enabled: true, + Env: []corev1.EnvVar{ + { + Name: "TEST_ENV", + Value: "test", + }, + }, + }, + }, + } + cfg := config.New(config.WithTargetAllocatorImage("default-image")) + + expected := corev1.Container{ + Name: "ta-container", + Image: "default-image", + Env: []corev1.EnvVar{ + { + Name: "TEST_ENV", + Value: "test", + }, + { + Name: "OTELCOL_NAMESPACE", + Value: "", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + APIVersion: "", + FieldPath: "metadata.namespace", + }, + ResourceFieldRef: nil, + ConfigMapKeyRef: nil, + SecretKeyRef: nil, + }, + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "ta-internal", + ReadOnly: false, + MountPath: "/conf", + SubPath: "", + MountPropagation: nil, + SubPathExpr: "", + }, + { + Name: "ta-secret", + ReadOnly: true, + MountPath: TACertMountPath, + }, + }, + Ports: []corev1.ContainerPort{ + { + Name: "https", + ContainerPort: naming.TargetAllocatorContainerPort, + Protocol: corev1.ProtocolTCP, + }, + }, + } + + // test + c := Container(cfg, logger, otelcol) + + // verify + assert.Equal(t, expected, c) +} + +func TestContainerDoesNotOverrideEnvVars(t *testing.T) { + // prepare + otelcol := v1alpha1.AmazonCloudWatchAgent{ + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + TargetAllocator: v1alpha1.AmazonCloudWatchAgentTargetAllocator{ + Enabled: true, + Env: []corev1.EnvVar{ + { + Name: "OTELCOL_NAMESPACE", + Value: "test", + }, + }, + }, + }, + } + cfg := config.New(config.WithTargetAllocatorImage("default-image")) + + expected := corev1.Container{ + Name: "ta-container", + Image: "default-image", + Env: []corev1.EnvVar{ + { + Name: "OTELCOL_NAMESPACE", + Value: "test", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "ta-internal", + ReadOnly: false, + MountPath: "/conf", + SubPath: "", + MountPropagation: nil, + SubPathExpr: "", + }, + { + Name: "ta-secret", + ReadOnly: true, + MountPath: TACertMountPath, + }, + }, + Ports: []corev1.ContainerPort{ + { + Name: "https", + ContainerPort: naming.TargetAllocatorContainerPort, + Protocol: corev1.ProtocolTCP, + }, + }, + } + + // test + c := Container(cfg, logger, otelcol) + + // verify + assert.Equal(t, expected, c) +} diff --git a/internal/manifests/targetallocator/deployment.go b/internal/manifests/targetallocator/deployment.go new file mode 100644 index 000000000..e47ef9a5d --- /dev/null +++ b/internal/manifests/targetallocator/deployment.go @@ -0,0 +1,64 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package targetallocator + +import ( + "strings" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/naming" +) + +// Deployment builds the deployment for the given instance. +func Deployment(params manifests.Params) (*appsv1.Deployment, error) { + name := naming.TargetAllocator(params.OtelCol.Name) + version := strings.Split(params.OtelCol.Spec.TargetAllocator.Image, ":") + labels := Labels(params.OtelCol, name) + if len(version) > 1 { + labels["app.kubernetes.io/version"] = version[len(version)-1] + } else { + labels["app.kubernetes.io/version"] = "latest" + } + + configMap, err := ConfigMap(params) + if err != nil { + params.Log.Info("failed to construct target allocator config map for annotations") + configMap = nil + } + annotations := Annotations(params.OtelCol, configMap) + + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: params.OtelCol.Namespace, + Labels: labels, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: params.OtelCol.Spec.TargetAllocator.Replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + Annotations: annotations, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: ServiceAccountName(params.OtelCol), + Containers: []corev1.Container{Container(params.Config, params.Log, params.OtelCol)}, + Volumes: Volumes(params.Config, params.OtelCol), + NodeSelector: params.OtelCol.Spec.TargetAllocator.NodeSelector, + Tolerations: params.OtelCol.Spec.TargetAllocator.Tolerations, + TopologySpreadConstraints: params.OtelCol.Spec.TargetAllocator.TopologySpreadConstraints, + Affinity: params.OtelCol.Spec.TargetAllocator.Affinity, + SecurityContext: params.OtelCol.Spec.TargetAllocator.SecurityContext, + }, + }, + }, + }, nil +} diff --git a/internal/manifests/targetallocator/deployment_test.go b/internal/manifests/targetallocator/deployment_test.go new file mode 100644 index 000000000..997837178 --- /dev/null +++ b/internal/manifests/targetallocator/deployment_test.go @@ -0,0 +1,368 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package targetallocator + +import ( + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v2" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/config" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" +) + +var testTolerationValues = []v1.Toleration{ + { + Key: "hii", + Value: "greeting", + Effect: "NoSchedule", + }, +} + +var testTopologySpreadConstraintValue = []v1.TopologySpreadConstraint{ + { + MaxSkew: 1, + TopologyKey: "kubernetes.io/hostname", + WhenUnsatisfiable: "DoNotSchedule", + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "foo": "bar", + }, + }, + }, +} + +var testAffinityValue = &v1.Affinity{ + NodeAffinity: &v1.NodeAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{ + NodeSelectorTerms: []v1.NodeSelectorTerm{ + { + MatchExpressions: []v1.NodeSelectorRequirement{ + { + Key: "node", + Operator: v1.NodeSelectorOpIn, + Values: []string{"test-node"}, + }, + }, + }, + }, + }, + }, +} + +var runAsUser int64 = 1000 +var runAsGroup int64 = 1000 + +var testSecurityContextValue = &v1.PodSecurityContext{ + RunAsUser: &runAsUser, + RunAsGroup: &runAsGroup, +} + +func TestDeploymentSecurityContext(t *testing.T) { + // Test default + otelcol1 := v1alpha1.AmazonCloudWatchAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-instance", + }, + } + + cfg := config.New() + + params1 := manifests.Params{ + OtelCol: otelcol1, + Config: cfg, + Log: logger, + } + d1, err := Deployment(params1) + if err != nil { + t.Fatal(err) + } + assert.Empty(t, d1.Spec.Template.Spec.SecurityContext) + + // Test SecurityContext + otelcol2 := v1alpha1.AmazonCloudWatchAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-instance-securitycontext", + }, + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + TargetAllocator: v1alpha1.AmazonCloudWatchAgentTargetAllocator{ + SecurityContext: testSecurityContextValue, + }, + }, + } + + cfg = config.New() + + params2 := manifests.Params{ + OtelCol: otelcol2, + Config: cfg, + Log: logger, + } + + d2, err := Deployment(params2) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, *testSecurityContextValue, *d2.Spec.Template.Spec.SecurityContext) +} + +func TestDeploymentNewDefault(t *testing.T) { + // prepare + otelcol := collectorInstance() + cfg := config.New() + + params := manifests.Params{ + OtelCol: otelcol, + Config: cfg, + Log: logger, + } + + // test + d, err := Deployment(params) + + assert.NoError(t, err) + + // verify + assert.Equal(t, "my-instance-target-allocator", d.GetName()) + assert.Equal(t, "my-instance-target-allocator", d.GetLabels()["app.kubernetes.io/name"]) + + assert.Len(t, d.Spec.Template.Spec.Containers, 1) + + // should only have the ConfigMap hash annotation + assert.Contains(t, d.Spec.Template.Annotations, configMapHashAnnotationKey) + assert.Len(t, d.Spec.Template.Annotations, 1) + + // the pod selector should match the pod spec's labels + assert.Equal(t, d.Spec.Template.Labels, d.Spec.Selector.MatchLabels) +} + +func TestDeploymentPodAnnotations(t *testing.T) { + // prepare + testPodAnnotationValues := map[string]string{"annotation-key": "annotation-value"} + otelcol := collectorInstance() + otelcol.Spec.PodAnnotations = testPodAnnotationValues + cfg := config.New() + + params := manifests.Params{ + OtelCol: otelcol, + Config: cfg, + Log: logger, + } + + // test + ds, err := Deployment(params) + assert.NoError(t, err) + // verify + assert.Equal(t, "my-instance-target-allocator", ds.Name) + assert.Subset(t, ds.Spec.Template.Annotations, testPodAnnotationValues) +} + +func collectorInstance() v1alpha1.AmazonCloudWatchAgent { + configYAML, err := os.ReadFile("testdata/test.yaml") + if err != nil { + fmt.Printf("Error getting yaml file: %v", err) + } + promCfg := v1alpha1.PrometheusConfig{} + err = yaml.Unmarshal(configYAML, &promCfg) + if err != nil { + fmt.Printf("failed to unmarshal config: %v", err) + } + return v1alpha1.AmazonCloudWatchAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-instance", + Namespace: "default", + }, + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + Image: "ghcr.io/aws/amazon-cloudwatch-agent-operator/amazon-cloudwatch-agent-operator:0.47.0", + Prometheus: promCfg, + }, + } +} + +func TestDeploymentNodeSelector(t *testing.T) { + // Test default + otelcol1 := v1alpha1.AmazonCloudWatchAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-instance", + }, + } + + cfg := config.New() + + params1 := manifests.Params{ + OtelCol: otelcol1, + Config: cfg, + Log: logger, + } + d1, err := Deployment(params1) + assert.NoError(t, err) + assert.Empty(t, d1.Spec.Template.Spec.NodeSelector) + + // Test nodeSelector + otelcol2 := v1alpha1.AmazonCloudWatchAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-instance-nodeselector", + }, + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + TargetAllocator: v1alpha1.AmazonCloudWatchAgentTargetAllocator{ + NodeSelector: map[string]string{ + "node-key": "node-value", + }, + }, + }, + } + + cfg = config.New() + + params2 := manifests.Params{ + OtelCol: otelcol2, + Config: cfg, + Log: logger, + } + + d2, err := Deployment(params2) + assert.NoError(t, err) + assert.Equal(t, map[string]string{"node-key": "node-value"}, d2.Spec.Template.Spec.NodeSelector) +} + +func TestDeploymentAffinity(t *testing.T) { + // Test default + otelcol1 := v1alpha1.AmazonCloudWatchAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-instance", + }, + } + + cfg := config.New() + + params1 := manifests.Params{ + OtelCol: otelcol1, + Config: cfg, + Log: logger, + } + d1, err := Deployment(params1) + assert.NoError(t, err) + assert.Empty(t, d1.Spec.Template.Spec.Affinity) + + // Test affinity + otelcol2 := v1alpha1.AmazonCloudWatchAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-instance-affinity", + }, + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + TargetAllocator: v1alpha1.AmazonCloudWatchAgentTargetAllocator{ + Affinity: testAffinityValue, + }, + }, + } + + cfg = config.New() + + params2 := manifests.Params{ + OtelCol: otelcol2, + Config: cfg, + Log: logger, + } + + d2, err := Deployment(params2) + assert.NoError(t, err) + assert.Equal(t, *testAffinityValue, *d2.Spec.Template.Spec.Affinity) +} + +func TestDeploymentTolerations(t *testing.T) { + // Test default + otelcol1 := v1alpha1.AmazonCloudWatchAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-instance", + }, + } + + cfg := config.New() + params1 := manifests.Params{ + OtelCol: otelcol1, + Config: cfg, + Log: logger, + } + d1, err := Deployment(params1) + assert.NoError(t, err) + assert.Equal(t, "my-instance-target-allocator", d1.Name) + assert.Empty(t, d1.Spec.Template.Spec.Tolerations) + + // Test Tolerations + otelcol2 := v1alpha1.AmazonCloudWatchAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-instance-toleration", + }, + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + TargetAllocator: v1alpha1.AmazonCloudWatchAgentTargetAllocator{ + Tolerations: testTolerationValues, + }, + }, + } + + params2 := manifests.Params{ + OtelCol: otelcol2, + Config: cfg, + Log: logger, + } + d2, err := Deployment(params2) + assert.NoError(t, err) + assert.Equal(t, "my-instance-toleration-target-allocator", d2.Name) + assert.NotNil(t, d2.Spec.Template.Spec.Tolerations) + assert.NotEmpty(t, d2.Spec.Template.Spec.Tolerations) + assert.Equal(t, testTolerationValues, d2.Spec.Template.Spec.Tolerations) +} + +func TestDeploymentTopologySpreadConstraints(t *testing.T) { + // Test default + otelcol1 := v1alpha1.AmazonCloudWatchAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-instance", + }, + } + + cfg := config.New() + + params1 := manifests.Params{ + OtelCol: otelcol1, + Config: cfg, + Log: logger, + } + d1, err := Deployment(params1) + assert.NoError(t, err) + assert.Equal(t, "my-instance-target-allocator", d1.Name) + assert.Empty(t, d1.Spec.Template.Spec.TopologySpreadConstraints) + + // Test TopologySpreadConstraints + otelcol2 := v1alpha1.AmazonCloudWatchAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-instance-topologyspreadconstraint", + }, + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + TargetAllocator: v1alpha1.AmazonCloudWatchAgentTargetAllocator{ + TopologySpreadConstraints: testTopologySpreadConstraintValue, + }, + }, + } + + cfg = config.New() + params2 := manifests.Params{ + OtelCol: otelcol2, + Config: cfg, + Log: logger, + } + + d2, err := Deployment(params2) + assert.NoError(t, err) + assert.Equal(t, "my-instance-topologyspreadconstraint-target-allocator", d2.Name) + assert.NotNil(t, d2.Spec.Template.Spec.TopologySpreadConstraints) + assert.NotEmpty(t, d2.Spec.Template.Spec.TopologySpreadConstraints) + assert.Equal(t, testTopologySpreadConstraintValue, d2.Spec.Template.Spec.TopologySpreadConstraints) +} diff --git a/internal/manifests/targetallocator/labels.go b/internal/manifests/targetallocator/labels.go new file mode 100644 index 000000000..98541faae --- /dev/null +++ b/internal/manifests/targetallocator/labels.go @@ -0,0 +1,31 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package targetallocator + +import ( + "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/naming" +) + +// Labels return the common labels to all TargetAllocator objects that are part of a managed AmazonCloudWatchAgent. +func Labels(instance v1alpha1.AmazonCloudWatchAgent, name string) map[string]string { + // new map every time, so that we don't touch the instance's label + base := map[string]string{} + if nil != instance.Labels { + for k, v := range instance.Labels { + base[k] = v + } + } + + base["app.kubernetes.io/managed-by"] = "amazon-cloudwatch-agent-operator" + base["app.kubernetes.io/instance"] = naming.Truncate("%s.%s", 63, instance.Namespace, instance.Name) + base["app.kubernetes.io/part-of"] = "amazon-cloudwatch-agent" + base["app.kubernetes.io/component"] = "amazon-cloudwatch-agent-target-allocator" + + if _, ok := base["app.kubernetes.io/name"]; !ok { + base["app.kubernetes.io/name"] = name + } + + return base +} diff --git a/internal/manifests/targetallocator/labels_test.go b/internal/manifests/targetallocator/labels_test.go new file mode 100644 index 000000000..b41730ee4 --- /dev/null +++ b/internal/manifests/targetallocator/labels_test.go @@ -0,0 +1,56 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package targetallocator + +import ( + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" +) + +const ( + name = "my-instance" + namespace = "my-ns" +) + +func TestLabelsCommonSet(t *testing.T) { + // prepare + otelcol := v1alpha1.AmazonCloudWatchAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } + + // test + labels := Labels(otelcol, name) + assert.Equal(t, "amazon-cloudwatch-agent-operator", labels["app.kubernetes.io/managed-by"]) + assert.Equal(t, "my-ns.my-instance", labels["app.kubernetes.io/instance"]) + assert.Equal(t, "amazon-cloudwatch-agent", labels["app.kubernetes.io/part-of"]) + assert.Equal(t, "amazon-cloudwatch-agent-target-allocator", labels["app.kubernetes.io/component"]) + assert.Equal(t, name, labels["app.kubernetes.io/name"]) +} + +func TestLabelsPropagateDown(t *testing.T) { + // prepare + otelcol := v1alpha1.AmazonCloudWatchAgent{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "myapp": "mycomponent", + "app.kubernetes.io/name": "test", + }, + }, + } + + // test + labels := Labels(otelcol, name) + + // verify + assert.Len(t, labels, 6) + assert.Equal(t, "mycomponent", labels["myapp"]) + assert.Equal(t, "test", labels["app.kubernetes.io/name"]) +} diff --git a/internal/manifests/targetallocator/service.go b/internal/manifests/targetallocator/service.go new file mode 100644 index 000000000..1a06b0c8e --- /dev/null +++ b/internal/manifests/targetallocator/service.go @@ -0,0 +1,43 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package targetallocator + +import ( + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/naming" +) + +func Service(params manifests.Params) *corev1.Service { + version := strings.Split(params.OtelCol.Spec.TargetAllocator.Image, ":") + labels := Labels(params.OtelCol, naming.TAService(params.OtelCol.Name)) + if len(version) > 1 { + labels["app.kubernetes.io/version"] = version[len(version)-1] + } else { + labels["app.kubernetes.io/version"] = "latest" + } + + selector := Labels(params.OtelCol, naming.TargetAllocator(params.OtelCol.Name)) + + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: naming.TAService(params.OtelCol.Name), + Namespace: params.OtelCol.Namespace, + Labels: labels, + }, + Spec: corev1.ServiceSpec{ + Selector: selector, + Ports: []corev1.ServicePort{{ + Name: "targetallocation", + Port: naming.TargetAllocatorServicePort, + TargetPort: intstr.FromInt32(naming.TargetAllocatorContainerPort), + }}, + }, + } +} diff --git a/internal/manifests/targetallocator/service_test.go b/internal/manifests/targetallocator/service_test.go new file mode 100644 index 000000000..e2a387cfa --- /dev/null +++ b/internal/manifests/targetallocator/service_test.go @@ -0,0 +1,44 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package targetallocator + +import ( + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/aws/amazon-cloudwatch-agent-operator/internal/config" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/naming" +) + +func TestServicePorts(t *testing.T) { + otelcol := collectorInstance() + cfg := config.New() + + params := manifests.Params{ + OtelCol: otelcol, + Config: cfg, + Log: logger, + } + + ports := []v1.ServicePort{{Name: "targetallocation", Port: naming.TargetAllocatorServicePort, TargetPort: intstr.FromInt32(naming.TargetAllocatorContainerPort)}} + + expectedLabels := map[string]string{ + "app.kubernetes.io/managed-by": "amazon-cloudwatch-agent-operator", + "app.kubernetes.io/instance": "default.my-instance", + "app.kubernetes.io/part-of": "amazon-cloudwatch-agent", + "app.kubernetes.io/component": "amazon-cloudwatch-agent-target-allocator", + "app.kubernetes.io/name": "my-instance-target-allocator", + } + + s := Service(params) + + assert.Equal(t, ports[0].Name, s.Spec.Ports[0].Name) + assert.Equal(t, ports[0].Port, s.Spec.Ports[0].Port) + assert.Equal(t, ports[0].TargetPort, s.Spec.Ports[0].TargetPort) + assert.Equal(t, expectedLabels, s.Spec.Selector) +} diff --git a/internal/manifests/targetallocator/serviceaccount.go b/internal/manifests/targetallocator/serviceaccount.go new file mode 100644 index 000000000..1883d255a --- /dev/null +++ b/internal/manifests/targetallocator/serviceaccount.go @@ -0,0 +1,45 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package targetallocator + +import ( + "strings" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" +) + +const targetAllocatorServiceAcctName = "target-allocator-service-acct" + +// ServiceAccountName returns the name of the existing or self-provisioned service account to use for the given instance. +func ServiceAccountName(instance v1alpha1.AmazonCloudWatchAgent) string { + if len(instance.Spec.TargetAllocator.ServiceAccount) == 0 { + return targetAllocatorServiceAcctName + } + + return instance.Spec.TargetAllocator.ServiceAccount +} + +// ServiceAccount returns the service account for the given instance. +func ServiceAccount(params manifests.Params) *corev1.ServiceAccount { + version := strings.Split(params.OtelCol.Spec.TargetAllocator.Image, ":") + labels := Labels(params.OtelCol, targetAllocatorServiceAcctName) + if len(version) > 1 { + labels["app.kubernetes.io/version"] = version[len(version)-1] + } else { + labels["app.kubernetes.io/version"] = "latest" + } + + return &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: targetAllocatorServiceAcctName, + Namespace: params.OtelCol.Namespace, + Labels: labels, + Annotations: params.OtelCol.Annotations, + }, + } +} diff --git a/internal/manifests/targetallocator/serviceaccount_test.go b/internal/manifests/targetallocator/serviceaccount_test.go new file mode 100644 index 000000000..7f4614e18 --- /dev/null +++ b/internal/manifests/targetallocator/serviceaccount_test.go @@ -0,0 +1,48 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package targetallocator + +import ( + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" +) + +func TestServiceAccountNewDefault(t *testing.T) { + // prepare + otelcol := v1alpha1.AmazonCloudWatchAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-instance", + }, + } + + // test + sa := ServiceAccountName(otelcol) + + // verify + assert.Equal(t, "target-allocator-service-acct", sa) +} + +func TestServiceAccountOverride(t *testing.T) { + // prepare + otelcol := v1alpha1.AmazonCloudWatchAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-instance", + }, + Spec: v1alpha1.AmazonCloudWatchAgentSpec{ + TargetAllocator: v1alpha1.AmazonCloudWatchAgentTargetAllocator{ + ServiceAccount: "my-special-sa", + }, + }, + } + + // test + sa := ServiceAccountName(otelcol) + + // verify + assert.Equal(t, "my-special-sa", sa) +} diff --git a/internal/manifests/targetallocator/targetallocator.go b/internal/manifests/targetallocator/targetallocator.go new file mode 100644 index 000000000..ebc9c2f9e --- /dev/null +++ b/internal/manifests/targetallocator/targetallocator.go @@ -0,0 +1,33 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package targetallocator + +import ( + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/aws/amazon-cloudwatch-agent-operator/internal/manifests" +) + +// Build creates the manifest for the TargetAllocator resource. +func Build(params manifests.Params) ([]client.Object, error) { + var resourceManifests []client.Object + if !params.OtelCol.Spec.TargetAllocator.Enabled { + return resourceManifests, nil + } + resourceFactories := []manifests.K8sManifestFactory{ + manifests.Factory(ConfigMap), + manifests.Factory(Deployment), + manifests.FactoryWithoutError(ServiceAccount), + manifests.FactoryWithoutError(Service), + } + for _, factory := range resourceFactories { + res, err := factory(params) + if err != nil { + return nil, err + } else if manifests.ObjectIsNotNil(res) { + resourceManifests = append(resourceManifests, res) + } + } + return resourceManifests, nil +} diff --git a/internal/manifests/targetallocator/testdata/test.yaml b/internal/manifests/targetallocator/testdata/test.yaml new file mode 100644 index 000000000..3332c6705 --- /dev/null +++ b/internal/manifests/targetallocator/testdata/test.yaml @@ -0,0 +1,6 @@ +config: + scrape_configs: + - job_name: otel-collector + scrape_interval: 10s + static_configs: + - targets: [ '0.0.0.0:8888', '0.0.0.0:9999' ] \ No newline at end of file diff --git a/internal/manifests/targetallocator/volume.go b/internal/manifests/targetallocator/volume.go new file mode 100644 index 000000000..06e16af2c --- /dev/null +++ b/internal/manifests/targetallocator/volume.go @@ -0,0 +1,49 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package targetallocator + +import ( + corev1 "k8s.io/api/core/v1" + + "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/config" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/naming" +) + +// Volumes builds the volumes for the given instance, including the config map volume. +func Volumes(cfg config.Config, otelcol v1alpha1.AmazonCloudWatchAgent) []corev1.Volume { + volumes := []corev1.Volume{{ + Name: naming.TAConfigMapVolume(), + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: naming.TAConfigMap(otelcol.Name)}, + Items: []corev1.KeyToPath{ + { + Key: cfg.TargetAllocatorConfigMapEntry(), + Path: cfg.TargetAllocatorConfigMapEntry(), + }}, + }, + }, + }, + { + Name: naming.TASecretVolume(), + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "amazon-cloudwatch-observability-agent-cert", + Items: []corev1.KeyToPath{ + { + Key: "tls.crt", + Path: "server.crt", + }, { + Key: "tls.key", + Path: "server.key", + }, + }, + }, + }, + }, + } + + return volumes +} diff --git a/internal/manifests/targetallocator/volume_test.go b/internal/manifests/targetallocator/volume_test.go new file mode 100644 index 000000000..77f3fe8a0 --- /dev/null +++ b/internal/manifests/targetallocator/volume_test.go @@ -0,0 +1,32 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package targetallocator + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/config" + "github.com/aws/amazon-cloudwatch-agent-operator/internal/naming" +) + +func TestVolumeNewDefault(t *testing.T) { + // prepare + otelcol := v1alpha1.AmazonCloudWatchAgent{} + cfg := config.New() + + // test + volumes := Volumes(cfg, otelcol) + + // verify + assert.Len(t, volumes, 2) + + // check if the number of elements in the volume source items list is 1 + assert.Len(t, volumes[0].VolumeSource.ConfigMap.Items, 1) + + // check that it's the ta-internal volume, with the config map + assert.Equal(t, naming.TAConfigMapVolume(), volumes[0].Name) +} diff --git a/internal/naming/main.go b/internal/naming/main.go index 251b88db9..2d0a75a39 100644 --- a/internal/naming/main.go +++ b/internal/naming/main.go @@ -9,6 +9,16 @@ func ConfigMap(otelcol string) string { return DNSName(Truncate("%s", 63, otelcol)) } +// TAConfigMap returns the name for the config map used in the TargetAllocator. +func TAConfigMap(otelcol string) string { + return DNSName(Truncate("%s-target-allocator", 63, otelcol)) +} + +// PrometheusConfigMap returns the name for the prometheus config map. +func PrometheusConfigMap(otelcol string) string { + return DNSName(Truncate("%s-prometheus-config", 63, otelcol)) +} + // ConfigMapVolume returns the name to use for the config map's volume in the pod. func ConfigMapVolume() string { return "otc-internal" @@ -19,11 +29,29 @@ func ConfigMapExtra(extraConfigMapName string) string { return DNSName(Truncate("configmap-%s", 63, extraConfigMapName)) } +// TAConfigMapVolume returns the name to use for the config map's volume in the TargetAllocator pod. +func TAConfigMapVolume() string { + return "ta-internal" +} +func TASecretVolume() string { + return "ta-secret" +} + +// PrometheusConfigMapVolume returns the name to use for the prometheus config map's volume in the pod. +func PrometheusConfigMapVolume() string { + return "prometheus-config" +} + // Container returns the name to use for the container in the pod. func Container() string { return "otc-container" } +// TAContainer returns the name to use for the container in the TargetAllocator pod. +func TAContainer() string { + return "ta-container" +} + // Collector builds the collector (deployment/daemonset) name based on the instance. func Collector(otelcol string) string { return DNSName(Truncate("%s", 63, otelcol)) @@ -49,6 +77,11 @@ func AmazonCloudWatchAgentName(otelcolName string) string { return DNSName(Truncate("%s", 63, otelcolName)) } +// TargetAllocator returns the TargetAllocator deployment resource name. +func TargetAllocator(otelcol string) string { + return DNSName(Truncate("%s-target-allocator", 63, otelcol)) +} + // HeadlessService builds the name for the headless service based on the instance. func HeadlessService(otelcol string) string { return DNSName(Truncate("%s-headless", 63, Service(otelcol))) @@ -74,6 +107,15 @@ func Route(otelcol string, prefix string) string { return DNSName(Truncate("%s-%s-route", 63, prefix, otelcol)) } +// TAService returns the name to use for the TargetAllocator service. +func TAService(otelcol string) string { + return DNSName(Truncate("%s-target-allocator-service", 63, otelcol)) + +} +func TAPodDestination(otelcol string) string { + return DNSName(Truncate("%s-target-allocator", 63, otelcol)) +} + // ServiceAccount builds the service account name based on the instance. func ServiceAccount(otelcol string) string { return DNSName(Truncate("%s", 63, otelcol)) diff --git a/internal/naming/port.go b/internal/naming/port.go index 5cac9f3c5..bd68e6187 100644 --- a/internal/naming/port.go +++ b/internal/naming/port.go @@ -9,6 +9,11 @@ import ( "strings" ) +const ( + TargetAllocatorServicePort = 80 + TargetAllocatorContainerPort = 8443 +) + var ( // DNS_LABEL constraints: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names dnsLabelValidation = regexp.MustCompile("^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$") diff --git a/internal/version/main.go b/internal/version/main.go index 0c7807e5b..593f7feb6 100644 --- a/internal/version/main.go +++ b/internal/version/main.go @@ -22,6 +22,7 @@ var ( autoInstrumentationGo string dcgmExporter string neuronMonitor string + targetAllocator string ) // Version holds this Operator's version as well as the version of some of the components it uses. @@ -39,6 +40,7 @@ type Version struct { AutoInstrumentationNginx string `json:"auto-instrumentation-nginx"` DcgmExporter string `json:"dcgm-exporter-version"` NeuronMonitor string `json:"neuron-monitor-version"` + TargetAllocator string `json:"target-allocator-version"` } // Get returns the Version object with the relevant information. @@ -57,12 +59,13 @@ func Get() Version { AutoInstrumentationNginx: AutoInstrumentationNginx(), DcgmExporter: DcgmExporter(), NeuronMonitor: NeuronMonitor(), + TargetAllocator: TargetAllocator(), } } func (v Version) String() string { return fmt.Sprintf( - "Version(Operator='%v', BuildDate='%v', AmazonCloudWatchAgent='%v', Go='%v', AutoInstrumentationJava='%v', AutoInstrumentationNodeJS='%v', AutoInstrumentationPython='%v', AutoInstrumentationDotNet='%v', AutoInstrumentationGo='%v', AutoInstrumentationApacheHttpd='%v', AutoInstrumentationNginx='%v', DcgmExporter='%v', , NeuronMonitor='%v')", + "Version(Operator='%v', BuildDate='%v', AmazonCloudWatchAgent='%v', Go='%v', AutoInstrumentationJava='%v', AutoInstrumentationNodeJS='%v', AutoInstrumentationPython='%v', AutoInstrumentationDotNet='%v', AutoInstrumentationGo='%v', AutoInstrumentationApacheHttpd='%v', AutoInstrumentationNginx='%v', DcgmExporter='%v', NeuronMonitor='%v', TargetAllocator='%v')", v.Operator, v.BuildDate, v.AmazonCloudWatchAgent, @@ -76,6 +79,7 @@ func (v Version) String() string { v.AutoInstrumentationNginx, v.DcgmExporter, v.NeuronMonitor, + v.TargetAllocator, ) } @@ -154,3 +158,14 @@ func NeuronMonitor() string { } return "0.0.0" } + +// TargetAllocator returns the default TargetAllocator to use when no versions are specified via CLI or configuration. +func TargetAllocator() string { + if len(targetAllocator) > 0 { + // this should always be set, as it's specified during the build + return targetAllocator + } + + // fallback value, useful for tests + return "0.0.0" +} diff --git a/internal/version/main_test.go b/internal/version/main_test.go index 9229c208c..ab0cfba3e 100644 --- a/internal/version/main_test.go +++ b/internal/version/main_test.go @@ -24,6 +24,21 @@ func TestVersionFromBuild(t *testing.T) { assert.Contains(t, Get().String(), otelCol) } +func TestTargetAllocatorFallbackVersion(t *testing.T) { + assert.Equal(t, "0.0.0", TargetAllocator()) +} + +func TestTargetAllocatorVersionFromBuild(t *testing.T) { + // prepare + targetAllocator = "0.0.2" // set during the build + defer func() { + targetAllocator = "" + }() + + assert.Equal(t, targetAllocator, TargetAllocator()) + assert.Contains(t, Get().String(), targetAllocator) +} + func TestAutoInstrumentationJavaFallbackVersion(t *testing.T) { assert.Equal(t, "0.0.0", AutoInstrumentationJava()) } diff --git a/main.go b/main.go index eae91a507..1e30c429f 100644 --- a/main.go +++ b/main.go @@ -52,6 +52,7 @@ const ( autoInstrumentationNodeJSImageRepository = "ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-nodejs" dcgmExporterImageRepository = "nvcr.io/nvidia/k8s/dcgm-exporter" neuronMonitorImageRepository = "public.ecr.aws/neuron" + targetAllocatorImageRepository = "ghcr.io/open-telemetry/opentelemetry-operator/target-allocator" ) var ( @@ -132,6 +133,7 @@ func main() { tlsOpt tlsConfig dcgmExporterImage string neuronMonitorImage string + targetAllocatorImage string ) pflag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") @@ -146,6 +148,7 @@ func main() { pflag.StringVar(&autoInstrumentationConfigStr, "auto-instrumentation-config", "", "The configuration for auto-instrumentation.") stringFlagOrEnv(&dcgmExporterImage, "dcgm-exporter-image", "RELATED_IMAGE_DCGM_EXPORTER", fmt.Sprintf("%s:%s", dcgmExporterImageRepository, v.DcgmExporter), "The default DCGM Exporter image. This image is used when no image is specified in the CustomResource.") stringFlagOrEnv(&neuronMonitorImage, "neuron-monitor-image", "RELATED_IMAGE_NEURON_MONITOR", fmt.Sprintf("%s:%s", neuronMonitorImageRepository, v.NeuronMonitor), "The default Neuron monitor image. This image is used when no image is specified in the CustomResource.") + stringFlagOrEnv(&targetAllocatorImage, "target-allocator-image", "RELATED_IMAGE_TARGET_ALLOCATOR", fmt.Sprintf("%s:%s", targetAllocatorImageRepository, v.TargetAllocator), "The default AmazonCloudWatchAgent target allocator image. This image is used when no image is specified in the CustomResource.") pflag.Parse() // set instrumentation cpu and memory limits in environment variables to be used for default instrumentation; default values received from https://github.com/open-telemetry/opentelemetry-operator/blob/main/apis/v1alpha1/instrumentation_webhook.go @@ -185,6 +188,7 @@ func main() { "auto-instrumentation-nodejs", autoInstrumentationNodeJS, "dcgm-exporter", dcgmExporterImage, "neuron-monitor", neuronMonitorImage, + "amazon-cloudwatch-agent-target-allocator", targetAllocatorImage, "build-date", v.BuildDate, "go-version", v.Go, "go-arch", runtime.GOARCH, @@ -201,6 +205,7 @@ func main() { config.WithAutoInstrumentationNodeJSImage(autoInstrumentationNodeJS), config.WithDcgmExporterImage(dcgmExporterImage), config.WithNeuronMonitorImage(neuronMonitorImage), + config.WithTargetAllocatorImage(targetAllocatorImage), ) watchNamespace, found := os.LookupEnv("WATCH_NAMESPACE") diff --git a/pkg/featuregate/featuregate.go b/pkg/featuregate/featuregate.go index 8c438d598..6ae36fc37 100644 --- a/pkg/featuregate/featuregate.go +++ b/pkg/featuregate/featuregate.go @@ -63,12 +63,12 @@ var ( featuregate.WithRegisterFromVersion("0.86.0"), featuregate.WithRegisterDescription("controls whether the operator supports multi instrumentation")) - // EnableTargetAllocatorRewrite is the feature gate that controls whether the collector's configuration should + // EnableTargetAllocatorRewrite is the feature gate that controls whether the prometheus configuration should // automatically be rewritten when the target allocator is enabled. EnableTargetAllocatorRewrite = featuregate.GlobalRegistry().MustRegister( "operator.collector.rewritetargetallocator", featuregate.StageBeta, - featuregate.WithRegisterDescription("controls whether the operator should configure the collector's targetAllocator configuration"), + featuregate.WithRegisterDescription("controls whether the operator should configure the prometheus targetAllocator configuration"), featuregate.WithRegisterFromVersion("v0.76.1"), ) diff --git a/pkg/featuregate/featuregate_test.go b/pkg/featuregate/featuregate_test.go index f759506ac..d71ca1246 100644 --- a/pkg/featuregate/featuregate_test.go +++ b/pkg/featuregate/featuregate_test.go @@ -6,10 +6,9 @@ package featuregate import ( "testing" - "go.opentelemetry.io/collector/featuregate" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/featuregate" ) const ( diff --git a/pkg/instrumentation/defaultinstrumentation.go b/pkg/instrumentation/defaultinstrumentation.go index 43419981e..0a9ef6df7 100644 --- a/pkg/instrumentation/defaultinstrumentation.go +++ b/pkg/instrumentation/defaultinstrumentation.go @@ -148,7 +148,7 @@ func getJavaEnvs(isAppSignalsEnabled bool, cloudwatchAgentServiceEndpoint, expor if isAppSignalsEnabled { isJavaRuntimeEnabled, ok := os.LookupEnv("AUTO_INSTRUMENTATION_JAVA_RUNTIME_ENABLED") if !ok { - isJavaRuntimeEnabled = "true"; + isJavaRuntimeEnabled = "true" } appSignalsEnvs := []corev1.EnvVar{ {Name: "OTEL_AWS_APP_SIGNALS_ENABLED", Value: "true"}, //TODO: remove in favor of new name once safe @@ -185,7 +185,7 @@ func getPythonEnvs(isAppSignalsEnabled bool, cloudwatchAgentServiceEndpoint, exp if isAppSignalsEnabled { isPythonRuntimeEnabled, ok := os.LookupEnv("AUTO_INSTRUMENTATION_PYTHON_RUNTIME_ENABLED") if !ok { - isPythonRuntimeEnabled = "true"; + isPythonRuntimeEnabled = "true" } envs = []corev1.EnvVar{ {Name: "OTEL_AWS_APP_SIGNALS_ENABLED", Value: "true"}, //TODO: remove in favor of new name once safe diff --git a/pkg/instrumentation/sdk.go b/pkg/instrumentation/sdk.go index 90d3dc45f..3a2ffd3a1 100644 --- a/pkg/instrumentation/sdk.go +++ b/pkg/instrumentation/sdk.go @@ -12,10 +12,6 @@ import ( "unsafe" "github.com/go-logr/logr" - - "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" - "github.com/aws/amazon-cloudwatch-agent-operator/pkg/constants" - "go.opentelemetry.io/otel/attribute" semconv "go.opentelemetry.io/otel/semconv/v1.7.0" appsv1 "k8s.io/api/apps/v1" @@ -26,6 +22,9 @@ import ( "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/aws/amazon-cloudwatch-agent-operator/apis/v1alpha1" + "github.com/aws/amazon-cloudwatch-agent-operator/pkg/constants" ) const ( diff --git a/versions.txt b/versions.txt index a031374ec..4cf317386 100644 --- a/versions.txt +++ b/versions.txt @@ -11,4 +11,5 @@ aws-otel-dotnet-instrumentation=1.6.0 aws-otel-nodejs-instrumentation=0.52.1 dcgm-exporter=3.3.7-3.5.0-ubuntu22.04 -neuron-monitor=1.0.1 \ No newline at end of file +neuron-monitor=1.0.1 +target-allocator=1.0.0 \ No newline at end of file