Skip to content

Commit d198928

Browse files
committed
fix(collector): roll pods when Prometheus config changes
The pod-template restart-trigger sha256 was computed from Spec.Config only, so a change to Spec.Prometheus (rendered into a separate ConfigMap) left the pod template byte-identical and the workload controller did not roll the pods. Fold the serialized Spec.Prometheus (PrometheusConfig.Yaml()) into the hash input when it is non-empty, so a Prometheus-only change bumps the pod-template annotation and triggers a rolling restart, matching agent-config behavior. When no Prometheus config is set the hash input is byte-identical to the agent config alone, leaving non-Prometheus agents unaffected.
1 parent ff47bc4 commit d198928

6 files changed

Lines changed: 82 additions & 27 deletions

File tree

RELEASE_NOTES

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
========================================================================
2-
Amazon CloudWatch Agent Operator (Unreleased)
3-
========================================================================
4-
Bug Fixes:
5-
* [TargetAllocator] Register the `--enable-prometheus-cr-watcher` CLI flag so the target-allocator no longer exits with "unknown flag" (CrashLoopBackOff) when the operator passes it for PrometheusCR.enabled. The flag is OR'd with the YAML `prometheus_cr.enabled` setting.
6-
71
========================================================================
82
Amazon CloudWatch Agent Operator v3.5.0 (2026-06-05)
93
========================================================================

cmd/amazon-cloudwatch-agent-target-allocator/config/config.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,7 @@ func LoadFromCLI(target *Config, flagSet *pflag.FlagSet) error {
119119
return err
120120
}
121121

122-
// Enable the Prometheus CR watcher when requested via the CLI flag. The YAML
123-
// `prometheus_cr.enabled` value is loaded before this point, so OR the flag in
124-
// rather than overwriting it: the watcher is enabled if either source sets it.
122+
// OR the CLI flag into the YAML value so either source can enable the watcher.
125123
prometheusCREnabled, err := getPrometheusCREnabled(flagSet)
126124
if err != nil {
127125
return err

cmd/amazon-cloudwatch-agent-target-allocator/main.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,7 @@ func main() {
8888
srv := server.NewServer(log, allocator, cfg.ListenAddr, httpOptions...)
8989

9090
discoveryCtx, discoveryCancel := context.WithCancel(ctx)
91-
// Service Discovery metrics MUST be created and passed to the discovery
92-
// manager. Each SD provider's NewDiscoverer fails when its DiscovererMetrics
93-
// is nil, so passing a nil sdMetrics map makes every provider (including
94-
// kubernetes_sd) fail to register, yielding zero discovered targets. Mirror
95-
// the upstream opentelemetry-operator target-allocator.
91+
// SD metrics must be non-nil; providers fail to register without them.
9692
sdRegistry := prometheus.NewRegistry()
9793
sdMetrics, sdMetricsErr := discovery.CreateAndRegisterSDMetrics(sdRegistry)
9894
if sdMetricsErr != nil {

cmd/amazon-cloudwatch-agent-target-allocator/watcher/promOperator.go

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,7 @@ func NewPrometheusCRWatcher(logger logr.Logger, cfg allocatorconfig.Config) (*Pr
5050
}
5151

5252
// TODO: We should make these durations configurable
53-
// The synthetic Prometheus object must carry a non-empty namespace: the
54-
// prometheus-operator config generator calls store.ForNamespace(prom.Namespace)
55-
// when generating the server configuration, which panics on an empty namespace
56-
// ("namespace can't be empty"). Mirror the upstream opentelemetry-operator
57-
// target-allocator by setting it to the collector/TA namespace, derived from the
58-
// operator-injected OTELCOL_NAMESPACE downward-API env (fallback: amazon-cloudwatch).
53+
// Namespace must be non-empty; the config generator panics otherwise.
5954
collectorNamespace := os.Getenv("OTELCOL_NAMESPACE")
6055
if collectorNamespace == "" {
6156
collectorNamespace = "amazon-cloudwatch"
@@ -68,11 +63,7 @@ func NewPrometheusCRWatcher(logger logr.Logger, cfg allocatorconfig.Config) (*Pr
6863
CommonPrometheusFields: monitoringv1.CommonPrometheusFields{
6964
ScrapeInterval: monitoringv1.Duration(cfg.PrometheusCR.ScrapeInterval.String()),
7065
},
71-
// EvaluationInterval must be non-empty: the prometheus-operator config
72-
// generator renders it verbatim as global.evaluation_interval, and the
73-
// downstream prometheus config parser rejects an empty value with
74-
// "empty duration string". The CWA TA does not evaluate rules, so mirror
75-
// the scrape interval as a sane non-empty default.
66+
// Must be non-empty; default to scrape interval.
7667
EvaluationInterval: monitoringv1.Duration(cfg.PrometheusCR.ScrapeInterval.String()),
7768
},
7869
}

internal/manifests/collector/annotations.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func Annotations(instance v1alpha1.AmazonCloudWatchAgent) map[string]string {
2828
}
2929

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

3333
return annotations
3434
}
@@ -51,11 +51,22 @@ func PodAnnotations(instance v1alpha1.AmazonCloudWatchAgent) map[string]string {
5151
}
5252

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

5656
return podAnnotations
5757
}
5858

59+
// configHashInput returns the combined config string used for the pod-template restart hash.
60+
func configHashInput(instance v1alpha1.AmazonCloudWatchAgent) string {
61+
config := instance.Spec.Config
62+
if !instance.Spec.Prometheus.IsEmpty() {
63+
if promYaml, err := instance.Spec.Prometheus.Yaml(); err == nil {
64+
config += promYaml
65+
}
66+
}
67+
return config
68+
}
69+
5970
func getConfigMapSHA(config string) string {
6071
h := sha256.Sum256([]byte(config))
6172
return fmt.Sprintf("%x", h)

internal/manifests/collector/annotations_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,68 @@ func TestAnnotationsPropagateDown(t *testing.T) {
7979
assert.Equal(t, "mycomponent", podAnnotations["myapp"])
8080
assert.Equal(t, "pod_annotation_value", podAnnotations["pod_annotation"])
8181
}
82+
83+
func promConfig(t *testing.T, replacement string) v1alpha1.PrometheusConfig {
84+
t.Helper()
85+
cfg := map[string]interface{}{
86+
"scrape_configs": []interface{}{
87+
map[string]interface{}{
88+
"job_name": "kubernetes-pods-annotated",
89+
"relabel_configs": []interface{}{
90+
map[string]interface{}{
91+
"target_label": "bug2probe",
92+
"replacement": replacement,
93+
},
94+
},
95+
},
96+
},
97+
}
98+
return v1alpha1.PrometheusConfig{
99+
Config: &v1alpha1.AnyConfig{Object: cfg},
100+
}
101+
}
102+
103+
// TestPrometheusConfigChangeBumpsHash asserts that changing only Spec.Prometheus
104+
// changes the pod-template config hash (so the pods roll on a Prometheus-only
105+
// change), while an unchanged spec yields a stable hash.
106+
func TestPrometheusConfigChangeBumpsHash(t *testing.T) {
107+
base := v1alpha1.AmazonCloudWatchAgent{
108+
ObjectMeta: metav1.ObjectMeta{Name: "my-instance", Namespace: "my-ns"},
109+
Spec: v1alpha1.AmazonCloudWatchAgentSpec{
110+
Config: "agent-config",
111+
Prometheus: promConfig(t, "value2"),
112+
},
113+
}
114+
115+
// same spec twice -> stable hash
116+
h1 := PodAnnotations(base)["amazon-cloudwatch-agent-operator-config/sha256"]
117+
h2 := PodAnnotations(base)["amazon-cloudwatch-agent-operator-config/sha256"]
118+
assert.Equal(t, h1, h2, "hash must be stable when nothing changes")
119+
120+
// change ONLY the prometheus config -> hash must change
121+
changed := base
122+
changed.Spec.Prometheus = promConfig(t, "value3")
123+
h3 := PodAnnotations(changed)["amazon-cloudwatch-agent-operator-config/sha256"]
124+
assert.NotEqual(t, h1, h3, "pod annotation hash must change when only Spec.Prometheus changes")
125+
126+
// metadata annotations hash must also reflect the prometheus change
127+
a1 := Annotations(base)["amazon-cloudwatch-agent-operator-config/sha256"]
128+
a3 := Annotations(changed)["amazon-cloudwatch-agent-operator-config/sha256"]
129+
assert.NotEqual(t, a1, a3, "metadata annotation hash must change when only Spec.Prometheus changes")
130+
}
131+
132+
// TestEmptyPrometheusHashUnchanged asserts that when no Prometheus config is set,
133+
// the hash is byte-identical to the agent-config-only sha256 (non-Prometheus
134+
// agents are unaffected by the fix).
135+
func TestEmptyPrometheusHashUnchanged(t *testing.T) {
136+
otelcol := v1alpha1.AmazonCloudWatchAgent{
137+
ObjectMeta: metav1.ObjectMeta{Name: "my-instance", Namespace: "my-ns"},
138+
Spec: v1alpha1.AmazonCloudWatchAgentSpec{Config: "test"},
139+
}
140+
141+
// sha256("test") == 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
142+
assert.Equal(t, "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
143+
Annotations(otelcol)["amazon-cloudwatch-agent-operator-config/sha256"])
144+
assert.Equal(t, "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
145+
PodAnnotations(otelcol)["amazon-cloudwatch-agent-operator-config/sha256"])
146+
}

0 commit comments

Comments
 (0)