Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
- Secure credentials [doc](https://github.com/kaasops/vector-operator/blob/main/docs/secure-credential.md)
- Collect logs from file [doc](https://github.com/kaasops/vector-operator/blob/main/docs/logs-from-file.md)
- Collect journald services logs [doc](https://github.com/kaasops/vector-operator/blob/main/docs/journald-logs.md)
- Force ConfigCheck via annotation [doc](https://github.com/kaasops/vector-operator/blob/main/docs/force-configcheck.md)
61 changes: 61 additions & 0 deletions docs/force-configcheck.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Force ConfigCheck via Annotation

## Problem

The pipeline controller uses hash-based change detection that includes `Spec`, `Labels`, and `ServiceName` annotation.
When external dependencies change (Secrets, ConfigMaps, Aggregator endpoints), the hash remains the same and configcheck does not run.

## Solution

Annotate your VectorPipeline or ClusterVectorPipeline with `vector-operator.kaasops.io/force-configcheck` to trigger configcheck.

The annotation value is included in the pipeline hash. When it changes, the hash changes, and configcheck runs.

## Usage

```bash
# Trigger configcheck
kubectl annotate vp my-pipeline \
vector-operator.kaasops.io/force-configcheck="$(date -u +%Y-%m-%dT%H:%M:%SZ)"

# Re-trigger (change the value)
kubectl annotate vp my-pipeline \
vector-operator.kaasops.io/force-configcheck="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--overwrite

# ClusterVectorPipeline
kubectl annotate cvp my-cluster-pipeline \
vector-operator.kaasops.io/force-configcheck="v2" --overwrite

# Batch: all pipelines with a specific label
kubectl get vp -l app=myapp -o name | \
xargs -P10 -I{} kubectl annotate {} \
vector-operator.kaasops.io/force-configcheck="$(date +%s)" --overwrite

# Batch: all pipelines in a namespace
kubectl get vp -n production -o name | \
xargs -P10 -I{} kubectl annotate -n production {} \
vector-operator.kaasops.io/force-configcheck="$(date +%s)" --overwrite

# Batch: only invalid pipelines
kubectl get vp -A -o json | \
jq -r '.items[] | select(.status.configCheckResult == false) | "-n \(.metadata.namespace) vp \(.metadata.name)"' | \
xargs -P10 -l kubectl annotate \
vector-operator.kaasops.io/force-configcheck="$(date +%s)" --overwrite
```

## How it works

1. User sets annotation with any value (timestamp, version, UUID)
2. The annotation value is included in the pipeline hash calculation
3. Changed value = changed hash = configcheck runs
4. After configcheck, the new hash is saved in `status.LastAppliedPipelineHash`
5. Same annotation value on next reconcile = same hash = no configcheck

## Notes

- The annotation value can be any non-empty string
- Setting the annotation to the same value has no effect (hash unchanged)
- Removing the annotation also changes the hash and triggers configcheck
- Pipelines without the annotation are unaffected (backward compatible)
- The controller never modifies the annotation — GitOps compatible
5 changes: 3 additions & 2 deletions internal/common/annotations.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package common

const (
AnnotationServiceName = "observability.kaasops.io/service-name"
AnnotationRestartedAt = "vector-operator.kaasops.io/restartedAt"
AnnotationServiceName = "observability.kaasops.io/service-name"
AnnotationRestartedAt = "vector-operator.kaasops.io/restartedAt"
AnnotationForceConfigCheck = "vector-operator.kaasops.io/force-configcheck"
)
14 changes: 8 additions & 6 deletions internal/pipeline/hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,18 @@ import (
)

type tmp struct {
Spec v1alpha1.VectorPipelineSpec
Labels map[string]string
ServiceName string
Spec v1alpha1.VectorPipelineSpec
Labels map[string]string
ServiceName string
ForceConfigCheck string `json:",omitempty"`
}

func GetPipelineHash(pipeline Pipeline) (*uint32, error) {
a, err := json.Marshal(tmp{
Spec: pipeline.GetSpec(),
Labels: pipeline.GetLabels(),
ServiceName: pipeline.GetAnnotations()[common.AnnotationServiceName],
Spec: pipeline.GetSpec(),
Labels: pipeline.GetLabels(),
ServiceName: pipeline.GetAnnotations()[common.AnnotationServiceName],
ForceConfigCheck: pipeline.GetAnnotations()[common.AnnotationForceConfigCheck],
})
if err != nil {
return nil, err
Expand Down
174 changes: 174 additions & 0 deletions test/e2e/force_configcheck_e2e_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
Copyright 2024.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package e2e

import (
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

"github.com/kaasops/vector-operator/test/e2e/framework"
"github.com/kaasops/vector-operator/test/e2e/framework/config"
)

const (
forceConfigCheckAgent = "force-cc-agent"
forceConfigCheckPipeline = "force-cc-pipeline"
forceConfigCheckClusterPipeline = "force-cc-cvp"
)

// Force ConfigCheck tests verify that the force-configcheck annotation
// triggers configcheck even when the pipeline spec has not changed.
var _ = Describe("Force ConfigCheck Annotation", Label(config.LabelSmoke, config.LabelFast), Ordered, func() {
f := framework.NewUniqueFramework("test-force-configcheck")

BeforeAll(func() {
f.Setup()

By("deploying Vector Agent")
f.ApplyTestData("force-configcheck/agent.yaml")
time.Sleep(5 * time.Second)
})

AfterAll(func() {
f.DeleteClusterResource("clustervectorpipeline", forceConfigCheckClusterPipeline)
f.Teardown()
f.PrintMetrics()
})

Context("pipeline without annotation", func() {
It("should validate pipeline normally", func() {
By("creating a VectorPipeline without force-configcheck annotation")
f.ApplyTestData("force-configcheck/pipeline.yaml")

By("waiting for pipeline to become valid")
f.WaitForPipelineValid(forceConfigCheckPipeline)

By("verifying agent processes the pipeline")
Eventually(func() error {
return f.VerifyAgentHasPipeline(forceConfigCheckAgent, forceConfigCheckPipeline)
}, config.ServiceCreateTimeout, config.DefaultPollInterval).Should(Succeed())
})
})

Context("adding force-configcheck annotation", func() {
It("should re-run configcheck when annotation is added", func() {
By("recording current pipeline hash")
hashBefore := f.GetPipelineStatus(forceConfigCheckPipeline, "LastAppliedPipelineHash")
Expect(hashBefore).NotTo(BeEmpty(), "Pipeline should have a hash after initial validation")

By("applying pipeline with force-configcheck annotation set to v1")
f.ApplyTestData("force-configcheck/pipeline-with-annotation.yaml")

By("waiting for pipeline to become valid again")
f.WaitForPipelineValid(forceConfigCheckPipeline)

By("verifying pipeline hash changed due to annotation")
Eventually(func() string {
return f.GetPipelineStatus(forceConfigCheckPipeline, "LastAppliedPipelineHash")
}, config.PipelineValidTimeout, config.DefaultPollInterval).ShouldNot(Equal(hashBefore),
"Hash should change after adding force-configcheck annotation")
})
})

Context("same annotation value", func() {
It("should not re-run configcheck for the same annotation value", func() {
By("recording current pipeline hash")
hashBefore := f.GetPipelineStatus(forceConfigCheckPipeline, "LastAppliedPipelineHash")

By("re-applying pipeline with same annotation value v1")
f.ApplyTestData("force-configcheck/pipeline-with-annotation.yaml")

By("waiting briefly for any reconciliation")
time.Sleep(5 * time.Second)

By("verifying hash has not changed (no configcheck re-run)")
hashAfter := f.GetPipelineStatus(forceConfigCheckPipeline, "LastAppliedPipelineHash")
Expect(hashAfter).To(Equal(hashBefore),
"Hash should not change when annotation value is the same")
})
})

Context("changed annotation value", func() {
It("should re-run configcheck when annotation value changes", func() {
By("recording current pipeline hash")
hashBefore := f.GetPipelineStatus(forceConfigCheckPipeline, "LastAppliedPipelineHash")

By("applying pipeline with changed annotation value v2")
f.ApplyTestData("force-configcheck/pipeline-with-annotation-v2.yaml")

By("waiting for pipeline to become valid again")
f.WaitForPipelineValid(forceConfigCheckPipeline)

By("verifying pipeline hash changed due to new annotation value")
Eventually(func() string {
return f.GetPipelineStatus(forceConfigCheckPipeline, "LastAppliedPipelineHash")
}, config.PipelineValidTimeout, config.DefaultPollInterval).ShouldNot(Equal(hashBefore),
"Hash should change after changing force-configcheck annotation value")
})
})

Context("ClusterVectorPipeline without annotation", func() {
It("should validate CVP normally", func() {
By("creating a ClusterVectorPipeline without force-configcheck annotation")
f.ApplyTestDataWithoutNamespaceReplacement("force-configcheck/cluster-pipeline.yaml")

By("waiting for CVP to become valid")
f.WaitForClusterPipelineValid(forceConfigCheckClusterPipeline)
})
})

Context("ClusterVectorPipeline with annotation", func() {
It("should re-run configcheck when annotation is added to CVP", func() {
By("recording current CVP hash")
hashBefore := f.GetClusterPipelineStatus(forceConfigCheckClusterPipeline, "LastAppliedPipelineHash")
Expect(hashBefore).NotTo(BeEmpty(), "CVP should have a hash after initial validation")

By("applying CVP with force-configcheck annotation set to v1")
f.ApplyTestDataWithoutNamespaceReplacement("force-configcheck/cluster-pipeline-with-annotation.yaml")

By("waiting for CVP to become valid again")
f.WaitForClusterPipelineValid(forceConfigCheckClusterPipeline)

By("verifying CVP hash changed due to annotation")
Eventually(func() string {
return f.GetClusterPipelineStatus(forceConfigCheckClusterPipeline, "LastAppliedPipelineHash")
}, config.PipelineValidTimeout, config.DefaultPollInterval).ShouldNot(Equal(hashBefore),
"CVP hash should change after adding force-configcheck annotation")
})
})

Context("ClusterVectorPipeline changed annotation value", func() {
It("should re-run configcheck when CVP annotation value changes", func() {
By("recording current CVP hash")
hashBefore := f.GetClusterPipelineStatus(forceConfigCheckClusterPipeline, "LastAppliedPipelineHash")

By("applying CVP with changed annotation value v2")
f.ApplyTestDataWithoutNamespaceReplacement("force-configcheck/cluster-pipeline-with-annotation-v2.yaml")

By("waiting for CVP to become valid again")
f.WaitForClusterPipelineValid(forceConfigCheckClusterPipeline)

By("verifying CVP hash changed due to new annotation value")
Eventually(func() string {
return f.GetClusterPipelineStatus(forceConfigCheckClusterPipeline, "LastAppliedPipelineHash")
}, config.PipelineValidTimeout, config.DefaultPollInterval).ShouldNot(Equal(hashBefore),
"CVP hash should change after changing force-configcheck annotation value")
})
})
})
7 changes: 7 additions & 0 deletions test/e2e/testdata/force-configcheck/agent.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: observability.kaasops.io/v1alpha1
kind: Vector
metadata:
name: force-cc-agent
spec:
agent:
image: timberio/vector:0.40.0-alpine
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
apiVersion: observability.kaasops.io/v1alpha1
kind: ClusterVectorPipeline
metadata:
name: force-cc-cvp
annotations:
vector-operator.kaasops.io/force-configcheck: "v2"
spec:
sources:
kubernetes_logs:
type: kubernetes_logs
extra_label_selector: "app=test-cluster"
sinks:
console:
type: console
inputs:
- kubernetes_logs
encoding:
codec: json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
apiVersion: observability.kaasops.io/v1alpha1
kind: ClusterVectorPipeline
metadata:
name: force-cc-cvp
annotations:
vector-operator.kaasops.io/force-configcheck: "v1"
spec:
sources:
kubernetes_logs:
type: kubernetes_logs
extra_label_selector: "app=test-cluster"
sinks:
console:
type: console
inputs:
- kubernetes_logs
encoding:
codec: json
16 changes: 16 additions & 0 deletions test/e2e/testdata/force-configcheck/cluster-pipeline.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: observability.kaasops.io/v1alpha1
kind: ClusterVectorPipeline
metadata:
name: force-cc-cvp
spec:
sources:
kubernetes_logs:
type: kubernetes_logs
extra_label_selector: "app=test-cluster"
sinks:
console:
type: console
inputs:
- kubernetes_logs
encoding:
codec: json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
apiVersion: observability.kaasops.io/v1alpha1
kind: VectorPipeline
metadata:
name: force-cc-pipeline
annotations:
vector-operator.kaasops.io/force-configcheck: "v2"
spec:
sources:
kubernetes_logs:
type: kubernetes_logs
extra_label_selector: "app=test-app"
sinks:
console:
type: console
inputs:
- kubernetes_logs
encoding:
codec: json
18 changes: 18 additions & 0 deletions test/e2e/testdata/force-configcheck/pipeline-with-annotation.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
apiVersion: observability.kaasops.io/v1alpha1
kind: VectorPipeline
metadata:
name: force-cc-pipeline
annotations:
vector-operator.kaasops.io/force-configcheck: "v1"
spec:
sources:
kubernetes_logs:
type: kubernetes_logs
extra_label_selector: "app=test-app"
sinks:
console:
type: console
inputs:
- kubernetes_logs
encoding:
codec: json
16 changes: 16 additions & 0 deletions test/e2e/testdata/force-configcheck/pipeline.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: observability.kaasops.io/v1alpha1
kind: VectorPipeline
metadata:
name: force-cc-pipeline
spec:
sources:
kubernetes_logs:
type: kubernetes_logs
extra_label_selector: "app=test-app"
sinks:
console:
type: console
inputs:
- kubernetes_logs
encoding:
codec: json
Loading