<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>RHACM – TechBlog about OpenShift/Ansible/Satellite and much more</title><link>https://blog.stderr.at/tags/rhacm/</link><description>TechBlog about OpenShift/Ansible/Satellite and much more</description><generator>Hugo 0.164.0</generator><language>en-us</language><copyright>Toni Schmidbauer &amp; Thomas Jungbauer</copyright><lastBuildDate>Sat, 24 Oct 2026 00:00:00 +0200</lastBuildDate><atom:link href="https://blog.stderr.at/tags/rhacm/index.xml" rel="self" type="application/rss+xml"/><item><title>[Ep.20] Managing ACM Policies with OpenShift GitOps</title><link>https://blog.stderr.at/gitopscollection/2026-10-24-acm-policies-with-gitops/</link><guid isPermaLink="true">https://blog.stderr.at/gitopscollection/2026-10-24-acm-policies-with-gitops/</guid><pubDate>Sat, 24 Oct 2026 00:00:00 +0200</pubDate><dc:creator>Articles by Thomas Jungbauer</dc:creator><category>OpenShift</category><category>GitOps</category><category>Security</category><description>Policies as code: let OpenShift GitOps deploy Advanced Cluster Management policies from Git, generate them with the PolicyGenerator, use hub templates for per-cluster values and bootstrap Argo CD on every new cluster with a policy.</description><content:encoded><![CDATA[<div class="paragraph">
<p>In <a href="/gitopscollection/2026-10-20-openshift-gitops-vs-acm/">[Ep.18]</a> I recommended ACM policies for the audited baseline of a cluster and Argo CD for everything that needs rendering. <a href="/gitopscollection/2026-10-23-acm-gitops-push-pull-agent/">[Ep.19]</a> showed how ACM feeds Argo CD with clusters and the different models that can be used in Argo CD. This part turns the integration around: <strong>Argo CD deploys ACM policies</strong>, and a policy <strong>bootstraps Argo CD</strong> on every new cluster.</p>
</div>
<div class="paragraph">
<p>The result is a setup where the policies live in Git, are reviewed in pull requests like any other change, and ACM does what it is good at: distribution, enforcement and compliance reporting.</p>
</div>
<div class="paragraph">
<p>The topic turned out to be too big for a single article, so I have split it into multiple parts and maybe I should have split into even more. This is the third:</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p><a href="/gitopscollection/2026-10-20-openshift-gitops-vs-acm/">[Ep.18] OpenShift GitOps or Advanced Cluster Management? Choosing the right tool</a> What each tool does, where they overlap and a decision guide.</p>
</li>
<li>
<p><a href="/gitopscollection/2026-10-23-acm-gitops-push-pull-agent/">[Ep.19] ACM and OpenShift GitOps: push, pull and the Argo CD Agent</a>: how ACM registers clusters in Argo CD, and how to set up each of the three delivery models.</p>
</li>
<li>
<p><strong>[Ep.20] Managing ACM Policies with OpenShift GitOps</strong> (this article): policies as code, the PolicyGenerator, hub templates and bootstrapping Argo CD with a policy.</p>
</li>
</ol>
</div>
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
Everything in this series was written against <strong>OpenShift GitOps 1.22</strong> (Argo CD 3.5) and <strong>Advanced Cluster Management 2.17</strong> (Open Cluster Management). Technology Preview and GA states move quickly in both products, so check the release notes of your versions before you commit to a design.
</td>
</tr>
</tbody></table>
</div>
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
While I am using the term &#34;Advanced Cluster Management&#34; in this article, I am referring to the product formerly known as Open Cluster Management. Same counts for Argo CD and OpenShift GitOps.
</td>
</tr>
</tbody></table>
</div>
<div class="sect1">
<h2 id="_three_integration_patterns">Three integration patterns</h2>
<div class="sectionbody">
<div class="paragraph">
<p>There are three ways to combine policies and Argo CD. They are not exclusive, most mature setups combine the first two.</p>
</div>
<table class="tableblock frame-all grid-all stretch">
<colgroup>
<col style="width: 12.5%;"/>
<col style="width: 37.5%;"/>
<col style="width: 25%;"/>
<col style="width: 25%;"/>
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top">Pattern</th>
<th class="tableblock halign-left valign-top">How it works</th>
<th class="tableblock halign-left valign-top">Strength</th>
<th class="tableblock halign-left valign-top">Weakness</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>A: Argo CD deploys policies</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Policies live in Git, generated by the PolicyGenerator. The hub Argo CD syncs them to the hub, ACM propagates and evaluates them.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Policy as code with review, compliance dashboard in ACM</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Needs the PolicyGenerator plugin in Argo CD</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>B: A policy bootstraps Argo CD</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">One enforced policy installs OpenShift GitOps and a root Application on every placed cluster. From there, Argo CD takes over.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">New clusters configure themselves on import</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">The bootstrap needs proper planning and automation.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>C: Policies configure directly</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">ConfigurationPolicies apply the cluster configuration themselves. No Argo CD on the spokes.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">One tool, compliance built in</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Weaker diff and ordering, large YAML in policies</p></td>
</tr>
</tbody>
</table>
<div class="paragraph">
<p>Pattern C is fine for small, compliance-driven setups. For everything else I recommend A for governance and B for the fleet bootstrap. The rest of this article shows both.</p>
</div>
<div class="paragraph">
<p>Whatever option you choose, one rule from <a href="/gitopscollection/2026-10-20-openshift-gitops-vs-acm/">Ep.18</a> still applies: <strong>one owner per object</strong>. If Argo CD manages an object, a policy may only look at it in <code>inform</code> mode.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_preparing_the_hub_argo_cd">Preparing the hub Argo CD</h2>
<div class="sectionbody">
<div class="paragraph">
<p>You need an Argo CD instance on the hub that can sync to the hub itself. That instance needs three things before it can deploy policies:</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>permissions for the policy API,</p>
</li>
<li>
<p>the PolicyGenerator plugin,</p>
</li>
<li>
<p>optionally, health checks for policies.</p>
</li>
</ol>
</div>
<div class="sect2">
<h3 id="_a_second_argo_cd_instance_for_the_hub">A second Argo CD instance for the hub</h3>
<div class="paragraph">
<p>In Ep.19, the default instance <code>openshift-gitops</code> became the principal of the Argo CD Agent. Its application-controller is disabled, so it cannot sync anything to the hub itself, and the manifests of agent Applications are rendered on the managed clusters anyway. The policies, however, must be created on the hub.</p>
</div>
<div class="paragraph">
<p>This article therefore uses a second, small Argo CD instance on the hub that only takes care of the hub configuration: <code>policy-gitops</code> in the namespace of the same name. If your default instance still has its application-controller (push or pull model), you can use it instead. Replace <code>policy-gitops</code> with <code>openshift-gitops</code> in the examples below.</p>
</div>
<div class="listingblock linenums">
<div class="title">Second Argo CD instance for the hub configuration</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: v1
kind: Namespace
metadata:
  name: policy-gitops
---
apiVersion: argoproj.io/v1beta1
kind: ArgoCD
metadata:
  name: policy-gitops <i class="conum" data-value="1"></i><b>(1)</b>
  namespace: policy-gitops
spec: {} <i class="conum" data-value="2"></i><b>(2)</b>
---
apiVersion: v1
kind: Namespace
metadata:
  name: acm-policies
  labels:
    argocd.argoproj.io/managed-by: policy-gitops <i class="conum" data-value="3"></i><b>(3)</b></code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>The service account of the application-controller is named after the instance: <code>policy-gitops-argocd-application-controller</code>.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>The PolicyGenerator plugin is added in the next sections.</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td><code>policy-gitops</code> is a namespace-scoped instance. The label allows it to manage the namespace <code>acm-policies</code>, where the policies live. The namespace must exist before the first sync, because the instance cannot create namespaces itself.</td>
</tr>
</tbody></table>
</div>
</div>
<div class="sect2">
<h3 id="_permissions">Permissions</h3>
<div class="paragraph">
<p>The application-controller of the instance cannot create policies and Placements. Create a ClusterRole and bind it to the controller’s service account:</p>
</div>
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
Not required if Argo CD runs as cluster-admin.
</td>
</tr>
</tbody></table>
</div>
<div class="listingblock linenums">
<div class="title">ClusterRole and ClusterRoleBinding for the hub Argo CD</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: openshift-gitops-policy-admin
rules:
  - apiGroups:
      - policy.open-cluster-management.io
    resources:
      - policies
      - configurationpolicies
      - certificatepolicies
      - operatorpolicies
      - policysets
      - placementbindings
    verbs: [&#34;get&#34;, &#34;list&#34;, &#34;watch&#34;, &#34;create&#34;, &#34;update&#34;, &#34;patch&#34;, &#34;delete&#34;]
  - apiGroups:
      - cluster.open-cluster-management.io
    resources:
      - placements
      - placements/status
      - placementdecisions
      - placementdecisions/status
    verbs: [&#34;get&#34;, &#34;list&#34;, &#34;watch&#34;, &#34;create&#34;, &#34;update&#34;, &#34;patch&#34;, &#34;delete&#34;]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: policy-gitops-policy-admin
subjects:
  - kind: ServiceAccount
    name: policy-gitops-argocd-application-controller
    namespace: policy-gitops
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: openshift-gitops-policy-admin</code></pre>
</div>
</div>
<div class="paragraph">
<p>The ClusterRole in the ACM documentation also contains <code>placementrules</code>. I left them out on purpose: PlacementRules are deprecated.</p>
</div>
</div>
<div class="sect2">
<h3 id="_the_policygenerator_plugin">The PolicyGenerator plugin</h3>
<div class="paragraph">
<p>Creating the policy YAML by hand is tough and error-prone. Every manifest has to be wrapped in a ConfigurationPolicy, which is wrapped in a Policy, which needs a Placement and a PlacementBinding. The <strong>PolicyGenerator</strong> solves this. It is a Kustomize generator plugin that takes plain manifests and wraps them for you.</p>
</div>
<div class="paragraph">
<p>The plugin is not part of the OpenShift GitOps images. An init container copies it from an ACM image into the repo-server, and Kustomize must be allowed to run alpha plugins.</p>
</div>
<div class="paragraph">
<p>The setup is described in the ACM documentation: <a href="https://docs.redhat.com/en/documentation/red_hat_advanced_cluster_management_for_kubernetes/2.17/html/gitops/gitops-overview#access-policy-gen-gitops" rel="noopener" target="_blank">ACM 2.17: Accessing the Policy Generator from OpenShift GitOps</a>.</p>
</div>
<div class="listingblock linenums">
<div class="title">ArgoCD resource on the hub with the PolicyGenerator plugin (excerpt)</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: argoproj.io/v1beta1
kind: ArgoCD
metadata:
  name: policy-gitops
  namespace: policy-gitops
spec:
  kustomizeBuildOptions: --enable-alpha-plugins <i class="conum" data-value="1"></i><b>(1)</b>
  repo:
    env:
      - name: KUSTOMIZE_PLUGIN_HOME
        value: /etc/kustomize/plugin
    initContainers:
      - name: policy-generator-install
        image: registry.redhat.io/rhacm2/multicluster-operators-subscription-rhel9:v2.17 <i class="conum" data-value="2"></i><b>(2)</b>
        command:
          - /bin/bash
        args:
          - -c
          - cp /policy-generator/PolicyGenerator-not-fips-compliant /policy-generator-tmp/PolicyGenerator <i class="conum" data-value="3"></i><b>(3)</b>
        volumeMounts:
          - mountPath: /policy-generator-tmp
            name: policy-generator
    volumeMounts:
      - mountPath: /etc/kustomize/plugin/policy.open-cluster-management.io/v1/policygenerator
        name: policy-generator
    volumes:
      - name: policy-generator
        emptyDir: {}</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Allows Kustomize to execute the generator plugin.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>The tag must match the minor version of your ACM hub (<code>oc get multiclusterhub -n open-cluster-management -o jsonpath=&#39;{.items[0].status.currentVersion}&#39;</code>). Update it after every hub upgrade, or let a policy do it (see <a href="#_keeping_the_image_tag_in_sync">Keeping the image tag in sync</a>).</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>Copies the plugin from the image to the plugin directory.</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>If your policy folders contain Helm charts that should be rendered before the policies are generated, add the environment variable <code>POLICY_GEN_ENABLE_HELM</code> with the value <code>&#34;true&#34;</code> to <code>spec.repo.env</code>.</p>
</div>
<div class="sect3">
<h4 id="_keeping_the_image_tag_in_sync">Keeping the image tag in sync</h4>
<div class="paragraph">
<p>A fixed tag has one weakness: after a hub upgrade, the PolicyGenerator in the repo-server is still the old one until somebody updates the tag. The ACM documentation provides an example of a policy that keeps the image tag in sync. After an upgrade of the hub, the policy changes the image of the init container automatically.</p>
</div>
<div class="paragraph">
<p>The policy runs on the hub only, so it needs a Placement for <code>local-cluster</code>. The hub carries the label <code>local-cluster=true</code>, and it is part of the cluster set <code>global</code>, which must be bound to the policy namespace:</p>
</div>
<div class="listingblock linenums">
<div class="title">Policy that keeps the PolicyGenerator image in sync with the hub</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: cluster.open-cluster-management.io/v1beta2
kind: ManagedClusterSetBinding
metadata:
  name: global
  namespace: acm-policies
spec:
  clusterSet: global <i class="conum" data-value="1"></i><b>(1)</b>
---
apiVersion: policy.open-cluster-management.io/v1
kind: Policy
metadata:
  name: argocd-policy-generator
  namespace: acm-policies
  annotations:
    policy.open-cluster-management.io/standards: NIST SP 800-53
    policy.open-cluster-management.io/categories: CM Configuration Management
    policy.open-cluster-management.io/controls: CM-2 Baseline Configuration
spec:
  remediationAction: enforce <i class="conum" data-value="2"></i><b>(2)</b>
  disabled: false
  policy-templates:
    - objectDefinition:
        apiVersion: policy.open-cluster-management.io/v1
        kind: ConfigurationPolicy
        metadata:
          name: argocd-policy-generator
        spec:
          severity: medium
          object-templates:
            - complianceType: musthave
              objectDefinition:
                apiVersion: argoproj.io/v1beta1
                kind: ArgoCD
                metadata:
                  name: policy-gitops
                  namespace: policy-gitops
                spec:
                  kustomizeBuildOptions: --enable-alpha-plugins
                  repo:
                    env:
                      - name: KUSTOMIZE_PLUGIN_HOME
                        value: /etc/kustomize/plugin
                    initContainers:
                      - name: policy-generator-install
                        image: &#39;{{ (index (lookup &#34;apps/v1&#34; &#34;Deployment&#34; &#34;open-cluster-management&#34; &#34;multicluster-operators-hub-subscription&#34;).spec.template.spec.containers 0).image }}&#39; <i class="conum" data-value="3"></i><b>(3)</b>
                        command:
                          - /bin/bash
                        args:
                          - -c
                          - cp /policy-generator/PolicyGenerator-not-fips-compliant /policy-generator-tmp/PolicyGenerator
                        volumeMounts:
                          - mountPath: /policy-generator-tmp
                            name: policy-generator
                    volumeMounts:
                      - mountPath: /etc/kustomize/plugin/policy.open-cluster-management.io/v1/policygenerator
                        name: policy-generator
                    volumes:
                      - name: policy-generator
                        emptyDir: {}
---
apiVersion: cluster.open-cluster-management.io/v1beta1
kind: Placement
metadata:
  name: hub-only
  namespace: acm-policies
spec:
  clusterSets:
    - global
  predicates:
    - requiredClusterSelector:
        labelSelector:
          matchLabels:
            local-cluster: &#34;true&#34; <i class="conum" data-value="4"></i><b>(4)</b>
---
apiVersion: policy.open-cluster-management.io/v1
kind: PlacementBinding
metadata:
  name: argocd-policy-generator
  namespace: acm-policies
placementRef:
  apiGroup: cluster.open-cluster-management.io
  kind: Placement
  name: hub-only
subjects:
  - apiGroup: policy.open-cluster-management.io
    kind: Policy
    name: argocd-policy-generator</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>The cluster set <code>global</code> contains every managed cluster, including the hub. The Placement below narrows it down to the hub. Creating a ManagedClusterSetBinding requires the permissions <code>managedclustersetbindings</code> and <code>managedclustersets/bind</code>, which are not part of the ClusterRole above. Create it once as cluster administrator, or extend the ClusterRole if Argo CD shall sync it.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>We enforce this policy.</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td><code>lookup</code> reads the deployment of the ACM subscription controller, and the template takes the image of its first container. That is exactly the image the init container needs, in the version of the running hub.</td>
</tr>
<tr>
<td><i class="conum" data-value="4"></i><b>4</b></td>
<td>ACM sets this label on the hub’s own ManagedCluster.</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>Two things to keep in mind:</p>
</div>
<div class="ulist">
<ul>
<li>
<p><strong>Write this policy as plain YAML</strong>, not as PolicyGenerator input. It installs the generator, so it cannot depend on it (hen-egg problem). Argo CD can sync it from Git without the plugin.</p>
</li>
<li>
<p><strong>One owner per object.</strong> The policy now manages the plugin part of the ArgoCD resource <code>policy-gitops</code>. If that resource is also deployed by Argo CD (for example, from a Helm chart for OpenShift GitOps), remove the plugin settings from there, or the two will overwrite each other.</p>
</li>
</ul>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_policy_health_in_argo_cd">Policy health in Argo CD</h3>
<div class="paragraph">
<p>By default, Argo CD considers a policy healthy as soon as it exists. That is not very useful: you want to see in Argo CD whether the policy is <strong>compliant</strong>. The policy collection of the Open Cluster Management project contains a policy that adds the matching health checks (<code>resourceHealthChecks</code>) to the ArgoCD resource: <a href="https://github.com/open-cluster-management-io/policy-collection/blob/main/stable/CM-Configuration-Management/argocd-policy-healthchecks.yaml" rel="noopener" target="_blank">argocd-policy-healthchecks.yaml</a>. The policy adds the health checks to every ArgoCD instance it finds on the cluster, so <code>policy-gitops</code> is covered as well. It is shipped with <code>remediationAction: inform</code>: set it to <code>enforce</code>, otherwise it only reports the missing health checks. After that, Argo CD shows a non-compliant policy as <strong>Degraded</strong>.</p>
</div>
<hr/>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_pattern_a_policies_as_code">Pattern A: policies as code</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Pattern A is the governance part of the integration: the policies live in Git, Argo CD on the hub brings them into the cluster, and ACM does the rest. You never create a policy in the ACM console or with <code>oc apply</code> again. Every change is a pull request, and the history of your compliance is stored in Git.</p>
</div>
<div class="paragraph">
<p>The example in this section builds two policies that show both sides of a policy:</p>
</div>
<div class="ulist">
<ul>
<li>
<p><strong>service-mesh</strong> installs Red Hat OpenShift Service Mesh 3 on the selected clusters: the Operator and a control plane. It is <em>enforced</em> and the manifests are applied in a fixed order, because the control plane resources can only be created after the Operator has installed its CRDs.</p>
</li>
<li>
<p><strong>no-kubeadmin</strong> checks that the <code>kubeadmin</code> secret does not exist anymore. It only <em>informs</em>: ACM reports every cluster where the secret is still present, but nothing is deleted automatically.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>Both start as plain Kubernetes manifests in Git. The PolicyGenerator wraps them into Policy, Placement and PlacementBinding, so you do not have to write the policy structure yourself.</p>
</div>
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
This requires the hub Argo CD instance <code>policy-gitops</code> with the PolicyGenerator plugin, as described in the previous section.
</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>The following diagram shows the complete flow, from Git to the compliance status in ACM and Argo CD:</p>
</div>
<div class="paragraph">
<div class="imageblock animated-svg" data-animated-svg>
  <div class="content">
<svg width="1400" height="680" viewBox="0 0 1400 680" role="img" aria-label="Policies as code: the hub Argo CD renders Policy, Placement and PlacementBinding from Git into the namespace acm-policies. ACM propagates a replicated policy per selected cluster, the policy framework on the managed cluster evaluates and enforces it, and the compliance status flows back to the ACM governance view and, with health checks, to Argo CD." version="1.1" id="as95c1e90-svg-policies-as-code" xmlns="http://www.w3.org/2000/svg" xmlns:c2pa="http://c2pa.org/manifest">
  <title id="as95c1e90-title-policies-as-code">Policies as code: the hub Argo CD renders Policy, Placement and PlacementBinding from Git into the namespace acm-policies. ACM propagates a replicated policy per selected cluster, the policy framework on the managed cluster evaluates and enforces it, and the compliance status flows back to the ACM governance view and, with health checks, to Argo CD.</title>
  <defs id="as95c1e90-defs-policies-as-code">
    <marker id="as95c1e90-arrow-0F766E" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#0F766E" id="as95c1e90-marker-path-0F766E" /></marker>
    <marker id="as95c1e90-arrow-C2410C" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#C2410C" id="as95c1e90-marker-path-C2410C" /></marker>
    <marker id="as95c1e90-arrow-4B5563" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#4B5563" id="as95c1e90-marker-path-4B5563" /></marker>
    </defs>
  <rect id="as95c1e90-bg" width="1400" height="680" fill="#FFFFFF" />
  <rect id="as95c1e90-zone-git" x="20" y="20" width="210" height="400" rx="14" fill="#F8FAFC" stroke="#E2E8F0" stroke-width="1.5" />
  <text id="as95c1e90-label-git" x="40" y="48" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="700" fill="#4B5563" text-anchor="start">GIT</text>
  <rect id="as95c1e90-zone-hub" x="250" y="20" width="1130" height="400" rx="14" fill="#F8FAFC" stroke="#E2E8F0" stroke-width="1.5" />
  <text id="as95c1e90-label-hub" x="270" y="48" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="700" fill="#4B5563" text-anchor="start">HUB</text>
  <rect id="as95c1e90-zone-spoke" x="250" y="450" width="1130" height="190" rx="14" fill="#F8FAFC" stroke="#E2E8F0" stroke-width="1.5" />
  <text id="as95c1e90-label-spoke" x="270" y="478" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="700" fill="#4B5563" text-anchor="start">MANAGED CLUSTERS  ·  bm, sno</text>
  <rect id="as95c1e90-ns-gitops" x="270" y="60" width="290" height="340" rx="10" fill="#FFFFFF" stroke="#CBD5E1" stroke-width="1.2" />
  <text id="as95c1e90-label-ns-gitops" x="286" y="84" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="700" fill="#C2410C" text-anchor="start">openshift-gitops</text>
  <rect id="as95c1e90-ns-policies" x="580" y="60" width="320" height="340" rx="10" fill="#FFFFFF" stroke="#CBD5E1" stroke-width="1.2" />
  <text id="as95c1e90-label-ns-policies" x="596" y="84" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="700" fill="#0F766E" text-anchor="start">acm-policies</text>
  <rect id="as95c1e90-ns-cluster" x="920" y="60" width="440" height="340" rx="10" fill="#FFFFFF" stroke="#CBD5E1" stroke-width="1.2" />
  <text id="as95c1e90-label-ns-cluster" x="936" y="84" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="700" fill="#4B5563" text-anchor="start">cluster namespaces  ·  bm, sno</text>
  <rect id="as95c1e90-rect-git" x="40" y="100" width="170" height="120" rx="10" fill="#E6F4F1" stroke="#0F766E" stroke-width="1.6" />
  <text id="as95c1e90-title-git" x="56" y="129" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#0F766E" text-anchor="start">Git</text>
  <text id="as95c1e90-line0-git" x="56" y="152" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">input/: plain</text>
  <text id="as95c1e90-line1-git" x="56" y="168" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">manifests, plus the</text>
  <text id="as95c1e90-line2-git" x="56" y="184" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">generator config</text>
  <text id="as95c1e90-note-git1" x="40" y="250" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#4B5563" text-anchor="start">Every policy change</text>
  <text id="as95c1e90-note-git2" x="40" y="266" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#4B5563" text-anchor="start">is a pull request.</text>
  <rect id="as95c1e90-rect-argocd" x="290" y="100" width="250" height="120" rx="10" fill="#FDF0E8" stroke="#C2410C" stroke-width="1.6" />
  <text id="as95c1e90-title-argocd" x="306" y="129" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#C2410C" text-anchor="start">hub Argo CD</text>
  <text id="as95c1e90-line0-argocd" x="306" y="152" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">renders the policies</text>
  <text id="as95c1e90-line1-argocd" x="306" y="168" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">(PolicyGenerator plugin</text>
  <text id="as95c1e90-line2-argocd" x="306" y="184" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">or Helm chart) and syncs</text>
  <circle id="as95c1e90-badge-1" cx="520" cy="120" r="11" fill="#1F2937" />
  <text id="as95c1e90-badge-1-text" x="520" y="124.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">1</text>
  <rect id="as95c1e90-rect-policies" x="600" y="100" width="280" height="120" rx="10" fill="#FFFFFF" stroke="#C2410C" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="as95c1e90-title-policies" x="616" y="129" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#C2410C" text-anchor="start">Policies</text>
  <text id="as95c1e90-line0-policies" x="616" y="152" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">Policy, Placement,</text>
  <text id="as95c1e90-line1-policies" x="616" y="168" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#4B5563" text-anchor="start">PlacementBinding</text>
  <text id="as95c1e90-line2-policies" x="616" y="184" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">rendered from Git</text>
  <circle id="as95c1e90-badge-2" cx="860" cy="120" r="11" fill="#1F2937" />
  <text id="as95c1e90-badge-2-text" x="860" y="124.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">2</text>
  <rect id="as95c1e90-rect-replicated" x="940" y="100" width="400" height="120" rx="10" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="as95c1e90-title-replicated" x="956" y="129" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#0F766E" text-anchor="start">Replicated policies</text>
  <text id="as95c1e90-line0-replicated" x="956" y="152" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">acm-policies.&lt;policy&gt; in bm and sno</text>
  <text id="as95c1e90-line1-replicated" x="956" y="168" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">created by ACM for every selected cluster,</text>
  <text id="as95c1e90-line2-replicated" x="956" y="184" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">IgnoreExtraneous set automatically</text>
  <circle id="as95c1e90-badge-3" cx="1320" cy="120" r="11" fill="#1F2937" />
  <text id="as95c1e90-badge-3-text" x="1320" y="124.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">3</text>
  <rect id="as95c1e90-rect-health" x="290" y="260" width="250" height="110" rx="10" fill="#F1F5F9" stroke="#4B5563" stroke-width="1.4" />
  <text id="as95c1e90-title-health" x="306" y="289" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#4B5563" text-anchor="start">Argo CD health</text>
  <text id="as95c1e90-line0-health" x="306" y="312" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">Healthy if compliant,</text>
  <text id="as95c1e90-line1-health" x="306" y="328" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">Degraded if not (with the</text>
  <text id="as95c1e90-line2-health" x="306" y="344" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">policy health checks)</text>
  <circle id="as95c1e90-badge-6" cx="520" cy="280" r="11" fill="#1F2937" />
  <text id="as95c1e90-badge-6-text" x="520" y="284.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">6</text>
  <rect id="as95c1e90-rect-governance" x="940" y="260" width="300" height="110" rx="10" fill="#F1F5F9" stroke="#4B5563" stroke-width="1.4" />
  <text id="as95c1e90-title-governance" x="956" y="289" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#4B5563" text-anchor="start">ACM governance</text>
  <text id="as95c1e90-line0-governance" x="956" y="312" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">compliance per policy and</text>
  <text id="as95c1e90-line1-governance" x="956" y="328" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">cluster, with history</text>
  <circle id="as95c1e90-badge-5" cx="1220" cy="280" r="11" fill="#1F2937" />
  <text id="as95c1e90-badge-5-text" x="1220" y="284.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">5</text>
  <rect id="as95c1e90-rect-framework" x="940" y="500" width="400" height="110" rx="10" fill="#F1F5F9" stroke="#4B5563" stroke-width="1.4" />
  <text id="as95c1e90-title-framework" x="956" y="529" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#4B5563" text-anchor="start">Policy framework</text>
  <text id="as95c1e90-line0-framework" x="956" y="552" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">pulls the replicated policy,</text>
  <text id="as95c1e90-line1-framework" x="956" y="568" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">evaluates it and enforces it</text>
  <text id="as95c1e90-line2-framework" x="956" y="584" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">(inform or enforce)</text>
  <circle id="as95c1e90-badge-4" cx="1320" cy="520" r="11" fill="#1F2937" />
  <text id="as95c1e90-badge-4-text" x="1320" y="524.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">4</text>
  <rect id="as95c1e90-rect-resources" x="600" y="500" width="280" height="110" rx="10" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="as95c1e90-title-resources" x="616" y="529" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#0F766E" text-anchor="start">Objects on the spoke</text>
  <text id="as95c1e90-line0-resources" x="616" y="552" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">created or checked</text>
  <text id="as95c1e90-line1-resources" x="616" y="568" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">by the policy</text>
  <path id="as95c1e90-flow-git-argocd" d="M 212 160 L 286 160" fill="none" stroke="#4B5563" stroke-width="2" stroke-linejoin="round" marker-end="url(#as95c1e90-arrow-4B5563)" />
  <text id="as95c1e90-label-sync" x="249" y="151" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="600" fill="#4B5563" text-anchor="middle">sync</text>
  <path id="as95c1e90-flow-argocd-policies" d="M 542 160 L 596 160" fill="none" stroke="#C2410C" stroke-width="2" stroke-linejoin="round" marker-end="url(#as95c1e90-arrow-C2410C)" />
  <path id="as95c1e90-flow-policies-replicated" d="M 882 150 L 936 150" fill="none" stroke="#0F766E" stroke-width="2" stroke-linejoin="round" marker-end="url(#as95c1e90-arrow-0F766E)" />
  <path id="as95c1e90-flow-replicated-framework" d="M 1290 222 L 1290 496" fill="none" stroke="#0F766E" stroke-width="2" stroke-linejoin="round" marker-end="url(#as95c1e90-arrow-0F766E)" />
  <text id="as95c1e90-label-pulled" x="1280" y="440" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="600" fill="#0F766E" text-anchor="end">pulled by the spoke</text>
  <path id="as95c1e90-flow-framework-resources" d="M 938 555 L 884 555" fill="none" stroke="#0F766E" stroke-width="2" stroke-linejoin="round" marker-end="url(#as95c1e90-arrow-0F766E)" />
  <path id="as95c1e90-status-framework-replicated" d="M 1320 498 L 1320 224" fill="none" stroke="#0F766E" stroke-width="1.6" stroke-linejoin="round" stroke-dasharray="5 3" marker-end="url(#as95c1e90-arrow-0F766E)" />
  <text id="as95c1e90-label-status" x="1330" y="440" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="600" fill="#0F766E" text-anchor="start">status</text>
  <path id="as95c1e90-status-replicated-policies" d="M 938 200 L 884 200" fill="none" stroke="#0F766E" stroke-width="1.6" stroke-linejoin="round" stroke-dasharray="5 3" marker-end="url(#as95c1e90-arrow-0F766E)" />
  <path id="as95c1e90-status-replicated-governance" d="M 1090 222 L 1090 256" fill="none" stroke="#0F766E" stroke-width="1.6" stroke-linejoin="round" stroke-dasharray="5 3" marker-end="url(#as95c1e90-arrow-0F766E)" />
  <path id="as95c1e90-status-policies-health" d="M 740 222 L 740 315 L 544 315" fill="none" stroke="#0F766E" stroke-width="1.6" stroke-linejoin="round" stroke-dasharray="5 3" marker-end="url(#as95c1e90-arrow-0F766E)" />
  <text id="as95c1e90-label-compliant" x="642" y="307" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="600" fill="#0F766E" text-anchor="middle">status.compliant</text>
  <rect id="as95c1e90-legend-you" x="40" y="652" width="22" height="14" rx="3" fill="#E6F4F1" stroke="#0F766E" stroke-width="1.6" />
  <text id="as95c1e90-legend-you-text" x="70" y="664" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">created by you</text>
  <rect id="as95c1e90-legend-acm" x="194.609375" y="652" width="22" height="14" rx="3" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="as95c1e90-legend-acm-text" x="224.609375" y="664" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">generated by ACM</text>
  <rect id="as95c1e90-legend-argo" x="370.5625" y="652" width="22" height="14" rx="3" fill="#FFFFFF" stroke="#C2410C" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="as95c1e90-legend-argo-text" x="400.5625" y="664" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">generated by Argo CD</text>
  <rect id="as95c1e90-legend-platform" x="566.984375" y="652" width="22" height="14" rx="3" fill="#F1F5F9" stroke="#4B5563" stroke-width="1.6" />
  <text id="as95c1e90-legend-platform-text" x="596.984375" y="664" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">platform component</text>
  <text id="as95c1e90-legend-extra" x="1380" y="664" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="600" fill="#4B5563" text-anchor="end">Dashed arrows: compliance status back to the hub</text>
<circle class="flow-dot" style="--d:0.00s" visibility="hidden" r="3.5" fill="#4B5563" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 212 160 L 286 160" begin="indefinite" data-begin="0.00" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:0.40s" visibility="hidden" r="4.5" fill="#C2410C" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 542 160 L 596 160" begin="indefinite" data-begin="0.40" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:0.80s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 882 150 L 936 150" begin="indefinite" data-begin="0.80" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:1.20s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 1290 222 L 1290 496" begin="indefinite" data-begin="1.20" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:1.60s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 938 555 L 884 555" begin="indefinite" data-begin="1.60" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:2.00s" visibility="hidden" r="3.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 1320 498 L 1320 224" begin="indefinite" data-begin="2.00" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:2.40s" visibility="hidden" r="3.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 938 200 L 884 200" begin="indefinite" data-begin="2.40" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:2.40s" visibility="hidden" r="3.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 1090 222 L 1090 256" begin="indefinite" data-begin="2.40" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:2.80s" visibility="hidden" r="3.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 740 222 L 740 315 L 544 315" begin="indefinite" data-begin="2.80" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  </svg>
</div>
  <div class="title">Policies as code: from plain manifests in Git to compliance on every cluster</div>
</div>

</div>
<div class="paragraph">
<p>What happens under the hood:</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>The <strong>hub Argo CD</strong> renders the policies from Git and syncs them to the hub.</p>
</li>
<li>
<p>The result is a set of <strong>Policy</strong>, <strong>Placement</strong> and <strong>PlacementBinding</strong> objects in the namespace <code>acm-policies</code>.</p>
</li>
<li>
<p>ACM creates a <strong>replicated policy</strong> in the namespace of every selected cluster on the hub.</p>
</li>
<li>
<p>The <strong>policy framework</strong> on each managed cluster pulls its replicated policy, evaluates it and, in <code>enforce</code> mode, fixes what is different.</p>
</li>
<li>
<p>The compliance status flows back to the hub and shows up in the <strong>ACM governance</strong> view, per policy and cluster.</p>
</li>
<li>
<p>With the policy health checks, Argo CD shows a non-compliant policy as <strong>Degraded</strong>.</p>
</li>
</ol>
</div>
<hr/>
<div class="sect2">
<h3 id="_repository_layout">Repository layout</h3>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-text hljs" data-lang="text">acm-policies/
├── kustomization.yaml
├── policy-generator.yaml
└── input
    ├── service-mesh
    │   ├── 01-subscription.yaml       # OpenShift Service Mesh 3 Operator
    │   ├── 02-namespace-istio-system.yaml
    │   ├── 03-namespace-istio-cni.yaml
    │   ├── 04-istiocni.yaml           # CNI plugin, required on OpenShift
    │   └── 05-istio.yaml              # the control plane
    └── no-kubeadmin
        └── kubeadmin-secret.yaml</code></pre>
</div>
</div>
<div class="paragraph">
<p>The files in <code>input/</code> are plain Kubernetes manifests, exactly as you would apply them with <code>oc apply</code>. The Operator is installed in the namespace <code>openshift-operators</code>, which already has an OperatorGroup for all namespaces, so neither a namespace nor an OperatorGroup is needed for it:</p>
</div>
<div class="listingblock linenums">
<div class="title">input/service-mesh/01-subscription.yaml</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
  name: servicemeshoperator3
  namespace: openshift-operators
spec:
  channel: stable-3.4 <i class="conum" data-value="1"></i><b>(1)</b>
  installPlanApproval: Automatic
  name: servicemeshoperator3
  source: redhat-operators
  sourceNamespace: openshift-marketplace</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Pins the minor version. Use <code>stable</code> to always get the latest version.</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>The control plane consists of two cluster-scoped resources, <code>IstioCNI</code> and <code>Istio</code>, each pointing to its own namespace (<code>istio-cni</code> and <code>istio-system</code>):</p>
</div>
<div class="listingblock linenums">
<div class="title">input/service-mesh/04-istiocni.yaml</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: sailoperator.io/v1
kind: IstioCNI
metadata:
  name: default
spec:
  namespace: istio-cni</code></pre>
</div>
</div>
<div class="listingblock linenums">
<div class="title">input/service-mesh/05-istio.yaml</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: sailoperator.io/v1
kind: Istio
metadata:
  name: default
spec:
  namespace: istio-system</code></pre>
</div>
</div>
<div class="paragraph">
<p>And the kubeadmin secret that must not exist:</p>
</div>
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
if your original kubeadmin secret is still present, you can ignore this yaml. Honestly, I am not even sure if this would work, once you have removed the secret.
</td>
</tr>
</tbody></table>
</div>
<div class="listingblock linenums">
<div class="title">input/no-kubeadmin/kubeadmin-secret.yaml</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: v1
kind: Secret
metadata:
  name: kubeadmin
  namespace: kube-system</code></pre>
</div>
</div>
<div class="paragraph">
<p>The <code>kustomization.yaml</code> only references the generator:</p>
</div>
<div class="listingblock linenums">
<div class="title">kustomization.yaml</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">generators:
  - policy-generator.yaml</code></pre>
</div>
</div>
<div class="paragraph">
<p>Render the folder locally before you push. If it does not render on your machine, it will not render in Argo CD either. With the PolicyGenerator binary in your Kustomize plugin path:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-bash hljs" data-lang="bash">kustomize build --enable-alpha-plugins acm-policies/</code></pre>
</div>
</div>
<div class="paragraph">
<p>The output contains a Policy, a Placement and a PlacementBinding per policy.</p>
</div>
<hr/>
</div>
<div class="sect2">
<h3 id="_the_policygenerator_configuration">The PolicyGenerator configuration</h3>
<div class="paragraph">
<p>The file <code>policy-generator.yaml</code> is the heart of the setup. It does not contain any policy itself, it only describes which policies the generator shall create from the manifests in <code>input/</code>. It has two parts:</p>
</div>
<div class="ulist">
<ul>
<li>
<p><code>policyDefaults</code> apply to every policy: the namespace on the hub where the policies are created, the default remediation action and severity, and the Placement that decides which clusters receive them.</p>
</li>
<li>
<p><code>policies</code> lists the policies, each with its name, the manifests it contains and the settings that differ from the defaults.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>For every entry in <code>policies</code>, the generator creates a Policy, a Placement and a PlacementBinding. The manifests are wrapped into a ConfigurationPolicy inside the Policy: by default all manifests of a policy into one, with <code>consolidateManifests: false</code> one per manifest. Our two policies use this as follows: <code>service-mesh</code> overrides the defaults to enforce and to apply its manifests in order, while <code>no-kubeadmin</code> keeps the defaults and only changes the compliance type.</p>
</div>
<div class="listingblock linenums">
<div class="title">policy-generator.yaml</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: policy.open-cluster-management.io/v1
kind: PolicyGenerator
metadata:
  name: cluster-baseline
policyDefaults:
  namespace: acm-policies
  remediationAction: inform <i class="conum" data-value="1"></i><b>(1)</b>
  severity: medium
  placement:
    labelSelector:
      matchExpressions:
        - key: name <i class="conum" data-value="5"></i><b>(5)</b>
          operator: In
          values:
            - sno
            - bm
policies:
  - name: service-mesh
    remediationAction: enforce <i class="conum" data-value="2"></i><b>(2)</b>
    orderManifests: true <i class="conum" data-value="3"></i><b>(3)</b>
    consolidateManifests: false
    manifests:
      - path: input/service-mesh/01-subscription.yaml
      - path: input/service-mesh/02-namespace-istio-system.yaml
      - path: input/service-mesh/03-namespace-istio-cni.yaml
      - path: input/service-mesh/04-istiocni.yaml
      - path: input/service-mesh/05-istio.yaml
  - name: no-kubeadmin
    manifests:
      - path: input/no-kubeadmin/
        complianceType: mustnothave <i class="conum" data-value="4"></i><b>(4)</b></code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Default for all policies: report only.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>This policy is enforced.</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>Creates one ConfigurationPolicy per manifest and makes each one depend on the previous one: first the Subscription, then the namespaces, then IstioCNI and Istio. Note that the Subscription policy is compliant as soon as the Subscription exists, not when the Operator is installed. Until the CRDs are there, the IstioCNI policy reports an error and is evaluated again. If you want to wait for the installed Operator, use an OperatorPolicy instead of the Subscription, as shown in Pattern B.</td>
</tr>
<tr>
<td><i class="conum" data-value="4"></i><b>4</b></td>
<td>The kubeadmin secret must not exist. In inform mode, ACM reports every cluster where it still exists. Remove it with care, only after an identity provider is configured.</td>
</tr>
<tr>
<td><i class="conum" data-value="5"></i><b>5</b></td>
<td>Selects the clusters <code>sno</code> and <code>bm</code>. ACM sets the label <code>name</code> on every ManagedCluster. For a growing fleet, a label such as <code>environment=production</code> or the cluster set label <code>cluster.open-cluster-management.io/clusterset</code> scales better than a list of names.</td>
</tr>
</tbody></table>
</div>
</div>
<div class="sect2">
<h3 id="_bind_the_cluster_sets_to_the_policy_namespace">Bind the cluster sets to the policy namespace</h3>
<div class="paragraph">
<p>A Placement only sees the clusters of cluster sets that are bound to its own namespace. The bindings from Ep.19 live in <code>openshift-gitops</code>, so the Placements in <code>acm-policies</code> would find no cluster at all. ACM then shows the warning <em>&#34;ClusterSets failed to load. Verify that there is at least one ClusterSet bound to your selected namespace.&#34;</em></p>
</div>
<div class="paragraph">
<p>Bind the cluster set <code>production</code> to <code>acm-policies</code>, and <code>global</code> as well if you use the policy that keeps the PolicyGenerator image in sync (its Placement selects the hub from <code>global</code>):</p>
</div>
<div class="listingblock linenums">
<div class="title">ManagedClusterSetBindings in the policy namespace</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: cluster.open-cluster-management.io/v1beta2
kind: ManagedClusterSetBinding
metadata:
  name: production
  namespace: acm-policies
spec:
  clusterSet: production
---
apiVersion: cluster.open-cluster-management.io/v1beta2
kind: ManagedClusterSetBinding
metadata:
  name: global
  namespace: acm-policies
spec:
  clusterSet: global</code></pre>
</div>
</div>
<div class="paragraph">
<p>The generated Placements select <code>sno</code> and <code>bm</code> by name, from the cluster sets bound to <code>acm-policies</code>. Check the result:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-bash hljs" data-lang="bash">oc get placementdecisions -n acm-policies \
  -o custom-columns=NAME:.metadata.name,CLUSTERS:.status.decisions[*].clusterName</code></pre>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_the_argo_cd_application">The Argo CD Application</h3>
<div class="paragraph">
<p>On the hub, one Application syncs the folder:</p>
</div>
<div class="listingblock linenums">
<div class="title">Application for the policies</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: acm-policies
  namespace: policy-gitops <i class="conum" data-value="1"></i><b>(1)</b>
spec:
  project: default
  source:
    repoURL: https://github.com/tjungbauer/acm-policies.git
    targetRevision: main
    path: .
  destination:
    server: https://kubernetes.default.svc
    namespace: acm-policies
  syncPolicy:
    automated:
      prune: true
      selfHeal: true</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>The Application belongs to the instance <code>policy-gitops</code>. No <code>CreateNamespace=true</code>: the namespace <code>acm-policies</code> already exists with the <code>managed-by</code> label.</td>
</tr>
</tbody></table>
</div>
</div>
<div class="sect2">
<h3 id="_replicated_policies">Replicated policies</h3>
<div class="paragraph">
<p>When ACM propagates a policy, it creates a copy in the namespace of every managed cluster on the hub, the <strong>replicated policy</strong>. Argo CD could treat these copies as extraneous resources of the Application and show it as <em>OutOfSync</em> or even try to delete them. ACM prevents that: it automatically sets the annotation <code>argocd.argoproj.io/compare-options: IgnoreExtraneous</code> on every replicated policy.</p>
</div>
<hr/>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_pattern_b_a_policy_bootstraps_argo_cd">Pattern B: a policy bootstraps Argo CD</h2>
<div class="sectionbody">
<div class="paragraph">
<p>The bootstrap question from <a href="/gitopscollection/2026-10-20-openshift-gitops-vs-acm/">Ep.18</a>: who installs Argo CD on a new cluster? With ACM, the answer is a policy. As soon as a cluster is imported and matches the Placement of the policy, the policy installs OpenShift GitOps and creates a root Application that points to the cluster’s folder in Git. From there, Argo CD configures the rest.</p>
</div>
<div class="paragraph">
<div class="imageblock animated-svg" data-animated-svg>
  <div class="content">
<svg width="1400" height="720" viewBox="0 0 1400 720" role="img" aria-label="Bootstrap policy: the Placement selects the ManagedClusters sno and bm by their name label. The enforced policy gitops-bootstrap applies five templates in order: namespace, OperatorPolicy for OpenShift GitOps, ClusterRoleBinding, repository secret and root Application. The local Argo CD then syncs everything else from Git." version="1.1" id="asa8d4a31-svg-bootstrap-policy" xmlns="http://www.w3.org/2000/svg" xmlns:c2pa="http://c2pa.org/manifest">
  <title id="asa8d4a31-title-bootstrap-policy">Bootstrap policy: the Placement selects the ManagedClusters sno and bm by their name label. The enforced policy gitops-bootstrap applies five templates in order: namespace, OperatorPolicy for OpenShift GitOps, ClusterRoleBinding, repository secret and root Application. The local Argo CD then syncs everything else from Git.</title>
  <defs id="asa8d4a31-defs-bootstrap-policy">
    <marker id="asa8d4a31-arrow-0F766E" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#0F766E" id="asa8d4a31-marker-path-0F766E" /></marker>
    <marker id="asa8d4a31-arrow-C2410C" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#C2410C" id="asa8d4a31-marker-path-C2410C" /></marker>
    <marker id="asa8d4a31-arrow-4B5563" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#4B5563" id="asa8d4a31-marker-path-4B5563" /></marker>
    </defs>
  <rect id="asa8d4a31-bg" width="1400" height="720" fill="#FFFFFF" />
  <rect id="asa8d4a31-zone-hub" x="20" y="20" width="560" height="660" rx="14" fill="#F8FAFC" stroke="#E2E8F0" stroke-width="1.5" />
  <text id="asa8d4a31-label-hub" x="40" y="48" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="700" fill="#4B5563" text-anchor="start">HUB</text>
  <rect id="asa8d4a31-zone-spoke" x="610" y="20" width="770" height="660" rx="14" fill="#F8FAFC" stroke="#E2E8F0" stroke-width="1.5" />
  <text id="asa8d4a31-label-spoke" x="630" y="48" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="700" fill="#4B5563" text-anchor="start">MANAGED CLUSTER  ·  sno</text>
  <rect style="--d:0.20s" class="rv" id="asa8d4a31-rect-cluster" x="40" y="70" width="250" height="100" rx="10" fill="#E6F4F1" stroke="#0F766E" stroke-width="1.6" />
  <text style="--d:0.20s" class="rv" id="asa8d4a31-title-cluster" x="56" y="99" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#0F766E" text-anchor="start">ManagedCluster sno</text>
  <text style="--d:0.20s" class="rv" id="asa8d4a31-line0-cluster" x="56" y="122" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#1F2937" text-anchor="start">label name=sno</text>
  <text style="--d:0.20s" class="rv" id="asa8d4a31-line1-cluster" x="56" y="138" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">the only manual step</text>
  <rect style="--d:0.20s" class="rv" id="asa8d4a31-rect-placement" x="310" y="70" width="250" height="100" rx="10" fill="#E6F4F1" stroke="#0F766E" stroke-width="1.6" />
  <text style="--d:0.20s" class="rv" id="asa8d4a31-title-placement" x="326" y="99" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#0F766E" text-anchor="start">Placement</text>
  <text style="--d:0.20s" class="rv" id="asa8d4a31-line0-placement" x="326" y="122" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">labelSelector</text>
  <text style="--d:0.20s" class="rv" id="asa8d4a31-line1-placement" x="326" y="138" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#1F2937" text-anchor="start">name in [sno, bm]</text>
  <path style="--d:0.20s" class="rv" id="asa8d4a31-flow-cluster-placement" d="M 292 120 L 306 120" fill="none" stroke="#0F766E" stroke-width="2" stroke-linejoin="round" marker-end="url(#asa8d4a31-arrow-0F766E)" />
  <rect style="--d:0.70s" class="rv" id="asa8d4a31-ns-policies" x="40" y="196" width="520" height="464" rx="10" fill="#FFFFFF" stroke="#CBD5E1" stroke-width="1.2" />
  <text style="--d:0.70s" class="rv" id="asa8d4a31-label-ns-policies" x="56" y="220" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="700" fill="#0F766E" text-anchor="start">acm-policies</text>
  <rect style="--d:0.70s" class="rv" id="asa8d4a31-rect-policy" x="56" y="232" width="488" height="412" rx="10" fill="#E6F4F1" stroke="#0F766E" stroke-width="1.6" />
  <text style="--d:0.70s" class="rv" id="asa8d4a31-title-policy" x="72" y="261" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#0F766E" text-anchor="start">Policy gitops-bootstrap</text>
  <text style="--d:0.70s" class="rv" id="asa8d4a31-line0-policy" x="72" y="282" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">enforce · templates applied in order, each one waits</text>
  <text style="--d:0.70s" class="rv" id="asa8d4a31-line1-policy" x="72" y="298" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">until the previous one is compliant</text>
  <rect style="--d:1.30s" class="rv" id="asa8d4a31-rect-tpl1" x="72" y="316" width="456" height="52" rx="8" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.4" />
  <circle style="--d:1.30s" class="rv" id="asa8d4a31-badge-tpl1" cx="96" cy="342" r="12" fill="#0F766E" />
  <text style="--d:1.30s" class="rv" id="asa8d4a31-badge-tpl1-text" x="96" y="346.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">1</text>
  <text style="--d:1.30s" class="rv" id="asa8d4a31-title-tpl1" x="120" y="338" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="13.5" font-weight="700" fill="#0F766E" text-anchor="start">Namespace</text>
  <text style="--d:1.30s" class="rv" id="asa8d4a31-line0-tpl1" x="120" y="356" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">openshift-gitops-operator</text>
  <rect style="--d:1.70s" class="rv" id="asa8d4a31-rect-obj1" x="650" y="316" width="320" height="52" rx="8" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.6" stroke-dasharray="6 4" />
  <text style="--d:1.70s" class="rv" id="asa8d4a31-title-obj1" x="666" y="338" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="13.5" font-weight="700" fill="#0F766E" text-anchor="start">Namespace</text>
  <text style="--d:1.70s" class="rv" id="asa8d4a31-line0-obj1" x="666" y="356" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#4B5563" text-anchor="start">openshift-gitops-operator</text>
  <path style="--d:1.70s" class="rv" id="asa8d4a31-flow-tpl1" d="M 530 342 L 646 342" fill="none" stroke="#0F766E" stroke-width="2" stroke-linejoin="round" marker-end="url(#asa8d4a31-arrow-0F766E)" />
  <rect style="--d:2.10s" class="rv" id="asa8d4a31-rect-tpl2" x="72" y="380" width="456" height="52" rx="8" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.4" />
  <circle style="--d:2.10s" class="rv" id="asa8d4a31-badge-tpl2" cx="96" cy="406" r="12" fill="#0F766E" />
  <text style="--d:2.10s" class="rv" id="asa8d4a31-badge-tpl2-text" x="96" y="410.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">2</text>
  <text style="--d:2.10s" class="rv" id="asa8d4a31-title-tpl2" x="120" y="402" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="13.5" font-weight="700" fill="#0F766E" text-anchor="start">OperatorPolicy</text>
  <text style="--d:2.10s" class="rv" id="asa8d4a31-line0-tpl2" x="120" y="420" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">OpenShift GitOps, channel gitops-1.22</text>
  <rect style="--d:2.50s" class="rv" id="asa8d4a31-rect-obj2" x="650" y="380" width="320" height="52" rx="8" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.6" stroke-dasharray="6 4" />
  <text style="--d:2.50s" class="rv" id="asa8d4a31-title-obj2" x="666" y="402" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="13.5" font-weight="700" fill="#0F766E" text-anchor="start">OpenShift GitOps</text>
  <text style="--d:2.50s" class="rv" id="asa8d4a31-line0-obj2" x="666" y="420" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#4B5563" text-anchor="start">operator and default Argo CD instance</text>
  <path style="--d:2.50s" class="rv" id="asa8d4a31-flow-tpl2" d="M 530 406 L 646 406" fill="none" stroke="#0F766E" stroke-width="2" stroke-linejoin="round" marker-end="url(#asa8d4a31-arrow-0F766E)" />
  <rect style="--d:2.90s" class="rv" id="asa8d4a31-rect-tpl3" x="72" y="444" width="456" height="52" rx="8" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.4" />
  <circle style="--d:2.90s" class="rv" id="asa8d4a31-badge-tpl3" cx="96" cy="470" r="12" fill="#0F766E" />
  <text style="--d:2.90s" class="rv" id="asa8d4a31-badge-tpl3-text" x="96" y="474.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">3</text>
  <text style="--d:2.90s" class="rv" id="asa8d4a31-title-tpl3" x="120" y="466" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="13.5" font-weight="700" fill="#0F766E" text-anchor="start">ClusterRoleBinding</text>
  <text style="--d:2.90s" class="rv" id="asa8d4a31-line0-tpl3" x="120" y="484" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">rights for the local application-controller</text>
  <rect style="--d:3.30s" class="rv" id="asa8d4a31-rect-obj3" x="650" y="444" width="320" height="52" rx="8" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.6" stroke-dasharray="6 4" />
  <text style="--d:3.30s" class="rv" id="asa8d4a31-title-obj3" x="666" y="466" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="13.5" font-weight="700" fill="#0F766E" text-anchor="start">ClusterRoleBinding</text>
  <text style="--d:3.30s" class="rv" id="asa8d4a31-line0-obj3" x="666" y="484" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#4B5563" text-anchor="start">local controller may deploy</text>
  <path style="--d:3.30s" class="rv" id="asa8d4a31-flow-tpl3" d="M 530 470 L 646 470" fill="none" stroke="#0F766E" stroke-width="2" stroke-linejoin="round" marker-end="url(#asa8d4a31-arrow-0F766E)" />
  <rect style="--d:3.70s" class="rv" id="asa8d4a31-rect-tpl4" x="72" y="508" width="456" height="52" rx="8" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.4" />
  <circle style="--d:3.70s" class="rv" id="asa8d4a31-badge-tpl4" cx="96" cy="534" r="12" fill="#0F766E" />
  <text style="--d:3.70s" class="rv" id="asa8d4a31-badge-tpl4-text" x="96" y="538.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">4</text>
  <text style="--d:3.70s" class="rv" id="asa8d4a31-title-tpl4" x="120" y="530" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="13.5" font-weight="700" fill="#0F766E" text-anchor="start">Repository secret</text>
  <text style="--d:3.70s" class="rv" id="asa8d4a31-line0-tpl4" x="120" y="548" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">copySecretData from the hub</text>
  <rect style="--d:4.10s" class="rv" id="asa8d4a31-rect-obj4" x="650" y="508" width="320" height="52" rx="8" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.6" stroke-dasharray="6 4" />
  <text style="--d:4.10s" class="rv" id="asa8d4a31-title-obj4" x="666" y="530" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="13.5" font-weight="700" fill="#0F766E" text-anchor="start">Repository secret</text>
  <text style="--d:4.10s" class="rv" id="asa8d4a31-line0-obj4" x="666" y="548" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#4B5563" text-anchor="start">Git credentials for the local Argo CD</text>
  <path style="--d:4.10s" class="rv" id="asa8d4a31-flow-tpl4" d="M 530 534 L 646 534" fill="none" stroke="#0F766E" stroke-width="2" stroke-linejoin="round" marker-end="url(#asa8d4a31-arrow-0F766E)" />
  <rect style="--d:4.50s" class="rv" id="asa8d4a31-rect-tpl5" x="72" y="572" width="456" height="52" rx="8" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.4" />
  <circle style="--d:4.50s" class="rv" id="asa8d4a31-badge-tpl5" cx="96" cy="598" r="12" fill="#0F766E" />
  <text style="--d:4.50s" class="rv" id="asa8d4a31-badge-tpl5-text" x="96" y="602.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">5</text>
  <text style="--d:4.50s" class="rv" id="asa8d4a31-title-tpl5" x="120" y="594" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="13.5" font-weight="700" fill="#0F766E" text-anchor="start">Root Application</text>
  <text style="--d:4.50s" class="rv" id="asa8d4a31-line0-tpl5" x="120" y="612" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">path clusters/sno/bootstrap</text>
  <rect style="--d:4.90s" class="rv" id="asa8d4a31-rect-obj5" x="650" y="572" width="320" height="52" rx="8" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.6" stroke-dasharray="6 4" />
  <text style="--d:4.90s" class="rv" id="asa8d4a31-title-obj5" x="666" y="594" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="13.5" font-weight="700" fill="#0F766E" text-anchor="start">Root Application</text>
  <text style="--d:4.90s" class="rv" id="asa8d4a31-line0-obj5" x="666" y="612" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#4B5563" text-anchor="start">app-of-apps for sno, prune: false</text>
  <path style="--d:4.90s" class="rv" id="asa8d4a31-flow-tpl5" d="M 530 598 L 646 598" fill="none" stroke="#0F766E" stroke-width="2" stroke-linejoin="round" marker-end="url(#asa8d4a31-arrow-0F766E)" />
  <text id="asa8d4a31-label-applied" x="650" y="306" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="600" fill="#0F766E" text-anchor="start">applied in order by the policy framework on sno</text>
  <rect style="--d:3.00s" class="rv" id="asa8d4a31-rect-localargo" x="1010" y="380" width="350" height="100" rx="10" fill="#FDF0E8" stroke="#C2410C" stroke-width="1.6" />
  <text style="--d:3.00s" class="rv" id="asa8d4a31-title-localargo" x="1026" y="409" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#C2410C" text-anchor="start">Local Argo CD</text>
  <text style="--d:3.00s" class="rv" id="asa8d4a31-line0-localargo" x="1026" y="432" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">installed by the operator,</text>
  <text style="--d:3.00s" class="rv" id="asa8d4a31-line1-localargo" x="1026" y="448" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">reads the repository secret</text>
  <path style="--d:3.00s" class="rv" id="asa8d4a31-flow-operator-localargo" d="M 972 406 L 1006 406" fill="none" stroke="#C2410C" stroke-width="2" stroke-linejoin="round" marker-end="url(#asa8d4a31-arrow-C2410C)" />
  <rect style="--d:5.40s" class="rv" id="asa8d4a31-rect-rest" x="1010" y="540" width="350" height="110" rx="10" fill="#FFFFFF" stroke="#C2410C" stroke-width="1.6" stroke-dasharray="6 4" />
  <text style="--d:5.40s" class="rv" id="asa8d4a31-title-rest" x="1026" y="569" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#C2410C" text-anchor="start">Everything else</text>
  <text style="--d:5.40s" class="rv" id="asa8d4a31-line0-rest" x="1026" y="592" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">operators, OAuth, RBAC, apps ...</text>
  <text style="--d:5.40s" class="rv" id="asa8d4a31-line1-rest" x="1026" y="608" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">synced by the local Argo CD</text>
  <text style="--d:5.40s" class="rv" id="asa8d4a31-line2-rest" x="1026" y="624" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#4B5563" text-anchor="start">from clusters/sno/</text>
  <path style="--d:5.40s" class="rv" id="asa8d4a31-flow-root-rest" d="M 972 598 L 1006 598" fill="none" stroke="#C2410C" stroke-width="2" stroke-linejoin="round" marker-end="url(#asa8d4a31-arrow-C2410C)" />
  <path style="--d:5.40s" class="rv" id="asa8d4a31-flow-localargo-rest" d="M 1185 482 L 1185 536" fill="none" stroke="#C2410C" stroke-width="2" stroke-linejoin="round" marker-end="url(#asa8d4a31-arrow-C2410C)" />
  <rect style="--d:6.00s" class="rv" id="asa8d4a31-rect-owner" x="650" y="80" width="710" height="96" rx="8" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.2" stroke-dasharray="6 4" />
  <text style="--d:6.00s" class="rv" id="asa8d4a31-owner1" x="670" y="110" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="13" font-weight="600" fill="#0F766E" text-anchor="start">The policy owns the five objects on the left.</text>
  <text style="--d:6.00s" class="rv" id="asa8d4a31-owner2" x="670" y="132" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="13" font-weight="600" fill="#C2410C" text-anchor="start">Argo CD owns everything the root Application brings in.</text>
  <text style="--d:6.00s" class="rv" id="asa8d4a31-owner3" x="670" y="154" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">Never manage the same object from both sides.</text>
  <rect id="asa8d4a31-legend-you" x="40" y="692" width="22" height="14" rx="3" fill="#E6F4F1" stroke="#0F766E" stroke-width="1.6" />
  <text id="asa8d4a31-legend-you-text" x="70" y="704" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">created by you</text>
  <rect id="asa8d4a31-legend-acm" x="194.609375" y="692" width="22" height="14" rx="3" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="asa8d4a31-legend-acm-text" x="224.609375" y="704" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">generated by ACM</text>
  <rect id="asa8d4a31-legend-argo" x="370.5625" y="692" width="22" height="14" rx="3" fill="#FFFFFF" stroke="#C2410C" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="asa8d4a31-legend-argo-text" x="400.5625" y="704" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">generated by Argo CD</text>
  <rect id="asa8d4a31-legend-platform" x="566.984375" y="692" width="22" height="14" rx="3" fill="#F1F5F9" stroke="#4B5563" stroke-width="1.6" />
  <text id="asa8d4a31-legend-platform-text" x="596.984375" y="704" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">platform component</text>
  <text id="asa8d4a31-legend-extra" x="1380" y="704" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="600" fill="#4B5563" text-anchor="end">Build-up order = order of the policy templates</text>
<circle class="flow-dot" style="--d:0.20s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 292 120 L 306 120" begin="indefinite" data-begin="0.20" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:1.70s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 530 342 L 646 342" begin="indefinite" data-begin="1.70" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:2.50s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 530 406 L 646 406" begin="indefinite" data-begin="2.50" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:3.30s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 530 470 L 646 470" begin="indefinite" data-begin="3.30" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:4.10s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 530 534 L 646 534" begin="indefinite" data-begin="4.10" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:4.90s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 530 598 L 646 598" begin="indefinite" data-begin="4.90" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:3.00s" visibility="hidden" r="4.5" fill="#C2410C" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 972 406 L 1006 406" begin="indefinite" data-begin="3.00" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:5.40s" visibility="hidden" r="4.5" fill="#C2410C" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 972 598 L 1006 598" begin="indefinite" data-begin="5.40" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:5.40s" visibility="hidden" r="4.5" fill="#C2410C" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 1185 482 L 1185 536" begin="indefinite" data-begin="5.40" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  </svg>
</div>
  <div class="title">Bootstrap policy: the templates are applied in order, then the local Argo CD takes over</div>
</div>

</div>
<div class="paragraph">
<p>We extend our repository with:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-text hljs" data-lang="text">acm-policies/
└── input
    └── gitops-bootstrap
        ├── 01-namespace.yaml          # openshift-gitops-operator
        ├── 02-operatorpolicy.yaml     # OpenShift GitOps Operator
        ├── 03-controller-rbac.yaml    # rights for the local controller
        ├── 04-repo-credentials.yaml   # Git credentials, copied from the hub
        └── 05-root-application.yaml   # app-of-apps for this cluster</code></pre>
</div>
</div>
<div class="paragraph">
<p>The first file creates the namespace for the Operator. The OperatorPolicy in the next step expects it to exist:</p>
</div>
<div class="listingblock linenums">
<div class="title">input/gitops-bootstrap/01-namespace.yaml</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: v1
kind: Namespace
metadata:
  name: openshift-gitops-operator</code></pre>
</div>
</div>
<div class="listingblock linenums">
<div class="title">input/gitops-bootstrap/02-operatorpolicy.yaml</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: policy.open-cluster-management.io/v1beta1
kind: OperatorPolicy <i class="conum" data-value="1"></i><b>(1)</b>
metadata:
  name: openshift-gitops-operator
spec:
  remediationAction: enforce
  severity: high
  complianceType: musthave
  subscription:
    name: openshift-gitops-operator
    namespace: openshift-gitops-operator
    channel: gitops-1.22 <i class="conum" data-value="2"></i><b>(2)</b>
    source: redhat-operators
    sourceNamespace: openshift-marketplace
  upgradeApproval: Automatic</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>The PolicyGenerator adds OperatorPolicy manifests unchanged as a policy template, no extra wrapping.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Pin the channel. Moving to the next version is a one-line pull request.</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>The root Application is the app-of-apps that contains the Application for every cluster.</p>
</div>
<div class="listingblock linenums">
<div class="title">input/gitops-bootstrap/05-root-application.yaml</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root
  namespace: openshift-gitops
spec:
  project: default
  source:
    repoURL: https://github.com/tjungbauer/openshift-clusterconfig-gitops.git
    targetRevision: main
    path: &#39;{{hub printf &#34;clusters/%s/bootstrap&#34; .ManagedClusterName hub}}&#39; <i class="conum" data-value="1"></i><b>(1)</b>
  destination:
    server: https://kubernetes.default.svc
  syncPolicy:
    automated:
      prune: false <i class="conum" data-value="2"></i><b>(2)</b>
      selfHeal: true</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>A hub template, resolved on the hub for every cluster: <code>sno</code> gets <code>clusters/sno/bootstrap</code>.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Never prune on the root Application. A broken path would otherwise remove everything below it.</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>The local Argo CD must be allowed to create whatever the root Application brings in. <code>03-controller-rbac.yaml</code> grants its application-controller cluster-admin rights, the same as in the pull model of <a href="/gitopscollection/2026-10-23-acm-gitops-push-pull-agent/">Ep.19</a>:</p>
</div>
<div class="listingblock linenums">
<div class="title">input/gitops-bootstrap/03-controller-rbac.yaml</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: argo-admin
subjects:
  - kind: ServiceAccount
    name: openshift-gitops-argocd-application-controller <i class="conum" data-value="1"></i><b>(1)</b>
    namespace: openshift-gitops
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin <i class="conum" data-value="2"></i><b>(2)</b></code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>The service account of the default instance <code>openshift-gitops</code>, which the Operator creates after the installation.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Simple and usually fine for platform configuration. For application teams, prefer namespaces labelled with <code>argocd.argoproj.io/managed-by</code>.</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>And the generator configuration ties it together:</p>
</div>
<div class="listingblock linenums">
<div class="title">policy-generator.yaml (excerpt)</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">policies:
  - name: gitops-bootstrap
    remediationAction: enforce
    orderManifests: true <i class="conum" data-value="1"></i><b>(1)</b>
    consolidateManifests: false
    placement:
      labelSelector:
        matchExpressions:
          - key: name <i class="conum" data-value="2"></i><b>(2)</b>
            operator: In
            values:
              - sno
              - bm
    manifests:
      - path: input/gitops-bootstrap/01-namespace.yaml
      - path: input/gitops-bootstrap/02-operatorpolicy.yaml
      - path: input/gitops-bootstrap/03-controller-rbac.yaml
      - path: input/gitops-bootstrap/04-repo-credentials.yaml
      - path: input/gitops-bootstrap/05-root-application.yaml</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>The order matters: the Application CRD only exists after the operator is installed. With <code>orderManifests</code>, the root Application is only created when the operator policy is compliant.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>The same selection as for the other policies: the clusters <code>sno</code> and <code>bm</code>, selected by the <code>name</code> label that ACM sets on every ManagedCluster. In a larger fleet, a dedicated label such as <code>gitops=enabled</code> makes the bootstrap an opt-in per cluster.</td>
</tr>
</tbody></table>
</div>
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
Pattern B is meant for clusters that get their Argo CD from this policy. If you followed the pull or agent part of Ep.19, the GitOps add-on of ACM has already installed OpenShift GitOps on <code>sno</code> and <code>bm</code>, and the policy and the add-on would then manage the same operator. Decide which of the two owns the operator before you enforce this policy, or test Pattern B on a cluster without the add-on.
</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>Who owns what afterwards? The policy owns exactly five objects: the namespace, the operator, the ClusterRoleBinding, the repository secret and the root Application. Everything the root Application brings in belongs to Argo CD. Keep it that way: if you also manage the OpenShift GitOps Subscription with Argo CD on the spoke, the two will fight.</p>
</div>
<hr/>
</div>
</div>
<div class="sect1">
<h2 id="_the_result_in_acm_argo_cd">The result in ACM &amp; Argo CD</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Once Argo CD has synced the repository, the policies show up in the ACM console under <strong>Governance</strong>.</p>
</div>
<div class="paragraph">
<p>The list of <strong>Policies</strong> shows the three policies of this article in the namespace <code>acm-policies</code>: <code>service-mesh</code>, <code>no-kubeadmin</code> and <code>gitops-bootstrap</code>. For each policy you see the remediation action (inform or enforce), the cluster violations and the policy set, if there is one.</p>
</div>
<div class="imageblock">
<div class="content">
<img src="/gitopscollection/images/ep-20/acm-governance-policies.png?width=80%" alt="ACM Governance policies"/>
</div>
<div class="title">Figure 1. Policies in the namespace acm-policies</div>
</div>
<div class="paragraph">
<p>A click on a policy opens the details. The <strong>Details</strong> tab shows the policy templates, here the ConfigurationPolicies and the OperatorPolicy that the PolicyGenerator created from the plain manifests, and the Placement with the selected clusters <code>sno</code> and <code>bm</code>.</p>
</div>
<div class="paragraph">
<p>The <code>no-kubeadmin</code> policy is a good example for <code>inform</code>: ACM only reports that the secret <code>kubeadmin</code> still exists, it does not delete it.</p>
</div>
<div class="imageblock">
<div class="content">
<img src="/gitopscollection/images/ep-20/acm-policy-no-kubeadmin-results.png?width=80%" alt="Results of the policy no-kubeadmin"/>
</div>
<div class="title">Figure 2. Results of the policy no-kubeadmin</div>
</div>
<div class="paragraph">
<p>In the Argo CD UI we can also verify the status (including the resource tree).</p>
</div>
<div class="paragraph">
<p>The Application <code>acm-policies</code> shows the status <strong>Degraded</strong>. This is because the policy <code>no-kubeadmin</code> is non-compliant …​ the secrets are still present.</p>
</div>
<div class="imageblock">
<div class="content">
<img src="/gitopscollection/images/ep-20/policy-gitops-acm-policies-tile.png?width=50%" alt="Application acm-policies in Argo CD"/>
</div>
<div class="title">Figure 3. Application acm-policies in Argo CD</div>
</div>
<div class="paragraph">
<p>The resource tree proves this:</p>
</div>
<div class="imageblock">
<div class="content">
<img src="/gitopscollection/images/ep-20/policy-gitops-acm-policies-tree.png?width=80%" alt="Resource tree of the application acm-policies"/>
</div>
<div class="title">Figure 4. Resource tree of the application acm-policies</div>
</div>
<hr/>
</div>
</div>
<div class="sect1">
<h2 id="_summary">Summary</h2>
<div class="sectionbody">
<div class="paragraph">
<p>That concludes the mini-series. ACM decides <em>where</em> and checks <em>compliance</em>, Argo CD decides <em>what</em> and keeps it <em>in sync</em>, and Git is the only place where changes are made.</p>
</div>
<div class="paragraph">
<p>And as always: <strong>If it is not in Git, it does not exist.</strong> That includes your ACM policies.</p>
</div>
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
The diagrams in this article have been manually drawn on a whiteboard. AI then generated the SVGs from photos and made them look equal in all articles.
</td>
</tr>
</tbody></table>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_references">References</h2>
<div class="sectionbody">
<div class="ulist">
<ul>
<li>
<p><a href="https://docs.redhat.com/en/documentation/red_hat_advanced_cluster_management_for_kubernetes/2.17/html/gitops/gitops-overview" rel="noopener" target="_blank">ACM 2.17: GitOps (managing policy definitions with OpenShift GitOps, Policy Generator integration)</a></p>
</li>
<li>
<p><a href="https://docs.redhat.com/en/documentation/red_hat_advanced_cluster_management_for_kubernetes/2.17/html/governance/index" rel="noopener" target="_blank">ACM 2.17: Governance (Policy Generator, OperatorPolicy, hub templates)</a></p>
</li>
<li>
<p><a href="https://github.com/open-cluster-management-io/policy-collection" rel="noopener" target="_blank">Open Cluster Management: policy collection</a></p>
</li>
<li>
<p><a href="/gitopscollection/2024-04-02-configure_app_of_apps/">[Ep.4] Configure App-of-Apps</a></p>
</li>
</ul>
</div>
</div>
</div>]]></content:encoded></item><item><title>[Ep.19] ACM and OpenShift GitOps: push, pull and the Argo CD Agent</title><link>https://blog.stderr.at/gitopscollection/2026-10-23-acm-gitops-push-pull-agent/</link><guid isPermaLink="true">https://blog.stderr.at/gitopscollection/2026-10-23-acm-gitops-push-pull-agent/</guid><pubDate>Fri, 23 Oct 2026 00:00:00 +0200</pubDate><dc:creator>Articles by Thomas Jungbauer</dc:creator><category>OpenShift</category><category>GitOps</category><description>How ACM registers clusters in OpenShift GitOps: push model, pull model and the Argo CD Agent add-on, with a step-by-step setup for each approach.</description><content:encoded><![CDATA[<div class="paragraph">
<p>In <a href="/gitopscollection/2026-10-20-openshift-gitops-vs-acm/">[Ep.18]</a> I compared OpenShift GitOps and Advanced Cluster Management and ended with the statement that they are layers, not competitors. ACM knows the fleet and Argo CD delivers the content. This part is about the glue between the two: how ACM registers its clusters in Argo CD, and the three ways an Argo CD Application can reach a managed cluster: the <strong>push model</strong>, the <strong>pull model</strong> and the <strong>Argo CD Agent</strong>.</p>
</div>
<div class="paragraph">
<p>For each of them I will explain what it actually is, how the data flows, and how to set it up step by step.</p>
</div>
<div class="paragraph">
<p>The topic turned out to be too big for a single article, so I have split it into multiple parts and maybe I should have split into even more. This is the second part:</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p><a href="/gitopscollection/2026-10-20-openshift-gitops-vs-acm/">[Ep.18] OpenShift GitOps or Advanced Cluster Management? Choosing the right tool</a> What each tool does, where they overlap and a decision guide.</p>
</li>
<li>
<p><strong>[Ep.19] ACM and OpenShift GitOps: push, pull and the Argo CD Agent</strong> (this article): how ACM registers clusters in Argo CD, and how to set up each of the three delivery models.</p>
</li>
<li>
<p><a href="/gitopscollection/2026-10-24-acm-policies-with-gitops/">[Ep.20] Managing ACM Policies with OpenShift GitOps</a>: policies as code, the PolicyGenerator, hub templates and bootstrapping Argo CD with a policy.</p>
</li>
</ol>
</div>
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
Everything in this series was written against <strong>OpenShift GitOps 1.22</strong> (Argo CD 3.5) and <strong>Advanced Cluster Management 2.17</strong> (Open Cluster Management). Technology Preview and GA states move quickly in both products, so check the release notes of your versions before you commit to a design.
</td>
</tr>
</tbody></table>
</div>
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
While I am using the term &#34;Advanced Cluster Management&#34; in this article, I am referring to the product formerly known as Open Cluster Management. Same counts for Argo CD and OpenShift GitOps.
</td>
</tr>
</tbody></table>
</div>
<div class="sect1">
<h2 id="_the_setup">The setup</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Before we dig into the models, let’s talk about my lab setup.</p>
</div>
<div class="ulist">
<ul>
<li>
<p>There are 3 clusters:</p>
<div class="ulist">
<ul>
<li>
<p><code>hub</code> is the ACM hub with openshift-gitops installed.</p>
</li>
<li>
<p><code>bm</code> is a bare metal cluster with a single node.</p>
</li>
<li>
<p><code>sno</code> a single node cluster.</p>
</li>
</ul>
</div>
</li>
</ul>
</div>
<div class="paragraph">
<p>The <code>hub</code> cluster is the starting point. Argo CD on the hub currently knows one cluster: <code>in-cluster</code>. This entity is automatically added during installation of Argo CD and it is the cluster Argo CD is hosted on.</p>
</div>
<div class="imageblock">
<div class="content">
<img src="/gitopscollection/images/ep-19/argocd-settings-clusters.png" alt="Argo CD settings clusters"/>
</div>
<div class="title">Figure 1. Argo CD settings clusters</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_prerequisites_for_all_models">Prerequisites for all models</h2>
<div class="sectionbody">
<div class="olist arabic">
<ol class="arabic">
<li>
<p>An ACM hub with the OpenShift GitOps Operator installed (the default instance in <code>openshift-gitops</code>).</p>
</li>
<li>
<p>The <strong>ManagedServiceAccount add-on</strong> enabled. ACM uses it to create and rotate the tokens for the push and the pull model. It is enabled by default with current versions of the multicluster engine, but check it if you upgraded from an old release: <code>oc get managedclusteraddon managed-serviceaccount -n &lt;cluster-name&gt;</code>.</p>
<div class="listingblock linenums">
<div class="content">
<pre class="highlightjs highlight"><code class="language-bash hljs" data-lang="bash">oc get managedclusteraddon -A | grep managed-serviceaccount                                                                                                                               ─╯
bm              managed-serviceaccount        True                   False
local-cluster   managed-serviceaccount        True                   False
sno             managed-serviceaccount        True                   False</code></pre>
</div>
</div>
</li>
<li>
<p>The managed clusters <code>sno</code> and <code>bm</code> are in a ManagedClusterSet. In this article I called it <code>production</code>.</p>
<div class="openblock">
<div class="content">
<div class="listingblock linenums">
<div class="title">ManagedClusterSet production</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: cluster.open-cluster-management.io/v1beta2
kind: ManagedClusterSet
metadata:
  name: production <i class="conum" data-value="1"></i><b>(1)</b>
spec:
  clusterSelector:
    selectorType: ExclusiveClusterSetLabel <i class="conum" data-value="2"></i><b>(2)</b></code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Cluster-scoped, so there is no namespace.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>The default. A cluster joins the set through the label <code>cluster.open-cluster-management.io/clusterset=production</code>. A cluster can only be in one such set at a time.</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>Add the clusters to the set by labelling them.</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-bash hljs" data-lang="bash">oc label managedcluster bm cluster.open-cluster-management.io/clusterset=production --overwrite
oc label managedcluster sno cluster.open-cluster-management.io/clusterset=production --overwrite</code></pre>
</div>
</div>
<div class="paragraph">
<p>Check the result:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-bash hljs" data-lang="bash">oc get managedclusterset production
oc get managedclusters -l cluster.open-cluster-management.io/clusterset=production</code></pre>
</div>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-bash hljs" data-lang="bash">NAME         EMPTY   AGE
production   False   53s
NAME   HUB ACCEPTED   MANAGED CLUSTER URLS                   JOINED   AVAILABLE   AGE
bm     true           https://api.bm.stdin.at:6443           True     True        172m
sno    true           https://api.sno.aws.ispworld.at:6443   True     True        51m</code></pre>
</div>
</div>
</div>
</div>
</li>
<li>
<p>A <code>ManagedClusterSetBinding</code> for the cluster set <code>production</code> in the <strong>openshift-gitops</strong> namespace.</p>
<div class="openblock">
<div class="content">
<div class="admonitionblock warning">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-warning" title="Warning"></i>
</td>
<td class="content">
It binds a ManagedClusterSet resource to a <strong>namespace</strong>. This is easy to forget: the Placement for Argo CD lives in the <strong>Argo CD namespace</strong>, so the cluster set must be bound there. Without this binding, the Placement does not find a single cluster.
</td>
</tr>
</tbody></table>
</div>
<div class="listingblock linenums">
<div class="title">ManagedClusterSetBinding in the Argo CD namespace</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: cluster.open-cluster-management.io/v1beta2
kind: ManagedClusterSetBinding
metadata:
  name: production
  namespace: openshift-gitops
spec:
  clusterSet: production</code></pre>
</div>
</div>
<div class="paragraph">
<p>Verify that your managed clusters are in the cluster set:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-bash hljs" data-lang="bash">oc get managedclusters -l cluster.open-cluster-management.io/clusterset=production</code></pre>
</div>
</div>
</div>
</div>
</li>
</ol>
</div>
<div class="paragraph">
<p>The following image shows the currently created objects in the hub cluster:</p>
</div>
<div class="paragraph">
<div class="imageblock animated-svg" data-animated-svg>
  <div class="content">
<svg
   width="1200"
   height="600"
   viewBox="0 0 1200 600"
   role="img"
   aria-label="Objects created in the prerequisites: GitOps instance, ManagedServiceAccount add-on, ManagedClusterSet production and its binding to openshift-gitops"
   version="1.1"
   id="ase622760-svg59"
  
  
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:dc="http://purl.org/dc/elements/1.1/">
  
  <title
     id="ase622760-title1">Objects created in the prerequisites: GitOps instance, ManagedServiceAccount add-on, ManagedClusterSet production and its binding to openshift-gitops</title>
  <defs
     id="ase622760-defs2">
    <rect
       x="0"
       y="529.88306"
       width="460.97997"
       height="100.61071"
       id="ase622760-rect63" />
    <marker
       id="ase622760-arrow-0F766E"
       viewBox="0 0 10 10"
       refX="8.5"
       refY="5"
       markerWidth="7"
       markerHeight="7"
       orient="auto-start-reverse">
      <path
         d="M 0 0 L 10 5 L 0 10 z"
         fill="#0F766E"
         id="ase622760-path1" />
    </marker>
    <marker
       id="ase622760-arrow-4B5563"
       viewBox="0 0 10 10"
       refX="8.5"
       refY="5"
       markerWidth="7"
       markerHeight="7"
       orient="auto-start-reverse">
      <path
         d="M 0 0 L 10 5 L 0 10 z"
         fill="#4B5563"
         id="ase622760-path2" />
    </marker>
  </defs>
  <rect
     width="1200"
     height="600"
     fill="#FFFFFF"
     id="ase622760-rect2" />
  <rect
     x="20"
     y="20"
     width="420"
     height="500"
     rx="14"
     fill="#F8FAFC"
     stroke="#E2E8F0"
     stroke-width="1.5"
     id="ase622760-rect3" />
  <text
     x="40"
     y="48"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="700"
     fill="#4B5563"
     text-anchor="start"
     id="ase622760-text3">HUB  ·  CLUSTER-SCOPED</text>
  <rect
     x="40"
     y="64"
     width="170"
     height="84"
     rx="10"
     fill="#EEF2FF"
     stroke="#4338CA"
     stroke-width="1.6"
     id="ase622760-rect4" />
  <text
     x="56"
     y="93"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="15"
     font-weight="700"
     fill="#4338CA"
     text-anchor="start"
     id="ase622760-text4">bm</text>
  <text
     x="56"
     y="116"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5px"
     font-weight="400"
     fill="#1f2937"
     text-anchor="start"
     id="ase622760-text5"><tspan
       style="font-weight:bold"
       id="ase622760-tspan60">ManagedCluster</tspan></text>
  <rect
     x="40"
     y="168"
     width="170"
     height="84"
     rx="10"
     fill="#EEF2FF"
     stroke="#4338CA"
     stroke-width="1.6"
     id="ase622760-rect5" />
  <text
     x="56"
     y="197"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="15"
     font-weight="700"
     fill="#4338CA"
     text-anchor="start"
     id="ase622760-text6">sno</text>
  <text
     x="56"
     y="220"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5px"
     font-weight="400"
     fill="#1f2937"
     text-anchor="start"
     id="ase622760-text7"><tspan
       style="font-weight:bold"
       id="ase622760-tspan61">ManagedCluster</tspan></text>
  <rect
     x="40"
     y="272"
     width="170"
     height="84"
     rx="10"
     fill="#FFFFFF"
     stroke="#4338CA"
     stroke-width="1.6"
     stroke-dasharray="6 4"
     id="ase622760-rect7" />
  <text
     x="56"
     y="301"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="15"
     font-weight="700"
     fill="#4338CA"
     text-anchor="start"
     id="ase622760-text8">local-cluster</text>
  <text
     x="56"
     y="324"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5px"
     font-weight="400"
     fill="#4b5563"
     text-anchor="start"
     id="ase622760-text9"><tspan
   style="font-weight:bold"
   id="ase622760-tspan62">ManagedCluster</tspan> (the</text>
  <text
     x="56"
     y="340"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5px"
     font-weight="400"
     fill="#4b5563"
     text-anchor="start"
     id="ase622760-text10">hub), not in production Set</text>
  <rect
     x="250"
     y="100"
     width="170"
     height="120"
     rx="10"
     fill="#E6F4F1"
     stroke="#0F766E"
     stroke-width="1.6"
     id="ase622760-rect10" />
  <text
     x="266"
     y="129"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="15px"
     font-weight="700"
     fill="#0f766e"
     text-anchor="start"
     id="ase622760-text12">production</text>
  <text
     x="266"
     y="152"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5px"
     font-weight="400"
     fill="#1f2937"
     text-anchor="start"
     id="ase622760-text13"><tspan
       style="font-weight:bold"
       id="ase622760-tspan59">ManagedClusterSet,</tspan></text>
  <text
     x="266"
     y="168"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#1F2937"
     text-anchor="start"
     id="ase622760-text14">one exclusive set per</text>
  <text
     x="266"
     y="184"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#1F2937"
     text-anchor="start"
     id="ase622760-text15">cluster</text>
  <path
     d="M 212 106 L 248 140"
     fill="none"
     stroke="#0F766E"
     stroke-width="2"
     stroke-linejoin="round"
     marker-end="url(#ase622760-arrow-0F766E)"
     id="ase622760-path15" />
  <path
     d="M 212 210 L 248 190"
     fill="none"
     stroke="#0F766E"
     stroke-width="2"
     stroke-linejoin="round"
     marker-end="url(#ase622760-arrow-0F766E)"
     id="ase622760-path16" />
  <text
     x="40"
     y="392"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="500"
     fill="#1F2937"
     text-anchor="start"
     id="ase622760-text16">bm and sno join the set with the label</text>
  <text
     x="40"
     y="412"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="600"
     fill="#0F766E"
     text-anchor="start"
     id="ase622760-text17">cluster.open-cluster-management.io/</text>
  <text
     x="40"
     y="430"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="600"
     fill="#0F766E"
     text-anchor="start"
     id="ase622760-text18">clusterset=production</text>
  <text
     x="40"
     y="462"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="ase622760-text19">The label is the only link between a</text>
  <text
     x="40"
     y="480"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="ase622760-text20">cluster and its set.</text>
  <rect
     x="460"
     y="20"
     width="720"
     height="500"
     rx="14"
     fill="#F8FAFC"
     stroke="#E2E8F0"
     stroke-width="1.5"
     id="ase622760-rect20" />
  <text
     x="480"
     y="48"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="700"
     fill="#4B5563"
     text-anchor="start"
     id="ase622760-text21">HUB  ·  NAMESPACES</text>
  <rect
     x="480"
     y="52"
     width="680"
     height="196"
     rx="10"
     fill="#FFFFFF"
     stroke="#CBD5E1"
     stroke-width="1.2"
     id="ase622760-rect21" />
  <text
     x="496"
     y="76"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="700"
     fill="#C2410C"
     text-anchor="start"
     id="ase622760-text22">openshift-gitops</text>
  <rect
     x="496"
     y="92"
     width="200"
     height="132"
     rx="10"
     fill="#FDF0E8"
     stroke="#C2410C"
     stroke-width="1.6"
     id="ase622760-rect22" />
  <text
     x="512"
     y="121"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="15px"
     font-weight="700"
     fill="#c2410c"
     text-anchor="start"
     id="ase622760-text24">ArgoCD</text>
  <text
     x="512"
     y="144"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#1F2937"
     text-anchor="start"
     id="ase622760-text25">openshift-gitops, the default</text>
  <text
     x="512"
     y="160"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#1F2937"
     text-anchor="start"
     id="ase622760-text26">instance of the OpenShift</text>
  <text
     x="512"
     y="176"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5px"
     font-weight="400"
     fill="#1f2937"
     text-anchor="start"
     id="ase622760-text27">GitOps Operator</text>
  <rect
     x="716"
     y="92"
     width="210"
     height="132"
     rx="10"
     fill="#E6F4F1"
     stroke="#0F766E"
     stroke-width="1.6"
     id="ase622760-rect27" />
  <text
     x="732"
     y="121"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="15px"
     font-weight="700"
     fill="#0f766e"
     text-anchor="start"
     id="ase622760-text29">Set binding</text>
  <text
     x="732"
     y="144"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5px"
     font-weight="400"
     fill="#1f2937"
     text-anchor="start"
     id="ase622760-text30"><tspan
       style="font-weight:bold"
       id="ase622760-tspan63">ManagedClusterSetBinding</tspan></text>
  <text
     x="732"
     y="160"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#1F2937"
     text-anchor="start"
     id="ase622760-text31">production: makes the set</text>
  <text
     x="732"
     y="176"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#1F2937"
     text-anchor="start"
     id="ase622760-text32">usable in this namespace</text>
  <rect
     x="946"
     y="92"
     width="196"
     height="132"
     rx="10"
     fill="#FFFFFF"
     stroke="#4B5563"
     stroke-width="1.4"
     stroke-dasharray="6 4"
     id="ase622760-rect32" />
  <text
     x="962"
     y="121"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="15"
     font-weight="700"
     fill="#4B5563"
     text-anchor="start"
     id="ase622760-text33">Placement</text>
  <text
     x="962"
     y="144"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="ase622760-text34">next step: selects clusters</text>
  <text
     x="962"
     y="160"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="ase622760-text35">from the bound set</text>
  <path
     d="M 928 158 L 944 158"
     fill="none"
     stroke="#4B5563"
     stroke-width="2"
     stroke-linejoin="round"
     stroke-dasharray="4 3"
     marker-end="url(#ase622760-arrow-4B5563)"
     id="ase622760-path35" />
  <path
     d="M 821 226 L 821 262 L 330 262 L 330 222"
     fill="none"
     stroke="#0F766E"
     stroke-width="2"
     stroke-linejoin="round"
     marker-end="url(#ase622760-arrow-0F766E)"
     id="ase622760-path36" />
  <circle class="flow-dot" style="--d:0.30s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 821 226 L 821 262 L 330 262 L 330 222" begin="indefinite" data-begin="0.30" dur="2.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <rect
     x="500"
     y="252"
     width="168"
     height="20"
     fill="#F8FAFC"
     id="ase622760-rect36" />
  <text
     x="584"
     y="266"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="600"
     fill="#0F766E"
     text-anchor="middle"
     id="ase622760-text36">clusterSet: production</text>
  <text
     x="480"
     y="306"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="700"
     fill="#4B5563"
     text-anchor="start"
     id="ase622760-text37">Cluster namespaces (one per managed cluster, created by ACM)</text>
  <rect
     x="480"
     y="318"
     width="216"
     height="182"
     rx="10"
     fill="#FFFFFF"
     stroke="#CBD5E1"
     stroke-width="1.2"
     id="ase622760-rect37" />
  <text
     x="496"
     y="342"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="700"
     fill="#4338CA"
     text-anchor="start"
     id="ase622760-text38">bm</text>
  <rect
     x="492"
     y="356"
     width="192"
     height="128"
     rx="10"
     fill="#FFFFFF"
     stroke="#0F766E"
     stroke-width="1.6"
     stroke-dasharray="6 4"
     id="ase622760-rect38" />
  <text
     x="508"
     y="385"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="15px"
     font-weight="700"
     fill="#0f766e"
     text-anchor="start"
     id="ase622760-text40">Add-on</text>
  <text
     x="508"
     y="408"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="ase622760-text41">ManagedClusterAddOn</text>
  <text
     x="508"
     y="424"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="ase622760-text42">managed-serviceaccount,</text>
  <text
     x="508"
     y="440"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="ase622760-text43">enabled by default</text>
  <rect
     x="712"
     y="318"
     width="216"
     height="182"
     rx="10"
     fill="#FFFFFF"
     stroke="#CBD5E1"
     stroke-width="1.2"
     id="ase622760-rect43" />
  <text
     x="728"
     y="342"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="700"
     fill="#4338CA"
     text-anchor="start"
     id="ase622760-text44">sno</text>
  <rect
     x="724"
     y="356"
     width="192"
     height="128"
     rx="10"
     fill="#FFFFFF"
     stroke="#0F766E"
     stroke-width="1.6"
     stroke-dasharray="6 4"
     id="ase622760-rect44" />
  <text
     x="740"
     y="385"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="15px"
     font-weight="700"
     fill="#0f766e"
     text-anchor="start"
     id="ase622760-text46">Add-on</text>
  <text
     x="740"
     y="408"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="ase622760-text47">ManagedClusterAddOn</text>
  <text
     x="740"
     y="424"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="ase622760-text48">managed-serviceaccount,</text>
  <text
     x="740"
     y="440"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="ase622760-text49">enabled by default</text>
  <rect
     x="944"
     y="318"
     width="216"
     height="182"
     rx="10"
     fill="#FFFFFF"
     stroke="#CBD5E1"
     stroke-width="1.2"
     id="ase622760-rect49" />
  <text
     x="960"
     y="342"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="700"
     fill="#4338CA"
     text-anchor="start"
     id="ase622760-text50">local-cluster</text>
  <rect
     x="956"
     y="356"
     width="192"
     height="128"
     rx="10"
     fill="#FFFFFF"
     stroke="#0F766E"
     stroke-width="1.6"
     stroke-dasharray="6 4"
     id="ase622760-rect50" />
  <text
     x="972"
     y="385"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="15px"
     font-weight="700"
     fill="#0f766e"
     text-anchor="start"
     id="ase622760-text52">Add-on</text>
  <text
     x="972"
     y="408"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="ase622760-text53">ManagedClusterAddOn</text>
  <text
     x="972"
     y="424"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="ase622760-text54">managed-serviceaccount,</text>
  <text
     x="972"
     y="440"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="ase622760-text55">enabled by default</text>
  <rect
     x="40"
     y="557"
     width="22"
     height="14"
     rx="3"
     fill="#E6F4F1"
     stroke="#0F766E"
     stroke-width="1.4"
     id="ase622760-rect55" />
  <text
     x="70"
     y="569"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12px"
     font-weight="400"
     fill="#1f2937"
     text-anchor="start"
     id="ase622760-text56">created by you</text>
  <rect
     x="166.234375"
     y="557"
     width="22"
     height="14"
     rx="3"
     fill="#FFFFFF"
     stroke="#0F766E"
     stroke-width="1.4"
     stroke-dasharray="4 3"
     id="ase622760-rect56" />
  <text
     x="196.23438"
     y="569"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12px"
     font-weight="400"
     fill="#1f2937"
     text-anchor="start"
     id="ase622760-text57">generated by ACM</text>
  
<circle class="flow-dot" style="--d:0.00s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 212 106 L 248 140" begin="indefinite" data-begin="0.00" dur="1.20s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:0.60s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 212 210 L 248 190" begin="indefinite" data-begin="0.60" dur="1.20s" repeatCount="indefinite" calcMode="linear"/></circle>
  </svg>
</div>
  <div class="title">Currently created objects in the hub cluster</div>
</div>

</div>
<hr/>
</div>
</div>
<div class="sect1">
<h2 id="_the_bridge_placement_gitopscluster_and_applicationset">The bridge: Placement, GitOpsCluster and ApplicationSet</h2>
<div class="sectionbody">
<div class="paragraph">
<p>So far, we only created the prerequisites for all three models. When we now look at the models all three of them start with the same chain of objects on the hub:</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>a <strong>Placement</strong> decides <em>which</em> managed clusters are used,</p>
</li>
<li>
<p>a <strong>GitOpsCluster</strong> makes these clusters known to an Argo CD instance, and</p>
</li>
<li>
<p>an <strong>ApplicationSet</strong> creates one Application per selected cluster.</p>
</li>
</ol>
</div>
<div class="paragraph">
<p>The GitOpsCluster is the actual bridge between ACM and Argo CD. ACM knows the clusters, Argo CD knows the Applications, and the GitOpsCluster translates one into the other.</p>
</div>
<div class="paragraph">
<p>What differs between the models is what happens <em>after</em> this chain: <em>who</em> talks to the managed cluster, <em>in which direction</em> the network connection goes, and <em>where</em> the reconciliation happens. That is why the GitOpsCluster and the ApplicationSet look different in each model and are created in the model sections below.</p>
</div>
<div class="paragraph">
<p>The Placement is identical for all three, so we create it now.</p>
</div>
<div class="sect2">
<h3 id="_the_placement">The Placement</h3>
<div class="paragraph">
<p>The Placement lives in the Argo CD namespace, next to the ManagedClusterSetBinding from the prerequisites. It selects the OpenShift clusters of the cluster set <code>production</code>.</p>
</div>
<div class="listingblock linenums">
<div class="title">Placement in the Argo CD namespace</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: cluster.open-cluster-management.io/v1beta1
kind: Placement
metadata:
  name: production-clusters
  namespace: openshift-gitops <i class="conum" data-value="1"></i><b>(1)</b>
spec:
  clusterSets:
    - production <i class="conum" data-value="2"></i><b>(2)</b>
  predicates:
    - requiredClusterSelector: <i class="conum" data-value="3"></i><b>(3)</b>
        labelSelector:
          matchExpressions:
            - key: vendor
              operator: In
              values:
                - OpenShift
  tolerations: <i class="conum" data-value="4"></i><b>(4)</b>
    - key: cluster.open-cluster-management.io/unreachable
      operator: Exists
    - key: cluster.open-cluster-management.io/unavailable
      operator: Exists</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>The same namespace as the GitOpsCluster that will reference it.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Represents the ManagedClusterSets from which the ManagedClusters are selected.</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>The predicate to select only OpenShift clusters.</td>
</tr>
<tr>
<td><i class="conum" data-value="4"></i><b>4</b></td>
<td>By default a Placement drops clusters that are unreachable. For GitOps we want to keep them, so that a short network hiccup does not remove the cluster secret and, worst case, the generated Applications.</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>The result of a Placement is a <strong>PlacementDecision</strong>. Check which clusters were selected:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-bash hljs" data-lang="bash">oc get placementdecisions -n openshift-gitops \
  -l cluster.open-cluster-management.io/placement=production-clusters \
  -o jsonpath=&#39;{.items[*].status.decisions[*].clusterName}{&#34;\n&#34;}&#39;</code></pre>
</div>
</div>
<div class="paragraph">
<p>With <code>bm</code> and <code>sno</code> in the set, the output is:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-none hljs">bm sno</code></pre>
</div>
</div>
<div class="paragraph">
<p>The Placement and its PlacementDecision complete the picture on the hub:</p>
</div>
<div class="paragraph">
<div class="imageblock animated-svg" data-animated-svg>
  <div class="content">
<svg
   width="1200"
   height="720"
   viewBox="0 0 1200 720"
   role="img"
   aria-label="Objects on the hub: managed clusters, ManagedClusterSet production, the binding to openshift-gitops, the Placement production-clusters with its PlacementDecision (bm, sno), and the ManagedServiceAccount add-ons"
   version="1.1"
   id="as5de8cd1-svg59"
  
  
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:dc="http://purl.org/dc/elements/1.1/">
  
  <title
     id="as5de8cd1-title1">Objects on the hub: managed clusters, ManagedClusterSet production, the binding to openshift-gitops, the Placement production-clusters with its PlacementDecision (bm, sno), and the ManagedServiceAccount add-ons</title>
  <defs
     id="as5de8cd1-defs2">
    <rect
       x="0"
       y="529.88306"
       width="460.97997"
       height="100.61071"
       id="as5de8cd1-rect63" />
    <marker
       id="as5de8cd1-arrow-0F766E"
       viewBox="0 0 10 10"
       refX="8.5"
       refY="5"
       markerWidth="7"
       markerHeight="7"
       orient="auto-start-reverse">
      <path
         d="M 0 0 L 10 5 L 0 10 z"
         fill="#0F766E"
         id="as5de8cd1-path1" />
    </marker>
    <marker
       id="as5de8cd1-arrow-4B5563"
       viewBox="0 0 10 10"
       refX="8.5"
       refY="5"
       markerWidth="7"
       markerHeight="7"
       orient="auto-start-reverse">
      <path
         d="M 0 0 L 10 5 L 0 10 z"
         fill="#4B5563"
         id="as5de8cd1-path2" />
    </marker>
  </defs>
  <rect
     width="1200"
     height="720"
     fill="#FFFFFF"
     id="as5de8cd1-rect2" />
  <rect
     x="20"
     y="20"
     width="420"
     height="614"
     rx="14"
     fill="#F8FAFC"
     stroke="#E2E8F0"
     stroke-width="1.5"
     id="as5de8cd1-rect3" />
  <text
     x="40"
     y="48"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="700"
     fill="#4B5563"
     text-anchor="start"
     id="as5de8cd1-text3">HUB  ·  CLUSTER-SCOPED</text>
  <rect
     x="40"
     y="64"
     width="170"
     height="84"
     rx="10"
     fill="#EEF2FF"
     stroke="#4338CA"
     stroke-width="1.6"
     id="as5de8cd1-rect4" />
  <text
     x="56"
     y="93"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="15"
     font-weight="700"
     fill="#4338CA"
     text-anchor="start"
     id="as5de8cd1-text4">bm</text>
  <text
     x="56"
     y="116"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5px"
     font-weight="400"
     fill="#1f2937"
     text-anchor="start"
     id="as5de8cd1-text5"><tspan
       style="font-weight:bold"
       id="as5de8cd1-tspan60">ManagedCluster</tspan></text>
  <rect
     x="40"
     y="168"
     width="170"
     height="84"
     rx="10"
     fill="#EEF2FF"
     stroke="#4338CA"
     stroke-width="1.6"
     id="as5de8cd1-rect5" />
  <text
     x="56"
     y="197"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="15"
     font-weight="700"
     fill="#4338CA"
     text-anchor="start"
     id="as5de8cd1-text6">sno</text>
  <text
     x="56"
     y="220"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5px"
     font-weight="400"
     fill="#1f2937"
     text-anchor="start"
     id="as5de8cd1-text7"><tspan
       style="font-weight:bold"
       id="as5de8cd1-tspan61">ManagedCluster</tspan></text>
  <rect
     x="40"
     y="272"
     width="170"
     height="84"
     rx="10"
     fill="#FFFFFF"
     stroke="#4338CA"
     stroke-width="1.6"
     stroke-dasharray="6 4"
     id="as5de8cd1-rect7" />
  <text
     x="56"
     y="301"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="15"
     font-weight="700"
     fill="#4338CA"
     text-anchor="start"
     id="as5de8cd1-text8">local-cluster</text>
  <text
     x="56"
     y="324"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5px"
     font-weight="400"
     fill="#4b5563"
     text-anchor="start"
     id="as5de8cd1-text9"><tspan
   style="font-weight:bold"
   id="as5de8cd1-tspan62">ManagedCluster</tspan> (the</text>
  <text
     x="56"
     y="340"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5px"
     font-weight="400"
     fill="#4b5563"
     text-anchor="start"
     id="as5de8cd1-text10">hub), not in production Set</text>
  <rect
     x="250"
     y="100"
     width="170"
     height="120"
     rx="10"
     fill="#E6F4F1"
     stroke="#0F766E"
     stroke-width="1.6"
     id="as5de8cd1-rect10" />
  <text
     x="266"
     y="129"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="15px"
     font-weight="700"
     fill="#0f766e"
     text-anchor="start"
     id="as5de8cd1-text12">production</text>
  <text
     x="266"
     y="152"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5px"
     font-weight="400"
     fill="#1f2937"
     text-anchor="start"
     id="as5de8cd1-text13"><tspan
       style="font-weight:bold"
       id="as5de8cd1-tspan59">ManagedClusterSet,</tspan></text>
  <text
     x="266"
     y="168"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#1F2937"
     text-anchor="start"
     id="as5de8cd1-text14">one exclusive set per</text>
  <text
     x="266"
     y="184"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#1F2937"
     text-anchor="start"
     id="as5de8cd1-text15">cluster</text>
  <path
     d="M 212 106 L 248 140"
     fill="none"
     stroke="#0F766E"
     stroke-width="2"
     stroke-linejoin="round"
     marker-end="url(#as5de8cd1-arrow-0F766E)"
     id="as5de8cd1-path15" />
  <path
     d="M 212 210 L 248 190"
     fill="none"
     stroke="#0F766E"
     stroke-width="2"
     stroke-linejoin="round"
     marker-end="url(#as5de8cd1-arrow-0F766E)"
     id="as5de8cd1-path16" />
  <text
     x="40"
     y="392"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="500"
     fill="#1F2937"
     text-anchor="start"
     id="as5de8cd1-text16">bm and sno join the set with the label</text>
  <text
     x="40"
     y="412"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="600"
     fill="#0F766E"
     text-anchor="start"
     id="as5de8cd1-text17">cluster.open-cluster-management.io/</text>
  <text
     x="40"
     y="430"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="600"
     fill="#0F766E"
     text-anchor="start"
     id="as5de8cd1-text18">clusterset=production</text>
  <text
     x="40"
     y="462"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="as5de8cd1-text19">The label is the only link between a</text>
  <text
     x="40"
     y="480"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="as5de8cd1-text20">cluster and its set.</text>
  <rect
     x="460"
     y="20"
     width="720"
     height="614"
     rx="14"
     fill="#F8FAFC"
     stroke="#E2E8F0"
     stroke-width="1.5"
     id="as5de8cd1-rect20" />
  <text
     x="480"
     y="48"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="700"
     fill="#4B5563"
     text-anchor="start"
     id="as5de8cd1-text21">HUB  ·  NAMESPACES</text>
  <rect
     x="480"
     y="52"
     width="680"
     height="344"
     rx="10"
     fill="#FFFFFF"
     stroke="#CBD5E1"
     stroke-width="1.2"
     id="as5de8cd1-rect21" />
  <text
     x="496"
     y="76"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="700"
     fill="#C2410C"
     text-anchor="start"
     id="as5de8cd1-text22">openshift-gitops</text>
  <rect
     x="496"
     y="92"
     width="200"
     height="132"
     rx="10"
     fill="#FDF0E8"
     stroke="#C2410C"
     stroke-width="1.6"
     id="as5de8cd1-rect22" />
  <text
     x="512"
     y="121"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="15px"
     font-weight="700"
     fill="#c2410c"
     text-anchor="start"
     id="as5de8cd1-text24">ArgoCD</text>
  <text
     x="512"
     y="144"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#1F2937"
     text-anchor="start"
     id="as5de8cd1-text25">openshift-gitops, the default</text>
  <text
     x="512"
     y="160"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#1F2937"
     text-anchor="start"
     id="as5de8cd1-text26">instance of the OpenShift</text>
  <text
     x="512"
     y="176"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5px"
     font-weight="400"
     fill="#1f2937"
     text-anchor="start"
     id="as5de8cd1-text27">GitOps Operator</text>
  <rect
     x="716"
     y="92"
     width="210"
     height="132"
     rx="10"
     fill="#E6F4F1"
     stroke="#0F766E"
     stroke-width="1.6"
     id="as5de8cd1-rect27" />
  <text
     x="732"
     y="121"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="15px"
     font-weight="700"
     fill="#0f766e"
     text-anchor="start"
     id="as5de8cd1-text29">Set binding</text>
  <text
     x="732"
     y="144"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5px"
     font-weight="400"
     fill="#1f2937"
     text-anchor="start"
     id="as5de8cd1-text30"><tspan
       style="font-weight:bold"
       id="as5de8cd1-tspan63">ManagedClusterSetBinding</tspan></text>
  <text
     x="732"
     y="160"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#1F2937"
     text-anchor="start"
     id="as5de8cd1-text31">production: makes the set</text>
  <text
     x="732"
     y="176"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#1F2937"
     text-anchor="start"
     id="as5de8cd1-text32">usable in this namespace</text>
  <rect
     x="946"
     y="92"
     width="196"
     height="132"
     rx="10"
     fill="#E6F4F1"
     stroke="#0F766E"
     stroke-width="1.6"
    
     id="as5de8cd1-rect32" />
  <text
     x="962"
     y="121"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="15"
     font-weight="700"
     fill="#0F766E"
     text-anchor="start"
     id="as5de8cd1-text33">Placement</text>
  <text
     x="962"
     y="144"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#1F2937"
     text-anchor="start"
     id="as5de8cd1-text34"><tspan style="font-weight:bold" id="as5de8cd1-tspan-placement-name">production-clusters</tspan></text>
  <text
     x="962"
     y="160"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#1F2937"
     text-anchor="start"
     id="as5de8cd1-text35">clusterSets: production</text>
  <text x="962" y="176" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start" id="as5de8cd1-text-placement-pred">vendor In OpenShift</text>
  <path
     d="M 928 158 L 944 158"
     fill="none"
     stroke="#0F766E"
     stroke-width="2"
     stroke-linejoin="round"
    
     marker-end="url(#as5de8cd1-arrow-0F766E)"
     id="as5de8cd1-path35" />
  <path d="M 1044 226 L 1044 266" fill="none" stroke="#0F766E" stroke-width="2" stroke-linejoin="round" marker-end="url(#as5de8cd1-arrow-0F766E)" id="as5de8cd1-path-placement-decision" />
  <text x="1054" y="251" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="600" fill="#0F766E" text-anchor="start" id="as5de8cd1-text-generates">generates</text>
  <rect x="946" y="270" width="196" height="108" rx="10" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.6" stroke-dasharray="6 4" id="as5de8cd1-rect-decision" />
  <text x="962" y="298" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#0F766E" text-anchor="start" id="as5de8cd1-text-decision-title">PlacementDecision</text>
  <text x="962" y="320" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start" id="as5de8cd1-text-decision-label">decisions:</text>
  <rect x="1030" y="306" width="40" height="22" rx="6" fill="#EEF2FF" stroke="#4338CA" stroke-width="1.4" id="as5de8cd1-rect-decision-bm" />
  <text x="1050" y="321" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#4338CA" text-anchor="middle" id="as5de8cd1-text-decision-bm">bm</text>
  <rect x="1078" y="306" width="46" height="22" rx="6" fill="#EEF2FF" stroke="#4338CA" stroke-width="1.4" id="as5de8cd1-rect-decision-sno" />
  <text x="1101" y="321" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#4338CA" text-anchor="middle" id="as5de8cd1-text-decision-sno">sno</text>
  <text x="962" y="350" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#4B5563" text-anchor="start" id="as5de8cd1-text-decision-note1">local-cluster is not in the</text>
  <text x="962" y="366" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#4B5563" text-anchor="start" id="as5de8cd1-text-decision-note2">set, so it is never selected</text>
  <path
     d="M 821 226 L 821 262 L 330 262 L 330 222"
     fill="none"
     stroke="#0F766E"
     stroke-width="2"
     stroke-linejoin="round"
     marker-end="url(#as5de8cd1-arrow-0F766E)"
     id="as5de8cd1-path36" />
  <circle class="flow-dot" style="--d:0.30s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 821 226 L 821 262 L 330 262 L 330 222" begin="indefinite" data-begin="0.30" dur="2.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <rect
     x="500"
     y="252"
     width="168"
     height="20"
     fill="#FFFFFF"
     id="as5de8cd1-rect36" />
  <text
     x="584"
     y="266"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="600"
     fill="#0F766E"
     text-anchor="middle"
     id="as5de8cd1-text36">clusterSet: production</text>
  <text
     x="480"
     y="420"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="700"
     fill="#4B5563"
     text-anchor="start"
     id="as5de8cd1-text37">Cluster namespaces (one per managed cluster, created by ACM)</text>
  <rect
     x="480"
     y="432"
     width="216"
     height="182"
     rx="10"
     fill="#FFFFFF"
     stroke="#CBD5E1"
     stroke-width="1.2"
     id="as5de8cd1-rect37" />
  <text
     x="496"
     y="456"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="700"
     fill="#4338CA"
     text-anchor="start"
     id="as5de8cd1-text38">bm</text>
  <rect
     x="492"
     y="470"
     width="192"
     height="128"
     rx="10"
     fill="#FFFFFF"
     stroke="#0F766E"
     stroke-width="1.6"
     stroke-dasharray="6 4"
     id="as5de8cd1-rect38" />
  <text
     x="508"
     y="499"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="15px"
     font-weight="700"
     fill="#0f766e"
     text-anchor="start"
     id="as5de8cd1-text40">Add-on</text>
  <text
     x="508"
     y="522"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="as5de8cd1-text41">ManagedClusterAddOn</text>
  <text
     x="508"
     y="538"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="as5de8cd1-text42">managed-serviceaccount,</text>
  <text
     x="508"
     y="554"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="as5de8cd1-text43">enabled by default</text>
  <rect
     x="712"
     y="432"
     width="216"
     height="182"
     rx="10"
     fill="#FFFFFF"
     stroke="#CBD5E1"
     stroke-width="1.2"
     id="as5de8cd1-rect43" />
  <text
     x="728"
     y="456"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="700"
     fill="#4338CA"
     text-anchor="start"
     id="as5de8cd1-text44">sno</text>
  <rect
     x="724"
     y="470"
     width="192"
     height="128"
     rx="10"
     fill="#FFFFFF"
     stroke="#0F766E"
     stroke-width="1.6"
     stroke-dasharray="6 4"
     id="as5de8cd1-rect44" />
  <text
     x="740"
     y="499"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="15px"
     font-weight="700"
     fill="#0f766e"
     text-anchor="start"
     id="as5de8cd1-text46">Add-on</text>
  <text
     x="740"
     y="522"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="as5de8cd1-text47">ManagedClusterAddOn</text>
  <text
     x="740"
     y="538"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="as5de8cd1-text48">managed-serviceaccount,</text>
  <text
     x="740"
     y="554"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="as5de8cd1-text49">enabled by default</text>
  <rect
     x="944"
     y="432"
     width="216"
     height="182"
     rx="10"
     fill="#FFFFFF"
     stroke="#CBD5E1"
     stroke-width="1.2"
     id="as5de8cd1-rect49" />
  <text
     x="960"
     y="456"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12"
     font-weight="700"
     fill="#4338CA"
     text-anchor="start"
     id="as5de8cd1-text50">local-cluster</text>
  <rect
     x="956"
     y="470"
     width="192"
     height="128"
     rx="10"
     fill="#FFFFFF"
     stroke="#0F766E"
     stroke-width="1.6"
     stroke-dasharray="6 4"
     id="as5de8cd1-rect50" />
  <text
     x="972"
     y="499"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="15px"
     font-weight="700"
     fill="#0f766e"
     text-anchor="start"
     id="as5de8cd1-text52">Add-on</text>
  <text
     x="972"
     y="522"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="as5de8cd1-text53">ManagedClusterAddOn</text>
  <text
     x="972"
     y="538"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="as5de8cd1-text54">managed-serviceaccount,</text>
  <text
     x="972"
     y="554"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12.5"
     font-weight="400"
     fill="#4B5563"
     text-anchor="start"
     id="as5de8cd1-text55">enabled by default</text>
  <rect
     x="40"
     y="671"
     width="22"
     height="14"
     rx="3"
     fill="#E6F4F1"
     stroke="#0F766E"
     stroke-width="1.4"
     id="as5de8cd1-rect55" />
  <text
     x="70"
     y="683"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12px"
     font-weight="400"
     fill="#1f2937"
     text-anchor="start"
     id="as5de8cd1-text56">created by you</text>
  <rect
     x="166.234375"
     y="671"
     width="22"
     height="14"
     rx="3"
     fill="#FFFFFF"
     stroke="#0F766E"
     stroke-width="1.4"
     stroke-dasharray="4 3"
     id="as5de8cd1-rect56" />
  <text
     x="196.23438"
     y="683"
     font-family="Inter, 'Liberation Sans', Arial, sans-serif"
     font-size="12px"
     font-weight="400"
     fill="#1f2937"
     text-anchor="start"
     id="as5de8cd1-text57">generated by ACM</text>
  
<circle class="flow-dot" style="--d:0.00s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 212 106 L 248 140" begin="indefinite" data-begin="0.00" dur="1.20s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:0.60s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 212 210 L 248 190" begin="indefinite" data-begin="0.60" dur="1.20s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:0.90s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 1044 226 L 1044 266" begin="indefinite" data-begin="0.90" dur="0.90s" repeatCount="indefinite" calcMode="linear"/></circle>
  </svg>
</div>
  <div class="title">Objects in the hub cluster after creating the Placement</div>
</div>

</div>
<div class="paragraph">
<p>At this point nothing has happened in Argo CD yet: <em>Settings &gt; Clusters</em> still shows only <code>in-cluster</code>. The Placement only <em>decides</em> what clusters could be selected. The GitOpsCluster in the next section turns the decision into Argo CD clusters.</p>
</div>
<hr/>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_the_push_model">The push model</h2>
<div class="sectionbody">
<div class="sect2">
<h3 id="_what_it_actually_is">What it actually is</h3>
<div class="paragraph">
<p>The push model is &#34;classic&#34; Argo CD. One Argo CD instance on the hub renders the manifests and applies them to the API server of every managed cluster, by default through the cluster proxy of the multicluster engine. ACM contributes the cluster registration, the credentials and the connection path.</p>
</div>
<div class="paragraph">
<div class="imageblock animated-svg" data-animated-svg>
  <div class="content">
<svg width="1400" height="620" viewBox="0 0 1400 620" role="img" aria-label="Push model: Placement, PlacementDecision and GitOpsCluster create a cluster secret on the hub; the ApplicationSet generates one Application per cluster. The hub Argo CD application-controller sends its API requests to the cluster-proxy on the hub, which forwards them through a tunnel opened by the managed cluster; only without the proxy does it fall back to the API server URL of the managed cluster" version="1.1" id="as4f6ec12-svg-push-model" xmlns="http://www.w3.org/2000/svg">
  <title id="as4f6ec12-title-push-model">Push model: Placement, PlacementDecision and GitOpsCluster create a cluster secret on the hub; the ApplicationSet generates one Application per cluster. The hub Argo CD application-controller sends its API requests to the cluster-proxy on the hub, which forwards them through a tunnel opened by the managed cluster; only without the proxy does it fall back to the API server URL of the managed cluster</title>
  <defs id="as4f6ec12-defs-push-model">
    <marker id="as4f6ec12-arrow-0F766E" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#0F766E" id="as4f6ec12-marker-path-0F766E" /></marker>
    <marker id="as4f6ec12-arrow-C2410C" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#C2410C" id="as4f6ec12-marker-path-C2410C" /></marker>
    <marker id="as4f6ec12-arrow-4B5563" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#4B5563" id="as4f6ec12-marker-path-4B5563" /></marker>
    </defs>
  <rect id="as4f6ec12-bg" width="1400" height="620" fill="#FFFFFF" />
  <rect id="as4f6ec12-zone-hub" x="20" y="20" width="970" height="560" rx="14" fill="#F8FAFC" stroke="#E2E8F0" stroke-width="1.5" />
  <text id="as4f6ec12-label-hub" x="40" y="48" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="700" fill="#4B5563" text-anchor="start">HUB</text>
  <rect id="as4f6ec12-zone-spoke" x="1050" y="20" width="330" height="560" rx="14" fill="#F8FAFC" stroke="#E2E8F0" stroke-width="1.5" />
  <text id="as4f6ec12-label-spoke" x="1070" y="48" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="700" fill="#4B5563" text-anchor="start">MANAGED CLUSTER  ·  bm, sno</text>
  <rect id="as4f6ec12-ns-gitops" x="40" y="60" width="730" height="480" rx="10" fill="#FFFFFF" stroke="#CBD5E1" stroke-width="1.2" />
  <text id="as4f6ec12-label-ns" x="56" y="84" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="700" fill="#C2410C" text-anchor="start">openshift-gitops</text>
  <rect id="as4f6ec12-ns-mce" x="790" y="360" width="185" height="180" rx="10" fill="#FFFFFF" stroke="#CBD5E1" stroke-width="1.2" />
  <text id="as4f6ec12-label-ns-mce" x="806" y="384" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="700" fill="#4B5563" text-anchor="start">multicluster-engine</text>
  <rect id="as4f6ec12-rect-placement" x="60" y="100" width="180" height="84" rx="10" fill="#E6F4F1" stroke="#0F766E" stroke-width="1.6" />
  <text id="as4f6ec12-title-placement" x="76" y="129" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#0F766E" text-anchor="start">Placement</text>
  <text id="as4f6ec12-line0-placement" x="76" y="152" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#1F2937" text-anchor="start">production-clusters</text>
  <rect id="as4f6ec12-rect-decision" x="290" y="100" width="190" height="84" rx="10" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="as4f6ec12-title-decision" x="306" y="129" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#0F766E" text-anchor="start">PlacementDecision</text>
  <text id="as4f6ec12-line0-decision" x="306" y="152" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">decisions: bm, sno</text>
  <rect id="as4f6ec12-rect-gitopscluster" x="540" y="100" width="210" height="84" rx="10" fill="#E6F4F1" stroke="#0F766E" stroke-width="1.6" />
  <text id="as4f6ec12-title-gitopscluster" x="556" y="129" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#0F766E" text-anchor="start">GitOpsCluster</text>
  <text id="as4f6ec12-line0-gitopscluster" x="556" y="152" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">bridge from ACM to Argo CD</text>
  <rect id="as4f6ec12-rect-secret" x="540" y="232" width="210" height="108" rx="10" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="as4f6ec12-title-secret" x="556" y="261" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#0F766E" text-anchor="start">Cluster secret</text>
  <text id="as4f6ec12-line0-secret" x="556" y="284" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">one per cluster, token of the</text>
  <text id="as4f6ec12-line1-secret" x="556" y="300" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#4B5563" text-anchor="start">ManagedServiceAccount</text>
  <text id="as4f6ec12-line2-secret" x="556" y="316" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">application-manager</text>
  <rect id="as4f6ec12-rect-appset" x="60" y="392" width="180" height="96" rx="10" fill="#E6F4F1" stroke="#0F766E" stroke-width="1.6" />
  <text id="as4f6ec12-title-appset" x="76" y="421" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#0F766E" text-anchor="start">ApplicationSet</text>
  <text id="as4f6ec12-line0-appset" x="76" y="444" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">generator:</text>
  <text id="as4f6ec12-line1-appset" x="76" y="460" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#1F2937" text-anchor="start">clusterDecisionResource</text>
  <rect id="as4f6ec12-rect-application" x="290" y="392" width="190" height="96" rx="10" fill="#FFFFFF" stroke="#C2410C" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="as4f6ec12-title-application" x="306" y="421" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#C2410C" text-anchor="start">Application</text>
  <text id="as4f6ec12-line0-application" x="306" y="444" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">one per selected cluster</text>
  <rect id="as4f6ec12-rect-controller" x="540" y="392" width="210" height="96" rx="10" fill="#FDF0E8" stroke="#C2410C" stroke-width="1.6" />
  <text id="as4f6ec12-title-controller" x="556" y="421" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#C2410C" text-anchor="start">Argo CD</text>
  <text id="as4f6ec12-line0-controller" x="556" y="444" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#1F2937" text-anchor="start">application-controller</text>
  <text id="as4f6ec12-line1-controller" x="556" y="460" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">reconciles all clusters</text>
  <rect id="as4f6ec12-rect-proxy" x="800" y="396" width="168" height="96" rx="10" fill="#F1F5F9" stroke="#4B5563" stroke-width="1.4" />
  <text id="as4f6ec12-title-proxy" x="816" y="425" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#4B5563" text-anchor="start">cluster-proxy</text>
  <text id="as4f6ec12-line0-proxy" x="816" y="448" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">default server URL</text>
  <text id="as4f6ec12-line1-proxy" x="816" y="464" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">in the cluster secret</text>
  <text id="as4f6ec12-note-spoke1" x="1070" y="124" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#1F2937" text-anchor="start">No Argo CD on the spoke:</text>
  <text id="as4f6ec12-note-spoke2" x="1070" y="142" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">the hub reconciles everything.</text>
  <text id="as4f6ec12-note-spoke3" x="1070" y="160" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">No hub, no reconciliation.</text>
  <rect id="as4f6ec12-rect-resources" x="1070" y="232" width="290" height="108" rx="10" fill="#FFFFFF" stroke="#C2410C" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="as4f6ec12-title-resources" x="1086" y="261" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#C2410C" text-anchor="start">Resources</text>
  <text id="as4f6ec12-line0-resources" x="1086" y="284" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">applied by the hub Argo CD,</text>
  <text id="as4f6ec12-line1-resources" x="1086" y="300" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">e.g. Namespaces, Deployments</text>
  <rect id="as4f6ec12-rect-apiserver" x="1070" y="392" width="290" height="96" rx="10" fill="#FFFFFF" stroke="#4B5563" stroke-width="1.4" />
  <text id="as4f6ec12-title-apiserver" x="1086" y="421" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#4338CA" text-anchor="start">kube-apiserver</text>
  <text id="as4f6ec12-line0-apiserver" x="1086" y="444" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">of each managed cluster,</text>
  <text id="as4f6ec12-line1-apiserver" x="1086" y="460" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">cluster-proxy agent alongside</text>
  <path id="as4f6ec12-flow-placement-decision" d="M 242 142 L 286 142" fill="none" stroke="#0F766E" stroke-width="2" stroke-linejoin="round" marker-end="url(#as4f6ec12-arrow-0F766E)" />
  <path id="as4f6ec12-flow-decision-gitopscluster" d="M 482 142 L 536 142" fill="none" stroke="#0F766E" stroke-width="2" stroke-linejoin="round" marker-end="url(#as4f6ec12-arrow-0F766E)" />
  <path id="as4f6ec12-flow-gitopscluster-secret" d="M 645 186 L 645 228" fill="none" stroke="#0F766E" stroke-width="2" stroke-linejoin="round" marker-end="url(#as4f6ec12-arrow-0F766E)" />
  <text id="as4f6ec12-label-creates" x="655" y="212" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="600" fill="#0F766E" text-anchor="start">creates</text>
  <path id="as4f6ec12-flow-secret-controller" d="M 645 342 L 645 388" fill="none" stroke="#0F766E" stroke-width="2" stroke-linejoin="round" marker-end="url(#as4f6ec12-arrow-0F766E)" />
  <text id="as4f6ec12-label-usedby" x="655" y="370" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="600" fill="#0F766E" text-anchor="start">used by</text>
  <path id="as4f6ec12-flow-decision-appset" d="M 385 186 L 385 290 L 150 290 L 150 388" fill="none" stroke="#0F766E" stroke-width="2" stroke-linejoin="round" marker-end="url(#as4f6ec12-arrow-0F766E)" />
  <text id="as4f6ec12-label-reads" x="162" y="282" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="600" fill="#0F766E" text-anchor="start">reads the decisions</text>
  <path id="as4f6ec12-flow-appset-application" d="M 242 440 L 286 440" fill="none" stroke="#C2410C" stroke-width="2" stroke-linejoin="round" marker-end="url(#as4f6ec12-arrow-C2410C)" />
  <path id="as4f6ec12-flow-application-controller" d="M 482 440 L 536 440" fill="none" stroke="#C2410C" stroke-width="2" stroke-linejoin="round" marker-end="url(#as4f6ec12-arrow-C2410C)" />
  <path id="as4f6ec12-flow-controller-proxy" d="M 752 430 L 796 430" fill="none" stroke="#C2410C" stroke-width="2" stroke-linejoin="round" marker-end="url(#as4f6ec12-arrow-C2410C)" />
  <text id="as4f6ec12-label-https" x="774" y="421" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="700" fill="#C2410C" text-anchor="middle">HTTPS</text>
  <circle class="flow-dot" style="--d:0.00s" visibility="hidden" r="3.5" fill="#4B5563" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 1068 462 L 974 462" begin="indefinite" data-begin="0.00" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <rect id="as4f6ec12-bg-tunnel-label" x="974" y="472" width="96" height="30" fill="#FFFFFF" />
  <path id="as4f6ec12-flow-proxy-apiserver" d="M 970 430 L 1066 430" fill="none" stroke="#C2410C" stroke-width="2" stroke-linejoin="round" marker-end="url(#as4f6ec12-arrow-C2410C)" />
  <text id="as4f6ec12-label-requests" x="1014" y="421" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="700" fill="#C2410C" text-anchor="middle">API requests</text>
  <path id="as4f6ec12-tunnel-spoke-hub" d="M 1068 462 L 974 462" fill="none" stroke="#4B5563" stroke-width="1.6" stroke-linejoin="round" stroke-dasharray="5 3" marker-end="url(#as4f6ec12-arrow-4B5563)" />
  <text id="as4f6ec12-label-tunnel1" x="1022" y="484" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="600" fill="#4B5563" text-anchor="middle">tunnel opened</text>
  <text id="as4f6ec12-label-tunnel2" x="1022" y="498" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="600" fill="#4B5563" text-anchor="middle">by the spoke</text>
  <path id="as4f6ec12-fallback-controller-apiserver" d="M 645 490 L 645 522 L 1120 522 L 1120 492" fill="none" stroke="#4B5563" stroke-width="1.3" stroke-linejoin="round" stroke-dasharray="2 3" marker-end="url(#as4f6ec12-arrow-4B5563)" />
  <text id="as4f6ec12-label-fallback" x="635" y="516" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="600" fill="#4B5563" text-anchor="end">fallback (no proxy): API server URL</text>
  <path id="as4f6ec12-flow-apiserver-resources" d="M 1215 390 L 1215 344" fill="none" stroke="#C2410C" stroke-width="2" stroke-linejoin="round" marker-end="url(#as4f6ec12-arrow-C2410C)" />
  <text id="as4f6ec12-label-applies" x="1225" y="370" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="600" fill="#C2410C" text-anchor="start">creates</text>
  <circle id="as4f6ec12-badge-1" cx="220" cy="120" r="11" fill="#1F2937" />
  <text id="as4f6ec12-badge-1-text" x="220" y="124.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">1</text>
  <circle id="as4f6ec12-badge-2" cx="730" cy="120" r="11" fill="#1F2937" />
  <text id="as4f6ec12-badge-2-text" x="730" y="124.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">2</text>
  <circle id="as4f6ec12-badge-3" cx="730" cy="252" r="11" fill="#1F2937" />
  <text id="as4f6ec12-badge-3-text" x="730" y="256.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">3</text>
  <circle id="as4f6ec12-badge-4" cx="952" cy="416" r="11" fill="#1F2937" />
  <text id="as4f6ec12-badge-4-text" x="952" y="420.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">4</text>
  <circle id="as4f6ec12-badge-5" cx="220" cy="412" r="11" fill="#1F2937" />
  <text id="as4f6ec12-badge-5-text" x="220" y="416.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">5</text>
  <rect id="as4f6ec12-legend-you" x="40" y="594" width="22" height="14" rx="3" fill="#E6F4F1" stroke="#0F766E" stroke-width="1.6" />
  <text id="as4f6ec12-legend-you-text" x="70" y="606" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">created by you</text>
  <rect id="as4f6ec12-legend-acm" x="176" y="594" width="22" height="14" rx="3" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="as4f6ec12-legend-acm-text" x="206" y="606" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">generated by ACM</text>
  <rect id="as4f6ec12-legend-argo" x="336" y="594" width="22" height="14" rx="3" fill="#FFFFFF" stroke="#C2410C" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="as4f6ec12-legend-argo-text" x="366" y="606" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">generated by Argo CD</text>
  <rect id="as4f6ec12-legend-platform" x="512" y="594" width="22" height="14" rx="3" fill="#F1F5F9" stroke="#4B5563" stroke-width="1.4" />
  <text id="as4f6ec12-legend-platform-text" x="542" y="606" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">platform component</text>
<circle class="flow-dot" style="--d:0.00s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 242 142 L 286 142" begin="indefinite" data-begin="0.00" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:0.40s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 482 142 L 536 142" begin="indefinite" data-begin="0.40" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:0.80s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 645 186 L 645 228" begin="indefinite" data-begin="0.80" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:1.20s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 645 342 L 645 388" begin="indefinite" data-begin="1.20" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:0.40s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 385 186 L 385 290 L 150 290 L 150 388" begin="indefinite" data-begin="0.40" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:1.20s" visibility="hidden" r="4.5" fill="#C2410C" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 242 440 L 286 440" begin="indefinite" data-begin="1.20" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:1.60s" visibility="hidden" r="4.5" fill="#C2410C" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 482 440 L 536 440" begin="indefinite" data-begin="1.60" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:2.00s" visibility="hidden" r="4.5" fill="#C2410C" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 752 430 L 796 430" begin="indefinite" data-begin="2.00" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:2.40s" visibility="hidden" r="4.5" fill="#C2410C" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 970 430 L 1066 430" begin="indefinite" data-begin="2.40" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:2.80s" visibility="hidden" r="4.5" fill="#C2410C" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 1215 390 L 1215 344" begin="indefinite" data-begin="2.80" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  </svg>
</div>
  <div class="title">Push model: the hub Argo CD applies the resources to every managed cluster</div>
</div>

</div>
<div class="paragraph">
<p>What happens under the hood (the numbers refer to the diagram above):</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>The <strong>Placement</strong> selects managed clusters, the result is stored in a <strong>PlacementDecision</strong>.</p>
</li>
<li>
<p>The <strong>GitOpsCluster</strong> controller reads the PlacementDecision and creates one Argo CD cluster secret per cluster in the Argo CD namespace, named <code>&lt;cluster-name&gt;-application-manager-cluster-secret</code>.</p>
</li>
<li>
<p>The token in the secret belongs to the <strong>ManagedServiceAccount</strong> <code>application-manager</code>. ACM rotates it automatically for you, and the cluster secret on the hub is updated when the token changes. <strong>No long-lived tokens</strong>.</p>
</li>
<li>
<p>The server URL in the cluster secret points to the <strong>cluster proxy</strong> of the multicluster engine. This is the default, because the <code>cluster-proxy</code> add-on is always enabled. Only if the cluster proxy is unavailable does ACM fall back to the API server URL of the managed cluster (OpenShift clusters only).</p>
</li>
<li>
<p>The <strong>ApplicationSet</strong> with the <code>clusterDecisionResource</code> generator creates one Application per cluster. <strong>The application-controller of the hub Argo CD reconciles all of them</strong>.</p>
</li>
</ol>
</div>
<div class="admonitionblock warning">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-warning" title="Warning"></i>
</td>
<td class="content">
<div class="paragraph">
<p>By default, the <code>application-manager</code> service account has <strong>cluster administrator</strong> permissions on the managed cluster. Everybody who can read secrets in the Argo CD namespace on the hub therefore effectively has cluster-admin on every registered managed cluster. Do not hand out the <code>admin</code> role for <code>openshift-gitops</code> lightly, and consider a customised service account (see below).</p>
</div>
</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>The consequences:</p>
</div>
<div class="ulist">
<ul>
<li>
<p><strong>One Argo CD to rule them all.</strong> One UI, one place to look, the full resource tree and live diff for every cluster.</p>
</li>
<li>
<p><strong>The hub carries the load.</strong> Every cluster adds watches and cache to the hub’s application-controller. With many clusters, the performance tuning from <a href="/gitopscollection/2026-03-27-openshift-gitops-performance-tuning/">Ep.16</a> becomes mandatory.</p>
</li>
<li>
<p><strong>No hub, no reconciliation.</strong> If the hub is down, nothing drifts back into the desired state on the spokes.</p>
</li>
<li>
<p><strong>Network:</strong> with the cluster proxy (default), the managed cluster opens the tunnel to the hub, so the hub does not need to reach the spoke API server directly. Only with the API-URL fallback must the hub reach the spoke’s API server.</p>
</li>
</ul>
</div>
</div>
<div class="sect2">
<h3 id="_step_1_the_gitopscluster">Step 1: The GitOpsCluster</h3>
<div class="paragraph">
<p>The GitOpsCluster connects the Placement with the Argo CD instance. This now finally creates the Argo CD clusters on the hub. In the Argo CD UI, <em>Settings &gt; Clusters</em> should show the new clusters:</p>
</div>
<div class="listingblock linenums">
<div class="title">GitOpsCluster for the push model</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: apps.open-cluster-management.io/v1beta1
kind: GitOpsCluster
metadata:
  name: gitops-production
  namespace: openshift-gitops
spec:
  argoServer:
    cluster: local-cluster <i class="conum" data-value="1"></i><b>(1)</b>
    argoNamespace: openshift-gitops <i class="conum" data-value="2"></i><b>(2)</b>
  placementRef:
    kind: Placement
    apiVersion: cluster.open-cluster-management.io/v1beta1
    name: production-clusters <i class="conum" data-value="3"></i><b>(3)</b></code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>The name of the local cluster, where Argo CD runs. On a default hub this is <code>local-cluster</code>.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>The namespace of the Argo CD instance.</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>The Placement from the prerequisites.</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>Verify that ACM created the cluster secrets:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-bash hljs" data-lang="bash">oc get gitopscluster gitops-production -n openshift-gitops -o yaml
oc get secrets -n openshift-gitops -l argocd.argoproj.io/secret-type=cluster</code></pre>
</div>
</div>
<div class="paragraph">
<p>You should see one <code>&lt;cluster-name&gt;-application-manager-cluster-secret</code> per selected cluster.</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-bash hljs" data-lang="bash">NAME                                        TYPE     DATA   AGE
bm-application-manager-cluster-secret       Opaque   3      32s
cluster-kubernetes.default.svc-3396314289   Opaque   3      10h
sno-application-manager-cluster-secret      Opaque   3      32s</code></pre>
</div>
</div>
<div class="paragraph">
<p>The clusters also appear in the Argo CD UI under <em>Settings &gt; Clusters</em>.
The API URL is the cluster proxy URL:</p>
</div>
<div class="imageblock">
<div class="content">
<img src="/gitopscollection/images/ep-19/gitopscluster-cluster-secrets.png" alt="GitOpsCluster cluster secrets"/>
</div>
<div class="title">Figure 2. GitOpsCluster cluster secrets</div>
</div>
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
The connection status is unknown because the cluster has no Argo CD Applications yet.
</td>
</tr>
</tbody></table>
</div>
</div>
<div class="sect2">
<h3 id="_step_2_the_applicationset">Step 2: The ApplicationSet</h3>
<div class="paragraph">
<p>For the ApplicationSet we use the <a href="https://argo-cd.readthedocs.io/en/latest/operator-manual/applicationset/Generators-Cluster-Decision-Resource/" rel="noopener" target="_blank"><strong>clusterDecisionResource</strong></a> generator. It reads the PlacementDecision directly, so the list of clusters always reflects the current state of the fleet. The generator requires a ConfigMap that tells it how to read PlacementDecisions. The GitOpsCluster controller creates it automatically under the name <code>acm-placement</code>:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-bash hljs" data-lang="bash">oc get configmap acm-placement -n openshift-gitops -o yaml</code></pre>
</div>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">data:
  apiVersion: cluster.open-cluster-management.io/v1beta1 <i class="conum" data-value="1"></i><b>(1)</b>
  kind: placementdecisions
  statusListKey: decisions <i class="conum" data-value="2"></i><b>(2)</b>
  matchKey: clusterName <i class="conum" data-value="3"></i><b>(3)</b></code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>The PlacementDecision API.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>The list in the PlacementDecision status that holds the selected clusters.</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>The field in each list entry that is matched against the <code>name</code> of the Argo CD cluster secret.</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>With this information we can create the ApplicationSet using our Compliance Operator as example:</p>
</div>
<div class="listingblock linenums">
<div class="title">ApplicationSet for the push model</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: compliance-operator
  namespace: openshift-gitops
spec:
  generators:
    - clusterDecisionResource:
        configMapRef: acm-placement <i class="conum" data-value="1"></i><b>(1)</b>
        labelSelector:
          matchLabels:
            cluster.open-cluster-management.io/placement: production-clusters <i class="conum" data-value="2"></i><b>(2)</b>
        requeueAfterSeconds: 180 <i class="conum" data-value="3"></i><b>(3)</b>
  template:
    metadata:
      name: &#39;compliance-operator-{{name}}&#39; <i class="conum" data-value="4"></i><b>(4)</b>
    spec:
      project: default
      source:
        repoURL: https://github.com/tjungbauer/openshift-clusterconfig-gitops.git
        targetRevision: main
        path: &#39;clusters/{{name}}/setup-compliance-operator&#39; <i class="conum" data-value="5"></i><b>(5)</b>
      destination:
        server: &#39;{{server}}&#39; <i class="conum" data-value="6"></i><b>(6)</b>
      syncPolicy:
        automated: <i class="conum" data-value="7"></i><b>(7)</b>
          prune: true
          selfHeal: true
  syncPolicy:
    preserveResourcesOnDeletion: true <i class="conum" data-value="8"></i><b>(8)</b></code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>The ConfigMap created by the GitOpsCluster controller.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>The name of our Placement.</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>How often the generator re-reads the PlacementDecision (in seconds).</td>
</tr>
<tr>
<td><i class="conum" data-value="4"></i><b>4</b></td>
<td>Create the name for the Application based on the cluster name.</td>
</tr>
<tr>
<td><i class="conum" data-value="5"></i><b>5</b></td>
<td>The path to the Compliance Operator setup in the Git repository. Each cluster has its own path in the Git repository.</td>
</tr>
<tr>
<td><i class="conum" data-value="6"></i><b>6</b></td>
<td>The server URL from the cluster secret, usually the cluster proxy URL.</td>
</tr>
<tr>
<td><i class="conum" data-value="7"></i><b>7</b></td>
<td>Automated sync policy for the Application.</td>
</tr>
<tr>
<td><i class="conum" data-value="8"></i><b>8</b></td>
<td>If a cluster leaves the Placement, the generated Application is removed. With this setting, the deployed resources on that cluster are kept.</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>In Argo CD we can see the ApplicationSet which automatically created two Applications, one for each (selected) cluster:</p>
</div>
<div class="imageblock">
<div class="content">
<img src="/gitopscollection/images/ep-19/argocd-applicationset-compliance-operator.png" alt="ApplicationSet Applications"/>
</div>
<div class="title">Figure 3. ApplicationSet Applications in Argo CD</div>
</div>
<div class="paragraph">
<p>The Applications have been automatically synced by the hub Argo CD (because we configured it to do so).</p>
</div>
<div class="imageblock">
<div class="content">
<img src="/gitopscollection/images/ep-19/argocd-applications-compliance-operator.png" alt="ApplicationSet Applications synced"/>
</div>
<div class="title">Figure 4. ApplicationSet Applications synced in Argo CD</div>
</div>
<div class="admonitionblock tip">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-tip" title="Tip"></i>
</td>
<td class="content">
The ApplicationSet created the Applications which were immediately synced by the hub Argo CD. They installed and configured the Compliance Operator on the two managed clusters: <code>bm</code> and <code>sno</code>.
</td>
</tr>
</tbody></table>
</div>
</div>
<div class="sect2">
<h3 id="_why_is_this_nice">Why is this nice?</h3>
<div class="paragraph">
<p>Compare this with manually maintained cluster secrets: no tokens, no rotation scripts. Label a new cluster with <code>vendor=OpenShift</code>, add it to the cluster set <code>production</code>, and within a few minutes it has its Application. Detach a cluster from ACM and the Application disappears. You do not need to maintain the cluster secrets manually.</p>
</div>
</div>
<div class="sect2">
<h3 id="_what_to_watch_out_for">What to watch out for</h3>
<div class="admonitionblock caution">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-caution" title="Caution"></i>
</td>
<td class="content">
Think about <code>prune: true</code> together with dynamic Placements. Without <code>preserveResourcesOnDeletion</code>, removing a cluster from the Placement (for example by changing a label) can delete the deployed resources on that cluster. Use it for everything that holds data or that the cluster cannot live without.
</td>
</tr>
</tbody></table>
</div>
</div>
<div class="sect2">
<h3 id="_least_privilege_a_customised_service_account">Least privilege: a customised service account</h3>
<div class="paragraph">
<p>For platform configuration, the ServiceAccount <code>application-manager</code> with cluster-admin is often acceptable: the Argo CD instance <em>is</em> the cluster administrator. For application teams it is too much. ACM lets you replace it with your own <strong>ManagedServiceAccount</strong> and grant it exactly the permissions you want with a <strong>ClusterPermission</strong>. Both resources are created in the namespace of the managed cluster on the hub:</p>
</div>
<div class="listingblock linenums">
<div class="title">ManagedServiceAccount and ClusterPermission for one managed cluster</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: authentication.open-cluster-management.io/v1alpha1
kind: ManagedServiceAccount
metadata:
  name: argocd-deployer
  namespace: sno <i class="conum" data-value="1"></i><b>(1)</b>
spec:
  rotation: {} <i class="conum" data-value="2"></i><b>(2)</b>
---
apiVersion: rbac.open-cluster-management.io/v1alpha1
kind: ClusterPermission
metadata:
  name: argocd-deployer
  namespace: sno
spec:
  roles: <i class="conum" data-value="3"></i><b>(3)</b>
    - namespace: shop
      rules:
        - apiGroups: [&#34;apps&#34;]
          resources: [&#34;deployments&#34;]
          verbs: [&#34;get&#34;, &#34;list&#34;, &#34;watch&#34;, &#34;create&#34;, &#34;update&#34;, &#34;patch&#34;, &#34;delete&#34;]
        - apiGroups: [&#34;&#34;]
          resources: [&#34;services&#34;, &#34;configmaps&#34;]
          verbs: [&#34;get&#34;, &#34;list&#34;, &#34;watch&#34;, &#34;create&#34;, &#34;update&#34;, &#34;patch&#34;, &#34;delete&#34;]
  roleBindings:
    - namespace: shop
      roleRef:
        kind: Role
      subject:
        apiGroup: authentication.open-cluster-management.io
        kind: ManagedServiceAccount
        name: argocd-deployer
  clusterRole: <i class="conum" data-value="4"></i><b>(4)</b>
    rules:
      - apiGroups: [&#34;*&#34;]
        resources: [&#34;*&#34;]
        verbs: [&#34;get&#34;, &#34;list&#34;, &#34;watch&#34;]
  clusterRoleBinding:
    subject:
      apiGroup: authentication.open-cluster-management.io
      kind: ManagedServiceAccount
      name: argocd-deployer</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>The namespace of the managed cluster on the hub. The service account is created on that cluster.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Let ACM rotate the token.</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>Write permissions only in the namespace <code>shop</code>.</td>
</tr>
<tr>
<td><i class="conum" data-value="4"></i><b>4</b></td>
<td>Read-only access to the rest of the cluster, so that Argo CD can discover API resources and show health.</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>Then reference the service account in the GitOpsCluster:</p>
</div>
<div class="listingblock linenums">
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: apps.open-cluster-management.io/v1beta1
kind: GitOpsCluster
metadata:
  name: gitops-team-shop
  namespace: openshift-gitops
spec:
  managedServiceAccountRef: argocd-deployer <i class="conum" data-value="1"></i><b>(1)</b>
  argoServer:
    cluster: local-cluster
    argoNamespace: openshift-gitops
  placementRef:
    kind: Placement
    apiVersion: cluster.open-cluster-management.io/v1beta1
    name: production-clusters</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Use this ManagedServiceAccount instead of <code>application-manager</code>. The cluster secrets are now named <code>&lt;cluster-name&gt;-argocd-deployer-cluster-secret</code>.</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>The <code>ManagedServiceAccount</code> and the <code>ClusterPermission</code> must exist for <strong>every</strong> cluster of the Placement. Creating them by hand does not scale and is not GitOps-like.</p>
</div>
<hr/>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_the_pull_model">The pull model</h2>
<div class="sectionbody">
<div class="sect2">
<h3 id="_what_it_actually_is_2">What it actually is</h3>
<div class="paragraph">
<p>The <strong>pull model</strong> turns the direction around. The Application is still <em>defined</em> on the hub, but the hub does not reconcile it. Instead, ACM transports the Application object itself to the managed cluster, where a <strong>local Argo CD instance</strong> does the actual work.</p>
</div>
<div class="paragraph">
<div class="imageblock animated-svg" data-animated-svg>
  <div class="content">
<svg width="1480" height="690" viewBox="0 0 1480 690" role="img" aria-label="Pull model: the ApplicationSet on the hub generates Applications that the hub Argo CD ignores. The propagation controller wraps each Application into a ManifestWork in the cluster namespace, the work agent on the managed cluster pulls it and creates the Application, and the local Argo CD reconciles it. Status returns through the ManifestWork and ACM search into the MulticlusterApplicationSetReport" version="1.1" id="asdf83d23-svg-pull-model" xmlns="http://www.w3.org/2000/svg" xmlns:c2pa="http://c2pa.org/manifest">
  <title id="asdf83d23-title-pull-model">Pull model: the ApplicationSet on the hub generates Applications that the hub Argo CD ignores. The propagation controller wraps each Application into a ManifestWork in the cluster namespace, the work agent on the managed cluster pulls it and creates the Application, and the local Argo CD reconciles it. Status returns through the ManifestWork and ACM search into the MulticlusterApplicationSetReport</title>
  <defs id="asdf83d23-defs-pull-model">
    <marker id="asdf83d23-arrow-0F766E" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#0F766E" id="asdf83d23-marker-path-0F766E" /></marker>
    <marker id="asdf83d23-arrow-C2410C" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#C2410C" id="asdf83d23-marker-path-C2410C" /></marker>
    <marker id="asdf83d23-arrow-4B5563" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#4B5563" id="asdf83d23-marker-path-4B5563" /></marker>
    </defs>
  <rect id="asdf83d23-bg" width="1480" height="690" fill="#FFFFFF" />
  <rect id="asdf83d23-zone-hub" x="20" y="20" width="1090" height="620" rx="14" fill="#F8FAFC" stroke="#E2E8F0" stroke-width="1.5" />
  <text id="asdf83d23-label-hub" x="40" y="48" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="700" fill="#4B5563" text-anchor="start">HUB</text>
  <rect id="asdf83d23-zone-spoke" x="1140" y="20" width="320" height="620" rx="14" fill="#F8FAFC" stroke="#E2E8F0" stroke-width="1.5" />
  <text id="asdf83d23-label-spoke" x="1160" y="48" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="700" fill="#4B5563" text-anchor="start">MANAGED CLUSTER  ·  bm, sno</text>
  <rect id="asdf83d23-ns-gitops" x="40" y="60" width="580" height="430" rx="10" fill="#FFFFFF" stroke="#CBD5E1" stroke-width="1.2" />
  <text id="asdf83d23-label-ns" x="56" y="84" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="700" fill="#C2410C" text-anchor="start">openshift-gitops</text>
  <rect id="asdf83d23-ns-ocm" x="640" y="60" width="260" height="430" rx="10" fill="#FFFFFF" stroke="#CBD5E1" stroke-width="1.2" />
  <text id="asdf83d23-label-ns-ocm" x="656" y="84" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="700" fill="#4B5563" text-anchor="start">open-cluster-management</text>
  <text id="asdf83d23-label-pod" x="656" y="478" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="400" fill="#4B5563" text-anchor="start">all three in the pod multicluster-integrations</text>
  <rect id="asdf83d23-ns-cluster" x="920" y="60" width="170" height="180" rx="10" fill="#FFFFFF" stroke="#CBD5E1" stroke-width="1.2" />
  <text id="asdf83d23-label-ns-cluster" x="936" y="84" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="700" fill="#4B5563" text-anchor="start">cluster namespaces</text>
  <rect id="asdf83d23-rect-appset" x="60" y="110" width="190" height="110" rx="10" fill="#E6F4F1" stroke="#0F766E" stroke-width="1.6" />
  <text id="asdf83d23-title-appset" x="76" y="139" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#0F766E" text-anchor="start">ApplicationSet</text>
  <text id="asdf83d23-line0-appset" x="76" y="162" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">generator:</text>
  <text id="asdf83d23-line1-appset" x="76" y="178" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#1F2937" text-anchor="start">clusterDecisionResource</text>
  <text id="asdf83d23-line2-appset" x="76" y="194" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">with pull annotations</text>
  <circle id="asdf83d23-badge-1" cx="230" cy="130" r="11" fill="#1F2937" />
  <text id="asdf83d23-badge-1-text" x="230" y="134.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">1</text>
  <rect id="asdf83d23-rect-application" x="270" y="110" width="190" height="110" rx="10" fill="#FFFFFF" stroke="#C2410C" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="asdf83d23-title-application" x="286" y="139" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#C2410C" text-anchor="start">Application</text>
  <text id="asdf83d23-line0-application" x="286" y="162" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">one per selected cluster</text>
  <text id="asdf83d23-line1-application" x="286" y="178" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#4B5563" text-anchor="start">skip-reconcile: true</text>
  <text id="asdf83d23-line2-application" x="286" y="194" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">hub Argo CD ignores it</text>
  <circle id="asdf83d23-badge-2" cx="440" cy="130" r="11" fill="#1F2937" />
  <text id="asdf83d23-badge-2-text" x="440" y="134.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">2</text>
  <rect id="asdf83d23-rect-report" x="300" y="250" width="305" height="96" rx="10" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="asdf83d23-title-report" x="316" y="279" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#0F766E" text-anchor="start">MulticlusterApplicationSetReport</text>
  <text id="asdf83d23-line0-report" x="316" y="302" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">sync and health per cluster,</text>
  <text id="asdf83d23-line1-report" x="316" y="318" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">aggregated, not real-time</text>
  <rect id="asdf83d23-rect-placement" x="60" y="380" width="170" height="84" rx="10" fill="#E6F4F1" stroke="#0F766E" stroke-width="1.6" />
  <text id="asdf83d23-title-placement" x="76" y="409" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#0F766E" text-anchor="start">Placement</text>
  <text id="asdf83d23-line0-placement" x="76" y="432" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#1F2937" text-anchor="start">production-clusters</text>
  <rect id="asdf83d23-rect-decision" x="256" y="380" width="180" height="84" rx="10" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="asdf83d23-title-decision" x="272" y="409" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#0F766E" text-anchor="start">PlacementDecision</text>
  <text id="asdf83d23-line0-decision" x="272" y="432" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">decisions: bm, sno</text>
  <rect id="asdf83d23-rect-secret" x="460" y="380" width="145" height="84" rx="10" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="asdf83d23-title-secret" x="476" y="409" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#0F766E" text-anchor="start">Cluster secret</text>
  <text id="asdf83d23-line0-secret" x="476" y="432" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">required, but</text>
  <text id="asdf83d23-line1-secret" x="476" y="448" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">not used to deploy</text>
  <rect id="asdf83d23-rect-propagation" x="656" y="110" width="228" height="110" rx="10" fill="#F1F5F9" stroke="#4B5563" stroke-width="1.4" />
  <text id="asdf83d23-title-propagation" x="672" y="139" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#4B5563" text-anchor="start">propagation</text>
  <text id="asdf83d23-line0-propagation" x="672" y="162" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#1F2937" text-anchor="start">controller</text>
  <text id="asdf83d23-line1-propagation" x="672" y="178" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">wraps each Application</text>
  <text id="asdf83d23-line2-propagation" x="672" y="194" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">into a ManifestWork</text>
  <circle id="asdf83d23-badge-3" cx="864" cy="130" r="11" fill="#1F2937" />
  <text id="asdf83d23-badge-3-text" x="864" y="134.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">3</text>
  <rect id="asdf83d23-rect-aggregation" x="656" y="250" width="228" height="96" rx="10" fill="#F1F5F9" stroke="#4B5563" stroke-width="1.4" />
  <text id="asdf83d23-title-aggregation" x="672" y="279" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#4B5563" text-anchor="start">aggregation</text>
  <text id="asdf83d23-line0-aggregation" x="672" y="302" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#1F2937" text-anchor="start">controller</text>
  <text id="asdf83d23-line1-aggregation" x="672" y="318" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">builds the report</text>
  <circle id="asdf83d23-badge-6" cx="864" cy="270" r="11" fill="#1F2937" />
  <text id="asdf83d23-badge-6-text" x="864" y="274.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">6</text>
  <rect id="asdf83d23-rect-sync" x="656" y="370" width="228" height="96" rx="10" fill="#F1F5F9" stroke="#4B5563" stroke-width="1.4" />
  <text id="asdf83d23-title-sync" x="672" y="399" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#4B5563" text-anchor="start">resource sync</text>
  <text id="asdf83d23-line0-sync" x="672" y="422" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#1F2937" text-anchor="start">controller</text>
  <text id="asdf83d23-line1-sync" x="672" y="438" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">queries ACM search, 10 s</text>
  <rect id="asdf83d23-rect-manifestwork" x="936" y="110" width="138" height="110" rx="10" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="asdf83d23-title-manifestwork" x="952" y="139" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#0F766E" text-anchor="start">ManifestWork</text>
  <text id="asdf83d23-line0-manifestwork" x="952" y="162" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">in namespace</text>
  <text id="asdf83d23-line1-manifestwork" x="952" y="178" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#4B5563" text-anchor="start">sno or bm</text>
  <text id="asdf83d23-line2-manifestwork" x="952" y="194" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">wraps the App</text>
  <rect id="asdf83d23-rect-agent" x="1160" y="110" width="280" height="110" rx="10" fill="#F1F5F9" stroke="#4B5563" stroke-width="1.4" />
  <text id="asdf83d23-title-agent" x="1176" y="139" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#4B5563" text-anchor="start">work agent</text>
  <text id="asdf83d23-line0-agent" x="1176" y="162" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">part of the klusterlet,</text>
  <text id="asdf83d23-line1-agent" x="1176" y="178" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">creates the Application</text>
  <text id="asdf83d23-line2-agent" x="1176" y="194" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">from the ManifestWork</text>
  <circle id="asdf83d23-badge-4" cx="1420" cy="130" r="11" fill="#1F2937" />
  <text id="asdf83d23-badge-4-text" x="1420" y="134.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">4</text>
  <rect id="asdf83d23-rect-spoke-app" x="1160" y="250" width="280" height="84" rx="10" fill="#FFFFFF" stroke="#C2410C" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="asdf83d23-title-spoke-app" x="1176" y="279" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#C2410C" text-anchor="start">Application</text>
  <text id="asdf83d23-line0-spoke-app" x="1176" y="302" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">in openshift-gitops on the spoke</text>
  <rect id="asdf83d23-rect-local-argo" x="1160" y="370" width="280" height="96" rx="10" fill="#FDF0E8" stroke="#C2410C" stroke-width="1.6" />
  <text id="asdf83d23-title-local-argo" x="1176" y="399" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#C2410C" text-anchor="start">Local Argo CD</text>
  <text id="asdf83d23-line0-local-argo" x="1176" y="422" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#1F2937" text-anchor="start">application-controller</text>
  <text id="asdf83d23-line1-local-argo" x="1176" y="438" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">pulls Git, reconciles locally</text>
  <circle id="asdf83d23-badge-5" cx="1420" cy="390" r="11" fill="#1F2937" />
  <text id="asdf83d23-badge-5-text" x="1420" y="394.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">5</text>
  <rect id="asdf83d23-rect-resources" x="1160" y="500" width="280" height="84" rx="10" fill="#FFFFFF" stroke="#C2410C" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="asdf83d23-title-resources" x="1176" y="529" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#C2410C" text-anchor="start">Resources</text>
  <text id="asdf83d23-line0-resources" x="1176" y="552" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">applied by the local Argo CD</text>
  <path id="asdf83d23-flow-placement-decision" d="M 232 422 L 252 422" fill="none" stroke="#0F766E" stroke-width="2" stroke-linejoin="round" marker-end="url(#asdf83d23-arrow-0F766E)" />
  <path id="asdf83d23-flow-decision-appset" d="M 345 378 L 345 362 L 150 362 L 150 224" fill="none" stroke="#0F766E" stroke-width="2" stroke-linejoin="round" marker-end="url(#asdf83d23-arrow-0F766E)" />
  <text id="asdf83d23-label-reads" x="160" y="356" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="600" fill="#0F766E" text-anchor="start">reads the decisions</text>
  <path id="asdf83d23-flow-appset-application" d="M 252 165 L 266 165" fill="none" stroke="#C2410C" stroke-width="2" stroke-linejoin="round" marker-end="url(#asdf83d23-arrow-C2410C)" />
  <path id="asdf83d23-flow-application-propagation" d="M 462 165 L 652 165" fill="none" stroke="#C2410C" stroke-width="2" stroke-linejoin="round" marker-end="url(#asdf83d23-arrow-C2410C)" />
  <text id="asdf83d23-label-watches" x="557" y="156" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="700" fill="#C2410C" text-anchor="middle">ocm-managed-cluster</text>
  <path id="asdf83d23-flow-propagation-manifestwork" d="M 886 165 L 932 165" fill="none" stroke="#0F766E" stroke-width="2" stroke-linejoin="round" marker-end="url(#asdf83d23-arrow-0F766E)" />
  <circle class="flow-dot" style="--d:0.00s" visibility="hidden" r="3.5" fill="#4B5563" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 1158 180 L 1078 180" begin="indefinite" data-begin="0.00" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <rect id="asdf83d23-bg-pull-label" x="1092" y="190" width="60" height="32" rx="0" fill="#F8FAFC" stroke="#F8FAFC" stroke-width="0" />
  <path id="asdf83d23-flow-manifestwork-agent" d="M 1076 150 L 1156 150" fill="none" stroke="#C2410C" stroke-width="2" stroke-linejoin="round" marker-end="url(#asdf83d23-arrow-C2410C)" />
  <text id="asdf83d23-label-app" x="1116" y="141" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="700" fill="#C2410C" text-anchor="middle">Application</text>
  <path id="asdf83d23-pull-spoke-hub" d="M 1158 180 L 1078 180" fill="none" stroke="#4B5563" stroke-width="1.6" stroke-linejoin="round" stroke-dasharray="5 3" marker-end="url(#asdf83d23-arrow-4B5563)" />
  <text id="asdf83d23-label-pull1" x="1118" y="202" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="600" fill="#4B5563" text-anchor="middle">pulled by</text>
  <text id="asdf83d23-label-pull2" x="1118" y="216" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="600" fill="#4B5563" text-anchor="middle">the spoke</text>
  <path id="asdf83d23-flow-agent-app" d="M 1300 222 L 1300 246" fill="none" stroke="#C2410C" stroke-width="2" stroke-linejoin="round" marker-end="url(#asdf83d23-arrow-C2410C)" />
  <path id="asdf83d23-flow-app-argo" d="M 1300 336 L 1300 366" fill="none" stroke="#C2410C" stroke-width="2" stroke-linejoin="round" marker-end="url(#asdf83d23-arrow-C2410C)" />
  <path id="asdf83d23-flow-argo-resources" d="M 1300 468 L 1300 496" fill="none" stroke="#C2410C" stroke-width="2" stroke-linejoin="round" marker-end="url(#asdf83d23-arrow-C2410C)" />
  <text id="asdf83d23-label-applies" x="1310" y="488" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="600" fill="#C2410C" text-anchor="start">applies</text>
  <path id="asdf83d23-status-manifestwork" d="M 1005 222 L 1005 298 L 888 298" fill="none" stroke="#0F766E" stroke-width="1.6" stroke-linejoin="round" stroke-dasharray="5 3" marker-end="url(#asdf83d23-arrow-0F766E)" />
  <text id="asdf83d23-label-mw-status" x="1015" y="270" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="600" fill="#0F766E" text-anchor="start">ManifestWork</text>
  <text id="asdf83d23-label-mw-status2" x="1015" y="284" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="600" fill="#0F766E" text-anchor="start">status</text>
  <path id="asdf83d23-status-search" d="M 1156 418 L 888 418" fill="none" stroke="#0F766E" stroke-width="1.6" stroke-linejoin="round" stroke-dasharray="5 3" marker-end="url(#asdf83d23-arrow-0F766E)" />
  <text id="asdf83d23-label-search" x="1022" y="409" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="600" fill="#0F766E" text-anchor="middle">ACM search: resources, errors</text>
  <path id="asdf83d23-status-sync-aggregation" d="M 770 368 L 770 350" fill="none" stroke="#0F766E" stroke-width="1.6" stroke-linejoin="round" stroke-dasharray="5 3" marker-end="url(#asdf83d23-arrow-0F766E)" />
  <path id="asdf83d23-status-aggregation-report" d="M 654 298 L 609 298" fill="none" stroke="#0F766E" stroke-width="1.6" stroke-linejoin="round" stroke-dasharray="5 3" marker-end="url(#asdf83d23-arrow-0F766E)" />
  <text id="asdf83d23-note-hub1" x="40" y="530" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#1F2937" text-anchor="start">The hub Argo CD only generates the Applications:</text>
  <text id="asdf83d23-note-hub2" x="40" y="548" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">reconciliation happens on every spoke, with its own OpenShift GitOps.</text>
  <text id="asdf83d23-note-hub3" x="40" y="566" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">No hub, still reconciliation. All connections are opened by the spoke.</text>
  <text id="asdf83d23-note-hub4" x="40" y="594" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="600" fill="#0F766E" text-anchor="start">Dashed arrows: status back to the hub.</text>
  <rect id="asdf83d23-legend-you" x="40" y="664" width="22" height="14" rx="3" fill="#E6F4F1" stroke="#0F766E" stroke-width="1.6" />
  <text id="asdf83d23-legend-you-text" x="70" y="676" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">created by you</text>
  <rect id="asdf83d23-legend-acm" x="194.609375" y="664" width="22" height="14" rx="3" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="asdf83d23-legend-acm-text" x="224.609375" y="676" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">generated by ACM</text>
  <rect id="asdf83d23-legend-argo" x="370.5625" y="664" width="22" height="14" rx="3" fill="#FFFFFF" stroke="#C2410C" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="asdf83d23-legend-argo-text" x="400.5625" y="676" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">generated by Argo CD</text>
  <rect id="asdf83d23-legend-platform" x="566.984375" y="664" width="22" height="14" rx="3" fill="#F1F5F9" stroke="#4B5563" stroke-width="1.6" />
  <text id="asdf83d23-legend-platform-text" x="596.984375" y="676" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">platform component</text>
<circle class="flow-dot" style="--d:0.00s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 232 422 L 252 422" begin="indefinite" data-begin="0.00" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:0.40s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 345 378 L 345 362 L 150 362 L 150 224" begin="indefinite" data-begin="0.40" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:0.80s" visibility="hidden" r="4.5" fill="#C2410C" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 252 165 L 266 165" begin="indefinite" data-begin="0.80" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:1.20s" visibility="hidden" r="4.5" fill="#C2410C" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 462 165 L 652 165" begin="indefinite" data-begin="1.20" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:1.60s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 886 165 L 932 165" begin="indefinite" data-begin="1.60" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:2.00s" visibility="hidden" r="4.5" fill="#C2410C" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 1076 150 L 1156 150" begin="indefinite" data-begin="2.00" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:2.40s" visibility="hidden" r="4.5" fill="#C2410C" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 1300 222 L 1300 246" begin="indefinite" data-begin="2.40" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:2.80s" visibility="hidden" r="4.5" fill="#C2410C" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 1300 336 L 1300 366" begin="indefinite" data-begin="2.80" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:3.20s" visibility="hidden" r="4.5" fill="#C2410C" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 1300 468 L 1300 496" begin="indefinite" data-begin="3.20" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:3.60s" visibility="hidden" r="3.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 1005 222 L 1005 298 L 888 298" begin="indefinite" data-begin="3.60" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:3.60s" visibility="hidden" r="3.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 1156 418 L 888 418" begin="indefinite" data-begin="3.60" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:4.00s" visibility="hidden" r="3.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 770 368 L 770 350" begin="indefinite" data-begin="4.00" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:4.40s" visibility="hidden" r="3.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 654 298 L 609 298" begin="indefinite" data-begin="4.40" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  </svg>
</div>
  <div class="title">Pull model: the hub ships the Application, the local Argo CD reconciles it</div>
</div>

</div>
<div class="paragraph">
<p>What happens under the hood:</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>The ApplicationSet on the hub generates an Application per cluster, just as in the push model.</p>
</li>
<li>
<p>The generated Application carries the label <code>apps.open-cluster-management.io/pull-to-ocm-managed-cluster: &#34;true&#34;</code> and the annotation <code>argocd.argoproj.io/skip-reconcile: &#34;true&#34;</code>. The latter tells the hub Argo CD to ignore it.</p>
</li>
<li>
<p>The <strong>propagation controller</strong> on the hub picks up every Application with the annotation <code>apps.open-cluster-management.io/ocm-managed-cluster</code>. It wraps the Application into a <strong>ManifestWork</strong> in the namespace of the target cluster. We are still on the hub cluster at this point.</p>
</li>
<li>
<p>The <strong>work agent</strong> on the managed cluster (part of the regular ACM klusterlet) pulls the ManifestWork and creates the Application in the <code>openshift-gitops</code> namespace of the managed cluster.</p>
</li>
<li>
<p>The <strong>local Argo CD on the managed cluster</strong> pulls Git and reconciles the Application against its own API server.</p>
</li>
<li>
<p>The status travels back in two ways: the ManifestWork reports the status of the Application object, and the <strong>resource sync controller</strong> queries the ACM search component on each managed cluster for the resource list and error messages. The <strong>aggregation controller</strong> combines both into a <strong>MulticlusterApplicationSetReport</strong> per ApplicationSet.</p>
</li>
</ol>
</div>
<div class="paragraph">
<p>All three controllers run as containers in the <code>multicluster-integrations</code> pod in the <code>open-cluster-management</code> namespace on the hub. The resource sync and aggregation controllers run every 10 seconds.</p>
</div>
<div class="paragraph">
<p>The consequences:</p>
</div>
<div class="ulist">
<ul>
<li>
<p><strong>The spokes do the work.</strong> The hub only creates ManifestWorks, which scales much better than one controller watching every cluster.</p>
</li>
<li>
<p><strong>The hub never connects to the spoke APIs.</strong> All connections are initiated by the managed cluster (the klusterlet of ACM does that anyway). Perfect for firewalls that only allow outbound traffic from the spoke.</p>
</li>
<li>
<p><strong>No hub, still reconciliation.</strong> If the hub goes away, the local Argo CD keeps self-healing with the last known Application.</p>
</li>
<li>
<p><strong>Limited visibility.</strong> The status on the hub is not real-time and not complete. Resources that failed to deploy are not part of the resource list, only the error messages show them. For details, you have to look at the Argo CD of the spoke.</p>
</li>
<li>
<p><strong>An Argo CD per cluster.</strong> Every managed cluster runs a full OpenShift GitOps instance, needs access to Git and its own repository credentials, and must be installed, updated and given the right permissions.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>The ACM documentation lists the requirements for the pull model:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>OpenShift GitOps installed on the hub <strong>and</strong> on every target managed cluster, in the namespace <code>openshift-gitops</code>.</p>
</li>
<li>
<p>OpenShift GitOps on the hub in version 1.9.0 or later, and <strong>equal to or newer than</strong> the version on the managed clusters.</p>
</li>
<li>
<p>A cluster secret per managed cluster on the hub, created by a GitOpsCluster. The ApplicationSet generator matches the PlacementDecision against these secrets, even if the hub never uses them to deploy.</p>
</li>
</ul>
</div>
<div class="admonitionblock warning">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-warning" title="Warning"></i>
</td>
<td class="content">
So the GitOpsCluster and the Placement from the push model are needed here as well.
</td>
</tr>
</tbody></table>
</div>
</div>
<div class="sect2">
<h3 id="_step_1_openshift_gitops_on_the_managed_clusters">Step 1: OpenShift GitOps on the managed clusters</h3>
<div class="paragraph">
<p>There are two ways to get a local Argo CD onto every spoke.</p>
</div>
<div class="sect3">
<h4 id="_option_a_the_gitops_add_on_in_basic_mode">Option A: the GitOps add-on in Basic mode</h4>
<div class="paragraph">
<p>The GitOpsCluster can install OpenShift GitOps automatically for you. Simply enable the add-on:</p>
</div>
<div class="listingblock linenums">
<div class="title">GitOpsCluster with the GitOps add-on in Basic mode</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: apps.open-cluster-management.io/v1beta1
kind: GitOpsCluster
metadata:
  name: gitops-production
  namespace: openshift-gitops
spec:
  argoServer:
    argoNamespace: openshift-gitops
  placementRef:
    kind: Placement
    apiVersion: cluster.open-cluster-management.io/v1beta1
    name: production-clusters
    namespace: openshift-gitops
  gitopsAddon:
    enabled: true <i class="conum" data-value="1"></i><b>(1)</b>
    overrideExistingConfigs: false <i class="conum" data-value="2"></i><b>(2)</b></code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Basic mode: deploys the OpenShift GitOps Operator into <code>openshift-gitops-operator</code> and an Argo CD instance into <code>openshift-gitops</code> on every selected cluster.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Safety feature: do not overwrite existing Argo CD configurations on the managed clusters.</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>When we update the GitOpsCluster resource, that we have created in the previous chapter, the managed clusters will start to install OpenShift GitOps. The Argo CD instance <code>acm-openshift-gitops</code> will be created in the namespace <code>openshift-gitops</code>.</p>
</div>
<div class="admonitionblock tip">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-tip" title="Tip"></i>
</td>
<td class="content">
This is by far the easiest way to get a local Argo CD onto every spoke as you need to configure the GitOpsCluster resource anyway. This automatically creates an ACM policy <code>gitops-production-argocd-policy</code> that manages the Argo CD resource on the spokes.
</td>
</tr>
</tbody></table>
</div>
<div class="admonitionblock warning">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-warning" title="Warning"></i>
</td>
<td class="content">
<div class="ulist">
<ul>
<li>
<p>Enabling the GitOps add-on through a GitOpsCluster <strong>disables the push model for all applications</strong>. This is a fleet-wide decision. If you need push and pull side by side, use option B.</p>
</li>
<li>
<p>The Basic mode is supported for <strong>OpenShift</strong> managed clusters only.</p>
</li>
<li>
<p>To customise the ArgoCD resource on a single spoke without the add-on reverting it, annotate it with <code>gitops-addon.open-cluster-management.io/skip: &#34;true&#34;</code>.</p>
</li>
</ul>
</div>
</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>Verify the add-on:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-bash hljs" data-lang="bash"># on a managed cluster
oc get pods -n open-cluster-management-agent-addon
oc get pods -n openshift-gitops-operator
oc get pods -n openshift-gitops</code></pre>
</div>
</div>
</div>
<div class="sect3">
<h4 id="_option_b_your_own_policy">Option B: your own policy</h4>
<div class="paragraph">
<p>If you want better control over the operator version, or you need the push model as well, install OpenShift GitOps with your <strong>own policy</strong>. This is the same OperatorPolicy pattern as in <a href="/gitopscollection/2026-10-20-openshift-gitops-vs-acm/#_option_2_acm_with_an_operatorpolicy">Ep.18</a>:</p>
</div>
<div class="listingblock linenums">
<div class="title">Policy that installs OpenShift GitOps on the managed clusters</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: policy.open-cluster-management.io/v1
kind: Policy
metadata:
  name: openshift-gitops-spoke
  namespace: acm-policies
spec:
  remediationAction: enforce <i class="conum" data-value="1"></i><b>(1)</b>
  disabled: false
  policy-templates:
    - objectDefinition: <i class="conum" data-value="2"></i><b>(2)</b>
        apiVersion: policy.open-cluster-management.io/v1
        kind: ConfigurationPolicy
        metadata:
          name: openshift-gitops-operator-namespace
        spec:
          severity: high
          object-templates:
            - complianceType: musthave
              objectDefinition:
                apiVersion: v1
                kind: Namespace
                metadata:
                  name: openshift-gitops-operator
    - objectDefinition: <i class="conum" data-value="3"></i><b>(3)</b>
        apiVersion: policy.open-cluster-management.io/v1beta1
        kind: OperatorPolicy
        metadata:
          name: openshift-gitops-operator
        spec:
          severity: high
          complianceType: musthave
          subscription:
            name: openshift-gitops-operator
            namespace: openshift-gitops-operator
            channel: gitops-1.22 <i class="conum" data-value="4"></i><b>(4)</b>
            source: redhat-operators
            sourceNamespace: openshift-marketplace
          upgradeApproval: Automatic</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Enforce the policy.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Create the namespace <code>openshift-gitops-operator</code> on the managed clusters.</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>Install the OpenShift GitOps Operator.</td>
</tr>
<tr>
<td><i class="conum" data-value="4"></i><b>4</b></td>
<td>Pin the channel. The hub must run the same or a newer OpenShift GitOps version than the spokes, so upgrade the hub first.</td>
</tr>
</tbody></table>
</div>
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
This installs the Namespace and the Operator, the Argo CD instance is created automatically by the Operator.
</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>Bind the policy with a Placement and a PlacementBinding in <code>acm-policies</code>, using the same label selector as the GitOps Placement, so that every cluster that should receive pull Applications also receives a local Argo CD. See above for examples.</p>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_step_2_permissions_for_the_local_argo_cd">Step 2: Permissions for the local Argo CD</h3>
<div class="paragraph">
<p>Now with Argo CD deployed on the managed cluster, the local application-controller must be allowed to create whatever the Applications contain. Without that, syncs fail with <code>forbidden</code> errors for the service account <code>openshift-gitops-argocd-application-controller</code>. The ACM documentation offers two ways:</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p><strong>Cluster administrator</strong>: a ClusterRoleBinding on every managed cluster. Simple, and for platform configuration usually what you want. For example, to install a new Operator.</p>
</li>
<li>
<p><strong>Least privilege</strong>: create every destination namespace on the managed cluster with the label <code>argocd.argoproj.io/managed-by: openshift-gitops</code>. The OpenShift GitOps Operator then grants the local instance admin rights in exactly these namespaces. The namespaces must be declared in Git, as part of the Application.</p>
</li>
</ol>
</div>
<div class="paragraph">
<p>For option 1, add the ClusterRoleBinding to the policy that manages the spokes (the <code>&lt;gitopscluster-name&gt;-argocd-policy</code> from option A or your own policy from option B):</p>
</div>
<div class="listingblock linenums">
<div class="title">Policy template granting the local controller cluster-admin</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">    - objectDefinition:
        apiVersion: policy.open-cluster-management.io/v1
        kind: ConfigurationPolicy
        metadata:
          name: openshift-gitops-controller-rbac
        spec:
          severity: high
          object-templates:
            - complianceType: musthave
              objectDefinition:
                apiVersion: rbac.authorization.k8s.io/v1
                kind: ClusterRoleBinding
                metadata:
                  name: argo-admin
                subjects:
                  - kind: ServiceAccount
                    name: openshift-gitops-argocd-application-controller
                    namespace: openshift-gitops
                roleRef:
                  apiGroup: rbac.authorization.k8s.io
                  kind: ClusterRole
                  name: cluster-admin</code></pre>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_step_3_the_applicationset_for_the_pull_model">Step 3: The ApplicationSet for the pull model</h3>
<div class="paragraph">
<p>The ApplicationSet uses the same generator as the push model: <code>clusterDecisionResource</code>. The template gets a label and three annotations, and the destination changes.</p>
</div>
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
The Compliance Operator is already installed on <code>sno</code> and <code>bm</code> by the push model. To keep the two models from fighting over the same objects, the pull example deploys something else: the console branding from the folder <a href="https://github.com/tjungbauer/openshift-clusterconfig-gitops/tree/main/clusters/management-cluster/branding" rel="noopener" target="_blank">clusters/&lt;cluster&gt;/branding</a> of my GitOps repository.
</td>
</tr>
</tbody></table>
</div>
<div class="listingblock linenums">
<div class="title">ApplicationSet for the pull model</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: branding-pull
  namespace: openshift-gitops
spec:
  generators:
    - clusterDecisionResource:
        configMapRef: acm-placement
        labelSelector:
          matchLabels:
            cluster.open-cluster-management.io/placement: production-clusters
        requeueAfterSeconds: 180
  template:
    metadata:
      name: &#39;branding-{{name}}&#39;
      labels:
        apps.open-cluster-management.io/pull-to-ocm-managed-cluster: &#39;true&#39; <i class="conum" data-value="1"></i><b>(1)</b>
      annotations:
        argocd.argoproj.io/skip-reconcile: &#39;true&#39; <i class="conum" data-value="2"></i><b>(2)</b>
        apps.open-cluster-management.io/ocm-managed-cluster: &#39;{{name}}&#39; <i class="conum" data-value="3"></i><b>(3)</b>
        apps.open-cluster-management.io/ocm-managed-cluster-app-namespace: openshift-gitops <i class="conum" data-value="4"></i><b>(4)</b>
    spec:
      project: default <i class="conum" data-value="5"></i><b>(5)</b>
      source:
        repoURL: https://github.com/tjungbauer/openshift-clusterconfig-gitops.git
        targetRevision: main
        path: &#39;clusters/{{name}}/branding&#39; <i class="conum" data-value="6"></i><b>(6)</b>
      destination:
        server: https://kubernetes.default.svc <i class="conum" data-value="7"></i><b>(7)</b>
      syncPolicy:
        automated:
          prune: true
          selfHeal: true</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Marks the Application as a pull model Application.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Prevents the Argo CD instance on the hub from reconciling the Application itself.</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>The target managed cluster. The propagation controller only processes Applications with this annotation.</td>
</tr>
<tr>
<td><i class="conum" data-value="4"></i><b>4</b></td>
<td>The namespace on the managed cluster where the Application is created, i.e. the namespace of the local Argo CD.</td>
</tr>
<tr>
<td><i class="conum" data-value="5"></i><b>5</b></td>
<td>The AppProject must exist <strong>on the managed cluster</strong>. The default instance always has <code>default</code>. Custom projects, and the repository credentials for private repositories, have to be created on the spokes, for example by another policy.</td>
</tr>
<tr>
<td><i class="conum" data-value="6"></i><b>6</b></td>
<td>One folder per cluster: <code>clusters/sno/branding</code> and <code>clusters/bm/branding</code>.</td>
</tr>
<tr>
<td><i class="conum" data-value="7"></i><b>7</b></td>
<td>From the perspective of the local Argo CD on the spoke, the destination is the cluster it runs on.</td>
</tr>
</tbody></table>
</div>
<div class="admonitionblock caution">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-caution" title="Caution"></i>
</td>
<td class="content">
<div class="paragraph">
<p>Do not forget the <code>skip-reconcile</code> annotation. Without it, the hub Argo CD tries to sync the Application to <code><a class="bare" href="https://kubernetes.default.svc">https://kubernetes.default.svc</a></code>, which is the <strong>hub</strong> itself. You would deploy the configuration of every managed cluster onto the hub.</p>
</div>
</td>
</tr>
</tbody></table>
</div>
</div>
<div class="sect2">
<h3 id="_step_4_verify_the_flow">Step 4: Verify the flow</h3>
<div class="paragraph">
<p>Follow the Application through the chain. First, the Applications on the hub, which are not reconciled there:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-bash hljs" data-lang="bash">oc get applications -n openshift-gitops -l apps.open-cluster-management.io/pull-to-ocm-managed-cluster=true</code></pre>
</div>
</div>
<div class="paragraph">
<p>Then the ManifestWork the propagation controller created in the namespace of one managed cluster:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-bash hljs" data-lang="bash">oc get manifestwork -n sno</code></pre>
</div>
</div>
<div class="paragraph">
<p>On the managed cluster, the Application now exists and is reconciled by the local Argo CD:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-bash hljs" data-lang="bash">oc get applications -n openshift-gitops</code></pre>
</div>
</div>
<div class="paragraph">
<p>Back on the hub, the aggregated status per ApplicationSet:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-bash hljs" data-lang="bash">oc get multiclusterapplicationsetreports -n openshift-gitops
oc get multiclusterapplicationsetreport branding-pull -n openshift-gitops -o yaml</code></pre>
</div>
</div>
<div class="paragraph">
<p>The report lists the sync and health status per cluster and a summary. A shortened example with one failing cluster:</p>
</div>
<div class="listingblock linenums">
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: apps.open-cluster-management.io/v1alpha1
kind: MulticlusterApplicationSetReport
metadata:
  name: branding-pull
  namespace: openshift-gitops
statuses:
  clusterConditions:
    - cluster: sno
      healthStatus: Healthy
      syncStatus: Synced
    - cluster: bm
      healthStatus: Missing
      syncStatus: OutOfSync
      conditions:
        - type: SyncError
          message: &#39;Failed sync attempt: ... is forbidden: User &#34;system:serviceaccount:openshift-gitops:openshift-gitops-argocd-application-controller&#34; cannot create resource ...&#39;
  summary:
    clusters: &#34;2&#34;
    healthy: &#34;1&#34;
    notHealthy: &#34;1&#34;
    synced: &#34;1&#34;
    notSynced: &#34;1&#34;</code></pre>
</div>
</div>
<div class="paragraph">
<p>The <code>forbidden</code> message in the example is the most common error with the pull model: the local controller is missing permissions (step 2). The report is the first place to look. For the details, go to the Argo CD of the spoke: it has the full conditions and events.</p>
</div>
</div>
<div class="sect2">
<h3 id="_the_visibility_trade_off">The visibility trade-off</h3>
<div class="paragraph">
<p>The pull model has one weak spot: <strong>visibility</strong>. The hub only gets an aggregated and delayed status. The hub cluster never connects directly to the spoke clusters and therefore never sees the full resource tree.</p>
</div>
<div class="paragraph">
<p>The result is the following:</p>
</div>
<div class="paragraph">
<p>While the ApplicationSet in the hub shows a healthy sync status:</p>
</div>
<div class="imageblock">
<div class="content">
<img src="/gitopscollection/images/ep-19/pull-argocd-applicationset-branding-pull.png" alt="Pull Model ApplicationSet"/>
</div>
<div class="title">Figure 5. ApplicationSet in the hub shows a healthy sync status</div>
</div>
<div class="paragraph">
<p>The Application itself shows also a healthy sync status BUT …​ it does not show any child-resources.</p>
</div>
<div class="imageblock">
<div class="content">
<img src="/gitopscollection/images/ep-19/pull-argocd-application-branding-bm.png" alt="Pull Model UI Trade-off"/>
</div>
<div class="title">Figure 6. Pull Model UI Trade-off</div>
</div>
<div class="admonitionblock caution">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-caution" title="Caution"></i>
</td>
<td class="content">
The whole resource tree is missing in the UI. You are basically blind and you cannot see a Diff. You can check <code>oc get multiclusterapplicationsetreport branding-pull -n openshift-gitops -o yaml</code> to see the full status, but that’s not as nice as seeing it in the UI.
</td>
</tr>
</tbody></table>
</div>
</div>
<div class="sect2">
<h3 id="_mixing_push_and_pull">Mixing push and pull</h3>
<div class="paragraph">
<p>The model is decided <strong>per ApplicationSet</strong>, by the label and annotations in the template. One hub Argo CD can therefore use push and pull ApplicationSets side by side. A pattern I like: platform components via push (one UI, full visibility for a small platform team), application workloads via pull.</p>
</div>
<div class="paragraph">
<p>The one exception is the GitOps add-on: once it is enabled through a GitOpsCluster, the push model is switched off. If you want to mix, install OpenShift GitOps on the spokes with your own policy (option B).</p>
</div>
<hr/>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_the_argo_cd_agent_via_the_gitops_add_on">The Argo CD Agent via the GitOps add-on</h2>
<div class="sectionbody">
<div class="sect2">
<h3 id="_what_it_actually_is_3">What it actually is</h3>
<div class="paragraph">
<p>The pull model has one weak spot: visibility. The hub only gets an aggregated, delayed status. The Argo CD Agent (see <a href="/gitopscollection/2026-01-14-argocd-agent/">Ep.15</a>) solves exactly that. A lightweight agent on the spoke reconciles locally and synchronises the full Application status back to the <strong>principal</strong> on the hub. The connection is initiated by the spoke (gRPC with mutual TLS), the hub holds no Kubernetes credentials for the spokes, and the Argo CD UI on the hub shows the full resource tree.</p>
</div>
<div class="paragraph">
<p>Setting up the agent by hand means a lot of plumbing: a CA, certificates for the principal and for every agent, registering every agent, installing the agent on every spoke. This is where ACM helps. The GitOps add-on in <strong>Agent mode</strong> does all of that for the clusters of a Placement: it installs OpenShift GitOps and the agent on the spokes, manages the PKI and creates the cluster secrets in agent mode.</p>
</div>
<div class="paragraph">
<div class="imageblock animated-svg" data-animated-svg>
  <div class="content">
<svg width="1400" height="780" viewBox="0 0 1400 780" role="img" aria-label="Argo CD Agent via the ACM GitOps add-on: the hub Argo CD runs as principal with its controller disabled. The GitOpsCluster creates certificates, agent-mode cluster secrets and ManifestWorks; the gitops-addon on the managed cluster installs OpenShift GitOps and the agent. The agent opens a gRPC connection with mutual TLS to the principal, receives the Applications routed by destination name, reconciles them locally and sends the full status back, so the hub UI shows the complete resource tree" version="1.1" id="asdcaeff4-svg-agent-model" xmlns="http://www.w3.org/2000/svg" xmlns:c2pa="http://c2pa.org/manifest">
  <title id="asdcaeff4-title-agent-model">Argo CD Agent via the ACM GitOps add-on: the hub Argo CD runs as principal with its controller disabled. The GitOpsCluster creates certificates, agent-mode cluster secrets and ManifestWorks; the gitops-addon on the managed cluster installs OpenShift GitOps and the agent. The agent opens a gRPC connection with mutual TLS to the principal, receives the Applications routed by destination name, reconciles them locally and sends the full status back, so the hub UI shows the complete resource tree</title>
  <defs id="asdcaeff4-defs-agent-model">
    <marker id="asdcaeff4-arrow-0F766E" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#0F766E" id="asdcaeff4-marker-path-0F766E" /></marker>
    <marker id="asdcaeff4-arrow-C2410C" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#C2410C" id="asdcaeff4-marker-path-C2410C" /></marker>
    <marker id="asdcaeff4-arrow-4B5563" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#4B5563" id="asdcaeff4-marker-path-4B5563" /></marker>
    </defs>
  <rect id="asdcaeff4-bg" width="1400" height="780" fill="#FFFFFF" />
  <rect id="asdcaeff4-zone-hub" x="20" y="20" width="1360" height="360" rx="14" fill="#F8FAFC" stroke="#E2E8F0" stroke-width="1.5" />
  <text id="asdcaeff4-label-hub" x="40" y="48" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="700" fill="#4B5563" text-anchor="start">HUB</text>
  <rect id="asdcaeff4-zone-spoke" x="20" y="450" width="1360" height="280" rx="14" fill="#F8FAFC" stroke="#E2E8F0" stroke-width="1.5" />
  <text id="asdcaeff4-label-spoke" x="40" y="478" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="700" fill="#4B5563" text-anchor="start">MANAGED CLUSTER  ·  bm, sno</text>
  <rect id="asdcaeff4-ns-gitops" x="40" y="60" width="1020" height="300" rx="10" fill="#FFFFFF" stroke="#CBD5E1" stroke-width="1.2" />
  <text id="asdcaeff4-label-ns" x="56" y="84" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="700" fill="#C2410C" text-anchor="start">openshift-gitops</text>
  <rect id="asdcaeff4-ns-cluster" x="1080" y="60" width="280" height="300" rx="10" fill="#FFFFFF" stroke="#CBD5E1" stroke-width="1.2" />
  <text id="asdcaeff4-label-ns-cluster" x="1096" y="84" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="700" fill="#4B5563" text-anchor="start">cluster namespaces</text>
  <rect id="asdcaeff4-rect-placement" x="60" y="100" width="180" height="100" rx="10" fill="#E6F4F1" stroke="#0F766E" stroke-width="1.6" />
  <text id="asdcaeff4-title-placement" x="76" y="129" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#0F766E" text-anchor="start">Placement</text>
  <text id="asdcaeff4-line0-placement" x="76" y="152" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#1F2937" text-anchor="start">production-clusters</text>
  <rect id="asdcaeff4-rect-gitopscluster" x="260" y="100" width="230" height="100" rx="10" fill="#E6F4F1" stroke="#0F766E" stroke-width="1.6" />
  <text id="asdcaeff4-title-gitopscluster" x="276" y="129" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#0F766E" text-anchor="start">GitOpsCluster</text>
  <text id="asdcaeff4-line0-gitopscluster" x="276" y="152" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#1F2937" text-anchor="start">gitopsAddon + argoCDAgent</text>
  <text id="asdcaeff4-line1-gitopscluster" x="276" y="168" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">mode: managed</text>
  <circle id="asdcaeff4-badge-2" cx="470" cy="120" r="11" fill="#1F2937" />
  <text id="asdcaeff4-badge-2-text" x="470" y="124.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">2</text>
  <rect id="asdcaeff4-rect-addon" x="1096" y="100" width="248" height="110" rx="10" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="asdcaeff4-title-addon" x="1112" y="129" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#0F766E" text-anchor="start">Add-on resources</text>
  <text id="asdcaeff4-line0-addon" x="1112" y="152" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">certificates, cluster secrets</text>
  <text id="asdcaeff4-line1-addon" x="1112" y="168" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">in agent mode, ManifestWorks</text>
  <text id="asdcaeff4-line2-addon" x="1112" y="184" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">per managed cluster</text>
  <rect id="asdcaeff4-rect-appset" x="60" y="240" width="180" height="100" rx="10" fill="#E6F4F1" stroke="#0F766E" stroke-width="1.6" />
  <text id="asdcaeff4-title-appset" x="76" y="269" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#0F766E" text-anchor="start">ApplicationSet</text>
  <text id="asdcaeff4-line0-appset" x="76" y="292" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">destination.name:</text>
  <text id="asdcaeff4-line1-appset" x="76" y="308" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#1F2937" text-anchor="start">&#x27;{{name}}&#x27;</text>
  <circle id="asdcaeff4-badge-5" cx="220" cy="260" r="11" fill="#1F2937" />
  <text id="asdcaeff4-badge-5-text" x="220" y="264.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">5</text>
  <rect id="asdcaeff4-rect-applications" x="260" y="240" width="230" height="100" rx="10" fill="#FFFFFF" stroke="#C2410C" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="asdcaeff4-title-applications" x="276" y="269" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#C2410C" text-anchor="start">Applications</text>
  <text id="asdcaeff4-line0-applications" x="276" y="292" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">on the hub, one per cluster,</text>
  <text id="asdcaeff4-line1-applications" x="276" y="308" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">routed by destination.name</text>
  <rect id="asdcaeff4-rect-principal" x="520" y="240" width="250" height="100" rx="10" fill="#FDF0E8" stroke="#C2410C" stroke-width="1.6" />
  <text id="asdcaeff4-title-principal" x="536" y="269" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#C2410C" text-anchor="start">Argo CD principal</text>
  <text id="asdcaeff4-line0-principal" x="536" y="292" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#1F2937" text-anchor="start">controller disabled</text>
  <text id="asdcaeff4-line1-principal" x="536" y="308" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">gRPC endpoint via Route</text>
  <circle id="asdcaeff4-badge-1" cx="750" cy="260" r="11" fill="#1F2937" />
  <text id="asdcaeff4-badge-1-text" x="750" y="264.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">1</text>
  <rect id="asdcaeff4-rect-ui" x="800" y="240" width="240" height="100" rx="10" fill="#F1F5F9" stroke="#4B5563" stroke-width="1.4" />
  <text id="asdcaeff4-title-ui" x="816" y="269" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#4B5563" text-anchor="start">Argo CD UI on the hub</text>
  <text id="asdcaeff4-line0-ui" x="816" y="292" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">full resource tree,</text>
  <text id="asdcaeff4-line1-ui" x="816" y="308" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">live status of every cluster</text>
  <rect id="asdcaeff4-rect-agent" x="520" y="500" width="250" height="96" rx="10" fill="#FDF0E8" stroke="#C2410C" stroke-width="1.6" />
  <text id="asdcaeff4-title-agent" x="536" y="529" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#C2410C" text-anchor="start">Argo CD Agent</text>
  <text id="asdcaeff4-line0-agent" x="536" y="552" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">mode managed,</text>
  <text id="asdcaeff4-line1-agent" x="536" y="568" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">opens the connection</text>
  <circle id="asdcaeff4-badge-4" cx="750" cy="520" r="11" fill="#1F2937" />
  <text id="asdcaeff4-badge-4-text" x="750" y="524.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">4</text>
  <rect id="asdcaeff4-rect-local" x="520" y="620" width="250" height="96" rx="10" fill="#FDF0E8" stroke="#C2410C" stroke-width="1.6" />
  <text id="asdcaeff4-title-local" x="536" y="649" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#C2410C" text-anchor="start">Local components</text>
  <text id="asdcaeff4-line0-local" x="536" y="672" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#1F2937" text-anchor="start">application-controller,</text>
  <text id="asdcaeff4-line1-local" x="536" y="688" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">repo-server, Redis, no UI</text>
  <circle id="asdcaeff4-badge-6" cx="750" cy="640" r="11" fill="#1F2937" />
  <text id="asdcaeff4-badge-6-text" x="750" y="644.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">6</text>
  <rect id="asdcaeff4-rect-resources" x="800" y="620" width="240" height="96" rx="10" fill="#FFFFFF" stroke="#C2410C" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="asdcaeff4-title-resources" x="816" y="649" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#C2410C" text-anchor="start">Resources</text>
  <text id="asdcaeff4-line0-resources" x="816" y="672" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">applied locally, Git pulled</text>
  <text id="asdcaeff4-line1-resources" x="816" y="688" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">by the spoke</text>
  <rect id="asdcaeff4-rect-gitops-addon" x="1096" y="500" width="248" height="96" rx="10" fill="#F1F5F9" stroke="#4B5563" stroke-width="1.4" />
  <text id="asdcaeff4-title-gitops-addon" x="1112" y="529" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="15" font-weight="700" fill="#4B5563" text-anchor="start">gitops-addon</text>
  <text id="asdcaeff4-line0-gitops-addon" x="1112" y="552" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">installs OpenShift GitOps</text>
  <text id="asdcaeff4-line1-gitops-addon" x="1112" y="568" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#1F2937" text-anchor="start">and the agent</text>
  <circle id="asdcaeff4-badge-3" cx="1324" cy="520" r="11" fill="#1F2937" />
  <text id="asdcaeff4-badge-3-text" x="1324" y="524.5" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#FFFFFF" text-anchor="middle">3</text>
  <text id="asdcaeff4-note-spoke1" x="60" y="524" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="700" fill="#1F2937" text-anchor="start">No Kubernetes credentials of the</text>
  <text id="asdcaeff4-note-spoke2" x="60" y="542" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">spoke on the hub. The agent opens</text>
  <text id="asdcaeff4-note-spoke3" x="60" y="560" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">the only connection (outbound).</text>
  <text id="asdcaeff4-note-spoke4" x="60" y="590" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12.5" font-weight="400" fill="#4B5563" text-anchor="start">No hub, still reconciliation.</text>
  <path id="asdcaeff4-flow-placement-gitopscluster" d="M 242 150 L 256 150" fill="none" stroke="#0F766E" stroke-width="2" stroke-linejoin="round" marker-end="url(#asdcaeff4-arrow-0F766E)" />
  <path id="asdcaeff4-flow-gitopscluster-addon" d="M 492 150 L 1092 150" fill="none" stroke="#0F766E" stroke-width="2" stroke-linejoin="round" marker-end="url(#asdcaeff4-arrow-0F766E)" />
  <text id="asdcaeff4-label-creates" x="792" y="141" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="600" fill="#0F766E" text-anchor="middle">creates</text>
  <path id="asdcaeff4-flow-addon-gitopsaddon" d="M 1220 212 L 1220 496" fill="none" stroke="#0F766E" stroke-width="2" stroke-linejoin="round" marker-end="url(#asdcaeff4-arrow-0F766E)" />
  <text id="asdcaeff4-label-mw1" x="1230" y="408" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="600" fill="#0F766E" text-anchor="start">ManifestWorks,</text>
  <text id="asdcaeff4-label-mw2" x="1230" y="422" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="600" fill="#0F766E" text-anchor="start">pulled by the klusterlet</text>
  <path id="asdcaeff4-flow-gitopsaddon-agent" d="M 1094 548 L 774 548" fill="none" stroke="#4B5563" stroke-width="1.6" stroke-linejoin="round" marker-end="url(#asdcaeff4-arrow-4B5563)" />
  <text id="asdcaeff4-label-installs" x="934" y="540" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="600" fill="#4B5563" text-anchor="middle">installs, with client certificate</text>
  <path id="asdcaeff4-flow-placement-appset" d="M 150 202 L 150 236" fill="none" stroke="#0F766E" stroke-width="2" stroke-linejoin="round" marker-end="url(#asdcaeff4-arrow-0F766E)" />
  <path id="asdcaeff4-flow-appset-applications" d="M 242 290 L 256 290" fill="none" stroke="#C2410C" stroke-width="2" stroke-linejoin="round" marker-end="url(#asdcaeff4-arrow-C2410C)" />
  <path id="asdcaeff4-flow-applications-principal" d="M 492 290 L 516 290" fill="none" stroke="#C2410C" stroke-width="2" stroke-linejoin="round" marker-end="url(#asdcaeff4-arrow-C2410C)" />
  <path id="asdcaeff4-flow-principal-ui" d="M 772 290 L 796 290" fill="none" stroke="#C2410C" stroke-width="2" stroke-linejoin="round" marker-end="url(#asdcaeff4-arrow-C2410C)" />
  <circle class="flow-dot" style="--d:1.60s" visibility="hidden" r="4.5" fill="#C2410C" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 575 342 L 575 496" begin="indefinite" data-begin="1.60" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <rect id="asdcaeff4-bg-conn-label" x="380" y="392" width="200" height="50" rx="0" fill="#FFFFFF" stroke="#FFFFFF" stroke-width="0" />
  <path id="asdcaeff4-flow-principal-agent" d="M 575 342 L 575 496" fill="none" stroke="#C2410C" stroke-width="2" stroke-linejoin="round" marker-end="url(#asdcaeff4-arrow-C2410C)" />
  <text id="asdcaeff4-label-apps" x="567" y="404" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="700" fill="#C2410C" text-anchor="end">Applications</text>
  <path id="asdcaeff4-conn-agent-principal" d="M 645 496 L 645 344" fill="none" stroke="#4B5563" stroke-width="1.6" stroke-linejoin="round" stroke-dasharray="5 3" marker-end="url(#asdcaeff4-arrow-4B5563)" />
  <text id="asdcaeff4-label-conn1" x="567" y="424" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="600" fill="#4B5563" text-anchor="end">gRPC stream, mTLS,</text>
  <text id="asdcaeff4-label-conn2" x="567" y="438" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="600" fill="#4B5563" text-anchor="end">opened by the agent</text>
  <path id="asdcaeff4-status-agent-principal" d="M 715 496 L 715 344" fill="none" stroke="#0F766E" stroke-width="1.6" stroke-linejoin="round" stroke-dasharray="5 3" marker-end="url(#asdcaeff4-arrow-0F766E)" />
  <text id="asdcaeff4-label-status1" x="725" y="404" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="700" fill="#0F766E" text-anchor="start">full status back:</text>
  <text id="asdcaeff4-label-status2" x="725" y="418" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="11" font-weight="600" fill="#0F766E" text-anchor="start">resource tree, health, events</text>
  <path id="asdcaeff4-flow-agent-local" d="M 645 598 L 645 616" fill="none" stroke="#C2410C" stroke-width="2" stroke-linejoin="round" marker-end="url(#asdcaeff4-arrow-C2410C)" />
  <path id="asdcaeff4-flow-local-resources" d="M 772 668 L 796 668" fill="none" stroke="#C2410C" stroke-width="2" stroke-linejoin="round" marker-end="url(#asdcaeff4-arrow-C2410C)" />
  <rect id="asdcaeff4-legend-you" x="40" y="752" width="22" height="14" rx="3" fill="#E6F4F1" stroke="#0F766E" stroke-width="1.6" />
  <text id="asdcaeff4-legend-you-text" x="70" y="764" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">created by you</text>
  <rect id="asdcaeff4-legend-acm" x="194.609375" y="752" width="22" height="14" rx="3" fill="#FFFFFF" stroke="#0F766E" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="asdcaeff4-legend-acm-text" x="224.609375" y="764" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">generated by ACM</text>
  <rect id="asdcaeff4-legend-argo" x="370.5625" y="752" width="22" height="14" rx="3" fill="#FFFFFF" stroke="#C2410C" stroke-width="1.6" stroke-dasharray="6 4" />
  <text id="asdcaeff4-legend-argo-text" x="400.5625" y="764" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">generated by Argo CD</text>
  <rect id="asdcaeff4-legend-platform" x="566.984375" y="752" width="22" height="14" rx="3" fill="#F1F5F9" stroke="#4B5563" stroke-width="1.6" />
  <text id="asdcaeff4-legend-platform-text" x="596.984375" y="764" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="400" fill="#1F2937" text-anchor="start">platform component</text>
  <text id="asdcaeff4-legend-tp" x="1380" y="764" font-family="Inter, 'Liberation Sans', Arial, sans-serif" font-size="12" font-weight="600" fill="#4B5563" text-anchor="end">Technology Preview in ACM 2.17</text>
<circle class="flow-dot" style="--d:0.00s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 242 150 L 256 150" begin="indefinite" data-begin="0.00" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:0.40s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 492 150 L 1092 150" begin="indefinite" data-begin="0.40" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:0.80s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 1220 212 L 1220 496" begin="indefinite" data-begin="0.80" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:1.20s" visibility="hidden" r="3.5" fill="#4B5563" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 1094 548 L 774 548" begin="indefinite" data-begin="1.20" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:0.40s" visibility="hidden" r="4.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 150 202 L 150 236" begin="indefinite" data-begin="0.40" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:0.00s" visibility="hidden" r="3.5" fill="#4B5563" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 645 496 L 645 344" begin="indefinite" data-begin="0.00" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:0.80s" visibility="hidden" r="4.5" fill="#C2410C" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 242 290 L 256 290" begin="indefinite" data-begin="0.80" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:1.20s" visibility="hidden" r="4.5" fill="#C2410C" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 492 290 L 516 290" begin="indefinite" data-begin="1.20" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:2.00s" visibility="hidden" r="4.5" fill="#C2410C" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 645 598 L 645 616" begin="indefinite" data-begin="2.00" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:2.40s" visibility="hidden" r="4.5" fill="#C2410C" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 772 668 L 796 668" begin="indefinite" data-begin="2.40" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:2.80s" visibility="hidden" r="3.5" fill="#0F766E" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 715 496 L 715 344" begin="indefinite" data-begin="2.80" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  <circle class="flow-dot" style="--d:3.20s" visibility="hidden" r="4.5" fill="#C2410C" stroke="#FFFFFF" stroke-width="1.5"><animateMotion path="M 772 290 L 796 290" begin="indefinite" data-begin="3.20" dur="1.40s" repeatCount="indefinite" calcMode="linear"/></circle>
  </svg>
</div>
  <div class="title">Argo CD Agent: the agent opens the connection, receives the Applications and sends the full status back</div>
</div>

</div>
<div class="paragraph">
<p>What happens under the hood:</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>The Argo CD instance on the hub runs as <strong>principal</strong>. In the ACM configuration its application-controller is disabled, and a Route exposes the gRPC endpoint for the agents.</p>
</li>
<li>
<p>The <strong>GitOpsCluster</strong> with the add-on and the agent enabled creates the certificates, the cluster secrets in agent mode and the ManifestWorks for every cluster of the Placement.</p>
</li>
<li>
<p>The <strong>gitops-addon</strong> on the managed cluster installs OpenShift GitOps and the agent, including the client certificate of the agent.</p>
</li>
<li>
<p>The <strong>agent</strong> opens the connection to the principal: a gRPC stream with mutual TLS, initiated by the spoke.</p>
</li>
<li>
<p>The <strong>ApplicationSet</strong> generates the Applications on the hub. With destination-based mapping, the principal sends each Application to the agent whose name matches <code>spec.destination.name</code>.</p>
</li>
<li>
<p>The <strong>local components</strong> on the managed cluster pull Git and reconcile. The agent sends the full status back, so the Argo CD UI on the hub shows the resource tree, health and events of every cluster.</p>
</li>
</ol>
</div>
<div class="admonitionblock important">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-important" title="Important"></i>
</td>
<td class="content">
<div class="ulist">
<ul>
<li>
<p>The Agent mode of the GitOps add-on is <strong>Technology Preview</strong> in ACM 2.17. With ACM 5.0 which is planned for November 2026 this will become GA.</p>
</li>
<li>
<p>Every cluster that runs the Argo CD Agent requires an <strong>OpenShift Platform Plus</strong> subscription.</p>
</li>
</ul>
</div>
</td>
</tr>
</tbody></table>
</div>
</div>
<div class="sect2">
<h3 id="_step_1_prepare_the_hub_argo_cd">Step 1: Prepare the hub Argo CD</h3>
<div class="paragraph">
<p>First we need to prepare Argo CD. We need to modify the Operator Subscription and the Argo CD resource. This step has an important consequence: the Argo CD instance on the hub becomes an agent <strong>principal</strong>. Its own application-controller is <strong>disabled</strong>, so this instance no longer pushes to any cluster.</p>
</div>
<div class="paragraph">
<p>OpenShift GitOps 1.22 itself also supports a hybrid architecture (GA), where the application-controller stays enabled and push and agent Applications run side by side in one instance. The ACM 2.17 documentation for the add-on does not cover that combination yet.</p>
</div>
<div class="paragraph">
<p>First, add the required environment variables to the <code>Subscription</code> of the OpenShift GitOps Operator on the hub:</p>
</div>
<div class="listingblock linenums">
<div class="title">Subscription of the OpenShift GitOps Operator on the hub (excerpt)</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">spec:
  config:
    env:
      - name: ARGOCD_CLUSTER_CONFIG_NAMESPACES <i class="conum" data-value="1"></i><b>(1)</b>
        value: openshift-gitops,local-cluster
      - name: ARGOCD_PRINCIPAL_TLS_SERVER_ALLOW_GENERATE <i class="conum" data-value="2"></i><b>(2)</b>
        value: &#34;false&#34;
      - name: ARGOCD_PRINCIPAL_REDIS_SERVER_ADDRESS <i class="conum" data-value="3"></i><b>(3)</b>
        value: openshift-gitops-redis:6379</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Namespaces in which the OpenShift GitOps Operator runs Argo CD instances as cluster-scoped instances, with permissions for the whole cluster. The ACM documentation lists the default instance in <code>openshift-gitops</code> and <code>local-cluster</code>, the cluster namespace of the hub itself.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Do not let the principal generate a temporary, self-signed TLS certificate. That option is meant for development only. The server certificate of the principal is provided instead (the GitOps add-on manages the PKI).</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>The Redis instance the principal uses. The default of the principal is <code>argocd-redis:6379</code>, which does not exist with OpenShift GitOps. The Redis service of the default instance is called <code>openshift-gitops-redis</code>.</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>Then configure the ArgoCD resource on the hub for agent mode:</p>
</div>
<div class="listingblock linenums">
<div class="title">ArgoCD resource on the hub in agent mode</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: argoproj.io/v1beta1
kind: ArgoCD
metadata:
  name: openshift-gitops
  namespace: openshift-gitops
spec:
  controller:
    enabled: false <i class="conum" data-value="1"></i><b>(1)</b>
  sourceNamespaces:
    - &#39;*&#39;
  argoCDAgent:
    principal:
      enabled: true <i class="conum" data-value="2"></i><b>(2)</b>
      destinationBasedMapping: true <i class="conum" data-value="3"></i><b>(3)</b>
      auth: &#34;mtls:CN=system:open-cluster-management:cluster:([^:]+):addon:gitops-addon:agent:gitops-addon-agent&#34; <i class="conum" data-value="4"></i><b>(4)</b>
      namespace:
        allowedNamespaces:
          - &#39;*&#39;
      server:
        route:
          enabled: true <i class="conum" data-value="5"></i><b>(5)</b></code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>The hub instance no longer reconciles Applications itself.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Turns the instance into an agent principal.</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>Applications are routed to an agent by <code>spec.destination.name</code> instead of by namespace.</td>
</tr>
<tr>
<td><i class="conum" data-value="4"></i><b>4</b></td>
<td>The agent identity is taken from the client certificate that the add-on issues.</td>
</tr>
<tr>
<td><i class="conum" data-value="5"></i><b>5</b></td>
<td>Exposes the principal with a Route. The agents connect to it, and the GitOpsCluster controller discovers its address from there.</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>Finally, the principal only propagates AppProjects whose <code>destinations</code> and <code>sourceNamespaces</code> match the agents. The ACM documentation therefore configures the <code>default</code> AppProject in <code>openshift-gitops</code> with matching (or wildcard) values.</p>
</div>
<div class="admonitionblock warning">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-warning" title="Warning"></i>
</td>
<td class="content">
If your hub Argo CD currently deploys anything via the push model, do not convert it. Install a second, dedicated Argo CD instance for the agent principal, or keep the agent for a separate hub. OpenShift GitOps 1.22 itself supports a <strong>hybrid architecture</strong> (push and agent Applications in one control plane, GA in 1.22), but the ACM 2.17 documentation for the add-on configures the hub instance with its controller disabled.
</td>
</tr>
</tbody></table>
</div>
</div>
<div class="sect2">
<h3 id="_step_2_the_gitopscluster_in_agent_mode">Step 2: The GitOpsCluster in agent mode</h3>
<div class="listingblock linenums">
<div class="title">GitOpsCluster with the GitOps add-on and the Argo CD Agent</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: apps.open-cluster-management.io/v1beta1
kind: GitOpsCluster
metadata:
  name: gitops-agent-clusters
  namespace: openshift-gitops
spec:
  argoServer:
    argoNamespace: openshift-gitops
  placementRef:
    kind: Placement
    apiVersion: cluster.open-cluster-management.io/v1beta1
    name: production-clusters
    namespace: openshift-gitops
  gitopsAddon:
    enabled: true <i class="conum" data-value="1"></i><b>(1)</b>
    argoCDAgent:
      enabled: true <i class="conum" data-value="2"></i><b>(2)</b>
      mode: managed <i class="conum" data-value="3"></i><b>(3)</b></code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Installs OpenShift GitOps on the spokes, as in Basic mode.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Additionally deploys the agent, creates the PKI and the cluster secrets in agent mode.</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td><code>managed</code>: the hub is the source of truth for Applications. <code>autonomous</code>: the spoke owns its Applications and the hub only observes them. The default is <code>managed</code>.</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>The controller discovers the principal address from the Route.</p>
</div>
<div class="paragraph">
<p>You can check the discovered address with the following command:</p>
</div>
<div class="admonitionblock warning">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-warning" title="Warning"></i>
</td>
<td class="content">
It will take a while until it appears.
</td>
</tr>
</tbody></table>
</div>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-bash hljs" data-lang="bash">oc get gitopscluster gitops-agent-clusters -n openshift-gitops \
  -o jsonpath=&#39;{.spec.gitopsAddon.argoCDAgent.serverAddress}{&#34;\n&#34;}&#39;</code></pre>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_step_3_permissions_on_the_managed_clusters">Step 3: Permissions on the managed clusters</h3>
<div class="paragraph">
<p>When the GitOpsCluster is created, ACM also creates the policy <code>&lt;gitopscluster-name&gt;-argocd-policy</code> on the hub. It is created once and then belongs to you, so it is the right place to add the permissions. Two service accounts on the spokes need them:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>the <strong>agent</strong> itself, which reports the status and the live resources to the principal, needs read access,</p>
</li>
<li>
<p>the local <strong>application-controller</strong>, which actually applies the manifests, needs write access to everything the Applications deploy.</p>
</li>
</ul>
</div>
<div class="listingblock linenums">
<div class="title">Additional object templates in the gitops-agent-clusters-argocd-policy</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">            - complianceType: musthave
              objectDefinition:
                apiVersion: rbac.authorization.k8s.io/v1
                kind: ClusterRoleBinding
                metadata:
                  name: acm-openshift-gitops-argocd-agent-cluster-reader
                subjects:
                  - kind: ServiceAccount
                    name: acm-openshift-gitops-agent-agent <i class="conum" data-value="1"></i><b>(1)</b>
                    namespace: openshift-gitops
                roleRef:
                  apiGroup: rbac.authorization.k8s.io
                  kind: ClusterRole
                  name: view
            - complianceType: musthave
              objectDefinition:
                apiVersion: rbac.authorization.k8s.io/v1
                kind: ClusterRoleBinding
                metadata:
                  name: argo-admin
                subjects:
                  - kind: ServiceAccount
                    name: acm-openshift-gitops-argocd-application-controller <i class="conum" data-value="2"></i><b>(2)</b>
                    namespace: openshift-gitops
                roleRef:
                  apiGroup: rbac.authorization.k8s.io
                  kind: ClusterRole
                  name: cluster-admin <i class="conum" data-value="3"></i><b>(3)</b></code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>The agent: read access is enough.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>The local application-controller of the instance the add-on creates.</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>Simple and usually fine for platform configuration. For application teams, use the least-privilege approach from the pull model instead: the label <code>argocd.argoproj.io/managed-by</code> on the destination namespaces.</td>
</tr>
</tbody></table>
</div>
</div>
<div class="sect2">
<h3 id="_step_4_the_applicationset">Step 4: The ApplicationSet</h3>
<div class="paragraph">
<p>With destination-based mapping, the ApplicationSet looks almost like a push ApplicationSet. The only difference is the destination: <code>name</code> instead of <code>server</code>.</p>
</div>
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
The push example already installed the Compliance Operator and the pull example the console branding. The agent example therefore uses a third folder, <code>clusters/&lt;cluster&gt;/branding-bottom</code>, so that the three models do not manage the same objects.
</td>
</tr>
</tbody></table>
</div>
<div class="listingblock linenums">
<div class="title">ApplicationSet for agent mode</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: branding-bottom-agent
  namespace: openshift-gitops
spec:
  generators:
    - clusterDecisionResource:
        configMapRef: acm-placement
        labelSelector:
          matchLabels:
            cluster.open-cluster-management.io/placement: production-clusters
        requeueAfterSeconds: 30
  template:
    metadata:
      name: &#39;branding-bottom-{{name}}&#39;
    spec:
      project: default
      source:
        repoURL: https://github.com/tjungbauer/openshift-clusterconfig-gitops.git
        targetRevision: main
        path: &#39;clusters/{{name}}/branding-bottom&#39; <i class="conum" data-value="1"></i><b>(1)</b>
      destination:
        name: &#39;{{name}}&#39; <i class="conum" data-value="2"></i><b>(2)</b>
      syncPolicy:
        automated:
          prune: true
          selfHeal: true</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>One folder per cluster: <code>clusters/sno/branding-bottom</code> and <code>clusters/bm/branding-bottom</code>.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>The principal routes the Application to the agent with this name, which is the name of the managed cluster.</td>
</tr>
</tbody></table>
</div>
</div>
<div class="sect2">
<h3 id="_step_5_verify">Step 5: Verify</h3>
<div class="listingblock">
<div class="content">
<pre class="highlightjs highlight"><code class="language-bash hljs" data-lang="bash"># hub: overall state, the conditions tell you which step failed
oc get gitopscluster gitops-agent-clusters -n openshift-gitops \
  -o jsonpath=&#39;{.status.conditions}&#39; | jq

# hub: the principal is running
oc get pods -n openshift-gitops -l app.kubernetes.io/name=openshift-gitops-agent-principal

# managed cluster: the agent is running
oc get pods -n openshift-gitops -l app.kubernetes.io/part-of=argocd-agent

# hub: Application status, synchronised by the agent
oc get applications.argoproj.io -n openshift-gitops</code></pre>
</div>
</div>
</div>
<div class="sect2">
<h3 id="_the_visibility_in_the_ui">The visibility in the UI</h3>
<div class="paragraph">
<p>The big advantage of the Argo CD Agent is the visibility in the UI. While the ApplicationSet in the hub shows a healthy sync status:</p>
</div>
<div class="imageblock">
<div class="content">
<img src="/gitopscollection/images/ep-19/agent-argocd-applicationset-branding-bottom-agent.png" alt="Agent Model ApplicationSet"/>
</div>
<div class="title">Figure 7. ApplicationSet in the hub shows a healthy sync status</div>
</div>
<div class="paragraph">
<p>The Application itself now also shows the full resource tree.</p>
</div>
<div class="imageblock">
<div class="content">
<img src="/gitopscollection/images/ep-19/agent-argocd-application-branding-bottom-bm.png" alt="Agent Model UI Visibility"/>
</div>
<div class="title">Figure 8. Agent Model UI Visibility</div>
</div>
<hr/>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_side_by_side">Side by side</h2>
<div class="sectionbody">
<table class="tableblock frame-all grid-all stretch">
<colgroup>
<col style="width: 18.1818%;"/>
<col style="width: 27.2727%;"/>
<col style="width: 27.2727%;"/>
<col style="width: 27.2728%;"/>
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top">Aspect</th>
<th class="tableblock halign-left valign-top">Push</th>
<th class="tableblock halign-left valign-top">Pull</th>
<th class="tableblock halign-left valign-top">Argo CD Agent (add-on)</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Status in ACM 2.17</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">GA, default</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">GA</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Technology Preview (GA in ACM 5.0)</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Where Argo CD reconciles</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Hub</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Managed cluster (full Argo CD instance)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Managed cluster (agent with local Argo CD components)</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Network direction</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Hub → spoke (cluster proxy or API)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Spoke → hub (klusterlet)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Spoke → hub (agent → principal)</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Spoke credentials on the hub</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Token per cluster</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Cluster secret exists, not used for delivery</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">mTLS certificates, managed by the add-on</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Status visible on the hub</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Full (resource tree, live diff)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Aggregated report, not real-time</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Full, synchronised by the agent</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Hub down</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">No reconciliation</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Reconciliation continues</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Reconciliation continues</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Footprint on the spoke</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">None</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Full OpenShift GitOps instance</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">OpenShift GitOps plus agent</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Git access needed from</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Hub only</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Every spoke</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Every spoke</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Can be mixed with push on the same hub instance</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">–</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Yes (unless the add-on is enabled)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Not in the ACM 2.17 documented setup (hub controller disabled)</p></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="sect1">
<h2 id="_summary">Summary</h2>
<div class="sectionbody">
<div class="paragraph">
<p>I have to admit that this article is a bit long. But I hope it gives you a good overview of the different models and how to set them up.</p>
</div>
<div class="paragraph">
<p>Always keep in mind:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>The <strong>GitOpsCluster</strong> is the bridge: it turns placed managed clusters into Argo CD cluster secrets and keeps the tokens rotated.</p>
</li>
<li>
<p><strong>Push</strong> is the default and the easiest start: one Argo CD on the hub, full visibility, but the hub holds cluster-admin tokens for every spoke unless you use your own ManagedServiceAccount. With a huge fleet, usually greater than 20 clusters, it becomes unmanageable.</p>
</li>
<li>
<p><strong>Pull</strong> moves the reconciliation to the spokes: the hub ships Applications as ManifestWorks, a local Argo CD does the work. More scalable and outbound-only, at the price of an Argo CD per cluster and a delayed, aggregated status as well as a trade-off in visibility.</p>
</li>
<li>
<p>The <strong>Argo CD Agent add-on</strong> combines pull-style distribution with a full UI on the hub, and ACM takes care of the PKI. It is Technology Preview in ACM 2.17 and in the documented setup it turns the hub Argo CD into a principal without its own controller.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>In the <a href="/gitopscollection/2026-10-24-acm-policies-with-gitops/">last part</a>, we turn the integration around: Argo CD deploys ACM policies.</p>
</div>
<div class="paragraph">
<p>And as always: <strong>If it is not in Git, it does not exist.</strong></p>
</div>
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
The diagrams in this article have been manually drawn on a whiteboard. AI then generated the SVGs from photos and made them look equal in all articles.
</td>
</tr>
</tbody></table>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_references">References</h2>
<div class="sectionbody">
<div class="ulist">
<ul>
<li>
<p><a href="https://docs.redhat.com/en/documentation/red_hat_advanced_cluster_management_for_kubernetes/2.17/html/gitops/gitops-overview" rel="noopener" target="_blank">ACM 2.17: GitOps (registering clusters, push and pull model, GitOps add-on, Argo CD Agent)</a></p>
</li>
<li>
<p><a href="https://docs.redhat.com/en/documentation/red_hat_openshift_gitops/1.22/html/release_notes/gitops-release-notes" rel="noopener" target="_blank">Red Hat OpenShift GitOps 1.22 release notes</a></p>
</li>
<li>
<p><a href="https://www.redhat.com/en/blog/manage-clusters-and-applications-scale-argo-cd-agent-red-hat-openshift-gitops" rel="noopener" target="_blank">Red Hat blog: Manage clusters and applications at scale with the Argo CD Agent</a></p>
</li>
<li>
<p><a href="/gitopscollection/2026-01-14-argocd-agent/">[Ep.15] OpenShift GitOps - Argo CD Agent</a></p>
</li>
</ul>
</div>
</div>
</div>]]></content:encoded></item><item><title>[Ep.18] OpenShift GitOps or Advanced Cluster Management? Choosing the right tool</title><link>https://blog.stderr.at/gitopscollection/2026-10-20-openshift-gitops-vs-acm/</link><guid isPermaLink="true">https://blog.stderr.at/gitopscollection/2026-10-20-openshift-gitops-vs-acm/</guid><pubDate>Tue, 20 Oct 2026 00:00:00 +0200</pubDate><dc:creator>Articles by Thomas Jungbauer</dc:creator><category>OpenShift</category><category>GitOps</category><category>Advanced Cluster Management</category><description>Compare OpenShift GitOps and Advanced Cluster Management: what each tool does, where they overlap, how they work together and when to choose which.</description><content:encoded><![CDATA[<div class="paragraph">
<p>At customer engagements where I support the implementation of a GitOps process, I often hear the question: &#34;<em>Should we use OpenShift GitOps (Argo CD) or Advanced Cluster Management (Open Cluster Management)</em>?&#34; The honest answer is: it is the wrong question. Both tools solve different problems, they overlap in one specific area, and in larger environments with numerous clusters they are designed to work together.</p>
</div>
<div class="paragraph">
<p>In this article I will try to explain what each tool is (and also what it is not), where they overlap, how they integrate, and I will walk through an example, so that you can decide for yourself which tool fits which job.</p>
</div>
<div class="paragraph">
<p>The topic turned out to be too big for a single article, so I have split it into multiple parts and maybe I should have split into even more. This is the first:</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p><strong>[Ep.18] OpenShift GitOps or Advanced Cluster Management?</strong> (this article): what each tool does, where they overlap and a decision guide.</p>
</li>
<li>
<p><a href="/gitopscollection/2026-10-23-acm-gitops-push-pull-agent/">[Ep.19] ACM and OpenShift GitOps: push, pull and the Argo CD Agent</a>: how ACM registers clusters in Argo CD, and how to set up each of the three delivery models.</p>
</li>
<li>
<p><a href="/gitopscollection/2026-10-24-acm-policies-with-gitops/">[Ep.20] Managing ACM Policies with OpenShift GitOps</a>: policies as code, the PolicyGenerator, hub templates and bootstrapping Argo CD with a policy.</p>
</li>
</ol>
</div>
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
Everything in this series was written against <strong>OpenShift GitOps 1.22</strong> (Argo CD 3.5) and <strong>Advanced Cluster Management 2.17</strong> (Open Cluster Management). Technology Preview and GA states move quickly in both products, so check the release notes of your versions before you commit to a design.
</td>
</tr>
</tbody></table>
</div>
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
While I am using the term &#34;Advanced Cluster Management&#34; in this article, I am referring to the product formerly known as Open Cluster Management. Same counts for Argo CD and OpenShift GitOps.
</td>
</tr>
</tbody></table>
</div>
<div class="sect1">
<h2 id="_the_short_answer">The short answer</h2>
<div class="sectionbody">
<div class="ulist">
<ul>
<li>
<p><strong>OpenShift GitOps</strong> (Argo CD) answers the question: <em>&#34;Is the state in Git the state on my cluster(s)?&#34;</em> It is a delivery engine.</p>
</li>
<li>
<p><strong>Advanced Cluster Management</strong> (ACM) answers the question: <em>&#34;Which clusters do I have, how are they doing, and do they comply with my rules?&#34;</em> It is a fleet management platform.</p>
</li>
<li>
<p>ACM does not replace Argo CD. For application delivery, ACM <strong>uses</strong> OpenShift GitOps. What ACM adds is the knowledge about the fleet: which clusters exist, which labels they carry, which ones are healthy.</p>
</li>
<li>
<p>The real overlap is <strong>cluster configuration</strong>: you can configure clusters with Argo CD Applications or with ACM Policies. Choose what fits best, and never let both tools manage the same object.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>Or, in one sentence I like to use with customers: <strong>ACM decides <em>where</em> and checks <em>compliance</em>. Argo CD decides <em>what</em> and keeps it <em>in sync</em>.</strong></p>
</div>
<div class="admonitionblock tip">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-tip" title="Tip"></i>
</td>
<td class="content">
Other features ACM offers are an automated cluster deployment and centralized observability. But these are not the focus of this article.
</td>
</tr>
</tbody></table>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_what_openshift_gitops_is">What OpenShift GitOps is</h2>
<div class="sectionbody">
<div class="paragraph">
<p>OpenShift GitOps is Red Hat’s distribution of <strong>Argo CD</strong>, delivered and lifecycle-managed by the OpenShift GitOps Operator. It is included in the OpenShift subscription. At the time of writing, the current version is 1.22, which ships Argo CD 3.5. New releases and features come quickly, always check the release notes of your version first.</p>
</div>
<div class="paragraph">
<p>The core job of Argo CD is <strong>reconciliation</strong>: it renders manifests (plain YAML, Kustomize, Helm, etc.), compares them with the live state of a cluster and syncs differences. Everything else (ApplicationSets, sync waves, hooks, RBAC via AppProjects, the UI) is built around that job.</p>
</div>
<div class="admonitionblock tip">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-tip" title="Tip"></i>
</td>
<td class="content">
If you want to dig into the details of the GitOps process, <a href="/gitopscollection/2023-12-11-gitops-intro/">Ep.1</a> is a good introduction to the topic.
</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>What OpenShift GitOps does <strong>not</strong> do:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>It does not create, upgrade, or destroy clusters. A cluster has to exist, and someone has to register it in Argo CD.</p>
</li>
<li>
<p>It does not report <em>compliance</em>. It tells you that an (Argo CD) Application is <strong>Synced</strong> and <strong>Healthy</strong>. It does not tell you that three out of 40 clusters violate your NIST 800-53 baseline.</p>
</li>
<li>
<p>It has no fleet-wide view of anything that is not an Argo CD Application: no search across clusters, no metrics, no inventory.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>That is perfectly fine. Argo CD was never meant to do any of these things.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_what_advanced_cluster_management_is">What Advanced Cluster Management is</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Red Hat Advanced Cluster Management for Kubernetes is a hub-and-spoke platform. You install it on a <strong>hub</strong> cluster and import or create <strong>managed clusters</strong>. It is a separate subscription, or part of <strong>OpenShift Platform Plus</strong>. At the time of writing, the current version is 2.17. The open source equivalent is called <a href="https://open-cluster-management.io" rel="noopener" target="_blank">Open Cluster Management</a>.</p>
</div>
<div class="paragraph">
<p>ACM is built around five pillars:</p>
</div>
<table class="tableblock frame-all grid-all stretch">
<colgroup>
<col style="width: 25%;"/>
<col style="width: 75%;"/>
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top">Pillar</th>
<th class="tableblock halign-left valign-top">What it does</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Cluster lifecycle</strong> (multicluster engine)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Create, import, upgrade, and destroy clusters: Installer Provided Infrastructure (IPI) on cloud providers, Hosted Control Planes, Assisted/Agent-based installs on bare metal, ROSA/ARO, import clusters and so on.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Governance</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Policies that check (inform) or enforce configuration on managed clusters, with compliance status reported back to the hub. Integrates with Gatekeeper, the Compliance Operator and certificate checks.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Application lifecycle</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Deploy applications to many clusters. It uses Argo CD in the background for this task.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Observability</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Centralized metrics and Grafana dashboards across the fleet.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Search and fleet views</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Search any resource across all managed clusters, including a dedicated view for virtual machines from OpenShift Virtualization.</p></td>
</tr>
</tbody>
</table>
<div class="paragraph">
<p>The building blocks you will meet everywhere in ACM are:</p>
</div>
<div class="ulist">
<ul>
<li>
<p><strong>ManagedCluster</strong> – the cluster that is managed, the representation of a spoke on the hub, carrying labels (for example <code>environment=production</code>, <code>region=eu-central</code>).</p>
</li>
<li>
<p><strong>ManagedClusterSet</strong> – a group of clusters, used for RBAC and scoping.</p>
</li>
<li>
<p><strong>ManagedClusterSetBinding</strong> – makes a cluster set usable from a namespace on the hub.</p>
</li>
<li>
<p><strong>Placement</strong> – a dynamic selection of clusters based on labels, etc. It defines the scope of a policy or application.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p><strong>Below is a more detailed description of the different objects</strong></p>
</div>
<div class="paragraph">
<p>The Placement is the important concept for this series. Argo CD selects clusters by looking at labels on cluster <em>secrets</em> that someone created. ACM selects clusters by looking at the actual fleet, including whether a cluster is available right now.</p>
</div>
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
ACM also has its own, older application model based on <strong>Channels</strong> and <strong>Subscriptions</strong> objects. That model is deprecated and will be removed in a future release.
</td>
</tr>
</tbody></table>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_side_by_side_comparison">Side-by-side comparison</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Let’s do a side-by-side comparison of the two tools:</p>
</div>
<table class="tableblock frame-all grid-all stretch">
<colgroup>
<col style="width: 25%;"/>
<col style="width: 37.5%;"/>
<col style="width: 37.5%;"/>
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top">Topic</th>
<th class="tableblock halign-left valign-top">OpenShift GitOps</th>
<th class="tableblock halign-left valign-top">Advanced Cluster Management</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Primary purpose</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Continuous delivery from Git</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Fleet management: lifecycle, governance, visibility</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Scope</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">One or more clusters, registered manually</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Hundreds or thousands of managed clusters</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Cluster creation and upgrades</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">No (cluster upgrades only via a ClusterVersion manifest, see <a href="/gitopscollection/2024-06-07-update-cluster-version-with-gitops/">Ep.10</a>)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Yes (including upgrades with pre/post hooks)</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Source of truth</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Git (or Helm/OCI repository)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">The hub cluster. The hub objects themselves can (and should) come from Git, so maybe it is still Git after all.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Configuration management</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Applications, ApplicationSets</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Policies (ConfigurationPolicy, OperatorPolicy, …​)</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>&#34;Inform only&#34; / audit mode</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Partly, shows OutOfSync of managed resources</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Yes (<code>remediationAction: inform</code>)</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Compliance reporting</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">No</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Yes, per policy, per cluster, with history</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Per-cluster templating</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">ApplicationSet generators, Helm values</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Hub templates and managed-cluster templates in policies</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Behaviour when the hub is unreachable</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Push model: nothing is reconciled. Agent/pull model: local reconciliation continues</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Policies keep being evaluated and enforced on the managed cluster</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Licensing</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Included in OpenShift. Clusters running the Argo CD Agent need OpenShift Platform Plus</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Separate subscription or OpenShift Platform Plus</p></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="sect1">
<h2 id="_who_does_what_in_a_combined_setup">Who does what in a combined setup</h2>
<div class="sectionbody">
<div class="paragraph">
<p>When both tools are in place, the question is no longer &#34;which one&#34;, but &#34;which one for which task&#34;. This is the split I would like to recommend:</p>
</div>
<table class="tableblock frame-all grid-all stretch">
<colgroup>
<col style="width: 25%;"/>
<col style="width: 37.5%;"/>
<col style="width: 37.5%;"/>
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top">Area</th>
<th class="tableblock halign-left valign-top">ACM</th>
<th class="tableblock halign-left valign-top">Argo CD</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Cluster lifecycle</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Create, import, upgrade, hibernate</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">–</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Fleet inventory and selection</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">ManagedCluster labels, ManagedClusterSets, Placements</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Consumes them (GitOpsCluster, cluster decision generator)</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Governance / Compliance</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Policies (inform/enforce), compliance history</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Deploys the policies from Git to the hub. Can show health status.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Cluster configuration</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Possible with ConfigurationPolicies</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Preferred</strong>: richer diff, sync waves, health</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Application delivery</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Placement of Applications, pull transport</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">The delivery engine (push, pull, agent)</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Observability</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Multicluster observability, search</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Sync and health status only</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Bootstrapping Argo CD on new clusters</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Policy or GitOps add-on installs it everywhere</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">–</p></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="sect1">
<h2 id="_where_they_overlap_cluster_configuration">Where they overlap: cluster configuration</h2>
<div class="sectionbody">
<div class="paragraph">
<p>While application deployment is clearly the responsibility of Argo CD, creating new clusters or upgrading existing ones is clearly ACM territory.
The grey zone is <strong>cluster configuration</strong>: Operators, OAuth, RBAC, network policies, the compliance baseline. Both tools can do it, but you should choose what fits best.</p>
</div>
<div class="paragraph">
<p>To make the difference visible, let’s implement the same requirement twice:</p>
</div>
<div class="paragraph">
<p><em>&#34;Every production cluster must run the Compliance Operator from the stable channel.&#34;</em></p>
</div>
<div class="sect2">
<h3 id="_option_1_argo_cd_with_an_applicationset">Option 1: Argo CD with an ApplicationSet</h3>
<div class="paragraph">
<p>Without ACM, Argo CD needs to know the target clusters. That means a cluster secret per cluster, carrying the labels we want to select on and the token to access the cluster:</p>
</div>
<div class="listingblock linenums">
<div class="title">Cluster secret for a production cluster</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: v1
kind: Secret
metadata:
  name: cluster-prod-eu-01
  namespace: openshift-gitops
  labels:
    argocd.argoproj.io/secret-type: cluster <i class="conum" data-value="1"></i><b>(1)</b>
    environment: production <i class="conum" data-value="2"></i><b>(2)</b>
type: Opaque
stringData:
  name: prod-eu-01
  server: https://api.prod-eu-01.example.com:6443
  config: | <i class="conum" data-value="3"></i><b>(3)</b>
    {
      &#34;bearerToken&#34;: &#34;&lt;token-of-a-service-account-on-the-spoke&gt;&#34;,
      &#34;tlsClientConfig&#34;: {
        &#34;insecure&#34;: false,
        &#34;caData&#34;: &#34;&lt;base64-encoded-ca&gt;&#34;
      }
    }</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Marks this secret as a cluster definition for Argo CD.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Our own label. The ApplicationSet uses it for the selection.</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>Access configuration for the cluster.</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>The ApplicationSet then uses the <strong>cluster generator</strong> to create one Application per matching cluster:</p>
</div>
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
Other generators are available too, but for simplicity only the cluster generator is used here.
</td>
</tr>
</tbody></table>
</div>
<div class="listingblock linenums">
<div class="title">ApplicationSet with cluster generator</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: compliance-operator
  namespace: openshift-gitops
spec:
  goTemplate: true
  goTemplateOptions: [&#34;missingkey=error&#34;]
  generators:
    - clusters:
        selector:
          matchLabels:
            environment: production <i class="conum" data-value="1"></i><b>(1)</b>
  template:
    metadata:
      name: &#39;compliance-operator-{{ .name }}&#39;
    spec:
      project: default
      source:
        repoURL: https://github.com/tjungbauer/openshift-clusterconfig-gitops.git
        targetRevision: main
        path: clusters/all/setup-compliance-operator
      destination:
        server: &#39;{{ .server }}&#39; <i class="conum" data-value="2"></i><b>(2)</b>
      syncPolicy:
        automated:
          prune: true
          selfHeal: true</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Selects all cluster secrets with this label.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>Argo CD on the hub pushes the manifests directly to the API of each spoke.</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>This works well and it is exactly what I do in my own demo environment. The catch is in the details:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>The token in the cluster secret must be created, rotated and stored securely. For 5 clusters that is fine, for 200 clusters it is a project of its own.</p>
</li>
<li>
<p>The hub needs network access to the API of every spoke.</p>
</li>
<li>
<p>Whoever controls this Argo CD instance effectively has the permissions of every token it holds.</p>
</li>
<li>
<p>The result is &#34;Synced/Healthy&#34;. There is no compliance report you can hand to an auditor.</p>
</li>
</ul>
</div>
</div>
<div class="sect2">
<h3 id="_option_2_acm_with_an_operatorpolicy">Option 2: ACM with an OperatorPolicy</h3>
<div class="paragraph">
<p>With ACM, the same requirement is a policy. A <strong>Placement</strong> selects the clusters, a <strong>PlacementBinding</strong> connects the policy with the Placement:</p>
</div>
<div class="listingblock linenums">
<div class="title">Policy, Placement and PlacementBinding</div>
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: policy.open-cluster-management.io/v1
kind: Policy
metadata:
  name: compliance-operator
  namespace: acm-policies
  annotations:
    policy.open-cluster-management.io/standards: NIST SP 800-53
    policy.open-cluster-management.io/categories: CM Configuration Management
    policy.open-cluster-management.io/controls: CM-2 Baseline Configuration
spec:
  disabled: false
  remediationAction: enforce <i class="conum" data-value="1"></i><b>(1)</b>
  policy-templates:
    - objectDefinition:
        apiVersion: policy.open-cluster-management.io/v1
        kind: ConfigurationPolicy <i class="conum" data-value="2"></i><b>(2)</b>
        metadata:
          name: compliance-operator-namespace
        spec:
          severity: high
          object-templates:
            - complianceType: musthave
              objectDefinition:
                apiVersion: v1
                kind: Namespace
                metadata:
                  name: openshift-compliance
    - objectDefinition:
        apiVersion: policy.open-cluster-management.io/v1beta1
        kind: OperatorPolicy <i class="conum" data-value="3"></i><b>(3)</b>
        metadata:
          name: compliance-operator
        spec:
          severity: high
          complianceType: musthave
          subscription:
            name: compliance-operator
            namespace: openshift-compliance
            channel: stable
            source: redhat-operators
            sourceNamespace: openshift-marketplace
          upgradeApproval: Automatic <i class="conum" data-value="4"></i><b>(4)</b>
---
apiVersion: cluster.open-cluster-management.io/v1beta1
kind: Placement
metadata:
  name: production-clusters
  namespace: acm-policies
spec:
  clusterSets:
    - production <i class="conum" data-value="5"></i><b>(5)</b>
  predicates:
    - requiredClusterSelector:
        labelSelector:
          matchLabels:
            environment: production
---
apiVersion: policy.open-cluster-management.io/v1
kind: PlacementBinding <i class="conum" data-value="6"></i><b>(6)</b>
metadata:
  name: compliance-operator
  namespace: acm-policies
placementRef:
  name: production-clusters
  apiGroup: cluster.open-cluster-management.io
  kind: Placement
subjects:
  - name: compliance-operator
    apiGroup: policy.open-cluster-management.io
    kind: Policy</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>Set this to <code>inform</code> and ACM only reports whether the clusters comply, without changing anything.</td>
</tr>
<tr>
<td><i class="conum" data-value="2"></i><b>2</b></td>
<td>A ConfigurationPolicy ensures that an object exists (<code>musthave</code>), matches exactly (<code>mustonlyhave</code>) or does not exist (<code>mustnothave</code>).</td>
</tr>
<tr>
<td><i class="conum" data-value="3"></i><b>3</b></td>
<td>The OperatorPolicy handles the OLM details: OperatorGroup, Subscription, install plan approval and the health of the CSV.</td>
</tr>
<tr>
<td><i class="conum" data-value="4"></i><b>4</b></td>
<td>Controls whether new versions are approved automatically. With <code>None</code>, ACM does not approve upgrades. With <code>spec.complianceConfig</code>, the policy can additionally report available upgrades, since ACM 2.17 even minor version upgrades on other channels.</td>
</tr>
<tr>
<td><i class="conum" data-value="5"></i><b>5</b></td>
<td>The Placement can only see clusters of cluster sets that are bound to the namespace <code>acm-policies</code> via a ManagedClusterSetBinding (see below).</td>
</tr>
<tr>
<td><i class="conum" data-value="6"></i><b>6</b></td>
<td>The PlacementBinding connects the policy with the Placement.</td>
</tr>
</tbody></table>
</div>
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
One important difference to Argo CD I maybe should have mentioned earlier: Argo CD can manage objects it knows. It has no concept of &#34;delete an object completely that cannot compare with Git&#34;. For example, delete the secret &#34;kubeadmin&#34;. ACM is able to do that, you can create a policy that ensures the secret is deleted.
</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>The <code>ManagedClusterSetBinding</code> mentioned in callout 5 looks like the following. It binds the cluster set <code>production</code> to the namespace <code>acm-policies</code>.</p>
</div>
<div class="listingblock linenums">
<div class="content">
<pre class="highlightjs highlight"><code class="language-yaml hljs" data-lang="yaml">apiVersion: cluster.open-cluster-management.io/v1beta2
kind: ManagedClusterSetBinding <i class="conum" data-value="1"></i><b>(1)</b>
metadata:
  name: production
  namespace: acm-policies
spec:
  clusterSet: production</code></pre>
</div>
</div>
<div class="colist arabic">
<table>
<tbody><tr>
<td><i class="conum" data-value="1"></i><b>1</b></td>
<td>The ManagedClusterSetBinding binds the cluster set <code>production</code> to the namespace <code>acm-policies</code>.</td>
</tr>
</tbody></table>
</div>
</div>
<div class="sect2">
<h3 id="_object_model_of_acm_together_with_argo_cd">Object model of ACM together with Argo CD</h3>
<div class="paragraph">
<p>Even after working with ACM for a while, I still find it difficult to understand the object model or to explain it to others. Therefore, I tried to create a visual representation of the objects and their relationships here, to help you to understand and to remind myself when I forget. :)</p>
</div>
<div class="paragraph">
<p>How do these objects play together? The easiest way to understand it is to follow a few clusters through the chain. In the following example, four clusters are imported into ACM. Each step narrows the selection down:</p>
</div>
<div class="olist arabic">
<ol class="arabic">
<li>
<p>Every imported cluster is a <strong>ManagedCluster</strong> on the hub. The labels are what everything else selects on.</p>
</li>
<li>
<p>The <strong>ManagedClusterSet</strong> <code>production</code> groups clusters. <code>dev-eu-01</code> belongs to another cluster set and is ignored.</p>
</li>
<li>
<p>The <strong>ManagedClusterSetBinding</strong> makes the cluster set usable in a namespace, here <code>openshift-gitops</code>. Without this binding, a Placement in that namespace does not find a single cluster, which is the most common reason for an empty selection.</p>
</li>
<li>
<p>The <strong>Placement</strong> selects from the bound cluster sets only. It asks for <code>environment=prod</code>, so <code>prod-eu-02</code> (a test cluster) is ignored as well.</p>
</li>
<li>
<p>The <strong>PlacementDecision</strong> is the result: <code>prod-eu-01</code> and <code>prod-us-01</code>. ACM keeps it up to date when clusters join, leave or change their labels.</p>
</li>
</ol>
</div>
<div class="imageblock">
<div class="content">
<img src="/gitopscollection/images/ep-18/ep18-acm-placement-selection.png?width=1000px" alt="Four example clusters are narrowed down step by step: the cluster set drops dev-eu-01" width="the Placement drops prod-eu-02" height="the PlacementDecision lists prod-eu-01 and prod-us-01"/>
</div>
<div class="title">Figure 1. How ACM selects clusters: from ManagedCluster to PlacementDecision</div>
</div>
<div class="paragraph">
<p>The <code>PlacementDecision</code> itself does not deploy anything. It is just a list and the point where it is handed over to the consumers (Argo CD). Other objects in the same namespace read it and act on it:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>The <strong>GitOpsCluster</strong> registers the selected managed clusters automatically in Argo CD by creating a cluster secret for each of them. ACM rotates the tokens automatically, therefore you do not need to take care of that.</p>
</li>
<li>
<p>An <strong>ApplicationSet</strong> creates one Application per selected cluster and uses these cluster secrets as destinations.</p>
</li>
<li>
<p>For governance, a <strong>PlacementBinding</strong> connects a Policy with a Placement. ACM then puts a copy of the Policy into the hub namespace of each selected cluster, where it gets picked up.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>Clusters that are not part of the decision receive nothing: no Application, no Policy.</p>
</div>
<div class="imageblock">
<div class="content">
<img src="/gitopscollection/images/ep-18/ep18-acm-placement-consumers.png?width=1000px" alt="GitOpsCluster creates Argo CD cluster secrets" width="the ApplicationSet creates one Application per selected cluster" height="the PlacementBinding propagates the Policy; only the selected clusters receive anything"/>
</div>
<div class="title">Figure 2. What uses the PlacementDecision</div>
</div>
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
Consumers can only reference a Placement in their own namespace. Policies usually live in a different namespace (for example <code>acm-policies</code>), so they need their own ManagedClusterSetBinding and Placement there, typically with the same selection.
</td>
</tr>
</tbody></table>
</div>
<hr/>
</div>
<div class="sect2">
<h3 id="_what_is_the_advantage_now">What is the advantage now?</h3>
<div class="paragraph">
<p>So after all these different objects and relationships, which look rather complex at the beginning, what is the advantage of ACM?</p>
</div>
<div class="ulist">
<ul>
<li>
<p>No extra credentials per cluster: the managed clusters are already registered with the hub. The policy is pulled by the policy addon on the spoke and evaluated locally. ACM takes care of the tokens automatically.</p>
</li>
<li>
<p>The hub does not need to reach the API of the spoke.</p>
</li>
<li>
<p>If the hub is unreachable, the spoke keeps enforcing the last known policy because it is stored locally. Good for edge clusters with unstable connectivity.</p>
</li>
<li>
<p>A compliance view: <em>&#34;compliance-operator: 37 compliant, 3 non-compliant&#34;</em>, with the reason per cluster and a history.</p>
</li>
</ul>
</div>
</div>
<div class="sect2">
<h3 id="_the_rule_of_thumb">The rule of thumb</h3>
<div class="admonitionblock tip">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-tip" title="Tip"></i>
</td>
<td class="content">
<div class="ulist">
<ul>
<li>
<p><strong>Applications and everything owned by application teams</strong>: → Argo CD.</p>
</li>
<li>
<p><strong>Baseline configuration that must be audited</strong> (security settings, mandatory Operators, certificates, &#34;this must never exist&#34;): → ACM Policies.</p>
</li>
<li>
<p><strong>Platform configuration that is complex to render</strong> (a full logging stack, a service mesh control plane): → Argo CD, optionally wrapped by an ACM Policy that only <em>checks</em> (<code>inform</code>) the result.</p>
</li>
</ul>
</div>
</td>
</tr>
</tbody></table>
</div>
</div>
<div class="sect2">
<h3 id="_the_anti_pattern_two_controllers_one_object">The anti-pattern: two controllers, one object</h3>
<div class="paragraph">
<p>One thing I have seen more than once: Argo CD manages an object with <code>selfHeal: true</code>, and an ACM policy <code>enforces</code> the same object with a slightly different value. Each controller &#34;fixes&#34; the drift caused by the other one. The object flips back and forth.</p>
</div>
<div class="admonitionblock warning">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-warning" title="Warning"></i>
</td>
<td class="content">
Every object must have exactly <strong>one</strong> owner. If an ACM Policy and Argo CD should both care about an object, let Argo CD own it and use the policy of ACM in <code>inform</code> mode only.
</td>
</tr>
</tbody></table>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_the_bootstrap_question_who_installs_argo_cd">The bootstrap question: who installs Argo CD?</h2>
<div class="sectionbody">
<div class="paragraph">
<p>GitOps needs one first imperative step: somebody has to install the OpenShift GitOps Operator and apply the first (root) Application. Everything after that comes from Git. This is also a question where ACM and Argo CD meet:</p>
</div>
<table class="tableblock frame-all grid-all stretch">
<colgroup>
<col style="width: 25%;"/>
<col style="width: 37.5%;"/>
<col style="width: 37.5%;"/>
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top">Approach</th>
<th class="tableblock halign-left valign-top">Good for</th>
<th class="tableblock halign-left valign-top">Watch out</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Script or <code>oc apply</code></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">PoC, a handful of clusters, workshops</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">A manual step per cluster. Version and document the script.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Automation (Ansible, Terraform, …​)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Customers with Automation environment</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">A second tool to maintain. Keep it to the bootstrap only.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">Install-time manifests</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Repeatable on-premises installations (agent-based installer)</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Must tolerate CRD timing until the operator is up.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">ACM Policy</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Fleets: every imported cluster is bootstrapped automatically</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Policy and Argo CD must not fight over the same objects.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock">ACM GitOpsCluster</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Hub Argo CD (push) or GitOps add-on (pull/agent) targets the cluster</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">The hub becomes part of the critical path.</p></td>
</tr>
</tbody>
</table>
<div class="admonitionblock tip">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-tip" title="Tip"></i>
</td>
<td class="content">
My recommendation: fewer than five clusters, use a script or Ansible. For a fleet that is managed by ACM, use a policy or the GitOpsCluster. Never: &#34;we click it in the console once&#34;. The bootstrap policy is shown in <a href="/gitopscollection/2026-10-24-acm-policies-with-gitops/">Ep.20</a>.
</td>
</tr>
</tbody></table>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_working_together_acm_as_the_brain_for_argo_cd">Working together: ACM as the brain for Argo CD</h2>
<div class="sectionbody">
<div class="paragraph">
<p>As mentioned above, ACM can register its managed clusters in OpenShift GitOps automatically. You no longer maintain cluster secrets by hand: ACM creates them, based on a Placement, rotates the credentials automatically and removes the secrets when a cluster is removed from the Placement.</p>
</div>
<div class="paragraph">
<p>The bridge between the two worlds is the <strong>GitOpsCluster</strong> custom resource. On top of it, ACM offers three options to get an Argo CD Application onto a managed cluster:</p>
</div>
<table class="tableblock frame-all grid-all stretch">
<colgroup>
<col style="width: 14.2857%;"/>
<col style="width: 42.8571%;"/>
<col style="width: 42.8572%;"/>
</colgroup>
<thead>
<tr>
<th class="tableblock halign-left valign-top">Model</th>
<th class="tableblock halign-left valign-top">How it works</th>
<th class="tableblock halign-left valign-top">Use it when</th>
</tr>
</thead>
<tbody>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Push</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Argo CD on the hub pushes manifests to the spoke API, like option 1 above. ACM creates and rotates the cluster secrets.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">A moderate number of clusters, good connectivity from hub to spokes, and you want <strong>one UI with full visibility</strong>.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Pull</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Applications are created on the hub but not reconciled there. Instead the target cluster pulls it and a local Argo CD instance on the target cluster reconciles it.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">Many clusters, outbound-only networks, or the hub must not hold credentials for the spokes.</p></td>
</tr>
<tr>
<td class="tableblock halign-left valign-top"><p class="tableblock"><strong>Argo CD Agent</strong></p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">ACM deploys the Argo CD Agent to the spokes via the GitOps add-on. Applications are defined on the hub, the agent reconciles locally and synchronises the full status back.</p></td>
<td class="tableblock halign-left valign-top"><p class="tableblock">The direction for large fleets and edge sites.</p></td>
</tr>
</tbody>
</table>
<div class="paragraph">
<p>The following diagram shows how the three models fit together.</p>
</div>
<div class="imageblock">
<div class="content">
<img src="/gitopscollection/images/ep-18/ep18-delivery-models.png?width=1000px" alt="Push reconciles on the hub" width="pull and the Argo CD Agent reconcile on the managed cluster"/>
</div>
<div class="title">Figure 3. Three ways an Argo CD Application reaches a managed cluster</div>
</div>
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
Yes, you read that right, the version will jump from 2.17 to 5.0 for ACM in the next release, to harmonize with OpenShift versioning.
</td>
</tr>
</tbody></table>
</div>
<div class="paragraph">
<p>What each model actually is, how the data flows in which direction and how to set it up step by step is the topic of the next part: <a href="/gitopscollection/2026-10-23-acm-gitops-push-pull-agent/">[Ep.19] ACM and OpenShift GitOps: push, pull and the Argo CD Agent</a>.</p>
</div>
<hr/>
</div>
</div>
<div class="sect1">
<h2 id="_decision_guide">Decision guide</h2>
<div class="sectionbody">
<div class="paragraph">
<p>The following are the scenarios I saw in real projects and what I personally would recommend. Verify if these fit your situation.</p>
</div>
<div class="paragraph">
<p>A quick summary of the scenarios is shown in the following slide I was using for a workshop (more details below):</p>
</div>
<div class="imageblock">
<div class="content">
<img src="/gitopscollection/images/ep-18/ep18-decision-guide.png?width=1000px" alt="Three questions lead to the scenarios A to E"/>
</div>
<div class="title">Figure 4. Decision guide: OpenShift GitOps alone or together with ACM</div>
</div>
<div class="sect2">
<h3 id="_scenario_a_one_to_three_clusters_one_platform_team">Scenario A: one to three clusters, one platform team</h3>
<div class="paragraph">
<p><em>Example: a single production cluster plus a test cluster.</em></p>
</div>
<div class="paragraph">
<p><strong>OpenShift GitOps only.</strong> ACM would add a hub, an additional component to operate and additional cost, without solving a real problem. Configure the clusters with Argo CD as described in the earlier episodes of this series, with either one Argo CD instance per cluster or a central Argo CD on a management cluster (recommended). If you need compliance reports, the Compliance Operator gives you scan results per cluster without ACM.</p>
</div>
</div>
<div class="sect2">
<h3 id="_scenario_b_3_to_20_clusters_all_reachable_created_by_another_team_or_process">Scenario B: 3 to 20 clusters, all reachable, created by another team or process</h3>
<div class="paragraph">
<p><em>Example: dev, test, prod in two data centres, installed by the infrastructure team.</em></p>
</div>
<div class="paragraph">
<p><strong>OpenShift GitOps is usually sufficient.</strong> A central Argo CD with an ApplicationSet cluster generator, or decentralised Argo CD instances per cluster. Start thinking about ACM when one of these questions comes up:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>&#34;Who rotates the cluster tokens in the Argo CD secrets?&#34;</p>
</li>
<li>
<p>&#34;Can you show us which clusters are not compliant?&#34;</p>
</li>
<li>
<p>&#34;Can we have one search, one observability view across all clusters?&#34;</p>
</li>
</ul>
</div>
</div>
<div class="sect2">
<h3 id="_scenario_c_20_to_several_hundred_clusters">Scenario C: 20 to several hundred clusters</h3>
<div class="paragraph">
<p><em>Example: clusters per business unit, factory or per tenant, created on demand.</em></p>
</div>
<div class="paragraph">
<p><strong>ACM plus OpenShift GitOps. Push for smaller fleets, pull or agent for larger fleets.</strong> ACM creates the clusters, labels them and adds them to cluster sets. The GitOpsCluster registers them in Argo CD automatically. Applications and complex platform components come via ApplicationSets driven by ACM Placements. A new cluster receives its full configuration without anybody touching Argo CD. Around 20 clusters is also where a single push hub usually needs sharding, and where the &#34;hub holds all the credentials&#34; discussion with the security team starts. <strong>The pull model or the Argo CD Agent become serious options here.</strong></p>
</div>
</div>
<div class="sect2">
<h3 id="_scenario_d_regulated_environment">Scenario D: regulated environment</h3>
<div class="paragraph">
<p><em>Example: banking or public sector, regular audits, a defined security baseline.</em></p>
</div>
<div class="paragraph">
<p><strong>ACM Policies for the baseline, Argo CD for the rest.</strong> The baseline lives as policies in Git, deployed by Argo CD to the hub, enforced and reported by ACM. Start in <code>inform</code> mode on existing clusters, check the compliance dashboard, then switch to <code>enforce</code>.</p>
</div>
</div>
<div class="sect2">
<h3 id="_scenario_e_edge_and_disconnected_sites">Scenario E: edge and disconnected sites</h3>
<div class="paragraph">
<p><em>Example: hundreds of small clusters (SNO, compact clusters) in stores, factories or vehicles with unstable connectivity.</em></p>
</div>
<div class="paragraph">
<p><strong>ACM plus OpenShift GitOps in pull or agent mode.</strong> The push model fails here: the hub cannot reliably reach the spokes. Policies are evaluated locally and continue to work when the connection is down. Applications are reconciled by a local Argo CD or the Argo CD Agent. Two things to keep in mind: every spoke still needs to reach its sources (Git or an OCI registry, often a local mirror), and every cluster that runs the Argo CD Agent needs an OpenShift Platform Plus subscription.</p>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_frequently_asked_questions">Frequently asked questions</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Just some questions I heard from customers during the last years:</p>
</div>
<div class="paragraph">
<p><strong>Can ACM replace Argo CD?</strong>
For applications, no. ACM’s application lifecycle is built on OpenShift GitOps. The older Channel and Subscription model still exists, but it is deprecated. I would not start a new project with Channels and Subscriptions.</p>
</div>
<div class="paragraph">
<p><strong>Can Argo CD replace ACM?</strong>
For delivery to existing, reachable clusters, yes. For cluster lifecycle, compliance reporting, fleet search and observability, no.</p>
</div>
<div class="paragraph">
<p><strong>Do we need ACM to do multi-cluster GitOps?</strong>
No. Argo CD handles multiple clusters on its own, and with the Argo CD Agent even in a hub-and-spoke model with local reconciliation (see <a href="/gitopscollection/2026-01-14-argocd-agent/">Ep.15</a>). ACM makes it easier to operate at scale, because the cluster registration, the token rotation and the agent installation are automated.</p>
</div>
<div class="paragraph">
<p><strong>Should the ACM hub itself be managed by Argo CD?</strong>
Yes. The MultiClusterHub, the policies, Placements, cluster sets, GitOpsCluster resources: all of it belongs in Git. The hub is just another cluster.</p>
</div>
<div class="paragraph">
<p><strong>Should I configure the cluster with Argo CD or with policies?</strong>
See the rule of thumb above. And whatever you choose always keep in mind: one owner per object.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_summary">Summary</h2>
<div class="sectionbody">
<div class="paragraph">
<p>So, which one is better? Neither. They are not competitors, they are layers.</p>
</div>
<div class="ulist">
<ul>
<li>
<p><strong>OpenShift GitOps</strong> is the delivery engine. It makes sure that what is in Git is running on a cluster. It is included in OpenShift and should be your default for applications and complex platform components, no matter how many clusters you have.</p>
</li>
<li>
<p><strong>Advanced Cluster Management</strong> is the fleet layer on top. It knows which clusters exist, creates and upgrades them, enforces and reports compliance, and feeds Argo CD with the list of clusters via Placements.</p>
</li>
<li>
<p>The real decision is not &#34;GitOps or ACM&#34;, but &#34;do I need fleet management?&#34; For a handful of clusters, usually not. For many clusters, dynamic cluster creation, regulated environments or edge sites, almost certainly yes.</p>
</li>
<li>
<p>Where both tools can do the same job, cluster configuration, use policies for the audited baseline and Argo CD for everything that needs rendering, and keep exactly one owner per object.</p>
</li>
</ul>
</div>
<div class="paragraph">
<p>In the <a href="/gitopscollection/2026-10-23-acm-gitops-push-pull-agent/">next part</a>, we look under the hood of the integration: push, pull and the Argo CD Agent.</p>
</div>
<div class="paragraph">
<p>And as always: <strong>If it is not in Git, it does not exist.</strong></p>
</div>
<div class="admonitionblock note">
<table>
<tbody><tr>
<td class="icon">
<i class="fa icon-note" title="Note"></i>
</td>
<td class="content">
The diagrams in this article have been manually drawn on a whiteboard. AI then generated the SVGs from photos and made them look equal in all articles.
</td>
</tr>
</tbody></table>
</div>
</div>
</div>
<div class="sect1">
<h2 id="_references">References</h2>
<div class="sectionbody">
<div class="ulist">
<ul>
<li>
<p><a href="https://docs.redhat.com/en/documentation/red_hat_openshift_gitops/1.22/html/release_notes/gitops-release-notes" rel="noopener" target="_blank">Red Hat OpenShift GitOps 1.22 release notes</a></p>
</li>
<li>
<p><a href="https://docs.redhat.com/en/documentation/red_hat_advanced_cluster_management_for_kubernetes/2.17" rel="noopener" target="_blank">Red Hat Advanced Cluster Management for Kubernetes 2.17 documentation</a></p>
</li>
<li>
<p><a href="https://docs.redhat.com/en/documentation/red_hat_advanced_cluster_management_for_kubernetes/2.17/html/gitops/gitops-overview" rel="noopener" target="_blank">ACM 2.17: GitOps overview</a></p>
</li>
<li>
<p><a href="https://www.redhat.com/en/blog/manage-clusters-and-applications-scale-argo-cd-agent-red-hat-openshift-gitops" rel="noopener" target="_blank">Red Hat blog: Manage clusters and applications at scale with the Argo CD Agent</a></p>
</li>
<li>
<p><a href="https://opengitops.dev/" rel="noopener" target="_blank">OpenGitOps principles</a></p>
</li>
</ul>
</div>
</div>
</div>]]></content:encoded></item></channel></rss>