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 }