github.com/argoproj-labs/argocd-operator@v0.10.0/controllers/argocd/dex.go (about)

     1  package argocd
     2  
     3  import (
     4  	"context"
     5  	e "errors"
     6  	"fmt"
     7  	"reflect"
     8  	"strings"
     9  	"time"
    10  
    11  	"gopkg.in/yaml.v2"
    12  	"k8s.io/apimachinery/pkg/api/errors"
    13  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  
    15  	corev1 "k8s.io/api/core/v1"
    16  	rbacv1 "k8s.io/api/rbac/v1"
    17  	"k8s.io/apimachinery/pkg/util/intstr"
    18  	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    19  
    20  	argoproj "github.com/argoproj-labs/argocd-operator/api/v1beta1"
    21  	"github.com/argoproj-labs/argocd-operator/common"
    22  	"github.com/argoproj-labs/argocd-operator/controllers/argoutil"
    23  )
    24  
    25  // DexConnector represents an authentication connector for Dex.
    26  type DexConnector struct {
    27  	Config map[string]interface{} `yaml:"config,omitempty"`
    28  	ID     string                 `yaml:"id"`
    29  	Name   string                 `yaml:"name"`
    30  	Type   string                 `yaml:"type"`
    31  }
    32  
    33  // UseDex determines whether Dex resources should be created and configured or not
    34  func UseDex(cr *argoproj.ArgoCD) bool {
    35  	if cr.Spec.SSO != nil {
    36  		return cr.Spec.SSO.Provider.ToLower() == argoproj.SSOProviderTypeDex
    37  	}
    38  
    39  	return false
    40  }
    41  
    42  // getDexOAuthClientSecret will return the OAuth client secret for the given ArgoCD.
    43  func (r *ReconcileArgoCD) getDexOAuthClientSecret(cr *argoproj.ArgoCD) (*string, error) {
    44  	sa := newServiceAccountWithName(common.ArgoCDDefaultDexServiceAccountName, cr)
    45  	if err := argoutil.FetchObject(r.Client, cr.Namespace, sa.Name, sa); err != nil {
    46  		return nil, err
    47  	}
    48  
    49  	// Find the token secret
    50  	var tokenSecret *corev1.ObjectReference
    51  	for _, saSecret := range sa.Secrets {
    52  		if strings.Contains(saSecret.Name, "token") {
    53  			tokenSecret = &saSecret
    54  			break
    55  		}
    56  	}
    57  
    58  	if tokenSecret == nil {
    59  		// This change of creating secret for dex service account,is due to change of reduction of secret-based service account tokens in k8s v1.24 so from k8s v1.24 no default secret for service account is created, but for dex to work we need to provide token of secret used by dex service account as a oauth token, this change helps to achieve it, in long run we should see do dex really requires a secret or it manages to create one using TokenRequest API or may be change how dex is used or configured by operator
    60  		secret := &corev1.Secret{
    61  			ObjectMeta: metav1.ObjectMeta{
    62  				GenerateName: "argocd-dex-server-token-",
    63  				Namespace:    cr.Namespace,
    64  				Annotations: map[string]string{
    65  					corev1.ServiceAccountNameKey: sa.Name,
    66  				},
    67  			},
    68  			Type: corev1.SecretTypeServiceAccountToken,
    69  		}
    70  		err := r.Client.Create(context.TODO(), secret)
    71  		if err != nil {
    72  			return nil, e.New("unable to locate and create ServiceAccount token for OAuth client secret")
    73  		}
    74  		err = controllerutil.SetControllerReference(cr, secret, r.Scheme)
    75  		if err != nil {
    76  			return nil, err
    77  		}
    78  		tokenSecret = &corev1.ObjectReference{
    79  			Name:      secret.Name,
    80  			Namespace: cr.Namespace,
    81  		}
    82  		sa.Secrets = append(sa.Secrets, *tokenSecret)
    83  		err = r.Client.Update(context.TODO(), sa)
    84  		if err != nil {
    85  			return nil, e.New("failed to add ServiceAccount token for OAuth client secret")
    86  		}
    87  	}
    88  
    89  	// Fetch the secret to obtain the token
    90  	secret := argoutil.NewSecretWithName(cr, tokenSecret.Name)
    91  	if err := argoutil.FetchObject(r.Client, cr.Namespace, secret.Name, secret); err != nil {
    92  		return nil, err
    93  	}
    94  
    95  	token := string(secret.Data["token"])
    96  	return &token, nil
    97  }
    98  
    99  // reconcileDexConfiguration will ensure that Dex is configured properly.
   100  func (r *ReconcileArgoCD) reconcileDexConfiguration(cm *corev1.ConfigMap, cr *argoproj.ArgoCD) error {
   101  	actual := cm.Data[common.ArgoCDKeyDexConfig]
   102  	desired := getDexConfig(cr)
   103  
   104  	// Append the default OpenShift dex config if the openShiftOAuth is requested through `.spec.sso.dex`.
   105  	if cr.Spec.SSO != nil && cr.Spec.SSO.Dex != nil && cr.Spec.SSO.Dex.OpenShiftOAuth {
   106  		cfg, err := r.getOpenShiftDexConfig(cr)
   107  		if err != nil {
   108  			return err
   109  		}
   110  		desired = cfg
   111  	}
   112  
   113  	if actual != desired {
   114  		// Update ConfigMap with desired configuration.
   115  		cm.Data[common.ArgoCDKeyDexConfig] = desired
   116  		if err := r.Client.Update(context.TODO(), cm); err != nil {
   117  			return err
   118  		}
   119  
   120  		// Trigger rollout of Dex Deployment to pick up changes.
   121  		deploy := newDeploymentWithSuffix("dex-server", "dex-server", cr)
   122  		if !argoutil.IsObjectFound(r.Client, deploy.Namespace, deploy.Name, deploy) {
   123  			log.Info("unable to locate dex deployment")
   124  			return nil
   125  		}
   126  
   127  		deploy.Spec.Template.ObjectMeta.Labels["dex.config.changed"] = time.Now().UTC().Format("01022006-150406-MST")
   128  		return r.Client.Update(context.TODO(), deploy)
   129  	}
   130  	return nil
   131  }
   132  
   133  // getOpenShiftDexConfig will return the configuration for the Dex server running on OpenShift.
   134  func (r *ReconcileArgoCD) getOpenShiftDexConfig(cr *argoproj.ArgoCD) (string, error) {
   135  	groups := []string{}
   136  
   137  	// Allow override of groups from CR
   138  	if cr.Spec.SSO != nil && cr.Spec.SSO.Dex != nil && cr.Spec.SSO.Dex.Groups != nil {
   139  		groups = cr.Spec.SSO.Dex.Groups
   140  	}
   141  
   142  	connector := DexConnector{
   143  		Type: "openshift",
   144  		ID:   "openshift",
   145  		Name: "OpenShift",
   146  		Config: map[string]interface{}{
   147  			"issuer":       "https://kubernetes.default.svc", // TODO: Should this be hard-coded?
   148  			"clientID":     getDexOAuthClientID(cr),
   149  			"clientSecret": "$oidc.dex.clientSecret",
   150  			"redirectURI":  r.getDexOAuthRedirectURI(cr),
   151  			"insecureCA":   true, // TODO: Configure for openshift CA,
   152  			"groups":       groups,
   153  		},
   154  	}
   155  
   156  	connectors := make([]DexConnector, 0)
   157  	connectors = append(connectors, connector)
   158  
   159  	dex := make(map[string]interface{})
   160  	dex["connectors"] = connectors
   161  
   162  	// add dex config from the Argo CD CR.
   163  	if err := addDexConfigFromCR(cr, dex); err != nil {
   164  		return "", err
   165  	}
   166  
   167  	bytes, err := yaml.Marshal(dex)
   168  	return string(bytes), err
   169  }
   170  
   171  func addDexConfigFromCR(cr *argoproj.ArgoCD, dex map[string]interface{}) error {
   172  	dexCfgStr := getDexConfig(cr)
   173  	if dexCfgStr == "" {
   174  		return nil
   175  	}
   176  
   177  	dexCfg := make(map[string]interface{})
   178  	if err := yaml.Unmarshal([]byte(dexCfgStr), dexCfg); err != nil {
   179  		return err
   180  	}
   181  
   182  	for k, v := range dexCfg {
   183  		dex[k] = v
   184  	}
   185  
   186  	return nil
   187  }
   188  
   189  // reconcileDexServiceAccount will ensure that the Dex ServiceAccount is configured properly for OpenShift OAuth.
   190  func (r *ReconcileArgoCD) reconcileDexServiceAccount(cr *argoproj.ArgoCD) error {
   191  	// if openShiftOAuth set to false in `.spec.sso.dex`, no need to configure it
   192  	if cr.Spec.SSO == nil || cr.Spec.SSO.Dex == nil || !cr.Spec.SSO.Dex.OpenShiftOAuth {
   193  		return nil // OpenShift OAuth not enabled, move along...
   194  	}
   195  
   196  	log.Info("oauth enabled, configuring dex service account")
   197  	sa := newServiceAccountWithName(common.ArgoCDDefaultDexServiceAccountName, cr)
   198  	if err := argoutil.FetchObject(r.Client, cr.Namespace, sa.Name, sa); err != nil {
   199  		return err
   200  	}
   201  
   202  	// Get the OAuth redirect URI that should be used.
   203  	uri := r.getDexOAuthRedirectURI(cr)
   204  	log.Info(fmt.Sprintf("URI: %s", uri))
   205  
   206  	// Get the current redirect URI
   207  	ann := sa.ObjectMeta.Annotations
   208  	currentURI, found := ann[common.ArgoCDKeyDexOAuthRedirectURI]
   209  	if found && currentURI == uri {
   210  		return nil // Redirect URI annotation found and correct, move along...
   211  	}
   212  
   213  	log.Info(fmt.Sprintf("current URI: %s is not correct, should be: %s", currentURI, uri))
   214  	if len(ann) <= 0 {
   215  		ann = make(map[string]string)
   216  	}
   217  
   218  	ann[common.ArgoCDKeyDexOAuthRedirectURI] = uri
   219  	sa.ObjectMeta.Annotations = ann
   220  
   221  	return r.Client.Update(context.TODO(), sa)
   222  }
   223  
   224  // reconcileDexDeployment will ensure the Deployment resource is present for the ArgoCD Dex component.
   225  func (r *ReconcileArgoCD) reconcileDexDeployment(cr *argoproj.ArgoCD) error {
   226  	deploy := newDeploymentWithSuffix("dex-server", "dex-server", cr)
   227  
   228  	AddSeccompProfileForOpenShift(r.Client, &deploy.Spec.Template.Spec)
   229  
   230  	dexEnv := proxyEnvVars()
   231  	if cr.Spec.SSO != nil && cr.Spec.SSO.Dex != nil {
   232  		dexEnv = append(dexEnv, cr.Spec.SSO.Dex.Env...)
   233  	}
   234  
   235  	deploy.Spec.Template.Spec.Containers = []corev1.Container{{
   236  		Command: []string{
   237  			"/shared/argocd-dex",
   238  			"rundex",
   239  		},
   240  		Image: getDexContainerImage(cr),
   241  		Name:  "dex",
   242  		Env:   dexEnv,
   243  		LivenessProbe: &corev1.Probe{
   244  			ProbeHandler: corev1.ProbeHandler{
   245  				HTTPGet: &corev1.HTTPGetAction{
   246  					Path: "/healthz/live",
   247  					Port: intstr.FromInt(common.ArgoCDDefaultDexMetricsPort),
   248  				},
   249  			},
   250  			InitialDelaySeconds: 60,
   251  			PeriodSeconds:       30,
   252  		},
   253  		Ports: []corev1.ContainerPort{
   254  			{
   255  				ContainerPort: common.ArgoCDDefaultDexHTTPPort,
   256  				Name:          "http",
   257  			}, {
   258  				ContainerPort: common.ArgoCDDefaultDexGRPCPort,
   259  				Name:          "grpc",
   260  			}, {
   261  				ContainerPort: common.ArgoCDDefaultDexMetricsPort,
   262  				Name:          "metrics",
   263  			},
   264  		},
   265  		Resources: getDexResources(cr),
   266  		SecurityContext: &corev1.SecurityContext{
   267  			AllowPrivilegeEscalation: boolPtr(false),
   268  			Capabilities: &corev1.Capabilities{
   269  				Drop: []corev1.Capability{
   270  					"ALL",
   271  				},
   272  			},
   273  			RunAsNonRoot: boolPtr(true),
   274  		},
   275  		VolumeMounts: []corev1.VolumeMount{{
   276  			Name:      "static-files",
   277  			MountPath: "/shared",
   278  		}},
   279  	}}
   280  
   281  	deploy.Spec.Template.Spec.InitContainers = []corev1.Container{{
   282  		Command: []string{
   283  			"cp",
   284  			"-n",
   285  			"/usr/local/bin/argocd",
   286  			"/shared/argocd-dex",
   287  		},
   288  		Env:             proxyEnvVars(),
   289  		Image:           getArgoContainerImage(cr),
   290  		ImagePullPolicy: corev1.PullAlways,
   291  		Name:            "copyutil",
   292  		Resources:       getDexResources(cr),
   293  		SecurityContext: &corev1.SecurityContext{
   294  			AllowPrivilegeEscalation: boolPtr(false),
   295  			Capabilities: &corev1.Capabilities{
   296  				Drop: []corev1.Capability{
   297  					"ALL",
   298  				},
   299  			},
   300  			RunAsNonRoot: boolPtr(true),
   301  		},
   302  		VolumeMounts: []corev1.VolumeMount{{
   303  			Name:      "static-files",
   304  			MountPath: "/shared",
   305  		}},
   306  	}}
   307  
   308  	deploy.Spec.Template.Spec.ServiceAccountName = fmt.Sprintf("%s-%s", cr.Name, common.ArgoCDDefaultDexServiceAccountName)
   309  	deploy.Spec.Template.Spec.Volumes = []corev1.Volume{{
   310  		Name: "static-files",
   311  		VolumeSource: corev1.VolumeSource{
   312  			EmptyDir: &corev1.EmptyDirVolumeSource{},
   313  		},
   314  	}}
   315  
   316  	existing := newDeploymentWithSuffix("dex-server", "dex-server", cr)
   317  	if argoutil.IsObjectFound(r.Client, cr.Namespace, existing.Name, existing) {
   318  
   319  		// dex uninstallation requested
   320  		if !UseDex(cr) {
   321  			log.Info("deleting the existing dex deployment because dex uninstallation has been requested")
   322  			return r.Client.Delete(context.TODO(), existing)
   323  		}
   324  		changed := false
   325  
   326  		actualImage := existing.Spec.Template.Spec.Containers[0].Image
   327  		desiredImage := getDexContainerImage(cr)
   328  		if actualImage != desiredImage {
   329  			existing.Spec.Template.Spec.Containers[0].Image = desiredImage
   330  			existing.Spec.Template.ObjectMeta.Labels["image.upgraded"] = time.Now().UTC().Format("01022006-150406-MST")
   331  			changed = true
   332  		}
   333  
   334  		actualImage = existing.Spec.Template.Spec.InitContainers[0].Image
   335  		desiredImage = getArgoContainerImage(cr)
   336  		if actualImage != desiredImage {
   337  			existing.Spec.Template.Spec.InitContainers[0].Image = desiredImage
   338  			existing.Spec.Template.ObjectMeta.Labels["image.upgraded"] = time.Now().UTC().Format("01022006-150406-MST")
   339  			changed = true
   340  		}
   341  		updateNodePlacement(existing, deploy, &changed)
   342  		if !reflect.DeepEqual(existing.Spec.Template.Spec.Containers[0].Env,
   343  			deploy.Spec.Template.Spec.Containers[0].Env) {
   344  			existing.Spec.Template.Spec.Containers[0].Env = deploy.Spec.Template.Spec.Containers[0].Env
   345  			changed = true
   346  		}
   347  
   348  		if !reflect.DeepEqual(existing.Spec.Template.Spec.InitContainers[0].Env,
   349  			deploy.Spec.Template.Spec.InitContainers[0].Env) {
   350  			existing.Spec.Template.Spec.InitContainers[0].Env = deploy.Spec.Template.Spec.InitContainers[0].Env
   351  			changed = true
   352  		}
   353  
   354  		if !reflect.DeepEqual(deploy.Spec.Template.Spec.Containers[0].Resources, existing.Spec.Template.Spec.Containers[0].Resources) {
   355  			existing.Spec.Template.Spec.Containers[0].Resources = deploy.Spec.Template.Spec.Containers[0].Resources
   356  			changed = true
   357  		}
   358  
   359  		if changed {
   360  			return r.Client.Update(context.TODO(), existing)
   361  		}
   362  		return nil // Deployment found with nothing to do, move along...
   363  	}
   364  
   365  	// if Dex installation has not been requested, do nothing
   366  	if !UseDex(cr) {
   367  		return nil
   368  	}
   369  
   370  	if err := controllerutil.SetControllerReference(cr, deploy, r.Scheme); err != nil {
   371  		return err
   372  	}
   373  
   374  	log.Info(fmt.Sprintf("creating deployment %s for Argo CD instance %s in namespace %s", deploy.Name, cr.Name, cr.Namespace))
   375  	return r.Client.Create(context.TODO(), deploy)
   376  }
   377  
   378  // reconcileDexService will ensure that the Service for Dex is present.
   379  func (r *ReconcileArgoCD) reconcileDexService(cr *argoproj.ArgoCD) error {
   380  	svc := newServiceWithSuffix("dex-server", "dex-server", cr)
   381  	if argoutil.IsObjectFound(r.Client, cr.Namespace, svc.Name, svc) {
   382  
   383  		// dex uninstallation requested
   384  		if !UseDex(cr) {
   385  			log.Info("deleting the existing Dex service because dex uninstallation has been requested")
   386  			return r.Client.Delete(context.TODO(), svc)
   387  		}
   388  		return nil
   389  	}
   390  
   391  	// if Dex installation has not been requested, do nothing
   392  	if !UseDex(cr) {
   393  		return nil // Dex is disabled, do nothing
   394  	}
   395  
   396  	svc.Spec.Selector = map[string]string{
   397  		common.ArgoCDKeyName: nameWithSuffix("dex-server", cr),
   398  	}
   399  
   400  	svc.Spec.Ports = []corev1.ServicePort{
   401  		{
   402  			Name:       "http",
   403  			Port:       common.ArgoCDDefaultDexHTTPPort,
   404  			Protocol:   corev1.ProtocolTCP,
   405  			TargetPort: intstr.FromInt(common.ArgoCDDefaultDexHTTPPort),
   406  		}, {
   407  			Name:       "grpc",
   408  			Port:       common.ArgoCDDefaultDexGRPCPort,
   409  			Protocol:   corev1.ProtocolTCP,
   410  			TargetPort: intstr.FromInt(common.ArgoCDDefaultDexGRPCPort),
   411  		},
   412  	}
   413  
   414  	if err := controllerutil.SetControllerReference(cr, svc, r.Scheme); err != nil {
   415  		return err
   416  	}
   417  
   418  	log.Info(fmt.Sprintf("creating service %s for Argo CD instance %s in namespace %s", svc.Name, cr.Name, cr.Namespace))
   419  	return r.Client.Create(context.TODO(), svc)
   420  }
   421  
   422  // reconcileDexResources consolidates all dex resources reconciliation calls. It serves as the single place to trigger both creation
   423  // and deletion of dex resources based on the specified configuration of dex
   424  func (r *ReconcileArgoCD) reconcileDexResources(cr *argoproj.ArgoCD) error {
   425  	if _, err := r.reconcileRole(common.ArgoCDDexServerComponent, policyRuleForDexServer(), cr); err != nil {
   426  		log.Error(err, "error reconciling dex role")
   427  	}
   428  
   429  	if err := r.reconcileRoleBinding(common.ArgoCDDexServerComponent, policyRuleForDexServer(), cr); err != nil {
   430  		log.Error(err, "error reconciling dex rolebinding")
   431  	}
   432  
   433  	if err := r.reconcileServiceAccountPermissions(common.ArgoCDDexServerComponent, policyRuleForDexServer(), cr); err != nil {
   434  		return err
   435  	}
   436  
   437  	// specialized handling for dex
   438  	if err := r.reconcileDexServiceAccount(cr); err != nil {
   439  		log.Error(err, "error reconciling dex serviceaccount")
   440  	}
   441  
   442  	// Reconcile dex config in argocd-cm, create dex config in argocd-cm if required (right after dex is enabled)
   443  	if err := r.reconcileArgoConfigMap(cr); err != nil {
   444  		log.Error(err, "error reconciling argocd-cm configmap")
   445  	}
   446  
   447  	if err := r.reconcileDexService(cr); err != nil {
   448  		log.Error(err, "error reconciling dex service")
   449  	}
   450  
   451  	if err := r.reconcileDexDeployment(cr); err != nil {
   452  		log.Error(err, "error reconciling dex deployment")
   453  	}
   454  
   455  	if err := r.reconcileStatusSSO(cr); err != nil {
   456  		log.Error(err, "error reconciling dex status")
   457  	}
   458  
   459  	return nil
   460  }
   461  
   462  // The code to create/delete notifications resources is written within the reconciliation logic itself. However, these functions must be called
   463  // in the right order depending on whether resources are getting created or deleted. During creation we must create the role and sa first.
   464  // RoleBinding and deployment are dependent on these resouces. During deletion the order is reversed.
   465  // Deployment and RoleBinding must be deleted before the role and sa. deleteDexResources will only be called during
   466  // delete events, so we don't need to worry about duplicate, recurring reconciliation calls
   467  func (r *ReconcileArgoCD) deleteDexResources(cr *argoproj.ArgoCD) error {
   468  	sa := &corev1.ServiceAccount{}
   469  	role := &rbacv1.Role{}
   470  
   471  	if err := argoutil.FetchObject(r.Client, cr.Namespace, fmt.Sprintf("%s-%s", cr.Name, common.ArgoCDDexServerComponent), sa); err != nil {
   472  		if !errors.IsNotFound(err) {
   473  			return err
   474  		}
   475  	}
   476  	if err := argoutil.FetchObject(r.Client, cr.Namespace, fmt.Sprintf("%s-%s", cr.Name, common.ArgoCDDexServerComponent), role); err != nil {
   477  		if !errors.IsNotFound(err) {
   478  			return err
   479  		}
   480  	}
   481  
   482  	if err := r.reconcileDexDeployment(cr); err != nil {
   483  		log.Error(err, "error reconciling dex deployment")
   484  	}
   485  
   486  	if err := r.reconcileDexService(cr); err != nil {
   487  		log.Error(err, "error reconciling dex service")
   488  	}
   489  
   490  	// Reconcile dex config in argocd-cm (right after dex is disabled)
   491  	// this is required for a one time trigger of reconcileDexConfiguration directly in case of a dex deletion event,
   492  	// since reconcileArgoConfigMap won't call reconcileDexConfiguration once dex has been disabled (to avoid reconciling on
   493  	// dexconfig unnecessarily when it isn't enabled)
   494  	cm := newConfigMapWithName(common.ArgoCDConfigMapName, cr)
   495  	if argoutil.IsObjectFound(r.Client, cr.Namespace, cm.Name, cm) {
   496  		if err := r.reconcileDexConfiguration(cm, cr); err != nil {
   497  			log.Error(err, "error reconciling dex configuration in configmap")
   498  		}
   499  	}
   500  
   501  	if err := r.reconcileRoleBinding(common.ArgoCDDexServerComponent, policyRuleForDexServer(), cr); err != nil {
   502  		log.Error(err, "error reconciling dex rolebinding")
   503  	}
   504  
   505  	if err := r.reconcileStatusSSO(cr); err != nil {
   506  		log.Error(err, "error reconciling dex status")
   507  	}
   508  
   509  	return nil
   510  }