Automate the setup of github identity provider with internal keycloak (#589)

* GitHub identity provider provision

Signed-off-by: Anatolii Bazko <abazko@redhat.com>
pull/631/head
Anatolii Bazko 2021-01-13 17:06:54 +02:00 committed by GitHub
parent 5b78d6b545
commit 482155b7e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 762 additions and 394 deletions

5
.vscode/launch.json vendored
View File

@ -99,7 +99,7 @@
"stable",
"che"
]
},
},
{
"type": "bashdb",
"request": "launch",
@ -145,7 +145,8 @@
"mode": "test",
"program": "${file}",
"env": {
"MOCK_API": true
"MOCK_API": true,
"CHE_FLAVOR": "che"
},
},
{

View File

@ -29,8 +29,10 @@ RUN export ARCH="$(uname -m)" && if [[ ${ARCH} == "x86_64" ]]; then export ARCH=
FROM registry.access.redhat.com/ubi8-minimal:8.3-230
COPY --from=builder /tmp/che-operator/che-operator /usr/local/bin/che-operator
COPY --from=builder /che-operator/templates/keycloak_provision /tmp/keycloak_provision
COPY --from=builder /che-operator/templates/oauth_provision /tmp/oauth_provision
COPY --from=builder /che-operator/templates/keycloak-provision.sh /tmp/keycloak-provision.sh
COPY --from=builder /che-operator/templates/oauth-provision.sh /tmp/oauth-provision.sh
COPY --from=builder /che-operator/templates/delete-identity-provider.sh /tmp/delete-identity-provider.sh
COPY --from=builder /che-operator/templates/create-github-identity-provider.sh /tmp/create-github-identity-provider.sh
# apply CVE fixes, if required
RUN microdnf update -y librepo libnghttp2 && microdnf clean all && rm -rf /var/cache/yum && echo "Installed Packages" && rpm -qa | sort -V && echo "End Of Installed Packages"
CMD ["che-operator"]

View File

@ -696,6 +696,10 @@ spec:
devfileRegistryURL:
description: Public URL to the Devfile registry
type: string
gitHubOAuthProvisioned:
description: Indicates whether an Identity Provider instance (Keycloak
/ RH SSO) has been configured to integrate with the GitHub OAuth.
type: boolean
helpLink:
description: A URL that can point to some URL where to find help related
to the current Operator status.

View File

@ -84,13 +84,13 @@ metadata:
categories: Developer Tools
certified: "false"
containerImage: quay.io/eclipse/che-operator:nightly
createdAt: "2021-01-08T14:09:35Z"
createdAt: "2021-01-13T13:41:43Z"
description: A Kube-native development solution that delivers portable and collaborative
developer workspaces.
operatorframework.io/suggested-namespace: eclipse-che
repository: https://github.com/eclipse/che-operator
support: Eclipse Foundation
name: eclipse-che-preview-kubernetes.v7.25.0-66.nightly
name: eclipse-che-preview-kubernetes.v7.25.0-68.nightly
namespace: placeholder
spec:
apiservicedefinitions: {}
@ -514,4 +514,4 @@ spec:
maturity: stable
provider:
name: Eclipse Foundation
version: 7.25.0-66.nightly
version: 7.25.0-68.nightly

View File

@ -696,6 +696,10 @@ spec:
devfileRegistryURL:
description: Public URL to the Devfile registry
type: string
gitHubOAuthProvisioned:
description: Indicates whether an Identity Provider instance (Keycloak
/ RH SSO) has been configured to integrate with the GitHub OAuth.
type: boolean
helpLink:
description: A URL that can point to some URL where to find help related
to the current Operator status.

View File

@ -75,13 +75,13 @@ metadata:
categories: Developer Tools, OpenShift Optional
certified: "false"
containerImage: quay.io/eclipse/che-operator:nightly
createdAt: "2021-01-08T14:09:42Z"
createdAt: "2021-01-13T13:41:52Z"
description: A Kube-native development solution that delivers portable and collaborative
developer workspaces in OpenShift.
operatorframework.io/suggested-namespace: eclipse-che
repository: https://github.com/eclipse/che-operator
support: Eclipse Foundation
name: eclipse-che-preview-openshift.v7.25.0-66.nightly
name: eclipse-che-preview-openshift.v7.25.0-68.nightly
namespace: placeholder
spec:
apiservicedefinitions: {}
@ -533,4 +533,4 @@ spec:
maturity: stable
provider:
name: Eclipse Foundation
version: 7.25.0-66.nightly
version: 7.25.0-68.nightly

View File

@ -697,6 +697,10 @@ spec:
devfileRegistryURL:
description: Public URL to the Devfile registry
type: string
gitHubOAuthProvisioned:
description: Indicates whether an Identity Provider instance (Keycloak
/ RH SSO) has been configured to integrate with the GitHub OAuth.
type: boolean
helpLink:
description: A URL that can point to some URL where to find help related
to the current Operator status.

View File

@ -57,8 +57,9 @@ echo "[INFO] CR file path: ${CR}"
kubectl apply -f deploy/crds/org_v1_che_crd.yaml
kubectl apply -f "${CR}" -n $CHE_NAMESPACE
cp templates/keycloak_provision /tmp/keycloak_provision
cp templates/oauth_provision /tmp/oauth_provision
cp templates/keycloak-provision.sh /tmp/keycloak-provision.sh
cp templates/delete-identity-provider.sh /tmp/delete-identity-provider.sh
cp templates/create-github-identity-provider.sh /tmp/create-github-identity-provider.sh
ENV_FILE=/tmp/che-operator-debug.env
rm -rf "${ENV_FILE}"
@ -72,7 +73,7 @@ echo "WATCH_NAMESPACE='${CHE_NAMESPACE}'" >> ${ENV_FILE}
echo "[WARN] Make sure that your CR contains valid ingress domain!"
operator-sdk run --local --namespace ${CHE_NAMESPACE} --enable-delve &
operator-sdk run --local --watch-namespace ${CHE_NAMESPACE} --enable-delve &
OPERATOR_SDK_PID=$!
wait ${OPERATOR_SDK_PID}

View File

@ -553,6 +553,9 @@ type CheClusterStatus struct {
// Indicates whether an Identity Provider instance (Keycloak / RH SSO) has been configured to integrate with the OpenShift OAuth.
// +optional
OpenShiftoAuthProvisioned bool `json:"openShiftoAuthProvisioned"`
// Indicates whether an Identity Provider instance (Keycloak / RH SSO) has been configured to integrate with the GitHub OAuth.
// +optional
GitHubOAuthProvisioned bool `json:"gitHubOAuthProvisioned"`
// Status of a Che installation. Can be `Available`, `Unavailable`, or `Available, Rolling Update in Progress`
// +optional
// +operator-sdk:gen-csv:customresourcedefinitions.statusDescriptors=true

View File

@ -718,15 +718,15 @@ func (r *ReconcileChe) Reconcile(request reconcile.Request) (reconcile.Result, e
}
// Create a new Postgres deployment
deploymentStatus := postgres.SyncPostgresDeploymentToCluster(deployContext)
provisioned, err := postgres.SyncPostgresDeploymentToCluster(deployContext)
if !tests {
if !deploymentStatus.Continue {
if !provisioned {
logrus.Infof("Waiting on deployment '%s' to be ready", postgres.PostgresDeploymentName)
if deploymentStatus.Err != nil {
logrus.Error(deploymentStatus.Err)
if err != nil {
logrus.Error(err)
}
return reconcile.Result{Requeue: deploymentStatus.Requeue}, deploymentStatus.Err
return reconcile.Result{}, err
}
}
@ -741,15 +741,16 @@ func (r *ReconcileChe) Reconcile(request reconcile.Request) (reconcile.Result, e
}
identityProviderPostgresPassword = password
}
pgCommand := identity_provider.GetPostgresProvisionCommand(identityProviderPostgresPassword)
dbStatus := instance.Status.DbProvisoned
// provision Db and users for Che and Keycloak servers
if !dbStatus {
podToExec, err := util.K8sclient.GetDeploymentPod(postgres.PostgresDeploymentName, instance.Namespace)
if err != nil {
return reconcile.Result{}, err
}
_, err = util.K8sclient.ExecIntoPod(podToExec, pgCommand, "create Keycloak DB, user, privileges", instance.Namespace)
_, err := util.K8sclient.ExecIntoPod(
instance,
postgres.PostgresDeploymentName,
func(cr *orgv1.CheCluster) (string, error) {
return identity_provider.GetPostgresProvisionCommand(identityProviderPostgresPassword), nil
},
"create Keycloak DB, user, privileges")
if err == nil {
for {
instance.Status.DbProvisoned = true
@ -842,7 +843,7 @@ func (r *ReconcileChe) Reconcile(request reconcile.Request) (reconcile.Result, e
if err != nil {
logrus.Errorf("Error provisioning the identity provider to cluster: %v", err)
}
return reconcile.Result{RequeueAfter: time.Second * 1}, err
return reconcile.Result{}, err
}
}
@ -886,12 +887,12 @@ func (r *ReconcileChe) Reconcile(request reconcile.Request) (reconcile.Result, e
}
// Create a new che deployment
deploymentStatus := server.SyncCheDeploymentToCluster(deployContext)
provisioned, err = server.SyncCheDeploymentToCluster(deployContext)
if !tests {
if !deploymentStatus.Continue {
if !provisioned {
logrus.Infof("Waiting on deployment '%s' to be ready", cheFlavor)
if deploymentStatus.Err != nil {
logrus.Error(deploymentStatus.Err)
if err != nil {
logrus.Error(err)
}
deployment, err := r.GetEffectiveDeployment(instance, cheFlavor)
@ -912,7 +913,7 @@ func (r *ReconcileChe) Reconcile(request reconcile.Request) (reconcile.Result, e
}
}
}
return reconcile.Result{Requeue: deploymentStatus.Requeue}, deploymentStatus.Err
return reconcile.Result{}, err
}
}
// Update available status

View File

@ -805,7 +805,7 @@ func TestCheController(t *testing.T) {
if err = r.client.Get(context.TODO(), types.NamespacedName{Name: cheCR.Name, Namespace: cheCR.Namespace}, cheCR); err != nil {
t.Errorf("Failed to get the Che custom resource %s: %s", cheCR.Name, err)
}
if err = identity_provider.SyncIdentityProviderItems(deployContext, "che"); err != nil {
if _, err = identity_provider.SyncOpenShiftIdentityProviderItems(deployContext); err != nil {
t.Errorf("Failed to create the items for the identity provider: %s", err)
}
oAuthClientName := cheCR.Spec.Auth.OAuthClientName

View File

@ -13,6 +13,7 @@ package che
import (
"context"
identity_provider "github.com/eclipse/che-operator/pkg/deploy/identity-provider"
orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1"
@ -51,12 +52,18 @@ func (r *ReconcileChe) ReconcileIdentityProvider(instance *orgv1.CheCluster, isO
if err := r.client.Get(context.TODO(), types.NamespacedName{Name: "keycloak", Namespace: instance.Namespace}, keycloakDeployment); err != nil {
logrus.Errorf("Deployment %s not found: %s", keycloakDeployment.Name, err)
}
deleteOpenShiftIdentityProviderProvisionCommand := identity_provider.GetDeleteOpenShiftIdentityProviderProvisionCommand(instance, isOpenShift4)
podToExec, err := util.K8sclient.GetDeploymentPod(keycloakDeployment.Name, instance.Namespace)
if err != nil {
logrus.Errorf("Failed to retrieve pod name. Further exec will fail")
providerName := "openshift-v3"
if isOpenShift4 {
providerName = "openshift-v4"
}
_, err = util.K8sclient.ExecIntoPod(podToExec, deleteOpenShiftIdentityProviderProvisionCommand, "delete OpenShift identity provider", instance.Namespace)
_, err := util.K8sclient.ExecIntoPod(
instance,
keycloakDeployment.Name,
func(cr *orgv1.CheCluster) (string, error) {
return identity_provider.GetIdentityProviderDeleteCommand(instance, providerName)
},
"delete OpenShift identity provider")
if err == nil {
oAuthClient := &oauth.OAuthClient{}
oAuthClientName := instance.Spec.Auth.OAuthClientName

View File

@ -109,9 +109,10 @@ const (
CheEclipseOrg = "che.eclipse.org"
// che.eclipse.org annotations
CheEclipseOrgMountPath = "che.eclipse.org/mount-path"
CheEclipseOrgMountAs = "che.eclipse.org/mount-as"
CheEclipseOrgEnvName = "che.eclipse.org/env-name"
CheEclipseOrgMountPath = "che.eclipse.org/mount-path"
CheEclipseOrgMountAs = "che.eclipse.org/mount-as"
CheEclipseOrgEnvName = "che.eclipse.org/env-name"
CheEclipseOrgGithubOAuthCredentials = "che.eclipse.org/github-oauth-credentials"
)
func InitDefaults(defaultsPath string) {

View File

@ -46,37 +46,26 @@ var DeploymentDiffOpts = cmp.Options{
}),
}
type DeploymentProvisioningStatus struct {
ProvisioningStatus
Deployment *appsv1.Deployment
}
func SyncDeploymentToCluster(
deployContext *DeployContext,
specDeployment *appsv1.Deployment,
clusterDeployment *appsv1.Deployment,
additionalDeploymentDiffOpts cmp.Options,
additionalDeploymentMerge func(*appsv1.Deployment, *appsv1.Deployment) *appsv1.Deployment) DeploymentProvisioningStatus {
additionalDeploymentMerge func(*appsv1.Deployment, *appsv1.Deployment) *appsv1.Deployment) (bool, error) {
if err := MountSecrets(specDeployment, deployContext); err != nil {
return DeploymentProvisioningStatus{
ProvisioningStatus: ProvisioningStatus{Requeue: true, Err: err},
}
return false, err
}
clusterDeployment, err := GetClusterDeployment(specDeployment.Name, specDeployment.Namespace, deployContext.ClusterAPI.Client)
if err != nil {
return DeploymentProvisioningStatus{
ProvisioningStatus: ProvisioningStatus{Err: err},
}
return false, err
}
if clusterDeployment == nil {
logrus.Infof("Creating a new object: %s, name %s", specDeployment.Kind, specDeployment.Name)
err := deployContext.ClusterAPI.Client.Create(context.TODO(), specDeployment)
return DeploymentProvisioningStatus{
ProvisioningStatus: ProvisioningStatus{Requeue: true, Err: err},
}
return false, err
}
// 2-step comparation process
@ -89,9 +78,7 @@ func SyncDeploymentToCluster(
fmt.Printf("Difference:\n%s", diff)
clusterDeployment = additionalDeploymentMerge(specDeployment, clusterDeployment)
err := deployContext.ClusterAPI.Client.Update(context.TODO(), clusterDeployment)
return DeploymentProvisioningStatus{
ProvisioningStatus: ProvisioningStatus{Requeue: true, Err: err},
}
return false, err
}
}
@ -101,19 +88,15 @@ func SyncDeploymentToCluster(
fmt.Printf("Difference:\n%s", diff)
clusterDeployment.Spec = specDeployment.Spec
err := deployContext.ClusterAPI.Client.Update(context.TODO(), clusterDeployment)
return DeploymentProvisioningStatus{
ProvisioningStatus: ProvisioningStatus{Requeue: true, Err: err},
}
return false, err
}
if clusterDeployment.Spec.Strategy.Type == appsv1.RollingUpdateDeploymentStrategyType && clusterDeployment.Status.Replicas > 1 {
logrus.Infof("Deployment %s is in the rolling update state.", specDeployment.Name)
}
return DeploymentProvisioningStatus{
ProvisioningStatus: ProvisioningStatus{Continue: clusterDeployment.Status.AvailableReplicas == 1 && clusterDeployment.Status.Replicas == 1},
Deployment: clusterDeployment,
}
provisioned := clusterDeployment.Status.AvailableReplicas == 1 && clusterDeployment.Status.Replicas == 1
return provisioned, nil
}
func GetClusterDeployment(name string, namespace string, client runtimeClient.Client) (*appsv1.Deployment, error) {

View File

@ -18,7 +18,7 @@ import (
v1 "k8s.io/api/core/v1"
)
func SyncDevfileRegistryDeploymentToCluster(deployContext *deploy.DeployContext) deploy.DeploymentProvisioningStatus {
func SyncDevfileRegistryDeploymentToCluster(deployContext *deploy.DeployContext) (bool, error) {
registryType := "devfile"
registryImage := util.GetValue(deployContext.CheCluster.Spec.Server.DevfileRegistryImage, deploy.DefaultDevfileRegistryImage(deployContext.CheCluster))
registryImagePullPolicy := v1.PullPolicy(util.GetValue(string(deployContext.CheCluster.Spec.Server.PluginRegistryPullPolicy), deploy.DefaultPullPolicyFromDockerImage(registryImage)))
@ -29,9 +29,7 @@ func SyncDevfileRegistryDeploymentToCluster(deployContext *deploy.DeployContext)
clusterDeployment, err := deploy.GetClusterDeployment(deploy.DevfileRegistry, deployContext.CheCluster.Namespace, deployContext.ClusterAPI.Client)
if err != nil {
return deploy.DeploymentProvisioningStatus{
ProvisioningStatus: deploy.ProvisioningStatus{Err: err},
}
return false, err
}
specDeployment, err := registry.GetSpecRegistryDeployment(
@ -45,9 +43,7 @@ func SyncDevfileRegistryDeploymentToCluster(deployContext *deploy.DeployContext)
probePath)
if err != nil {
return deploy.DeploymentProvisioningStatus{
ProvisioningStatus: deploy.ProvisioningStatus{Err: err},
}
return false, err
}
return deploy.SyncDeploymentToCluster(deployContext, specDeployment, clusterDeployment, nil, nil)

View File

@ -76,15 +76,14 @@ func SyncDevfileRegistryToCluster(deployContext *deploy.DeployContext, cheHost s
deployContext.InternalService.DevfileRegistryHost = fmt.Sprintf("http://%s.%s.svc:8080", deploy.DevfileRegistry, deployContext.CheCluster.Namespace)
// Deploy devfile registry
deploymentStatus := SyncDevfileRegistryDeploymentToCluster(deployContext)
provisioned, err := SyncDevfileRegistryDeploymentToCluster(deployContext)
if !util.IsTestMode() {
if !deploymentStatus.Continue {
if !provisioned {
logrus.Info("Waiting on deployment '" + deploy.DevfileRegistry + "' to be ready")
if deploymentStatus.Err != nil {
logrus.Error(deploymentStatus.Err)
if err != nil {
logrus.Error(err)
}
return false, deploymentStatus.Err
return provisioned, err
}
}
}

View File

@ -35,7 +35,6 @@ import (
)
const (
KeycloakDeploymentName = "keycloak"
selectSslRequiredCommand = "OUT=$(psql keycloak -tAc \"SELECT 1 FROM REALM WHERE id = 'master'\"); " +
"if [ $OUT -eq 1 ]; then psql keycloak -tAc \"SELECT ssl_required FROM REALM WHERE id = 'master'\"; fi"
updateSslRequiredCommand = "psql keycloak -c \"update REALM set ssl_required='NONE' where id = 'master'\""
@ -59,19 +58,15 @@ var (
}
)
func SyncKeycloakDeploymentToCluster(deployContext *deploy.DeployContext) deploy.DeploymentProvisioningStatus {
clusterDeployment, err := deploy.GetClusterDeployment(KeycloakDeploymentName, deployContext.CheCluster.Namespace, deployContext.ClusterAPI.Client)
func SyncKeycloakDeploymentToCluster(deployContext *deploy.DeployContext) (bool, error) {
clusterDeployment, err := deploy.GetClusterDeployment(IdentityProviderDeploymentName, deployContext.CheCluster.Namespace, deployContext.ClusterAPI.Client)
if err != nil {
return deploy.DeploymentProvisioningStatus{
ProvisioningStatus: deploy.ProvisioningStatus{Err: err},
}
return false, err
}
specDeployment, err := getSpecKeycloakDeployment(deployContext, clusterDeployment)
if err != nil {
return deploy.DeploymentProvisioningStatus{
ProvisioningStatus: deploy.ProvisioningStatus{Err: err},
}
return false, err
}
return deploy.SyncDeploymentToCluster(deployContext, specDeployment, clusterDeployment, keycloakCustomDiffOpts, keycloakAdditionalDeploymentMerge)
@ -81,7 +76,7 @@ func getSpecKeycloakDeployment(
deployContext *deploy.DeployContext,
clusterDeployment *appsv1.Deployment) (*appsv1.Deployment, error) {
optionalEnv := true
labels := deploy.GetLabels(deployContext.CheCluster, KeycloakDeploymentName)
labels := deploy.GetLabels(deployContext.CheCluster, IdentityProviderDeploymentName)
cheFlavor := deploy.DefaultCheFlavor(deployContext.CheCluster)
keycloakImage := util.GetValue(deployContext.CheCluster.Spec.Auth.IdentityProviderImage, deploy.DefaultKeycloakImage(deployContext.CheCluster))
pullPolicy := corev1.PullPolicy(util.GetValue(string(deployContext.CheCluster.Spec.Auth.IdentityProviderImagePullPolicy), deploy.DefaultPullPolicyFromDockerImage(keycloakImage)))
@ -553,7 +548,7 @@ func getSpecKeycloakDeployment(
APIVersion: "apps/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: KeycloakDeploymentName,
Name: IdentityProviderDeploymentName,
Namespace: deployContext.CheCluster.Namespace,
Labels: labels,
Annotations: map[string]string{
@ -577,7 +572,7 @@ func getSpecKeycloakDeployment(
},
Containers: []corev1.Container{
{
Name: KeycloakDeploymentName,
Name: IdentityProviderDeploymentName,
Image: keycloakImage,
ImagePullPolicy: pullPolicy,
Command: []string{
@ -586,7 +581,7 @@ func getSpecKeycloakDeployment(
Args: args,
Ports: []corev1.ContainerPort{
{
Name: KeycloakDeploymentName,
Name: IdentityProviderDeploymentName,
ContainerPort: 8080,
Protocol: "TCP",
},
@ -672,7 +667,7 @@ func isSslRequiredUpdatedForMasterRealm(deployContext *deploy.DeployContext) boo
return false
}
clusterDeployment, _ := deploy.GetClusterDeployment(KeycloakDeploymentName, deployContext.CheCluster.Namespace, deployContext.ClusterAPI.Client)
clusterDeployment, _ := deploy.GetClusterDeployment(IdentityProviderDeploymentName, deployContext.CheCluster.Namespace, deployContext.ClusterAPI.Client)
if clusterDeployment == nil {
return false
}
@ -686,23 +681,25 @@ func isSslRequiredUpdatedForMasterRealm(deployContext *deploy.DeployContext) boo
return dbValue == "NONE"
}
func getSslRequiredForMasterRealm(checluster *orgv1.CheCluster) (string, error) {
podName, err := util.K8sclient.GetDeploymentPod(postgres.PostgresDeploymentName, checluster.Namespace)
if err != nil {
return "", err
}
stdout, err := util.K8sclient.ExecIntoPod(podName, selectSslRequiredCommand, "", checluster.Namespace)
func getSslRequiredForMasterRealm(cr *orgv1.CheCluster) (string, error) {
stdout, err := util.K8sclient.ExecIntoPod(
cr,
postgres.PostgresDeploymentName,
func(cr *orgv1.CheCluster) (string, error) {
return selectSslRequiredCommand, nil
},
"")
return strings.TrimSpace(stdout), err
}
func updateSslRequiredForMasterRealm(checluster *orgv1.CheCluster) error {
podName, err := util.K8sclient.GetDeploymentPod(postgres.PostgresDeploymentName, checluster.Namespace)
if err != nil {
return err
}
_, err = util.K8sclient.ExecIntoPod(podName, updateSslRequiredCommand, "Update ssl_required to NONE", checluster.Namespace)
func updateSslRequiredForMasterRealm(cr *orgv1.CheCluster) error {
_, err := util.K8sclient.ExecIntoPod(
cr,
postgres.PostgresDeploymentName,
func(cr *orgv1.CheCluster) (string, error) {
return updateSslRequiredCommand, nil
},
"Update ssl_required to NONE")
return err
}
@ -721,13 +718,11 @@ func ProvisionKeycloakResources(deployContext *deploy.DeployContext) error {
}
}
keycloakProvisionCommand := GetKeycloakProvisionCommand(deployContext.CheCluster)
podToExec, err := util.K8sclient.GetDeploymentPod(KeycloakDeploymentName, deployContext.CheCluster.Namespace)
if err != nil {
logrus.Errorf("Failed to retrieve pod name. Further exec will fail")
}
_, err = util.K8sclient.ExecIntoPod(podToExec, keycloakProvisionCommand, "create realm, client and user", deployContext.CheCluster.Namespace)
_, err := util.K8sclient.ExecIntoPod(
deployContext.CheCluster,
IdentityProviderDeploymentName,
GetKeycloakProvisionCommand,
"create realm, client and user")
return err
}

View File

@ -14,7 +14,6 @@ package identity_provider
import (
"bytes"
"io/ioutil"
"strings"
"text/template"
v1 "github.com/eclipse/che-operator/pkg/apis/org/v1"
@ -34,152 +33,140 @@ func GetPostgresProvisionCommand(identityProviderPostgresPassword string) (comma
return command
}
func GetKeycloakProvisionCommand(cr *v1.CheCluster) (command string) {
requiredActions := ""
updateAdminPassword := cr.Spec.Auth.UpdateAdminPassword
func GetKeycloakProvisionCommand(cr *v1.CheCluster) (command string, err error) {
cheFlavor := deploy.DefaultCheFlavor(cr)
keycloakRealm := util.GetValue(cr.Spec.Auth.IdentityProviderRealm, cheFlavor)
keycloakClientId := util.GetValue(cr.Spec.Auth.IdentityProviderClientId, cheFlavor+"-public")
keycloakUserEnvVar := "${KEYCLOAK_USER}"
keycloakPasswordEnvVar := "${KEYCLOAK_PASSWORD}"
requiredActions := (map[bool]string{true: "\"UPDATE_PASSWORD\"", false: ""})[cr.Spec.Auth.UpdateAdminPassword]
keycloakTheme := (map[bool]string{true: "rh-sso", false: "che"})[cheFlavor == "codeready"]
realmDisplayName := (map[bool]string{true: "CodeReady Workspaces", false: "Eclipse Che"})[cheFlavor == "codeready"]
if updateAdminPassword {
requiredActions = "\"UPDATE_PASSWORD\""
script, keycloakRealm, keycloakClientId, keycloakUserEnvVar, keycloakPasswordEnvVar := getDefaults(cr)
data := struct {
Script string
KeycloakAdminUserName string
KeycloakAdminPassword string
KeycloakRealm string
RealmDisplayName string
KeycloakTheme string
CheHost string
KeycloakClientId string
RequiredActions string
}{
script,
keycloakUserEnvVar,
keycloakPasswordEnvVar,
keycloakRealm,
realmDisplayName,
keycloakTheme,
cr.Spec.Server.CheHost,
keycloakClientId,
requiredActions,
}
file, err := ioutil.ReadFile("/tmp/keycloak_provision")
if err != nil {
logrus.Errorf("Failed to locate keycloak entrypoint file: %s", err)
}
keycloakTheme := "che"
realmDisplayName := "Eclipse Che"
script := "/opt/jboss/keycloak/bin/kcadm.sh"
if cheFlavor == "codeready" {
keycloakTheme = "rh-sso"
realmDisplayName = "CodeReady Workspaces"
script = "/opt/eap/bin/kcadm.sh"
keycloakUserEnvVar = "${SSO_ADMIN_USERNAME}"
keycloakPasswordEnvVar = "${SSO_ADMIN_PASSWORD}"
}
str := string(file)
r := strings.NewReplacer("$script", script,
"$keycloakAdminUserName", keycloakUserEnvVar,
"$keycloakAdminPassword", keycloakPasswordEnvVar,
"$keycloakRealm", keycloakRealm,
"$realmDisplayName", realmDisplayName,
"$keycloakClientId", keycloakClientId,
"$keycloakTheme", keycloakTheme,
"$cheHost", cr.Spec.Server.CheHost,
"$requiredActions", requiredActions)
createRealmClientUserCommand := r.Replace(str)
if cheFlavor == "che" {
command = "cd /scripts && export JAVA_TOOL_OPTIONS=-Duser.home=. && " + createRealmClientUserCommand
} else {
command = "cd /home/jboss && " + createRealmClientUserCommand
}
return command
return getCommandFromTemplateFile(cr, "/tmp/keycloak-provision.sh", data)
}
func GetOpenShiftIdentityProviderProvisionCommand(cr *v1.CheCluster, oAuthClientName string, oauthSecret string, isOpenShift4 bool) (command string, err error) {
cheFlavor := deploy.DefaultCheFlavor(cr)
func GetOpenShiftIdentityProviderProvisionCommand(cr *v1.CheCluster, oAuthClientName string, oauthSecret string) (string, error) {
isOpenShift4 := util.IsOpenShift4
providerId := (map[bool]string{true: "openshift-v4", false: "openshift-v3"})[isOpenShift4]
openShiftApiUrl, err := util.GetClusterPublicHostname(isOpenShift4)
if err != nil {
logrus.Errorf("Failed to auto-detect public OpenShift API URL. Configure it in Identity provider details page in Keycloak admin console: %s", err)
return "", err
}
keycloakUserEnvVar := "${KEYCLOAK_USER}"
keycloakPasswordEnvVar := "${KEYCLOAK_PASSWORD}"
keycloakRealm := util.GetValue(cr.Spec.Auth.IdentityProviderRealm, cheFlavor)
script := "/opt/jboss/keycloak/bin/kcadm.sh"
if cheFlavor == "codeready" {
script = "/opt/eap/bin/kcadm.sh"
keycloakUserEnvVar = "${SSO_ADMIN_USERNAME}"
keycloakPasswordEnvVar = "${SSO_ADMIN_PASSWORD}"
script, keycloakRealm, keycloakClientId, keycloakUserEnvVar, keycloakPasswordEnvVar := getDefaults(cr)
data := struct {
Script string
KeycloakAdminUserName string
KeycloakAdminPassword string
KeycloakRealm string
ProviderId string
OAuthClientName string
OauthSecret string
OpenShiftApiUrl string
KeycloakClientId string
}{
script,
keycloakUserEnvVar,
keycloakPasswordEnvVar,
keycloakRealm,
providerId,
oAuthClientName,
oauthSecret,
openShiftApiUrl,
keycloakClientId,
}
keycloakClientId := util.GetValue(cr.Spec.Auth.IdentityProviderClientId, cheFlavor+"-public")
return getCommandFromTemplateFile(cr, "/tmp/oauth-provision.sh", data)
}
providerId := "openshift-v3"
if isOpenShift4 {
providerId = "openshift-v4"
func GetGitHubIdentityProviderCreateCommand(deployContext *deploy.DeployContext) (string, error) {
cr := deployContext.CheCluster
script, keycloakRealm, _, keycloakUserEnvVar, keycloakPasswordEnvVar := getDefaults(cr)
data := struct {
Script string
KeycloakAdminUserName string
KeycloakAdminPassword string
KeycloakRealm string
ProviderId string
}{
script,
keycloakUserEnvVar,
keycloakPasswordEnvVar,
keycloakRealm,
"github",
}
return getCommandFromTemplateFile(cr, "/tmp/create-github-identity-provider.sh", data)
}
file, err := ioutil.ReadFile("/tmp/oauth_provision")
if err != nil {
logrus.Errorf("Failed to locate keycloak oauth provisioning file: %s", err)
func GetIdentityProviderDeleteCommand(cr *v1.CheCluster, identityProvider string) (string, error) {
script, keycloakRealm, _, keycloakUserEnvVar, keycloakPasswordEnvVar := getDefaults(cr)
data := struct {
Script string
KeycloakRealm string
KeycloakAdminUserName string
KeycloakAdminPassword string
ProviderId string
}{
script,
keycloakRealm,
keycloakUserEnvVar,
keycloakPasswordEnvVar,
identityProvider,
}
createOpenShiftIdentityProviderTemplate := string(file)
/*
In order to have the token-exchange currently working and easily usable, we should (in case of Keycloak) be able to
- Automatically redirect the user to its Keycloak account page to set those required values when the email is empty (instead of failing here: https://github.com/eclipse/che/blob/master/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitalizationFilter.java#L125)
- Or at least point with a link to the place where it can be set (the KeycloakSettings PROFILE_ENDPOINT_SETTING value)
(cf. here: https://github.com/eclipse/che/blob/master/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSettings.java#L117)
*/
return getCommandFromTemplateFile(cr, "/tmp/delete-identity-provider.sh", data)
}
template, err := template.New("IdentityProviderProvisioning").Parse(createOpenShiftIdentityProviderTemplate)
func getCommandFromTemplateFile(cr *v1.CheCluster, templateFile string, data interface{}) (string, error) {
cheFlavor := deploy.DefaultCheFlavor(cr)
file, err := ioutil.ReadFile(templateFile)
if err != nil {
return "", err
}
template, err := template.New("Template").Parse(string(file))
if err != nil {
return "", err
}
buffer := new(bytes.Buffer)
err = template.Execute(
buffer,
struct {
Script string
KeycloakAdminUserName string
KeycloakAdminPassword string
KeycloakRealm string
ProviderId string
OAuthClientName string
OauthSecret string
OpenShiftApiUrl string
KeycloakClientId string
}{
script,
keycloakUserEnvVar,
keycloakPasswordEnvVar,
keycloakRealm,
providerId,
oAuthClientName,
oauthSecret,
openShiftApiUrl,
keycloakClientId,
})
err = template.Execute(buffer, data)
if err != nil {
return "", err
}
if cheFlavor == "che" {
command = "cd /scripts && export JAVA_TOOL_OPTIONS=-Duser.home=. && " + buffer.String()
} else {
command = "cd /home/jboss && " + buffer.String()
return "cd /scripts && export JAVA_TOOL_OPTIONS=-Duser.home=. && " + buffer.String(), nil
}
return command, nil
return "cd /home/jboss && " + buffer.String(), nil
}
func GetDeleteOpenShiftIdentityProviderProvisionCommand(cr *v1.CheCluster, isOpenShift4 bool) (command string) {
func getDefaults(cr *v1.CheCluster) (string, string, string, string, string) {
cheFlavor := deploy.DefaultCheFlavor(cr)
keycloakRealm := util.GetValue(cr.Spec.Auth.IdentityProviderRealm, cheFlavor)
script := "/opt/jboss/keycloak/bin/kcadm.sh"
keycloakUserEnvVar := "${KEYCLOAK_USER}"
keycloakPasswordEnvVar := "${KEYCLOAK_PASSWORD}"
keycloakClientId := util.GetValue(cr.Spec.Auth.IdentityProviderClientId, cheFlavor+"-public")
if cheFlavor == "codeready" {
script = "/opt/eap/bin/kcadm.sh"
keycloakUserEnvVar = "${SSO_ADMIN_USERNAME}"
keycloakPasswordEnvVar = "${SSO_ADMIN_PASSWORD}"
return "/opt/eap/bin/kcadm.sh", keycloakRealm, keycloakClientId, "${SSO_ADMIN_USERNAME}", "${SSO_ADMIN_PASSWORD}"
}
providerName := "openshift-v3"
if isOpenShift4 {
providerName = "openshift-v4"
}
deleteOpenShiftIdentityProviderCommand :=
script + " config credentials --server http://0.0.0.0:8080/auth " +
"--realm master --user " + keycloakUserEnvVar + " --password " + keycloakPasswordEnvVar + " && " +
"if " + script + " get identity-provider/instances/" + providerName + " -r " + keycloakRealm + " ; then " +
script + " delete identity-provider/instances/" + providerName + " -r " + keycloakRealm + " ; fi"
if cheFlavor == "che" {
command = "cd /scripts && export JAVA_TOOL_OPTIONS=-Duser.home=. && " + deleteOpenShiftIdentityProviderCommand
} else {
command = "cd /home/jboss && " + deleteOpenShiftIdentityProviderCommand
}
return command
return "/opt/jboss/keycloak/bin/kcadm.sh", keycloakRealm, keycloakClientId, "${KEYCLOAK_USER}", "${KEYCLOAK_PASSWORD}"
}

View File

@ -16,106 +16,128 @@ import (
"fmt"
"strings"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/selection"
orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1"
"github.com/eclipse/che-operator/pkg/deploy"
"github.com/eclipse/che-operator/pkg/deploy/expose"
"github.com/eclipse/che-operator/pkg/util"
"github.com/google/go-cmp/cmp/cmpopts"
oauth "github.com/openshift/api/oauth/v1"
"github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)
const (
Keycloak = "keycloak"
IdentityProviderServiceName = "keycloak"
IdentityProviderExposureName = "keycloak"
IdentityProviderDeploymentName = "keycloak"
)
var (
oAuthClientDiffOpts = cmpopts.IgnoreFields(oauth.OAuthClient{}, "TypeMeta", "ObjectMeta")
syncItems = []func(*deploy.DeployContext) (bool, error){
syncService,
syncExposure,
SyncKeycloakDeploymentToCluster,
syncKeycloakResources,
syncOpenShiftIdentityProvider,
SyncGitHubOAuth,
}
)
// SyncIdentityProviderToCluster instantiates the identity provider (Keycloak) in the cluster. Returns true if
// the provisioning is complete, false if requeue of the reconcile request is needed.
func SyncIdentityProviderToCluster(deployContext *deploy.DeployContext) (bool, error) {
instance := deployContext.CheCluster
cheHost := instance.Spec.Server.CheHost
protocol := "http"
if instance.Spec.Server.TlsSupport {
protocol = "https"
}
cheFlavor := deploy.DefaultCheFlavor(instance)
cheMultiUser := deploy.GetCheMultiUser(instance)
tests := util.IsTestMode()
isOpenShift := util.IsOpenShift
if instance.Spec.Auth.ExternalIdentityProvider {
cr := deployContext.CheCluster
if cr.Spec.Auth.ExternalIdentityProvider {
return true, nil
}
cheMultiUser := deploy.GetCheMultiUser(cr)
if cheMultiUser == "false" {
if util.K8sclient.IsDeploymentExists("keycloak", instance.Namespace) {
util.K8sclient.DeleteDeployment("keycloak", instance.Namespace)
if util.K8sclient.IsDeploymentExists("keycloak", cr.Namespace) {
util.K8sclient.DeleteDeployment("keycloak", cr.Namespace)
}
return true, nil
}
keycloakLabels := deploy.GetLabels(instance, "keycloak")
serviceStatus := deploy.SyncServiceToCluster(deployContext, "keycloak", []string{"http"}, []int32{8080}, keycloakLabels)
if !tests {
if !serviceStatus.Continue {
logrus.Info("Waiting on service 'keycloak' to be ready")
if serviceStatus.Err != nil {
logrus.Error(serviceStatus.Err)
for _, syncItem := range syncItems {
provisioned, err := syncItem(deployContext)
if !util.IsTestMode() {
if !provisioned {
return false, err
}
return false, serviceStatus.Err
}
}
additionalLabels := (map[bool]string{true: instance.Spec.Auth.IdentityProviderRoute.Labels, false: instance.Spec.Auth.IdentityProviderIngress.Labels})[util.IsOpenShift]
endpoint, done, err := expose.Expose(deployContext, cheHost, Keycloak, additionalLabels)
return true, nil
}
func syncService(deployContext *deploy.DeployContext) (bool, error) {
cr := deployContext.CheCluster
labels := deploy.GetLabels(cr, IdentityProviderServiceName)
serviceStatus := deploy.SyncServiceToCluster(
deployContext,
IdentityProviderServiceName,
[]string{"http"},
[]int32{8080},
labels)
return serviceStatus.Continue, serviceStatus.Err
}
func syncExposure(deployContext *deploy.DeployContext) (bool, error) {
cr := deployContext.CheCluster
protocol := (map[bool]string{
true: "https",
false: "http"})[cr.Spec.Server.TlsSupport]
additionalLabels := (map[bool]string{
true: cr.Spec.Auth.IdentityProviderRoute.Labels,
false: cr.Spec.Auth.IdentityProviderIngress.Labels})[util.IsOpenShift]
endpoint, done, err := expose.Expose(
deployContext,
cr.Spec.Server.CheHost,
IdentityProviderExposureName,
additionalLabels)
if !done {
return false, err
}
keycloakURL := protocol + "://" + endpoint
deployContext.InternalService.KeycloakHost = fmt.Sprintf("%s://%s.%s.svc:%d", "http", "keycloak", deployContext.CheCluster.Namespace, 8080)
if instance.Spec.Auth.IdentityProviderURL != keycloakURL {
instance.Spec.Auth.IdentityProviderURL = keycloakURL
keycloakURL := protocol + "://" + endpoint
deployContext.InternalService.KeycloakHost = fmt.Sprintf("%s://%s.%s.svc:%d", "http", "keycloak", cr.Namespace, 8080)
if cr.Spec.Auth.IdentityProviderURL != keycloakURL {
cr.Spec.Auth.IdentityProviderURL = keycloakURL
if err := deploy.UpdateCheCRSpec(deployContext, "Keycloak URL", keycloakURL); err != nil {
return false, err
}
instance.Status.KeycloakURL = keycloakURL
cr.Status.KeycloakURL = keycloakURL
if err := deploy.UpdateCheCRStatus(deployContext, "Keycloak URL", keycloakURL); err != nil {
return false, err
}
}
deploymentStatus := SyncKeycloakDeploymentToCluster(deployContext)
if !tests {
if !deploymentStatus.Continue {
logrus.Info("Waiting on deployment 'keycloak' to be ready")
if deploymentStatus.Err != nil {
logrus.Error(deploymentStatus.Err)
}
return true, nil
}
return false, deploymentStatus.Err
}
}
if !tests {
if !instance.Status.KeycloakProvisoned {
func syncKeycloakResources(deployContext *deploy.DeployContext) (bool, error) {
if !util.IsTestMode() {
cr := deployContext.CheCluster
if !cr.Status.KeycloakProvisoned {
if err := ProvisionKeycloakResources(deployContext); err != nil {
logrus.Error(err)
return false, err
}
for {
instance.Status.KeycloakProvisoned = true
cr.Status.KeycloakProvisoned = true
if err := deploy.UpdateCheCRStatus(deployContext, "status: provisioned with Keycloak", "true"); err != nil &&
errors.IsConflict(err) {
@ -127,61 +149,64 @@ func SyncIdentityProviderToCluster(deployContext *deploy.DeployContext) (bool, e
}
}
if isOpenShift && util.IsOAuthEnabled(instance) {
if err := SyncIdentityProviderItems(deployContext, cheFlavor); err != nil {
return true, nil
}
func syncOpenShiftIdentityProvider(deployContext *deploy.DeployContext) (bool, error) {
cr := deployContext.CheCluster
if util.IsOpenShift && util.IsOAuthEnabled(cr) {
return SyncOpenShiftIdentityProviderItems(deployContext)
}
return true, nil
}
func SyncOpenShiftIdentityProviderItems(deployContext *deploy.DeployContext) (bool, error) {
cr := deployContext.CheCluster
oAuthClientName := cr.Spec.Auth.OAuthClientName
if len(oAuthClientName) < 1 {
oAuthClientName = cr.Name + "-openshift-identity-provider-" + strings.ToLower(util.GeneratePasswd(6))
cr.Spec.Auth.OAuthClientName = oAuthClientName
if err := deploy.UpdateCheCRSpec(deployContext, "oAuthClient name", oAuthClientName); err != nil {
return false, err
}
}
return true, nil
}
func SyncIdentityProviderItems(deployContext *deploy.DeployContext, cheFlavor string) error {
instance := deployContext.CheCluster
tests := util.IsTestMode()
isOpenShift4 := util.IsOpenShift4
keycloakDeploymentName := KeycloakDeploymentName
oAuthClientName := instance.Spec.Auth.OAuthClientName
if len(oAuthClientName) < 1 {
oAuthClientName = instance.Name + "-openshift-identity-provider-" + strings.ToLower(util.GeneratePasswd(6))
instance.Spec.Auth.OAuthClientName = oAuthClientName
if err := deploy.UpdateCheCRSpec(deployContext, "oAuthClient name", oAuthClientName); err != nil {
return err
}
}
oauthSecret := instance.Spec.Auth.OAuthSecret
oauthSecret := cr.Spec.Auth.OAuthSecret
if len(oauthSecret) < 1 {
oauthSecret = util.GeneratePasswd(12)
instance.Spec.Auth.OAuthSecret = oauthSecret
cr.Spec.Auth.OAuthSecret = oauthSecret
if err := deploy.UpdateCheCRSpec(deployContext, "oAuth secret name", oauthSecret); err != nil {
return err
return false, err
}
}
keycloakURL := instance.Spec.Auth.IdentityProviderURL
keycloakRealm := util.GetValue(instance.Spec.Auth.IdentityProviderRealm, cheFlavor)
oAuthClient := deploy.NewOAuthClient(oAuthClientName, oauthSecret, keycloakURL, keycloakRealm, isOpenShift4)
if _, err := deploy.Sync(deployContext, oAuthClient, oAuthClientDiffOpts); err != nil {
return err
keycloakURL := cr.Spec.Auth.IdentityProviderURL
cheFlavor := deploy.DefaultCheFlavor(cr)
keycloakRealm := util.GetValue(cr.Spec.Auth.IdentityProviderRealm, cheFlavor)
oAuthClient := deploy.NewOAuthClient(oAuthClientName, oauthSecret, keycloakURL, keycloakRealm, util.IsOpenShift4)
provisioned, err := deploy.Sync(deployContext, oAuthClient, oAuthClientDiffOpts)
if !provisioned {
return false, err
}
if !tests && !instance.Status.OpenShiftoAuthProvisioned {
// note that this uses the instance.Spec.Auth.IdentityProviderRealm and instance.Spec.Auth.IdentityProviderClientId.
// because we're not doing much of a change detection on those fields, we can't react on them changing here.
openShiftIdentityProviderCommand, err := GetOpenShiftIdentityProviderProvisionCommand(instance, oAuthClientName, oauthSecret, isOpenShift4)
if err != nil {
logrus.Errorf("Failed to build identity provider provisioning command")
return err
}
podToExec, err := util.K8sclient.GetDeploymentPod(keycloakDeploymentName, instance.Namespace)
if err != nil {
logrus.Errorf("Failed to retrieve pod name. Further exec will fail")
return err
}
_, err = util.K8sclient.ExecIntoPod(podToExec, openShiftIdentityProviderCommand, "create OpenShift identity provider", instance.Namespace)
if err == nil {
if !util.IsTestMode() {
if !cr.Status.OpenShiftoAuthProvisioned {
// note that this uses the instance.Spec.Auth.IdentityProviderRealm and instance.Spec.Auth.IdentityProviderClientId.
// because we're not doing much of a change detection on those fields, we can't react on them changing here.
_, err := util.K8sclient.ExecIntoPod(
cr,
IdentityProviderDeploymentName,
func(cr *orgv1.CheCluster) (string, error) {
return GetOpenShiftIdentityProviderProvisionCommand(cr, oAuthClientName, oauthSecret)
},
"Create OpenShift identity provider")
if err != nil {
return false, err
}
for {
instance.Status.OpenShiftoAuthProvisioned = true
cr.Status.OpenShiftoAuthProvisioned = true
if err := deploy.UpdateCheCRStatus(deployContext, "status: provisioned with OpenShift identity provider", "true"); err != nil &&
errors.IsConflict(err) {
@ -192,7 +217,80 @@ func SyncIdentityProviderItems(deployContext *deploy.DeployContext, cheFlavor st
}
}
}
return nil
return true, nil
}
// SyncGitHubOAuth provisions GitHub OAuth if secret with
// annotation `che.eclipse.org/github-oauth-credentials=true` is mounted into a container
func SyncGitHubOAuth(deployContext *deploy.DeployContext) (bool, error) {
cr := deployContext.CheCluster
// find mounted GitHug OAuth credentials
secrets := &corev1.SecretList{}
kubernetesPartOfLabelSelectorRequirement, _ := labels.NewRequirement(deploy.KubernetesPartOfLabelKey, selection.Equals, []string{deploy.CheEclipseOrg})
kubernetesComponentLabelSelectorRequirement, _ := labels.NewRequirement(deploy.KubernetesComponentLabelKey, selection.Equals, []string{IdentityProviderDeploymentName + "-secret"})
listOptions := &client.ListOptions{
LabelSelector: labels.NewSelector().
Add(*kubernetesPartOfLabelSelectorRequirement).
Add(*kubernetesComponentLabelSelectorRequirement),
}
if err := deployContext.ClusterAPI.Client.List(context.TODO(), secrets, listOptions); err != nil {
return false, err
}
isGitHubOAuthCredentialsExists := false
for _, secret := range secrets.Items {
if secret.Annotations[deploy.CheEclipseOrgGithubOAuthCredentials] == "true" {
isGitHubOAuthCredentialsExists = true
break
}
}
if isGitHubOAuthCredentialsExists {
if !cr.Status.GitHubOAuthProvisioned {
if !util.IsTestMode() {
_, err := util.K8sclient.ExecIntoPod(
cr,
IdentityProviderDeploymentName,
func(cr *orgv1.CheCluster) (string, error) {
return GetGitHubIdentityProviderCreateCommand(deployContext)
},
"Create GitHub OAuth")
if err != nil {
return false, err
}
}
cr.Status.GitHubOAuthProvisioned = true
if err := deploy.UpdateCheCRStatus(deployContext, "status: GitHub OAuth provisioned", "true"); err != nil {
return false, err
}
}
} else {
if cr.Status.GitHubOAuthProvisioned {
if !util.IsTestMode() {
_, err := util.K8sclient.ExecIntoPod(
cr,
IdentityProviderDeploymentName,
func(cr *orgv1.CheCluster) (string, error) {
return GetIdentityProviderDeleteCommand(cr, "github")
},
"Delete GitHub OAuth")
if err != nil {
return false, err
}
}
cr.Status.GitHubOAuthProvisioned = false
if err := deploy.UpdateCheCRStatus(deployContext, "status: GitHub OAuth provisioned", "false"); err != nil {
return false, err
}
}
}
return true, nil
}
func reload(deployContext *deploy.DeployContext) error {

View File

@ -0,0 +1,162 @@
//
// Copyright (c) 2012-2019 Red Hat, Inc.
// This program and the accompanying materials are made
// available under the terms of the Eclipse Public License 2.0
// which is available at https://www.eclipse.org/legal/epl-2.0/
//
// SPDX-License-Identifier: EPL-2.0
//
// Contributors:
// Red Hat, Inc. - initial API and implementation
//
package identity_provider
import (
"os"
"reflect"
"github.com/eclipse/che-operator/pkg/deploy"
"github.com/google/go-cmp/cmp"
orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
"testing"
)
func TestSyncGitHubOAuth(t *testing.T) {
type testCase struct {
name string
initCR *orgv1.CheCluster
expectedCR *orgv1.CheCluster
initObjects []runtime.Object
}
testCases := []testCase{
{
name: "Should provision GitHub OAuth",
initCR: &orgv1.CheCluster{
ObjectMeta: metav1.ObjectMeta{
Namespace: "eclipse-che",
},
},
expectedCR: &orgv1.CheCluster{
ObjectMeta: metav1.ObjectMeta{
Namespace: "eclipse-che",
ResourceVersion: "1",
},
Status: orgv1.CheClusterStatus{
GitHubOAuthProvisioned: true,
},
},
initObjects: []runtime.Object{
&corev1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "github-credentials",
Namespace: "eclipse-che",
Labels: map[string]string{
deploy.KubernetesPartOfLabelKey: deploy.CheEclipseOrg,
deploy.KubernetesComponentLabelKey: "keycloak-secret",
},
Annotations: map[string]string{
deploy.CheEclipseOrgGithubOAuthCredentials: "true",
},
},
Data: map[string][]byte{
"key": []byte("key-data"),
},
},
},
},
{
name: "Should not provision GitHub OAuth",
initCR: &orgv1.CheCluster{
ObjectMeta: metav1.ObjectMeta{
Namespace: "eclipse-che",
},
},
expectedCR: &orgv1.CheCluster{
ObjectMeta: metav1.ObjectMeta{
Namespace: "eclipse-che",
},
},
initObjects: []runtime.Object{
&corev1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "github-credentials",
Namespace: "eclipse-che",
Labels: map[string]string{
deploy.KubernetesPartOfLabelKey: deploy.CheEclipseOrg,
deploy.KubernetesComponentLabelKey: "keycloak-secret",
},
},
Data: map[string][]byte{
"key": []byte("key-data"),
},
},
},
},
{
name: "Should delete GitHub OAuth",
initCR: &orgv1.CheCluster{
ObjectMeta: metav1.ObjectMeta{
Namespace: "eclipse-che",
},
Status: orgv1.CheClusterStatus{
GitHubOAuthProvisioned: true,
},
},
expectedCR: &orgv1.CheCluster{
ObjectMeta: metav1.ObjectMeta{
Namespace: "eclipse-che",
ResourceVersion: "1",
},
Status: orgv1.CheClusterStatus{
GitHubOAuthProvisioned: false,
},
},
initObjects: []runtime.Object{},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
logf.SetLogger(zap.LoggerTo(os.Stdout, true))
orgv1.SchemeBuilder.AddToScheme(scheme.Scheme)
testCase.initObjects = append(testCase.initObjects, testCase.initCR)
cli := fake.NewFakeClientWithScheme(scheme.Scheme, testCase.initObjects...)
deployContext := &deploy.DeployContext{
CheCluster: testCase.initCR,
ClusterAPI: deploy.ClusterAPI{
Client: cli,
Scheme: scheme.Scheme,
},
}
_, err := SyncGitHubOAuth(deployContext)
if err != nil {
t.Fatalf("Error mounting secret: %v", err)
}
if !reflect.DeepEqual(testCase.expectedCR, testCase.initCR) {
t.Errorf("Expected CR and CR returned from API server differ (-want, +got): %v", cmp.Diff(testCase.expectedCR, testCase.initCR))
}
})
}
}

View File

@ -18,7 +18,7 @@ import (
corev1 "k8s.io/api/core/v1"
)
func SyncPluginRegistryDeploymentToCluster(deployContext *deploy.DeployContext) deploy.DeploymentProvisioningStatus {
func SyncPluginRegistryDeploymentToCluster(deployContext *deploy.DeployContext) (bool, error) {
registryType := "plugin"
registryImage := util.GetValue(deployContext.CheCluster.Spec.Server.PluginRegistryImage, deploy.DefaultPluginRegistryImage(deployContext.CheCluster))
registryImagePullPolicy := corev1.PullPolicy(util.GetValue(string(deployContext.CheCluster.Spec.Server.PluginRegistryPullPolicy), deploy.DefaultPullPolicyFromDockerImage(registryImage)))
@ -29,9 +29,7 @@ func SyncPluginRegistryDeploymentToCluster(deployContext *deploy.DeployContext)
clusterDeployment, err := deploy.GetClusterDeployment(deploy.PluginRegistry, deployContext.CheCluster.Namespace, deployContext.ClusterAPI.Client)
if err != nil {
return deploy.DeploymentProvisioningStatus{
ProvisioningStatus: deploy.ProvisioningStatus{Err: err},
}
return false, err
}
specDeployment, err := registry.GetSpecRegistryDeployment(
@ -45,9 +43,7 @@ func SyncPluginRegistryDeploymentToCluster(deployContext *deploy.DeployContext)
probePath)
if err != nil {
return deploy.DeploymentProvisioningStatus{
ProvisioningStatus: deploy.ProvisioningStatus{Err: err},
}
return false, err
}
return deploy.SyncDeploymentToCluster(deployContext, specDeployment, clusterDeployment, nil, nil)

View File

@ -14,6 +14,7 @@ package plugin_registry
import (
"encoding/json"
"fmt"
"github.com/eclipse/che-operator/pkg/deploy"
"github.com/eclipse/che-operator/pkg/deploy/expose"
"github.com/eclipse/che-operator/pkg/util"
@ -77,15 +78,14 @@ func SyncPluginRegistryToCluster(deployContext *deploy.DeployContext, cheHost st
deployContext.InternalService.PluginRegistryHost = fmt.Sprintf("http://%s.%s.svc:8080/v3", deploy.PluginRegistry, deployContext.CheCluster.Namespace)
// Deploy plugin registry
deploymentStatus := SyncPluginRegistryDeploymentToCluster(deployContext)
provisioned, err := SyncPluginRegistryDeploymentToCluster(deployContext)
if !util.IsTestMode() {
if !deploymentStatus.Continue {
if !provisioned {
logrus.Info("Waiting on deployment '" + deploy.PluginRegistry + "' to be ready")
if deploymentStatus.Err != nil {
logrus.Error(deploymentStatus.Err)
if err != nil {
logrus.Error(err)
}
return false, deploymentStatus.Err
return provisioned, err
}
}
}

View File

@ -30,19 +30,15 @@ var (
postgresAdminPassword = util.GeneratePasswd(12)
)
func SyncPostgresDeploymentToCluster(deployContext *deploy.DeployContext) deploy.DeploymentProvisioningStatus {
func SyncPostgresDeploymentToCluster(deployContext *deploy.DeployContext) (bool, error) {
clusterDeployment, err := deploy.GetClusterDeployment(PostgresDeploymentName, deployContext.CheCluster.Namespace, deployContext.ClusterAPI.Client)
if err != nil {
return deploy.DeploymentProvisioningStatus{
ProvisioningStatus: deploy.ProvisioningStatus{Err: err},
}
return false, err
}
specDeployment, err := getSpecPostgresDeployment(deployContext, clusterDeployment)
if err != nil {
return deploy.DeploymentProvisioningStatus{
ProvisioningStatus: deploy.ProvisioningStatus{Err: err},
}
return false, err
}
return deploy.SyncDeploymentToCluster(deployContext, specDeployment, clusterDeployment, nil, nil)

View File

@ -27,19 +27,15 @@ import (
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
func SyncCheDeploymentToCluster(deployContext *deploy.DeployContext) deploy.DeploymentProvisioningStatus {
func SyncCheDeploymentToCluster(deployContext *deploy.DeployContext) (bool, error) {
clusterDeployment, err := deploy.GetClusterDeployment(deploy.DefaultCheFlavor(deployContext.CheCluster), deployContext.CheCluster.Namespace, deployContext.ClusterAPI.Client)
if err != nil {
return deploy.DeploymentProvisioningStatus{
ProvisioningStatus: deploy.ProvisioningStatus{Err: err},
}
return false, err
}
specDeployment, err := getSpecCheDeployment(deployContext)
if err != nil {
return deploy.DeploymentProvisioningStatus{
ProvisioningStatus: deploy.ProvisioningStatus{Err: err},
}
return false, err
}
return deploy.SyncDeploymentToCluster(deployContext, specDeployment, clusterDeployment, nil, nil)

View File

@ -100,9 +100,7 @@ func update(deployContext *DeployContext, actual runtime.Object, blueprint metav
}
err = clusterAPI.Client.Create(context.TODO(), obj)
if err != nil {
return false, err
}
return false, err
} else {
obj, err := setOwnerReferenceAndConvertToRuntime(deployContext, blueprint)
if err != nil {
@ -113,14 +111,10 @@ func update(deployContext *DeployContext, actual runtime.Object, blueprint metav
obj.(metav1.Object).SetResourceVersion(actualMeta.GetResourceVersion())
err = clusterAPI.Client.Update(context.TODO(), obj)
if err != nil {
return false, err
}
return false, err
}
return true, nil
}
return false, nil
return true, nil
}
func isUpdateUsingDeleteCreate(kind string) bool {

View File

@ -16,6 +16,7 @@ import (
"fmt"
"io"
v1 "github.com/eclipse/che-operator/pkg/apis/org/v1"
"github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -53,7 +54,26 @@ func GetK8Client() *k8s {
return nil
}
func (cl *k8s) ExecIntoPod(podName string, command string, reason string, namespace string) (string, error) {
func (cl *k8s) ExecIntoPod(
cr *v1.CheCluster,
deploymentName string,
getCommand func(*v1.CheCluster) (string, error),
reason string) (string, error) {
command, err := getCommand(cr)
if err != nil {
return "", err
}
pod, err := cl.GetDeploymentPod(deploymentName, cr.Namespace)
if err != nil {
return "", err
}
return cl.DoExecIntoPod(cr.Namespace, pod, command, reason)
}
func (cl *k8s) DoExecIntoPod(namespace string, podName string, command string, reason string) (string, error) {
if reason != "" {
logrus.Infof("Running exec for '%s' in the pod '%s'", reason, podName)
}

View File

@ -0,0 +1,41 @@
#
# Copyright (c) 2020 Red Hat, Inc.
# This program and the accompanying materials are made
# available under the terms of the Eclipse Public License 2.0
# which is available at https://www.eclipse.org/legal/epl-2.0/
#
# SPDX-License-Identifier: EPL-2.0
#
# Contributors:
# Red Hat, Inc. - initial API and implementation
#
connectToKeycloak() {
{{ .Script }} config credentials --server http://0.0.0.0:8080/auth --realm master --user {{ .KeycloakAdminUserName }} --password {{ .KeycloakAdminPassword }}
}
createIdentityProvider() {
{{ .Script }} get identity-provider/instances/{{ .ProviderId }} -r {{ .KeycloakRealm }}
if [ $? -eq 0 ]; then
echo "{{ .ProviderId }} identity provider exists."
else
echo "Create new {{ .ProviderId }} identity provider."
if [ -z "${GITHUB_CLIENT_ID}" ] || [ -z "${GITHUB_SECRET}" ]; then
echo "Either 'GITHUB_CLIENT_ID' or 'GITHUB_SECRET' aren't set" 1>&2
exit 1
fi
{{ .Script }} create identity-provider/instances \
-r {{ .KeycloakRealm }} \
-s alias={{ .ProviderId }} \
-s providerId={{ .ProviderId }} \
-s enabled=true \
-s storeToken=true \
-s config.useJwksUrl=true \
-s config.clientId=${GITHUB_CLIENT_ID} \
-s config.clientSecret=${GITHUB_SECRET} \
-s config.defaultScope=repo,user,write:public_key
fi
}
connectToKeycloak
createIdentityProvider

View File

@ -0,0 +1,28 @@
#
# Copyright (c) 2020 Red Hat, Inc.
# This program and the accompanying materials are made
# available under the terms of the Eclipse Public License 2.0
# which is available at https://www.eclipse.org/legal/epl-2.0/
#
# SPDX-License-Identifier: EPL-2.0
#
# Contributors:
# Red Hat, Inc. - initial API and implementation
#
connectToKeycloak() {
{{ .Script }} config credentials --server http://0.0.0.0:8080/auth --realm master --user {{ .KeycloakAdminUserName }} --password {{ .KeycloakAdminPassword }}
}
deleteIdentityProvider() {
{{ .Script }} get identity-provider/instances/{{ .ProviderId }} -r {{ .KeycloakRealm }}
if [ ! $? -eq 0 ]; then
echo "{{ .ProviderId }} identity provider does not exists."
else
echo "Delete {{ .ProviderId }} identity provider."
{{ .Script }} delete identity-provider/instances/{{ .ProviderId }} -r {{ .KeycloakRealm }}
fi
}
connectToKeycloak
deleteIdentityProvider

View File

@ -0,0 +1,74 @@
#
# Copyright (c) 2020 Red Hat, Inc.
# This program and the accompanying materials are made
# available under the terms of the Eclipse Public License 2.0
# which is available at https://www.eclipse.org/legal/epl-2.0/
#
# SPDX-License-Identifier: EPL-2.0
#
# Contributors:
# Red Hat, Inc. - initial API and implementation
#
connectToKeycloak() {
{{ .Script }} config credentials --server http://0.0.0.0:8080/auth --realm master --user {{ .KeycloakAdminUserName }} --password {{ .KeycloakAdminPassword }}
}
provisionKeycloak() {
{{ .Script }} update realms/master -s sslRequired=none
{{ .Script }} config truststore --trustpass ${SSO_TRUSTSTORE_PASSWORD} ${SSO_TRUSTSTORE_DIR}/${SSO_TRUSTSTORE}
{{ .Script }} get realms/{{ .KeycloakRealm }}
if [ $? -eq 0 ]; then
echo "{{ .KeycloakRealm }} realm exists."
exit 0
fi
echo "Provision {{ .KeycloakRealm }} realm."
{{ .Script }} create realms \
-s realm='{{ .KeycloakRealm }}' \
-s displayName='{{ .RealmDisplayName }}' \
-s enabled=true \
-s sslRequired=none \
-s registrationAllowed=false \
-s resetPasswordAllowed=true \
-s loginTheme={{ .KeycloakTheme }} \
-s accountTheme={{ .KeycloakTheme }} \
-s adminTheme={{ .KeycloakTheme }} \
-s emailTheme={{ .KeycloakTheme }}
{{ .Script }} create clients \
-r '{{ .KeycloakRealm }}' \
-s clientId={{ .KeycloakClientId }} \
-s id={{ .KeycloakClientId }} \
-s webOrigins='["http://{{ .CheHost }}", "https://{{ .CheHost }}"]' \
-s redirectUris='["http://{{ .CheHost }}/dashboard/*", "https://{{ .CheHost }}/dashboard/*", "http://{{ .CheHost }}/workspace-loader/*", "https://{{ .CheHost }}/workspace-loader/*", "http://{{ .CheHost }}/_app/*", "https://{{ .CheHost }}/_app/*", "http://{{ .CheHost }}/swagger/*", "https://{{ .CheHost }}/swagger/*"]' \
-s directAccessGrantsEnabled=true \
-s publicClient=true
{{ .Script }} create users \
-r '{{ .KeycloakRealm }}' \
-s username=admin \
-s email=\"admin@admin.com\" \
-s enabled=true \
-s requiredActions='[{{ .RequiredActions }}]'
{{ .Script }} set-password \
-r '{{ .KeycloakRealm }}' \
--username admin \
--new-password admin
{{ .Script }} add-roles \
-r '{{ .KeycloakRealm }}' \
--uusername admin \
--cclientid broker \
--rolename read-token
CLIENT_ID=$({{ .Script }} get clients -r '{{ .KeycloakRealm }}' -q clientId=broker | sed -n 's/.*"id" *: *"\([^"]\+\).*/\1/p')
{{ .Script }} update clients/${CLIENT_ID} \
-r '{{ .KeycloakRealm }}' \
-s "defaultRoles+=read-token"
}
connectToKeycloak
provisionKeycloak

View File

@ -1,37 +0,0 @@
$script config credentials --server http://0.0.0.0:8080/auth \
--realm master \
--user $keycloakAdminUserName \
--password $keycloakAdminPassword \
&& $script update realms/master -s sslRequired=none \
&& $script config truststore --trustpass ${SSO_TRUSTSTORE_PASSWORD} ${SSO_TRUSTSTORE_DIR}/${SSO_TRUSTSTORE} \
&& $script get realms/$keycloakRealm; \
if [ $? -eq 0 ]; then echo "Realm exists"; exit 0; fi \
&& $script create realms -s realm='$keycloakRealm' \
-s displayName='$realmDisplayName' \
-s enabled=true \
-s sslRequired=none \
-s registrationAllowed=false \
-s resetPasswordAllowed=true \
-s loginTheme=$keycloakTheme \
-s accountTheme=$keycloakTheme \
-s adminTheme=$keycloakTheme \
-s emailTheme=$keycloakTheme \
&& $script create clients -r '$keycloakRealm' \
-s clientId=$keycloakClientId \
-s id=$keycloakClientId \
-s 'webOrigins=["http://$cheHost", "https://$cheHost"]' \
-s 'redirectUris=["http://$cheHost/dashboard/*", "https://$cheHost/dashboard/*", "http://$cheHost/workspace-loader/*", "https://$cheHost/workspace-loader/*", "http://$cheHost/_app/*", "https://$cheHost/_app/*", "http://$cheHost/swagger/*", "https://$cheHost/swagger/*"]' \
-s 'directAccessGrantsEnabled'=true \
-s publicClient=true \
&& $script create users -s username=admin \
-s email=\"admin@admin.com\" \
-s enabled=true -r '$keycloakRealm' \
-s 'requiredActions=[$requiredActions]' \
&& $script set-password -r '$keycloakRealm' --username admin \
--new-password admin \
&& $script add-roles -r '$keycloakRealm' \
--uusername admin \
--cclientid broker \
--rolename read-token \
&& CLIENT_ID=$($script get clients -r '$keycloakRealm' -q clientId=broker | sed -n 's/.*"id" *: *"\([^"]\+\).*/\1/p') \
&& $script update clients/$CLIENT_ID -r '$keycloakRealm' -s "defaultRoles+=read-token"

View File

@ -1,3 +1,15 @@
#
# Copyright (c) 2020 Red Hat, Inc.
# This program and the accompanying materials are made
# available under the terms of the Eclipse Public License 2.0
# which is available at https://www.eclipse.org/legal/epl-2.0/
#
# SPDX-License-Identifier: EPL-2.0
#
# Contributors:
# Red Hat, Inc. - initial API and implementation
#
connect_to_keycloak() {
{{ .Script }} config credentials --server http://0.0.0.0:8080/auth --realm master --user {{ .KeycloakAdminUserName }} --password {{ .KeycloakAdminPassword }}
{{ .Script }} config truststore --trustpass ${SSO_TRUSTSTORE_PASSWORD} ${SSO_TRUSTSTORE_DIR}/${SSO_TRUSTSTORE}