From c71e83ffe619b8e74a411a226dc793f86c519dcb Mon Sep 17 00:00:00 2001 From: day0hero Date: Wed, 10 Dec 2025 21:35:59 +0000 Subject: [PATCH 1/2] Add global infrastructure values - infra.Status.ControlPlaneTopology is how we can programmatically determine the type of cluster we're on. - External=HyperShift - SingleReplica/HighlyAvailable-Traditional/Normal - infra.Status.APIServerURL is how we can programmatically determine the api address of the cluster. - In the past we have gotten away with this because we could use the clusterDomain but HyperShift uses the DNS name of the ELB it provisions for the hosted cluster. - In addition to these changes, added a sample pattern crd for testing the operator. --- cmd/main.go | 2 + config/manager/kustomization.yaml | 2 +- config/samples/test-pattern.yaml | 13 +++ hack/operator-build-deploy.sh | 96 ----------------------- internal/controller/argo.go | 43 ++++++---- internal/controller/argo_test.go | 16 ++-- internal/controller/pattern_controller.go | 24 +++++- 7 files changed, 75 insertions(+), 121 deletions(-) create mode 100644 config/samples/test-pattern.yaml delete mode 100755 hack/operator-build-deploy.sh diff --git a/cmd/main.go b/cmd/main.go index 8fd068306..9a73f6bf7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -43,6 +43,7 @@ import ( gitopsv1alpha1 "github.com/hybrid-cloud-patterns/patterns-operator/api/v1alpha1" controllers "github.com/hybrid-cloud-patterns/patterns-operator/internal/controller" "github.com/hybrid-cloud-patterns/patterns-operator/version" + configv1 "github.com/openshift/api/config/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" @@ -58,6 +59,7 @@ func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(gitopsv1alpha1.AddToScheme(scheme)) + utilruntime.Must(configv1.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme } diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 0b6b5a20c..8805f62aa 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -13,4 +13,4 @@ kind: Kustomization images: - name: controller newName: quay.io/validatedpatterns/patterns-operator - newTag: 0.0.63 + newTag: 0.0.64 diff --git a/config/samples/test-pattern.yaml b/config/samples/test-pattern.yaml new file mode 100644 index 000000000..5e752c0ab --- /dev/null +++ b/config/samples/test-pattern.yaml @@ -0,0 +1,13 @@ +apiVersion: gitops.hybrid-cloud-patterns.io/v1alpha1 +kind: Pattern +metadata: + name: test-pattern-infra-params + namespace: default +spec: + clusterGroupName: test-group + gitSpec: + targetRepo: "https://github.com/validatedpatterns/multicloud-gitops" + targetRevision: "main" + multiSourceConfig: + enabled: false + helmRepoUrl: "https://charts.validatedpatterns.io/" diff --git a/hack/operator-build-deploy.sh b/hack/operator-build-deploy.sh deleted file mode 100755 index 82fd83825..000000000 --- a/hack/operator-build-deploy.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/bin/bash -set -e -o pipefail - -CATALOGSOURCE="test-pattern-operator" -NS="openshift-operators" -OPERATOR="patterns-operator" -VERSION="${VERSION:-6.6.6}" -UPLOADREGISTRY="${UPLOADREGISTRY:-kuemper.int.rhx/bandini}" - -wait_for_resource() { - local resource_type=$1 # Either "packagemanifest", "operator", or "csv" - local name=$2 # Name of the resource (e.g., Operator or CSV) - local namespace=$3 # Namespace (optional, required for CSV and Operator) - local label=$4 # Label selector (only for packagemanifests) - - echo "⏳ Waiting for $resource_type: $name" - while true; do - set +e - if [[ "$resource_type" == "packagemanifest" ]]; then - oc get -n openshift-marketplace packagemanifests -l "catalog=${label}" --field-selector "metadata.name=${name}" &> /dev/null - elif [[ "$resource_type" == "operator" ]]; then - oc get operators.operators.coreos.com "${name}.${namespace}" &> /dev/null - elif [[ "$resource_type" == "csv" ]]; then - STATUS=$(oc get csv "$name" -n "$namespace" -o jsonpath='{.status.phase}' 2>/dev/null) - if [[ "$STATUS" == "Succeeded" ]]; then - echo "✅ Operator installation completed successfully!" - break - fi - echo "⏳ Operator installation in progress... (Current status: ${STATUS:-Not Found})" - else - echo "❌ Unknown resource type: $resource_type" - return 1 - fi - ret=$? - set -e - - if [[ $ret -eq 0 && "$resource_type" != "csv" ]]; then - echo "✅ $resource_type: $name is available!" - break - fi - - sleep 10 - done -} - -apply_subscription() { - oc delete -n ${NS} subscription/${OPERATOR} || /bin/true - oc delete catalogsource/${CATALOGSOURCE} || /bin/true - oc apply -f - <&1) -ret=$? -if [ $ret -ne 0 ]; then - echo "Could not reach cluster: ${OUT}" - exit 1 -fi - -make VERSION=${VERSION} UPLOADREGISTRY="${UPLOADREGISTRY}" CHANNELS=fast USE_IMAGE_DIGESTS="" \ - manifests bundle generate docker-build docker-push bundle-build bundle-push catalog-build \ - catalog-push catalog-install - -wait_for_resource "packagemanifest" "${OPERATOR}" "" "${CATALOGSOURCE}" -apply_subscription -wait_for_resource "operator" "${OPERATOR}" "${NS}" - -while true; do - set +e - INSTALLED_CSV=$(oc get subscriptions.operators.coreos.com "${OPERATOR}" -n "${NS}" -o jsonpath='{.status.installedCSV}') - if [ -z "${INSTALLED_CSV}" ]; then - sleep 10 - else - break - fi -done - -wait_for_resource "csv" "${INSTALLED_CSV}" "${NS}" - diff --git a/internal/controller/argo.go b/internal/controller/argo.go index a25fa0e20..848aa7511 100644 --- a/internal/controller/argo.go +++ b/internal/controller/argo.go @@ -24,6 +24,7 @@ import ( "strconv" "strings" + configv1 "github.com/openshift/api/config/v1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -347,7 +348,7 @@ func getArgoCD(client dynamic.Interface, name, namespace string) (*argooperator. return argo, err } -func newApplicationParameters(p *api.Pattern) []argoapi.HelmParameter { +func newApplicationParameters(p *api.Pattern, infra *configv1.Infrastructure) []argoapi.HelmParameter { parameters := []argoapi.HelmParameter{ { Name: "global.pattern", @@ -411,6 +412,20 @@ func newApplicationParameters(p *api.Pattern) []argoapi.HelmParameter { Value: p.Spec.ExperimentalCapabilities, }, } + if infra != nil { + log.Printf("Adding infrastructure parameters: clusterAPIServerURL=%s, controlPlaneTopology=%s", infra.Status.APIServerURL, infra.Status.ControlPlaneTopology) + parameters = append(parameters, argoapi.HelmParameter{ + Name: "global.clusterAPIServerURL", + Value: infra.Status.APIServerURL, + }, + argoapi.HelmParameter{ + Name: "global.controlPlaneTopology", + Value: string(infra.Status.ControlPlaneTopology), + }, + ) + } else { + log.Printf("Warning: infra is nil, skipping infrastructure parameters (global.clusterAPIServerURL and global.controlPlaneTopology)") + } parameters = append(parameters, argoapi.HelmParameter{ Name: "global.multiSourceTargetRevision", Value: getClusterGroupChartVersion(p), @@ -491,7 +506,7 @@ func newApplicationValues(p *api.Pattern) string { // libraries. E.g. a string '/overrides/values-{{ $.Values.global.clusterPlatform }}.yaml' // will be converted to '/overrides/values-AWS.yaml' // 4. We return the list of templated strings back as an array -func getSharedValueFiles(p *api.Pattern, prefix string) ([]string, error) { +func getSharedValueFiles(p *api.Pattern, prefix string, infra *configv1.Infrastructure) ([]string, error) { gitDir := p.Status.LocalCheckoutPath if _, err := os.Stat(gitDir); err != nil { return nil, fmt.Errorf("%s path does not exist", gitDir) @@ -521,7 +536,7 @@ func getSharedValueFiles(p *api.Pattern, prefix string) ([]string, error) { if !ok { return nil, fmt.Errorf("type assertion failed at index %d: Not a string", i) } - valueMap := convertArgoHelmParametersToMap(newApplicationParameters(p)) + valueMap := convertArgoHelmParametersToMap(newApplicationParameters(p, infra)) templatedString, err := helmTpl(str, valueFiles, valueMap) // we only log an error, but try to keep going @@ -595,9 +610,9 @@ func commonApplicationSpec(p *api.Pattern, sources []argoapi.ApplicationSource) return spec } -func commonApplicationSourceHelm(p *api.Pattern, prefix string) *argoapi.ApplicationSourceHelm { +func commonApplicationSourceHelm(p *api.Pattern, prefix string, infra *configv1.Infrastructure) *argoapi.ApplicationSourceHelm { valueFiles := newApplicationValueFiles(p, prefix) - sharedValueFiles, err := getSharedValueFiles(p, prefix) + sharedValueFiles, err := getSharedValueFiles(p, prefix, infra) if err != nil { fmt.Printf("Could not fetch sharedValueFiles: %s", err) } @@ -607,7 +622,7 @@ func commonApplicationSourceHelm(p *api.Pattern, prefix string) *argoapi.Applica ValueFiles: valueFiles, // Parameters is a list of Helm parameters which are passed to the helm template command upon manifest generation - Parameters: newApplicationParameters(p), + Parameters: newApplicationParameters(p, infra), // This is to be able to pass down the extraParams to the single applications Values: newApplicationValues(p), @@ -644,7 +659,7 @@ func newArgoOperatorApplication(p *api.Pattern, spec *argoapi.ApplicationSpec) * return &app } -func newSourceApplication(p *api.Pattern) *argoapi.Application { +func newSourceApplication(p *api.Pattern, infra *configv1.Infrastructure) *argoapi.Application { // Argo uses... // r := regexp.MustCompile("(/|:)") // root := filepath.Join(os.TempDir(), r.ReplaceAllString(NormalizeGitURL(rawRepoURL), "_")) @@ -653,7 +668,7 @@ func newSourceApplication(p *api.Pattern) *argoapi.Application { RepoURL: p.Spec.GitConfig.TargetRepo, Path: "common/clustergroup", TargetRevision: p.Spec.GitConfig.TargetRevision, - Helm: commonApplicationSourceHelm(p, ""), + Helm: commonApplicationSourceHelm(p, "", infra), } spec := commonApplicationSpec(p, []argoapi.ApplicationSource{source}) @@ -661,7 +676,7 @@ func newSourceApplication(p *api.Pattern) *argoapi.Application { return newArgoOperatorApplication(p, spec) } -func newMultiSourceApplication(p *api.Pattern) *argoapi.Application { +func newMultiSourceApplication(p *api.Pattern, infra *configv1.Infrastructure) *argoapi.Application { sources := []argoapi.ApplicationSource{} var baseSource *argoapi.ApplicationSource @@ -681,14 +696,14 @@ func newMultiSourceApplication(p *api.Pattern) *argoapi.Application { RepoURL: p.Spec.MultiSourceConfig.HelmRepoUrl, Chart: "clustergroup", TargetRevision: getClusterGroupChartVersion(p), - Helm: commonApplicationSourceHelm(p, "$patternref"), + Helm: commonApplicationSourceHelm(p, "$patternref", infra), } } else { baseSource = &argoapi.ApplicationSource{ RepoURL: p.Spec.MultiSourceConfig.ClusterGroupGitRepoUrl, Path: ".", TargetRevision: p.Spec.MultiSourceConfig.ClusterGroupChartGitRevision, - Helm: commonApplicationSourceHelm(p, "$patternref"), + Helm: commonApplicationSourceHelm(p, "$patternref", infra), } } sources = append(sources, *baseSource) @@ -712,14 +727,14 @@ func getClusterGroupChartVersion(p *api.Pattern) string { return clusterGroupChartVersion } -func newArgoApplication(p *api.Pattern) *argoapi.Application { +func newArgoApplication(p *api.Pattern, infra *configv1.Infrastructure) *argoapi.Application { // -- ArgoCD Application var targetApp *argoapi.Application if *p.Spec.MultiSourceConfig.Enabled { - targetApp = newMultiSourceApplication(p) + targetApp = newMultiSourceApplication(p, infra) } else { - targetApp = newSourceApplication(p) + targetApp = newSourceApplication(p, infra) } return targetApp diff --git a/internal/controller/argo_test.go b/internal/controller/argo_test.go index ff5a6089e..9e6d0e091 100644 --- a/internal/controller/argo_test.go +++ b/internal/controller/argo_test.go @@ -94,7 +94,7 @@ var _ = Describe("Argo Pattern", func() { TargetRevision: pattern.Spec.GitConfig.TargetRevision, Helm: &argoapi.ApplicationSourceHelm{ ValueFiles: newApplicationValueFiles(pattern, ""), - Parameters: newApplicationParameters(pattern), + Parameters: newApplicationParameters(pattern, nil), Values: newApplicationValues(pattern), IgnoreMissingValueFiles: true, }, @@ -137,7 +137,7 @@ var _ = Describe("Argo Pattern", func() { // This is needed to debug any failures as gomega truncates the diff output format.MaxDepth = 100 format.MaxLength = 0 - Expect(newArgoApplication(pattern)).To(Equal(argoApp)) + Expect(newArgoApplication(pattern, nil)).To(Equal(argoApp)) }) }) Context("Default multi source", func() { @@ -160,7 +160,7 @@ var _ = Describe("Argo Pattern", func() { *appSource, } multiSourceArgoApp.Spec.Sources[1].Helm.ValueFiles = newApplicationValueFiles(pattern, "$patternref") - Expect(newMultiSourceApplication(pattern)).To(Equal(multiSourceArgoApp)) + Expect(newMultiSourceApplication(pattern, nil)).To(Equal(multiSourceArgoApp)) }) }) Context("multiSource with MultiSourceClusterGroupChartGitRevision set", func() { @@ -184,7 +184,7 @@ var _ = Describe("Argo Pattern", func() { *appSource, } multiSourceArgoApp.Spec.Sources[1].Helm.ValueFiles = newApplicationValueFiles(pattern, "$patternref") - Expect(newMultiSourceApplication(pattern)).To(Equal(multiSourceArgoApp)) + Expect(newMultiSourceApplication(pattern, nil)).To(Equal(multiSourceArgoApp)) }) }) }) @@ -405,7 +405,7 @@ var _ = Describe("Argo Pattern", func() { } }) It("Test default newApplicationParameters", func() { - Expect(newApplicationParameters(pattern)).To(Equal(append(appParameters, + Expect(newApplicationParameters(pattern, nil)).To(Equal(append(appParameters, argoapi.HelmParameter{ Name: "global.multiSourceSupport", Value: "false", @@ -439,7 +439,7 @@ var _ = Describe("Argo Pattern", func() { Value: "test2value", }, } - Expect(newApplicationParameters(pattern)).To(Equal(append(appParameters, + Expect(newApplicationParameters(pattern, nil)).To(Equal(append(appParameters, argoapi.HelmParameter{ Name: "global.multiSourceSupport", Value: "false", @@ -472,7 +472,7 @@ var _ = Describe("Argo Pattern", func() { It("Test newApplicationParameters with multiSource", func() { tmpBool := true pattern.Spec.MultiSourceConfig.Enabled = &tmpBool - Expect(newApplicationParameters(pattern)).To(Equal(append(appParameters, + Expect(newApplicationParameters(pattern, nil)).To(Equal(append(appParameters, argoapi.HelmParameter{ Name: "global.multiSourceSupport", Value: "true", @@ -500,7 +500,7 @@ var _ = Describe("Argo Pattern", func() { var sources []argoapi.ApplicationSource BeforeEach(func() { - multiSourceArgoApp = newMultiSourceApplication(pattern) + multiSourceArgoApp = newMultiSourceApplication(pattern, nil) sources = multiSourceArgoApp.Spec.Sources }) It("compareSource() function identical", func() { diff --git a/internal/controller/pattern_controller.go b/internal/controller/pattern_controller.go index a7230d8b8..bcac96447 100644 --- a/internal/controller/pattern_controller.go +++ b/internal/controller/pattern_controller.go @@ -30,6 +30,7 @@ import ( kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" @@ -40,6 +41,7 @@ import ( argoapi "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" argoclient "github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned" + configv1 "github.com/openshift/api/config/v1" configclient "github.com/openshift/client-go/config/clientset/versioned" routeclient "github.com/openshift/client-go/route/clientset/versioned" olmclient "github.com/operator-framework/operator-lifecycle-manager/pkg/api/client/clientset/versioned" @@ -223,7 +225,17 @@ func (r *PatternReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct return r.actionPerformed(qualifiedInstance, ret, err) } - targetApp := newArgoApplication(qualifiedInstance) + // Fetch infrastructure for cluster API server URL and control plane topology + infra := &configv1.Infrastructure{} + if err := r.Get(ctx, types.NamespacedName{Name: "cluster"}, infra); err != nil { + // If infrastructure cannot be fetched, continue with nil (handled in newApplicationParameters) + log.Printf("Warning: Could not fetch infrastructure object: %v. Parameters global.clusterAPIServerURL and global.controlPlaneTopology will not be set.", err) + infra = nil + } else { + log.Printf("Successfully fetched infrastructure: APIServerURL=%s, ControlPlaneTopology=%s", infra.Status.APIServerURL, infra.Status.ControlPlaneTopology) + } + + targetApp := newArgoApplication(qualifiedInstance, infra) _ = controllerutil.SetOwnerReference(qualifiedInstance, targetApp, r.Scheme) app, err := getApplication(r.argoClient, applicationName(qualifiedInstance), clusterWideNS) if app == nil { @@ -510,7 +522,15 @@ func (r *PatternReconciler) finalizeObject(instance *api.Pattern) error { } ns := getClusterWideArgoNamespace() - targetApp := newArgoApplication(qualifiedInstance) + // Fetch infrastructure for cluster API server URL and control plane topology + infra := &configv1.Infrastructure{} + if err := r.Get(context.TODO(), types.NamespacedName{Name: "cluster"}, infra); err != nil { + // If infrastructure cannot be fetched, continue with nil (handled in newApplicationParameters) + log.Printf("Warning: Could not fetch infrastructure object during finalization: %v. Parameters global.clusterAPIServerURL and global.controlPlaneTopology will not be set.", err) + infra = nil + } + + targetApp := newArgoApplication(qualifiedInstance, infra) _ = controllerutil.SetOwnerReference(qualifiedInstance, targetApp, r.Scheme) app, _ := getApplication(r.argoClient, applicationName(qualifiedInstance), ns) From 47f598d057ff8becd692851e9f978a510151990a Mon Sep 17 00:00:00 2001 From: day0hero Date: Thu, 11 Dec 2025 07:50:05 +0000 Subject: [PATCH 2/2] putting script back --- hack/operator-build-deploy.sh | 96 +++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100755 hack/operator-build-deploy.sh diff --git a/hack/operator-build-deploy.sh b/hack/operator-build-deploy.sh new file mode 100755 index 000000000..82fd83825 --- /dev/null +++ b/hack/operator-build-deploy.sh @@ -0,0 +1,96 @@ +#!/bin/bash +set -e -o pipefail + +CATALOGSOURCE="test-pattern-operator" +NS="openshift-operators" +OPERATOR="patterns-operator" +VERSION="${VERSION:-6.6.6}" +UPLOADREGISTRY="${UPLOADREGISTRY:-kuemper.int.rhx/bandini}" + +wait_for_resource() { + local resource_type=$1 # Either "packagemanifest", "operator", or "csv" + local name=$2 # Name of the resource (e.g., Operator or CSV) + local namespace=$3 # Namespace (optional, required for CSV and Operator) + local label=$4 # Label selector (only for packagemanifests) + + echo "⏳ Waiting for $resource_type: $name" + while true; do + set +e + if [[ "$resource_type" == "packagemanifest" ]]; then + oc get -n openshift-marketplace packagemanifests -l "catalog=${label}" --field-selector "metadata.name=${name}" &> /dev/null + elif [[ "$resource_type" == "operator" ]]; then + oc get operators.operators.coreos.com "${name}.${namespace}" &> /dev/null + elif [[ "$resource_type" == "csv" ]]; then + STATUS=$(oc get csv "$name" -n "$namespace" -o jsonpath='{.status.phase}' 2>/dev/null) + if [[ "$STATUS" == "Succeeded" ]]; then + echo "✅ Operator installation completed successfully!" + break + fi + echo "⏳ Operator installation in progress... (Current status: ${STATUS:-Not Found})" + else + echo "❌ Unknown resource type: $resource_type" + return 1 + fi + ret=$? + set -e + + if [[ $ret -eq 0 && "$resource_type" != "csv" ]]; then + echo "✅ $resource_type: $name is available!" + break + fi + + sleep 10 + done +} + +apply_subscription() { + oc delete -n ${NS} subscription/${OPERATOR} || /bin/true + oc delete catalogsource/${CATALOGSOURCE} || /bin/true + oc apply -f - <&1) +ret=$? +if [ $ret -ne 0 ]; then + echo "Could not reach cluster: ${OUT}" + exit 1 +fi + +make VERSION=${VERSION} UPLOADREGISTRY="${UPLOADREGISTRY}" CHANNELS=fast USE_IMAGE_DIGESTS="" \ + manifests bundle generate docker-build docker-push bundle-build bundle-push catalog-build \ + catalog-push catalog-install + +wait_for_resource "packagemanifest" "${OPERATOR}" "" "${CATALOGSOURCE}" +apply_subscription +wait_for_resource "operator" "${OPERATOR}" "${NS}" + +while true; do + set +e + INSTALLED_CSV=$(oc get subscriptions.operators.coreos.com "${OPERATOR}" -n "${NS}" -o jsonpath='{.status.installedCSV}') + if [ -z "${INSTALLED_CSV}" ]; then + sleep 10 + else + break + fi +done + +wait_for_resource "csv" "${INSTALLED_CSV}" "${NS}" +