diff --git a/common/names.go b/common/names.go index 8c68ad5be..4cded4316 100644 --- a/common/names.go +++ b/common/names.go @@ -29,9 +29,6 @@ const ( // ArgoCDRBACConfigMapName is the upstream hard-coded RBAC ConfigMap name. ArgoCDRBACConfigMapName = "argocd-rbac-cm" - // ArgoCDSecretName is the upstream hard-coded ArgoCD Secret name. - ArgoCDSecretName = "argocd-secret" - // ArgoCDTLSCertsConfigMapName is the upstream hard-coded TLS certificate data ConfigMap name. ArgoCDTLSCertsConfigMapName = "argocd-tls-certs-cm" @@ -50,6 +47,12 @@ const ( // ArgoCDCASuffix is the name suffix for ArgoCD CA resources. ArgoCDCASuffix = "ca" + // ArgoCDGRPCSuffix is the name suffix for ArgoCD GRPC resources. + ArgoCDGRPCSuffix = "grpc" + + // ArgoCDTLSSuffix is the name suffix for ArgoCD TLS resources. + ArgoCDTLSSuffix = "tls" + // ArgoCDGrafanaConfigMapSuffix is the default suffix for the Grafana configuration ConfigMap. ArgoCDGrafanaConfigMapSuffix = "grafana-config" diff --git a/config/crd/bases/argoproj.io_argocds.yaml b/config/crd/bases/argoproj.io_argocds.yaml index 69cd14b26..7a4267b15 100644 --- a/config/crd/bases/argoproj.io_argocds.yaml +++ b/config/crd/bases/argoproj.io_argocds.yaml @@ -19260,4 +19260,4 @@ status: kind: "" plural: "" conditions: [] - storedVersions: [] + storedVersions: [] \ No newline at end of file diff --git a/controllers/argocd/argocdcommon/argohelper.go b/controllers/argocd/argocdcommon/argohelper.go index cbd791633..3d153d03a 100644 --- a/controllers/argocd/argocdcommon/argohelper.go +++ b/controllers/argocd/argocdcommon/argohelper.go @@ -6,6 +6,7 @@ import ( argoproj "github.com/argoproj-labs/argocd-operator/api/v1beta1" "github.com/argoproj-labs/argocd-operator/common" "github.com/argoproj-labs/argocd-operator/pkg/util" + "github.com/sethvargo/go-password/password" ) func GetArgoContainerImage(cr *argoproj.ArgoCD) string { @@ -27,3 +28,25 @@ func GetArgoContainerImage(cr *argoproj.ArgoCD) string { return util.CombineImageTag(img, tag) } + +// GenerateArgoAdminPassword will generate and return the admin password for Argo CD. +func GenerateArgoAdminPassword() ([]byte, error) { + pass, err := password.Generate( + common.ArgoCDDefaultAdminPasswordLength, + common.ArgoCDDefaultAdminPasswordNumDigits, + common.ArgoCDDefaultAdminPasswordNumSymbols, + false, false) + + return []byte(pass), err +} + +// GenerateArgoServerKey will generate and return the server signature key for session validation. +func GenerateArgoServerSessionKey() ([]byte, error) { + pass, err := password.Generate( + common.ArgoCDDefaultServerSessionKeyLength, + common.ArgoCDDefaultServerSessionKeyNumDigits, + common.ArgoCDDefaultServerSessionKeyNumSymbols, + false, false) + + return []byte(pass), err +} diff --git a/controllers/argocd/argocdcommon/helper.go b/controllers/argocd/argocdcommon/helper.go index 64a5c34ca..430024caa 100644 --- a/controllers/argocd/argocdcommon/helper.go +++ b/controllers/argocd/argocdcommon/helper.go @@ -11,3 +11,15 @@ func UpdateIfChanged(existingVal, desiredVal interface{}, extraAction func(), ch *changed = true } } + +func UpdateIfChangedSlice(existingVal, desiredVal interface{}, extraAction func(), changed *bool) (interface{}, interface{}) { + if !reflect.DeepEqual(existingVal, desiredVal) { + existingVal = desiredVal + if extraAction != nil { + extraAction() + } + *changed = true + } + + return existingVal, desiredVal +} diff --git a/controllers/argocd/deployment.go b/controllers/argocd/deployment.go index 5d9c7b647..9010c3f6d 100644 --- a/controllers/argocd/deployment.go +++ b/controllers/argocd/deployment.go @@ -1353,7 +1353,7 @@ func (r *ArgoCDReconciler) triggerDeploymentRollout(deployment *appsv1.Deploymen return nil } - deployment.Spec.Template.ObjectMeta.Labels[key] = nowNano() + deployment.Spec.Template.ObjectMeta.Labels[key] = util.NowNano() return r.Client.Update(context.TODO(), deployment) } diff --git a/controllers/argocd/ingress.go b/controllers/argocd/ingress.go index 10be039d2..84204101e 100644 --- a/controllers/argocd/ingress.go +++ b/controllers/argocd/ingress.go @@ -24,6 +24,7 @@ import ( argoproj "github.com/argoproj-labs/argocd-operator/api/v1beta1" "github.com/argoproj-labs/argocd-operator/common" + "github.com/argoproj-labs/argocd-operator/controllers/argocd/secret" "github.com/argoproj-labs/argocd-operator/pkg/util" ) @@ -150,7 +151,7 @@ func (r *ArgoCDReconciler) reconcileArgoServerIngress(cr *argoproj.ArgoCD) error Hosts: []string{ getArgoServerHost(cr), }, - SecretName: common.ArgoCDSecretName, + SecretName: secret.ArgoCDSecretName, }, } @@ -225,7 +226,7 @@ func (r *ArgoCDReconciler) reconcileArgoServerGRPCIngress(cr *argoproj.ArgoCD) e Hosts: []string{ getArgoServerGRPCHost(cr), }, - SecretName: common.ArgoCDSecretName, + SecretName: secret.ArgoCDSecretName, }, } @@ -302,7 +303,7 @@ func (r *ArgoCDReconciler) reconcileGrafanaIngress(cr *argoproj.ArgoCD) error { cr.Name, getGrafanaHost(cr), }, - SecretName: common.ArgoCDSecretName, + SecretName: secret.ArgoCDSecretName, }, } @@ -376,7 +377,7 @@ func (r *ArgoCDReconciler) reconcilePrometheusIngress(cr *argoproj.ArgoCD) error ingress.Spec.TLS = []networkingv1.IngressTLS{ { Hosts: []string{cr.Name}, - SecretName: common.ArgoCDSecretName, + SecretName: secret.ArgoCDSecretName, }, } diff --git a/controllers/argocd/keycloak.go b/controllers/argocd/keycloak.go index bd7207143..9f62ddc76 100644 --- a/controllers/argocd/keycloak.go +++ b/controllers/argocd/keycloak.go @@ -24,6 +24,7 @@ import ( argoproj "github.com/argoproj-labs/argocd-operator/api/v1beta1" "github.com/argoproj-labs/argocd-operator/common" + "github.com/argoproj-labs/argocd-operator/controllers/argocd/secret" "github.com/argoproj-labs/argocd-operator/pkg/cluster" util "github.com/argoproj-labs/argocd-operator/pkg/util" "github.com/argoproj-labs/argocd-operator/pkg/workloads" @@ -1060,7 +1061,7 @@ func (r *ArgoCDReconciler) updateArgoCDConfiguration(cr *argoproj.ArgoCD, kRoute // Update the ArgoCD client secret for OIDC in argocd-secret. argoCDSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: common.ArgoCDSecretName, + Name: secret.ArgoCDSecretName, Namespace: cr.Namespace, }, } diff --git a/controllers/argocd/removeFunctions.go b/controllers/argocd/removeFunctions.go new file mode 100644 index 000000000..eee41867a --- /dev/null +++ b/controllers/argocd/removeFunctions.go @@ -0,0 +1,462 @@ +package argocd + +import ( + "context" + "crypto/rsa" + "crypto/x509" + json "encoding/json" + "fmt" + "os" + "sort" + "strings" + + corev1 "k8s.io/api/core/v1" + + argoproj "github.com/argoproj-labs/argocd-operator/api/v1beta1" + "github.com/argoproj-labs/argocd-operator/common" + "github.com/argoproj-labs/argocd-operator/controllers/argocd/secret" + "github.com/argoproj-labs/argocd-operator/pkg/util" + argopass "github.com/argoproj/argo-cd/v2/util/password" + tlsutil "github.com/operator-framework/operator-sdk/pkg/tls" + "github.com/sethvargo/go-password/password" + "k8s.io/apimachinery/pkg/labels" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +func (r *ArgoCDReconciler) getClusterSecrets(cr *argoproj.ArgoCD) (*corev1.SecretList, error) { + + clusterSecrets := &corev1.SecretList{} + opts := &client.ListOptions{ + LabelSelector: labels.SelectorFromSet(map[string]string{ + common.ArgoCDArgoprojKeySecretType: "cluster", + }), + Namespace: cr.Namespace, + } + + if err := r.Client.List(context.TODO(), clusterSecrets, opts); err != nil { + return nil, err + } + + return clusterSecrets, nil +} + +// generateArgoAdminPassword will generate and return the admin password for Argo CD. +func generateArgoAdminPassword() ([]byte, error) { + pass, err := password.Generate( + common.ArgoCDDefaultAdminPasswordLength, + common.ArgoCDDefaultAdminPasswordNumDigits, + common.ArgoCDDefaultAdminPasswordNumSymbols, + false, false) + + return []byte(pass), err +} + +// generateArgoServerKey will generate and return the server signature key for session validation. +func generateArgoServerSessionKey() ([]byte, error) { + pass, err := password.Generate( + common.ArgoCDDefaultServerSessionKeyLength, + common.ArgoCDDefaultServerSessionKeyNumDigits, + common.ArgoCDDefaultServerSessionKeyNumSymbols, + false, false) + + return []byte(pass), err +} + +// hasArgoAdminPasswordChanged will return true if the Argo admin password has changed. +func hasArgoAdminPasswordChanged(actual *corev1.Secret, expected *corev1.Secret) bool { + actualPwd := string(actual.Data[common.ArgoCDKeyAdminPassword]) + expectedPwd := string(expected.Data[common.ArgoCDKeyAdminPassword]) + + validPwd, _ := argopass.VerifyPassword(expectedPwd, actualPwd) + if !validPwd { + log.Info("admin password has changed") + return true + } + return false +} + +// hasArgoTLSChanged will return true if the Argo TLS certificate or key have changed. +func hasArgoTLSChanged(actual *corev1.Secret, expected *corev1.Secret) bool { + actualCert := string(actual.Data[corev1.TLSCertKey]) + actualKey := string(actual.Data[corev1.TLSPrivateKeyKey]) + expectedCert := string(expected.Data[corev1.TLSCertKey]) + expectedKey := string(expected.Data[corev1.TLSPrivateKeyKey]) + + if actualCert != expectedCert || actualKey != expectedKey { + log.Info("tls secret has changed") + return true + } + return false +} + +// reconcileClusterMainSecret will ensure that the main Secret is present for the Argo CD cluster. +func (r *ArgoCDReconciler) reconcileClusterMainSecret(cr *argoproj.ArgoCD) error { + secret := util.NewSecretWithSuffix(cr, "cluster") + if util.IsObjectFound(r.Client, cr.Namespace, secret.Name, secret) { + return nil // Secret found, do nothing + } + + adminPassword, err := generateArgoAdminPassword() + if err != nil { + return err + } + + secret.Data = map[string][]byte{ + common.ArgoCDKeyAdminPassword: adminPassword, + } + + if err := controllerutil.SetControllerReference(cr, secret, r.Scheme); err != nil { + return err + } + return r.Client.Create(context.TODO(), secret) +} + +// newCASecret creates a new CA secret with the given suffix for the given ArgoCD. +func newCASecret(cr *argoproj.ArgoCD) (*corev1.Secret, error) { + secret := util.NewTLSSecret(cr, "ca") + + key, err := util.NewPrivateKey() + if err != nil { + return nil, err + } + + cert, err := util.NewSelfSignedCACertificate(cr.Name, key) + if err != nil { + return nil, err + } + + // This puts both ca.crt and tls.crt into the secret. + secret.Data = map[string][]byte{ + corev1.TLSCertKey: util.EncodeCertificatePEM(cert), + corev1.ServiceAccountRootCAKey: util.EncodeCertificatePEM(cert), + corev1.TLSPrivateKeyKey: util.EncodePrivateKeyPEM(key), + } + + return secret, nil +} + +// reconcileClusterCASecret ensures the CA Secret is created for the ArgoCD cluster. +func (r *ArgoCDReconciler) reconcileClusterCASecret(cr *argoproj.ArgoCD) error { + secret := util.NewSecretWithSuffix(cr, "ca") + if util.IsObjectFound(r.Client, cr.Namespace, secret.Name, secret) { + return nil // Secret found, do nothing + } + + secret, err := newCASecret(cr) + if err != nil { + return err + } + + if err := controllerutil.SetControllerReference(cr, secret, r.Scheme); err != nil { + return err + } + return r.Client.Create(context.TODO(), secret) +} + +// reconcileClusterTLSSecret ensures the TLS Secret is created for the ArgoCD cluster. +func (r *ArgoCDReconciler) reconcileClusterTLSSecret(cr *argoproj.ArgoCD) error { + secret := util.NewTLSSecret(cr, "tls") + if util.IsObjectFound(r.Client, cr.Namespace, secret.Name, secret) { + return nil // Secret found, do nothing + } + + caSecret := util.NewSecretWithSuffix(cr, "ca") + caSecret, err := util.FetchSecret(r.Client, cr.ObjectMeta, caSecret.Name) + if err != nil { + return err + } + + caCert, err := util.ParsePEMEncodedCert(caSecret.Data[corev1.TLSCertKey]) + if err != nil { + return err + } + + caKey, err := util.ParsePEMEncodedPrivateKey(caSecret.Data[corev1.TLSPrivateKeyKey]) + if err != nil { + return err + } + + secret, err = newCertificateSecret("tls", caCert, caKey, cr) + if err != nil { + return err + } + + if err := controllerutil.SetControllerReference(cr, secret, r.Scheme); err != nil { + return err + } + + return r.Client.Create(context.TODO(), secret) +} + +// newCertificateSecret creates a new secret using the given name suffix for the given TLS certificate. +func newCertificateSecret(suffix string, caCert *x509.Certificate, caKey *rsa.PrivateKey, cr *argoproj.ArgoCD) (*corev1.Secret, error) { + secret := util.NewTLSSecret(cr, suffix) + + key, err := util.NewPrivateKey() + if err != nil { + return nil, err + } + + cfg := &tlsutil.CertConfig{ + CertName: secret.Name, + CertType: tlsutil.ClientAndServingCert, + CommonName: secret.Name, + Organization: []string{cr.ObjectMeta.Namespace}, + } + + dnsNames := []string{ + cr.ObjectMeta.Name, + util.NameWithSuffix(cr.Name, "grpc"), + fmt.Sprintf("%s.%s.svc.cluster.local", cr.ObjectMeta.Name, cr.ObjectMeta.Namespace), + } + + if cr.Spec.Grafana.Enabled { + dnsNames = append(dnsNames, getGrafanaHost(cr)) + } + if cr.Spec.Prometheus.Enabled { + dnsNames = append(dnsNames, getPrometheusHost(cr)) + } + + cert, err := util.NewSignedCertificate(cfg, dnsNames, key, caCert, caKey) + if err != nil { + return nil, err + } + + secret.Data = map[string][]byte{ + corev1.TLSCertKey: util.EncodeCertificatePEM(cert), + corev1.TLSPrivateKeyKey: util.EncodePrivateKeyPEM(key), + } + + return secret, nil +} + +// reconcileClusterPermissionsSecret ensures ArgoCD instance is namespace-scoped +func (r *ArgoCDReconciler) reconcileClusterPermissionsSecret(cr *argoproj.ArgoCD) error { + var clusterConfigInstance bool + secret := util.NewSecretWithSuffix(cr, "default-cluster-config") + secret.Labels[common.ArgoCDArgoprojKeySecretType] = "cluster" + dataBytes, _ := json.Marshal(map[string]interface{}{ + "tlsClientConfig": map[string]interface{}{ + "insecure": false, + }, + }) + + namespaceList := corev1.NamespaceList{} + listOption := client.MatchingLabels{ + common.ArgoCDArgoprojKeyManagedBy: cr.Namespace, + } + if err := r.Client.List(context.TODO(), &namespaceList, listOption); err != nil { + return err + } + + var namespaces []string + for _, namespace := range namespaceList.Items { + namespaces = append(namespaces, namespace.Name) + } + + if !util.ContainsString(namespaces, cr.Namespace) { + namespaces = append(namespaces, cr.Namespace) + } + sort.Strings(namespaces) + + secret.Data = map[string][]byte{ + "config": dataBytes, + "name": []byte("in-cluster"), + "server": []byte(common.ArgoCDDefaultServer), + "namespaces": []byte(strings.Join(namespaces, ",")), + } + + if allowedNamespace(cr.Namespace, os.Getenv("ARGOCD_CLUSTER_CONFIG_NAMESPACES")) { + clusterConfigInstance = true + } + + clusterSecrets, err := r.getClusterSecrets(cr) + if err != nil { + return err + } + + for _, s := range clusterSecrets.Items { + // check if cluster secret with default server address exists + if string(s.Data["server"]) == common.ArgoCDDefaultServer { + // if the cluster belongs to cluster config namespace, + // remove all namespaces from cluster secret, + // else update the list of namespaces if value differs. + if clusterConfigInstance { + delete(s.Data, "namespaces") + } else { + ns := strings.Split(string(s.Data["namespaces"]), ",") + for _, n := range namespaces { + if !util.ContainsString(ns, strings.TrimSpace(n)) { + ns = append(ns, strings.TrimSpace(n)) + } + } + sort.Strings(ns) + s.Data["namespaces"] = []byte(strings.Join(ns, ",")) + } + return r.Client.Update(context.TODO(), &s) + } + } + + if clusterConfigInstance { + // do nothing + return nil + } + + if err := controllerutil.SetControllerReference(cr, secret, r.Scheme); err != nil { + return err + } + return r.Client.Create(context.TODO(), secret) +} + +// reconcileArgoSecret will ensure that the Argo CD Secret is present. +func (r *ArgoCDReconciler) reconcileArgoSecret(cr *argoproj.ArgoCD) error { + clusterSecret := util.NewSecretWithSuffix(cr, "cluster") + secret := util.NewSecretWithName(cr, secret.ArgoCDSecretName) + + if !util.IsObjectFound(r.Client, cr.Namespace, clusterSecret.Name, clusterSecret) { + log.Info(fmt.Sprintf("cluster secret [%s] not found, waiting to reconcile argo secret [%s]", clusterSecret.Name, secret.Name)) + return nil + } + + tlsSecret := util.NewSecretWithSuffix(cr, "tls") + if !util.IsObjectFound(r.Client, cr.Namespace, tlsSecret.Name, tlsSecret) { + log.Info(fmt.Sprintf("tls secret [%s] not found, waiting to reconcile argo secret [%s]", tlsSecret.Name, secret.Name)) + return nil + } + + if util.IsObjectFound(r.Client, cr.Namespace, secret.Name, secret) { + return r.reconcileExistingArgoSecret(cr, secret, clusterSecret, tlsSecret) + } + + // Secret not found, create it... + hashedPassword, err := argopass.HashPassword(string(clusterSecret.Data[common.ArgoCDKeyAdminPassword])) + if err != nil { + return err + } + + sessionKey, err := generateArgoServerSessionKey() + if err != nil { + return err + } + + secret.Data = map[string][]byte{ + common.ArgoCDKeyAdminPassword: []byte(hashedPassword), + common.ArgoCDKeyAdminPasswordMTime: util.NowBytes(), + common.ArgoCDKeyServerSecretKey: sessionKey, + corev1.TLSCertKey: tlsSecret.Data[corev1.TLSCertKey], + corev1.TLSPrivateKeyKey: tlsSecret.Data[corev1.TLSPrivateKeyKey], + } + + if cr.Spec.SSO != nil && cr.Spec.SSO.Provider.ToLower() == argoproj.SSOProviderTypeDex { + dexOIDCClientSecret, err := r.getDexOAuthClientSecret(cr) + if err != nil { + return nil + } + secret.Data[common.ArgoCDDexSecretKey] = []byte(*dexOIDCClientSecret) + } + + if err := controllerutil.SetControllerReference(cr, secret, r.Scheme); err != nil { + return err + } + return r.Client.Create(context.TODO(), secret) +} + +// reconcileExistingArgoSecret will ensure that the Argo CD Secret is up to date. +func (r *ArgoCDReconciler) reconcileExistingArgoSecret(cr *argoproj.ArgoCD, secret *corev1.Secret, clusterSecret *corev1.Secret, tlsSecret *corev1.Secret) error { + changed := false + + if secret.Data == nil { + secret.Data = make(map[string][]byte) + } + + if secret.Data[common.ArgoCDKeyServerSecretKey] == nil { + sessionKey, err := generateArgoServerSessionKey() + if err != nil { + return err + } + secret.Data[common.ArgoCDKeyServerSecretKey] = sessionKey + } + + if hasArgoAdminPasswordChanged(secret, clusterSecret) { + pwBytes, ok := clusterSecret.Data[common.ArgoCDKeyAdminPassword] + if ok { + hashedPassword, err := argopass.HashPassword(strings.TrimRight(string(pwBytes), "\n")) + if err != nil { + return err + } + + secret.Data[common.ArgoCDKeyAdminPassword] = []byte(hashedPassword) + secret.Data[common.ArgoCDKeyAdminPasswordMTime] = util.NowBytes() + changed = true + } + } + + if hasArgoTLSChanged(secret, tlsSecret) { + secret.Data[corev1.TLSCertKey] = tlsSecret.Data[corev1.TLSCertKey] + secret.Data[corev1.TLSPrivateKeyKey] = tlsSecret.Data[corev1.TLSPrivateKeyKey] + changed = true + } + + if cr.Spec.SSO != nil && cr.Spec.SSO.Provider.ToLower() == argoproj.SSOProviderTypeDex { + dexOIDCClientSecret, err := r.getDexOAuthClientSecret(cr) + if err != nil { + return err + } + actual := string(secret.Data[common.ArgoCDDexSecretKey]) + if dexOIDCClientSecret != nil { + expected := *dexOIDCClientSecret + if actual != expected { + secret.Data[common.ArgoCDDexSecretKey] = []byte(*dexOIDCClientSecret) + changed = true + } + } + } + + if changed { + log.Info("updating argo secret") + if err := r.Client.Update(context.TODO(), secret); err != nil { + return err + } + } + + return nil +} + +// reconcileClusterSecrets will reconcile all Secret resources for the ArgoCD cluster. +func (r *ArgoCDReconciler) reconcileClusterSecrets(cr *argoproj.ArgoCD) error { + if err := r.reconcileClusterMainSecret(cr); err != nil { + return err + } + + if err := r.reconcileClusterCASecret(cr); err != nil { + return err + } + + if err := r.reconcileClusterTLSSecret(cr); err != nil { + return err + } + + if err := r.reconcileClusterPermissionsSecret(cr); err != nil { + return err + } + + if err := r.reconcileGrafanaSecret(cr); err != nil { + return err + } + + return nil +} + +// reconcileSecrets will reconcile all ArgoCD Secret resources. +func (r *ArgoCDReconciler) reconcileSecrets(cr *argoproj.ArgoCD) error { + if err := r.reconcileClusterSecrets(cr); err != nil { + return err + } + + if err := r.reconcileArgoSecret(cr); err != nil { + return err + } + + return nil +} diff --git a/controllers/argocd/secret.go b/controllers/argocd/secret.go index 62fef7eb3..118a90f8e 100644 --- a/controllers/argocd/secret.go +++ b/controllers/argocd/secret.go @@ -16,18 +16,8 @@ package argocd import ( "context" - "crypto/rsa" "crypto/sha256" - "crypto/x509" - "encoding/json" "fmt" - "os" - "sort" - "strings" - "time" - - argopass "github.com/argoproj/argo-cd/v2/util/password" - tlsutil "github.com/operator-framework/operator-sdk/pkg/tls" argoproj "github.com/argoproj-labs/argocd-operator/api/v1beta1" "github.com/argoproj-labs/argocd-operator/common" @@ -35,329 +25,10 @@ import ( corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) -// hasArgoAdminPasswordChanged will return true if the Argo admin password has changed. -func hasArgoAdminPasswordChanged(actual *corev1.Secret, expected *corev1.Secret) bool { - actualPwd := string(actual.Data[common.ArgoCDKeyAdminPassword]) - expectedPwd := string(expected.Data[common.ArgoCDKeyAdminPassword]) - - validPwd, _ := argopass.VerifyPassword(expectedPwd, actualPwd) - if !validPwd { - log.Info("admin password has changed") - return true - } - return false -} - -// hasArgoTLSChanged will return true if the Argo TLS certificate or key have changed. -func hasArgoTLSChanged(actual *corev1.Secret, expected *corev1.Secret) bool { - actualCert := string(actual.Data[corev1.TLSCertKey]) - actualKey := string(actual.Data[corev1.TLSPrivateKeyKey]) - expectedCert := string(expected.Data[corev1.TLSCertKey]) - expectedKey := string(expected.Data[corev1.TLSPrivateKeyKey]) - - if actualCert != expectedCert || actualKey != expectedKey { - log.Info("tls secret has changed") - return true - } - return false -} - -// nowBytes is a shortcut function to return the current date/time in RFC3339 format. -func nowBytes() []byte { - return []byte(time.Now().UTC().Format(time.RFC3339)) -} - -// nowNano returns a string with the current UTC time as epoch in nanoseconds -func nowNano() string { - return fmt.Sprintf("%d", time.Now().UTC().UnixNano()) -} - -// newCASecret creates a new CA secret with the given suffix for the given ArgoCD. -func newCASecret(cr *argoproj.ArgoCD) (*corev1.Secret, error) { - secret := util.NewTLSSecret(cr, "ca") - - key, err := util.NewPrivateKey() - if err != nil { - return nil, err - } - - cert, err := util.NewSelfSignedCACertificate(cr.Name, key) - if err != nil { - return nil, err - } - - // This puts both ca.crt and tls.crt into the secret. - secret.Data = map[string][]byte{ - corev1.TLSCertKey: util.EncodeCertificatePEM(cert), - corev1.ServiceAccountRootCAKey: util.EncodeCertificatePEM(cert), - corev1.TLSPrivateKeyKey: util.EncodePrivateKeyPEM(key), - } - - return secret, nil -} - -// newCertificateSecret creates a new secret using the given name suffix for the given TLS certificate. -func newCertificateSecret(suffix string, caCert *x509.Certificate, caKey *rsa.PrivateKey, cr *argoproj.ArgoCD) (*corev1.Secret, error) { - secret := util.NewTLSSecret(cr, suffix) - - key, err := util.NewPrivateKey() - if err != nil { - return nil, err - } - - cfg := &tlsutil.CertConfig{ - CertName: secret.Name, - CertType: tlsutil.ClientAndServingCert, - CommonName: secret.Name, - Organization: []string{cr.ObjectMeta.Namespace}, - } - - dnsNames := []string{ - cr.ObjectMeta.Name, - util.NameWithSuffix(cr.Name, "grpc"), - fmt.Sprintf("%s.%s.svc.cluster.local", cr.ObjectMeta.Name, cr.ObjectMeta.Namespace), - } - - if cr.Spec.Grafana.Enabled { - dnsNames = append(dnsNames, getGrafanaHost(cr)) - } - if cr.Spec.Prometheus.Enabled { - dnsNames = append(dnsNames, getPrometheusHost(cr)) - } - - cert, err := util.NewSignedCertificate(cfg, dnsNames, key, caCert, caKey) - if err != nil { - return nil, err - } - - secret.Data = map[string][]byte{ - corev1.TLSCertKey: util.EncodeCertificatePEM(cert), - corev1.TLSPrivateKeyKey: util.EncodePrivateKeyPEM(key), - } - - return secret, nil -} - -// reconcileArgoSecret will ensure that the Argo CD Secret is present. -func (r *ArgoCDReconciler) reconcileArgoSecret(cr *argoproj.ArgoCD) error { - clusterSecret := util.NewSecretWithSuffix(cr, "cluster") - secret := util.NewSecretWithName(cr, common.ArgoCDSecretName) - - if !util.IsObjectFound(r.Client, cr.Namespace, clusterSecret.Name, clusterSecret) { - log.Info(fmt.Sprintf("cluster secret [%s] not found, waiting to reconcile argo secret [%s]", clusterSecret.Name, secret.Name)) - return nil - } - - tlsSecret := util.NewSecretWithSuffix(cr, "tls") - if !util.IsObjectFound(r.Client, cr.Namespace, tlsSecret.Name, tlsSecret) { - log.Info(fmt.Sprintf("tls secret [%s] not found, waiting to reconcile argo secret [%s]", tlsSecret.Name, secret.Name)) - return nil - } - - if util.IsObjectFound(r.Client, cr.Namespace, secret.Name, secret) { - return r.reconcileExistingArgoSecret(cr, secret, clusterSecret, tlsSecret) - } - - // Secret not found, create it... - hashedPassword, err := argopass.HashPassword(string(clusterSecret.Data[common.ArgoCDKeyAdminPassword])) - if err != nil { - return err - } - - sessionKey, err := generateArgoServerSessionKey() - if err != nil { - return err - } - - secret.Data = map[string][]byte{ - common.ArgoCDKeyAdminPassword: []byte(hashedPassword), - common.ArgoCDKeyAdminPasswordMTime: nowBytes(), - common.ArgoCDKeyServerSecretKey: sessionKey, - corev1.TLSCertKey: tlsSecret.Data[corev1.TLSCertKey], - corev1.TLSPrivateKeyKey: tlsSecret.Data[corev1.TLSPrivateKeyKey], - } - - if cr.Spec.SSO != nil && cr.Spec.SSO.Provider.ToLower() == argoproj.SSOProviderTypeDex { - dexOIDCClientSecret, err := r.getDexOAuthClientSecret(cr) - if err != nil { - return nil - } - secret.Data[common.ArgoCDDexSecretKey] = []byte(*dexOIDCClientSecret) - } - - if err := controllerutil.SetControllerReference(cr, secret, r.Scheme); err != nil { - return err - } - return r.Client.Create(context.TODO(), secret) -} - -// reconcileClusterMainSecret will ensure that the main Secret is present for the Argo CD cluster. -func (r *ArgoCDReconciler) reconcileClusterMainSecret(cr *argoproj.ArgoCD) error { - secret := util.NewSecretWithSuffix(cr, "cluster") - if util.IsObjectFound(r.Client, cr.Namespace, secret.Name, secret) { - return nil // Secret found, do nothing - } - - adminPassword, err := generateArgoAdminPassword() - if err != nil { - return err - } - - secret.Data = map[string][]byte{ - common.ArgoCDKeyAdminPassword: adminPassword, - } - - if err := controllerutil.SetControllerReference(cr, secret, r.Scheme); err != nil { - return err - } - return r.Client.Create(context.TODO(), secret) -} - -// reconcileClusterTLSSecret ensures the TLS Secret is created for the ArgoCD cluster. -func (r *ArgoCDReconciler) reconcileClusterTLSSecret(cr *argoproj.ArgoCD) error { - secret := util.NewTLSSecret(cr, "tls") - if util.IsObjectFound(r.Client, cr.Namespace, secret.Name, secret) { - return nil // Secret found, do nothing - } - - caSecret := util.NewSecretWithSuffix(cr, "ca") - caSecret, err := util.FetchSecret(r.Client, cr.ObjectMeta, caSecret.Name) - if err != nil { - return err - } - - caCert, err := util.ParsePEMEncodedCert(caSecret.Data[corev1.TLSCertKey]) - if err != nil { - return err - } - - caKey, err := util.ParsePEMEncodedPrivateKey(caSecret.Data[corev1.TLSPrivateKeyKey]) - if err != nil { - return err - } - - secret, err = newCertificateSecret("tls", caCert, caKey, cr) - if err != nil { - return err - } - - if err := controllerutil.SetControllerReference(cr, secret, r.Scheme); err != nil { - return err - } - - return r.Client.Create(context.TODO(), secret) -} - -// reconcileClusterCASecret ensures the CA Secret is created for the ArgoCD cluster. -func (r *ArgoCDReconciler) reconcileClusterCASecret(cr *argoproj.ArgoCD) error { - secret := util.NewSecretWithSuffix(cr, "ca") - if util.IsObjectFound(r.Client, cr.Namespace, secret.Name, secret) { - return nil // Secret found, do nothing - } - - secret, err := newCASecret(cr) - if err != nil { - return err - } - - if err := controllerutil.SetControllerReference(cr, secret, r.Scheme); err != nil { - return err - } - return r.Client.Create(context.TODO(), secret) -} - -// reconcileClusterSecrets will reconcile all Secret resources for the ArgoCD cluster. -func (r *ArgoCDReconciler) reconcileClusterSecrets(cr *argoproj.ArgoCD) error { - if err := r.reconcileClusterMainSecret(cr); err != nil { - return err - } - - if err := r.reconcileClusterCASecret(cr); err != nil { - return err - } - - if err := r.reconcileClusterTLSSecret(cr); err != nil { - return err - } - - if err := r.reconcileClusterPermissionsSecret(cr); err != nil { - return err - } - - if err := r.reconcileGrafanaSecret(cr); err != nil { - return err - } - - return nil -} - -// reconcileExistingArgoSecret will ensure that the Argo CD Secret is up to date. -func (r *ArgoCDReconciler) reconcileExistingArgoSecret(cr *argoproj.ArgoCD, secret *corev1.Secret, clusterSecret *corev1.Secret, tlsSecret *corev1.Secret) error { - changed := false - - if secret.Data == nil { - secret.Data = make(map[string][]byte) - } - - if secret.Data[common.ArgoCDKeyServerSecretKey] == nil { - sessionKey, err := generateArgoServerSessionKey() - if err != nil { - return err - } - secret.Data[common.ArgoCDKeyServerSecretKey] = sessionKey - } - - if hasArgoAdminPasswordChanged(secret, clusterSecret) { - pwBytes, ok := clusterSecret.Data[common.ArgoCDKeyAdminPassword] - if ok { - hashedPassword, err := argopass.HashPassword(strings.TrimRight(string(pwBytes), "\n")) - if err != nil { - return err - } - - secret.Data[common.ArgoCDKeyAdminPassword] = []byte(hashedPassword) - secret.Data[common.ArgoCDKeyAdminPasswordMTime] = nowBytes() - changed = true - } - } - - if hasArgoTLSChanged(secret, tlsSecret) { - secret.Data[corev1.TLSCertKey] = tlsSecret.Data[corev1.TLSCertKey] - secret.Data[corev1.TLSPrivateKeyKey] = tlsSecret.Data[corev1.TLSPrivateKeyKey] - changed = true - } - - if cr.Spec.SSO != nil && cr.Spec.SSO.Provider.ToLower() == argoproj.SSOProviderTypeDex { - dexOIDCClientSecret, err := r.getDexOAuthClientSecret(cr) - if err != nil { - return err - } - actual := string(secret.Data[common.ArgoCDDexSecretKey]) - if dexOIDCClientSecret != nil { - expected := *dexOIDCClientSecret - if actual != expected { - secret.Data[common.ArgoCDDexSecretKey] = []byte(*dexOIDCClientSecret) - changed = true - } - } - } - - if changed { - log.Info("updating argo secret") - if err := r.Client.Update(context.TODO(), secret); err != nil { - return err - } - } - - return nil -} - // reconcileGrafanaSecret will ensure that the Grafana Secret is present. func (r *ArgoCDReconciler) reconcileGrafanaSecret(cr *argoproj.ArgoCD) error { if !cr.Spec.Grafana.Enabled { @@ -420,84 +91,6 @@ func (r *ArgoCDReconciler) reconcileGrafanaSecret(cr *argoproj.ArgoCD) error { return r.Client.Create(context.TODO(), secret) } -// reconcileClusterPermissionsSecret ensures ArgoCD instance is namespace-scoped -func (r *ArgoCDReconciler) reconcileClusterPermissionsSecret(cr *argoproj.ArgoCD) error { - var clusterConfigInstance bool - secret := util.NewSecretWithSuffix(cr, "default-cluster-config") - secret.Labels[common.ArgoCDArgoprojKeySecretType] = "cluster" - dataBytes, _ := json.Marshal(map[string]interface{}{ - "tlsClientConfig": map[string]interface{}{ - "insecure": false, - }, - }) - - namespaceList := corev1.NamespaceList{} - listOption := client.MatchingLabels{ - common.ArgoCDArgoprojKeyManagedBy: cr.Namespace, - } - if err := r.Client.List(context.TODO(), &namespaceList, listOption); err != nil { - return err - } - - var namespaces []string - for _, namespace := range namespaceList.Items { - namespaces = append(namespaces, namespace.Name) - } - - if !util.ContainsString(namespaces, cr.Namespace) { - namespaces = append(namespaces, cr.Namespace) - } - sort.Strings(namespaces) - - secret.Data = map[string][]byte{ - "config": dataBytes, - "name": []byte("in-cluster"), - "server": []byte(common.ArgoCDDefaultServer), - "namespaces": []byte(strings.Join(namespaces, ",")), - } - - if allowedNamespace(cr.Namespace, os.Getenv("ARGOCD_CLUSTER_CONFIG_NAMESPACES")) { - clusterConfigInstance = true - } - - clusterSecrets, err := r.getClusterSecrets(cr) - if err != nil { - return err - } - - for _, s := range clusterSecrets.Items { - // check if cluster secret with default server address exists - if string(s.Data["server"]) == common.ArgoCDDefaultServer { - // if the cluster belongs to cluster config namespace, - // remove all namespaces from cluster secret, - // else update the list of namespaces if value differs. - if clusterConfigInstance { - delete(s.Data, "namespaces") - } else { - ns := strings.Split(string(s.Data["namespaces"]), ",") - for _, n := range namespaces { - if !util.ContainsString(ns, strings.TrimSpace(n)) { - ns = append(ns, strings.TrimSpace(n)) - } - } - sort.Strings(ns) - s.Data["namespaces"] = []byte(strings.Join(ns, ",")) - } - return r.Client.Update(context.TODO(), &s) - } - } - - if clusterConfigInstance { - // do nothing - return nil - } - - if err := controllerutil.SetControllerReference(cr, secret, r.Scheme); err != nil { - return err - } - return r.Client.Create(context.TODO(), secret) -} - // reconcileRepoServerTLSSecret checks whether the argocd-repo-server-tls secret // has changed since our last reconciliation loop. It does so by comparing the // checksum of tls.crt and tls.key in the status of the ArgoCD CR against the @@ -667,33 +260,3 @@ func (r *ArgoCDReconciler) reconcileRedisTLSSecret(cr *argoproj.ArgoCD, useTLSFo return nil } - -// reconcileSecrets will reconcile all ArgoCD Secret resources. -func (r *ArgoCDReconciler) reconcileSecrets(cr *argoproj.ArgoCD) error { - if err := r.reconcileClusterSecrets(cr); err != nil { - return err - } - - if err := r.reconcileArgoSecret(cr); err != nil { - return err - } - - return nil -} - -func (r *ArgoCDReconciler) getClusterSecrets(cr *argoproj.ArgoCD) (*corev1.SecretList, error) { - - clusterSecrets := &corev1.SecretList{} - opts := &client.ListOptions{ - LabelSelector: labels.SelectorFromSet(map[string]string{ - common.ArgoCDArgoprojKeySecretType: "cluster", - }), - Namespace: cr.Namespace, - } - - if err := r.Client.List(context.TODO(), clusterSecrets, opts); err != nil { - return nil, err - } - - return clusterSecrets, nil -} diff --git a/controllers/argocd/secret/constants.go b/controllers/argocd/secret/constants.go new file mode 100644 index 000000000..5ea58a573 --- /dev/null +++ b/controllers/argocd/secret/constants.go @@ -0,0 +1,22 @@ +package secret + +// names +const ( + // ArgoCDSecretName is the upstream hard-coded ArgoCD Secret name. + ArgoCDSecretName = "argocd-secret" + + InClusterSecretName = "in-cluster" + + SecretsControllerName = "secrets-controller" +) + +// suffixes +const ( + DefaultClusterConfigSuffix = "default-cluster-config" + DefaultClusterCredentialsSuffix = "cluster" +) + +// values +const ( + ClusterSecretType = "cluster" +) diff --git a/controllers/argocd/secret/secret.go b/controllers/argocd/secret/secret.go index e881f7896..f2a09514a 100644 --- a/controllers/argocd/secret/secret.go +++ b/controllers/argocd/secret/secret.go @@ -1,10 +1,32 @@ package secret import ( + "crypto/rsa" + "crypto/x509" + "encoding/json" + "fmt" + "sort" + "strings" + argoproj "github.com/argoproj-labs/argocd-operator/api/v1beta1" + "github.com/argoproj-labs/argocd-operator/common" + "github.com/argoproj-labs/argocd-operator/controllers/argocd/argocdcommon" + "github.com/argoproj-labs/argocd-operator/pkg/mutation" + "github.com/argoproj-labs/argocd-operator/pkg/util" + "github.com/argoproj-labs/argocd-operator/pkg/workloads" + argopass "github.com/argoproj/argo-cd/v2/util/password" "github.com/go-logr/logr" + tlsutil "github.com/operator-framework/operator-sdk/pkg/tls" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + amerr "k8s.io/apimachinery/pkg/util/errors" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + cntrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) type SecretReconciler struct { @@ -17,7 +39,494 @@ type SecretReconciler struct { } func (sr *SecretReconciler) Reconcile() error { + var reconciliationErrors []error + sr.Logger = ctrl.Log.WithName(SecretsControllerName).WithValues("instance", sr.Instance.Name, "instance-namespace", sr.Instance.Namespace) + + if err := sr.reconcileCredentialsSecret(); err != nil { + reconciliationErrors = append(reconciliationErrors, err) + } + + if err := sr.reconcileClusterPermissionsSecret(); err != nil { + reconciliationErrors = append(reconciliationErrors, err) + } + + if err := sr.reconcileCASecret(); err != nil { + return err + } + + if err := sr.reconcileTLSSecret(); err != nil { + return err + } + + if err := sr.reconcileArgoCDSecret(); err != nil { + return err + } + + // TO DO : skipping grafana for now - it should either be in its own controller + // or preferably skipped altogether by the operator + + return amerr.NewAggregate(reconciliationErrors) +} + +// reconcileArgoCDSecret creates and updates the "argocd-secret" secret for the given +// Argo CD instance. It requires the credentials secret, CA secret and TLS secret +// to be present on the cluster already +func (sr *SecretReconciler) reconcileArgoCDSecret() error { + credSecretName := util.NameWithSuffix(sr.Instance.Name, DefaultClusterCredentialsSuffix) + credsSecret, err := workloads.GetSecret(credSecretName, sr.Instance.Namespace, sr.Client) + if err != nil { + sr.Logger.Error(err, "reconcileArgoCDSecret: failed to retrieve secret", "name", credSecretName, "namespace", sr.Instance.Namespace) + return err + } + + tlsSecretName := util.NameWithSuffix(sr.Instance.Name, common.ArgoCDTLSSuffix) + tlsSecret, err := workloads.GetSecret(tlsSecretName, sr.Instance.Namespace, sr.Client) + if err != nil { + sr.Logger.Error(err, "reconcileArgoCDSecret: failed to retrieve secret", "name", tlsSecretName, "namespace", sr.Instance.Namespace) + return err + } + + argocdSecretTmpl := sr.getDesiredSecretTmplObj(ArgoCDSecretName) + hashedPassword, err := argopass.HashPassword(string(credsSecret.Data[common.ArgoCDKeyAdminPassword])) + if err != nil { + sr.Logger.Error(err, "reconcileArgoCDSecret: failed to hash admin credentials") + return err + } + sessionKey, err := argocdcommon.GenerateArgoServerSessionKey() + if err != nil { + sr.Logger.Error(err, "reconcileArgoCDSecret: failed to generate session key") + return err + } + argocdSecretTmpl.Data = map[string][]byte{ + common.ArgoCDKeyAdminPassword: []byte(hashedPassword), + common.ArgoCDKeyAdminPasswordMTime: util.NowBytes(), + common.ArgoCDKeyServerSecretKey: sessionKey, + corev1.TLSCertKey: tlsSecret.Data[corev1.TLSCertKey], + corev1.TLSPrivateKeyKey: tlsSecret.Data[corev1.TLSPrivateKeyKey], + } + + // TO DO: Let dex controller populate dex oauth client secret information instead of doing it here + + existingArgoCDSecret, err := workloads.GetSecret(ArgoCDSecretName, sr.Instance.Namespace, sr.Client) + if err != nil { + if !errors.IsNotFound(err) { + sr.Logger.Error(err, "reconcileArgoCDSecret: failed to retrieve secret", "name", ArgoCDSecretName, "namespace", sr.Instance.Namespace) + return err + } + + argocdSecretReq := sr.getSecretRequest(*argocdSecretTmpl) + argocdSecret, err := workloads.RequestSecret(argocdSecretReq) + if err != nil { + sr.Logger.Error(err, "reconcileArgoCDSecret: failed to request secret", "name", ArgoCDSecretName, "namespace", sr.Instance.Namespace) + return err + } + + if err = controllerutil.SetControllerReference(sr.Instance, argocdSecret, sr.Scheme); err != nil { + sr.Logger.Error(err, "reconcileClusterPermissionsSecret: failed to set owner reference for secret", "name", argocdSecret.Name, "namespace", argocdSecret.Namespace) + } + + if err = workloads.CreateSecret(argocdSecret, sr.Client); err != nil { + sr.Logger.Error(err, "reconcileClusterPermissionsSecret: failed to create secret", "name", argocdSecret.Name, "namespace", argocdSecret.Namespace) + return err + } + sr.Logger.V(0).Info("reconcileClusterPermissionsSecret: secret created", "name", argocdSecret.Name, "namespace", argocdSecret.Namespace) + return nil + } + + argocdSecretChanged := false + if existingArgoCDSecret.Data == nil { + existingArgoCDSecret.Data = make(map[string][]byte) + } + if existingArgoCDSecret.Data[common.ArgoCDKeyServerSecretKey] == nil { + sessionKey, err := argocdcommon.GenerateArgoServerSessionKey() + if err != nil { + sr.Logger.Error(err, "reconcileArgoCDSecret: failed to generate session key") + return err + } + existingArgoCDSecret.Data[common.ArgoCDKeyServerSecretKey] = sessionKey + argocdSecretChanged = true + } + + if ArgoAdminPasswordChanged(existingArgoCDSecret, credsSecret) { + sr.Logger.V(1).Info("admin password changed, updating secret", "name", existingArgoCDSecret.Name, "namespace", existingArgoCDSecret.Namespace) + pwBytes, ok := credsSecret.Data[common.ArgoCDKeyAdminPassword] + if ok { + hashedPassword, err := argopass.HashPassword(strings.TrimRight(string(pwBytes), "\n")) + if err != nil { + sr.Logger.Error(err, "reconcileArgoCDSecret: failed to hash admin credentials") + return err + } + existingArgoCDSecret.Data[common.ArgoCDKeyAdminPassword] = []byte(hashedPassword) + existingArgoCDSecret.Data[common.ArgoCDKeyAdminPasswordMTime] = util.NowBytes() + argocdSecretChanged = true + } + } + + if ArgoTLSChanged(existingArgoCDSecret, tlsSecret) { + sr.Logger.V(1).Info("TLS key or certificate changed, updating secret", "name", existingArgoCDSecret.Name, "namespace", existingArgoCDSecret.Namespace) + existingArgoCDSecret.Data[corev1.TLSCertKey] = tlsSecret.Data[corev1.TLSCertKey] + existingArgoCDSecret.Data[corev1.TLSPrivateKeyKey] = tlsSecret.Data[corev1.TLSPrivateKeyKey] + argocdSecretChanged = true + } + + // TO DO: Let dex controller populate dex oauth client secret information instead of doing it here + + if argocdSecretChanged { + if err = workloads.UpdateSecret(existingArgoCDSecret, sr.Client); err != nil { + sr.Logger.Error(err, "reconcileArgoCDSecret: failed to update secret", "name", existingArgoCDSecret.Name, "namespace", existingArgoCDSecret.Namespace) + return err + } + sr.Logger.V(0).Info("reconcileArgoCDSecret: secret updated", "name", existingArgoCDSecret.Name, "namespace", existingArgoCDSecret.Namespace) + return nil + } - // controller logic goes here + // nothing to do return nil } + +// reconcileClusterPermissionsSecret creates and updates the cluster secret for the host +// cluster of the given Argo CD instance ("in-cluster" secret). It updates the list +// of managed namespaces depending on the scope of the instance +func (sr *SecretReconciler) reconcileClusterPermissionsSecret() error { + clusterPermSecretName := util.NameWithSuffix(sr.Instance.Name, DefaultClusterConfigSuffix) + + managedNsList, _ := util.ConvertMapToSlices(sr.ManagedNamespaces) + sort.Strings(managedNsList) + + existingSecret, err := workloads.GetSecret(clusterPermSecretName, sr.Instance.Namespace, sr.Client) + if err != nil { + if !errors.IsNotFound(err) { + sr.Logger.Error(err, "reconcileClusterPermissionsSecret: failed to retrieve secret", "name", clusterPermSecretName, "namespace", sr.Instance.Namespace) + return err + } + + clusterPermSecretTmpl := sr.getDesiredSecretTmplObj(clusterPermSecretName) + clusterPermSecretTmpl.Labels[common.ArgoCDArgoprojKeySecretType] = ClusterSecretType + dataBytes, _ := json.Marshal(map[string]interface{}{ + "tlsClientConfig": map[string]interface{}{ + "insecure": false, + }, + }) + + clusterPermSecretTmpl.Data = map[string][]byte{ + "config": dataBytes, + "name": []byte(InClusterSecretName), + "server": []byte(common.ArgoCDDefaultServer), + "namespaces": []byte(strings.Join(managedNsList, ",")), + } + if sr.ClusterScoped { + delete(clusterPermSecretTmpl.Data, "namespaces") + } + clusterPermSecretReq := sr.getSecretRequest(*clusterPermSecretTmpl) + clusterPermSecret, err := workloads.RequestSecret(clusterPermSecretReq) + if err != nil { + sr.Logger.Error(err, "reconcileClusterPermissionsSecret: failed to request secret", "name", clusterPermSecretName, "namespace", sr.Instance.Namespace) + return err + } + + if err = controllerutil.SetControllerReference(sr.Instance, clusterPermSecret, sr.Scheme); err != nil { + sr.Logger.Error(err, "reconcileClusterPermissionsSecret: failed to set owner reference for secret", "name", clusterPermSecret.Name, "namespace", clusterPermSecret.Namespace) + } + + if err = workloads.CreateSecret(clusterPermSecret, sr.Client); err != nil { + sr.Logger.Error(err, "reconcileClusterPermissionsSecret: failed to create secret", "name", clusterPermSecret.Name, "namespace", clusterPermSecret.Namespace) + return err + } + sr.Logger.V(0).Info("reconcileClusterPermissionsSecret: secret created", "name", clusterPermSecret.Name, "namespace", clusterPermSecret.Namespace) + return nil + } + + clusterPermSecretChanged := false + + if sr.ClusterScoped { + delete(existingSecret.Data, "namespaces") + clusterPermSecretChanged = true + } else { + nsListChanged := false + newNsList := []string{} + + if existingNsListBytes, ok := existingSecret.Data["namespaces"]; ok { + existingNsList := strings.Split(string(existingNsListBytes), ",") + sort.Strings(existingNsList) + updatedNsList, _ := argocdcommon.UpdateIfChangedSlice(existingNsList, managedNsList, nil, &nsListChanged) + if updatedList, ok := updatedNsList.([]string); ok { + sort.Strings(updatedList) + newNsList = updatedList + } + } else { + newNsList = managedNsList + nsListChanged = true + } + + if nsListChanged { + sort.Strings(newNsList) + existingSecret.Data["namespaces"] = []byte(strings.Join(newNsList, ",")) + clusterPermSecretChanged = true + } + } + + if clusterPermSecretChanged { + if err = workloads.UpdateSecret(existingSecret, sr.Client); err != nil { + sr.Logger.Error(err, "reconcileClusterPermissionsSecret: failed to update secret", "name", existingSecret.Name, "namespace", existingSecret.Namespace) + return err + } + sr.Logger.V(0).Info("reconcileClusterPermissionsSecret: secret updated", "name", existingSecret.Name, "namespace", existingSecret.Namespace) + return nil + } + + // nothing to do + return nil +} + +// reconcileCredentialsSecret will ensure that the default Argo CD admin credentials Secret is always present on the cluster. +func (sr *SecretReconciler) reconcileCredentialsSecret() error { + credSecretName := util.NameWithSuffix(sr.Instance.Name, DefaultClusterCredentialsSuffix) + + _, err := workloads.GetSecret(credSecretName, sr.Instance.Namespace, sr.Client) + if err != nil { + if !errors.IsNotFound(err) { + sr.Logger.Error(err, "reconcileClusterCredentialsSecret: failed to retrieve secret", "name", credSecretName, "namespace", sr.Instance.Namespace) + return err + } + + adminPassword, err := argocdcommon.GenerateArgoAdminPassword() + if err != nil { + sr.Logger.Error(err, "reconcileClusterCredentialsSecret: failed to generate admin password") + return err + } + + credSecretTmpl := sr.getDesiredSecretTmplObj(credSecretName) + credSecretTmpl.Type = corev1.SecretTypeOpaque + credSecretTmpl.Data = map[string][]byte{ + common.ArgoCDKeyAdminPassword: adminPassword, + } + secretReq := sr.getSecretRequest(*credSecretTmpl) + clusterCredsSecret, err := workloads.RequestSecret(secretReq) + if err != nil { + sr.Logger.Error(err, "reconcileClusterCredentialsSecret: failed to request secret", "name", credSecretName, "namespace", sr.Instance.Namespace) + return err + } + + if err = controllerutil.SetControllerReference(sr.Instance, clusterCredsSecret, sr.Scheme); err != nil { + sr.Logger.Error(err, "reconcileClusterCredentialsSecret: failed to set owner reference for secret", "name", clusterCredsSecret.Name, "namespace", clusterCredsSecret.Namespace) + } + + if err = workloads.CreateSecret(clusterCredsSecret, sr.Client); err != nil { + sr.Logger.Error(err, "reconcileClusterCredentialsSecret: failed to create secret", "name", clusterCredsSecret.Name, "namespace", clusterCredsSecret.Namespace) + return err + } + sr.Logger.V(0).Info("reconcileClusterCredentialsSecret: secret created", "name", clusterCredsSecret.Name, "namespace", clusterCredsSecret.Namespace) + return nil + } + + // secret exists, nothing to do + return nil +} + +// reconcileCASecret ensures that CA secret is always present on the cluster +func (sr *SecretReconciler) reconcileCASecret() error { + caSecretName := util.NameWithSuffix(sr.Instance.Name, common.ArgoCDCASuffix) + + _, err := workloads.GetSecret(caSecretName, sr.Instance.Namespace, sr.Client) + if err != nil { + if !errors.IsNotFound(err) { + sr.Logger.Error(err, "reconcileClusterCASecret: failed to retrieve secret", "name", caSecretName, "namespace", sr.Instance.Namespace) + return err + } + + caSecret, err := sr.getDesiredCASecret(caSecretName) + if err != nil { + sr.Logger.Error(err, "reconcileClusterCASecret: failed to generate secret", "name", caSecretName) + return err + } + + if err = controllerutil.SetControllerReference(sr.Instance, caSecret, sr.Scheme); err != nil { + sr.Logger.Error(err, "reconcileClusterCASecret: failed to set owner reference for secret", "name", caSecret.Name, "namespace", caSecret.Namespace) + } + + if err = workloads.CreateSecret(caSecret, sr.Client); err != nil { + sr.Logger.Error(err, "reconcileClusterCASecret: failed to create secret", "name", caSecret.Name, "namespace", caSecret.Namespace) + return err + } + sr.Logger.V(0).Info("reconcileClusterCASecret: secret created", "name", caSecret.Name, "namespace", caSecret.Namespace) + return nil + } + + // secret exists, nothing to do + return nil +} + +// reconcileTLSSecret ensures that cluster TLS secret is always present on the cluster +func (sr *SecretReconciler) reconcileTLSSecret() error { + tlsSecretName := util.NameWithSuffix(sr.Instance.Name, common.ArgoCDTLSSuffix) + + _, err := workloads.GetSecret(tlsSecretName, sr.Instance.Namespace, sr.Client) + if err != nil { + if !errors.IsNotFound(err) { + sr.Logger.Error(err, "reconcileClusterTLSSecret: failed to retrieve secret", "name", tlsSecretName, "namespace", sr.Instance.Namespace) + return err + } + + caSecretName := util.NameWithSuffix(sr.Instance.Name, common.ArgoCDCASuffix) + caSecret, err := workloads.GetSecret(caSecretName, sr.Instance.Namespace, sr.Client) + if err != nil { + if !errors.IsNotFound(err) { + sr.Logger.Error(err, "reconcileClusterTLSSecret: failed to retrieve secret", "name", caSecretName, "namespace", sr.Instance.Namespace) + return err + } + + sr.Logger.Error(err, "reconcileClusterTLSSecret: CA secret required, but not found", "name", caSecretName, "namespace", sr.Instance.Namespace) + return err + } + + caCert, err := util.ParsePEMEncodedCert(caSecret.Data[corev1.TLSCertKey]) + if err != nil { + sr.Logger.Error(err, "reconcileClusterTLSSecret: failed to parse encoded cert") + return err + } + + caKey, err := util.ParsePEMEncodedPrivateKey(caSecret.Data[corev1.TLSPrivateKeyKey]) + if err != nil { + sr.Logger.Error(err, "reconcileClusterTLSSecret: failed to parse encoded private key") + return err + } + + tlsSecret, err := sr.getDesiredTLSCertSecret(tlsSecretName, caCert, caKey) + if err != nil { + sr.Logger.Error(err, "reconcileClusterTLSSecret: failed to generate secret", "name", tlsSecretName) + } + + if err = controllerutil.SetControllerReference(sr.Instance, caSecret, sr.Scheme); err != nil { + sr.Logger.Error(err, "reconcileClusterTLSSecret: failed to set owner reference for secret", "name", tlsSecret.Name, "namespace", tlsSecret.Namespace) + } + + if err = workloads.CreateSecret(tlsSecret, sr.Client); err != nil { + sr.Logger.Error(err, "reconcileClusterTLSSecret: failed to create secret", "name", tlsSecret.Name, "namespace", tlsSecret.Namespace) + return err + } + sr.Logger.V(0).Info("reconcileClusterTLSSecret: secret created", "name", tlsSecret.Name, "namespace", tlsSecret.Namespace) + return nil + } + + // secret exists, nothing to do + return nil +} + +// getDesiredCASecret generates a new private key and self signed certificate for the CA, +// creates a TLS secret and injects the encoded cert and key pem artifacts into the secret +func (sr *SecretReconciler) getDesiredCASecret(caSecretName string) (*corev1.Secret, error) { + key, err := util.NewPrivateKey() + if err != nil { + sr.Logger.Error(err, "getDesiredCASecret: failed to generate private key") + return nil, err + } + + cert, err := util.NewSelfSignedCACertificate(sr.Instance.Name, key) + if err != nil { + sr.Logger.Error(err, "getDesiredCASecret: failed to generate self signed CA certificate") + return nil, err + } + + // construct desired secret template, use it to populate a secret request object + // and receive the constructed CA secret from the workloads package + caSecretTmpl := sr.getDesiredSecretTmplObj(caSecretName) + caSecretTmpl.Type = corev1.SecretTypeTLS + // This puts both ca.crt and tls.crt into the secret request. + caSecretTmpl.Data = map[string][]byte{ + corev1.TLSCertKey: util.EncodeCertificatePEM(cert), + corev1.ServiceAccountRootCAKey: util.EncodeCertificatePEM(cert), + corev1.TLSPrivateKeyKey: util.EncodePrivateKeyPEM(key), + } + secretReq := sr.getSecretRequest(*caSecretTmpl) + caSecret, err := workloads.RequestSecret(secretReq) + if err != nil { + sr.Logger.Error(err, "getDesiredCASecret: failed to request secret", "name", caSecretName, "namespace", sr.Instance.Namespace) + return nil, err + } + + return caSecret, nil +} + +// getDesiredTLSCertSecret generates a new private key and signed TLS certificate using given CA cert and key, +// injects it into a new TLS secret and returns it +func (sr *SecretReconciler) getDesiredTLSCertSecret(tlsCertName string, caCert *x509.Certificate, caKey *rsa.PrivateKey) (*corev1.Secret, error) { + key, err := util.NewPrivateKey() + if err != nil { + sr.Logger.Error(err, "getDesiredTLSCertSecret: failed to generate private key") + return nil, err + } + + cfg := &tlsutil.CertConfig{ + CertName: tlsCertName, + CertType: tlsutil.ClientAndServingCert, + CommonName: tlsCertName, + Organization: []string{sr.Instance.Namespace}, + } + + dnsNames := []string{ + sr.Instance.Name, + util.NameWithSuffix(sr.Instance.Name, common.ArgoCDGRPCSuffix), + fmt.Sprintf("%s.%s.svc.cluster.local", sr.Instance.Name, sr.Instance.Namespace), + } + + cert, err := util.NewSignedCertificate(cfg, dnsNames, key, caCert, caKey) + if err != nil { + sr.Logger.Error(err, "getDesiredTLSCertSecret: failed to generate signed certificate") + return nil, err + } + + tlsSecretTmpl := sr.getDesiredSecretTmplObj(tlsCertName) + tlsSecretTmpl.Type = corev1.SecretTypeTLS + tlsSecretTmpl.Data = map[string][]byte{ + corev1.TLSCertKey: util.EncodeCertificatePEM(cert), + corev1.TLSPrivateKeyKey: util.EncodePrivateKeyPEM(key), + } + secretReq := sr.getSecretRequest(*tlsSecretTmpl) + tlsSecret, err := workloads.RequestSecret(secretReq) + if err != nil { + sr.Logger.Error(err, "getDesiredTLSCertSecret: failed to request secret", "name", tlsSecret, "namespace", sr.Instance.Namespace) + return nil, err + } + + return tlsSecret, nil +} + +// GetClusterSecrets receives a list of secrets carrying the Argo CD cluster-secret label +func (sr *SecretReconciler) GetClusterSecrets() (*corev1.SecretList, error) { + clusterSecrets := &corev1.SecretList{} + listOpts := make([]cntrlClient.ListOption, 0) + listOpts = append(listOpts, cntrlClient.MatchingLabelsSelector{ + Selector: labels.SelectorFromSet(map[string]string{ + common.ArgoCDArgoprojKeySecretType: ClusterSecretType, + }), + }) + + clusterSecrets, err := workloads.ListSecrets(sr.Instance.Namespace, sr.Client, listOpts) + if err != nil { + sr.Logger.Error(err, "GetClusterSecrets: failed to retrieve cluster secrets") + return nil, err + } + return clusterSecrets, nil +} + +// getSecretRequest returns a populated secret request object from the given secret template +func (sr *SecretReconciler) getSecretRequest(secretTmpl corev1.Secret) workloads.SecretRequest { + return workloads.SecretRequest{ + ObjectMeta: secretTmpl.ObjectMeta, + Data: secretTmpl.Data, + Type: secretTmpl.Type, + Client: sr.Client, + Mutations: []mutation.MutateFunc{mutation.ApplyReconcilerMutation}, + } +} + +func (sr *SecretReconciler) getDesiredSecretTmplObj(name string) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: sr.Instance.Namespace, + Labels: common.DefaultLabels(name, sr.Instance.Name, ""), + Annotations: sr.Instance.Annotations, + }, + } +} diff --git a/controllers/argocd/secret/secret_test.go b/controllers/argocd/secret/secret_test.go new file mode 100644 index 000000000..703e6dc03 --- /dev/null +++ b/controllers/argocd/secret/secret_test.go @@ -0,0 +1,332 @@ +package secret + +import ( + "reflect" + "sort" + "strings" + "testing" + + argoproj "github.com/argoproj-labs/argocd-operator/api/v1beta1" + "github.com/argoproj-labs/argocd-operator/common" + "github.com/argoproj-labs/argocd-operator/controllers/argocd/argocdcommon" + "github.com/argoproj-labs/argocd-operator/pkg/util" + "github.com/argoproj-labs/argocd-operator/pkg/workloads" + argopass "github.com/argoproj/argo-cd/v2/util/password" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +var ( + testName = "test-name" + testNamespace = "test-ns" + testKey = "testKey" + testVal = "testVal" + testKVP = map[string]string{ + testKey: testVal, + } +) + +type secretOpt func(*corev1.Secret) + +func makeTestSecretsReconciler(t *testing.T, objs ...runtime.Object) *SecretReconciler { + s := scheme.Scheme + assert.NoError(t, argoproj.AddToScheme(s)) + + cl := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build() + logger := ctrl.Log.WithName(SecretsControllerName) + + return &SecretReconciler{ + Client: cl, + Scheme: s, + Instance: argocdcommon.MakeTestArgoCD(), + Logger: logger, + } +} + +func Test_reconcileCredentialsSecret(t *testing.T) { + testSR := makeTestSecretsReconciler(t) + testSR.Instance = argocdcommon.MakeTestArgoCD(func(ac *argoproj.ArgoCD) { + ac.Name = testName + ac.Namespace = testNamespace + ac.Annotations = testKVP + }) + + err := testSR.reconcileCredentialsSecret() + assert.NoError(t, err) + + _, err = workloads.GetSecret("test-name-cluster", "test-ns", testSR.Client) + assert.NoError(t, err) + + err = workloads.DeleteSecret("test-name-cluster", "test-ns", testSR.Client) + assert.NoError(t, err) + + err = testSR.reconcileCredentialsSecret() + assert.NoError(t, err) + + _, err = workloads.GetSecret("test-name-cluster", "test-ns", testSR.Client) + assert.NoError(t, err) +} + +func Test_reconcileCASecret(t *testing.T) { + testSR := makeTestSecretsReconciler(t) + testSR.Instance = argocdcommon.MakeTestArgoCD(func(ac *argoproj.ArgoCD) { + ac.Name = testName + ac.Namespace = testNamespace + ac.Annotations = testKVP + }) + + err := testSR.reconcileCASecret() + assert.NoError(t, err) + + _, err = workloads.GetSecret("test-name-ca", "test-ns", testSR.Client) + assert.NoError(t, err) + + err = workloads.DeleteSecret("test-name-ca", "test-ns", testSR.Client) + assert.NoError(t, err) + + err = testSR.reconcileCASecret() + assert.NoError(t, err) + + existingCAsecret, err := workloads.GetSecret("test-name-ca", "test-ns", testSR.Client) + assert.NoError(t, err) + + expectedDataKeys := []string{ + corev1.ServiceAccountRootCAKey, + corev1.TLSCertKey, + corev1.TLSPrivateKeyKey, + } + + if actualDataKeys := util.ByteMapKeys(existingCAsecret.Data); !reflect.DeepEqual(expectedDataKeys, actualDataKeys) { + assert.Equal(t, expectedDataKeys, actualDataKeys) + } + +} + +func Test_reconcileTLSSecret(t *testing.T) { + testSR := makeTestSecretsReconciler(t) + testSR.Instance = argocdcommon.MakeTestArgoCD(func(ac *argoproj.ArgoCD) { + ac.Name = testName + ac.Namespace = testNamespace + ac.Annotations = testKVP + }) + + // expect not found error for missing CA secret + err := testSR.reconcileTLSSecret() + assert.Error(t, err) + + caSecret, _ := testSR.getDesiredCASecret("test-name-ca") + err = workloads.CreateSecret(caSecret, testSR.Client) + assert.NoError(t, err) + + err = testSR.reconcileTLSSecret() + assert.NoError(t, err) + + _, err = workloads.GetSecret("test-name-tls", "test-ns", testSR.Client) + assert.NoError(t, err) + + err = workloads.DeleteSecret("test-name-tls", "test-ns", testSR.Client) + assert.NoError(t, err) + + err = testSR.reconcileTLSSecret() + assert.NoError(t, err) + + _, err = workloads.GetSecret("test-name-tls", "test-ns", testSR.Client) + assert.NoError(t, err) +} + +func Test_reconcileClusterPermissionsSecret(t *testing.T) { + testSR := makeTestSecretsReconciler(t) + testSR.Instance = argocdcommon.MakeTestArgoCD(func(ac *argoproj.ArgoCD) { + ac.Name = testName + ac.Namespace = testNamespace + ac.Annotations = testKVP + }) + + // set instance to cluster scoped, verify empty managed namespace list + testSR.ClusterScoped = true + err := testSR.reconcileClusterPermissionsSecret() + assert.NoError(t, err) + + existingClusterPermSecret, err := workloads.GetSecret("test-name-default-cluster-config", "test-ns", testSR.Client) + assert.NoError(t, err) + + assert.Nil(t, existingClusterPermSecret.Data["namespaces"]) + + // update instance to namespace scoped, verify updated managed namespace list + testSR.ClusterScoped = false + testSR.ManagedNamespaces = map[string]string{ + "ns-1": "", + "ns-3": "", + "ns-5": "", + } + + err = testSR.reconcileClusterPermissionsSecret() + assert.NoError(t, err) + + existingClusterPermSecret, err = workloads.GetSecret("test-name-default-cluster-config", "test-ns", testSR.Client) + assert.NoError(t, err) + expectedNsList := []string{"ns-1", "ns-3", "ns-5"} + + assert.Equal(t, expectedNsList, strings.Split(string(existingClusterPermSecret.Data["namespaces"]), ",")) + + // update managed ns list + testSR.ManagedNamespaces = map[string]string{ + "ns-2": "", + "ns-4": "", + "ns-6": "", + } + + err = testSR.reconcileClusterPermissionsSecret() + assert.NoError(t, err) + + existingClusterPermSecret, err = workloads.GetSecret("test-name-default-cluster-config", "test-ns", testSR.Client) + assert.NoError(t, err) + expectedNsList = []string{"ns-2", "ns-4", "ns-6"} + + assert.Equal(t, expectedNsList, strings.Split(string(existingClusterPermSecret.Data["namespaces"]), ",")) + + // switch back to cluster scoped, verify empty managed ns list + testSR.ClusterScoped = true + err = testSR.reconcileClusterPermissionsSecret() + assert.NoError(t, err) + + existingClusterPermSecret, err = workloads.GetSecret("test-name-default-cluster-config", "test-ns", testSR.Client) + assert.NoError(t, err) + + assert.Nil(t, existingClusterPermSecret.Data["namespaces"]) +} + +func Test_reconcileArgoCDSecret(t *testing.T) { + testSR := makeTestSecretsReconciler(t) + testSR.Instance = argocdcommon.MakeTestArgoCD(func(ac *argoproj.ArgoCD) { + ac.Name = testName + ac.Namespace = testNamespace + ac.Annotations = testKVP + }) + + // expect not found error for missing secrets + err := testSR.reconcileArgoCDSecret() + assert.Error(t, err) + + err = testSR.reconcileCredentialsSecret() + assert.NoError(t, err) + + // expect not found error for missing secrets + err = testSR.reconcileArgoCDSecret() + assert.Error(t, err) + + err = testSR.reconcileCASecret() + assert.NoError(t, err) + + err = testSR.reconcileTLSSecret() + assert.NoError(t, err) + + err = testSR.reconcileArgoCDSecret() + assert.NoError(t, err) + + existingArgoCDSecret, err := workloads.GetSecret(ArgoCDSecretName, testNamespace, testSR.Client) + assert.NoError(t, err) + + existingCredsSecret, err := workloads.GetSecret("test-name-cluster", testNamespace, testSR.Client) + assert.NoError(t, err) + + // update existing secret data with new password and remove session key from argocd-secret, verify if argocd-secret is + // updated appropriately + existingCredsSecret.Data = map[string][]byte{ + common.ArgoCDKeyAdminPassword: []byte("new-pw"), + } + delete(existingArgoCDSecret.Data, common.ArgoCDKeyServerSecretKey) + + err = workloads.UpdateSecret(existingCredsSecret, testSR.Client) + assert.NoError(t, err) + err = workloads.UpdateSecret(existingArgoCDSecret, testSR.Client) + assert.NoError(t, err) + + err = testSR.reconcileArgoCDSecret() + assert.NoError(t, err) + + existingArgoCDSecret, err = workloads.GetSecret(ArgoCDSecretName, testNamespace, testSR.Client) + assert.NoError(t, err) + + pwdUnchanged, _ := argopass.VerifyPassword("new-pw", string(existingArgoCDSecret.Data["admin.password"])) + assert.True(t, pwdUnchanged) + + assert.NotNil(t, existingArgoCDSecret.Data[common.ArgoCDKeyServerSecretKey]) +} + +func Test_getClusterSecrets(t *testing.T) { + testSR := makeTestSecretsReconciler(t, + getTestSecret(func(s *corev1.Secret) { + s.Name = "secret-1" + s.Labels[common.ArgoCDArgoprojKeySecretType] = "cluster" + s.Namespace = testNamespace + }), + getTestSecret(func(s *corev1.Secret) { + s.Name = "secret-2" + s.Namespace = testNamespace + }), + getTestSecret(func(s *corev1.Secret) { + s.Name = "secret-3" + s.Labels[common.ArgoCDArgoprojKeySecretType] = "cluster" + s.Namespace = testNamespace + }), + ) + testSR.Instance = argocdcommon.MakeTestArgoCD(func(ac *argoproj.ArgoCD) { + ac.Name = testName + ac.Namespace = testNamespace + ac.Annotations = testKVP + }) + + expectedSecrets := []string{"secret-1", "secret-3"} + + actualSecretList, err := testSR.GetClusterSecrets() + assert.NoError(t, err) + + actualSecrets := []string{} + for _, secret := range actualSecretList.Items { + actualSecrets = append(actualSecrets, secret.Name) + } + sort.Strings(actualSecrets) + + assert.Equal(t, expectedSecrets, actualSecrets) +} + +func Test_getDesiredSecretTmplObj(t *testing.T) { + testSR := makeTestSecretsReconciler(t) + testSR.Instance = argocdcommon.MakeTestArgoCD(func(ac *argoproj.ArgoCD) { + ac.Name = testName + ac.Namespace = testNamespace + ac.Annotations = testKVP + }) + + expectedSecretTmpObj := getTestSecret(func(s *corev1.Secret) { + s.Annotations = testKVP + }) + actualSecretTmplObj := testSR.getDesiredSecretTmplObj(testName) + assert.Equal(t, expectedSecretTmpObj, actualSecretTmplObj) +} + +func getTestSecret(opts ...secretOpt) *corev1.Secret { + desiredSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + Namespace: "test-ns", + Labels: map[string]string{ + "app.kubernetes.io/name": "test-name", + "app.kubernetes.io/part-of": "argocd", + "app.kubernetes.io/instance": "test-name", + "app.kubernetes.io/managed-by": "argocd-operator", + }, + }, + } + + for _, opt := range opts { + opt(desiredSecret) + } + return desiredSecret +} diff --git a/controllers/argocd/secret/util.go b/controllers/argocd/secret/util.go new file mode 100644 index 000000000..891f31646 --- /dev/null +++ b/controllers/argocd/secret/util.go @@ -0,0 +1,32 @@ +package secret + +import ( + "github.com/argoproj-labs/argocd-operator/common" + argopass "github.com/argoproj/argo-cd/v2/util/password" + corev1 "k8s.io/api/core/v1" +) + +// ArgoAdminPasswordChanged will return true if the Argo admin password has changed. +func ArgoAdminPasswordChanged(actual *corev1.Secret, expected *corev1.Secret) bool { + actualPwd := string(actual.Data[common.ArgoCDKeyAdminPassword]) + expectedPwd := string(expected.Data[common.ArgoCDKeyAdminPassword]) + + validPwd, _ := argopass.VerifyPassword(expectedPwd, actualPwd) + if !validPwd { + return true + } + return false +} + +// ArgoTLSChanged will return true if the Argo TLS certificate or key have changed. +func ArgoTLSChanged(actual *corev1.Secret, expected *corev1.Secret) bool { + actualCert := string(actual.Data[corev1.TLSCertKey]) + actualKey := string(actual.Data[corev1.TLSPrivateKeyKey]) + expectedCert := string(expected.Data[corev1.TLSCertKey]) + expectedKey := string(expected.Data[corev1.TLSPrivateKeyKey]) + + if actualCert != expectedCert || actualKey != expectedKey { + return true + } + return false +} diff --git a/controllers/argocd/secret_test.go b/controllers/argocd/secret_test.go index e0cdf2890..0e9c00a9a 100644 --- a/controllers/argocd/secret_test.go +++ b/controllers/argocd/secret_test.go @@ -4,55 +4,17 @@ import ( "context" "crypto/sha256" "fmt" - "reflect" - "sort" "testing" - "github.com/stretchr/testify/assert" - corev1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" - logf "sigs.k8s.io/controller-runtime/pkg/log" argoproj "github.com/argoproj-labs/argocd-operator/api/v1beta1" - "github.com/argoproj-labs/argocd-operator/common" - "github.com/argoproj-labs/argocd-operator/pkg/util" ) -func Test_newCASecret(t *testing.T) { - cr := &argoproj.ArgoCD{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-argocd", - Namespace: "argocd", - }, - } - - s, err := newCASecret(cr) - if err != nil { - t.Fatal(err) - } - want := []string{ - corev1.ServiceAccountRootCAKey, - corev1.TLSCertKey, - corev1.TLSPrivateKeyKey, - } - if k := byteMapKeys(s.Data); !reflect.DeepEqual(want, k) { - t.Fatalf("got %#v, want %#v", k, want) - } -} - -func byteMapKeys(m map[string][]byte) []string { - r := []string{} - for k := range m { - r = append(r, k) - } - sort.Strings(r) - return r -} - func Test_ArgoCDReconciler_ReconcileRepoTLSSecret(t *testing.T) { argocd := &argoproj.ArgoCD{ ObjectMeta: metav1.ObjectMeta{ @@ -207,46 +169,6 @@ func Test_ArgoCDReconciler_ReconcileRepoTLSSecret(t *testing.T) { } -func Test_ArgoCDReconciler_ReconcileExistingArgoSecret(t *testing.T) { - argocd := &argoproj.ArgoCD{ - ObjectMeta: metav1.ObjectMeta{ - Name: "argocd", - Namespace: "argocd-operator", - }, - } - - clusterSecret := util.NewSecretWithSuffix(argocd, "cluster") - clusterSecret.Data = map[string][]byte{common.ArgoCDKeyAdminPassword: []byte("something")} - tlsSecret := util.NewSecretWithSuffix(argocd, "tls") - r := makeTestReconciler(t, argocd) - r.Client.Create(context.TODO(), clusterSecret) - r.Client.Create(context.TODO(), tlsSecret) - - err := r.reconcileArgoSecret(argocd) - - assert.NoError(t, err) - - testSecret := &corev1.Secret{} - secretErr := r.Client.Get(context.TODO(), types.NamespacedName{Name: "argocd-secret", Namespace: "argocd-operator"}, testSecret) - assert.NoError(t, secretErr) - - // if you remove the secret.Data it should come back, including the secretKey - testSecret.Data = nil - r.Client.Update(context.TODO(), testSecret) - - _ = r.reconcileExistingArgoSecret(argocd, testSecret, clusterSecret, tlsSecret) - _ = r.Client.Get(context.TODO(), types.NamespacedName{Name: "argocd-secret", Namespace: "argocd-operator"}, testSecret) - - if testSecret.Data == nil { - t.Errorf("Expected data for data.server but got nothing") - } - - if testSecret.Data[common.ArgoCDKeyServerSecretKey] == nil { - t.Errorf("Expected data for data.server.secretKey but got nothing") - } - -} - func Test_ArgoCDReconciler_ReconcileRedisTLSSecret(t *testing.T) { argocd := &argoproj.ArgoCD{ ObjectMeta: metav1.ObjectMeta{ @@ -417,44 +339,3 @@ func Test_ArgoCDReconciler_ReconcileRedisTLSSecret(t *testing.T) { } }) } - -func Test_ArgoCDReconciler_ClusterPermissionsSecret(t *testing.T) { - logf.SetLogger(ZapLogger(true)) - a := makeTestArgoCD() - r := makeTestReconciler(t, a) - assert.NoError(t, createNamespace(r, a.Namespace, "")) - - testSecret := util.NewSecretWithSuffix(a, "default-cluster-config") - //assert.ErrorContains(t, r.Client.Get(context.TODO(), types.NamespacedName{Name: testSecret.Name, Namespace: testSecret.Namespace}, testSecret), "not found") - //TODO: https://github.com/stretchr/testify/pull/1022 introduced ErrorContains, but is not yet available in a tagged release. Revert to ErrorContains once this becomes available - assert.Error(t, r.Client.Get(context.TODO(), types.NamespacedName{Name: testSecret.Name, Namespace: testSecret.Namespace}, testSecret)) - assert.Contains(t, r.Client.Get(context.TODO(), types.NamespacedName{Name: testSecret.Name, Namespace: testSecret.Namespace}, testSecret).Error(), "not found") - - assert.NoError(t, r.reconcileClusterPermissionsSecret(a)) - assert.NoError(t, r.Client.Get(context.TODO(), types.NamespacedName{Name: testSecret.Name, Namespace: testSecret.Namespace}, testSecret)) - assert.Equal(t, string(testSecret.Data["namespaces"]), a.Namespace) - - want := "argocd,someRandomNamespace" - testSecret.Data["namespaces"] = []byte("someRandomNamespace") - r.Client.Update(context.TODO(), testSecret) - - // reconcile to check namespace with the label gets added - assert.NoError(t, r.reconcileClusterPermissionsSecret(a)) - assert.NoError(t, r.Client.Get(context.TODO(), types.NamespacedName{Name: testSecret.Name, Namespace: testSecret.Namespace}, testSecret)) - assert.Equal(t, string(testSecret.Data["namespaces"]), want) - - assert.NoError(t, createNamespace(r, "xyz", a.Namespace)) - want = "argocd,someRandomNamespace,xyz" - // reconcile to check namespace with the label gets added - assert.NoError(t, r.reconcileClusterPermissionsSecret(a)) - assert.NoError(t, r.Client.Get(context.TODO(), types.NamespacedName{Name: testSecret.Name, Namespace: testSecret.Namespace}, testSecret)) - assert.Equal(t, string(testSecret.Data["namespaces"]), want) - - t.Setenv("ARGOCD_CLUSTER_CONFIG_NAMESPACES", a.Namespace) - - assert.NoError(t, r.reconcileClusterPermissionsSecret(a)) - //assert.ErrorContains(t, r.Client.Get(context.TODO(), types.NamespacedName{Name: testSecret.Name, Namespace: testSecret.Namespace}, testSecret), "not found") - //TODO: https://github.com/stretchr/testify/pull/1022 introduced ErrorContains, but is not yet available in a tagged release. Revert to ErrorContains once this becomes available - assert.NoError(t, r.Client.Get(context.TODO(), types.NamespacedName{Name: testSecret.Name, Namespace: testSecret.Namespace}, testSecret)) - assert.Nil(t, r.Client.Get(context.TODO(), types.NamespacedName{Name: testSecret.Name, Namespace: testSecret.Namespace}, testSecret)) -} diff --git a/controllers/argocd/statefulset.go b/controllers/argocd/statefulset.go index 4e90fbdab..7572bba7c 100644 --- a/controllers/argocd/statefulset.go +++ b/controllers/argocd/statefulset.go @@ -730,7 +730,7 @@ func (r *ArgoCDReconciler) triggerStatefulSetRollout(sts *appsv1.StatefulSet, ke return nil } - sts.Spec.Template.ObjectMeta.Labels[key] = nowNano() + sts.Spec.Template.ObjectMeta.Labels[key] = util.NowNano() return r.Client.Update(context.TODO(), sts) } diff --git a/controllers/argocd/util.go b/controllers/argocd/util.go index 59a326447..654ad13c8 100644 --- a/controllers/argocd/util.go +++ b/controllers/argocd/util.go @@ -35,7 +35,6 @@ import ( monitoringv1 "github.com/coreos/prometheus-operator/pkg/apis/monitoring/v1" oappsv1 "github.com/openshift/api/apps/v1" routev1 "github.com/openshift/api/route/v1" - "github.com/sethvargo/go-password/password" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" @@ -55,28 +54,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" ) -// generateArgoAdminPassword will generate and return the admin password for Argo CD. -func generateArgoAdminPassword() ([]byte, error) { - pass, err := password.Generate( - common.ArgoCDDefaultAdminPasswordLength, - common.ArgoCDDefaultAdminPasswordNumDigits, - common.ArgoCDDefaultAdminPasswordNumSymbols, - false, false) - - return []byte(pass), err -} - -// generateArgoServerKey will generate and return the server signature key for session validation. -func generateArgoServerSessionKey() ([]byte, error) { - pass, err := password.Generate( - common.ArgoCDDefaultServerSessionKeyLength, - common.ArgoCDDefaultServerSessionKeyNumDigits, - common.ArgoCDDefaultServerSessionKeyNumSymbols, - false, false) - - return []byte(pass), err -} - // getArgoApplicationControllerResources will return the ResourceRequirements for the Argo CD application controller container. func getArgoApplicationControllerResources(cr *argoproj.ArgoCD) corev1.ResourceRequirements { resources := corev1.ResourceRequirements{} diff --git a/pkg/networking/ingress_test.go b/pkg/networking/ingress_test.go index bf6e1b1d3..4ab3b771a 100644 --- a/pkg/networking/ingress_test.go +++ b/pkg/networking/ingress_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/argoproj-labs/argocd-operator/common" + "github.com/argoproj-labs/argocd-operator/controllers/argocd/secret" "github.com/argoproj-labs/argocd-operator/pkg/mutation" "github.com/argoproj-labs/argocd-operator/pkg/util" "github.com/openshift/client-go/apps/clientset/versioned/scheme" @@ -51,7 +52,7 @@ func getTestIngress(opts ...ingressOpt) *networkingv1.Ingress { Hosts: []string{ "test.host.com", }, - SecretName: common.ArgoCDSecretName, + SecretName: secret.ArgoCDSecretName, }, }, }, @@ -106,7 +107,7 @@ func TestRequestIngress(t *testing.T) { Hosts: []string{ "test.host.com", }, - SecretName: common.ArgoCDSecretName, + SecretName: secret.ArgoCDSecretName, }, }, }, @@ -146,7 +147,7 @@ func TestRequestIngress(t *testing.T) { Hosts: []string{ "test.host.com", }, - SecretName: common.ArgoCDSecretName, + SecretName: secret.ArgoCDSecretName, }, }, }, @@ -188,7 +189,7 @@ func TestRequestIngress(t *testing.T) { Hosts: []string{ "test.host.com", }, - SecretName: common.ArgoCDSecretName, + SecretName: secret.ArgoCDSecretName, }, }, }, @@ -230,7 +231,7 @@ func TestRequestIngress(t *testing.T) { Hosts: []string{ "test.host.com", }, - SecretName: common.ArgoCDSecretName, + SecretName: secret.ArgoCDSecretName, }, }, }, diff --git a/pkg/util/map.go b/pkg/util/map.go index 02fcd91f2..c151ab23c 100644 --- a/pkg/util/map.go +++ b/pkg/util/map.go @@ -1,5 +1,7 @@ package util +import "sort" + // combines 2 maps and returns the result. In case of conflicts, values in 2nd input overwrite values in 1st input func MergeMaps(a, b map[string]string) map[string]string { mergedMap := make(map[string]string, 0) @@ -26,3 +28,26 @@ func AppendStringMap(src map[string]string, add map[string]string) map[string]st } return res } + +// ConvertMapToSlices takes a map of string type as input and returns 2 separate slices +// containing keys and values +func ConvertMapToSlices(src map[string]string) ([]string, []string) { + keys := []string{} + values := []string{} + + for key, value := range src { + keys = append(keys, key) + values = append(values, value) + } + return keys, values +} + +// ByteMapKeys accepts a map containg string keys and []byte values, and returns a slice of the keys +func ByteMapKeys(m map[string][]byte) []string { + r := []string{} + for k := range m { + r = append(r, k) + } + sort.Strings(r) + return r +} diff --git a/pkg/util/time.go b/pkg/util/time.go new file mode 100644 index 000000000..0454f7014 --- /dev/null +++ b/pkg/util/time.go @@ -0,0 +1,16 @@ +package util + +import ( + "fmt" + "time" +) + +// nowBytes is a shortcut function to return the current date/time in RFC3339 format. +func NowBytes() []byte { + return []byte(time.Now().UTC().Format(time.RFC3339)) +} + +// nowNano returns a string with the current UTC time as epoch in nanoseconds +func NowNano() string { + return fmt.Sprintf("%d", time.Now().UTC().UnixNano()) +}