github.com/argoproj-labs/argocd-operator@v0.10.0/controllers/argocd/keycloak.go (about) 1 // Copyright 2021 ArgoCD Operator Developers 2 // Copyright 2021 ArgoCD Operator Developers 3 // 4 // Licensed under the Apache License, Version 2.0 (the "License"); 5 // you may not use this file except in compliance with the License. 6 // You may obtain a copy of the License at 7 // 8 // http://www.apache.org/licenses/LICENSE-2.0 9 // 10 // Unless required by applicable law or agreed to in writing, software 11 // distributed under the License is distributed on an "AS IS" BASIS, 12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 // See the License for the specific language governing permissions and 14 // limitations under the License. 15 16 package argocd 17 18 import ( 19 "context" 20 b64 "encoding/base64" 21 json "encoding/json" 22 "fmt" 23 "os" 24 25 argoproj "github.com/argoproj-labs/argocd-operator/api/v1beta1" 26 "github.com/argoproj-labs/argocd-operator/common" 27 "github.com/argoproj-labs/argocd-operator/controllers/argoutil" 28 29 appsv1 "github.com/openshift/api/apps/v1" 30 31 oauthv1 "github.com/openshift/api/oauth/v1" 32 routev1 "github.com/openshift/api/route/v1" 33 template "github.com/openshift/api/template/v1" 34 oappsv1client "github.com/openshift/client-go/apps/clientset/versioned/typed/apps/v1" 35 oauthclient "github.com/openshift/client-go/oauth/clientset/versioned/typed/oauth/v1" 36 templatev1client "github.com/openshift/client-go/template/clientset/versioned/typed/template/v1" 37 "gopkg.in/yaml.v2" 38 k8sappsv1 "k8s.io/api/apps/v1" 39 corev1 "k8s.io/api/core/v1" 40 networkingv1 "k8s.io/api/networking/v1" 41 "k8s.io/apimachinery/pkg/api/errors" 42 resourcev1 "k8s.io/apimachinery/pkg/api/resource" 43 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 44 "k8s.io/apimachinery/pkg/runtime" 45 "k8s.io/apimachinery/pkg/types" 46 "k8s.io/apimachinery/pkg/util/intstr" 47 "k8s.io/client-go/kubernetes" 48 "k8s.io/client-go/util/retry" 49 "sigs.k8s.io/controller-runtime/pkg/client/config" 50 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 51 ) 52 53 const ( 54 // SuccessResonse is returned when a realm is created in keycloak. 55 successResponse = "201 Created" 56 // ExpectedReplicas is used to identify the keycloak running status. 57 expectedReplicas int32 = 1 58 // ServingCertSecretName is a secret that holds the service certificate. 59 servingCertSecretName = "sso-x509-https-secret" 60 // Authentication api path for keycloak. 61 authURL = "/auth/realms/master/protocol/openid-connect/token" 62 // Realm api path for keycloak. 63 realmURL = "/auth/admin/realms" 64 // Keycloak client for Argo CD. 65 keycloakClient = "argocd" 66 // Keycloak realm for Argo CD. 67 keycloakRealm = "argocd" 68 // Identifier for Keycloak. 69 defaultKeycloakIdentifier = "keycloak" 70 // Identifier for TemplateInstance and Template. 71 defaultTemplateIdentifier = "rhsso" 72 // Default name for Keycloak broker. 73 defaultKeycloakBrokerName = "keycloak-broker" 74 // Default Keycloak Instance Admin user. 75 defaultKeycloakAdminUser = "admin" 76 // Default Keycloak Instance Admin password. 77 defaultKeycloakAdminPassword = "admin" 78 // Default Hostname for Keycloak Ingress. 79 keycloakIngressHost = "keycloak-ingress" 80 ) 81 82 var ( 83 // client secret for keycloak, argocd and openshift-v4 IdP. 84 oAuthClientSecret = generateRandomString(8) 85 graceTime int64 = 75 86 portTLS int32 = 8443 87 httpPort int32 = 8080 88 controllerRef bool = true 89 ) 90 91 // getKeycloakContainerImage will return the container image for the Keycloak. 92 // 93 // There are three possible options for configuring the image, and this is the 94 // order of preference. 95 // 96 // 1. from the Spec, the spec.sso.keycloak field has an image and version to use for 97 // generating an image reference. 98 // 2. From the Environment, this looks for the `ARGOCD_KEYCLOAK_IMAGE` field and uses 99 // that if the spec is not configured. 100 // 3. the default is configured in common.ArgoCDKeycloakVersion and 101 // common.ArgoCDKeycloakImageName. 102 func getKeycloakContainerImage(cr *argoproj.ArgoCD) string { 103 defaultImg, defaultTag := false, false 104 105 img := "" 106 tag := "" 107 108 if cr.Spec.SSO.Keycloak != nil && cr.Spec.SSO.Keycloak.Image != "" { 109 img = cr.Spec.SSO.Keycloak.Image 110 } 111 112 if img == "" { 113 img = common.ArgoCDKeycloakImage 114 if IsTemplateAPIAvailable() { 115 img = common.ArgoCDKeycloakImageForOpenShift 116 } 117 defaultImg = true 118 } 119 120 if cr.Spec.SSO.Keycloak != nil && cr.Spec.SSO.Keycloak.Version != "" { 121 tag = cr.Spec.SSO.Keycloak.Version 122 } 123 124 if tag == "" { 125 tag = common.ArgoCDKeycloakVersion 126 if IsTemplateAPIAvailable() { 127 tag = common.ArgoCDKeycloakVersionForOpenShift 128 } 129 defaultTag = true 130 } 131 if e := os.Getenv(common.ArgoCDKeycloakImageEnvName); e != "" && (defaultTag && defaultImg) { 132 return e 133 } 134 return argoutil.CombineImageTag(img, tag) 135 } 136 137 func getKeycloakConfigMapTemplate(ns string) *corev1.ConfigMap { 138 return &corev1.ConfigMap{ 139 ObjectMeta: metav1.ObjectMeta{ 140 Annotations: map[string]string{ 141 "description": "ConfigMap providing service ca bundle", 142 "service.beta.openshift.io/inject-cabundle": "true", 143 }, 144 Labels: map[string]string{ 145 "application": "${APPLICATION_NAME}", 146 }, 147 Name: "${APPLICATION_NAME}-service-ca", 148 Namespace: ns, 149 }, 150 TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "ConfigMap"}, 151 } 152 } 153 154 func getKeycloakSecretTemplate(ns string) *corev1.Secret { 155 return &corev1.Secret{ 156 ObjectMeta: metav1.ObjectMeta{ 157 Labels: map[string]string{ 158 "application": "${APPLICATION_NAME}", 159 }, 160 Name: "${APPLICATION_NAME}-secret", 161 Namespace: ns, 162 }, 163 TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Secret"}, 164 StringData: map[string]string{ 165 "SSO_USERNAME": "${SSO_ADMIN_USERNAME}", 166 "SSO_PASSWORD": "${SSO_ADMIN_PASSWORD}", 167 }, 168 } 169 } 170 171 // defaultKeycloakResources for Keycloak container. 172 func defaultKeycloakResources() corev1.ResourceRequirements { 173 return corev1.ResourceRequirements{ 174 Requests: corev1.ResourceList{ 175 corev1.ResourceMemory: resourcev1.MustParse("512Mi"), 176 corev1.ResourceCPU: resourcev1.MustParse("500m"), 177 }, 178 Limits: corev1.ResourceList{ 179 corev1.ResourceMemory: resourcev1.MustParse("1024Mi"), 180 corev1.ResourceCPU: resourcev1.MustParse("1000m"), 181 }, 182 } 183 } 184 185 // getKeycloakResources will return the ResourceRequirements for the Keycloak container. 186 func getKeycloakResources(cr *argoproj.ArgoCD) corev1.ResourceRequirements { 187 188 // Default values for Keycloak resources requirements. 189 resources := defaultKeycloakResources() 190 191 // Allow override of resource requirements from CR 192 if cr.Spec.SSO.Keycloak != nil && cr.Spec.SSO.Keycloak.Resources != nil { 193 resources = *cr.Spec.SSO.Keycloak.Resources 194 } 195 196 return resources 197 } 198 199 func getKeycloakContainer(cr *argoproj.ArgoCD) corev1.Container { 200 envVars := []corev1.EnvVar{ 201 {Name: "SSO_HOSTNAME", Value: "${SSO_HOSTNAME}"}, 202 {Name: "DB_MIN_POOL_SIZE", Value: "${DB_MIN_POOL_SIZE}"}, 203 {Name: "DB_MAX_POOL_SIZE", Value: "${DB_MAX_POOL_SIZE}"}, 204 {Name: "DB_TX_ISOLATION", Value: "${DB_TX_ISOLATION}"}, 205 {Name: "OPENSHIFT_DNS_PING_SERVICE_NAME", Value: "${APPLICATION_NAME}-ping"}, 206 {Name: "OPENSHIFT_DNS_PING_SERVICE_PORT", Value: "8888"}, 207 {Name: "X509_CA_BUNDLE", Value: "/var/run/configmaps/service-ca/service-ca.crt /var/run/secrets/kubernetes.io/serviceaccount/*.crt"}, 208 {Name: "SSO_ADMIN_USERNAME", Value: "${SSO_ADMIN_USERNAME}"}, 209 {Name: "SSO_ADMIN_PASSWORD", Value: "${SSO_ADMIN_PASSWORD}"}, 210 {Name: "SSO_REALM", Value: "${SSO_REALM}"}, 211 {Name: "SSO_SERVICE_USERNAME", Value: "${SSO_SERVICE_USERNAME}"}, 212 {Name: "SSO_SERVICE_PASSWORD", Value: "${SSO_SERVICE_PASSWORD}"}, 213 } 214 215 return corev1.Container{ 216 Env: proxyEnvVars(envVars...), 217 Image: getKeycloakContainerImage(cr), 218 ImagePullPolicy: "Always", 219 LivenessProbe: &corev1.Probe{ 220 TimeoutSeconds: 240, 221 ProbeHandler: corev1.ProbeHandler{ 222 Exec: &corev1.ExecAction{ 223 Command: []string{ 224 "/bin/bash", 225 "-c", 226 "/opt/eap/bin/livenessProbe.sh", 227 }, 228 }, 229 }, 230 InitialDelaySeconds: 120, 231 }, 232 Name: "${APPLICATION_NAME}", 233 Ports: []corev1.ContainerPort{ 234 {ContainerPort: 8778, Name: "jolokia", Protocol: "TCP"}, 235 {ContainerPort: 8080, Name: "http", Protocol: "TCP"}, 236 {ContainerPort: 8443, Name: "https", Protocol: "TCP"}, 237 {ContainerPort: 8888, Name: "ping", Protocol: "TCP"}, 238 }, 239 ReadinessProbe: &corev1.Probe{ 240 TimeoutSeconds: 240, 241 InitialDelaySeconds: 120, 242 ProbeHandler: corev1.ProbeHandler{ 243 Exec: &corev1.ExecAction{ 244 Command: []string{ 245 "/bin/bash", 246 "-c", 247 "/opt/eap/bin/readinessProbe.sh", 248 }, 249 }, 250 }, 251 }, 252 Resources: getKeycloakResources(cr), 253 VolumeMounts: []corev1.VolumeMount{ 254 { 255 MountPath: "/etc/x509/https", 256 Name: "sso-x509-https-volume", 257 ReadOnly: true, 258 }, 259 { 260 MountPath: "/var/run/configmaps/service-ca", 261 Name: "service-ca", 262 ReadOnly: true, 263 }, 264 { 265 Name: "sso-probe-netrc-volume", 266 MountPath: "/mnt/rh-sso", 267 ReadOnly: false, 268 }, 269 }, 270 } 271 } 272 273 func getKeycloakDeploymentConfigTemplate(cr *argoproj.ArgoCD) *appsv1.DeploymentConfig { 274 ns := cr.Namespace 275 var medium corev1.StorageMedium = "Memory" 276 keycloakContainer := getKeycloakContainer(cr) 277 278 dc := &appsv1.DeploymentConfig{ 279 ObjectMeta: metav1.ObjectMeta{ 280 Annotations: map[string]string{ 281 "argocd.argoproj.io/realm-created": "false", 282 }, 283 Labels: map[string]string{"application": "${APPLICATION_NAME}"}, 284 Name: "${APPLICATION_NAME}", 285 Namespace: ns, 286 OwnerReferences: []metav1.OwnerReference{ 287 { 288 APIVersion: "argoproj.io/v1alpha1", 289 UID: cr.UID, 290 Name: cr.Name, 291 Controller: &controllerRef, 292 Kind: "ArgoCD", 293 }, 294 }, 295 }, 296 TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "DeploymentConfig"}, 297 Spec: appsv1.DeploymentConfigSpec{ 298 Replicas: 1, 299 Selector: map[string]string{"deploymentConfig": "${APPLICATION_NAME}"}, 300 Strategy: appsv1.DeploymentStrategy{ 301 Type: "Recreate", 302 Resources: corev1.ResourceRequirements{ 303 Requests: corev1.ResourceList{ 304 corev1.ResourceMemory: resourcev1.MustParse("256Mi"), 305 corev1.ResourceCPU: resourcev1.MustParse("250m"), 306 }, 307 Limits: corev1.ResourceList{ 308 corev1.ResourceMemory: resourcev1.MustParse("512Mi"), 309 corev1.ResourceCPU: resourcev1.MustParse("500m"), 310 }, 311 }, 312 }, 313 Template: &corev1.PodTemplateSpec{ 314 ObjectMeta: metav1.ObjectMeta{ 315 Labels: map[string]string{ 316 "application": "${APPLICATION_NAME}", 317 "deploymentConfig": "${APPLICATION_NAME}", 318 }, 319 Name: "${APPLICATION_NAME}", 320 }, 321 Spec: corev1.PodSpec{ 322 Containers: []corev1.Container{ 323 keycloakContainer, 324 }, 325 TerminationGracePeriodSeconds: &graceTime, 326 Volumes: []corev1.Volume{ 327 { 328 Name: "sso-x509-https-volume", 329 VolumeSource: corev1.VolumeSource{ 330 Secret: &corev1.SecretVolumeSource{ 331 SecretName: servingCertSecretName, 332 }, 333 }, 334 }, 335 { 336 Name: "service-ca", 337 VolumeSource: corev1.VolumeSource{ 338 ConfigMap: &corev1.ConfigMapVolumeSource{ 339 LocalObjectReference: corev1.LocalObjectReference{ 340 Name: "${APPLICATION_NAME}-service-ca", 341 }, 342 }, 343 }, 344 }, 345 { 346 Name: "sso-probe-netrc-volume", 347 VolumeSource: corev1.VolumeSource{ 348 EmptyDir: &corev1.EmptyDirVolumeSource{ 349 Medium: medium, 350 }, 351 }, 352 }, 353 }, 354 NodeSelector: common.DefaultNodeSelector(), 355 }, 356 }, 357 Triggers: appsv1.DeploymentTriggerPolicies{ 358 appsv1.DeploymentTriggerPolicy{ 359 Type: "ConfigChange", 360 }, 361 }, 362 }, 363 } 364 365 if cr.Spec.NodePlacement != nil { 366 dc.Spec.Template.Spec.NodeSelector = argoutil.AppendStringMap(dc.Spec.Template.Spec.NodeSelector, cr.Spec.NodePlacement.NodeSelector) 367 dc.Spec.Template.Spec.Tolerations = cr.Spec.NodePlacement.Tolerations 368 } 369 370 return dc 371 372 } 373 374 func getKeycloakServiceTemplate(ns string) *corev1.Service { 375 return &corev1.Service{ 376 ObjectMeta: metav1.ObjectMeta{ 377 Labels: map[string]string{"application": "${APPLICATION_NAME}"}, 378 Name: "${APPLICATION_NAME}", 379 Namespace: ns, 380 Annotations: map[string]string{ 381 "description": "The web server's https port", 382 "service.alpha.openshift.io/serving-cert-secret-name": servingCertSecretName, 383 }, 384 }, 385 TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Service"}, 386 Spec: corev1.ServiceSpec{ 387 Ports: []corev1.ServicePort{ 388 {Port: portTLS, TargetPort: intstr.FromInt(int(portTLS))}, 389 }, 390 Selector: map[string]string{ 391 "deploymentConfig": "${APPLICATION_NAME}", 392 }, 393 }, 394 } 395 } 396 397 func getKeycloakRouteTemplate(ns string) *routev1.Route { 398 return &routev1.Route{ 399 ObjectMeta: metav1.ObjectMeta{ 400 Labels: map[string]string{"application": "${APPLICATION_NAME}"}, 401 Name: "${APPLICATION_NAME}", 402 Namespace: ns, 403 Annotations: map[string]string{"description": "Route for application's https service"}, 404 }, 405 TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Route"}, 406 Spec: routev1.RouteSpec{ 407 TLS: &routev1.TLSConfig{ 408 Termination: "reencrypt", 409 }, 410 To: routev1.RouteTargetReference{ 411 Name: "${APPLICATION_NAME}", 412 }, 413 }, 414 } 415 } 416 417 func newKeycloakTemplateInstance(cr *argoproj.ArgoCD) (*template.TemplateInstance, error) { 418 tpl, err := newKeycloakTemplate(cr) 419 if err != nil { 420 return nil, err 421 } 422 return &template.TemplateInstance{ 423 ObjectMeta: metav1.ObjectMeta{ 424 Name: defaultTemplateIdentifier, 425 Namespace: cr.Namespace, 426 }, 427 Spec: template.TemplateInstanceSpec{ 428 Template: tpl, 429 }, 430 }, nil 431 } 432 433 func newKeycloakTemplate(cr *argoproj.ArgoCD) (template.Template, error) { 434 ns := cr.Namespace 435 tmpl := template.Template{} 436 configMapTemplate := getKeycloakConfigMapTemplate(ns) 437 secretTemplate := getKeycloakSecretTemplate(ns) 438 deploymentConfigTemplate := getKeycloakDeploymentConfigTemplate(cr) 439 serviceTemplate := getKeycloakServiceTemplate(ns) 440 routeTemplate := getKeycloakRouteTemplate(ns) 441 442 configMap, err := json.Marshal(configMapTemplate) 443 if err != nil { 444 return tmpl, err 445 } 446 447 secret, err := json.Marshal(secretTemplate) 448 if err != nil { 449 return tmpl, err 450 } 451 452 deploymentConfig, err := json.Marshal(deploymentConfigTemplate) 453 if err != nil { 454 return tmpl, err 455 } 456 457 service, err := json.Marshal(serviceTemplate) 458 if err != nil { 459 return tmpl, err 460 } 461 462 route, err := json.Marshal(routeTemplate) 463 if err != nil { 464 return tmpl, err 465 } 466 467 tmpl = template.Template{ 468 ObjectMeta: metav1.ObjectMeta{ 469 Annotations: map[string]string{ 470 "description": "RH-SSO Template for Installing keycloak", 471 "iconClass": "icon-sso", 472 "openshift.io/display-name": "Keycloak", 473 "tags": "keycloak", 474 "version": "9.0.4-SNAPSHOT", 475 }, 476 Name: defaultTemplateIdentifier, 477 Namespace: ns, 478 }, 479 Objects: []runtime.RawExtension{ 480 { 481 Raw: json.RawMessage(configMap), 482 }, 483 { 484 Raw: json.RawMessage(secret), 485 }, 486 { 487 Raw: json.RawMessage(deploymentConfig), 488 }, 489 { 490 Raw: json.RawMessage(service), 491 }, 492 { 493 Raw: json.RawMessage(route), 494 }, 495 }, 496 Parameters: []template.Parameter{ 497 {Name: "APPLICATION_NAME", Value: "keycloak", Required: true}, 498 {Name: "SSO_HOSTNAME"}, 499 {Name: "DB_MIN_POOL_SIZE"}, 500 {Name: "DB_MAX_POOL_SIZE"}, 501 {Name: "DB_TX_ISOLATION"}, 502 {Name: "IMAGE_STREAM_NAMESPACE", Value: "openshift", Required: true}, 503 {Name: "SSO_ADMIN_USERNAME", Generate: "expression", From: "[a-zA-Z0-9]{8}", Required: true}, 504 {Name: "SSO_ADMIN_PASSWORD", Generate: "expression", From: "[a-zA-Z0-9]{8}", Required: true}, 505 {Name: "SSO_REALM", DisplayName: "RH-SSO Realm"}, 506 {Name: "SSO_SERVICE_USERNAME", DisplayName: "RH-SSO Service Username"}, 507 {Name: "SSO_SERVICE_PASSWORD", DisplayName: "RH-SSO Service Password"}, 508 {Name: "MEMORY_LIMIT", Value: "1Gi"}, 509 }, 510 } 511 return tmpl, err 512 } 513 514 func newKeycloakIngress(cr *argoproj.ArgoCD) *networkingv1.Ingress { 515 516 pathType := networkingv1.PathTypeImplementationSpecific 517 518 // Add default annotations 519 atns := make(map[string]string) 520 atns[common.ArgoCDKeyIngressSSLRedirect] = "true" 521 atns[common.ArgoCDKeyIngressBackendProtocol] = "HTTP" 522 523 return &networkingv1.Ingress{ 524 ObjectMeta: metav1.ObjectMeta{ 525 Annotations: atns, 526 Name: defaultKeycloakIdentifier, 527 Namespace: cr.Namespace, 528 }, 529 Spec: networkingv1.IngressSpec{ 530 TLS: []networkingv1.IngressTLS{ 531 { 532 Hosts: []string{keycloakIngressHost}, 533 }, 534 }, 535 Rules: []networkingv1.IngressRule{ 536 { 537 Host: keycloakIngressHost, 538 IngressRuleValue: networkingv1.IngressRuleValue{ 539 HTTP: &networkingv1.HTTPIngressRuleValue{ 540 Paths: []networkingv1.HTTPIngressPath{ 541 { 542 Path: "/", 543 Backend: networkingv1.IngressBackend{ 544 Service: &networkingv1.IngressServiceBackend{ 545 Name: defaultKeycloakIdentifier, 546 Port: networkingv1.ServiceBackendPort{ 547 Name: "http", 548 }, 549 }, 550 }, 551 PathType: &pathType, 552 }, 553 }, 554 }, 555 }, 556 }, 557 }, 558 }, 559 } 560 } 561 562 func newKeycloakService(cr *argoproj.ArgoCD) *corev1.Service { 563 564 return &corev1.Service{ 565 ObjectMeta: metav1.ObjectMeta{ 566 Name: defaultKeycloakIdentifier, 567 Namespace: cr.Namespace, 568 Labels: map[string]string{ 569 "app": defaultKeycloakIdentifier, 570 }, 571 }, 572 Spec: corev1.ServiceSpec{ 573 Ports: []corev1.ServicePort{ 574 {Name: "http", Port: httpPort, TargetPort: intstr.FromInt(int(httpPort))}, 575 }, 576 Selector: map[string]string{ 577 "app": defaultKeycloakIdentifier, 578 }, 579 Type: "LoadBalancer", 580 }, 581 } 582 } 583 584 func getKeycloakContainerEnv() []corev1.EnvVar { 585 return []corev1.EnvVar{ 586 {Name: "KEYCLOAK_USER", Value: defaultKeycloakAdminUser}, 587 {Name: "KEYCLOAK_PASSWORD", Value: defaultKeycloakAdminPassword}, 588 {Name: "PROXY_ADDRESS_FORWARDING", Value: "true"}, 589 } 590 } 591 592 func newKeycloakDeployment(cr *argoproj.ArgoCD) *k8sappsv1.Deployment { 593 594 var replicas int32 = 1 595 return &k8sappsv1.Deployment{ 596 ObjectMeta: metav1.ObjectMeta{ 597 Name: defaultKeycloakIdentifier, 598 Namespace: cr.Namespace, 599 Annotations: map[string]string{ 600 "argocd.argoproj.io/realm-created": "false", 601 }, 602 Labels: map[string]string{ 603 "app": defaultKeycloakIdentifier, 604 }, 605 }, 606 Spec: k8sappsv1.DeploymentSpec{ 607 Replicas: &replicas, 608 Selector: &metav1.LabelSelector{ 609 MatchLabels: map[string]string{ 610 "app": defaultKeycloakIdentifier, 611 }, 612 }, 613 Template: corev1.PodTemplateSpec{ 614 ObjectMeta: metav1.ObjectMeta{ 615 Labels: map[string]string{ 616 "app": defaultKeycloakIdentifier, 617 }, 618 }, 619 Spec: corev1.PodSpec{ 620 Containers: []corev1.Container{ 621 { 622 Name: defaultKeycloakIdentifier, 623 Image: getKeycloakContainerImage(cr), 624 Env: proxyEnvVars(getKeycloakContainerEnv()...), 625 Ports: []corev1.ContainerPort{ 626 {Name: "http", ContainerPort: httpPort}, 627 {Name: "https", ContainerPort: portTLS}, 628 }, 629 ReadinessProbe: &corev1.Probe{ 630 ProbeHandler: corev1.ProbeHandler{ 631 HTTPGet: &corev1.HTTPGetAction{ 632 Path: "/auth/realms/master", 633 Port: intstr.FromInt(int(httpPort)), 634 }, 635 }, 636 }, 637 }, 638 }, 639 }, 640 }, 641 }, 642 } 643 } 644 645 func (r *ReconcileArgoCD) newKeycloakInstance(cr *argoproj.ArgoCD) error { 646 647 // Create Keycloak Ingress 648 ing := newKeycloakIngress(cr) 649 err := r.Client.Get(context.TODO(), types.NamespacedName{Name: ing.Name, 650 Namespace: ing.Namespace}, ing) 651 652 if err != nil { 653 if errors.IsNotFound(err) { 654 if err := controllerutil.SetControllerReference(cr, ing, r.Scheme); err != nil { 655 return err 656 } 657 err = r.Client.Create(context.TODO(), ing) 658 if err != nil { 659 return err 660 } 661 } else { 662 return err 663 } 664 } 665 666 // Create Keycloak Service 667 svc := newKeycloakService(cr) 668 err = r.Client.Get(context.TODO(), types.NamespacedName{Name: svc.Name, 669 Namespace: svc.Namespace}, svc) 670 671 if err != nil { 672 if errors.IsNotFound(err) { 673 if err := controllerutil.SetControllerReference(cr, svc, r.Scheme); err != nil { 674 return err 675 } 676 err = r.Client.Create(context.TODO(), svc) 677 if err != nil { 678 return err 679 } 680 } else { 681 return err 682 } 683 } 684 685 // Create Keycloak Deployment 686 dep := newKeycloakDeployment(cr) 687 err = r.Client.Get(context.TODO(), types.NamespacedName{Name: dep.Name, 688 Namespace: dep.Namespace}, dep) 689 690 if err != nil { 691 if errors.IsNotFound(err) { 692 if err := controllerutil.SetControllerReference(cr, dep, r.Scheme); err != nil { 693 return err 694 } 695 err = r.Client.Create(context.TODO(), dep) 696 if err != nil { 697 return err 698 } 699 } else { 700 return err 701 } 702 } 703 704 return nil 705 } 706 707 // prepares a keycloak config which is used in creating keycloak realm configuration. 708 func (r *ReconcileArgoCD) prepareKeycloakConfig(cr *argoproj.ArgoCD) (*keycloakConfig, error) { 709 710 var tlsVerification bool 711 // Get keycloak hostname from route. 712 // keycloak hostname is required to post realm configuration to keycloak when keycloak cannot be accessed using service name 713 // due to network policies or operator running outside the cluster or development purpose. 714 existingKeycloakRoute := &routev1.Route{ 715 ObjectMeta: metav1.ObjectMeta{ 716 Name: defaultKeycloakIdentifier, 717 Namespace: cr.Namespace, 718 }, 719 } 720 err := r.Client.Get(context.TODO(), types.NamespacedName{Name: existingKeycloakRoute.Name, 721 Namespace: existingKeycloakRoute.Namespace}, existingKeycloakRoute) 722 if err != nil { 723 return nil, err 724 } 725 kRouteURL := fmt.Sprintf("https://%s", existingKeycloakRoute.Spec.Host) 726 727 // Get ArgoCD hostname from route. ArgoCD hostname is used in the keycloak client configuration. 728 existingArgoCDRoute := &routev1.Route{ 729 ObjectMeta: metav1.ObjectMeta{ 730 Name: fmt.Sprintf("%s-%s", cr.Name, "server"), 731 Namespace: cr.Namespace, 732 }, 733 } 734 err = r.Client.Get(context.TODO(), types.NamespacedName{Name: existingArgoCDRoute.Name, 735 Namespace: existingArgoCDRoute.Namespace}, existingArgoCDRoute) 736 if err != nil { 737 return nil, err 738 } 739 aRouteURL := fmt.Sprintf("https://%s", existingArgoCDRoute.Spec.Host) 740 741 // Get keycloak Secret for credentials. credentials are required to authenticate with keycloak. 742 existingSecret := &corev1.Secret{ 743 ObjectMeta: metav1.ObjectMeta{ 744 Name: fmt.Sprintf("%s-%s", defaultKeycloakIdentifier, "secret"), 745 Namespace: cr.Namespace, 746 }, 747 } 748 err = r.Client.Get(context.TODO(), types.NamespacedName{Name: existingSecret.Name, 749 Namespace: existingSecret.Namespace}, existingSecret) 750 if err != nil { 751 return nil, err 752 } 753 754 userEnc := b64.URLEncoding.EncodeToString(existingSecret.Data["SSO_USERNAME"]) 755 passEnc := b64.URLEncoding.EncodeToString(existingSecret.Data["SSO_PASSWORD"]) 756 757 username, _ := b64.URLEncoding.DecodeString(userEnc) 758 password, _ := b64.URLEncoding.DecodeString(passEnc) 759 760 // Get Keycloak Service Cert 761 serverCert, err := r.getKCServerCert(cr) 762 if err != nil { 763 return nil, err 764 } 765 766 // By default TLS Verification should be enabled. 767 if cr.Spec.SSO.Keycloak == nil || (cr.Spec.SSO.Keycloak.VerifyTLS == nil || *cr.Spec.SSO.Keycloak.VerifyTLS) { 768 tlsVerification = true 769 } 770 771 cfg := &keycloakConfig{ 772 ArgoName: cr.Name, 773 ArgoNamespace: cr.Namespace, 774 Username: string(username), 775 Password: string(password), 776 KeycloakURL: kRouteURL, 777 ArgoCDURL: aRouteURL, 778 KeycloakServerCert: serverCert, 779 VerifyTLS: tlsVerification, 780 } 781 782 return cfg, nil 783 } 784 785 // prepares a keycloak config which is used in creating keycloak realm configuration for kubernetes. 786 func (r *ReconcileArgoCD) prepareKeycloakConfigForK8s(cr *argoproj.ArgoCD) (*keycloakConfig, error) { 787 788 // Get keycloak hostname from ingress. 789 // keycloak hostname is required to post realm configuration to keycloak when keycloak cannot be accessed using service name 790 // due to network policies or operator running outside the cluster or development purpose. 791 existingKeycloakIng := &networkingv1.Ingress{ 792 ObjectMeta: metav1.ObjectMeta{ 793 Name: defaultKeycloakIdentifier, 794 Namespace: cr.Namespace, 795 }, 796 } 797 err := r.Client.Get(context.TODO(), types.NamespacedName{Name: existingKeycloakIng.Name, 798 Namespace: existingKeycloakIng.Namespace}, existingKeycloakIng) 799 if err != nil { 800 return nil, err 801 } 802 kIngURL := fmt.Sprintf("https://%s", existingKeycloakIng.Spec.Rules[0].Host) 803 804 // Get ArgoCD hostname from Ingress. ArgoCD hostname is used in the keycloak client configuration. 805 existingArgoCDIng := &networkingv1.Ingress{ 806 ObjectMeta: metav1.ObjectMeta{ 807 Name: fmt.Sprintf("%s-%s", cr.Name, "server"), 808 Namespace: cr.Namespace, 809 }, 810 } 811 err = r.Client.Get(context.TODO(), types.NamespacedName{Name: existingArgoCDIng.Name, 812 Namespace: existingArgoCDIng.Namespace}, existingArgoCDIng) 813 if err != nil { 814 return nil, err 815 } 816 aIngURL := fmt.Sprintf("https://%s", existingArgoCDIng.Spec.Rules[0].Host) 817 818 cfg := &keycloakConfig{ 819 ArgoName: cr.Name, 820 ArgoNamespace: cr.Namespace, 821 Username: defaultKeycloakAdminUser, 822 Password: defaultKeycloakAdminPassword, 823 KeycloakURL: kIngURL, 824 ArgoCDURL: aIngURL, 825 VerifyTLS: false, 826 } 827 828 return cfg, nil 829 } 830 831 // creates a keycloak realm configuration which when posted to keycloak using http client creates a keycloak realm. 832 func createRealmConfig(cfg *keycloakConfig) ([]byte, error) { 833 834 ks := &CustomKeycloakAPIRealm{ 835 Realm: keycloakRealm, 836 Enabled: true, 837 SslRequired: "external", 838 Clients: []*KeycloakAPIClient{ 839 { 840 ClientID: keycloakClient, 841 Name: keycloakClient, 842 RootURL: cfg.ArgoCDURL, 843 AdminURL: cfg.ArgoCDURL, 844 ClientAuthenticatorType: "client-secret", 845 Secret: oAuthClientSecret, 846 RedirectUris: []string{fmt.Sprintf("%s/%s", 847 cfg.ArgoCDURL, "auth/callback")}, 848 WebOrigins: []string{cfg.ArgoCDURL}, 849 DefaultClientScopes: []string{ 850 "web-origins", 851 "role_list", 852 "roles", 853 "profile", 854 "groups", 855 "email", 856 }, 857 StandardFlowEnabled: true, 858 }, 859 }, 860 ClientScopes: []KeycloakClientScope{ 861 { 862 Name: "groups", 863 Protocol: "openid-connect", 864 ProtocolMappers: []KeycloakProtocolMapper{ 865 { 866 Name: "groups", 867 Protocol: "openid-connect", 868 ProtocolMapper: "oidc-usermodel-attribute-mapper", 869 Config: map[string]string{ 870 "aggregate.attrs": "false", 871 "multivalued": "true", 872 "userinfo.token.claim": "true", 873 "user.attribute": "groups", 874 "id.token.claim": "true", 875 "access.token.claim": "true", 876 "claim.name": "groups", 877 }, 878 }, 879 }, 880 }, 881 { 882 Name: "email", 883 Protocol: "openid-connect", 884 ProtocolMappers: []KeycloakProtocolMapper{ 885 { 886 Name: "email", 887 Protocol: "openid-connect", 888 ProtocolMapper: "oidc-usermodel-property-mapper", 889 Config: map[string]string{ 890 "userinfo.token.claim": "true", 891 "user.attribute": "email", 892 "id.token.claim": "true", 893 "access.token.claim": "true", 894 "claim.name": "email", 895 "jsonType.label": "String", 896 }, 897 }, 898 }, 899 }, 900 { 901 Name: "profile", 902 Protocol: "openid-connect", 903 Attributes: map[string]string{ 904 "include.in.token.scope": "true", 905 "display.on.consent.screen": "true", 906 }, 907 }, 908 }, 909 } 910 911 // Add OpenShift-v4 as Identity Provider only for OpenShift environment. 912 // No Identity Provider is configured by default for non-openshift environments. 913 if IsTemplateAPIAvailable() { 914 baseURL := "https://kubernetes.default.svc.cluster.local" 915 if isProxyCluster() { 916 baseURL = getOpenShiftAPIURL() 917 } 918 919 ks.IdentityProviders = []*KeycloakIdentityProvider{ 920 { 921 Alias: "openshift-v4", 922 DisplayName: "Login with OpenShift", 923 ProviderID: "openshift-v4", 924 Config: map[string]string{ 925 "baseUrl": baseURL, 926 "clientSecret": oAuthClientSecret, 927 "clientId": getOAuthClient(cfg.ArgoNamespace), 928 "defaultScope": "user:full", 929 "syncMode": "FORCE", 930 }, 931 }, 932 } 933 ks.IdentityProviderMappers = []*KeycloakIdentityProviderMapper{ 934 { 935 Name: "groups", 936 IdentityProviderAlias: "openshift-v4", 937 IdentityProviderMapper: "openshift-v4-user-attribute-mapper", 938 Config: map[string]string{ 939 "syncMode": "INHERIT", 940 "jsonField": "groups", 941 "userAttribute": "groups", 942 }, 943 }, 944 } 945 } 946 947 json, err := json.Marshal(ks) 948 if err != nil { 949 return nil, err 950 } 951 952 return json, nil 953 } 954 955 // Gets Keycloak Server cert. This cert is used to authenticate the api calls to the Keycloak service. 956 func (r *ReconcileArgoCD) getKCServerCert(cr *argoproj.ArgoCD) ([]byte, error) { 957 958 sslCertsSecret := &corev1.Secret{ 959 ObjectMeta: metav1.ObjectMeta{ 960 Name: servingCertSecretName, 961 Namespace: cr.Namespace, 962 }, 963 } 964 965 err := r.Client.Get(context.TODO(), types.NamespacedName{Name: sslCertsSecret.Name, Namespace: sslCertsSecret.Namespace}, sslCertsSecret) 966 967 switch { 968 case err == nil: 969 return sslCertsSecret.Data["tls.crt"], nil 970 case errors.IsNotFound(err): 971 return nil, nil 972 default: 973 return nil, err 974 } 975 } 976 977 func getOAuthClient(ns string) string { 978 return fmt.Sprintf("%s-%s", defaultKeycloakBrokerName, ns) 979 } 980 981 // Updates OIDC configuration for ArgoCD. 982 func (r *ReconcileArgoCD) updateArgoCDConfiguration(cr *argoproj.ArgoCD, kRouteURL string) error { 983 984 // Update the ArgoCD client secret for OIDC in argocd-secret. 985 argoCDSecret := &corev1.Secret{ 986 ObjectMeta: metav1.ObjectMeta{ 987 Name: common.ArgoCDSecretName, 988 Namespace: cr.Namespace, 989 }, 990 } 991 992 err := r.Client.Get(context.TODO(), types.NamespacedName{Name: argoCDSecret.Name, Namespace: argoCDSecret.Namespace}, argoCDSecret) 993 if err != nil { 994 log.Error(err, fmt.Sprintf("ArgoCD secret not found for ArgoCD %s in namespace %s", 995 cr.Name, cr.Namespace)) 996 return err 997 } 998 999 argoCDSecret.Data["oidc.keycloak.clientSecret"] = []byte(oAuthClientSecret) 1000 err = r.Client.Update(context.TODO(), argoCDSecret) 1001 if err != nil { 1002 log.Error(err, fmt.Sprintf("Error updating ArgoCD Secret for ArgoCD %s in namespace %s", 1003 cr.Name, cr.Namespace)) 1004 return err 1005 } 1006 1007 // Create openshift OAuthClient 1008 if IsTemplateAPIAvailable() { 1009 oAuthClient := &oauthv1.OAuthClient{ 1010 TypeMeta: metav1.TypeMeta{ 1011 Kind: "OAuthClient", 1012 APIVersion: "oauth.openshift.io/v1", 1013 }, 1014 ObjectMeta: metav1.ObjectMeta{ 1015 Name: getOAuthClient(cr.Namespace), 1016 Namespace: cr.Namespace, 1017 }, 1018 Secret: oAuthClientSecret, 1019 RedirectURIs: []string{fmt.Sprintf("%s/auth/realms/%s/broker/openshift-v4/endpoint", 1020 kRouteURL, keycloakClient)}, 1021 GrantMethod: "prompt", 1022 } 1023 1024 err = controllerutil.SetOwnerReference(cr, oAuthClient, r.Scheme) 1025 if err != nil { 1026 return err 1027 } 1028 1029 err = r.Client.Get(context.TODO(), types.NamespacedName{Name: oAuthClient.Name}, oAuthClient) 1030 if err != nil { 1031 if errors.IsNotFound(err) { 1032 err = r.Client.Create(context.TODO(), oAuthClient) 1033 if err != nil { 1034 return err 1035 } 1036 } 1037 } 1038 } 1039 1040 // Update ArgoCD instance for OIDC Config with Keycloakrealm URL 1041 rootCA := "" 1042 if cr.Spec.SSO.Keycloak.RootCA != "" { 1043 rootCA = cr.Spec.SSO.Keycloak.RootCA 1044 } 1045 o, err := yaml.Marshal(oidcConfig{ 1046 Name: "Keycloak", 1047 Issuer: fmt.Sprintf("%s/auth/realms/%s", 1048 kRouteURL, keycloakRealm), 1049 ClientID: keycloakClient, 1050 ClientSecret: "$oidc.keycloak.clientSecret", 1051 RequestedScope: []string{"openid", "profile", "email", "groups"}, 1052 RootCA: rootCA, 1053 }) 1054 1055 if err != nil { 1056 return err 1057 } 1058 1059 argoCDCM := newConfigMapWithName(common.ArgoCDConfigMapName, cr) 1060 err = r.Client.Get(context.TODO(), types.NamespacedName{Name: argoCDCM.Name, Namespace: argoCDCM.Namespace}, argoCDCM) 1061 if err != nil { 1062 log.Error(err, fmt.Sprintf("ArgoCD configmap not found for ArgoCD %s in namespace %s", 1063 cr.Name, cr.Namespace)) 1064 1065 return err 1066 } 1067 1068 argoCDCM.Data[common.ArgoCDKeyOIDCConfig] = string(o) 1069 err = r.Client.Update(context.TODO(), argoCDCM) 1070 if err != nil { 1071 log.Error(err, fmt.Sprintf("Error updating OIDC Configuration for ArgoCD %s in namespace %s", 1072 cr.Name, cr.Namespace)) 1073 return err 1074 } 1075 1076 // Update RBAC for ArgoCD Instance. 1077 argoRBACCM := newConfigMapWithName(common.ArgoCDRBACConfigMapName, cr) 1078 err = r.Client.Get(context.TODO(), types.NamespacedName{Name: argoRBACCM.Name, Namespace: argoRBACCM.Namespace}, argoRBACCM) 1079 if err != nil { 1080 log.Error(err, fmt.Sprintf("ArgoCD RBAC configmap not found for ArgoCD %s in namespace %s", 1081 cr.Name, cr.Namespace)) 1082 1083 return err 1084 } 1085 1086 argoRBACCM.Data["scopes"] = "[groups,email]" 1087 err = r.Client.Update(context.TODO(), argoRBACCM) 1088 if err != nil { 1089 log.Error(err, fmt.Sprintf("Error updating ArgoCD RBAC configmap %s in namespace %s", 1090 cr.Name, cr.Namespace)) 1091 return err 1092 } 1093 1094 return nil 1095 } 1096 1097 // HandleKeycloakPodDeletion resets the Realm Creation Status to false when keycloak pod is deleted. 1098 func handleKeycloakPodDeletion(dc *appsv1.DeploymentConfig) error { 1099 cfg, err := config.GetConfig() 1100 if err != nil { 1101 log.Error(err, "unable to get k8s config") 1102 return err 1103 } 1104 1105 // Initialize deployment config client. 1106 dcClient, err := oappsv1client.NewForConfig(cfg) 1107 if err != nil { 1108 log.Error(err, fmt.Sprintf("unable to create apps client for Deployment config %s in namespace %s", 1109 dc.Name, dc.Namespace)) 1110 return err 1111 } 1112 1113 log.Info("Set the Realm Creation status annoation to false") 1114 existingDC, err := dcClient.DeploymentConfigs(dc.Namespace).Get(context.TODO(), defaultKeycloakIdentifier, metav1.GetOptions{}) 1115 if err != nil { 1116 return err 1117 } 1118 1119 existingDC.Annotations["argocd.argoproj.io/realm-created"] = "false" 1120 _, err = dcClient.DeploymentConfigs(dc.Namespace).Update(context.TODO(), existingDC, metav1.UpdateOptions{}) 1121 if err != nil { 1122 return err 1123 } 1124 1125 return nil 1126 } 1127 1128 func (r *ReconcileArgoCD) reconcileKeycloakConfiguration(cr *argoproj.ArgoCD) error { 1129 1130 // TemplateAPI is available, Install keycloak using openshift templates. 1131 if IsTemplateAPIAvailable() { 1132 err := r.reconcileKeycloakForOpenShift(cr) 1133 if err != nil { 1134 return err 1135 } 1136 } else { 1137 err := r.reconcileKeycloak(cr) 1138 if err != nil { 1139 return err 1140 } 1141 } 1142 1143 return nil 1144 } 1145 1146 func deleteKeycloakConfiguration(cr *argoproj.ArgoCD) error { 1147 1148 // If SSO is installed using OpenShift templates. 1149 if IsTemplateAPIAvailable() { 1150 err := deleteKeycloakConfigForOpenShift(cr) 1151 if err != nil { 1152 return err 1153 } 1154 } else { 1155 err := deleteKeycloakConfigForK8s(cr) 1156 if err != nil { 1157 return err 1158 } 1159 } 1160 1161 return nil 1162 } 1163 1164 // Delete Keycloak configuration for OpenShift 1165 func deleteKeycloakConfigForOpenShift(cr *argoproj.ArgoCD) error { 1166 cfg, err := config.GetConfig() 1167 if err != nil { 1168 log.Error(err, fmt.Sprintf("unable to get k8s config for ArgoCD %s in namespace %s", 1169 cr.Name, cr.Namespace)) 1170 return err 1171 } 1172 1173 // Initialize template client. 1174 templateclient, err := templatev1client.NewForConfig(cfg) 1175 if err != nil { 1176 log.Error(err, fmt.Sprintf("unable to create Template client for ArgoCD %s in namespace %s", 1177 cr.Name, cr.Namespace)) 1178 return err 1179 } 1180 1181 log.Info(fmt.Sprintf("Delete Template Instance for ArgoCD %s in namespace %s", 1182 cr.Name, cr.Namespace)) 1183 1184 // We use the foreground propagation policy to ensure that the garbage 1185 // collector removes all instantiated objects before the TemplateInstance 1186 // itself disappears. 1187 foreground := metav1.DeletePropagationForeground 1188 deleteOptions := metav1.DeleteOptions{PropagationPolicy: &foreground} 1189 err = templateclient.TemplateInstances(cr.Namespace).Delete(context.TODO(), defaultTemplateIdentifier, deleteOptions) 1190 if err != nil { 1191 return err 1192 } 1193 1194 err = deleteOAuthClient(cr) 1195 if err != nil { 1196 return err 1197 } 1198 1199 return nil 1200 } 1201 1202 // Delete OpenShift OAuthClient 1203 func deleteOAuthClient(cr *argoproj.ArgoCD) error { 1204 1205 cfg, err := config.GetConfig() 1206 if err != nil { 1207 log.Error(err, fmt.Sprintf("unable to get k8s config for ArgoCD %s in namespace %s", 1208 cr.Name, cr.Namespace)) 1209 return err 1210 } 1211 1212 // We use the foreground propagation policy to ensure that the garbage 1213 // collector removes all instantiated objects before the TemplateInstance 1214 // itself disappears. 1215 foreground := metav1.DeletePropagationForeground 1216 deleteOptions := metav1.DeleteOptions{PropagationPolicy: &foreground} 1217 1218 // Delete OAuthClient created for keycloak. 1219 oauth, err := oauthclient.NewForConfig(cfg) 1220 if err != nil { 1221 log.Error(err, fmt.Sprintf("unable to create oAuth client for ArgoCD %s in namespace %s", 1222 cr.Name, cr.Namespace)) 1223 return err 1224 } 1225 log.Info(fmt.Sprintf("Delete OAuthClient for ArgoCD %s in namespace %s", 1226 cr.Name, cr.Namespace)) 1227 1228 oa := getOAuthClient(cr.Namespace) 1229 1230 // TODO: Remove the oauth.OAuthClients().Get and proceed with delete once the issue is resolved. 1231 // OAuthClient configuration does not get deleted from previous instances occasionally. 1232 // It is safe to verify if OAuthClient exists and perform delete. 1233 // https://github.com/openshift/client-go/issues/209 1234 _, err = oauth.OAuthClients().Get(context.TODO(), oa, metav1.GetOptions{}) 1235 if err == nil { 1236 err = oauth.OAuthClients().Delete(context.TODO(), oa, deleteOptions) 1237 if err != nil { 1238 return err 1239 } 1240 } 1241 1242 return nil 1243 } 1244 1245 // Delete Keycloak configuration for Kubernetes 1246 func deleteKeycloakConfigForK8s(cr *argoproj.ArgoCD) error { 1247 1248 cfg, err := config.GetConfig() 1249 if err != nil { 1250 log.Error(err, fmt.Sprintf("unable to get k8s config for ArgoCD %s in namespace %s", 1251 cr.Name, cr.Namespace)) 1252 return err 1253 } 1254 1255 clientset, err := kubernetes.NewForConfig(cfg) 1256 if err != nil { 1257 return err 1258 } 1259 1260 log.Info(fmt.Sprintf("Delete Keycloak deployment for ArgoCD %s in namespace %s", 1261 cr.Name, cr.Namespace)) 1262 1263 // We use the foreground propagation policy to ensure that the garbage 1264 // collector removes all instantiated objects before the TemplateInstance 1265 // itself disappears. 1266 foreground := metav1.DeletePropagationForeground 1267 deleteOptions := metav1.DeleteOptions{PropagationPolicy: &foreground} 1268 err = clientset.AppsV1().Deployments(cr.Namespace).Delete(context.TODO(), defaultKeycloakIdentifier, deleteOptions) 1269 if err != nil { 1270 return err 1271 } 1272 1273 log.Info(fmt.Sprintf("Delete Keycloak Service for ArgoCD %s in namespace %s", 1274 cr.Name, cr.Namespace)) 1275 1276 err = clientset.CoreV1().Services(cr.Namespace).Delete(context.TODO(), defaultKeycloakIdentifier, deleteOptions) 1277 if err != nil { 1278 return err 1279 } 1280 1281 log.Info(fmt.Sprintf("Delete Keycloak Ingress for ArgoCD %s in namespace %s", 1282 cr.Name, cr.Namespace)) 1283 1284 err = clientset.ExtensionsV1beta1().Ingresses(cr.Namespace).Delete(context.TODO(), defaultKeycloakIdentifier, deleteOptions) 1285 if err != nil { 1286 return err 1287 } 1288 1289 return nil 1290 } 1291 1292 // Installs and configures Keycloak for OpenShift 1293 func (r *ReconcileArgoCD) reconcileKeycloakForOpenShift(cr *argoproj.ArgoCD) error { 1294 1295 templateInstanceRef, err := newKeycloakTemplateInstance(cr) 1296 if err != nil { 1297 return err 1298 } 1299 err = r.Client.Get(context.TODO(), types.NamespacedName{Name: templateInstanceRef.Name, 1300 Namespace: templateInstanceRef.Namespace}, &template.TemplateInstance{}) 1301 if err != nil { 1302 if errors.IsNotFound(err) { 1303 log.Info(fmt.Sprintf("Template API found, Installing keycloak using openshift templates for ArgoCD %s in namespace %s", 1304 cr.Name, cr.Namespace)) 1305 1306 if err := controllerutil.SetControllerReference(cr, templateInstanceRef, r.Scheme); err != nil { 1307 return err 1308 } 1309 1310 err = r.Client.Create(context.TODO(), templateInstanceRef) 1311 if err != nil { 1312 return err 1313 } 1314 } else { 1315 return err 1316 } 1317 } 1318 1319 existingDC := &appsv1.DeploymentConfig{ 1320 ObjectMeta: metav1.ObjectMeta{ 1321 Name: defaultKeycloakIdentifier, 1322 Namespace: cr.Namespace, 1323 }, 1324 } 1325 err = r.Client.Get(context.TODO(), types.NamespacedName{Name: existingDC.Name, Namespace: existingDC.Namespace}, existingDC) 1326 if err != nil { 1327 log.Error(err, fmt.Sprintf("Keycloak Deployment not found or being created for ArgoCD %s in namespace %s", 1328 cr.Name, cr.Namespace)) 1329 } else { 1330 // Handle Image upgrades 1331 desiredImage := getKeycloakContainerImage(cr) 1332 if existingDC.Spec.Template.Spec.Containers[0].Image != desiredImage { 1333 existingDC.Spec.Template.Spec.Containers[0].Image = desiredImage 1334 1335 err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { 1336 return r.Client.Update(context.TODO(), existingDC) 1337 }) 1338 1339 if err != nil { 1340 return err 1341 } 1342 } 1343 } 1344 1345 // Proceed with the keycloak configuration only once the keycloak pod is up and running. 1346 if existingDC.Status.AvailableReplicas == expectedReplicas { 1347 1348 cfg, err := r.prepareKeycloakConfig(cr) 1349 if err != nil { 1350 return err 1351 } 1352 1353 // keycloakRouteURL is used to update the OIDC configuration for ArgoCD. 1354 keycloakRouteURL := cfg.KeycloakURL 1355 1356 // If Keycloak deployment exists and a realm is already created for ArgoCD, Do not create a new one. 1357 if existingDC.Annotations["argocd.argoproj.io/realm-created"] == "false" { 1358 1359 // Create a keycloak realm and publish. 1360 response, err := createRealm(cfg) 1361 if err != nil { 1362 log.Error(err, fmt.Sprintf("Failed posting keycloak realm configuration for ArgoCD %s in namespace %s", 1363 cr.Name, cr.Namespace)) 1364 return err 1365 } 1366 1367 if response == successResponse { 1368 log.Info(fmt.Sprintf("Successfully created keycloak realm for ArgoCD %s in namespace %s", 1369 cr.Name, cr.Namespace)) 1370 1371 // TODO: Remove the deleteOAuthClient invocation once the issue is resolved. 1372 // OAuthClient configuration does not get deleted from previous instances occasionally. 1373 // It is safe to delete before updating the OIDC config. 1374 // https://github.com/openshift/client-go/issues/209 1375 err = deleteOAuthClient(cr) 1376 if err != nil { 1377 return err 1378 } 1379 1380 // Update Realm creation. This will avoid posting of realm configuration on further reconciliations. 1381 err = r.Client.Get(context.TODO(), types.NamespacedName{Name: existingDC.Name, Namespace: existingDC.Namespace}, existingDC) 1382 if err != nil { 1383 return err 1384 } 1385 1386 existingDC.Annotations["argocd.argoproj.io/realm-created"] = "true" 1387 err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { 1388 return r.Client.Update(context.TODO(), existingDC) 1389 }) 1390 1391 if err != nil { 1392 return err 1393 } 1394 1395 } 1396 } 1397 1398 // Updates OIDC Configuration in the argocd-cm when Keycloak is initially configured 1399 // or when user requests to update the OIDC configuration through `.spec.sso.keycloak.rootCA`. 1400 err = r.updateArgoCDConfiguration(cr, keycloakRouteURL) 1401 if err != nil { 1402 log.Error(err, fmt.Sprintf("Failed to update OIDC Configuration for ArgoCD %s in namespace %s", 1403 cr.Name, cr.Namespace)) 1404 return err 1405 } 1406 } 1407 1408 return nil 1409 } 1410 1411 // Installs and configures Keycloak for Kubernetes 1412 func (r *ReconcileArgoCD) reconcileKeycloak(cr *argoproj.ArgoCD) error { 1413 1414 err := r.newKeycloakInstance(cr) 1415 if err != nil { 1416 log.Error(err, fmt.Sprintf("Failed creating keycloak instance for ArgoCD %s in Namespace %s", 1417 cr.Name, cr.Namespace)) 1418 return err 1419 } 1420 1421 existingDeployment := &k8sappsv1.Deployment{ 1422 ObjectMeta: metav1.ObjectMeta{ 1423 Name: defaultKeycloakIdentifier, 1424 Namespace: cr.Namespace, 1425 }, 1426 } 1427 1428 err = r.Client.Get(context.TODO(), types.NamespacedName{Name: existingDeployment.Name, Namespace: existingDeployment.Namespace}, existingDeployment) 1429 if err != nil { 1430 log.Error(err, fmt.Sprintf("Keycloak Deployment not found or being created for ArgoCD %s in namespace %s", 1431 cr.Name, cr.Namespace)) 1432 } else { 1433 // Handle Image upgrades 1434 desiredImage := getKeycloakContainerImage(cr) 1435 if existingDeployment.Spec.Template.Spec.Containers[0].Image != desiredImage { 1436 existingDeployment.Spec.Template.Spec.Containers[0].Image = desiredImage 1437 1438 err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { 1439 return r.Client.Update(context.TODO(), existingDeployment) 1440 }) 1441 1442 if err != nil { 1443 return err 1444 } 1445 } 1446 } 1447 1448 // Proceed with the keycloak configuration only once the keycloak pod is up and running. 1449 if existingDeployment.Status.AvailableReplicas == expectedReplicas { 1450 1451 cfg, err := r.prepareKeycloakConfigForK8s(cr) 1452 if err != nil { 1453 return err 1454 } 1455 1456 // kIngURL is used to update the OIDC configuration for ArgoCD. 1457 kIngURL := cfg.KeycloakURL 1458 1459 // If Keycloak deployment exists and a realm is already created for ArgoCD, Do not create a new one. 1460 if existingDeployment.Annotations["argocd.argoproj.io/realm-created"] == "false" { 1461 // Create a keycloak realm and publish. 1462 response, err := createRealm(cfg) 1463 if err != nil { 1464 log.Error(err, fmt.Sprintf("Failed posting keycloak realm configuration for ArgoCD %s in namespace %s", 1465 cr.Name, cr.Namespace)) 1466 return err 1467 } 1468 1469 if response == successResponse { 1470 log.Info("Successfully created keycloak realm for ArgoCD %s in namespace %s") 1471 1472 // Update Realm creation. This will avoid posting of realm configuration on further reconciliations. 1473 existingDeployment.Annotations["argocd.argoproj.io/realm-created"] = "true" 1474 err = r.Client.Update(context.TODO(), existingDeployment) 1475 if err != nil { 1476 return err 1477 } 1478 } 1479 } 1480 1481 // Updates OIDC Configuration in the argocd-cm when Keycloak is initially configured 1482 // or when user requests to update the OIDC configuration through `.spec.sso.keycloak.rootCA`. 1483 err = r.updateArgoCDConfiguration(cr, kIngURL) 1484 if err != nil { 1485 log.Error(err, fmt.Sprintf("Failed to update OIDC Configuration for ArgoCD %s in namespace %s", 1486 cr.Name, cr.Namespace)) 1487 return err 1488 } 1489 } 1490 1491 return nil 1492 }