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  }