Skip to content
Merged
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ require (
)

require (
dario.cat/mergo v1.0.2
github.com/argoproj/argo-cd/v3 v3.3.10
sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20250308055145-5fe7bb3edc86
sigs.k8s.io/controller-tools v0.16.4
Expand All @@ -38,7 +39,6 @@ require (
require (
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cyphar.com/go-pathrs v0.2.5 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/42wim/httpsig v1.2.4 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.0 // indirect
Expand Down
116 changes: 78 additions & 38 deletions internal/controller/argo.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"strconv"
"strings"

"dario.cat/mergo"
v1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/resource"
Expand Down Expand Up @@ -521,6 +522,39 @@ return health_status`,
return &s
}

func applyCustomArgoOverlay(argoCD *unstructured.Unstructured, customYAML string) error {
var overlay map[string]any
if err := yaml.Unmarshal([]byte(customYAML), &overlay); err != nil {
return fmt.Errorf("failed to parse custom ArgoCD YAML: %w", err)
}

spec, found, err := unstructured.NestedMap(argoCD.Object, "spec")
if err != nil {
return fmt.Errorf("failed to get spec from ArgoCD: %w", err)
}
if !found {
spec = make(map[string]any)
}

if err := mergo.Merge(&spec, overlay, mergo.WithOverride); err != nil {
return fmt.Errorf("failed to merge custom ArgoCD overlay: %w", err)
}

argoCD.Object["spec"] = spec
return nil
}

func applyCustomArgoOverlayToSpec(argo *argooperator.ArgoCD, customYAML string) error {
var overlay argooperator.ArgoCDSpec
if err := yaml.Unmarshal([]byte(customYAML), &overlay); err != nil {
return fmt.Errorf("failed to parse custom ArgoCD YAML into spec: %w", err)
}
if err := mergo.Merge(&argo.Spec, overlay, mergo.WithOverride); err != nil {
return fmt.Errorf("failed to merge custom ArgoCD overlay: %w", err)
}
return nil
}

func compareArgoCD(goal, actual *argooperator.ArgoCD) bool {
if goal == nil && actual == nil {
return true
Expand All @@ -545,57 +579,63 @@ func createOrUpdateArgoCD(client dynamic.Interface, fullClient kubernetes.Interf
argo := newArgoCD(name, namespace, patternsOperatorConfig)
gvr := schema.GroupVersionResource{Group: ArgoCDGroup, Version: ArgoCDVersion, Resource: ArgoCDResource}

var err error
// we skip this check if fullClient is explicitly nil for simpler testing
if fullClient != nil {
err = checkAPIVersion(fullClient, ArgoCDGroup, ArgoCDVersion)
if err != nil {
if err := checkAPIVersion(fullClient, ArgoCDGroup, ArgoCDVersion); err != nil {
return fmt.Errorf("cannot find a sufficiently recent argocd crd version: %v", err)
}
}

if !haveArgo(client, name, namespace) {
// create it
obj, errConvert := runtime.DefaultUnstructuredConverter.ToUnstructured(argo)
if errConvert != nil {
return fmt.Errorf("failed to convert ArgoCD to unstructured for create: %v", errConvert)
}
newArgo := &unstructured.Unstructured{Object: obj}
_, err = client.Resource(gvr).Namespace(namespace).Create(context.TODO(), newArgo, metav1.CreateOptions{})
} else { // update it
oldArgo, oldUnstructured, errGet := getArgoCDFunc(client, name, namespace)
if errGet != nil {
return fmt.Errorf("failed to get existing ArgoCD %s/%s: %v", namespace, name, errGet)
}
if oldArgo == nil || oldUnstructured == nil {
return fmt.Errorf("getArgoCD returned nil ArgoCD object for %s/%s", namespace, name)
customYAML := patternsOperatorConfig.getStringValue(configKeyCustomArgoYaml)
if customYAML != "" {
if err := applyCustomArgoOverlayToSpec(argo, customYAML); err != nil {
log.Printf("Failed to apply %s overlay to spec: %v", configKeyCustomArgoYaml, err)
}
}

// ArgoCD is up to date, skipping update
if compareArgoCD(argo, oldArgo) {
return nil
}
obj, errConvert := runtime.DefaultUnstructuredConverter.ToUnstructured(argo)
if errConvert != nil {
return fmt.Errorf("failed to convert ArgoCD to unstructured: %v", errConvert)
}
newArgo := &unstructured.Unstructured{Object: obj}

argo.SetResourceVersion(oldArgo.GetResourceVersion())
obj, errConvert := runtime.DefaultUnstructuredConverter.ToUnstructured(argo)
if errConvert != nil {
return fmt.Errorf("failed to convert ArgoCD to unstructured for update: %v", errConvert)
// Apply overlay at the unstructured level to preserve fields not in the vendored Go types
if customYAML != "" {
if err := applyCustomArgoOverlay(newArgo, customYAML); err != nil {
log.Printf("Failed to apply %s unstructured overlay: %v", configKeyCustomArgoYaml, err)
}
newArgo := &unstructured.Unstructured{Object: obj}

// Preserve spec fields not known to this vendored argocd-operator version
// (e.g. networkPolicy added in gitops-operator v1.20.3) to avoid
// stripping them and causing infinite reconciliation loops.
oldSpec, _, _ := unstructured.NestedMap(oldUnstructured.Object, "spec")
newSpec, _, _ := unstructured.NestedMap(newArgo.Object, "spec")
for key, val := range oldSpec {
if _, exists := newSpec[key]; !exists {
_ = unstructured.SetNestedField(newArgo.Object, val, "spec", key)
}
}

if !haveArgo(client, name, namespace) {
_, err := client.Resource(gvr).Namespace(namespace).Create(context.TODO(), newArgo, metav1.CreateOptions{})
return err
}

oldArgo, oldUnstructured, errGet := getArgoCDFunc(client, name, namespace)
if errGet != nil {
return fmt.Errorf("failed to get existing ArgoCD %s/%s: %v", namespace, name, errGet)
}
if oldUnstructured == nil {
return fmt.Errorf("getArgoCD returned nil ArgoCD object for %s/%s", namespace, name)
}

// Preserve spec fields not known to this vendored argocd-operator version
// (e.g. networkPolicy added in gitops-operator v1.20.3) to avoid
// stripping them and causing infinite reconciliation loops.
oldSpec, _, _ := unstructured.NestedMap(oldUnstructured.Object, "spec")
newSpec, _, _ := unstructured.NestedMap(newArgo.Object, "spec")
for key, val := range oldSpec {
if _, exists := newSpec[key]; !exists {
_ = unstructured.SetNestedField(newArgo.Object, val, "spec", key)
}
}

_, err = client.Resource(gvr).Namespace(namespace).Update(context.TODO(), newArgo, metav1.UpdateOptions{})
if compareArgoCD(argo, oldArgo) {
return nil
}

newArgo.SetResourceVersion(oldUnstructured.GetResourceVersion())
_, err := client.Resource(gvr).Namespace(namespace).Update(context.TODO(), newArgo, metav1.UpdateOptions{})
return err
}

Expand Down
Loading