Repository: argoproj-labs/argocd-operator
Pull request #893 (2023-04-19 20:48:02)

docs: Add documentation to describe alternate method to deploy app resources to non-control plane namespaces
**What type of PR is this?**
> Uncomment only one ` /kind` line, and delete the rest.
> For example, `> /kind bug` would simply become: `/kind bug`

> /kind bug
> /kind chore
> /kind cleanup
> /kind failing-test
> /kind enhancement
 /kind documentation
> /kind code-refactoring


**What does this PR do / why we need it**:
Sometimes users needing to deploy app resources to namespaces different from the one Argo CD instance is installed in, don't have the permissions to apply the "manged-by" label to that namespace. Argo CD provides an alternate mechanism that allows users to create labels with pre-configured labels at app sync time. This PR extends existing documentation on deploying resources to other namespaces by describing steps for this alternate method  


**Have you updated the necessary documentation?**

* [x] Documentation update is required by this PR.
* [x] Documentation has been updated.

**Which issue(s) this PR fixes**:

Fixes #?

**How to test changes / Special notes to the reviewer**:



Patch:
diff --git a/controllers/argocd/hpa.go b/controllers/argocd/hpa.go
index b289ca5e..0cee4a3c 100644
--- a/controllers/argocd/hpa.go
+++ b/controllers/argocd/hpa.go
@@ -16,6 +16,7 @@ package argocd
 
 import (
 	"context"
+	"reflect"
 
 	autoscaling "k8s.io/api/autoscaling/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -25,6 +26,12 @@ import (
 	"github.com/argoproj-labs/argocd-operator/controllers/argoutil"
 )
 
+var (
+	maxReplicas int32 = 3
+	minReplicas int32 = 1
+	tcup        int32 = 50
+)
+
 func newHorizontalPodAutoscaler(cr *argoprojv1a1.ArgoCD) *autoscaling.HorizontalPodAutoscaler {
 	return &autoscaling.HorizontalPodAutoscaler{
 		ObjectMeta: metav1.ObjectMeta{
@@ -50,39 +57,54 @@ func newHorizontalPodAutoscalerWithSuffix(suffix string, cr *argoprojv1a1.ArgoCD
 	return newHorizontalPodAutoscalerWithName(nameWithSuffix(suffix, cr), cr)
 }
 
-// reconcileServerHPA will ensure that the HorizontalPodAutoscaler is present for the Argo CD Server component.
+// reconcileServerHPA will ensure that the HorizontalPodAutoscaler is present for the Argo CD Server component, and reconcile any detected changes.
 func (r *ReconcileArgoCD) reconcileServerHPA(cr *argoprojv1a1.ArgoCD) error {
-	hpa := newHorizontalPodAutoscalerWithSuffix("server", cr)
-	if argoutil.IsObjectFound(r.Client, cr.Namespace, hpa.Name, hpa) {
+
+	defaultHPA := newHorizontalPodAutoscalerWithSuffix("server", cr)
+	defaultHPA.Spec = autoscaling.HorizontalPodAutoscalerSpec{
+		MaxReplicas:                    maxReplicas,
+		MinReplicas:                    &minReplicas,
+		TargetCPUUtilizationPercentage: &tcup,
+		ScaleTargetRef: autoscaling.CrossVersionObjectReference{
+			APIVersion: "apps/v1",
+			Kind:       "Deployment",
+			Name:       nameWithSuffix("server", cr),
+		},
+	}
+
+	existingHPA := newHorizontalPodAutoscalerWithSuffix("server", cr)
+	if argoutil.IsObjectFound(r.Client, cr.Namespace, existingHPA.Name, existingHPA) {
 		if !cr.Spec.Server.Autoscale.Enabled {
-			return r.Client.Delete(context.TODO(), hpa) // HorizontalPodAutoscaler found but globally disabled, delete it.
+			return r.Client.Delete(context.TODO(), existingHPA) // HorizontalPodAutoscaler found but globally disabled, delete it.
+		}
+
+		changed := false
+		// HorizontalPodAutoscaler found, reconcile if necessary changes detected
+		if cr.Spec.Server.Autoscale.HPA != nil {
+			if !reflect.DeepEqual(existingHPA.Spec, cr.Spec.Server.Autoscale.HPA) {
+				existingHPA.Spec = *cr.Spec.Server.Autoscale.HPA
+				changed = true
+			}
 		}
-		return nil // HorizontalPodAutoscaler found and configured, nothing do to, move along...
+
+		if changed {
+			return r.Client.Update(context.TODO(), existingHPA)
+		}
+
+		// HorizontalPodAutoscaler found, no changes detected
+		return nil
 	}
 
 	if !cr.Spec.Server.Autoscale.Enabled {
 		return nil // AutoScale not enabled, move along...
 	}
 
+	// AutoScale enabled, no existing HPA found, create
 	if cr.Spec.Server.Autoscale.HPA != nil {
-		hpa.Spec = *cr.Spec.Server.Autoscale.HPA
-	} else {
-		hpa.Spec.MaxReplicas = 3
-
-		var minrReplicas int32 = 1
-		hpa.Spec.MinReplicas = &minrReplicas
-
-		var tcup int32 = 50
-		hpa.Spec.TargetCPUUtilizationPercentage = &tcup
-
-		hpa.Spec.ScaleTargetRef = autoscaling.CrossVersionObjectReference{
-			APIVersion: "apps/v1",
-			Kind:       "Deployment",
-			Name:       nameWithSuffix("server", cr),
-		}
+		defaultHPA.Spec = *cr.Spec.Server.Autoscale.HPA
 	}
 
-	return r.Client.Create(context.TODO(), hpa)
+	return r.Client.Create(context.TODO(), defaultHPA)
 }
 
 // reconcileAutoscalers will ensure that all HorizontalPodAutoscalers are present for the given ArgoCD.
diff --git a/docs/usage/deploy-to-different-namespaces.md b/docs/usage/deploy-to-different-namespaces.md
index 89e1b2ba..e4e9f002 100644
--- a/docs/usage/deploy-to-different-namespaces.md
+++ b/docs/usage/deploy-to-different-namespaces.md
@@ -10,5 +10,39 @@ kind: Namespace
 metadata:
   name: bar
   labels:
-    argocd.argoproj.io/managed-by: foo // namespace of the Argo CD instance
+    argocd.argoproj.io/managed-by: foo // namespace of managing Argo CD instance
 ```
+!!! note
+    The above described method assumes that the user has admin privileges on their cluster, which would allow them to apply labels to namespaces. 
+
+
+Users that do not have permissions to label namespaces can follow an alternate method to achieve the same behavior by leveraging the `.spec.syncPolicy` field of an application. SyncPolicy allows users to have a namespace created  with certain labels pre-configured at the time of application sync. Consider the following example Application:
+
+```yaml
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+  name: guestbook
+  namespace: argocd
+spec:
+  syncPolicy:
+    managedNamespaceMetadata:
+      labels:
+        argocd.argoproj.io/managed-by: foo // namespace of managing Argo CD instance 
+    syncOptions:
+    - CreateNamespace=true
+  destination:
+    server: https://kubernetes.default.svc
+    namespace: bar 
+```
+
+The above described application will create a new namespace `bar` carrying a label `argocd.arogproj.io/managed-by: foo` at the time of application sync, and then deploy application resources in it, without requiring the user to be able to label namespace `bar` manually.
+
+Users creating applications using the Argo CD UI instead of CLI must check the "auto-create namespace" box, and then switch to the yaml editor to add the label into `.spec.syncPolicy.managedNamespaceMetadata.labels` as described above.
+
+A few points to keep in mind:
+
+- This method requires that the user create the namespace at app sync time using `createNamespace=true` or checking the `auto-create namespace` box in the UI, and not include their own namespace manifest in their git repository. 
+- A destination namespace must be set in `.spec.destination.namespace`  
+
+See https://argo-cd.readthedocs.io/en/stable/user-guide/sync-options/#namespace-metadata for more information 
\ No newline at end of file


Test patch:
diff --git a/controllers/argocd/hpa_test.go b/controllers/argocd/hpa_test.go
new file mode 100644
index 00000000..1090bff8
--- /dev/null
+++ b/controllers/argocd/hpa_test.go
@@ -0,0 +1,91 @@
+package argocd
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	autoscaling "k8s.io/api/autoscaling/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/types"
+	logf "sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+var (
+	min     int32 = 5
+	max     int32 = 8
+	cpuUtil int32 = 45
+)
+
+func TestReconcileHPA(t *testing.T) {
+
+	logf.SetLogger(ZapLogger(true))
+	a := makeTestArgoCD()
+	r := makeTestReconciler(t, a)
+
+	existingHPA := newHorizontalPodAutoscalerWithSuffix("server", a)
+
+	defaultHPASpec := autoscaling.HorizontalPodAutoscalerSpec{
+		MaxReplicas:                    maxReplicas,
+		MinReplicas:                    &minReplicas,
+		TargetCPUUtilizationPercentage: &tcup,
+		ScaleTargetRef: autoscaling.CrossVersionObjectReference{
+			APIVersion: "apps/v1",
+			Kind:       "Deployment",
+			Name:       nameWithSuffix("server", a),
+		},
+	}
+
+	updatedHPASpec := autoscaling.HorizontalPodAutoscalerSpec{
+		MaxReplicas:                    max,
+		MinReplicas:                    &min,
+		TargetCPUUtilizationPercentage: &cpuUtil,
+		ScaleTargetRef: autoscaling.CrossVersionObjectReference{
+			APIVersion: "apps/v1",
+			Kind:       "Deployment",
+			Name:       nameWithSuffix("server", a),
+		},
+	}
+
+	err := r.Client.Get(context.TODO(), types.NamespacedName{
+		Name:      "argocd-server",
+		Namespace: testNamespace,
+	}, existingHPA)
+	assert.True(t, errors.IsNotFound(err))
+
+	a.Spec.Server.Autoscale.Enabled = true
+
+	err = r.reconcileServerHPA(a)
+	assert.NoError(t, err)
+
+	err = r.Client.Get(context.TODO(), types.NamespacedName{
+		Name:      "argocd-server",
+		Namespace: testNamespace,
+	}, existingHPA)
+	assert.NoError(t, err)
+	assert.Equal(t, defaultHPASpec, existingHPA.Spec)
+
+	a.Spec.Server.Autoscale.HPA = &updatedHPASpec
+
+	err = r.reconcileServerHPA(a)
+	assert.NoError(t, err)
+
+	err = r.Client.Get(context.TODO(), types.NamespacedName{
+		Name:      "argocd-server",
+		Namespace: testNamespace,
+	}, existingHPA)
+	assert.NoError(t, err)
+	assert.Equal(t, updatedHPASpec, existingHPA.Spec)
+
+	a.Spec.Server.Autoscale.Enabled = false
+
+	err = r.reconcileServerHPA(a)
+	assert.NoError(t, err)
+
+	err = r.Client.Get(context.TODO(), types.NamespacedName{
+		Name:      "argocd-server",
+		Namespace: testNamespace,
+	}, existingHPA)
+	assert.True(t, errors.IsNotFound(err))
+
+}