Skip to content

Commit ce1bb97

Browse files
committed
feat: add force-configcheck annotation to trigger configcheck manually
Include annotation value in pipeline hash so changing it triggers configcheck even when spec is unchanged. Uses omitempty for backward compatibility — existing pipelines without the annotation are unaffected. Signed-off-by: Aleksandr Aleksandrov <aaleksandrov.cy@gmail.com>
1 parent a31e1d2 commit ce1bb97

12 files changed

Lines changed: 358 additions & 8 deletions

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
- Secure credentials [doc](https://github.com/kaasops/vector-operator/blob/main/docs/secure-credential.md)
66
- Collect logs from file [doc](https://github.com/kaasops/vector-operator/blob/main/docs/logs-from-file.md)
77
- Collect journald services logs [doc](https://github.com/kaasops/vector-operator/blob/main/docs/journald-logs.md)
8+
- Force ConfigCheck via annotation [doc](https://github.com/kaasops/vector-operator/blob/main/docs/force-configcheck.md)

docs/force-configcheck.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Force ConfigCheck via Annotation
2+
3+
## Problem
4+
5+
The pipeline controller uses hash-based change detection that includes `Spec`, `Labels`, and `ServiceName` annotation.
6+
When external dependencies change (Secrets, ConfigMaps, Aggregator endpoints), the hash remains the same and configcheck does not run.
7+
8+
## Solution
9+
10+
Annotate your VectorPipeline or ClusterVectorPipeline with `vector-operator.kaasops.io/force-configcheck` to trigger configcheck.
11+
12+
The annotation value is included in the pipeline hash. When it changes, the hash changes, and configcheck runs.
13+
14+
## Usage
15+
16+
```bash
17+
# Trigger configcheck
18+
kubectl annotate vp my-pipeline \
19+
vector-operator.kaasops.io/force-configcheck="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
20+
21+
# Re-trigger (change the value)
22+
kubectl annotate vp my-pipeline \
23+
vector-operator.kaasops.io/force-configcheck="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
24+
--overwrite
25+
26+
# ClusterVectorPipeline
27+
kubectl annotate cvp my-cluster-pipeline \
28+
vector-operator.kaasops.io/force-configcheck="v2" --overwrite
29+
30+
# Batch: all pipelines with a specific label
31+
kubectl get vp -l app=myapp -o name | \
32+
xargs -P10 -I{} kubectl annotate {} \
33+
vector-operator.kaasops.io/force-configcheck="$(date +%s)" --overwrite
34+
35+
# Batch: all pipelines in a namespace
36+
kubectl get vp -n production -o name | \
37+
xargs -P10 -I{} kubectl annotate -n production {} \
38+
vector-operator.kaasops.io/force-configcheck="$(date +%s)" --overwrite
39+
40+
# Batch: only invalid pipelines
41+
kubectl get vp -A -o json | \
42+
jq -r '.items[] | select(.status.configCheckResult == false) | "-n \(.metadata.namespace) vp \(.metadata.name)"' | \
43+
xargs -P10 -l kubectl annotate \
44+
vector-operator.kaasops.io/force-configcheck="$(date +%s)" --overwrite
45+
```
46+
47+
## How it works
48+
49+
1. User sets annotation with any value (timestamp, version, UUID)
50+
2. The annotation value is included in the pipeline hash calculation
51+
3. Changed value = changed hash = configcheck runs
52+
4. After configcheck, the new hash is saved in `status.LastAppliedPipelineHash`
53+
5. Same annotation value on next reconcile = same hash = no configcheck
54+
55+
## Notes
56+
57+
- The annotation value can be any non-empty string
58+
- Setting the annotation to the same value has no effect (hash unchanged)
59+
- Removing the annotation also changes the hash and triggers configcheck
60+
- Pipelines without the annotation are unaffected (backward compatible)
61+
- The controller never modifies the annotation — GitOps compatible

internal/common/annotations.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package common
22

33
const (
4-
AnnotationServiceName = "observability.kaasops.io/service-name"
5-
AnnotationRestartedAt = "vector-operator.kaasops.io/restartedAt"
4+
AnnotationServiceName = "observability.kaasops.io/service-name"
5+
AnnotationRestartedAt = "vector-operator.kaasops.io/restartedAt"
6+
AnnotationForceConfigCheck = "vector-operator.kaasops.io/force-configcheck"
67
)

internal/pipeline/hash.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,18 @@ import (
2525
)
2626

2727
type tmp struct {
28-
Spec v1alpha1.VectorPipelineSpec
29-
Labels map[string]string
30-
ServiceName string
28+
Spec v1alpha1.VectorPipelineSpec
29+
Labels map[string]string
30+
ServiceName string
31+
ForceConfigCheck string `json:",omitempty"`
3132
}
3233

3334
func GetPipelineHash(pipeline Pipeline) (*uint32, error) {
3435
a, err := json.Marshal(tmp{
35-
Spec: pipeline.GetSpec(),
36-
Labels: pipeline.GetLabels(),
37-
ServiceName: pipeline.GetAnnotations()[common.AnnotationServiceName],
36+
Spec: pipeline.GetSpec(),
37+
Labels: pipeline.GetLabels(),
38+
ServiceName: pipeline.GetAnnotations()[common.AnnotationServiceName],
39+
ForceConfigCheck: pipeline.GetAnnotations()[common.AnnotationForceConfigCheck],
3840
})
3941
if err != nil {
4042
return nil, err
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
Copyright 2024.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package e2e
18+
19+
import (
20+
"time"
21+
22+
. "github.com/onsi/ginkgo/v2"
23+
. "github.com/onsi/gomega"
24+
25+
"github.com/kaasops/vector-operator/test/e2e/framework"
26+
"github.com/kaasops/vector-operator/test/e2e/framework/config"
27+
)
28+
29+
const (
30+
forceConfigCheckAgent = "force-cc-agent"
31+
forceConfigCheckPipeline = "force-cc-pipeline"
32+
forceConfigCheckClusterPipeline = "force-cc-cvp"
33+
)
34+
35+
// Force ConfigCheck tests verify that the force-configcheck annotation
36+
// triggers configcheck even when the pipeline spec has not changed.
37+
var _ = Describe("Force ConfigCheck Annotation", Label(config.LabelSmoke, config.LabelFast), Ordered, func() {
38+
f := framework.NewUniqueFramework("test-force-configcheck")
39+
40+
BeforeAll(func() {
41+
f.Setup()
42+
43+
By("deploying Vector Agent")
44+
f.ApplyTestData("force-configcheck/agent.yaml")
45+
time.Sleep(5 * time.Second)
46+
})
47+
48+
AfterAll(func() {
49+
f.DeleteClusterResource("clustervectorpipeline", forceConfigCheckClusterPipeline)
50+
f.Teardown()
51+
f.PrintMetrics()
52+
})
53+
54+
Context("pipeline without annotation", func() {
55+
It("should validate pipeline normally", func() {
56+
By("creating a VectorPipeline without force-configcheck annotation")
57+
f.ApplyTestData("force-configcheck/pipeline.yaml")
58+
59+
By("waiting for pipeline to become valid")
60+
f.WaitForPipelineValid(forceConfigCheckPipeline)
61+
62+
By("verifying agent processes the pipeline")
63+
Eventually(func() error {
64+
return f.VerifyAgentHasPipeline(forceConfigCheckAgent, forceConfigCheckPipeline)
65+
}, config.ServiceCreateTimeout, config.DefaultPollInterval).Should(Succeed())
66+
})
67+
})
68+
69+
Context("adding force-configcheck annotation", func() {
70+
It("should re-run configcheck when annotation is added", func() {
71+
By("recording current pipeline hash")
72+
hashBefore := f.GetPipelineStatus(forceConfigCheckPipeline, "LastAppliedPipelineHash")
73+
Expect(hashBefore).NotTo(BeEmpty(), "Pipeline should have a hash after initial validation")
74+
75+
By("applying pipeline with force-configcheck annotation set to v1")
76+
f.ApplyTestData("force-configcheck/pipeline-with-annotation.yaml")
77+
78+
By("waiting for pipeline to become valid again")
79+
f.WaitForPipelineValid(forceConfigCheckPipeline)
80+
81+
By("verifying pipeline hash changed due to annotation")
82+
Eventually(func() string {
83+
return f.GetPipelineStatus(forceConfigCheckPipeline, "LastAppliedPipelineHash")
84+
}, config.PipelineValidTimeout, config.DefaultPollInterval).ShouldNot(Equal(hashBefore),
85+
"Hash should change after adding force-configcheck annotation")
86+
})
87+
})
88+
89+
Context("same annotation value", func() {
90+
It("should not re-run configcheck for the same annotation value", func() {
91+
By("recording current pipeline hash")
92+
hashBefore := f.GetPipelineStatus(forceConfigCheckPipeline, "LastAppliedPipelineHash")
93+
94+
By("re-applying pipeline with same annotation value v1")
95+
f.ApplyTestData("force-configcheck/pipeline-with-annotation.yaml")
96+
97+
By("waiting briefly for any reconciliation")
98+
time.Sleep(5 * time.Second)
99+
100+
By("verifying hash has not changed (no configcheck re-run)")
101+
hashAfter := f.GetPipelineStatus(forceConfigCheckPipeline, "LastAppliedPipelineHash")
102+
Expect(hashAfter).To(Equal(hashBefore),
103+
"Hash should not change when annotation value is the same")
104+
})
105+
})
106+
107+
Context("changed annotation value", func() {
108+
It("should re-run configcheck when annotation value changes", func() {
109+
By("recording current pipeline hash")
110+
hashBefore := f.GetPipelineStatus(forceConfigCheckPipeline, "LastAppliedPipelineHash")
111+
112+
By("applying pipeline with changed annotation value v2")
113+
f.ApplyTestData("force-configcheck/pipeline-with-annotation-v2.yaml")
114+
115+
By("waiting for pipeline to become valid again")
116+
f.WaitForPipelineValid(forceConfigCheckPipeline)
117+
118+
By("verifying pipeline hash changed due to new annotation value")
119+
Eventually(func() string {
120+
return f.GetPipelineStatus(forceConfigCheckPipeline, "LastAppliedPipelineHash")
121+
}, config.PipelineValidTimeout, config.DefaultPollInterval).ShouldNot(Equal(hashBefore),
122+
"Hash should change after changing force-configcheck annotation value")
123+
})
124+
})
125+
126+
Context("ClusterVectorPipeline without annotation", func() {
127+
It("should validate CVP normally", func() {
128+
By("creating a ClusterVectorPipeline without force-configcheck annotation")
129+
f.ApplyTestDataWithoutNamespaceReplacement("force-configcheck/cluster-pipeline.yaml")
130+
131+
By("waiting for CVP to become valid")
132+
f.WaitForClusterPipelineValid(forceConfigCheckClusterPipeline)
133+
})
134+
})
135+
136+
Context("ClusterVectorPipeline with annotation", func() {
137+
It("should re-run configcheck when annotation is added to CVP", func() {
138+
By("recording current CVP hash")
139+
hashBefore := f.GetClusterPipelineStatus(forceConfigCheckClusterPipeline, "LastAppliedPipelineHash")
140+
Expect(hashBefore).NotTo(BeEmpty(), "CVP should have a hash after initial validation")
141+
142+
By("applying CVP with force-configcheck annotation set to v1")
143+
f.ApplyTestDataWithoutNamespaceReplacement("force-configcheck/cluster-pipeline-with-annotation.yaml")
144+
145+
By("waiting for CVP to become valid again")
146+
f.WaitForClusterPipelineValid(forceConfigCheckClusterPipeline)
147+
148+
By("verifying CVP hash changed due to annotation")
149+
Eventually(func() string {
150+
return f.GetClusterPipelineStatus(forceConfigCheckClusterPipeline, "LastAppliedPipelineHash")
151+
}, config.PipelineValidTimeout, config.DefaultPollInterval).ShouldNot(Equal(hashBefore),
152+
"CVP hash should change after adding force-configcheck annotation")
153+
})
154+
})
155+
156+
Context("ClusterVectorPipeline changed annotation value", func() {
157+
It("should re-run configcheck when CVP annotation value changes", func() {
158+
By("recording current CVP hash")
159+
hashBefore := f.GetClusterPipelineStatus(forceConfigCheckClusterPipeline, "LastAppliedPipelineHash")
160+
161+
By("applying CVP with changed annotation value v2")
162+
f.ApplyTestDataWithoutNamespaceReplacement("force-configcheck/cluster-pipeline-with-annotation-v2.yaml")
163+
164+
By("waiting for CVP to become valid again")
165+
f.WaitForClusterPipelineValid(forceConfigCheckClusterPipeline)
166+
167+
By("verifying CVP hash changed due to new annotation value")
168+
Eventually(func() string {
169+
return f.GetClusterPipelineStatus(forceConfigCheckClusterPipeline, "LastAppliedPipelineHash")
170+
}, config.PipelineValidTimeout, config.DefaultPollInterval).ShouldNot(Equal(hashBefore),
171+
"CVP hash should change after changing force-configcheck annotation value")
172+
})
173+
})
174+
})
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
apiVersion: observability.kaasops.io/v1alpha1
2+
kind: Vector
3+
metadata:
4+
name: force-cc-agent
5+
spec:
6+
agent:
7+
image: timberio/vector:0.40.0-alpine
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
apiVersion: observability.kaasops.io/v1alpha1
2+
kind: ClusterVectorPipeline
3+
metadata:
4+
name: force-cc-cvp
5+
annotations:
6+
vector-operator.kaasops.io/force-configcheck: "v2"
7+
spec:
8+
sources:
9+
kubernetes_logs:
10+
type: kubernetes_logs
11+
extra_label_selector: "app=test-cluster"
12+
sinks:
13+
console:
14+
type: console
15+
inputs:
16+
- kubernetes_logs
17+
encoding:
18+
codec: json
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
apiVersion: observability.kaasops.io/v1alpha1
2+
kind: ClusterVectorPipeline
3+
metadata:
4+
name: force-cc-cvp
5+
annotations:
6+
vector-operator.kaasops.io/force-configcheck: "v1"
7+
spec:
8+
sources:
9+
kubernetes_logs:
10+
type: kubernetes_logs
11+
extra_label_selector: "app=test-cluster"
12+
sinks:
13+
console:
14+
type: console
15+
inputs:
16+
- kubernetes_logs
17+
encoding:
18+
codec: json
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apiVersion: observability.kaasops.io/v1alpha1
2+
kind: ClusterVectorPipeline
3+
metadata:
4+
name: force-cc-cvp
5+
spec:
6+
sources:
7+
kubernetes_logs:
8+
type: kubernetes_logs
9+
extra_label_selector: "app=test-cluster"
10+
sinks:
11+
console:
12+
type: console
13+
inputs:
14+
- kubernetes_logs
15+
encoding:
16+
codec: json
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
apiVersion: observability.kaasops.io/v1alpha1
2+
kind: VectorPipeline
3+
metadata:
4+
name: force-cc-pipeline
5+
annotations:
6+
vector-operator.kaasops.io/force-configcheck: "v2"
7+
spec:
8+
sources:
9+
kubernetes_logs:
10+
type: kubernetes_logs
11+
extra_label_selector: "app=test-app"
12+
sinks:
13+
console:
14+
type: console
15+
inputs:
16+
- kubernetes_logs
17+
encoding:
18+
codec: json

0 commit comments

Comments
 (0)