Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions RELEASE_NOTES
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
========================================================================
Amazon CloudWatch Agent Operator (Unreleased)
========================================================================
Bug Fixes:
* Roll agent pods when the Prometheus config changes: the pod-template config
hash now folds in the serialized Spec.Prometheus, so a Prometheus-only change
triggers a rolling restart (previously only Spec.Config changes did)

========================================================================
Amazon CloudWatch Agent Operator v3.5.0 (2026-06-05)
========================================================================
Expand Down
21 changes: 19 additions & 2 deletions internal/manifests/collector/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func Annotations(instance v1alpha1.AmazonCloudWatchAgent) map[string]string {
}

// make sure sha256 for configMap is always calculated
annotations["amazon-cloudwatch-agent-operator-config/sha256"] = getConfigMapSHA(instance.Spec.Config)
annotations["amazon-cloudwatch-agent-operator-config/sha256"] = getConfigMapSHA(configHashInput(instance))

return annotations
}
Expand All @@ -51,11 +51,28 @@ func PodAnnotations(instance v1alpha1.AmazonCloudWatchAgent) map[string]string {
}

// make sure sha256 for configMap is always calculated
podAnnotations["amazon-cloudwatch-agent-operator-config/sha256"] = getConfigMapSHA(instance.Spec.Config)
podAnnotations["amazon-cloudwatch-agent-operator-config/sha256"] = getConfigMapSHA(configHashInput(instance))

return podAnnotations
}

// configHashInput returns the string whose sha256 is used as the pod-template
// restart trigger. It folds the serialized Prometheus config into the agent
// config so that a Prometheus-only change (written to a separate ConfigMap)
// bumps the pod-template hash and triggers a rolling restart, matching the
// behavior of an agent-config change. When no Prometheus config is set
// (Spec.Prometheus.IsEmpty()), the input is byte-identical to the agent config
// alone, so non-Prometheus agents are unaffected.
func configHashInput(instance v1alpha1.AmazonCloudWatchAgent) string {
config := instance.Spec.Config
if !instance.Spec.Prometheus.IsEmpty() {
if promYaml, err := instance.Spec.Prometheus.Yaml(); err == nil {
config += promYaml
}
}
return config
}

func getConfigMapSHA(config string) string {
h := sha256.Sum256([]byte(config))
return fmt.Sprintf("%x", h)
Expand Down
65 changes: 65 additions & 0 deletions internal/manifests/collector/annotations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,68 @@ func TestAnnotationsPropagateDown(t *testing.T) {
assert.Equal(t, "mycomponent", podAnnotations["myapp"])
assert.Equal(t, "pod_annotation_value", podAnnotations["pod_annotation"])
}

func promConfig(t *testing.T, replacement string) v1alpha1.PrometheusConfig {
t.Helper()
cfg := map[string]interface{}{
"scrape_configs": []interface{}{
map[string]interface{}{
"job_name": "kubernetes-pods-annotated",
"relabel_configs": []interface{}{
map[string]interface{}{
"target_label": "bug2probe",
"replacement": replacement,
},
},
},
},
}
return v1alpha1.PrometheusConfig{
Config: &v1alpha1.AnyConfig{Object: cfg},
}
}

// TestPrometheusConfigChangeBumpsHash asserts that changing only Spec.Prometheus
// changes the pod-template config hash (so the pods roll on a Prometheus-only
// change), while an unchanged spec yields a stable hash.
func TestPrometheusConfigChangeBumpsHash(t *testing.T) {
base := v1alpha1.AmazonCloudWatchAgent{
ObjectMeta: metav1.ObjectMeta{Name: "my-instance", Namespace: "my-ns"},
Spec: v1alpha1.AmazonCloudWatchAgentSpec{
Config: "agent-config",
Prometheus: promConfig(t, "value2"),
},
}

// same spec twice -> stable hash
h1 := PodAnnotations(base)["amazon-cloudwatch-agent-operator-config/sha256"]
h2 := PodAnnotations(base)["amazon-cloudwatch-agent-operator-config/sha256"]
assert.Equal(t, h1, h2, "hash must be stable when nothing changes")

// change ONLY the prometheus config -> hash must change
changed := base
changed.Spec.Prometheus = promConfig(t, "value3")
h3 := PodAnnotations(changed)["amazon-cloudwatch-agent-operator-config/sha256"]
assert.NotEqual(t, h1, h3, "pod annotation hash must change when only Spec.Prometheus changes")

// metadata annotations hash must also reflect the prometheus change
a1 := Annotations(base)["amazon-cloudwatch-agent-operator-config/sha256"]
a3 := Annotations(changed)["amazon-cloudwatch-agent-operator-config/sha256"]
assert.NotEqual(t, a1, a3, "metadata annotation hash must change when only Spec.Prometheus changes")
}

// TestEmptyPrometheusHashUnchanged asserts that when no Prometheus config is set,
// the hash is byte-identical to the agent-config-only sha256 (non-Prometheus
// agents are unaffected by the fix).
func TestEmptyPrometheusHashUnchanged(t *testing.T) {
otelcol := v1alpha1.AmazonCloudWatchAgent{
ObjectMeta: metav1.ObjectMeta{Name: "my-instance", Namespace: "my-ns"},
Spec: v1alpha1.AmazonCloudWatchAgentSpec{Config: "test"},
}

// sha256("test") == 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
assert.Equal(t, "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
Annotations(otelcol)["amazon-cloudwatch-agent-operator-config/sha256"])
assert.Equal(t, "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
PodAnnotations(otelcol)["amazon-cloudwatch-agent-operator-config/sha256"])
}
Loading