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
2 changes: 1 addition & 1 deletion .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
- name: Run golangci-lint
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
with:
version: v2.11.3
version: v2.12.2
args: --timeout 5m -v

#--exclude SA5011
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ GOFLAGS=-mod=vendor
REGISTRY ?= localhost
UPLOADREGISTRY ?= quay.io/validatedpatterns
GOLANGCI_IMG ?= docker.io/golangci/golangci-lint
GOLANGCI_VERSION ?= 2.11.3
GOLANGCI_VERSION ?= 2.12.2


# CI uses a non-writable home dir, make sure .cache is writable
Expand Down
29 changes: 18 additions & 11 deletions internal/controller/acm.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,28 +28,35 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
)

const (
apiVersionV1 = "v1"
acmGroup = "cluster.open-cluster-management.io"
acmManagedClusterRes = "managedclusters"
acmLocalCluster = "local-cluster"
)

func haveACMHub(r *PatternReconciler) bool {
gvrMCH := schema.GroupVersionResource{Group: "operator.open-cluster-management.io", Version: "v1", Resource: "multiclusterhubs"}
gvrMCH := schema.GroupVersionResource{Group: "operator.open-cluster-management.io", Version: apiVersionV1, Resource: "multiclusterhubs"}

mch, err := r.dynamicClient.Resource(gvrMCH).Namespace("open-cluster-management").Get(context.Background(), "multiclusterhub", metav1.GetOptions{})
mch, err := r.dynamicClient.Resource(gvrMCH).Namespace(acmNamespace).Get(context.Background(), "multiclusterhub", metav1.GetOptions{})
if err != nil {
log.Printf("Error obtaining hub: %s\n", err)
return false
}

return strings.EqualFold(
mch.GetAnnotations()["patterns.gitops.validatedpatterns.io/managed"],
"true",
boolTrue,
)
}

// listManagedClusters lists all ManagedCluster resources (excluding local-cluster)
// Returns a list of cluster names and an error
func (r *PatternReconciler) listManagedClusters(ctx context.Context) ([]string, error) {
gvrMC := schema.GroupVersionResource{
Group: "cluster.open-cluster-management.io",
Version: "v1",
Resource: "managedclusters",
Group: acmGroup,
Version: apiVersionV1,
Resource: acmManagedClusterRes,
}

// ManagedCluster is a cluster-scoped resource, so no namespace needed
Expand All @@ -62,7 +69,7 @@ func (r *PatternReconciler) listManagedClusters(ctx context.Context) ([]string,
for _, item := range mcList.Items {
name := item.GetName()
// Exclude local-cluster (hub cluster)
if name != "local-cluster" {
if name != acmLocalCluster {
clusterNames = append(clusterNames, name)
}
}
Expand All @@ -74,9 +81,9 @@ func (r *PatternReconciler) listManagedClusters(ctx context.Context) ([]string,
// Returns the number of clusters deleted and an error
func (r *PatternReconciler) deleteManagedClusters(ctx context.Context) (int, error) {
gvrMC := schema.GroupVersionResource{
Group: "cluster.open-cluster-management.io",
Version: "v1",
Resource: "managedclusters",
Group: acmGroup,
Version: apiVersionV1,
Resource: acmManagedClusterRes,
}

// ManagedCluster is a cluster-scoped resource, so no namespace needed
Expand All @@ -89,7 +96,7 @@ func (r *PatternReconciler) deleteManagedClusters(ctx context.Context) (int, err
for _, item := range mcList.Items {
name := item.GetName()
// Exclude local-cluster (hub cluster)
if name == "local-cluster" {
if name == acmLocalCluster {
continue
}

Expand Down
4 changes: 3 additions & 1 deletion internal/controller/analytics.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"github.com/hybrid-cloud-patterns/patterns-operator/version"
)

const analyticsPattern = "Pattern"

type VpAnalyticsInterface interface {
SendPatternInstallationInfo(p *api.Pattern) bool
SendPatternStartEventInfo(p *api.Pattern) bool
Expand Down Expand Up @@ -98,7 +100,7 @@ func getAnalyticsContext(p *api.Pattern) *analytics.Context {

ctx := &analytics.Context{
Extra: map[string]any{
"Pattern": p.Name,
analyticsPattern: p.Name,
"Domain": getSimpleDomain(p),
"OperatorVersion": version.Version,
"RepoBaseName": getBaseGitRepo(p),
Expand Down
102 changes: 68 additions & 34 deletions internal/controller/argo.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,43 @@ import (

// Which ArgoCD objects we're creating
const (
ArgoCDGroup = "argoproj.io"
ArgoCDVersion = "v1beta1"
ArgoCDResource = "argocds"
ArgoCDGroup = "argoproj.io"
ArgoCDVersion = "v1beta1"
ArgoCDResource = "argocds"
ArgoCDKind = "ArgoCD"
ArgoCDAPIVersion = ArgoCDGroup + "/" + ArgoCDVersion

ApplicationKind = "Application"

DefaultProject = "default"
InClusterDestination = "in-cluster"
PatternRef = "patternref"
SyncOptionForce = "Force=true"
)

// Unstructured map keys
const (
FieldAPIVersion = "apiVersion"
FieldKind = "kind"
FieldMetadata = "metadata"
FieldName = "name"
)

// Volume/ConfigMap names for CA bundle handling
const (
KubeRootCACM = "kube-root-ca.crt"
CaBundlesVol = "ca-bundles"
)

// Helm parameter names
const (
ParamMultiSourceSupport = "global.multiSourceSupport"
ParamMultiSourceRepoUrl = "global.multiSourceRepoUrl"
ParamExperimentalCapabilities = "global.experimentalCapabilities"
ParamGitOpsSubNamespace = "global.gitOpsSubNamespace"
ParamVpArgoNamespace = "global.vpArgoNamespace"
ParamMultiSourceTargetRevision = "global.multiSourceTargetRevision"
ParamDeletePattern = "global.deletePattern"
)

// ConsoleLink constants
Expand All @@ -62,7 +96,7 @@ func newArgoCD(name, namespace string, patternsOperatorConfig PatternsOperatorCo
"g, cluster-admins, role:admin",
"g, admin, role:admin",
}
for argoAdmin := range strings.SplitSeq(patternsOperatorConfig.getStringValue("gitops.additionalArgoAdmins"), ",") {
for argoAdmin := range strings.SplitSeq(patternsOperatorConfig.getStringValue(configKeyAdditionalAdmins), ",") {
argoAdmin = strings.TrimSpace(argoAdmin)
if argoAdmin != "" {
argoPolicies = append(argoPolicies, "g, "+argoAdmin+", role:admin")
Expand Down Expand Up @@ -160,12 +194,12 @@ health_status.message = "An install plan for a subscription is pending installat
return health_status`,
},
}
if patternsOperatorConfig.getBoolValue("gitops.applicationHealthCheckEnabled") {
if patternsOperatorConfig.getBoolValue(configKeyHealthCheck) {
// As of ArgoCD 1.8 the Application health check was dropped (see https://github.com/argoproj/argo-cd/issues/3781),
// but in app-of-apps pattern this is needed in order to implement children apps dependencies via sync-waves
resourceHealthChecks = append(resourceHealthChecks, argooperator.ResourceHealthCheck{
Group: "argoproj.io",
Kind: "Application",
Group: ArgoCDGroup,
Kind: ApplicationKind,
Check: `local health_status = {}
health_status.status = "Progressing"
health_status.message = ""
Expand Down Expand Up @@ -199,32 +233,32 @@ return health_status`,
VolumeSource: v1.VolumeSource{
ConfigMap: &v1.ConfigMapVolumeSource{
LocalObjectReference: v1.LocalObjectReference{
Name: "kube-root-ca.crt",
Name: KubeRootCACM,
},
},
},
},
{
Name: "trusted-ca-bundle",
Name: trustedBundleCM,
VolumeSource: v1.VolumeSource{
ConfigMap: &v1.ConfigMapVolumeSource{
LocalObjectReference: v1.LocalObjectReference{
Name: "trusted-ca-bundle",
Name: trustedBundleCM,
},
Optional: &trueBool,
},
},
},
{
Name: "ca-bundles",
Name: CaBundlesVol,
VolumeSource: v1.VolumeSource{
EmptyDir: &v1.EmptyDirVolumeSource{},
},
},
}
initVolumeMounts := []v1.VolumeMount{
{
Name: "ca-bundles",
Name: CaBundlesVol,
MountPath: "/etc/pki/tls/certs",
},
}
Expand All @@ -239,11 +273,11 @@ return health_status`,
MountPath: "/var/run/kube-root-ca", // ca.crt field
},
{
Name: "trusted-ca-bundle",
Name: trustedBundleCM,
MountPath: "/var/run/trusted-ca", // ca-bundle.crt field
},
{
Name: "ca-bundles",
Name: CaBundlesVol,
MountPath: "/tmp/ca-bundles",
},
},
Expand All @@ -257,8 +291,8 @@ return health_status`,

s := argooperator.ArgoCD{
TypeMeta: metav1.TypeMeta{
Kind: "ArgoCD",
APIVersion: "argoproj.io/v1beta1",
Kind: ArgoCDKind,
APIVersion: ArgoCDAPIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Expand Down Expand Up @@ -509,10 +543,10 @@ func createOrUpdateConsoleLink(client dynamic.Interface, argoName, argoNamespace

consoleLinkObj := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": ConsoleLinkGroup + "/" + ConsoleLinkVersion,
"kind": "ConsoleLink",
"metadata": map[string]any{
"name": linkName,
FieldAPIVersion: ConsoleLinkGroup + "/" + ConsoleLinkVersion,
FieldKind: "ConsoleLink",
FieldMetadata: map[string]any{
FieldName: linkName,
},
"spec": map[string]any{
"applicationMenu": map[string]any{
Expand Down Expand Up @@ -614,28 +648,28 @@ func newApplicationParameters(p *api.Pattern) []argoapi.HelmParameter {
Value: strconv.FormatBool(p.Spec.GitConfig.TokenSecret != ""),
},
{
Name: "global.multiSourceSupport",
Name: ParamMultiSourceSupport,
Value: strconv.FormatBool(*p.Spec.MultiSourceConfig.Enabled),
},
{
Name: "global.multiSourceRepoUrl",
Name: ParamMultiSourceRepoUrl,
Value: p.Spec.MultiSourceConfig.HelmRepoUrl,
},

{
Name: "global.experimentalCapabilities",
Name: ParamExperimentalCapabilities,
Value: p.Spec.ExperimentalCapabilities,
},
}
_, gitOpsSubNamespace := DetectGitOpsSubscription()
parameters = append(parameters, argoapi.HelmParameter{
Name: "global.gitOpsSubNamespace",
Name: ParamGitOpsSubNamespace,
Value: gitOpsSubNamespace,
}, argoapi.HelmParameter{
Name: "global.vpArgoNamespace",
Name: ParamVpArgoNamespace,
Value: getClusterWideArgoNamespace(),
}, argoapi.HelmParameter{
Name: "global.multiSourceTargetRevision",
Name: ParamMultiSourceTargetRevision,
Value: getClusterGroupChartVersion(p),
})
for _, extra := range p.Spec.ExtraParameters {
Expand All @@ -662,7 +696,7 @@ func newApplicationParameters(p *api.Pattern) []argoapi.HelmParameter {
deletePatternValue = "DeleteChildApps"
}
parameters = append(parameters, argoapi.HelmParameter{
Name: "global.deletePattern",
Name: ParamDeletePattern,
Value: string(deletePatternValue),
ForceString: true,
})
Expand Down Expand Up @@ -807,12 +841,12 @@ func commonSyncPolicy(p *api.Pattern) *argoapi.SyncPolicy {
func commonApplicationSpec(p *api.Pattern, sources []argoapi.ApplicationSource) *argoapi.ApplicationSpec {
spec := &argoapi.ApplicationSpec{
Destination: argoapi.ApplicationDestination{
Name: "in-cluster",
Name: InClusterDestination,
Namespace: p.Namespace,
},
// Project is a reference to the project this application belongs to.
// The empty string means that application belongs to the 'default' project.
Project: "default",
Project: DefaultProject,

// IgnoreDifferences is a list of resources and their fields which should be ignored during comparison
// IgnoreDifferences []ResourceIgnoreDifferences `json:"ignoreDifferences,omitempty" protobuf:"bytes,5,name=ignoreDifferences"`
Expand Down Expand Up @@ -909,7 +943,7 @@ func newMultiSourceApplication(p *api.Pattern) *argoapi.Application {
valuesSource := &argoapi.ApplicationSource{
RepoURL: p.Spec.GitConfig.TargetRepo,
TargetRevision: p.Spec.GitConfig.TargetRevision,
Ref: "patternref",
Ref: PatternRef,
}
sources = append(sources, *valuesSource)

Expand Down Expand Up @@ -984,10 +1018,10 @@ func newArgoGiteaApplication(p *api.Pattern, patternsOperatorConfig PatternsOper
}
spec := &argoapi.ApplicationSpec{
Destination: argoapi.ApplicationDestination{
Name: "in-cluster",
Name: InClusterDestination,
Namespace: GiteaNamespace,
},
Project: "default",
Project: DefaultProject,
Source: &argoapi.ApplicationSource{
RepoURL: patternsOperatorConfig.getStringValue("gitea.helmRepoUrl"),
TargetRevision: patternsOperatorConfig.getStringValue("gitea.chartVersion"),
Expand Down Expand Up @@ -1296,14 +1330,14 @@ func updateHelmParameter(goal api.PatternParameter, actual []argoapi.HelmParamet
// syncApplication syncs the application with prune and force options if such a sync is not already in progress.
// Returns nil if a sync is already in progress, error otherwise
func syncApplication(client argoclient.Interface, app *argoapi.Application, withPrune bool) error {
if app.Operation != nil && app.Operation.Sync != nil && app.Operation.Sync.Prune == withPrune && slices.Contains(app.Operation.Sync.SyncOptions, "Force=true") {
if app.Operation != nil && app.Operation.Sync != nil && app.Operation.Sync.Prune == withPrune && slices.Contains(app.Operation.Sync.SyncOptions, SyncOptionForce) {
return nil
}

app.Operation = &argoapi.Operation{
Sync: &argoapi.SyncOperation{
Prune: withPrune,
SyncOptions: []string{"Force=true"},
SyncOptions: []string{SyncOptionForce},
},
}

Expand Down
Loading
Loading