diff --git a/bundle/manifests/argoproj.io_argocds.yaml b/bundle/manifests/argoproj.io_argocds.yaml index 4980e27b7e9..da1cf638ff0 100644 --- a/bundle/manifests/argoproj.io_argocds.yaml +++ b/bundle/manifests/argoproj.io_argocds.yaml @@ -699,6 +699,21 @@ spec: description: LogLevel refers to the log level used by the Agent component. type: string + metrics: + description: Metrics defines the metrics configuration for + the Agent ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object redis: description: Redis defines the Redis options for the Agent component. @@ -922,6 +937,21 @@ spec: description: LogLevel refers to the log level used by the Principal component. type: string + metrics: + description: Metrics defines the metrics configuration for + the Principal ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object namespace: description: Namespace is the configuration for the Principal component namespace. @@ -1242,6 +1272,21 @@ spec: Controller component. Defaults to ArgoCDDefaultLogLevel if not configured. Valid options are debug, info, error, and warn. type: string + metrics: + description: Metrics defines the metrics configuration for the + Application Controller ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object parallelismLimit: description: ParallelismLimit defines the limit for parallel kubectl operations @@ -1325,6 +1370,15 @@ spec: description: Sharding contains the options for the Application Controller sharding configuration. properties: + algorithm: + description: DistributionAlgorithm determines what algorithm + will be used for distribution of shards. Valid options are + legacy, round-robin, and consistent-hashing + enum: + - legacy + - round-robin + - consistent-hashing + type: string clustersPerShard: description: ClustersPerShard defines the maximum number of clusters managed by each argocd shard @@ -1905,7 +1959,6 @@ spec: NetworkPolicy resources for this Argo CD instance. properties: enabled: - default: true description: |- Enabled defines whether NetworkPolicy resources should be created for this Argo CD instance. When enabled, the operator will reconcile NetworkPolicies for Argo CD components. @@ -2133,12 +2186,7 @@ spec: image: description: Image is the Argo CD Notifications image (optional) type: string - logLevel: - description: LogLevel describes the log level that should be used - by the argocd-notifications. Defaults to ArgoCDDefaultLogLevel - if not set. Valid options are debug,info, error, and warn. - type: string - logformat: + logFormat: description: LogFormat refers to the log format used by the argocd-notifications. Defaults to ArgoCDDefaultLogFormat if not configured. Valid options are text or json. @@ -2146,6 +2194,29 @@ spec: - text - json type: string + logLevel: + description: LogLevel describes the log level that should be used + by the argocd-notifications. Defaults to ArgoCDDefaultLogLevel + if not set. Valid options are debug,info, error, and warn. + type: string + logformat: + description: 'Deprecated: use LogFormat instead.' + type: string + metrics: + description: Metrics defines the metrics configuration for the + Notifications ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object replicas: description: Replicas defines the number of replicas to run for notifications-controller @@ -4247,6 +4318,21 @@ spec: by the Repo Server. Defaults to ArgoCDDefaultLogLevel if not set. Valid options are debug, info, error, and warn. type: string + metrics: + description: Metrics defines the metrics configuration for the + Repo Server ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object mountsatoken: description: MountSAToken describes whether you would like to have the Repo server mount the service account token @@ -8278,6 +8364,21 @@ spec: ArgoCD Server component. Defaults to ArgoCDDefaultLogLevel if not set. Valid options are debug, info, error, and warn. type: string + metrics: + description: Metrics defines the metrics configuration for the + Server ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object replicas: description: Replicas defines the number of replicas for argocd-server. Default is nil. Value should be greater than or equal to 0. @@ -8755,6 +8856,145 @@ spec: description: Version is the tag to use with the ArgoCD container image for all ArgoCD components. type: string + webhookSecrets: + description: |- + WebhookSecrets references Kubernetes Secrets that supply webhook credentials per provider. + The operator syncs values into argocd-secret under the keys Argo CD expects. + properties: + azureDevOps: + description: 'AzureDevOps: Secret key references for the Azure + DevOps webhook username and password (or PAT).' + properties: + passwordSecretRef: + description: PasswordSecretRef points to the key holding the + password or PAT. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + usernameSecretRef: + description: UsernameSecretRef points to the key holding the + username. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + x-kubernetes-validations: + - message: usernameSecretRef and passwordSecretRef must be set + together + rule: (has(self.usernameSecretRef) && has(self.passwordSecretRef)) + || (!has(self.usernameSecretRef) && !has(self.passwordSecretRef)) + bitbucket: + description: 'Bitbucket: Secret key reference for the Bitbucket + Cloud webhook UUID.' + properties: + webhookUUIDSecretRef: + description: WebhookUUIDSecretRef points to the key holding + the Bitbucket Cloud webhook UUID. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + bitbucketServer: + description: 'BitbucketServer: Secret key reference for the Bitbucket + Server webhook secret.' + properties: + webhookSecretRef: + description: WebhookSecretRef points to the key holding the + Bitbucket Server webhook shared secret. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + github: + description: 'GitHub: Secret key reference for the GitHub webhook + shared secret.' + properties: + webhookSecretRef: + description: WebhookSecretRef points to the key holding the + GitHub webhook shared secret. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + gitlab: + description: 'GitLab: Secret key reference for the GitLab webhook + shared secret.' + properties: + webhookSecretRef: + description: WebhookSecretRef points to the key holding the + GitLab webhook shared secret. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + gogs: + description: 'Gogs: Secret key reference for the Gogs webhook + shared secret.' + properties: + webhookSecretRef: + description: WebhookSecretRef points to the key holding the + Gogs webhook shared secret. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + type: object type: object status: description: ArgoCDStatus defines the observed state of ArgoCD @@ -9129,12 +9369,7 @@ spec: type: string description: Custom labels to pods deployed by the operator type: object - logLevel: - description: LogLevel describes the log level that should be used - by the ApplicationSet controller. Defaults to ArgoCDDefaultLogLevel - if not set. Valid options are debug,info, error, and warn. - type: string - logformat: + logFormat: description: LogFormat refers to the log format used by the ApplicationSet component. Defaults to ArgoCDDefaultLogFormat if not configured. Valid options are text or json. @@ -9142,6 +9377,14 @@ spec: - text - json type: string + logLevel: + description: LogLevel describes the log level that should be used + by the ApplicationSet controller. Defaults to ArgoCDDefaultLogLevel + if not set. Valid options are debug,info, error, and warn. + type: string + logformat: + description: 'Deprecated: use LogFormat instead.' + type: string resources: description: Resources defines the Compute Resources required by the container for ApplicationSet. @@ -11586,6 +11829,21 @@ spec: description: LogLevel refers to the log level used by the Agent component. type: string + metrics: + description: Metrics defines the metrics configuration for + the Agent ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object redis: description: Redis defines the Redis options for the Agent component. @@ -11595,6 +11853,66 @@ spec: server to be used by the PrincAgentipal component. type: string type: object + resources: + description: Resources defines the Compute Resources required + by the container for the Argo CD Agent agent component. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object tls: description: TLS defines the TLS options for the Agent component. properties: @@ -11813,6 +12131,21 @@ spec: description: LogLevel refers to the log level used by the Principal component. type: string + metrics: + description: Metrics defines the metrics configuration for + the Principal ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object namespace: description: Namespace is the configuration for the Principal component namespace. @@ -11865,6 +12198,66 @@ spec: the TLS certificate and key for the resource proxy. type: string type: object + resources: + description: Resources defines the Compute Resources required + by the container for the Argo CD Agent principal component. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object server: description: Server defines the server options for the Principal component. @@ -11945,11 +12338,20 @@ spec: required: - content type: object + clusterDomain: + description: |- + ClusterDomain is the cluster domain suffix used for constructing service FQDNs. Defaults to "cluster.local". + The full FQDN will be: ..svc. + This is useful for clusters that use a different DNS suffix (e.g., "CLUSTER_ID.cluster.local", "edge.local"). + type: string cmdParams: additionalProperties: type: string - description: CmdParams specifies command-line parameters for the Argo - CD components. + description: |- + CmdParams specifies command-line parameters for the Argo CD components. + The only keys currently supported for this parameter are: + - controller.resource.health.persist + - applicationsetcontroller.enable.tokenref.strict.mode — when ApplicationSet-in-any-namespace is active, the operator defaults this to "true" type: object configManagementPlugins: description: 'Deprecated: ConfigManagementPlugins field is no longer @@ -13692,6 +14094,21 @@ spec: Controller component. Defaults to ArgoCDDefaultLogLevel if not configured. Valid options are debug, info, error, and warn. type: string + metrics: + description: Metrics defines the metrics configuration for the + Application Controller ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object parallelismLimit: description: ParallelismLimit defines the limit for parallel kubectl operations @@ -13780,6 +14197,15 @@ spec: description: Sharding contains the options for the Application Controller sharding configuration. properties: + algorithm: + description: DistributionAlgorithm determines what algorithm + will be used for distribution of shards. Valid options are + legacy, round-robin, and consistent-hashing + enum: + - legacy + - round-robin + - consistent-hashing + type: string clustersPerShard: description: ClustersPerShard defines the maximum number of clusters managed by each argocd shard @@ -18031,7 +18457,6 @@ spec: NetworkPolicy resources for this Argo CD instance. properties: enabled: - default: true description: |- Enabled defines whether NetworkPolicy resources are created for this Argo CD instance. When enabled, the operator will reconcile NetworkPolicies for Argo CD components. @@ -18259,12 +18684,7 @@ spec: image: description: Image is the Argo CD Notifications image (optional) type: string - logLevel: - description: LogLevel describes the log level that should be used - by the argocd-notifications. Defaults to ArgoCDDefaultLogLevel - if not set. Valid options are debug,info, error, and warn. - type: string - logformat: + logFormat: description: LogFormat refers to the log format used by the argocd-notifications. Defaults to ArgoCDDefaultLogFormat if not configured. Valid options are text or json. @@ -18272,6 +18692,29 @@ spec: - text - json type: string + logLevel: + description: LogLevel describes the log level that should be used + by the argocd-notifications. Defaults to ArgoCDDefaultLogLevel + if not set. Valid options are debug,info, error, and warn. + type: string + logformat: + description: 'Deprecated: use LogFormat instead.' + type: string + metrics: + description: Metrics defines the metrics configuration for the + Notifications ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object replicas: description: Replicas defines the number of replicas to run for notifications-controller @@ -20396,6 +20839,21 @@ spec: by the Repo Server. Defaults to ArgoCDDefaultLogLevel if not set. Valid options are debug, info, error, and warn. type: string + metrics: + description: Metrics defines the metrics configuration for the + Repo Server ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object mountsatoken: description: MountSAToken describes whether you would like to have the Repo server mount the service account token @@ -26203,6 +26661,21 @@ spec: ArgoCD Server component. Defaults to ArgoCDDefaultLogLevel if not set. Valid options are debug, info, error, and warn. type: string + metrics: + description: Metrics defines the metrics configuration for the + Server ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object replicas: description: Replicas defines the number of replicas for argocd-server. Default is nil. Value should be greater than or equal to 0. @@ -32216,6 +32689,149 @@ spec: description: Version is the tag to use with the ArgoCD container image for all ArgoCD components. type: string + webTerminalEnabled: + description: WebTerminalEnabled allows you to get a shell inside a + running pod just like you would with kubectl exec + type: boolean + webhookSecrets: + description: |- + WebhookSecrets references Kubernetes Secrets that supply webhook credentials per provider. + The operator syncs values into argocd-secret under the keys Argo CD expects. + properties: + azureDevOps: + description: 'AzureDevOps: Secret key references for the Azure + DevOps webhook username and password (or PAT).' + properties: + passwordSecretRef: + description: PasswordSecretRef points to the key holding the + password or PAT. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + usernameSecretRef: + description: UsernameSecretRef points to the key holding the + username. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + x-kubernetes-validations: + - message: usernameSecretRef and passwordSecretRef must be set + together + rule: (has(self.usernameSecretRef) && has(self.passwordSecretRef)) + || (!has(self.usernameSecretRef) && !has(self.passwordSecretRef)) + bitbucket: + description: 'Bitbucket: Secret key reference for the Bitbucket + Cloud webhook UUID.' + properties: + webhookUUIDSecretRef: + description: WebhookUUIDSecretRef points to the key holding + the Bitbucket Cloud webhook UUID. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + bitbucketServer: + description: 'BitbucketServer: Secret key reference for the Bitbucket + Server webhook secret.' + properties: + webhookSecretRef: + description: WebhookSecretRef points to the key holding the + Bitbucket Server webhook shared secret. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + github: + description: 'GitHub: Secret key reference for the GitHub webhook + shared secret.' + properties: + webhookSecretRef: + description: WebhookSecretRef points to the key holding the + GitHub webhook shared secret. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + gitlab: + description: 'GitLab: Secret key reference for the GitLab webhook + shared secret.' + properties: + webhookSecretRef: + description: WebhookSecretRef points to the key holding the + GitLab webhook shared secret. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + gogs: + description: 'Gogs: Secret key reference for the Gogs webhook + shared secret.' + properties: + webhookSecretRef: + description: WebhookSecretRef points to the key holding the + Gogs webhook shared secret. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + type: object type: object x-kubernetes-validations: - message: spec.sso and spec.oidcConfig cannot both be set diff --git a/bundle/manifests/gitops-operator.clusterserviceversion.yaml b/bundle/manifests/gitops-operator.clusterserviceversion.yaml index 429e28f0275..143532cabd4 100644 --- a/bundle/manifests/gitops-operator.clusterserviceversion.yaml +++ b/bundle/manifests/gitops-operator.clusterserviceversion.yaml @@ -190,7 +190,7 @@ metadata: capabilities: Deep Insights console.openshift.io/plugins: '["gitops-plugin"]' containerImage: quay.io/redhat-developer/gitops-operator - createdAt: "2026-06-11T15:05:37Z" + createdAt: "2026-06-16T17:25:03Z" description: Enables teams to adopt GitOps principles for managing cluster configurations and application delivery across hybrid multi-cluster Kubernetes environments. features.operators.openshift.io/disconnected: "true" diff --git a/config/crd/bases/argoproj.io_argocds.yaml b/config/crd/bases/argoproj.io_argocds.yaml index 56185f64d61..09c6829ba7d 100644 --- a/config/crd/bases/argoproj.io_argocds.yaml +++ b/config/crd/bases/argoproj.io_argocds.yaml @@ -688,6 +688,21 @@ spec: description: LogLevel refers to the log level used by the Agent component. type: string + metrics: + description: Metrics defines the metrics configuration for + the Agent ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object redis: description: Redis defines the Redis options for the Agent component. @@ -911,6 +926,21 @@ spec: description: LogLevel refers to the log level used by the Principal component. type: string + metrics: + description: Metrics defines the metrics configuration for + the Principal ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object namespace: description: Namespace is the configuration for the Principal component namespace. @@ -1231,6 +1261,21 @@ spec: Controller component. Defaults to ArgoCDDefaultLogLevel if not configured. Valid options are debug, info, error, and warn. type: string + metrics: + description: Metrics defines the metrics configuration for the + Application Controller ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object parallelismLimit: description: ParallelismLimit defines the limit for parallel kubectl operations @@ -1314,6 +1359,15 @@ spec: description: Sharding contains the options for the Application Controller sharding configuration. properties: + algorithm: + description: DistributionAlgorithm determines what algorithm + will be used for distribution of shards. Valid options are + legacy, round-robin, and consistent-hashing + enum: + - legacy + - round-robin + - consistent-hashing + type: string clustersPerShard: description: ClustersPerShard defines the maximum number of clusters managed by each argocd shard @@ -1894,7 +1948,6 @@ spec: NetworkPolicy resources for this Argo CD instance. properties: enabled: - default: true description: |- Enabled defines whether NetworkPolicy resources should be created for this Argo CD instance. When enabled, the operator will reconcile NetworkPolicies for Argo CD components. @@ -2122,12 +2175,7 @@ spec: image: description: Image is the Argo CD Notifications image (optional) type: string - logLevel: - description: LogLevel describes the log level that should be used - by the argocd-notifications. Defaults to ArgoCDDefaultLogLevel - if not set. Valid options are debug,info, error, and warn. - type: string - logformat: + logFormat: description: LogFormat refers to the log format used by the argocd-notifications. Defaults to ArgoCDDefaultLogFormat if not configured. Valid options are text or json. @@ -2135,6 +2183,29 @@ spec: - text - json type: string + logLevel: + description: LogLevel describes the log level that should be used + by the argocd-notifications. Defaults to ArgoCDDefaultLogLevel + if not set. Valid options are debug,info, error, and warn. + type: string + logformat: + description: 'Deprecated: use LogFormat instead.' + type: string + metrics: + description: Metrics defines the metrics configuration for the + Notifications ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object replicas: description: Replicas defines the number of replicas to run for notifications-controller @@ -4236,6 +4307,21 @@ spec: by the Repo Server. Defaults to ArgoCDDefaultLogLevel if not set. Valid options are debug, info, error, and warn. type: string + metrics: + description: Metrics defines the metrics configuration for the + Repo Server ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object mountsatoken: description: MountSAToken describes whether you would like to have the Repo server mount the service account token @@ -8267,6 +8353,21 @@ spec: ArgoCD Server component. Defaults to ArgoCDDefaultLogLevel if not set. Valid options are debug, info, error, and warn. type: string + metrics: + description: Metrics defines the metrics configuration for the + Server ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object replicas: description: Replicas defines the number of replicas for argocd-server. Default is nil. Value should be greater than or equal to 0. @@ -8744,6 +8845,145 @@ spec: description: Version is the tag to use with the ArgoCD container image for all ArgoCD components. type: string + webhookSecrets: + description: |- + WebhookSecrets references Kubernetes Secrets that supply webhook credentials per provider. + The operator syncs values into argocd-secret under the keys Argo CD expects. + properties: + azureDevOps: + description: 'AzureDevOps: Secret key references for the Azure + DevOps webhook username and password (or PAT).' + properties: + passwordSecretRef: + description: PasswordSecretRef points to the key holding the + password or PAT. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + usernameSecretRef: + description: UsernameSecretRef points to the key holding the + username. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + x-kubernetes-validations: + - message: usernameSecretRef and passwordSecretRef must be set + together + rule: (has(self.usernameSecretRef) && has(self.passwordSecretRef)) + || (!has(self.usernameSecretRef) && !has(self.passwordSecretRef)) + bitbucket: + description: 'Bitbucket: Secret key reference for the Bitbucket + Cloud webhook UUID.' + properties: + webhookUUIDSecretRef: + description: WebhookUUIDSecretRef points to the key holding + the Bitbucket Cloud webhook UUID. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + bitbucketServer: + description: 'BitbucketServer: Secret key reference for the Bitbucket + Server webhook secret.' + properties: + webhookSecretRef: + description: WebhookSecretRef points to the key holding the + Bitbucket Server webhook shared secret. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + github: + description: 'GitHub: Secret key reference for the GitHub webhook + shared secret.' + properties: + webhookSecretRef: + description: WebhookSecretRef points to the key holding the + GitHub webhook shared secret. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + gitlab: + description: 'GitLab: Secret key reference for the GitLab webhook + shared secret.' + properties: + webhookSecretRef: + description: WebhookSecretRef points to the key holding the + GitLab webhook shared secret. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + gogs: + description: 'Gogs: Secret key reference for the Gogs webhook + shared secret.' + properties: + webhookSecretRef: + description: WebhookSecretRef points to the key holding the + Gogs webhook shared secret. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + type: object type: object status: description: ArgoCDStatus defines the observed state of ArgoCD @@ -9118,12 +9358,7 @@ spec: type: string description: Custom labels to pods deployed by the operator type: object - logLevel: - description: LogLevel describes the log level that should be used - by the ApplicationSet controller. Defaults to ArgoCDDefaultLogLevel - if not set. Valid options are debug,info, error, and warn. - type: string - logformat: + logFormat: description: LogFormat refers to the log format used by the ApplicationSet component. Defaults to ArgoCDDefaultLogFormat if not configured. Valid options are text or json. @@ -9131,6 +9366,14 @@ spec: - text - json type: string + logLevel: + description: LogLevel describes the log level that should be used + by the ApplicationSet controller. Defaults to ArgoCDDefaultLogLevel + if not set. Valid options are debug,info, error, and warn. + type: string + logformat: + description: 'Deprecated: use LogFormat instead.' + type: string resources: description: Resources defines the Compute Resources required by the container for ApplicationSet. @@ -11575,6 +11818,21 @@ spec: description: LogLevel refers to the log level used by the Agent component. type: string + metrics: + description: Metrics defines the metrics configuration for + the Agent ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object redis: description: Redis defines the Redis options for the Agent component. @@ -11584,6 +11842,66 @@ spec: server to be used by the PrincAgentipal component. type: string type: object + resources: + description: Resources defines the Compute Resources required + by the container for the Argo CD Agent agent component. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object tls: description: TLS defines the TLS options for the Agent component. properties: @@ -11802,6 +12120,21 @@ spec: description: LogLevel refers to the log level used by the Principal component. type: string + metrics: + description: Metrics defines the metrics configuration for + the Principal ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object namespace: description: Namespace is the configuration for the Principal component namespace. @@ -11854,6 +12187,66 @@ spec: the TLS certificate and key for the resource proxy. type: string type: object + resources: + description: Resources defines the Compute Resources required + by the container for the Argo CD Agent principal component. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object server: description: Server defines the server options for the Principal component. @@ -11934,11 +12327,20 @@ spec: required: - content type: object + clusterDomain: + description: |- + ClusterDomain is the cluster domain suffix used for constructing service FQDNs. Defaults to "cluster.local". + The full FQDN will be: ..svc. + This is useful for clusters that use a different DNS suffix (e.g., "CLUSTER_ID.cluster.local", "edge.local"). + type: string cmdParams: additionalProperties: type: string - description: CmdParams specifies command-line parameters for the Argo - CD components. + description: |- + CmdParams specifies command-line parameters for the Argo CD components. + The only keys currently supported for this parameter are: + - controller.resource.health.persist + - applicationsetcontroller.enable.tokenref.strict.mode — when ApplicationSet-in-any-namespace is active, the operator defaults this to "true" type: object configManagementPlugins: description: 'Deprecated: ConfigManagementPlugins field is no longer @@ -13681,6 +14083,21 @@ spec: Controller component. Defaults to ArgoCDDefaultLogLevel if not configured. Valid options are debug, info, error, and warn. type: string + metrics: + description: Metrics defines the metrics configuration for the + Application Controller ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object parallelismLimit: description: ParallelismLimit defines the limit for parallel kubectl operations @@ -13769,6 +14186,15 @@ spec: description: Sharding contains the options for the Application Controller sharding configuration. properties: + algorithm: + description: DistributionAlgorithm determines what algorithm + will be used for distribution of shards. Valid options are + legacy, round-robin, and consistent-hashing + enum: + - legacy + - round-robin + - consistent-hashing + type: string clustersPerShard: description: ClustersPerShard defines the maximum number of clusters managed by each argocd shard @@ -18020,7 +18446,6 @@ spec: NetworkPolicy resources for this Argo CD instance. properties: enabled: - default: true description: |- Enabled defines whether NetworkPolicy resources are created for this Argo CD instance. When enabled, the operator will reconcile NetworkPolicies for Argo CD components. @@ -18248,12 +18673,7 @@ spec: image: description: Image is the Argo CD Notifications image (optional) type: string - logLevel: - description: LogLevel describes the log level that should be used - by the argocd-notifications. Defaults to ArgoCDDefaultLogLevel - if not set. Valid options are debug,info, error, and warn. - type: string - logformat: + logFormat: description: LogFormat refers to the log format used by the argocd-notifications. Defaults to ArgoCDDefaultLogFormat if not configured. Valid options are text or json. @@ -18261,6 +18681,29 @@ spec: - text - json type: string + logLevel: + description: LogLevel describes the log level that should be used + by the argocd-notifications. Defaults to ArgoCDDefaultLogLevel + if not set. Valid options are debug,info, error, and warn. + type: string + logformat: + description: 'Deprecated: use LogFormat instead.' + type: string + metrics: + description: Metrics defines the metrics configuration for the + Notifications ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object replicas: description: Replicas defines the number of replicas to run for notifications-controller @@ -20385,6 +20828,21 @@ spec: by the Repo Server. Defaults to ArgoCDDefaultLogLevel if not set. Valid options are debug, info, error, and warn. type: string + metrics: + description: Metrics defines the metrics configuration for the + Repo Server ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object mountsatoken: description: MountSAToken describes whether you would like to have the Repo server mount the service account token @@ -26192,6 +26650,21 @@ spec: ArgoCD Server component. Defaults to ArgoCDDefaultLogLevel if not set. Valid options are debug, info, error, and warn. type: string + metrics: + description: Metrics defines the metrics configuration for the + Server ServiceMonitor. + properties: + interval: + description: |- + Interval specifies the Prometheus scrape interval for this component's ServiceMonitor. + If empty, Prometheus uses its default scrape interval. + type: string + scrapeTimeout: + description: |- + ScrapeTimeout specifies the Prometheus scrape timeout for this component's ServiceMonitor. + If empty, Prometheus uses the global scrape timeout. + type: string + type: object replicas: description: Replicas defines the number of replicas for argocd-server. Default is nil. Value should be greater than or equal to 0. @@ -32205,6 +32678,149 @@ spec: description: Version is the tag to use with the ArgoCD container image for all ArgoCD components. type: string + webTerminalEnabled: + description: WebTerminalEnabled allows you to get a shell inside a + running pod just like you would with kubectl exec + type: boolean + webhookSecrets: + description: |- + WebhookSecrets references Kubernetes Secrets that supply webhook credentials per provider. + The operator syncs values into argocd-secret under the keys Argo CD expects. + properties: + azureDevOps: + description: 'AzureDevOps: Secret key references for the Azure + DevOps webhook username and password (or PAT).' + properties: + passwordSecretRef: + description: PasswordSecretRef points to the key holding the + password or PAT. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + usernameSecretRef: + description: UsernameSecretRef points to the key holding the + username. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + x-kubernetes-validations: + - message: usernameSecretRef and passwordSecretRef must be set + together + rule: (has(self.usernameSecretRef) && has(self.passwordSecretRef)) + || (!has(self.usernameSecretRef) && !has(self.passwordSecretRef)) + bitbucket: + description: 'Bitbucket: Secret key reference for the Bitbucket + Cloud webhook UUID.' + properties: + webhookUUIDSecretRef: + description: WebhookUUIDSecretRef points to the key holding + the Bitbucket Cloud webhook UUID. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + bitbucketServer: + description: 'BitbucketServer: Secret key reference for the Bitbucket + Server webhook secret.' + properties: + webhookSecretRef: + description: WebhookSecretRef points to the key holding the + Bitbucket Server webhook shared secret. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + github: + description: 'GitHub: Secret key reference for the GitHub webhook + shared secret.' + properties: + webhookSecretRef: + description: WebhookSecretRef points to the key holding the + GitHub webhook shared secret. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + gitlab: + description: 'GitLab: Secret key reference for the GitLab webhook + shared secret.' + properties: + webhookSecretRef: + description: WebhookSecretRef points to the key holding the + GitLab webhook shared secret. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + gogs: + description: 'Gogs: Secret key reference for the Gogs webhook + shared secret.' + properties: + webhookSecretRef: + description: WebhookSecretRef points to the key holding the + Gogs webhook shared secret. + properties: + key: + description: Key in the Secret whose value should be used. + type: string + name: + description: Name of the Secret. + type: string + required: + - key + - name + type: object + type: object + type: object type: object x-kubernetes-validations: - message: spec.sso and spec.oidcConfig cannot both be set diff --git a/test/openshift/e2e/ginkgo/parallel/1-043_validate_log_level_format_test.go b/test/openshift/e2e/ginkgo/parallel/1-043_validate_log_level_format_test.go index f5505748dc1..ac9b509a59a 100644 --- a/test/openshift/e2e/ginkgo/parallel/1-043_validate_log_level_format_test.go +++ b/test/openshift/e2e/ginkgo/parallel/1-043_validate_log_level_format_test.go @@ -129,5 +129,64 @@ var _ = Describe("GitOps Operator Parallel E2E Tests", func() { }).Should(BeTrue()) }) + + It("verifies that the deprecated spec.logformat field is honoured for applicationSet and notifications", func() { + By("creating a fresh test namespace") + ns, cleanupFunc = fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + + By("creating ArgoCD CR using the deprecated lowercase logformat field on applicationSet and notifications") + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "argocd", Namespace: ns.Name}, + Spec: argov1beta1api.ArgoCDSpec{ + Server: argov1beta1api.ArgoCDServerSpec{ + Route: argov1beta1api.ArgoCDRouteSpec{Enabled: true}, + }, + ApplicationSet: &argov1beta1api.ArgoCDApplicationSet{ + //nolint:staticcheck // intentionally using deprecated field to verify backward compatibility in e2e + Logformat: "json", + }, + Notifications: argov1beta1api.ArgoCDNotifications{ + Enabled: true, + //nolint:staticcheck // intentionally using deprecated field to verify backward compatibility in e2e + Logformat: "json", + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("waiting for the ArgoCD instance to become fully available") + + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + deploymentCommandContains := func(deplName, flag, value string) bool { + depl := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: deplName, Namespace: ns.Name}, + } + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(depl), depl); err != nil { + GinkgoWriter.Println("error fetching deployment", deplName, ":", err) + return false + } + if len(depl.Spec.Template.Spec.Containers) == 0 { + GinkgoWriter.Println("deployment", deplName, "has no containers yet") + return false + } + cmdStr := strings.Join(depl.Spec.Template.Spec.Containers[0].Command, " ") + GinkgoWriter.Println(deplName, "command:", cmdStr) + + return strings.Contains(cmdStr, flag+" "+value) + } + By("verifying argocd-applicationset-controller Deployment has --logformat json from deprecated field") + + Eventually(func() bool { + return deploymentCommandContains("argocd-applicationset-controller", "--logformat", "json") + }, "2m", "5s").Should(BeTrue()) + By("verifying argocd-notifications-controller Deployment has --logformat json from deprecated field") + + Eventually(func() bool { + return deploymentCommandContains("argocd-notifications-controller", "--logformat", "json") + }, "2m", "5s").Should(BeTrue()) + + }) }) }) diff --git a/test/openshift/e2e/ginkgo/parallel/1-048_validate_controller_sharding_test.go b/test/openshift/e2e/ginkgo/parallel/1-048_validate_controller_sharding_test.go index ded047ffb2e..382df26dfab 100644 --- a/test/openshift/e2e/ginkgo/parallel/1-048_validate_controller_sharding_test.go +++ b/test/openshift/e2e/ginkgo/parallel/1-048_validate_controller_sharding_test.go @@ -26,6 +26,7 @@ import ( "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture" argocdFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/argocd" k8sFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/k8s" + "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/statefulset" fixtureUtils "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/utils" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -94,6 +95,43 @@ var _ = Describe("GitOps Operator Parallel E2E Tests", func() { } Expect(match).To(BeTrue(), "StatefulSet should have expected ARGOCD_CONTROLLER_REPLICAS") + By("ensuring algorithm can be set") + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.Controller.Sharding = argov1beta1api.ArgoCDApplicationControllerShardSpec{ + Enabled: true, + Replicas: 3, + DistributionAlgorithm: "round-robin", + } + }) + + By("checking if ARGOCD_CONTROLLER_SHARDING_ALGORITHM env var is set in the app controller StatefulSet") + Eventually(statefulSet).Should(k8sFixture.ExistByName()) + Eventually(statefulSet, "60s", "5s").Should(statefulset.HaveContainerWithEnvVar("ARGOCD_CONTROLLER_SHARDING_ALGORITHM", "round-robin", 0), "Statefulset should have expected ARGOCD_CONTROLLER_SHARDING_ALGORITHM to be round-robin") + + By("unset algorithm and ensure that it is not set") + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.Controller.Sharding = argov1beta1api.ArgoCDApplicationControllerShardSpec{ + Enabled: true, + Replicas: 3, + } + }) + + By("checking if ARGOCD_CONTROLLER_SHARDING_ALGORITHM env var is not set in app controller StatefulSet") + Eventually(statefulSet).Should(k8sFixture.ExistByName()) + Eventually(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(statefulSet), statefulSet); err != nil { + return false + } + if len(statefulSet.Spec.Template.Spec.Containers) == 0 { + return false + } + for _, env := range statefulSet.Spec.Template.Spec.Containers[0].Env { + if env.Name == "ARGOCD_CONTROLLER_SHARDING_ALGORITHM" { + return false + } + } + return true + }, "60s", "5s").Should(BeTrue(), "StatefulSet should have no env variable named ARGOCD_CONTROLLER_SHARDING_ALGORITHM") By("disabling sharding") argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { ac.Spec.Controller.Sharding = argov1beta1api.ArgoCDApplicationControllerShardSpec{ diff --git a/test/openshift/e2e/ginkgo/parallel/1-095_validate_dex_clientsecret_test.go b/test/openshift/e2e/ginkgo/parallel/1-095_validate_dex_clientsecret_test.go index 1febd4ba8df..9708e7507e4 100644 --- a/test/openshift/e2e/ginkgo/parallel/1-095_validate_dex_clientsecret_test.go +++ b/test/openshift/e2e/ginkgo/parallel/1-095_validate_dex_clientsecret_test.go @@ -18,19 +18,44 @@ package parallel import ( "context" + "strings" + "time" argov1beta1api "github.com/argoproj-labs/argocd-operator/api/v1beta1" + "github.com/argoproj-labs/argocd-operator/common" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture" argocdFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/argocd" k8sFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/k8s" + secretFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/secret" fixtureUtils "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/utils" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) +// newArgoCDForDexOpenShiftOAuthE2E returns the ArgoCD CR. +func newArgoCDForDexOpenShiftOAuthE2E(namespace string) *argov1beta1api.ArgoCD { + return &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "example-argocd", Namespace: namespace}, + Spec: argov1beta1api.ArgoCDSpec{ + SSO: &argov1beta1api.ArgoCDSSOSpec{ + Provider: argov1beta1api.SSOProviderTypeDex, + Dex: &argov1beta1api.ArgoCDDexSpec{ + OpenShiftOAuth: true, + }, + }, + Server: argov1beta1api.ArgoCDServerSpec{ + Route: argov1beta1api.ArgoCDRouteSpec{ + Enabled: true, + }, + }, + }, + } +} + var _ = Describe("GitOps Operator Parallel E2E Tests", func() { Context("1-095_validate_dex_clientsecret", func() { @@ -47,69 +72,195 @@ var _ = Describe("GitOps Operator Parallel E2E Tests", func() { ctx = context.Background() }) - It("verifies that Dex serviceaccount token secret is not leaked, and is correctly set in Argo CD argocd-secret Secret", func() { + It("verifies that the Dex client secret is sourced from a short-lived TokenRequest token and is correctly set in argocd-secret", func() { By("creating simple Argo CD instance with Dex and Openshift OAuth enabled") ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() defer cleanupFunc() - argoCD := &argov1beta1api.ArgoCD{ - ObjectMeta: metav1.ObjectMeta{Name: "example-argocd", Namespace: ns.Name}, - Spec: argov1beta1api.ArgoCDSpec{ - SSO: &argov1beta1api.ArgoCDSSOSpec{ - Provider: argov1beta1api.SSOProviderTypeDex, - Dex: &argov1beta1api.ArgoCDDexSpec{ - OpenShiftOAuth: true, - }, - }, - Server: argov1beta1api.ArgoCDServerSpec{ - Route: argov1beta1api.ArgoCDRouteSpec{ - Enabled: true, - }, - }, - }, - } + argoCD := newArgoCDForDexOpenShiftOAuthE2E(ns.Name) Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) By("waiting for ArgoCD CR to be reconciled and the instance to be ready") Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) - serviceAccount := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "example-argocd-argocd-dex-server", Namespace: ns.Name}} + dexSAName := "example-argocd-argocd-dex-server" + serviceAccount := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: dexSAName, Namespace: ns.Name}} Eventually(serviceAccount).Should(k8sFixture.ExistByName()) - argocdCM := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{Name: "argocd-cm", Namespace: ns.Name}, - } - Eventually(argocdCM).Should(k8sFixture.ExistByName()) + By("verifying no additional non-expiring kubernetes.io/service-account-token Secrets exist for the Dex SA beyond platform-created ones (OCP <4.16 creates one for image registry)") + Consistently(func() bool { + secretList := &corev1.SecretList{} + if err := k8sClient.List(ctx, secretList, client.InNamespace(ns.Name)); err != nil { + return false + } + + tokenCount := 0 + for _, s := range secretList.Items { + if s.Type == corev1.SecretTypeServiceAccountToken && + strings.HasPrefix(s.Name, dexSAName+"-token-") && + s.Annotations[corev1.ServiceAccountNameKey] == dexSAName { + tokenCount++ + } + } + + // Allow max 1 token (platform-created on OCP 4.14/4.15), but operator shouldn't create more + // On OCP 4.16+, tokenCount will be 0 as no automatic tokens are created + return tokenCount <= 1 + }, "20s", "4s").Should(BeTrue(), "operator should not create additional legacy kubernetes.io/service-account-token Secrets beyond platform-created ones") By("verifying argocd-cm ConfigMap is not leaking oidc dex client secret") - dexConfig := argocdCM.Data["dex.config"] + argocdCM := &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: "argocd-cm", Namespace: ns.Name}} + Eventually(argocdCM).Should(k8sFixture.ExistByName()) - Expect(dexConfig).To(ContainSubstring("clientSecret: $oidc.dex.clientSecret"), "'$oidc.dex.clientSecret' should be set. Any other value implies that the client secret is exposed via ConfigMap") + Eventually(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(argocdCM), argocdCM); err != nil { + return false + } + return strings.Contains(argocdCM.Data["dex.config"], "clientSecret: $oidc.dex.clientSecret") + }, "2m", "5s").Should(BeTrue(), "'$oidc.dex.clientSecret' should be set. Any other value implies that the client secret is exposed via ConfigMap") - By("validating that the Dex Client Secret was copied from dex serviceaccount token secret in to argocd-secret, by the operator") + By("verifying the Dex SA has no non-expiring kubernetes.io/service-account-token Secrets in its .secrets list") + dexSANoLegacyTokenRefs := func() bool { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(serviceAccount), serviceAccount); err != nil { + return false + } + for _, ref := range serviceAccount.Secrets { + if strings.Contains(ref.Name, "dex-server-token") { + GinkgoWriter.Println("Dex SA still has legacy token Secret reference:", ref.Name) + return false + } + } + return true + } + Eventually(dexSANoLegacyTokenRefs, "2m", "5s").Should(BeTrue(), "Dex SA .secrets must not reference any legacy non-expiring token Secrets") + By("verifying that absence of legacy token Secret references in the Dex SA .secrets list persists") + Consistently(dexSANoLegacyTokenRefs, "20s", "4s").Should(BeTrue(), "Dex SA .secrets must keep no legacy non-expiring token Secret references") - // The operator now creates an Opaque secret with a deterministic name for the Dex token - // (via TokenRequest API) instead of using auto-generated kubernetes.io/service-account-token secrets. - // The secret name follows the pattern: --token - dexTokenSecretName := "example-argocd-argocd-dex-server-token" // #nosec G101 -- This is a Kubernetes secret name, not a credential + By("verifying the dedicated short-lived Dex token Secret was created by the operator") + tokenSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "example-argocd-argocd-dex-server-token", Namespace: ns.Name}} + Eventually(tokenSecret, "2m", "5s").Should(k8sFixture.ExistByName()) + Eventually(tokenSecret).Should(secretFixture.HaveNonEmptyKeyValue("token")) + Eventually(tokenSecret).Should(secretFixture.HaveNonEmptyKeyValue("expiry")) - // Extract the clientSecret from the Dex token secret - dexTokenSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: dexTokenSecretName, Namespace: ns.Name}} - Eventually(dexTokenSecret, "30s", "2s").Should(k8sFixture.ExistByName()) - tokenFromDexSecret := dexTokenSecret.Data["token"] - Expect(tokenFromDexSecret).ToNot(BeEmpty()) - // Verify the secret also contains an expiry field - Expect(dexTokenSecret.Data["expiry"]).ToNot(BeEmpty()) + By("verifying the token expiry is a valid RFC3339 timestamp in the future") + Eventually(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(tokenSecret), tokenSecret); err != nil { + return false + } + expiry, err := time.Parse(time.RFC3339, string(tokenSecret.Data["expiry"])) + if err != nil { + GinkgoWriter.Println("expiry is not valid RFC3339:", string(tokenSecret.Data["expiry"]), err) + return false + } + GinkgoWriter.Println("token expiry:", expiry.UTC()) + return time.Until(expiry) > 0 + }, "2m", "5s").Should(BeTrue(), "Dex token 'expiry' must be a valid RFC3339 timestamp in the future") - // actualClientSecret is the value of the secret in argocd-secret where argocd-operator should copy the secret from + By("validating that the Dex client secret in argocd-secret matches the token in the dedicated token Secret") argocdSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "argocd-secret", Namespace: ns.Name}} Eventually(argocdSecret).Should(k8sFixture.ExistByName()) + Eventually(argocdSecret).Should(secretFixture.HaveNonEmptyKeyValue("oidc.dex.clientSecret")) - actualClientSecret := argocdSecret.Data["oidc.dex.clientSecret"] + Eventually(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(tokenSecret), tokenSecret); err != nil { + return false + } + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(argocdSecret), argocdSecret); err != nil { + return false + } + return string(tokenSecret.Data["token"]) == string(argocdSecret.Data["oidc.dex.clientSecret"]) + }, "2m", "5s").Should(BeTrue(), "Dex client secret in argocd-secret must match the token in the dedicated Dex token Secret") + }) - Expect(string(actualClientSecret)).To(Equal(string(tokenFromDexSecret)), "Dex Client Secret for OIDC is not valid") + It("verifies the operator deletes legacy non-expiring Dex kubernetes.io/service-account-token Secrets and drops them from the Dex SA", func() { + + By("creating simple Argo CD instance with Dex and Openshift OAuth enabled") + ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer cleanupFunc() + + argoCD := newArgoCDForDexOpenShiftOAuthE2E(ns.Name) + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("waiting for ArgoCD CR to be reconciled and the instance to be ready") + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + dexSAName := "example-argocd-argocd-dex-server" + dexSA := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: dexSAName, Namespace: ns.Name}} + Eventually(dexSA).Should(k8sFixture.ExistByName()) + + legacyName := dexSAName + "-token-e2elegacy" + By("creating a legacy non-expiring kubernetes.io/service-account-token Secret for the Dex SA (operator-tracked label required for cleanup list)") + legacySecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: legacyName, + Namespace: ns.Name, + Labels: map[string]string{ + common.ArgoCDTrackedByOperatorLabel: common.ArgoCDAppName, + }, + Annotations: map[string]string{ + corev1.ServiceAccountNameKey: dexSAName, + }, + }, + Type: corev1.SecretTypeServiceAccountToken, + } + Expect(k8sClient.Create(ctx, legacySecret)).To(Succeed()) + + By("adding the legacy Secret to the Dex SA .secrets list to mimic stale controller state") + Eventually(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(dexSA), dexSA); err != nil { + GinkgoWriter.Printf("get Dex SA: %v\n", err) + return false + } + for _, ref := range dexSA.Secrets { + if ref.Name == legacyName { + return true + } + } + dexSA.Secrets = append(dexSA.Secrets, corev1.ObjectReference{Name: legacyName}) + if err := k8sClient.Update(ctx, dexSA); err != nil { + GinkgoWriter.Printf("update Dex SA with legacy secret ref: %v\n", err) + return false + } + return true + }, "2m", "3s").Should(BeTrue(), "Dex SA should list the synthetic legacy token Secret reference") + + By("triggering reconciliation so the operator runs legacy Dex token Secret cleanup (creating the Secret does not enqueue the ArgoCD reconcile)") + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + if ac.Annotations == nil { + ac.Annotations = make(map[string]string) + } + ac.Annotations["test.argocd.argoproj.io/trigger-legacy-dex-token-reconcile"] = time.Now().Format(time.RFC3339Nano) + }) + + By("waiting for the operator to delete the legacy Secret") + legacySecretGone := func() bool { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(legacySecret), legacySecret) + return apierrors.IsNotFound(err) + } + Eventually(legacySecretGone, "2m", "5s").Should(BeTrue(), "legacy kubernetes.io/service-account-token Secret must be deleted") + By("verifying the legacy Secret stays deleted") + Consistently(legacySecretGone, "20s", "4s").Should(BeTrue(), "legacy kubernetes.io/service-account-token Secret must not reappear") + + By("waiting for the Dex SA to no longer reference legacy dex-server-token Secrets") + dexSANoLegacyTokenRefs := func() bool { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(dexSA), dexSA); err != nil { + return false + } + for _, ref := range dexSA.Secrets { + if strings.Contains(ref.Name, "dex-server-token") { + GinkgoWriter.Println("Dex SA still has legacy token Secret reference:", ref.Name) + return false + } + } + return true + } + Eventually(dexSANoLegacyTokenRefs, "2m", "5s").Should(BeTrue(), "Dex SA .secrets must not reference legacy non-expiring token Secrets") + By("verifying that absence of legacy token Secret references in the Dex SA .secrets list persists") + Consistently(dexSANoLegacyTokenRefs, "20s", "4s").Should(BeTrue(), "Dex SA .secrets must keep no legacy non-expiring token Secret references") + By("verifying the Argo CD instance stays healthy after legacy cleanup") + Eventually(argoCD, "2m", "5s").Should(argocdFixture.BeAvailable()) }) }) diff --git a/test/openshift/e2e/ginkgo/parallel/1-125_validate_applicationset_cleanup_when_spec_field_omitted_test.go b/test/openshift/e2e/ginkgo/parallel/1-125_validate_applicationset_cleanup_when_spec_field_omitted_test.go new file mode 100644 index 00000000000..b95c7333660 --- /dev/null +++ b/test/openshift/e2e/ginkgo/parallel/1-125_validate_applicationset_cleanup_when_spec_field_omitted_test.go @@ -0,0 +1,133 @@ +/* +Copyright 2026. + +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 parallel + +import ( + "context" + + argov1beta1api "github.com/argoproj-labs/argocd-operator/api/v1beta1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture" + argocdFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/argocd" + k8sFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/k8s" + fixtureUtils "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/utils" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("GitOps Operator Parallel E2E Tests", func() { + + Context("1-125_validate_applicationset_cleanup_when_spec_field_omitted", func() { + + var ( + k8sClient client.Client + ctx context.Context + ) + + BeforeEach(func() { + fixture.EnsureParallelCleanSlate() + k8sClient, _ = fixtureUtils.GetE2ETestKubeClient() + ctx = context.Background() + }) + + It("removes ApplicationSet workload and namespaced RBAC when spec.applicationSet is removed from the ArgoCD CR", func() { + testNS, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer cleanupFunc() + + const crName = "argocd-appset-spec-omit" + appsetWorkloadName := crName + "-applicationset-controller" + + By("creating Argo CD with spec.applicationSet set to an empty object") + argocdCR := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{ + Name: crName, + Namespace: testNS.Name, + }, + Spec: argov1beta1api.ArgoCDSpec{ + Controller: argov1beta1api.ArgoCDApplicationControllerSpec{ + Enabled: ptr.To(true), + }, + Redis: argov1beta1api.ArgoCDRedisSpec{ + Enabled: ptr.To(true), + }, + Repo: argov1beta1api.ArgoCDRepoSpec{ + Enabled: ptr.To(true), + }, + Server: argov1beta1api.ArgoCDServerSpec{ + Enabled: ptr.To(true), + }, + ApplicationSet: &argov1beta1api.ArgoCDApplicationSet{}, + }, + } + Expect(k8sClient.Create(ctx, argocdCR)).To(Succeed()) + Eventually(argocdCR, "5m", "5s").Should(argocdFixture.BeAvailable()) + + By("verifying ApplicationSet controller resources exist") + depl := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: appsetWorkloadName, Namespace: testNS.Name}, + } + Eventually(depl, "5m", "5s").Should(k8sFixture.ExistByName()) + + sa := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{Name: appsetWorkloadName, Namespace: testNS.Name}, + } + Eventually(sa, "5m", "5s").Should(k8sFixture.ExistByName()) + + role := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{Name: appsetWorkloadName, Namespace: testNS.Name}, + } + Eventually(role, "5m", "5s").Should(k8sFixture.ExistByName()) + + rb := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: appsetWorkloadName, Namespace: testNS.Name}, + } + Eventually(rb, "5m", "5s").Should(k8sFixture.ExistByName()) + + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: appsetWorkloadName, Namespace: testNS.Name}, + } + Eventually(svc, "5m", "5s").Should(k8sFixture.ExistByName()) + + By("removing spec.applicationSet from the ArgoCD CR") + argocdFixture.Update(argocdCR, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.ApplicationSet = nil + }) + Eventually(argocdCR, "5m", "5s").Should(argocdFixture.BeAvailable()) + + By("verifying ApplicationSet resources are deleted") + Eventually(depl, "5m", "5s").Should(k8sFixture.NotExistByName()) + Consistently(depl, "30s", "5s").Should(k8sFixture.NotExistByName()) + + Eventually(sa, "5m", "5s").Should(k8sFixture.NotExistByName()) + Consistently(sa, "30s", "5s").Should(k8sFixture.NotExistByName()) + + Eventually(role, "5m", "5s").Should(k8sFixture.NotExistByName()) + Consistently(role, "30s", "5s").Should(k8sFixture.NotExistByName()) + + Eventually(rb, "5m", "5s").Should(k8sFixture.NotExistByName()) + Consistently(rb, "30s", "5s").Should(k8sFixture.NotExistByName()) + + Eventually(svc, "5m", "5s").Should(k8sFixture.NotExistByName()) + Consistently(svc, "30s", "5s").Should(k8sFixture.NotExistByName()) + }) + }) +}) diff --git a/test/openshift/e2e/ginkgo/parallel/1-125_validate_server_serving_cert_annotation_restore_test.go b/test/openshift/e2e/ginkgo/parallel/1-125_validate_server_serving_cert_annotation_restore_test.go new file mode 100644 index 00000000000..887bf111251 --- /dev/null +++ b/test/openshift/e2e/ginkgo/parallel/1-125_validate_server_serving_cert_annotation_restore_test.go @@ -0,0 +1,213 @@ +/* +Copyright 2025. + +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 parallel + +import ( + "context" + + argov1beta1api "github.com/argoproj-labs/argocd-operator/api/v1beta1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/argoproj-labs/argocd-operator/common" + certFixture "github.com/argoproj-labs/argocd-operator/tests/ginkgo/fixture/certificate" + routev1 "github.com/openshift/api/route/v1" + "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture" + argocdFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/argocd" + k8sFixture "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/k8s" + fixtureUtils "github.com/redhat-developer/gitops-operator/test/openshift/e2e/ginkgo/fixture/utils" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("GitOps Operator Parallel E2E Tests", func() { + + Context("1-125_validate_server_serving_cert_annotation_restore", func() { + + var ( + ctx context.Context + k8sClient client.Client + ) + + BeforeEach(func() { + fixture.EnsureParallelCleanSlate() + k8sClient, _ = fixtureUtils.GetE2ETestKubeClient() + ctx = context.Background() + }) + + It("restores service.beta.openshift.io/serving-cert-secret-name when Route TLS returns from passthrough to default reencrypt", func() { + ns, nsCleanup := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer nsCleanup() + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example", + Namespace: ns.Name, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + Eventually(argoCD, "3m", "5s").Should(argocdFixture.HaveServerStatus("Running")) + + serverSvc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-server", + Namespace: ns.Name, + }, + } + Eventually(serverSvc, "3m", "5s").Should(k8sFixture.ExistByName()) + Eventually(serverSvc, "3m", "5s").Should( + k8sFixture.HaveAnnotationWithValue(common.AnnotationOpenShiftServiceCA, common.ArgoCDServerTLSSecretName)) + + By("setting Route TLS termination to passthrough so AutoTLS is disabled and the serving-cert annotation is removed") + + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.Server.Route.TLS = &routev1.TLSConfig{ + Termination: routev1.TLSTerminationPassthrough, + } + }) + + Eventually(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(serverSvc), serverSvc); err != nil { + GinkgoWriter.Println(err) + return false + } + if serverSvc.Annotations == nil { + return true + } + _, present := serverSvc.Annotations[common.AnnotationOpenShiftServiceCA] + return !present + }, "3m", "5s").Should(BeTrue(), "serving-cert annotation should be removed under passthrough") + + By("clearing Route TLS so the operator defaults to reencrypt again") + + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.Server.Route.TLS = nil + }) + + By("verifying the serving-cert annotation is restored") + Eventually(serverSvc, "3m", "5s").Should( + k8sFixture.HaveAnnotationWithValue(common.AnnotationOpenShiftServiceCA, common.ArgoCDServerTLSSecretName)) + + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + Eventually(argoCD, "3m", "5s").Should(argocdFixture.HaveServerStatus("Running")) + + By("simulating a stale Service after upgrade: remove serving-cert annotation only") + + k8sFixture.Update(serverSvc, func(obj client.Object) { + s := obj.(*corev1.Service) + if s.Annotations != nil { + delete(s.Annotations, common.AnnotationOpenShiftServiceCA) + } + }) + + Eventually(serverSvc, "3m", "5s").Should( + k8sFixture.HaveAnnotationWithValue(common.AnnotationOpenShiftServiceCA, common.ArgoCDServerTLSSecretName)) + + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + Eventually(argoCD, "3m", "5s").Should(argocdFixture.HaveServerStatus("Running")) + + }) + + It("does not add serving-cert annotation when argocd-server-tls already exists as a user-managed secret (not Service CA)", func() { + certPem, keyPem, err := certFixture.GenerateCert() + Expect(err).NotTo(HaveOccurred()) + + ns, nsCleanup := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer nsCleanup() + + By("pre-creating argocd-server-tls without OpenShift Service CA annotations or ownerReferences") + + customTLS := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.ArgoCDServerTLSSecretName, + Namespace: ns.Name, + }, + Type: corev1.SecretTypeTLS, + StringData: map[string]string{ + "tls.crt": string(certPem), + "tls.key": string(keyPem), + }, + } + Expect(k8sClient.Create(ctx, customTLS)).To(Succeed()) + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-argocd", + Namespace: ns.Name, + }, + Spec: argov1beta1api.ArgoCDSpec{ + Server: argov1beta1api.ArgoCDServerSpec{ + Route: argov1beta1api.ArgoCDRouteSpec{ + TLS: &routev1.TLSConfig{ + Termination: routev1.TLSTerminationReencrypt, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + Eventually(argoCD, "3m", "5s").Should(argocdFixture.HaveServerStatus("Running")) + + serverSvc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-argocd-server", + Namespace: ns.Name, + }, + } + Eventually(serverSvc, "3m", "5s").Should(k8sFixture.ExistByName()) + + By("verifying the server Service never gets the OpenShift serving-cert annotation") + + Consistently(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(serverSvc), serverSvc); err != nil { + GinkgoWriter.Println(err) + return false + } + if serverSvc.Annotations == nil { + return true + } + _, present := serverSvc.Annotations[common.AnnotationOpenShiftServiceCA] + return !present + }, "2m", "5s").Should(BeTrue()) + + By("verifying annotation stays absent after an ArgoCD reconcile trigger") + + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + if ac.Annotations == nil { + ac.Annotations = map[string]string{} + } + ac.Annotations["argocds.argoproj.io/e2e-custom-tls-no-svc-ca-touch"] = "1" + }) + + Consistently(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(serverSvc), serverSvc); err != nil { + GinkgoWriter.Println(err) + return false + } + if serverSvc.Annotations == nil { + return true + } + _, present := serverSvc.Annotations[common.AnnotationOpenShiftServiceCA] + return !present + }, "2m", "5s").Should(BeTrue()) + }) + }) +}) diff --git a/test/openshift/e2e/ginkgo/parallel/1-126_validate_declarative_webhook_secrets_test.go b/test/openshift/e2e/ginkgo/parallel/1-126_validate_declarative_webhook_secrets_test.go new file mode 100644 index 00000000000..c7fa37b36d7 --- /dev/null +++ b/test/openshift/e2e/ginkgo/parallel/1-126_validate_declarative_webhook_secrets_test.go @@ -0,0 +1,700 @@ +/* +Copyright 2025. + +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 parallel + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + argov1beta1api "github.com/argoproj-labs/argocd-operator/api/v1beta1" + "github.com/argoproj-labs/argocd-operator/common" + "github.com/argoproj-labs/argocd-operator/tests/ginkgo/fixture" + argocdFixture "github.com/argoproj-labs/argocd-operator/tests/ginkgo/fixture/argocd" + k8sFixture "github.com/argoproj-labs/argocd-operator/tests/ginkgo/fixture/k8s" + secretFixture "github.com/argoproj-labs/argocd-operator/tests/ginkgo/fixture/secret" + fixtureUtils "github.com/argoproj-labs/argocd-operator/tests/ginkgo/fixture/utils" +) + +var _ = Describe("GitOps Operator Parallel E2E Tests", func() { + + Context("1-126_validate_declarative_webhook_secrets", func() { + + var ( + k8sClient client.Client + ctx context.Context + ) + + BeforeEach(func() { + fixture.EnsureParallelCleanSlate() + var err error + k8sClient, _, err = fixtureUtils.GetE2ETestKubeClientWithError() + Expect(err).NotTo(HaveOccurred()) + ctx = context.Background() + }) + + It("verifies spec.webhookSecrets.github.webhookSecretRef is synced into argocd-secret as webhook.github.secret", func() { + By("creating Argo CD instance") + ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer cleanupFunc() + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "example-argocd-github", Namespace: ns.Name}, + Spec: argov1beta1api.ArgoCDSpec{ + Server: argov1beta1api.ArgoCDServerSpec{ + Route: argov1beta1api.ArgoCDRouteSpec{Enabled: true}, + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("waiting for ArgoCD CR to be reconciled and the instance to be ready") + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + By("waiting for argocd-secret to exist") + argocdSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "argocd-secret", Namespace: ns.Name}} + Eventually(argocdSecret, "2m", "3s").Should(k8sFixture.ExistByName()) + + By("creating user Secret with GitHub webhook token") + expectedToken := "e2e-github-webhook-secret-token" + userSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "github-webhook-credentials", Namespace: ns.Name}, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{"token": expectedToken}, + } + Expect(k8sClient.Create(ctx, userSecret)).To(Succeed()) + + By("setting spec.webhookSecrets.github.webhookSecretRef on the ArgoCD CR") + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(argoCD), argoCD)).To(Succeed()) + argoCD.Spec.WebhookSecrets = &argov1beta1api.ArgoCDWebhookSecretsSpec{ + GitHub: &argov1beta1api.ArgoCDWebhookSecretsGitHub{ + WebhookSecretRef: &argov1beta1api.WebhookSecretKeySelector{ + Name: "github-webhook-credentials", + Key: "token", + }, + }, + } + Expect(k8sClient.Update(ctx, argoCD)).To(Succeed()) + + By("waiting for argocd-secret to contain webhook.github.secret matching the referenced Secret") + Eventually(argocdSecret, "2m", "3s").Should( + secretFixture.HaveDataKeyValue(common.ArgoCDKeyGitHubWebhookSecret, []byte(expectedToken)), + ) + }) + + It("verifies spec.webhookSecrets.gitlab.webhookSecretRef is synced into argocd-secret as webhook.gitlab.secret", func() { + By("creating Argo CD instance") + ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer cleanupFunc() + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "example-argocd", Namespace: ns.Name}, + Spec: argov1beta1api.ArgoCDSpec{ + Server: argov1beta1api.ArgoCDServerSpec{ + Route: argov1beta1api.ArgoCDRouteSpec{Enabled: true}, + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("waiting for ArgoCD CR to be reconciled and the instance to be ready") + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + By("waiting for argocd-secret to exist") + argocdSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "argocd-secret", Namespace: ns.Name}} + Eventually(argocdSecret, "2m", "3s").Should(k8sFixture.ExistByName()) + + By("creating user Secret with GitLab webhook credentials") + expected := "e2e-gitlab-webhook-secret-value" + userSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "gitlab-webhook-credentials", Namespace: ns.Name}, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{"secret": expected}, + } + Expect(k8sClient.Create(ctx, userSecret)).To(Succeed()) + + By("setting spec.webhookSecrets.gitlab.webhookSecretRef on the ArgoCD CR") + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(argoCD), argoCD)).To(Succeed()) + argoCD.Spec.WebhookSecrets = &argov1beta1api.ArgoCDWebhookSecretsSpec{ + GitLab: &argov1beta1api.ArgoCDWebhookSecretsGitLab{ + WebhookSecretRef: &argov1beta1api.WebhookSecretKeySelector{ + Name: "gitlab-webhook-credentials", + Key: "secret", + }, + }, + } + Expect(k8sClient.Update(ctx, argoCD)).To(Succeed()) + + By("waiting for argocd-secret to contain webhook.gitlab.secret matching the referenced Secret") + Eventually(argocdSecret, "2m", "3s").Should( + secretFixture.HaveDataKeyValue(common.ArgoCDKeyGitLabWebhookSecret, []byte(expected)), + ) + }) + + It("verifies spec.webhookSecrets.azureDevOps username and password secretRefs are synced into argocd-secret", func() { + By("creating Argo CD instance") + ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer cleanupFunc() + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "example-argocd-ado", Namespace: ns.Name}, + Spec: argov1beta1api.ArgoCDSpec{ + Server: argov1beta1api.ArgoCDServerSpec{ + Route: argov1beta1api.ArgoCDRouteSpec{Enabled: true}, + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("waiting for ArgoCD CR to be reconciled and the instance to be ready") + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + By("waiting for argocd-secret to exist") + argocdSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "argocd-secret", Namespace: ns.Name}} + Eventually(argocdSecret, "2m", "3s").Should(k8sFixture.ExistByName()) + + By("creating user Secret with Azure DevOps webhook credentials") + expectedUser := "e2e-ado-username" + expectedPass := "e2e-ado-password-or-pat" + userSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "ado-webhook-credentials", Namespace: ns.Name}, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{ + "username": expectedUser, + "password": expectedPass, + }, + } + Expect(k8sClient.Create(ctx, userSecret)).To(Succeed()) + + By("setting spec.webhookSecrets.azureDevOps on the ArgoCD CR") + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(argoCD), argoCD)).To(Succeed()) + argoCD.Spec.WebhookSecrets = &argov1beta1api.ArgoCDWebhookSecretsSpec{ + AzureDevOps: &argov1beta1api.ArgoCDWebhookSecretsAzureDevOps{ + UsernameSecretRef: &argov1beta1api.WebhookSecretKeySelector{ + Name: "ado-webhook-credentials", + Key: "username", + }, + PasswordSecretRef: &argov1beta1api.WebhookSecretKeySelector{ + Name: "ado-webhook-credentials", + Key: "password", + }, + }, + } + Expect(k8sClient.Update(ctx, argoCD)).To(Succeed()) + + By("waiting for both Azure DevOps keys in argocd-secret") + Eventually(argocdSecret, "2m", "3s").Should( + And( + secretFixture.HaveDataKeyValue(common.ArgoCDKeyAzureDevOpsWebhookUsername, []byte(expectedUser)), + secretFixture.HaveDataKeyValue(common.ArgoCDKeyAzureDevOpsWebhookPassword, []byte(expectedPass)), + ), + ) + }) + + It("verifies GitHub and GitLab webhook secret references can be configured together on one ArgoCD instance", func() { + By("creating Argo CD instance") + ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer cleanupFunc() + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "example-argocd-multi", Namespace: ns.Name}, + Spec: argov1beta1api.ArgoCDSpec{ + Server: argov1beta1api.ArgoCDServerSpec{ + Route: argov1beta1api.ArgoCDRouteSpec{Enabled: true}, + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("waiting for ArgoCD CR to be reconciled and the instance to be ready") + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + By("waiting for argocd-secret to exist") + argocdSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "argocd-secret", Namespace: ns.Name}} + Eventually(argocdSecret, "2m", "3s").Should(k8sFixture.ExistByName()) + + By("creating user Secrets for GitHub and GitLab webhook credentials") + ghToken := "e2e-combined-github-token" + glSecret := "e2e-combined-gitlab-secret" + Expect(k8sClient.Create(ctx, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "gh-creds", Namespace: ns.Name}, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{"token": ghToken}, + })).To(Succeed()) + Expect(k8sClient.Create(ctx, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "gl-creds", Namespace: ns.Name}, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{"secret": glSecret}, + })).To(Succeed()) + + By("setting spec.webhookSecrets.github and spec.webhookSecrets.gitlab on the ArgoCD CR") + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(argoCD), argoCD)).To(Succeed()) + argoCD.Spec.WebhookSecrets = &argov1beta1api.ArgoCDWebhookSecretsSpec{ + GitHub: &argov1beta1api.ArgoCDWebhookSecretsGitHub{ + WebhookSecretRef: &argov1beta1api.WebhookSecretKeySelector{Name: "gh-creds", Key: "token"}, + }, + GitLab: &argov1beta1api.ArgoCDWebhookSecretsGitLab{ + WebhookSecretRef: &argov1beta1api.WebhookSecretKeySelector{Name: "gl-creds", Key: "secret"}, + }, + } + Expect(k8sClient.Update(ctx, argoCD)).To(Succeed()) + + By("waiting for argocd-secret to contain GitHub and GitLab webhook keys") + Eventually(argocdSecret, "2m", "3s").Should( + And( + secretFixture.HaveDataKeyValue(common.ArgoCDKeyGitHubWebhookSecret, []byte(ghToken)), + secretFixture.HaveDataKeyValue(common.ArgoCDKeyGitLabWebhookSecret, []byte(glSecret)), + ), + ) + }) + + It("verifies Bitbucket Cloud, Bitbucket Server, and Gogs webhook secret references are synced into argocd-secret", func() { + By("creating Argo CD instance") + ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer cleanupFunc() + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "example-argocd-bb-gogs", Namespace: ns.Name}, + Spec: argov1beta1api.ArgoCDSpec{ + Server: argov1beta1api.ArgoCDServerSpec{ + Route: argov1beta1api.ArgoCDRouteSpec{Enabled: true}, + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("waiting for ArgoCD CR to be reconciled and the instance to be ready") + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + By("waiting for argocd-secret to exist") + argocdSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "argocd-secret", Namespace: ns.Name}} + Eventually(argocdSecret, "2m", "3s").Should(k8sFixture.ExistByName()) + + By("creating user Secrets for Bitbucket Cloud, Bitbucket Server, and Gogs") + bbUUID := "e2e-bb-cloud-uuid" + bbSrv := "e2e-bb-server-secret" + gogsVal := "e2e-gogs-secret" + Expect(k8sClient.Create(ctx, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "bb-cloud-creds", Namespace: ns.Name}, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{"uuid": bbUUID}, + })).To(Succeed()) + Expect(k8sClient.Create(ctx, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "bb-server-creds", Namespace: ns.Name}, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{"secret": bbSrv}, + })).To(Succeed()) + Expect(k8sClient.Create(ctx, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "gogs-creds", Namespace: ns.Name}, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{"secret": gogsVal}, + })).To(Succeed()) + + By("setting spec.webhookSecrets for Bitbucket Cloud, Bitbucket Server, and Gogs on the ArgoCD CR") + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(argoCD), argoCD)).To(Succeed()) + argoCD.Spec.WebhookSecrets = &argov1beta1api.ArgoCDWebhookSecretsSpec{ + Bitbucket: &argov1beta1api.ArgoCDWebhookSecretsBitbucket{ + WebhookUUIDSecretRef: &argov1beta1api.WebhookSecretKeySelector{Name: "bb-cloud-creds", Key: "uuid"}, + }, + BitbucketServer: &argov1beta1api.ArgoCDWebhookSecretsBitbucketServer{ + WebhookSecretRef: &argov1beta1api.WebhookSecretKeySelector{Name: "bb-server-creds", Key: "secret"}, + }, + Gogs: &argov1beta1api.ArgoCDWebhookSecretsGogs{ + WebhookSecretRef: &argov1beta1api.WebhookSecretKeySelector{Name: "gogs-creds", Key: "secret"}, + }, + } + Expect(k8sClient.Update(ctx, argoCD)).To(Succeed()) + + By("waiting for argocd-secret to contain Bitbucket Cloud, Bitbucket Server, and Gogs webhook keys") + Eventually(argocdSecret, "2m", "3s").Should( + And( + secretFixture.HaveDataKeyValue(common.ArgoCDKeyBitbucketCloudWebhookSecret, []byte(bbUUID)), + secretFixture.HaveDataKeyValue(common.ArgoCDKeyBitbucketServerWebhookSecret, []byte(bbSrv)), + secretFixture.HaveDataKeyValue(common.ArgoCDKeyGogsWebhookSecret, []byte(gogsVal)), + ), + ) + }) + + It("does not remove webhook.github.secret when spec.webhookSecrets is never set on the Argo CD CR", func() { + ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer cleanupFunc() + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "example-argocd-manual-github-webhook", Namespace: ns.Name}, + Spec: argov1beta1api.ArgoCDSpec{ + Server: argov1beta1api.ArgoCDServerSpec{ + Route: argov1beta1api.ArgoCDRouteSpec{Enabled: true}, + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + argocdSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "argocd-secret", Namespace: ns.Name}} + By("waiting for argocd-secret and operator-populated keys") + Eventually(argocdSecret, "5m", "5s").Should(k8sFixture.ExistByName()) + Eventually(argocdSecret, "5m", "5s").Should(secretFixture.HaveNonEmptyKeyValue(common.ArgoCDKeyAdminPassword)) + + manualToken := []byte("e2e-manual-only-github-webhook-token") + By("writing webhook.github.secret on argocd-secret without spec.webhookSecrets") + secretFixture.Update(argocdSecret, func(s *corev1.Secret) { + if s.Data == nil { + s.Data = map[string][]byte{} + } + s.Data[common.ArgoCDKeyGitHubWebhookSecret] = manualToken + }) + + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(argoCD), argoCD)).To(Succeed()) + Expect(argoCD.Spec.WebhookSecrets).To(BeNil()) + + Eventually(argocdSecret, "2m", "3s").Should( + secretFixture.HaveDataKeyValue(common.ArgoCDKeyGitHubWebhookSecret, manualToken), + ) + Consistently(argocdSecret, "30s", "5s").Should( + secretFixture.HaveDataKeyValue(common.ArgoCDKeyGitHubWebhookSecret, manualToken), + ) + }) + + It("preserves webhook.github.secret after declarative sync when entire spec.webhookSecrets is cleared", func() { + ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer cleanupFunc() + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "example-argocd-nil-whole-webhooksecrets", Namespace: ns.Name}, + Spec: argov1beta1api.ArgoCDSpec{ + Server: argov1beta1api.ArgoCDServerSpec{ + Route: argov1beta1api.ArgoCDRouteSpec{Enabled: true}, + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + argocdSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "argocd-secret", Namespace: ns.Name}} + By("waiting for argocd-secret and operator-populated keys") + Eventually(argocdSecret, "5m", "5s").Should(k8sFixture.ExistByName()) + Eventually(argocdSecret, "5m", "5s").Should(secretFixture.HaveNonEmptyKeyValue(common.ArgoCDKeyAdminPassword)) + + token := "e2e-nil-stanza-github-token" + Expect(k8sClient.Create(ctx, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "gh-nil-stanza-creds", Namespace: ns.Name}, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{"token": token}, + })).To(Succeed()) + + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(argoCD), argoCD)).To(Succeed()) + argoCD.Spec.WebhookSecrets = &argov1beta1api.ArgoCDWebhookSecretsSpec{ + GitHub: &argov1beta1api.ArgoCDWebhookSecretsGitHub{ + WebhookSecretRef: &argov1beta1api.WebhookSecretKeySelector{Name: "gh-nil-stanza-creds", Key: "token"}, + }, + } + Expect(k8sClient.Update(ctx, argoCD)).To(Succeed()) + + Eventually(argocdSecret, "2m", "3s").Should( + secretFixture.HaveDataKeyValue(common.ArgoCDKeyGitHubWebhookSecret, []byte(token)), + ) + + By("merging spec.webhookSecrets: null so the field is unset (operator skips per-provider webhook cleanup)") + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(argoCD), argoCD)).To(Succeed()) + Expect(k8sClient.Patch(ctx, argoCD, client.RawPatch(types.MergePatchType, []byte(`{"spec":{"webhookSecrets":null}}`)))).To(Succeed()) + + Eventually(argocdSecret, "2m", "3s").Should( + secretFixture.HaveDataKeyValue(common.ArgoCDKeyGitHubWebhookSecret, []byte(token)), + ) + Consistently(argocdSecret, "30s", "5s").Should( + secretFixture.HaveDataKeyValue(common.ArgoCDKeyGitHubWebhookSecret, []byte(token)), + ) + }) + + It("removes webhook.github.secret from argocd-secret when GitHub is not declared in spec.webhookSecrets", func() { + By("creating Argo CD instance and syncing GitHub webhook secret") + ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer cleanupFunc() + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "example-argocd-clear-gh", Namespace: ns.Name}, + Spec: argov1beta1api.ArgoCDSpec{ + Server: argov1beta1api.ArgoCDServerSpec{ + Route: argov1beta1api.ArgoCDRouteSpec{Enabled: true}, + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("waiting for ArgoCD CR to be reconciled and the instance to be ready") + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + By("waiting for argocd-secret to exist") + argocdSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "argocd-secret", Namespace: ns.Name}} + Eventually(argocdSecret, "2m", "3s").Should(k8sFixture.ExistByName()) + + token := "e2e-clear-github-token" + Expect(k8sClient.Create(ctx, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "gh-clear-creds", Namespace: ns.Name}, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{"token": token}, + })).To(Succeed()) + + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(argoCD), argoCD)).To(Succeed()) + argoCD.Spec.WebhookSecrets = &argov1beta1api.ArgoCDWebhookSecretsSpec{ + GitHub: &argov1beta1api.ArgoCDWebhookSecretsGitHub{ + WebhookSecretRef: &argov1beta1api.WebhookSecretKeySelector{Name: "gh-clear-creds", Key: "token"}, + }, + } + Expect(k8sClient.Update(ctx, argoCD)).To(Succeed()) + + By("waiting for argocd-secret to contain webhook.github.secret") + Eventually(argocdSecret, "2m", "3s").Should( + secretFixture.HaveDataKeyValue(common.ArgoCDKeyGitHubWebhookSecret, []byte(token)), + ) + + By("making GitHub undeclared in spec.webhookSecrets via merge patch") + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(argoCD), argoCD)).To(Succeed()) + gitHubRemovalPatch := []byte(`{"spec":{"webhookSecrets":{"github":null}}}`) + Expect(k8sClient.Patch(ctx, argoCD, client.RawPatch(types.MergePatchType, gitHubRemovalPatch))).To(Succeed()) + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + By("waiting for webhook.github.secret to be removed from argocd-secret") + Eventually(argocdSecret, "2m", "3s").Should(secretFixture.NotHaveDataKey(common.ArgoCDKeyGitHubWebhookSecret)) + }) + + It("removes webhook.gitlab.secret when GitLab is dropped from spec while GitHub remains", func() { + By("creating Argo CD instance and syncing GitHub and GitLab webhook secrets") + ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer cleanupFunc() + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "example-argocd-partial-gl", Namespace: ns.Name}, + Spec: argov1beta1api.ArgoCDSpec{ + Server: argov1beta1api.ArgoCDServerSpec{ + Route: argov1beta1api.ArgoCDRouteSpec{Enabled: true}, + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("waiting for ArgoCD CR to be reconciled and the instance to be ready") + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + By("waiting for argocd-secret to exist") + argocdSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "argocd-secret", Namespace: ns.Name}} + Eventually(argocdSecret, "2m", "3s").Should(k8sFixture.ExistByName()) + + ghTok := "e2e-partial-gh-token" + glSec := "e2e-partial-gl-secret" + Expect(k8sClient.Create(ctx, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "partial-gh", Namespace: ns.Name}, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{"token": ghTok}, + })).To(Succeed()) + Expect(k8sClient.Create(ctx, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "partial-gl", Namespace: ns.Name}, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{"secret": glSec}, + })).To(Succeed()) + + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(argoCD), argoCD)).To(Succeed()) + argoCD.Spec.WebhookSecrets = &argov1beta1api.ArgoCDWebhookSecretsSpec{ + GitHub: &argov1beta1api.ArgoCDWebhookSecretsGitHub{ + WebhookSecretRef: &argov1beta1api.WebhookSecretKeySelector{Name: "partial-gh", Key: "token"}, + }, + GitLab: &argov1beta1api.ArgoCDWebhookSecretsGitLab{ + WebhookSecretRef: &argov1beta1api.WebhookSecretKeySelector{Name: "partial-gl", Key: "secret"}, + }, + } + Expect(k8sClient.Update(ctx, argoCD)).To(Succeed()) + + By("waiting for argocd-secret to contain GitHub and GitLab webhook keys") + Eventually(argocdSecret, "2m", "3s").Should( + And( + secretFixture.HaveDataKeyValue(common.ArgoCDKeyGitHubWebhookSecret, []byte(ghTok)), + secretFixture.HaveDataKeyValue(common.ArgoCDKeyGitLabWebhookSecret, []byte(glSec)), + ), + ) + + By("removing only GitLab from spec.webhookSecrets via merge patch") + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(argoCD), argoCD)).To(Succeed()) + gitLabRemovalPatch := []byte(`{"spec":{"webhookSecrets":{"gitlab":null}}}`) + Expect(k8sClient.Patch(ctx, argoCD, client.RawPatch(types.MergePatchType, gitLabRemovalPatch))).To(Succeed()) + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + By("waiting for GitLab key to be removed and GitHub key unchanged") + Eventually(argocdSecret, "2m", "3s").Should( + And( + secretFixture.HaveDataKeyValue(common.ArgoCDKeyGitHubWebhookSecret, []byte(ghTok)), + secretFixture.NotHaveDataKey(common.ArgoCDKeyGitLabWebhookSecret), + ), + ) + }) + + It("removes Azure DevOps webhook keys from argocd-secret when azureDevOps is dropped from spec.webhookSecrets", func() { + By("creating Argo CD instance and syncing Azure DevOps webhook credentials") + ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer cleanupFunc() + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "example-argocd-drop-ado", Namespace: ns.Name}, + Spec: argov1beta1api.ArgoCDSpec{ + Server: argov1beta1api.ArgoCDServerSpec{ + Route: argov1beta1api.ArgoCDRouteSpec{Enabled: true}, + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("waiting for ArgoCD CR to be reconciled and the instance to be ready") + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + By("waiting for argocd-secret to exist") + argocdSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "argocd-secret", Namespace: ns.Name}} + Eventually(argocdSecret, "2m", "3s").Should(k8sFixture.ExistByName()) + + u := "e2e-drop-ado-user" + p := "e2e-drop-ado-pass" + Expect(k8sClient.Create(ctx, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "ado-drop-creds", Namespace: ns.Name}, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{"username": u, "password": p}, + })).To(Succeed()) + + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(argoCD), argoCD)).To(Succeed()) + argoCD.Spec.WebhookSecrets = &argov1beta1api.ArgoCDWebhookSecretsSpec{ + AzureDevOps: &argov1beta1api.ArgoCDWebhookSecretsAzureDevOps{ + UsernameSecretRef: &argov1beta1api.WebhookSecretKeySelector{Name: "ado-drop-creds", Key: "username"}, + PasswordSecretRef: &argov1beta1api.WebhookSecretKeySelector{Name: "ado-drop-creds", Key: "password"}, + }, + } + Expect(k8sClient.Update(ctx, argoCD)).To(Succeed()) + + By("waiting for argocd-secret to contain Azure DevOps webhook username and password") + Eventually(argocdSecret, "2m", "3s").Should( + And( + secretFixture.HaveDataKeyValue(common.ArgoCDKeyAzureDevOpsWebhookUsername, []byte(u)), + secretFixture.HaveDataKeyValue(common.ArgoCDKeyAzureDevOpsWebhookPassword, []byte(p)), + ), + ) + + By("removing only azureDevOps from spec.webhookSecrets via merge patch") + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(argoCD), argoCD)).To(Succeed()) + adoRemovalPatch := []byte(`{"spec":{"webhookSecrets":{"azureDevOps":null}}}`) + Expect(k8sClient.Patch(ctx, argoCD, client.RawPatch(types.MergePatchType, adoRemovalPatch))).To(Succeed()) + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + By("waiting for Azure DevOps webhook keys to be removed from argocd-secret") + Eventually(argocdSecret, "2m", "3s").Should( + And( + secretFixture.NotHaveDataKey(common.ArgoCDKeyAzureDevOpsWebhookUsername), + secretFixture.NotHaveDataKey(common.ArgoCDKeyAzureDevOpsWebhookPassword), + ), + ) + }) + + It("clears both Azure DevOps webhook keys from argocd-secret when only one ref can be resolved", func() { + By("creating Argo CD instance and syncing Azure DevOps webhook credentials") + ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer cleanupFunc() + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "example-argocd-ado-atomic", Namespace: ns.Name}, + Spec: argov1beta1api.ArgoCDSpec{ + Server: argov1beta1api.ArgoCDServerSpec{ + Route: argov1beta1api.ArgoCDRouteSpec{Enabled: true}, + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + argocdSecret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "argocd-secret", Namespace: ns.Name}} + Eventually(argocdSecret, "2m", "3s").Should(k8sFixture.ExistByName()) + + u := "e2e-ado-atomic-user" + p := "e2e-ado-atomic-pass" + Expect(k8sClient.Create(ctx, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "ado-atomic-creds", Namespace: ns.Name}, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{"username": u, "password": p}, + })).To(Succeed()) + + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(argoCD), argoCD)).To(Succeed()) + argoCD.Spec.WebhookSecrets = &argov1beta1api.ArgoCDWebhookSecretsSpec{ + AzureDevOps: &argov1beta1api.ArgoCDWebhookSecretsAzureDevOps{ + UsernameSecretRef: &argov1beta1api.WebhookSecretKeySelector{Name: "ado-atomic-creds", Key: "username"}, + PasswordSecretRef: &argov1beta1api.WebhookSecretKeySelector{Name: "ado-atomic-creds", Key: "password"}, + }, + } + Expect(k8sClient.Update(ctx, argoCD)).To(Succeed()) + + Eventually(argocdSecret, "2m", "3s").Should( + And( + secretFixture.HaveDataKeyValue(common.ArgoCDKeyAzureDevOpsWebhookUsername, []byte(u)), + secretFixture.HaveDataKeyValue(common.ArgoCDKeyAzureDevOpsWebhookPassword, []byte(p)), + ), + ) + + By("pointing passwordSecretRef at a Secret that does not exist (username still valid)") + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.WebhookSecrets.AzureDevOps.PasswordSecretRef = &argov1beta1api.WebhookSecretKeySelector{ + Name: "ado-atomic-missing-secret", + Key: "password", + } + }) + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + By("waiting for both Azure DevOps keys to be removed (atomic credential pair)") + Eventually(argocdSecret, "2m", "3s").Should( + And( + secretFixture.NotHaveDataKey(common.ArgoCDKeyAzureDevOpsWebhookUsername), + secretFixture.NotHaveDataKey(common.ArgoCDKeyAzureDevOpsWebhookPassword), + ), + ) + }) + + It("rejects spec.webhookSecrets.azureDevOps when only usernameSecretRef is set (CRD validation: both refs required together)", func() { + By("creating namespace and an ArgoCD CR with azureDevOps missing passwordSecretRef") + ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer cleanupFunc() + + invalid := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "example-argocd-invalid-ado-pair", Namespace: ns.Name}, + Spec: argov1beta1api.ArgoCDSpec{ + Server: argov1beta1api.ArgoCDServerSpec{ + Route: argov1beta1api.ArgoCDRouteSpec{Enabled: true}, + }, + WebhookSecrets: &argov1beta1api.ArgoCDWebhookSecretsSpec{ + AzureDevOps: &argov1beta1api.ArgoCDWebhookSecretsAzureDevOps{ + UsernameSecretRef: &argov1beta1api.WebhookSecretKeySelector{Name: "only-user-ref", Key: "username"}, + // PasswordSecretRef intentionally omitted — violates CRD XValidation (pair rule). + }, + }, + }, + } + + err := k8sClient.Create(ctx, invalid) + Expect(err).To(HaveOccurred(), "apiserver should reject azureDevOps with only usernameSecretRef") + msg := err.Error() + Expect(msg).To(And( + ContainSubstring("usernameSecretRef"), + ContainSubstring("passwordSecretRef"), + ContainSubstring("together"), + )) + }) + }) +}) diff --git a/test/openshift/e2e/ginkgo/parallel/1-126_validate_servicemonitor_metrics_config_test.go b/test/openshift/e2e/ginkgo/parallel/1-126_validate_servicemonitor_metrics_config_test.go new file mode 100644 index 00000000000..6abffd98a13 --- /dev/null +++ b/test/openshift/e2e/ginkgo/parallel/1-126_validate_servicemonitor_metrics_config_test.go @@ -0,0 +1,412 @@ +/* +Copyright 2026. + +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 parallel + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + + argov1beta1api "github.com/argoproj-labs/argocd-operator/api/v1beta1" + "github.com/argoproj-labs/argocd-operator/tests/ginkgo/fixture" + argocdFixture "github.com/argoproj-labs/argocd-operator/tests/ginkgo/fixture/argocd" + k8sFixture "github.com/argoproj-labs/argocd-operator/tests/ginkgo/fixture/k8s" + fixtureUtils "github.com/argoproj-labs/argocd-operator/tests/ginkgo/fixture/utils" +) + +var _ = Describe("GitOps Operator Parallel E2E Tests", func() { + + Context("1-126_validate_servicemonitor_metrics_config", func() { + + var ( + k8sClient client.Client + ctx context.Context + ) + + BeforeEach(func() { + fixture.EnsureParallelCleanSlate() + k8sClient, _ = fixtureUtils.GetE2ETestKubeClient() + ctx = context.Background() + }) + + It("verifies per-component metrics config is applied to ServiceMonitors", func() { + + ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer cleanupFunc() + + controllerSMName := "example-argocd-metrics" + repoSMName := "example-argocd-repo-server-metrics" + serverSMName := "example-argocd-server-metrics" + notifSMName := "example-argocd-notifications-controller-metrics" + + By("Case 1: Create with both interval and scrapeTimeout on all components") + + By("creating ArgoCD instance with metrics config on all components") + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-argocd", + Namespace: ns.Name, + }, + Spec: argov1beta1api.ArgoCDSpec{ + Prometheus: argov1beta1api.ArgoCDPrometheusSpec{ + Enabled: true, + }, + Controller: argov1beta1api.ArgoCDApplicationControllerSpec{ + Metrics: &argov1beta1api.ArgoCDMetricsSpec{ + Interval: "30s", + ScrapeTimeout: "10s", + }, + }, + Repo: argov1beta1api.ArgoCDRepoSpec{ + Metrics: &argov1beta1api.ArgoCDMetricsSpec{ + Interval: "45s", + ScrapeTimeout: "15s", + }, + }, + Server: argov1beta1api.ArgoCDServerSpec{ + Metrics: &argov1beta1api.ArgoCDMetricsSpec{ + Interval: "60s", + ScrapeTimeout: "20s", + }, + }, + Notifications: argov1beta1api.ArgoCDNotifications{ + Enabled: true, + Metrics: &argov1beta1api.ArgoCDMetricsSpec{ + Interval: "90s", + ScrapeTimeout: "30s", + }, + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("waiting for ArgoCD CR to be available") + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + By("verifying controller ServiceMonitor") + controllerSM := &monitoringv1.ServiceMonitor{ + ObjectMeta: metav1.ObjectMeta{Name: controllerSMName, Namespace: ns.Name}, + } + Eventually(controllerSM, "2m", "5s").Should(k8sFixture.ExistByName()) + Expect(controllerSM.Spec.Endpoints).To(HaveLen(1)) + Expect(controllerSM.Spec.Endpoints[0].Interval).To(Equal(monitoringv1.Duration("30s"))) + Expect(controllerSM.Spec.Endpoints[0].ScrapeTimeout).To(Equal(monitoringv1.Duration("10s"))) + + By("verifying repo-server ServiceMonitor") + repoSM := &monitoringv1.ServiceMonitor{ + ObjectMeta: metav1.ObjectMeta{Name: repoSMName, Namespace: ns.Name}, + } + Eventually(repoSM, "2m", "5s").Should(k8sFixture.ExistByName()) + Expect(repoSM.Spec.Endpoints).To(HaveLen(1)) + Expect(repoSM.Spec.Endpoints[0].Interval).To(Equal(monitoringv1.Duration("45s"))) + Expect(repoSM.Spec.Endpoints[0].ScrapeTimeout).To(Equal(monitoringv1.Duration("15s"))) + + By("verifying server ServiceMonitor") + serverSM := &monitoringv1.ServiceMonitor{ + ObjectMeta: metav1.ObjectMeta{Name: serverSMName, Namespace: ns.Name}, + } + Eventually(serverSM, "2m", "5s").Should(k8sFixture.ExistByName()) + Expect(serverSM.Spec.Endpoints).To(HaveLen(1)) + Expect(serverSM.Spec.Endpoints[0].Interval).To(Equal(monitoringv1.Duration("60s"))) + Expect(serverSM.Spec.Endpoints[0].ScrapeTimeout).To(Equal(monitoringv1.Duration("20s"))) + + By("verifying notifications ServiceMonitor") + notifSM := &monitoringv1.ServiceMonitor{ + ObjectMeta: metav1.ObjectMeta{Name: notifSMName, Namespace: ns.Name}, + } + Eventually(notifSM, "2m", "5s").Should(k8sFixture.ExistByName()) + Expect(notifSM.Spec.Endpoints).To(HaveLen(1)) + Expect(notifSM.Spec.Endpoints[0].Interval).To(Equal(monitoringv1.Duration("90s"))) + Expect(notifSM.Spec.Endpoints[0].ScrapeTimeout).To(Equal(monitoringv1.Duration("30s"))) + + By("Case 2: Update components with new values") + + By("updating metrics config on all components") + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.Controller.Metrics = &argov1beta1api.ArgoCDMetricsSpec{ + Interval: "120s", ScrapeTimeout: "50s", + } + ac.Spec.Repo.Metrics = &argov1beta1api.ArgoCDMetricsSpec{ + Interval: "150s", ScrapeTimeout: "60s", + } + ac.Spec.Server.Metrics = &argov1beta1api.ArgoCDMetricsSpec{ + Interval: "180s", ScrapeTimeout: "70s", + } + ac.Spec.Notifications.Metrics = &argov1beta1api.ArgoCDMetricsSpec{ + Interval: "200s", ScrapeTimeout: "80s", + } + }) + + By("verifying controller ServiceMonitor is updated") + Eventually(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(controllerSM), controllerSM); err != nil { + return false + } + if len(controllerSM.Spec.Endpoints) == 0 { + return false + } + return controllerSM.Spec.Endpoints[0].Interval == monitoringv1.Duration("120s") && + controllerSM.Spec.Endpoints[0].ScrapeTimeout == monitoringv1.Duration("50s") + }, "2m", "5s").Should(BeTrue()) + + By("verifying repo-server ServiceMonitor is updated") + Eventually(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(repoSM), repoSM); err != nil { + return false + } + if len(repoSM.Spec.Endpoints) == 0 { + return false + } + return repoSM.Spec.Endpoints[0].Interval == monitoringv1.Duration("150s") && + repoSM.Spec.Endpoints[0].ScrapeTimeout == monitoringv1.Duration("60s") + }, "2m", "5s").Should(BeTrue()) + + By("verifying server ServiceMonitor is updated") + Eventually(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(serverSM), serverSM); err != nil { + return false + } + if len(serverSM.Spec.Endpoints) == 0 { + return false + } + return serverSM.Spec.Endpoints[0].Interval == monitoringv1.Duration("180s") && + serverSM.Spec.Endpoints[0].ScrapeTimeout == monitoringv1.Duration("70s") + }, "2m", "5s").Should(BeTrue()) + + By("verifying notifications ServiceMonitor is updated") + Eventually(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(notifSM), notifSM); err != nil { + return false + } + if len(notifSM.Spec.Endpoints) == 0 { + return false + } + return notifSM.Spec.Endpoints[0].Interval == monitoringv1.Duration("200s") && + notifSM.Spec.Endpoints[0].ScrapeTimeout == monitoringv1.Duration("80s") + }, "2m", "5s").Should(BeTrue()) + + By("Case 3: Clear metrics from all components") + + By("clearing metrics config from all components") + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.Controller.Metrics = nil + ac.Spec.Repo.Metrics = nil + ac.Spec.Server.Metrics = nil + ac.Spec.Notifications.Metrics = nil + }) + + By("verifying controller ServiceMonitor has empty interval and scrapeTimeout") + Eventually(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(controllerSM), controllerSM); err != nil { + return false + } + if len(controllerSM.Spec.Endpoints) == 0 { + return false + } + return controllerSM.Spec.Endpoints[0].Interval == "" && + controllerSM.Spec.Endpoints[0].ScrapeTimeout == "" + }, "2m", "5s").Should(BeTrue()) + + By("verifying repo-server ServiceMonitor has empty interval and scrapeTimeout") + Eventually(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(repoSM), repoSM); err != nil { + return false + } + if len(repoSM.Spec.Endpoints) == 0 { + return false + } + return repoSM.Spec.Endpoints[0].Interval == "" && + repoSM.Spec.Endpoints[0].ScrapeTimeout == "" + }, "2m", "5s").Should(BeTrue()) + + By("verifying server ServiceMonitor has empty interval and scrapeTimeout") + Eventually(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(serverSM), serverSM); err != nil { + return false + } + if len(serverSM.Spec.Endpoints) == 0 { + return false + } + return serverSM.Spec.Endpoints[0].Interval == "" && + serverSM.Spec.Endpoints[0].ScrapeTimeout == "" + }, "2m", "5s").Should(BeTrue()) + + By("verifying notifications ServiceMonitor has empty interval and scrapeTimeout") + Eventually(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(notifSM), notifSM); err != nil { + return false + } + if len(notifSM.Spec.Endpoints) == 0 { + return false + } + return notifSM.Spec.Endpoints[0].Interval == "" && + notifSM.Spec.Endpoints[0].ScrapeTimeout == "" + }, "2m", "5s").Should(BeTrue()) + }) + + It("verifies principal metrics config is applied to ServiceMonitor", func() { + + ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer cleanupFunc() + + argoCDName := "example-argocd" + principalSMName := argoCDName + "-agent-principal-metrics" + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{ + Name: argoCDName, + Namespace: ns.Name, + }, + Spec: argov1beta1api.ArgoCDSpec{ + Prometheus: argov1beta1api.ArgoCDPrometheusSpec{Enabled: true}, + Controller: argov1beta1api.ArgoCDApplicationControllerSpec{Enabled: ptr.To(false)}, + ArgoCDAgent: &argov1beta1api.ArgoCDAgentSpec{ + Principal: &argov1beta1api.PrincipalSpec{ + Enabled: ptr.To(true), + Metrics: &argov1beta1api.ArgoCDMetricsSpec{ + Interval: "40s", + ScrapeTimeout: "12s", + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + principalSM := &monitoringv1.ServiceMonitor{ + ObjectMeta: metav1.ObjectMeta{Name: principalSMName, Namespace: ns.Name}, + } + Eventually(principalSM, "2m", "5s").Should(k8sFixture.ExistByName()) + Expect(principalSM.Spec.Endpoints).To(HaveLen(1)) + Expect(principalSM.Spec.Endpoints[0].Interval).To(Equal(monitoringv1.Duration("40s"))) + Expect(principalSM.Spec.Endpoints[0].ScrapeTimeout).To(Equal(monitoringv1.Duration("12s"))) + + By("updating principal metrics config") + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.ArgoCDAgent.Principal.Metrics = &argov1beta1api.ArgoCDMetricsSpec{ + Interval: "130s", ScrapeTimeout: "55s", + } + }) + + Eventually(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(principalSM), principalSM); err != nil { + return false + } + if len(principalSM.Spec.Endpoints) == 0 { + return false + } + return principalSM.Spec.Endpoints[0].Interval == monitoringv1.Duration("130s") && + principalSM.Spec.Endpoints[0].ScrapeTimeout == monitoringv1.Duration("55s") + }, "2m", "5s").Should(BeTrue()) + + By("clearing principal metrics config") + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.ArgoCDAgent.Principal.Metrics = nil + }) + + Eventually(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(principalSM), principalSM); err != nil { + return false + } + if len(principalSM.Spec.Endpoints) == 0 { + return false + } + return principalSM.Spec.Endpoints[0].Interval == "" && + principalSM.Spec.Endpoints[0].ScrapeTimeout == "" + }, "2m", "5s").Should(BeTrue()) + }) + + It("verifies agent metrics config is applied to ServiceMonitor", func() { + + ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + defer cleanupFunc() + + argoCDName := "example-argocd" + agentSMName := argoCDName + "-agent-agent-metrics" + + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{ + Name: argoCDName, + Namespace: ns.Name, + }, + Spec: argov1beta1api.ArgoCDSpec{ + Prometheus: argov1beta1api.ArgoCDPrometheusSpec{Enabled: true}, + Controller: argov1beta1api.ArgoCDApplicationControllerSpec{Enabled: ptr.To(false)}, + Server: argov1beta1api.ArgoCDServerSpec{Enabled: ptr.To(false)}, + ArgoCDAgent: &argov1beta1api.ArgoCDAgentSpec{ + Agent: &argov1beta1api.AgentSpec{ + Enabled: ptr.To(true), + Metrics: &argov1beta1api.ArgoCDMetricsSpec{ + Interval: "50s", + ScrapeTimeout: "18s", + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + agentSM := &monitoringv1.ServiceMonitor{ + ObjectMeta: metav1.ObjectMeta{Name: agentSMName, Namespace: ns.Name}, + } + Eventually(agentSM, "2m", "5s").Should(k8sFixture.ExistByName()) + Expect(agentSM.Spec.Endpoints).To(HaveLen(1)) + Expect(agentSM.Spec.Endpoints[0].Interval).To(Equal(monitoringv1.Duration("50s"))) + Expect(agentSM.Spec.Endpoints[0].ScrapeTimeout).To(Equal(monitoringv1.Duration("18s"))) + + By("updating agent metrics config") + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.ArgoCDAgent.Agent.Metrics = &argov1beta1api.ArgoCDMetricsSpec{ + Interval: "160s", ScrapeTimeout: "65s", + } + }) + + Eventually(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(agentSM), agentSM); err != nil { + return false + } + if len(agentSM.Spec.Endpoints) == 0 { + return false + } + return agentSM.Spec.Endpoints[0].Interval == monitoringv1.Duration("160s") && + agentSM.Spec.Endpoints[0].ScrapeTimeout == monitoringv1.Duration("65s") + }, "2m", "5s").Should(BeTrue()) + + By("clearing agent metrics config") + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.ArgoCDAgent.Agent.Metrics = nil + }) + + Eventually(func() bool { + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(agentSM), agentSM); err != nil { + return false + } + if len(agentSM.Spec.Endpoints) == 0 { + return false + } + return agentSM.Spec.Endpoints[0].Interval == "" && + agentSM.Spec.Endpoints[0].ScrapeTimeout == "" + }, "2m", "5s").Should(BeTrue()) + }) + }) +}) diff --git a/test/openshift/e2e/ginkgo/parallel/1-131_validate_rolebinding_no_tracking_annotation_propagation_test.go b/test/openshift/e2e/ginkgo/parallel/1-131_validate_rolebinding_no_tracking_annotation_propagation_test.go new file mode 100644 index 00000000000..e6b986888a0 --- /dev/null +++ b/test/openshift/e2e/ginkgo/parallel/1-131_validate_rolebinding_no_tracking_annotation_propagation_test.go @@ -0,0 +1,118 @@ +/* +Copyright 2025. + +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 parallel + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + argov1beta1api "github.com/argoproj-labs/argocd-operator/api/v1beta1" + "github.com/argoproj-labs/argocd-operator/common" + "github.com/argoproj-labs/argocd-operator/tests/ginkgo/fixture" + k8sFixture "github.com/argoproj-labs/argocd-operator/tests/ginkgo/fixture/k8s" + fixtureUtils "github.com/argoproj-labs/argocd-operator/tests/ginkgo/fixture/utils" +) + +// When an ArgoCD CR is itself managed by another Argo CD instance, it +// carries Argo CD resource-tracking annotations (e.g. argocd.argoproj.io/tracking-id). The +// operator should not propagate those annotations onto the RoleBindings it creates in managed +// namespaces, otherwise the central Argo CD treats those RoleBindings as owned resources and +// prunes them. +var _ = Describe("GitOps Operator Parallel E2E Tests", func() { + + Context("1-131_validate_rolebinding_no_tracking_annotation_propagation", func() { + + const ( + trackingIDAnnotation = "argocd.argoproj.io/tracking-id" + installationIDAnnotation = "argocd.argoproj.io/installation-id" + ) + + var ( + k8sClient client.Client + ctx context.Context + ) + + BeforeEach(func() { + fixture.EnsureParallelCleanSlate() + k8sClient, _ = fixtureUtils.GetE2ETestKubeClient() + ctx = context.Background() + }) + + It("does not propagate Argo CD tracking annotations from the ArgoCD CR to RoleBindings created in managed namespaces", func() { + + By("creating a namespace to contain the Argo CD instance") + argoCDNS, cleanupFunc := fixture.CreateNamespaceWithCleanupFunc("appteam-argocd-1-131") + defer cleanupFunc() + + By("creating a managed namespace where the operator will create RoleBindings") + managedNS, cleanupFunc := fixture.CreateManagedNamespaceWithCleanupFunc("appteam-apps-1-131", argoCDNS.Name) + defer cleanupFunc() + + By("creating an ArgoCD instance carrying tracking annotations, as if deployed by a central Argo CD") + argoCD := &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{ + Name: "appteam", + Namespace: argoCDNS.Name, + Annotations: map[string]string{ + trackingIDAnnotation: "central-gitops:argoproj.io/ArgoCD:central/appteam", + installationIDAnnotation: "central-argocd", + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + rbNames := []string{"appteam-argocd-application-controller", "appteam-argocd-server"} + + By("verifying the operator created the expected RoleBindings in the managed namespace") + for _, rbName := range rbNames { + roleBinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: rbName, Namespace: managedNS.Name}, + } + Eventually(roleBinding, "60s", "5s").Should(k8sFixture.ExistByName()) + } + + By("verifying the RoleBindings did NOT inherit the Argo CD tracking annotations from the CR") + for _, rbName := range rbNames { + roleBinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: rbName, Namespace: managedNS.Name}, + } + // The operator's own default annotation should be present + Eventually(roleBinding).Should(k8sFixture.HaveAnnotationWithValue(common.AnnotationName, argoCD.Name)) + // Central Argo CD's tracking annotations are not present. + Consistently(roleBinding, "15s", "3s").Should(k8sFixture.NotHaveAnnotation(trackingIDAnnotation)) + Consistently(roleBinding, "5s", "1s").Should(k8sFixture.NotHaveAnnotation(installationIDAnnotation)) + } + + By("verifying the RoleBindings in the Argo CD's own namespace also did NOT inherit the tracking annotations") + for _, rbName := range rbNames { + roleBinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: rbName, Namespace: argoCDNS.Name}, + } + Eventually(roleBinding, "60s", "5s").Should(k8sFixture.ExistByName()) + Eventually(roleBinding).Should(k8sFixture.HaveAnnotationWithValue(common.AnnotationName, argoCD.Name)) + Consistently(roleBinding, "5s", "1s").Should(k8sFixture.NotHaveAnnotation(trackingIDAnnotation)) + Consistently(roleBinding, "5s", "1s").Should(k8sFixture.NotHaveAnnotation(installationIDAnnotation)) + } + }) + + }) +}) diff --git a/test/openshift/e2e/ginkgo/sequential/1-037_validate_applicationset_in_any_namespace_test.go b/test/openshift/e2e/ginkgo/sequential/1-037_validate_applicationset_in_any_namespace_test.go index 55d11bdbe50..ef2e8c65d4a 100644 --- a/test/openshift/e2e/ginkgo/sequential/1-037_validate_applicationset_in_any_namespace_test.go +++ b/test/openshift/e2e/ginkgo/sequential/1-037_validate_applicationset_in_any_namespace_test.go @@ -47,7 +47,7 @@ var _ = Describe("GitOps Operator Sequential E2E Tests", func() { fixture.EnsureSequentialCleanSlate() fixture.SetEnvInOperatorSubscriptionOrDeployment("ARGOCD_CLUSTER_CONFIG_NAMESPACES", - "openshift-gitops, argocd-e2e-cluster-config, appset-argocd, appset-old-ns, appset-new-ns, appset-argocd-clusterrole, appset-target-ns") + "openshift-gitops, argocd-e2e-cluster-config, appset-argocd, appset-old-ns, appset-new-ns, appset-argocd-clusterrole, appset-target-ns, appset-argocd-err, appset-target-err") k8sClient, _ = utils.GetE2ETestKubeClient() ctx = context.Background() @@ -56,7 +56,8 @@ var _ = Describe("GitOps Operator Sequential E2E Tests", func() { AfterEach(func() { fixture.OutputDebugOnFail("appset-argocd", "appset-old-ns", "appset-new-ns", "appset-namespace-scoped", "target-ns-1-037", - "team-1", "team-2", "team-frontend", "team-backend", "team-3", "other-ns", "appset-argocd-clusterrole", "appset-target-ns") + "team-1", "team-2", "team-frontend", "team-backend", "team-3", "other-ns", "appset-argocd-clusterrole", "appset-target-ns", + "appset-argocd-err", "appset-target-err") // Clean up namespaces created for _, namespaceCleanupFunction := range cleanupFunctions { @@ -602,6 +603,106 @@ var _ = Describe("GitOps Operator Sequential E2E Tests", func() { }) + It("verifies that ApplicationSet reconcile is blocked when namespace list fails and retry adds --applicationset-namespaces after RBAC restore", func() { + if fixture.EnvLocalRun() { + Skip("Skipping RBAC test for LOCAL_RUN - operator runs locally without cluster ClusterRole") + } + + By("finding operator deployment") + operatorDeployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "openshift-gitops-operator-controller-manager", + Namespace: "openshift-gitops-operator", + }, + } + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(operatorDeployment), operatorDeployment); err != nil { + var nsList corev1.NamespaceList + Expect(k8sClient.List(ctx, &nsList)).To(Succeed()) + found := false + for i := range nsList.Items { + dep := &appsv1.Deployment{} + if getErr := k8sClient.Get(ctx, client.ObjectKey{Namespace: nsList.Items[i].Name, Name: "openshift-gitops-operator-controller-manager"}, dep); getErr == nil { + operatorDeployment = dep + found = true + break + } + } + if !found { + Skip("Operator deployment not found - test requires operator running in cluster") + } + } + + By("revoking operator list namespaces permission from every bound operator ClusterRole") + saName := operatorDeployment.Spec.Template.Spec.ServiceAccountName + if saName == "" { + saName = "default" + } + operatorClusterRoleSnapshots := snapshotsForOperatorNamespacesListPermission( + ctx, k8sClient, operatorDeployment.Namespace, saName, fixture.EnvNonOLM()) + if len(operatorClusterRoleSnapshots) == 0 { + Skip("No operator-managed ClusterRole with namespaces list permission found") + } + + var revokedClusterRoleSnapshots []operatorClusterRoleRulesSnapshot + defer func() { + if len(revokedClusterRoleSnapshots) == 0 { + return + } + restoreOperatorClusterRoleRulesSnapshots(revokedClusterRoleSnapshots) + }() + for _, snapshot := range operatorClusterRoleSnapshots { + clusterRole := &rbacv1.ClusterRole{ObjectMeta: metav1.ObjectMeta{Name: snapshot.name}} + modifiedRules := snapshot.modifiedRules + clusterroleFixture.Update(clusterRole, func(cr *rbacv1.ClusterRole) { cr.Rules = modifiedRules }) + revokedClusterRoleSnapshots = append(revokedClusterRoleSnapshots, snapshot) + } + + By("creating Argo CD with ApplicationSet and source namespaces") + argocdNS, cleanupArgocd := fixture.CreateNamespaceWithCleanupFunc("appset-argocd-err") + cleanupFunctions = append(cleanupFunctions, cleanupArgocd) + targetNS, cleanupTarget := fixture.CreateNamespaceWithCleanupFunc("appset-target-err") + cleanupFunctions = append(cleanupFunctions, cleanupTarget) + argoCD := &v1beta1.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example", + Namespace: argocdNS.Name, + }, + Spec: v1beta1.ArgoCDSpec{ + SourceNamespaces: []string{targetNS.Name}, + ApplicationSet: &v1beta1.ArgoCDApplicationSet{ + SourceNamespaces: []string{targetNS.Name}, + }, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + appsetDeployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-applicationset-controller", + Namespace: argoCD.Namespace, + }, + } + + By("verifying ApplicationSet deployment is not created while namespace list fails (blocked reconcile)") + Consistently(appsetDeployment, "30s", "2s").Should(k8sFixture.NotExistByName()) + + By("restoring operator list namespaces permission") + restoreOperatorClusterRoleRulesSnapshots(revokedClusterRoleSnapshots) + revokedClusterRoleSnapshots = nil + + By("triggering Argo CD reconcile after RBAC restore") + argocdFixture.Update(argoCD, func(ac *v1beta1.ArgoCD) { + if ac.Annotations == nil { + ac.Annotations = map[string]string{} + } + ac.Annotations["e2e-test-reconcile-trigger"] = "true" + }) + + By("verifying retry creates deployment with --applicationset-namespaces") + Eventually(appsetDeployment, "2m", "5s").Should(k8sFixture.ExistByName()) + Eventually(appsetDeployment).Should(deploymentFixture.HaveContainerCommandSubstring("--applicationset-namespaces "+targetNS.Name, 0)) + }) + It("verifies that ArgoCD sourcenamespaces resources are cleaned up automatically", func() { By("creating Argo CD namespace and appset source namespace") @@ -1282,3 +1383,143 @@ var _ = Describe("GitOps Operator Sequential E2E Tests", func() { }) }) + +const nonOLMManagerClusterRoleName = "manager-role" + +type operatorClusterRoleRulesSnapshot struct { + name string + originalRules []rbacv1.PolicyRule + modifiedRules []rbacv1.PolicyRule +} + +func removeListVerbFromNamespacesRules(rules []rbacv1.PolicyRule) ([]rbacv1.PolicyRule, bool) { + var modifiedRules []rbacv1.PolicyRule + removedListFromNamespaces := false + + for _, rule := range rules { + var namespaceResources, otherResources []string + for _, resource := range rule.Resources { + if resource == "namespaces" { + namespaceResources = append(namespaceResources, resource) + } else { + otherResources = append(otherResources, resource) + } + } + if len(namespaceResources) == 0 { + modifiedRules = append(modifiedRules, rule) + continue + } + + hasList := false + for _, verb := range rule.Verbs { + if verb == "list" { + hasList = true + break + } + } + if !hasList { + modifiedRules = append(modifiedRules, rule) + continue + } + removedListFromNamespaces = true + + var namespaceVerbs []string + for _, verb := range rule.Verbs { + if verb != "list" { + namespaceVerbs = append(namespaceVerbs, verb) + } + } + if len(namespaceVerbs) > 0 { + modifiedRules = append(modifiedRules, rbacv1.PolicyRule{ + APIGroups: rule.APIGroups, + Resources: []string{"namespaces"}, + Verbs: namespaceVerbs, + ResourceNames: rule.ResourceNames, + NonResourceURLs: rule.NonResourceURLs, + }) + } + if len(otherResources) > 0 { + modifiedRules = append(modifiedRules, rbacv1.PolicyRule{ + APIGroups: rule.APIGroups, + Resources: otherResources, + Verbs: rule.Verbs, + ResourceNames: rule.ResourceNames, + NonResourceURLs: rule.NonResourceURLs, + }) + } + } + + return modifiedRules, removedListFromNamespaces +} + +func isOperatorManagedClusterRole(clusterRole *rbacv1.ClusterRole, nonOLMInstall bool) bool { + if nonOLMInstall { + return clusterRole.Name == nonOLMManagerClusterRoleName + } + + return clusterRole.Labels["olm.managed"] == "true" && + strings.HasPrefix(clusterRole.Labels["olm.owner"], "openshift-gitops-operator") +} + +func snapshotsForOperatorNamespacesListPermission(ctx context.Context, k8sClient client.Client, serviceAccountNamespace, serviceAccountName string, nonOLMInstall bool) []operatorClusterRoleRulesSnapshot { + var crbList rbacv1.ClusterRoleBindingList + Expect(k8sClient.List(ctx, &crbList)).To(Succeed()) + + seenClusterRoles := map[string]bool{} + var snapshots []operatorClusterRoleRulesSnapshot + + for i := range crbList.Items { + crb := &crbList.Items[i] + if crb.RoleRef.Kind != "ClusterRole" { + continue + } + if seenClusterRoles[crb.RoleRef.Name] { + continue + } + + matchesServiceAccount := false + for _, subject := range crb.Subjects { + if subject.Kind == rbacv1.ServiceAccountKind && + subject.Namespace == serviceAccountNamespace && + subject.Name == serviceAccountName { + matchesServiceAccount = true + break + } + } + if !matchesServiceAccount { + continue + } + + clusterRole := &rbacv1.ClusterRole{ObjectMeta: metav1.ObjectMeta{Name: crb.RoleRef.Name}} + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(clusterRole), clusterRole); err != nil { + continue + } + if !isOperatorManagedClusterRole(clusterRole, nonOLMInstall) { + continue + } + + modifiedRules, removedListFromNamespaces := removeListVerbFromNamespacesRules(clusterRole.Rules) + if !removedListFromNamespaces { + continue + } + + originalRules := make([]rbacv1.PolicyRule, len(clusterRole.Rules)) + copy(originalRules, clusterRole.Rules) + snapshots = append(snapshots, operatorClusterRoleRulesSnapshot{ + name: clusterRole.Name, + originalRules: originalRules, + modifiedRules: modifiedRules, + }) + seenClusterRoles[clusterRole.Name] = true + } + + return snapshots +} + +func restoreOperatorClusterRoleRulesSnapshots(snapshots []operatorClusterRoleRulesSnapshot) { + for _, snapshot := range snapshots { + clusterRole := &rbacv1.ClusterRole{ObjectMeta: metav1.ObjectMeta{Name: snapshot.name}} + originalRules := snapshot.originalRules + clusterroleFixture.Update(clusterRole, func(cr *rbacv1.ClusterRole) { cr.Rules = originalRules }) + } +} diff --git a/test/openshift/e2e/ginkgo/sequential/1-051_validate_argocd_agent_principal_test.go b/test/openshift/e2e/ginkgo/sequential/1-051_validate_argocd_agent_principal_test.go index d602471edfc..6af261cbe11 100644 --- a/test/openshift/e2e/ginkgo/sequential/1-051_validate_argocd_agent_principal_test.go +++ b/test/openshift/e2e/ginkgo/sequential/1-051_validate_argocd_agent_principal_test.go @@ -23,6 +23,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" routev1 "github.com/openshift/api/route/v1" + monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" @@ -913,5 +914,54 @@ var _ = Describe("GitOps Operator Sequential E2E Tests", func() { Eventually(principalNetworkPolicy).Should(k8sFixture.NotExistByName()) }) + + It("should create and delete principal ServiceMonitor based on prometheus enabled flag", func() { + + By("Create ArgoCD instance with principal enabled and prometheus enabled") + + argoCD.Spec.Prometheus.Enabled = true + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("Verify expected resources are created for principal pod") + + verifyExpectedResourcesExist(ns) + + By("Verify principal ServiceMonitor is created") + + principalServiceMonitor := &monitoringv1.ServiceMonitor{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-agent-principal-metrics", argoCDName), + Namespace: ns.Name, + }, + } + Eventually(principalServiceMonitor, "2m", "2s").Should(k8sFixture.ExistByName()) + + By("Disable prometheus and verify ServiceMonitor is deleted") + + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: argoCDName, Namespace: ns.Name}, argoCD)).To(Succeed()) + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.Prometheus.Enabled = false + }) + + Eventually(principalServiceMonitor, "2m", "2s").Should(k8sFixture.NotExistByName()) + + By("Re-enable prometheus and verify ServiceMonitor is recreated") + + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: argoCDName, Namespace: ns.Name}, argoCD)).To(Succeed()) + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.Prometheus.Enabled = true + }) + + Eventually(principalServiceMonitor, "2m", "2s").Should(k8sFixture.ExistByName()) + + By("Disable principal and verify ServiceMonitor is deleted") + + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: argoCDName, Namespace: ns.Name}, argoCD)).To(Succeed()) + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.ArgoCDAgent.Principal.Enabled = ptr.To(false) + }) + + Eventually(principalServiceMonitor, "2m", "2s").Should(k8sFixture.NotExistByName()) + }) }) }) diff --git a/test/openshift/e2e/ginkgo/sequential/1-052_validate_argocd_agent_agent_test.go b/test/openshift/e2e/ginkgo/sequential/1-052_validate_argocd_agent_agent_test.go index 0ff34a638ae..4f09dd428ee 100644 --- a/test/openshift/e2e/ginkgo/sequential/1-052_validate_argocd_agent_agent_test.go +++ b/test/openshift/e2e/ginkgo/sequential/1-052_validate_argocd_agent_agent_test.go @@ -22,6 +22,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -584,5 +585,54 @@ var _ = Describe("GitOps Operator Sequential E2E Tests", func() { return err != nil }, "60s", "2s").Should(BeTrue(), "ArgoCD should be deleted") }) + + It("should create and delete agent ServiceMonitor based on prometheus enabled flag", func() { + + By("Create ArgoCD instance with agent enabled and prometheus enabled") + + argoCD.Spec.Prometheus.Enabled = true + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("Verify expected resources are created for agent pod") + + verifyExpectedResourcesExist(ns) + + By("Verify agent ServiceMonitor is created") + + agentServiceMonitor := &monitoringv1.ServiceMonitor{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-agent-agent-metrics", argoCDName), + Namespace: ns.Name, + }, + } + Eventually(agentServiceMonitor, "2m", "2s").Should(k8sFixture.ExistByName()) + + By("Disable prometheus and verify ServiceMonitor is deleted") + + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: argoCDName, Namespace: ns.Name}, argoCD)).To(Succeed()) + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.Prometheus.Enabled = false + }) + + Eventually(agentServiceMonitor, "2m", "2s").Should(k8sFixture.NotExistByName()) + + By("Re-enable prometheus and verify ServiceMonitor is recreated") + + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: argoCDName, Namespace: ns.Name}, argoCD)).To(Succeed()) + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.Prometheus.Enabled = true + }) + + Eventually(agentServiceMonitor, "2m", "2s").Should(k8sFixture.ExistByName()) + + By("Disable agent and verify ServiceMonitor is deleted") + + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: argoCDName, Namespace: ns.Name}, argoCD)).To(Succeed()) + argocdFixture.Update(argoCD, func(ac *argov1beta1api.ArgoCD) { + ac.Spec.ArgoCDAgent.Agent.Enabled = ptr.To(false) + }) + + Eventually(agentServiceMonitor, "2m", "2s").Should(k8sFixture.NotExistByName()) + }) }) })