diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 0a6fcfb..ed4533d 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -84,6 +84,14 @@ const ( // AnnotationDeletionAttempts tracks number of failed deletion attempts. AnnotationDeletionAttempts = Domain + "/deletion-attempts" + // Transformation Annotations + + // AnnotationTransform contains YAML transformation rules for mirrored resources. + AnnotationTransform = Domain + "/transform" + + // AnnotationTransformStrict enables strict mode (transformation errors block mirroring). + AnnotationTransformStrict = Domain + "/transform-strict" + // Finalizers // FinalizerName is the finalizer added to source resources. diff --git a/pkg/controller/mirror.go b/pkg/controller/mirror.go index 9b74189..58027b1 100644 --- a/pkg/controller/mirror.go +++ b/pkg/controller/mirror.go @@ -404,7 +404,7 @@ func applyTransformations(source, mirror runtime.Object, targetNamespace string) return mirror, nil } - transformRules, hasTransform := sourceAnnotations[transformer.AnnotationTransform] + transformRules, hasTransform := sourceAnnotations[constants.AnnotationTransform] if !hasTransform || transformRules == "" { return mirror, nil // No transformation rules } @@ -422,9 +422,9 @@ func applyTransformations(source, mirror runtime.Object, targetNamespace string) } // Copy transform annotations from source - mirrorAnnotations[transformer.AnnotationTransform] = transformRules - if strictMode, hasStrict := sourceAnnotations[transformer.AnnotationTransformStrict]; hasStrict { - mirrorAnnotations[transformer.AnnotationTransformStrict] = strictMode + mirrorAnnotations[constants.AnnotationTransform] = transformRules + if strictMode, hasStrict := sourceAnnotations[constants.AnnotationTransformStrict]; hasStrict { + mirrorAnnotations[constants.AnnotationTransformStrict] = strictMode } mirrorObj.SetAnnotations(mirrorAnnotations) @@ -443,8 +443,8 @@ func applyTransformations(source, mirror runtime.Object, targetNamespace string) // Remove transform annotations from result (they shouldn't persist on mirrors) if transformedObj, ok := transformed.(metav1.Object); ok { annotations := transformedObj.GetAnnotations() - delete(annotations, transformer.AnnotationTransform) - delete(annotations, transformer.AnnotationTransformStrict) + delete(annotations, constants.AnnotationTransform) + delete(annotations, constants.AnnotationTransformStrict) transformedObj.SetAnnotations(annotations) } diff --git a/pkg/hash/content.go b/pkg/hash/content.go index f9adc3c..93cdfe7 100644 --- a/pkg/hash/content.go +++ b/pkg/hash/content.go @@ -51,19 +51,37 @@ func extractContent(obj runtime.Object) (interface{}, error) { // extractSecretContent extracts content from a Secret. func extractSecretContent(secret *corev1.Secret) map[string]interface{} { - return map[string]interface{}{ + content := map[string]interface{}{ "type": string(secret.Type), "data": secret.Data, "stringData": secret.StringData, } + + // Include transform annotation in hash so changes to transformation rules trigger updates + if secret.Annotations != nil { + if transform, exists := secret.Annotations[constants.AnnotationTransform]; exists { + content["transform"] = transform + } + } + + return content } // extractConfigMapContent extracts content from a ConfigMap. func extractConfigMapContent(cm *corev1.ConfigMap) map[string]interface{} { - return map[string]interface{}{ + content := map[string]interface{}{ "data": cm.Data, "binaryData": cm.BinaryData, } + + // Include transform annotation in hash so changes to transformation rules trigger updates + if cm.Annotations != nil { + if transform, exists := cm.Annotations[constants.AnnotationTransform]; exists { + content["transform"] = transform + } + } + + return content } // extractUnstructuredContent extracts content from an unstructured resource (CRDs, etc.). @@ -100,6 +118,14 @@ func extractUnstructuredContent(obj runtime.Object) (interface{}, error) { } } + // Include transform annotation in hash so changes to transformation rules trigger updates + annotations := uCopy.GetAnnotations() + if annotations != nil { + if transform, exists := annotations[constants.AnnotationTransform]; exists { + content["transform"] = transform + } + } + return content, nil } diff --git a/pkg/transformer/transformer.go b/pkg/transformer/transformer.go index eb42d32..17eab61 100644 --- a/pkg/transformer/transformer.go +++ b/pkg/transformer/transformer.go @@ -11,14 +11,8 @@ import ( "gopkg.in/yaml.v3" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" -) -const ( - // AnnotationTransform is the annotation key for transformation rules - AnnotationTransform = "kubemirror.raczylo.com/transform" - - // AnnotationTransformStrict enables strict mode (errors block mirroring) - AnnotationTransformStrict = "kubemirror.raczylo.com/transform-strict" + "github.com/lukaszraczylo/kubemirror/pkg/constants" ) // Transformer applies transformation rules to Kubernetes resources. @@ -93,7 +87,7 @@ func (t *Transformer) parseTransformRules(u *unstructured.Unstructured) (*Transf return &TransformRules{}, nil } - rulesYAML, exists := annotations[AnnotationTransform] + rulesYAML, exists := annotations[constants.AnnotationTransform] if !exists || rulesYAML == "" { return &TransformRules{}, nil } @@ -256,7 +250,7 @@ func (t *Transformer) isStrictMode(u *unstructured.Unstructured) bool { return false } - strictValue, exists := annotations[AnnotationTransformStrict] + strictValue, exists := annotations[constants.AnnotationTransformStrict] return exists && (strictValue == "true" || strictValue == "1") } diff --git a/pkg/transformer/transformer_test.go b/pkg/transformer/transformer_test.go index 9f8bd52..9f021ea 100644 --- a/pkg/transformer/transformer_test.go +++ b/pkg/transformer/transformer_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/lukaszraczylo/kubemirror/pkg/constants" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" @@ -30,7 +31,7 @@ func TestTransformer_Transform(t *testing.T) { Name: "test-config", Namespace: "default", Annotations: map[string]string{ - AnnotationTransform: ` + constants.AnnotationTransform: ` rules: - path: data.LOG_LEVEL value: "error" @@ -63,7 +64,7 @@ rules: Name: "test-config", Namespace: "default", Annotations: map[string]string{ - AnnotationTransform: ` + constants.AnnotationTransform: ` rules: - path: data.API_URL template: "https://{{.TargetNamespace}}.api.example.com" @@ -93,7 +94,7 @@ rules: Name: "test-config", Namespace: "default", Annotations: map[string]string{ - AnnotationTransform: ` + constants.AnnotationTransform: ` rules: - path: data.NAMESPACE_UPPER template: "{{upper .TargetNamespace}}" @@ -136,7 +137,7 @@ rules: "app": "myapp", }, Annotations: map[string]string{ - AnnotationTransform: ` + constants.AnnotationTransform: ` rules: - path: metadata.labels merge: @@ -165,7 +166,7 @@ rules: Name: "test-config", Namespace: "default", Annotations: map[string]string{ - AnnotationTransform: ` + constants.AnnotationTransform: ` rules: - path: metadata.labels merge: @@ -193,7 +194,7 @@ rules: Name: "test-config", Namespace: "default", Annotations: map[string]string{ - AnnotationTransform: ` + constants.AnnotationTransform: ` rules: - path: data.DEBUG_MODE delete: true @@ -227,8 +228,8 @@ rules: Name: "test-config", Namespace: "default", Annotations: map[string]string{ - AnnotationTransform: "invalid: yaml: [[[", - AnnotationTransformStrict: "true", + constants.AnnotationTransform: "invalid: yaml: [[[", + constants.AnnotationTransformStrict: "true", }, }, Data: map[string]string{}, @@ -247,11 +248,11 @@ rules: Name: "test-config", Namespace: "default", Annotations: map[string]string{ - AnnotationTransform: ` + constants.AnnotationTransform: ` rules: - value: "something" `, - AnnotationTransformStrict: "true", + constants.AnnotationTransformStrict: "true", }, }, Data: map[string]string{}, @@ -273,8 +274,8 @@ rules: Name: "test-config", Namespace: "default", Annotations: map[string]string{ - AnnotationTransform: rules, - AnnotationTransformStrict: "true", + constants.AnnotationTransform: rules, + constants.AnnotationTransformStrict: "true", }, }, Data: map[string]string{}, @@ -319,7 +320,7 @@ rules: Name: "test-config", Namespace: "default", Annotations: map[string]string{ - AnnotationTransform: "invalid yaml [[[", + constants.AnnotationTransform: "invalid yaml [[[", }, }, Data: map[string]string{ @@ -347,7 +348,7 @@ rules: Name: "test-config", Namespace: "default", Annotations: map[string]string{ - AnnotationTransform: ` + constants.AnnotationTransform: ` rules: - path: data.KEY1 value: "first" @@ -402,7 +403,7 @@ rules: "name": "test-pod", "namespace": "default", "annotations": map[string]interface{}{ - AnnotationTransform: ` + constants.AnnotationTransform: ` rules: - path: spec.containers[0].image template: "registry.{{.TargetNamespace}}.example.com/app:v1" @@ -457,7 +458,7 @@ rules: Name: "test-config", Namespace: "default", Annotations: map[string]string{ - AnnotationTransform: ` + constants.AnnotationTransform: ` rules: - path: data.VALUE template: "{{.TargetNamespace}}-empty" @@ -1336,7 +1337,7 @@ rules: "name": "test-config", "namespace": "source-namespace", "annotations": map[string]interface{}{ - AnnotationTransform: tt.rules, + constants.AnnotationTransform: tt.rules, }, }, },