diff --git a/.gitignore b/.gitignore index 367c81d04..02020b6af 100644 --- a/.gitignore +++ b/.gitignore @@ -107,5 +107,7 @@ tags !.vscode/extensions.json .history +build/ +bin/ # End of https://www.gitignore.io/api/go,vim,emacs,visualstudiocode diff --git a/Dockerfile b/Dockerfile index b07827a7a..870a8c49d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,12 +20,12 @@ USER root #RUN subscription-manager register --username me --password mypwd --auto-attach #RUN subscription-manager repos --enable rhel-7-server-optional-rpms --enable rhel-server-rhscl-7-rpms ADD . /go/src/github.com/eclipse/che-operator -RUN cd /go/src/github.com/eclipse/che-operator && go test -v ./... && \ +RUN cd /go/src/github.com/eclipse/che-operator && export MOCK_API=true && go test -v ./... && \ OOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o /tmp/che-operator/che-operator \ - /go/src/github.com/eclipse/che-operator/cmd/che-operator/main.go && cd .. + /go/src/github.com/eclipse/che-operator/cmd/manager/main.go && cd .. # https://access.redhat.com/containers/?tab=tags#/registry.access.redhat.com/rhel7 -FROM registry.access.redhat.com/rhel7:7.6-151.1550575774 +FROM registry.access.redhat.com/rhel7:7.6-202 ENV SUMMARY="Red Hat CodeReady Workspaces Operator container" \ DESCRIPTION="Red Hat CodeReady Workspaces Operator container" \ @@ -41,7 +41,7 @@ LABEL summary="$SUMMARY" \ io.openshift.tags="$PRODNAME,$COMPNAME" \ com.redhat.component="$PRODNAME-$COMPNAME" \ name="$PRODNAME/$COMPNAME" \ - version="1.0" \ + version="1.1" \ license="EPLv2" \ maintainer="Nick Boldt " \ io.openshift.expose-services="" \ @@ -50,4 +50,4 @@ LABEL summary="$SUMMARY" \ COPY --from=builder /tmp/che-operator/che-operator /usr/local/bin/che-operator COPY --from=builder /go/src/github.com/eclipse/che-operator/deploy/keycloak_provision /tmp/keycloak_provision RUN yum list installed && echo "End Of Installed Packages" -CMD ["che-operator"] +CMD ["che-operator"] \ No newline at end of file diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 000000000..43ea28d6b --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,186 @@ +# +# Copyright (c) 2012-2018 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 +# Force dep to vendor the code generators, which aren't imported just used at dev time. +required = [ + "k8s.io/code-generator/cmd/defaulter-gen", + "k8s.io/code-generator/cmd/deepcopy-gen", + "k8s.io/code-generator/cmd/conversion-gen", + "k8s.io/code-generator/cmd/client-gen", + "k8s.io/code-generator/cmd/lister-gen", + "k8s.io/code-generator/cmd/informer-gen", + "k8s.io/code-generator/cmd/openapi-gen", + "k8s.io/gengo/args", +] + +[[override]] + name = "k8s.io/code-generator" + # revision for tag "kubernetes-1.12.3" + revision = "3dcf91f64f638563e5106f21f50c31fa361c918d" + +[[override]] + name = "k8s.io/api" + # revision for tag "kubernetes-1.12.3" + revision = "b503174bad5991eb66f18247f52e41c3258f6348" + +[[override]] + name = "k8s.io/apiextensions-apiserver" + # revision for tag "kubernetes-1.12.3" + revision = "0cd23ebeb6882bd1cdc2cb15fc7b2d72e8a86a5b" + +[[override]] + name = "k8s.io/apimachinery" + # revision for tag "kubernetes-1.12.3" + revision = "eddba98df674a16931d2d4ba75edc3a389bf633a" + +[[override]] + name = "k8s.io/client-go" + # revision for tag "kubernetes-1.12.3" + revision = "d082d5923d3cc0bfbb066ee5fbdea3d0ca79acf8" + +[[override]] + name = "sigs.k8s.io/controller-runtime" + version = "=v0.1.8" + +[[constraint]] + name = "github.com/operator-framework/operator-sdk" + version = "=v0.5.0" #osdk_version_annotation + +[[override]] + name = "github.com/PuerkitoBio/urlesc" + revision = "de5bf2ad457846296e2031421a34e2568e304e35" + +[[override]] + name = "github.com/alecthomas/template" + revision = "a0175ee3bccc567396460bf5acd36800cb10c49c" + +[[override]] + name = "github.com/alecthomas/units" + revision = "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a" + +[[override]] + name = "github.com/beorn7/perks" + revision = "3a771d992973f24aa725d07868b467d1ddfceafb" + +[[override]] + name = "github.com/docker/spdystream" + revision = "449fdfce4d962303d702fec724ef0ad181c92528" + +[[override]] + name = "github.com/go-logr/logr" + revision = "9fb12b3b21c5415d16ac18dc5cd42c1cfdd40c4e" + +[[override]] + name = "github.com/go-openapi/jsonpointer" + version = "v0.16.0" + +[[override]] + name = "github.com/go-openapi/jsonreference" + version = "v0.16.0" + +[[override]] + name = "github.com/go-openapi/spec" + version = "v0.16.0" + +[[override]] + name = "github.com/go-openapi/swag" + version = "v0.16.0" + +[[override]] + name = "github.com/golang/glog" + revision = "44145f04b68cf362d9c4df2182967c2275eaefed" + +[[override]] + name = "github.com/golang/groupcache" + revision = "c65c006176ff7ff98bb916961c7abbc6b0afc0aa" + +[[override]] + name = "github.com/google/btree" + revision = "4030bb1f1f0c35b30ca7009e9ebd06849dd45306" + +[[override]] + name = "github.com/google/gofuzz" + revision = "44d81051d367757e1c7c6a5a86423ece9afcf63c" + +[[override]] + name = "github.com/gregjones/httpcache" + revision = "9cad4c3443a7200dd6400aef47183728de563a38" + +[[override]] + name = "github.com/json-iterator/go" + version = "v1.1.3" + +[[override]] + name = "github.com/mailru/easyjson" + revision = "60711f1a8329503b04e1c88535f419d0bb440bff" + +[[override]] + name = "github.com/mattbaird/jsonpatch" + revision = "81af80346b1a01caae0cbc27fd3c1ba5b11e189f" + +[[override]] + name = "github.com/modern-go/reflect2" + version = "1.0.0" + +[[override]] + name = "github.com/petar/GoLLRB" + revision = "53be0d36a84c2a886ca057d34b6aa4468df9ccb4" + +[[override]] + name = "github.com/prometheus/client_golang" + version = "0.9.2" + +[[override]] + name = "github.com/prometheus/client_model" + revision = "fd36f4220a901265f90734c3183c5f0c91daa0b8" + +[[override]] + name = "github.com/prometheus/common" + revision = "bf857faf208676b0294bd49959aa34c8d98163ed" + +[[override]] + name = "github.com/prometheus/procfs" + revision = "6ed1f7e1041181781dd2826d3001075d011a80cc" + +[[override]] + name = "github.com/sirupsen/logrus" + version = "0.11.5" + +[[override]] + name = "github.com/spf13/pflag" + version = "v1.0.1" + +[[override]] + name = "google.golang.org/appengine" + version = "v1.2.0" + +[[override]] + name = "gopkg.in/inf.v0" + revision = "3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4" + +[[override]] + name = "gopkg.in/yaml.v2" + version = "v2.2.1" + +[[override]] + name = "k8s.io/gengo" + revision = "fd15ee9cc2f77baa4f31e59e6acbf21146455073" + +[[override]] + name = "k8s.io/kube-openapi" + revision = "0317810137be915b9cf888946c6e115c1bfac693" + +[prune] + go-tests = true + non-go = true + + [[prune.project]] + name = "k8s.io/code-generator" + non-go = false diff --git a/README.md b/README.md index 9307af7f4..020e68115 100644 --- a/README.md +++ b/README.md @@ -1,156 +1,116 @@ -# Che/Codeready Operator +## Che/CodeReady Workspaces Operator -Che Operator creates Eclipse Che k8s and OpenShift resources such as pvcs, services, deployments, routes, ingresses etc. +Che/CodeReady workspaces operator uses [Operator SDK](https://github.com/operator-framework/operator-sdk) and [Go Kube client](https://github.com/kubernetes/client-go) to deploy, update and manage K8S/OpenShift resources that constitute a multi user Eclipse Che/CodeReady Workspaces cluster. -The operator is a k8s a pod that runs an image with Go runtime and a compiled binary of an operator itself. +The operator watches for a Custom Resource of Kind `CheCluster`, and operator controller executes its business logic when a new Che object is created, namely: -Though operator-sdk framework is used, Che operator is rather an installer since no CRD and API group are created, -and thus, the operator does not watch resources. Once deployment is completed, the operator pod exits. +* creates k8s/OpenShift objects +* verifies successful deployment of Postgres, Keycloak and Che +* runs exec into Postgres and Keycloak pods to provisions databases, users, realm and clients +* updates CR spec and status (passwords, URLs, provisioning statuses etc.) +* continuously watches CR, update Che ConfigMap accordingly and schedule a new Che deployment +* changes state of certain objects depending on CR fields: + * turn on/off TLS mode (reconfigure routes, update ConfigMap) + * turn on/off OpenShift oAuth (login with OpenShift in Che) (create identity provider, oAuth client, update Che ConfigMap) +* updates Che deployment with a new image:tag when a new operator version brings in a new Che tag -## Pre-Reqs +## Project State: Beta -OpenShift/K8S cluster with at least 4GB or RAM and 2 PVs, local `oc` or `kubectl`. +The project is in its early development and breaking changes are possible. -## How to deploy +## How to Deploy -Deploy script will create a namespace, operator service account and a rolebinding for it (admin privileges within the namespace), -and run an operator pod that will create all required objects and perform provisioning: +``` +./deploy.sh +``` + +The script will create sa, role, role binding, operator deployment, CRD and CR. + +Wait until Che deployment is scaled to 1 and Che route is created. + +When on pure k8s, make sure you provide a global ingress domain in `deploy/crds/org_v1_che_cr.yaml` for example: ```bash -deploy/deploy.sh $infra $namespace + k8s: + ingressDomain: '192.168.99.101.nip.io' ``` -Deploy to MiniKube to a default namespace: +### OpenShift oAuth + +Bear in mind that che-operator service account needs to have cluster admin privileges so that the operator can create oauthclient at a cluster scope. +There is `oc adm` command in both scripts. Uncomment it if you need these features. +Make sure your current user has cluster-admin privileges. + +### TLS + +#### OpenShift + +When using self-signed certificates make sure you set `server.selfSignedCerts` to true and grant che-operator service account cluster admin privileges +or create a secret called `self-signed-certificate` in a target namespace with ca.crt holding your OpenShift router crt body. + +#### K8S + +When enabling TLS, make sure you create a secret with crt and key, and let the Operator know about it in `k8s.tlsSecretName` + +## How to Configure + +The operator watches all objects it creates and reconciles them with CR state. It means that if you edit, say, a configMap che, the operator will revert changes. +Since not all Che configuration properties are custom resource spec fields, the operator creates a second configMap called custom. You can use this configmap +for any configuration that is not supported by CR. + +## How to Build Operator Image ```bash -deploy/deploy.sh minikube - -``` -`minikube` as infra argument will instruct the script to detect MiniKube IP and pass global ingress domain value to the operator. - -Create default namespace eclipse-che and deploy to k8s: - -``` -deploy/deploy.sh k8s +docker build -t $registry/$repo:$tag ``` -Create a default eclipse-che namespace and deploy to OpenShift: +You can then use the resulting image in operator deployment (deploy/operator.yaml) + +## Build and Deploy to a local cluster: + +There's a little script that will build a Docker image and deploy an operator to a selected namespace, +as well as create service account, role, role binding, CRD and example CR. ``` -deploy/deploy.sh -``` - -Create a namespace of choice and deploy to OpenShift: +oc new-project $namespace +build_deploy_local.sh $namespace -``` -deploy/deploy.sh openshift myproject ``` -If a namespace cannot be created (for example, it exists), a new namespace with a generated name (`eclipse-che${random int}`) will be created. +## How to Run/Debug Locally -This will deploy Che operator with the default settings: +You can run/debug this operator on your local machine (not deployed to a k8s cluster), +provided that the below pre-reqs are met. -* upstream Che -* no tls -* no login with OpenShift in Che -* Postgres passwords are auto-generated, Keycloak admin password is `admin` -* Some object names are default ones (eg databases, users etc) -* Common PVC strategy (all workspaces use one shared PVC) -* All workspace objects get created in a target namespace (Che server uses service account token) -* Multi-host ingress strategy when on k8s +### Pre-Reqs: Local kubeconfig +Go client grabs kubeconfig either from InClusterConfig or ~/.kube locally. +Make sure you oc login (or your current kubectl context points to a target cluster and namespace), +and current user/server account can create objects in a target namespace. -## Deploy custom image +### Pre-Reqs: WATCH_NAMESPACE Environment Variable -Provide your own image in `deploy/config.yaml`: +The operator detects namespace to watch by getting value of `WATCH_NAMESPACE` environment variable. +You can set it in Run configuration in the IDE, or export this env before executing the binary. -```shell -CHE_IMAGE: "myRegistry/myImage:myTag" -``` +This applies both to Run and Debug. -## Deploy CodeReady Workspaces +### Pre-Reqs: /tmp/keycloak_provision file -In `deploy/config.yaml`: +The operator grabs this file and replaces values to get a string used as exec command to create Keycloak realm, client and user. +Make sure you run the following before running/debugging: ``` -CHE_FLAVOR: "codeready" +cp deploy/keycloak_provision /tmp/keycloak_provision ``` -Che flavor is a string that will be used in names of certain objects like routes, realms, clients etc. +This file is added to a Docker image, thus this step isn't required when deploying an operator image. -## Defaults and Configuration -To deploy to OpenShift with all defaults, no user input is required. You may configure Che installation in a configmap `deploy/config.yaml`. -The operator will use envs from this configmap and make decisions accordingly. -Currently, only the most critical envs are added to configmap, and it will expand in time, making it possible to fine tune Che before deploying -## What is deployed? -The Operator creates a handful of objects for: -* Postgres DB -* Keycloak/Red Hat SSO -* Che server itself -After Postgres and Keycloak pods start and health checks confirm the services are up, the operator run execs into db and keycloak pods to provision -databases, users, Keycloak realm, client and user. The operator watches respective deployments to get events signalling that the deployment is scaled to 1. -## How to configure installation -`deploy/config.yaml` is a config map with env variables that influences choices an operator makes. Each env is commented, and each one has defaults. -This configmap is an operator's env, and evn variables are then taken to Che server configmap. -What can be configured: - -* external DB and Keycloak: `CHE_EXTERNAL_DB` and `CHE_EXTERNAL_KEYCLOAK` default to false. -If you do not need instances of Postgres and Keycloak and want to connect to own infra, set both envs to `true` and provide connection details in envs below the above booleans. - -Your DB user **MUST** be a `SUPERUSER`. - -**Important!** The operator does not perform Postgres and Keycloak provisioning if external instances are used. -Thus, you need to pre-create db and user for Che. -Also create (or use existing) realm and client that should be public and should have: - -Redirect URIs: `${PROTOCOL}://${CHE_HOST}/*` -WebOrigins: `${PROTOCOL}://${CHE_HOST}` - -* Login with OpenShift in Che. Not supported on k8s infra - -* TLS. Set `TLS_SUPPORT` to true if you want to deploy Che in https mode. -When on k8s, make sure you create a secret in che namespace and provide its name in `TLS_SECRET_NAME` - -* TLS and self signed certs. Provide a base64 string of your self signed cert in `SELF_SIGNED_CERT`. When on k8s, you can get it from crt part of the secret. - -* Fake dns. If, for example, you want to deploy on k8s with a fake ingress domain of example.com and you need to point it to your Minikube IP, `HOST_ALIAS_IP` and `HOST_ALIAS_HOSTNAME` are the two envs to use. - -## How to build and deploy own operator image - -### Build an image - -To build an Operator and package it into a Docker image run: - -`docker build -t che-operator .` - -If you want to deploy your custom Operator image to a remote k8s/OpenShift cluster, make sure you push an image and change its name in -`--image` and `--overrides` in kubectl/oc run command in the script. - -### Build and deploy to a local cluster - -Run `deploy/build_deploy_local.sh $infra $namespace` - -Minishift and Minikube users will need to execute the below command to use VM Docker daemon when building an image: - -``` -eval $(minikube docker-env) -``` - -## How to add new envs to configmap? - -If you need to add new envs to be added to Che configmap by default: - -* add key:value to deploy/config.yaml -* add operator variable to `pkg/operator/vars.go` that will take its value from environment -* add env and operator variable as value to `pkg/operator/che_cm.go` - -## Deploy Script - -It is something quick and dirty and is likely to be substituted with a feature rich CLI. \ No newline at end of file + diff --git a/build_deploy_local.sh b/build_deploy_local.sh new file mode 100755 index 000000000..ffa97de8a --- /dev/null +++ b/build_deploy_local.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# +# Copyright (c) 2012-2018 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 + +set -e + +BASE_DIR=$(cd "$(dirname "$0")"; pwd) + +docker build -t che/operator . +kubectl apply -f ${BASE_DIR}/deploy/service_account.yaml -n=$1 +kubectl apply -f ${BASE_DIR}/deploy/role.yaml -n=$1 +kubectl apply -f ${BASE_DIR}/deploy/role_binding.yaml -n=$1 +kubectl apply -f ${BASE_DIR}/deploy/crds/org_v1_che_crd.yaml -n=$1 +# sometimes the operator cannot get CRD right away +sleep 2 +# uncomment when on OpenShift if you need to use self signed certs and login with OpenShift in Che +#oc adm policy add-cluster-role-to-user cluster-admin -z che-operator -n=$1 +kubectl apply -f ${BASE_DIR}/operator-local.yaml -n=$1 +kubectl apply -f ${BASE_DIR}/deploy/crds/org_v1_che_cr.yaml -n=$1 + diff --git a/cmd/manager/main.go b/cmd/manager/main.go new file mode 100644 index 000000000..03d6ba26c --- /dev/null +++ b/cmd/manager/main.go @@ -0,0 +1,112 @@ +// +// 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 main + +import ( + + "context" + "flag" + "fmt" + "github.com/eclipse/che-operator/pkg/util" + "github.com/operator-framework/operator-sdk/pkg/k8sutil" + "github.com/prometheus/common/log" + "github.com/sirupsen/logrus" + "os" + "runtime" + + "github.com/eclipse/che-operator/pkg/apis" + "github.com/eclipse/che-operator/pkg/controller" + "github.com/operator-framework/operator-sdk/pkg/leader" + "github.com/operator-framework/operator-sdk/pkg/ready" + sdkVersion "github.com/operator-framework/operator-sdk/version" + _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" + "sigs.k8s.io/controller-runtime/pkg/client/config" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/runtime/signals" + //logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + +) + +func printVersion() { + logrus.Infof(fmt.Sprintf("Go Version: %s", runtime.Version())) + logrus.Infof(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH)) + logrus.Infof(fmt.Sprintf("operator-sdk Version: %v", sdkVersion.Version)) + isOpenShift, err := util.DetectOpenShift() + if err != nil { + logrus.Fatalf("Operator is exiting. An error occurred when detecting current infra: %s", err) + + } + infra := "Kubernetes" + if isOpenShift { + infra = "OpenShift" + } + logrus.Infof(fmt.Sprintf("Operator is running on %v", infra)) + +} + +func main() { + flag.Parse() + //logf.SetLogger(logf.ZapLogger(false)) + printVersion() + namespace, err := k8sutil.GetWatchNamespace() + if err != nil { + logrus.Errorf( "Failed to get watch namespace. Using default namespace eclipse-che: %s", err) + namespace = "eclipse-che" + } + + // Get a config to talk to the apiserver + cfg, err := config.GetConfig() + if err != nil { + log.Error(err, "") + os.Exit(1) + } + + // Become the leader before proceeding + leader.Become(context.TODO(), "che-operator-lock") + + r := ready.NewFileReady() + err = r.Set() + if err != nil { + log.Error(err, "") + os.Exit(1) + } + defer r.Unset() + + // Create a new Cmd to provide shared dependencies and start components + mgr, err := manager.New(cfg, manager.Options{Namespace: namespace}) + if err != nil { + log.Error(err, "") + os.Exit(1) + } + + logrus.Info("Registering Components") + + // Setup Scheme for all resources + if err := apis.AddToScheme(mgr.GetScheme()); err != nil { + logrus.Error(err, "") + os.Exit(1) + } + + // Setup all Controllers + if err := controller.AddToManager(mgr); err != nil { + log.Error(err, "") + os.Exit(1) + } + + logrus.Info("Starting the Cmd") + + // Start the Cmd + if err := mgr.Start(signals.SetupSignalHandler()); err != nil { + logrus.Error(err, "Manager exited non-zero") + os.Exit(1) + } +} diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 000000000..5c31eaa6f --- /dev/null +++ b/deploy.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# +# Copyright (c) 2012-2018 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 +#set -e + +BASE_DIR=$(cd "$(dirname "$0")"; pwd) + +oc apply -f ${BASE_DIR}/service_account.yaml +oc apply -f ${BASE_DIR}/role.yaml +oc apply -f ${BASE_DIR}/role_binding.yaml +oc apply -f ${BASE_DIR}/crds/org_v1_che_crd.yaml +# sometimes the operator cannot get CRD right away +sleep 2 +# uncomment if you need Login with OpenShift and/or use self signed certificates and tls +#oc adm policy add-cluster-role-to-user cluster-admin -z che-operator +oc apply -f ${BASE_DIR}/operator.yaml +oc apply -f ${BASE_DIR}/crds/org_v1_che_cr.yaml \ No newline at end of file diff --git a/deploy/crds/org_v1_che_cr.yaml b/deploy/crds/org_v1_che_cr.yaml new file mode 100644 index 000000000..d4cee814d --- /dev/null +++ b/deploy/crds/org_v1_che_cr.yaml @@ -0,0 +1,72 @@ +# +# 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 + +apiVersion: org.eclipse.che/v1 +kind: CheCluster +metadata: + name: eclipse-che +spec: + server: + cheImage: '' + cheImageTag: '' + # defaults to `che`. When set to `codeready`, CodeReady Workspaces is deployed + # the difference is in images, labels, exec commands + cheFlavor: '' + # when set to true the operator will attempt to get a secret in openshift router namespace + # to add it to Java trust store of Che server. Requires cluster-admin provileges for operator service account + selfSignedCert: + # TLS mode for Che. Make sure you either have public cert, or set selfSignedCert to true + tlsSupport: + proxyURL: '' + proxyPort: '' + proxyUser: '' + proxyPassword: '' + nonProxyHosts: '' + pluginRegistryUrl: '' + database: + # when set to true, the operator skips deploying Postgres, and passes connection details of existing DB to Che server + externalDb: + chePostgresHostname: '' + chePostgresPort: '' + chePostgresUser: '' + chePostgresPassword: '' + chePostgresDb: '' + storage: + # defaults to 'common' (one PVC for all workspacees). Can be 'unique' (PVC per volume), or 'per-workspace' + pvcStrategy: '' + # default to 1Gi + pvcClaimSize: '' + # use a special pod to pre-create subpaths in a common volume + preCreateSubPaths: true + auth: + # when set to true, the operator skips deploying Keycloak, + #and passes connection details of existing Keycloak auth server to Che server + externalKeycloak: + keycloakURL: '' + keycloakPostgresPassword: '' + keycloakAdminUserName: '' + keycloakAdminPassword: 'admin' + keycloakRealm: '' + keycloakClientId: '' + openShiftoAuth: + openShiftApiUrl: '' + k8s: + # your global ingress domain + ingressDomain: '192.168.99.101.nip.io' + # defaults to nginx + ingressClass: '' + # default to multi-host - -. + ingressStrategy: '' + # tls secret name will be used in ingress tls spec + tlsSecretName: '' + + + diff --git a/deploy/crds/org_v1_che_crd.yaml b/deploy/crds/org_v1_che_crd.yaml new file mode 100644 index 000000000..3a3b2312a --- /dev/null +++ b/deploy/crds/org_v1_che_crd.yaml @@ -0,0 +1,25 @@ +# +# 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 +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: checlusters.org.eclipse.che +spec: + group: org.eclipse.che + names: + kind: CheCluster + listKind: CheClusterList + plural: checlusters + singular: checluster + scope: Namespaced + version: v1 + subresources: + status: {} diff --git a/deploy/keycloak_provision b/deploy/keycloak_provision new file mode 100644 index 000000000..b01b471dd --- /dev/null +++ b/deploy/keycloak_provision @@ -0,0 +1,34 @@ +$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 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=true \ + -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/*", "https://$cheHost/*"]' \ + -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 \ No newline at end of file diff --git a/deploy/olm-catalog/codeready-workspaces.1.1.0.csv.yaml b/deploy/olm-catalog/codeready-workspaces.1.1.0.csv.yaml new file mode 100644 index 000000000..a9215ee7d --- /dev/null +++ b/deploy/olm-catalog/codeready-workspaces.1.1.0.csv.yaml @@ -0,0 +1,343 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + name: crwoperator.v1.1.0 + namespace: placeholder + annotations: + alm-examples: |- + [ + { + "apiVersion":"org.eclipse.che/v1", + "kind":"CheCluster", + "metadata":{ + "name":"codeready" + }, + "spec":{ + "server":{ + "cheFlavor":"codeready", + "cheImage": "quay.io/crw/server-container", + "cheImageTag": "1.1-45", + "tlsSupport":false, + "selfSignedCert":false + }, + "database":{ + "externalDb":false, + "chePostgresHostname":"", + "chePostgresPort":"", + "chePostgresUser":"", + "chePostgresPassword":"", + "chePostgresDb":"" + }, + "auth":{ + "openShiftoAuth":false, + "externalKeycloak":false, + "keycloakURL":"", + "keycloakRealm":"", + "keycloakClientId":"" + }, + "storage":{ + "pvcStrategy":"common", + "pvcClaimSize":"1Gi", + "preCreateSubPaths": true + } + } + } + ] + certified: "true" + categories: "Developer Tools" + containerImage: quay.io/crw/operator-container:1.1-7 + createdAt: 2019-03-06 + description: A collaborative kubernetes-native development solution that delivers OpenShift workspaces and in-browser IDE for rapid cloud application development. + support: Red Hat, Inc. +spec: + install: + strategy: deployment + spec: + clusterPermissions: + - serviceAccountName: codeready-operator + rules: + - apiGroups: + - extensions/v1beta1 + resources: + - ingresses + verbs: + - "*" + - apiGroups: + - route.openshift.io + resources: + - routes + verbs: + - "*" + - apiGroups: + - rbac.authorization.k8s.io + resources: + - roles + - rolebindings + - clusterroles + - clusterrolebindings + verbs: + - "*" + - apiGroups: + - "" + resources: + - pods + - services + - serviceaccounts + - endpoints + - persistentvolumeclaims + - events + - configmaps + - secrets + - pods/exec + - pods/log + verbs: + - '*' + - apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - apiGroups: + - apps + resources: + - deployments + verbs: + - '*' + - apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - get + - create + - apiGroups: + - org.eclipse.che + resources: + - '*' + verbs: + - '*' + deployments: + - name: codeready-operator + spec: + replicas: 1 + selector: + matchLabels: + app: codeready-operator + template: + metadata: + labels: + app: codeready-operator + spec: + containers: + - name: codeready-operator + image: quay.io/crw/operator-container:1.1-7 + env: + - name: WATCH_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: OPERATOR_NAME + value: "codeready-operator" + command: + - /usr/local/bin/che-operator + ports: + - containerPort: 60000 + name: metrics + imagePullPolicy: IfNotPresent + restartPolicy: Always + terminationGracePeriodSeconds: 5 + serviceAccountName: codeready-operator + customresourcedefinitions: + owned: + - description: CodeReady Workspaces cluster with DB and Auth Server + displayName: CodeReady Workspaces Cluster + kind: CheCluster + name: checlusters.org.eclipse.che + version: v1 + specDescriptors: + - description: Login to CodeReady Workspaces with with OpenShift credentials + displayName: OpenShift oAuth + path: auth.openShiftoAuth + x-descriptors: + - 'urn:alm:descriptor:com.tectonic.ui:booleanSwitch' + - description: CodeReady Workspaces TLS Mode + displayName: TLS Mode + path: server.tlsSupport + x-descriptors: + - 'urn:alm:descriptor:com.tectonic.ui:booleanSwitch' + statusDescriptors: + - description: Route to access CodeReady Workspaces + displayName: CodeReady Workspaces URL + path: cheURL + x-descriptors: + - urn:alm:descriptor:org.w3:link + - description: Route to access Keycloak Admin Console + displayName: RedHat SSO Admin Console URL + path: keycloakURL + x-descriptors: + - urn:alm:descriptor:org.w3:link + - description: CodeReady Workspaces server version + displayName: CodeReady Workspaces version + path: cheVersion + x-descriptors: + - 'urn:alm:descriptor:com.tectonic.ui:label' + - description: The current status of the application. + displayName: Status + path: cheClusterRunning + x-descriptors: + - 'urn:alm:descriptor:io.kubernetes.phase' + keywords: + - codeready + - workspaces + - devtools + - developer + - ide + - java + displayName: CodeReady Workspaces + provider: + name: Red Hat, Inc + url: https://developers.redhat.com/products/codeready-workspaces/overview/ + maturity: stable + installModes: + - supported: true + type: OwnNamespace + - supported: true + type: SingleNamespace + - supported: true + type: MultiNamespace + - supported: false + type: AllNamespaces + version: 1.1.0 + maintainers: + - email: eivantsov@redhat.com + name: Eugene Ivantsov + description: > + A collaborative kubernetes-native development solution that delivers OpenShift workspaces and in-browser IDE for rapid cloud application development. + This operator will install Postgres, Red Hat SSO and CodeReady Workspaces server, as well as configure all 3 services. + + ## Pre-Reqs + + Operator service account will require cluster-admin privileges if you enable either `server.selfSignedCerts` or `auth.openShiftoAuth`. + After the operator is installed, grant **codeready-operator** service account such privileges: + + + ``` + oc adm policy add-cluster-role-to-user cluster-admin -z codeready-operator -n=$CodeReadyNamespace + ``` + + ## How to Install + + Press Install button, choose upgrade strategy, wait for **Installed** Operator status. + + When the Operator is installed, create a new CR of Kind CheCluster (click Create New button). + CR spec contains all defaults (see below) + + You may start using CodeReady Workspaces when CR status is set to Available and you see a URL to CodeReady Workspaces + + ## Defaults + + By default the operator will deploy CodeReady Workspaces with: + + * bundled Postgres and Red Hat SSO + + * common PVC strategy + + * Auto-generated passwords + + * HTTP mode (non-secure routes) + + * Regular login (no Login with OpenShift) + + ## Installation Options + + CodeReady Workspaces operator installation options include: + + * Connection to external database and Keycloak/RedHat SSO + + * Configuration of default passwords and object names + + * TLS mode + + * PVC strategy (once shared PVC for all workspaces, PVC per workspace, or PVC per volume) + + * Authentication options + + ### External Database and Keycloak + + If you want the Operator skip deploying Postgres and Red Hat SSO but rather to connect to existing DB and RH SSO/Keycloak + set respective fields to true in a custom resource spec, as well as provide the Operator with connection/authentication details: + + + + `externalDb: true` + + + `chePostgresHostname: 'yourPostgresHost'` + + + `chePostgresPort: '5432'` + + + `chePostgresUser: 'myuser'` + + + `chePostgresPassword: 'mypass'` + + + `chePostgresDb: 'mydb'` + + + `externalKeycloak: true` + + + `keycloakURL: 'https://my-keycloak.com'` + + + `keycloakRealm: 'myrealm'` + + + `keycloakClientId: 'myClient'` + + + ### TLS Mode + + To activate TLS mode set the respective field in the CR spec to true (server block): + + + ``` + tlsSupport: true + ``` + + #### Self Signed Certificates + + If you want to use CodeReady with TLS enabled but OpenShift router does not use certificates signed by a public authority + you can use self signed certificates, which an Operator can create for you (requires cluster-admin privileges): + + + ``` + selfSignedCert: true + ``` + + + You can also manually create such a secret: + + + + ``` + oc create secret self-signed-certificate generic --from-file=/path/to/certificate/ca.crt -n=$codeReadyNamespace + ``` + links: + - name: Product Page + url: https://developers.redhat.com/products/codeready-workspaces/overview/ + - name: Documentation + url: https://access.redhat.com/documentation/en-us/red_hat_codeready_workspaces_for_openshift + - name: Operator GitHub Repo + url: https://github.com/eclipse/che-operator + icon: + - base64data: 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 + mediatype: image/png diff --git a/deploy/olm-catalog/codeready-workspaces.crd.yaml b/deploy/olm-catalog/codeready-workspaces.crd.yaml new file mode 100644 index 000000000..ec7b9103f --- /dev/null +++ b/deploy/olm-catalog/codeready-workspaces.crd.yaml @@ -0,0 +1,15 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: checlusters.org.eclipse.che +spec: + group: org.eclipse.che + names: + kind: CheCluster + listKind: CheClusterList + plural: checlusters + singular: checluster + scope: Namespaced + version: v1 + subresources: + status: {} diff --git a/deploy/olm-catalog/codeready-workspaces.package.yaml b/deploy/olm-catalog/codeready-workspaces.package.yaml new file mode 100644 index 000000000..e514f9994 --- /dev/null +++ b/deploy/olm-catalog/codeready-workspaces.package.yaml @@ -0,0 +1,4 @@ +packageName: codeready-workspaces +channels: +- name: final + currentCSV: crwoperator.v1.1.0 diff --git a/deploy/operator-local.yaml b/deploy/operator-local.yaml new file mode 100644 index 000000000..d6a69f5a0 --- /dev/null +++ b/deploy/operator-local.yaml @@ -0,0 +1,53 @@ +# +# 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 +apiVersion: apps/v1 +kind: Deployment +metadata: + name: che-operator +spec: + replicas: 1 + selector: + matchLabels: + name: che-operator + template: + metadata: + labels: + name: che-operator + spec: + serviceAccountName: che-operator + containers: + - name: che-operator + image: che/operator + ports: + - containerPort: 60000 + name: metrics + command: + - che-operator + imagePullPolicy: IfNotPresent + readinessProbe: + exec: + command: + - stat + - /tmp/operator-sdk-ready + initialDelaySeconds: 4 + periodSeconds: 10 + failureThreshold: 1 + env: + - name: WATCH_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: OPERATOR_NAME + value: "che-operator" diff --git a/deploy/operator.yaml b/deploy/operator.yaml new file mode 100644 index 000000000..8e917f52b --- /dev/null +++ b/deploy/operator.yaml @@ -0,0 +1,53 @@ +# +# 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 +apiVersion: apps/v1 +kind: Deployment +metadata: + name: che-operator +spec: + replicas: 1 + selector: + matchLabels: + name: che-operator + template: + metadata: + labels: + name: che-operator + spec: + serviceAccountName: che-operator + containers: + - name: che-operator + image: eivantsov/operator-container + ports: + - containerPort: 60000 + name: metrics + command: + - che-operator + imagePullPolicy: IfNotPresent + readinessProbe: + exec: + command: + - stat + - /tmp/operator-sdk-ready + initialDelaySeconds: 4 + periodSeconds: 10 + failureThreshold: 1 + env: + - name: WATCH_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: OPERATOR_NAME + value: "che-operator" diff --git a/deploy/role.yaml b/deploy/role.yaml new file mode 100644 index 000000000..edc6b8095 --- /dev/null +++ b/deploy/role.yaml @@ -0,0 +1,78 @@ +# +# 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 +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + creationTimestamp: null + name: che-operator + +rules: + - apiGroups: + - extensions/v1beta1 + resources: + - ingresses + verbs: + - "*" + - apiGroups: + - route.openshift.io + resources: + - routes + verbs: + - "*" + - apiGroups: + - rbac.authorization.k8s.io + resources: + - roles + - rolebindings + - clusterroles + - clusterrolebindings + verbs: + - "*" + - apiGroups: + - "" + resources: + - pods + - services + - serviceaccounts + - endpoints + - persistentvolumeclaims + - events + - configmaps + - secrets + - pods/exec + - pods/log + verbs: + - '*' + - apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - apiGroups: + - apps + resources: + - deployments + verbs: + - '*' + - apiGroups: + - monitoring.coreos.com + resources: + - servicemonitors + verbs: + - get + - create + - apiGroups: + - org.eclipse.che + resources: + - '*' + verbs: + - '*' \ No newline at end of file diff --git a/deploy/role_binding.yaml b/deploy/role_binding.yaml new file mode 100644 index 000000000..062c7ec17 --- /dev/null +++ b/deploy/role_binding.yaml @@ -0,0 +1,21 @@ +# +# 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 +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: che-operator +subjects: +- kind: ServiceAccount + name: che-operator +roleRef: + kind: Role + name: che-operator + apiGroup: rbac.authorization.k8s.io diff --git a/deploy/service_account.yaml b/deploy/service_account.yaml new file mode 100644 index 000000000..1d1d30092 --- /dev/null +++ b/deploy/service_account.yaml @@ -0,0 +1,14 @@ +# +# 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 +apiVersion: v1 +kind: ServiceAccount +metadata: + name: che-operator diff --git a/pkg/apis/addtoscheme_org_v1.go b/pkg/apis/addtoscheme_org_v1.go new file mode 100644 index 000000000..f11911b37 --- /dev/null +++ b/pkg/apis/addtoscheme_org_v1.go @@ -0,0 +1,21 @@ +// +// 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 apis + +import ( + "github.com/eclipse/che-operator/pkg/apis/org/v1" +) + +func init() { + // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back + AddToSchemes = append(AddToSchemes, v1.SchemeBuilder.AddToScheme) +} diff --git a/pkg/apis/apis.go b/pkg/apis/apis.go new file mode 100644 index 000000000..787bc839f --- /dev/null +++ b/pkg/apis/apis.go @@ -0,0 +1,24 @@ +// +// 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 apis + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +// AddToSchemes may be used to add all resources defined in the project to a Scheme +var AddToSchemes runtime.SchemeBuilder + +// AddToScheme adds all Resources to the Scheme +func AddToScheme(s *runtime.Scheme) error { + return AddToSchemes.AddToScheme(s) +} diff --git a/pkg/apis/org/v1/che_types.go b/pkg/apis/org/v1/che_types.go new file mode 100644 index 000000000..42870ef04 --- /dev/null +++ b/pkg/apis/org/v1/che_types.go @@ -0,0 +1,129 @@ +// +// 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 v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// CheClusterSpec defines the desired state of CheCluster +type CheClusterSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file + Server CheClusterSpecServer `json:"server"` + Database CheClusterSpecDB `json:"database"` + Auth CheClusterSpecAuth `json:"auth"` + Storage CheClusterSpecStorage `json:"storage"` + K8SOnly CheClusterSpecK8SOnly `json:"k8s"` +} + +type CheClusterSpecServer struct { + CheImage string `json:"cheImage"` + CheImageTag string `json:"cheImageTag"` + CheFlavor string `json:"cheFlavor"` + CheHost string `json:"cheHost"` + CheLogLevel string `json:"cheLogLevel"` + CheDebug string `json:"cheDebug"` + SelfSignedCert bool `json:"selfSignedCert"` + TlsSupport bool `json:"tlsSupport"` + PluginRegistryUrl string `json:"pluginRegistryUrl"` + ProxyURL string `json:"proxyURL"` + ProxyPort string `json:"proxyPort"` + NonProxyHosts string `json:"nonProxyHosts"` + ProxyUser string `json:"proxyUser"` + ProxyPassword string `json:"proxyPassword"` +} + +type CheClusterSpecDB struct { + + ExternalDB bool `json:"externalDb"` + ChePostgresDBHostname string `json:"chePostgresHostName"` + ChePostgresPort string `json:"chePostgresPort"` + ChePostgresUser string `json:"chePostgresUser"` + ChePostgresPassword string `json:"chePostgresPassword"` + ChePostgresDb string `json:"chePostgresDb"` + PostgresImage string `json:"postgresImage"` +} + +type CheClusterSpecAuth struct { + + ExternalKeycloak bool `json:"externalKeycloak"` + KeycloakURL string `json:"keycloakURL"` + KeycloakAdminUserName string `json:"keycloakAdminUserName"` + KeycloakAdminPassword string `json:"keycloakAdminPassword"` + KeycloakRealm string `json:"keycloakRealm"` + KeycloakClientId string `json:"keycloakClientId"` + KeycloakPostgresPassword string `json:"keycloakPostgresPassword"` + UpdateAdminPassword bool `json:"updateAdminPassword"` + OpenShiftOauth bool `json:"openShiftoAuth"` + OauthClientName string `json:"oAuthClientName"` + OauthSecret string `json:"oAuthSecret"` + KeycloakImage string `json:"keycloakImage"` +} + + +type CheClusterSpecStorage struct { + PvcStrategy string `json:"pvcStrategy"` + PvcClaimSize string `json:"pvcClaimSize"` + PreCreateSubPaths bool `json:"preCreateSubPaths"` + PvcJobsImage string `json:"pvcJobsImage"` +} + +type CheClusterSpecK8SOnly struct { + IngressDomain string `json:"ingressDomain"` + IngressStrategy string `json:"ingressStrategy"` + IngressClass string `json:"ingressClass"` + TlsSecretName string `json:"tlsSecretName"` +} + +// CheClusterStatus defines the observed state of CheCluster +type CheClusterStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file + DbProvisoned bool `json:"dbProvisioned"` + KeycloakProvisoned bool `json:"keycloakProvisioned"` + OpenShiftoAuthProvisioned bool `json:"openShiftoAuthProvisioned"` + CheClusterRunning string `json:"cheClusterRunning"` + CheVersion string `json:"cheVersion"` + CheURL string `json:"cheURL"` + KeycloakURL string `json:"keycloakURL"` +} + + + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CheCluster is the Schema for the ches API +// +k8s:openapi-gen=true +type CheCluster struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec CheClusterSpec `json:"spec,omitempty"` + Status CheClusterStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CheClusterList contains a list of CheCluster +type CheClusterList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []CheCluster `json:"items"` +} + +func init() { + SchemeBuilder.Register(&CheCluster{}, &CheClusterList{}) +} diff --git a/pkg/apis/org/v1/doc.go b/pkg/apis/org/v1/doc.go new file mode 100644 index 000000000..8978a1c28 --- /dev/null +++ b/pkg/apis/org/v1/doc.go @@ -0,0 +1,15 @@ +// +// 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 v1 contains API Schema definitions for the org v1 API group +// +k8s:deepcopy-gen=package,register +// +groupName=org.eclipse.che +package v1 diff --git a/pkg/apis/org/v1/register.go b/pkg/apis/org/v1/register.go new file mode 100644 index 000000000..31d5c4271 --- /dev/null +++ b/pkg/apis/org/v1/register.go @@ -0,0 +1,30 @@ +// +// 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 +// +// NOTE: Boilerplate only. Ignore this file. + +// Package v1 contains API Schema definitions for the org v1 API group +// +k8s:deepcopy-gen=package,register +// +groupName=org.eclipse.che +package v1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/runtime/scheme" +) + +var ( + // SchemeGroupVersion is group version used to register these objects + SchemeGroupVersion = schema.GroupVersion{Group: "org.eclipse.che", Version: "v1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} +) diff --git a/pkg/apis/org/v1/zz_generated.deepcopy.go b/pkg/apis/org/v1/zz_generated.deepcopy.go new file mode 100644 index 000000000..ab783afcf --- /dev/null +++ b/pkg/apis/org/v1/zz_generated.deepcopy.go @@ -0,0 +1,214 @@ +// +// 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 +// +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CheCluster) DeepCopyInto(out *CheCluster) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CheCluster. +func (in *CheCluster) DeepCopy() *CheCluster { + if in == nil { + return nil + } + out := new(CheCluster) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CheCluster) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CheClusterList) DeepCopyInto(out *CheClusterList) { + *out = *in + out.TypeMeta = in.TypeMeta + out.ListMeta = in.ListMeta + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CheCluster, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CheClusterList. +func (in *CheClusterList) DeepCopy() *CheClusterList { + if in == nil { + return nil + } + out := new(CheClusterList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CheClusterList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CheClusterSpec) DeepCopyInto(out *CheClusterSpec) { + *out = *in + out.Server = in.Server + out.Database = in.Database + out.Auth = in.Auth + out.Storage = in.Storage + out.K8SOnly = in.K8SOnly + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CheClusterSpec. +func (in *CheClusterSpec) DeepCopy() *CheClusterSpec { + if in == nil { + return nil + } + out := new(CheClusterSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CheClusterSpecAuth) DeepCopyInto(out *CheClusterSpecAuth) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CheClusterSpecAuth. +func (in *CheClusterSpecAuth) DeepCopy() *CheClusterSpecAuth { + if in == nil { + return nil + } + out := new(CheClusterSpecAuth) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CheClusterSpecDB) DeepCopyInto(out *CheClusterSpecDB) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CheClusterSpecDB. +func (in *CheClusterSpecDB) DeepCopy() *CheClusterSpecDB { + if in == nil { + return nil + } + out := new(CheClusterSpecDB) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CheClusterSpecK8SOnly) DeepCopyInto(out *CheClusterSpecK8SOnly) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CheClusterSpecK8SOnly. +func (in *CheClusterSpecK8SOnly) DeepCopy() *CheClusterSpecK8SOnly { + if in == nil { + return nil + } + out := new(CheClusterSpecK8SOnly) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CheClusterSpecServer) DeepCopyInto(out *CheClusterSpecServer) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CheClusterSpecServer. +func (in *CheClusterSpecServer) DeepCopy() *CheClusterSpecServer { + if in == nil { + return nil + } + out := new(CheClusterSpecServer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CheClusterSpecStorage) DeepCopyInto(out *CheClusterSpecStorage) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CheClusterSpecStorage. +func (in *CheClusterSpecStorage) DeepCopy() *CheClusterSpecStorage { + if in == nil { + return nil + } + out := new(CheClusterSpecStorage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CheClusterStatus) DeepCopyInto(out *CheClusterStatus) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CheClusterStatus. +func (in *CheClusterStatus) DeepCopy() *CheClusterStatus { + if in == nil { + return nil + } + out := new(CheClusterStatus) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/controller/add_che.go b/pkg/controller/add_che.go new file mode 100644 index 000000000..fbeb1946c --- /dev/null +++ b/pkg/controller/add_che.go @@ -0,0 +1,21 @@ +// +// 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 controller + +import ( + "github.com/eclipse/che-operator/pkg/controller/che" +) + +func init() { + // AddToManagerFuncs is a list of functions to create controllers and add them to a manager. + AddToManagerFuncs = append(AddToManagerFuncs, che.Add) +} diff --git a/pkg/controller/che/che_controller.go b/pkg/controller/che/che_controller.go new file mode 100644 index 000000000..d0412d7dd --- /dev/null +++ b/pkg/controller/che/che_controller.go @@ -0,0 +1,640 @@ +// +// 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 che + +import ( + "context" + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + "github.com/eclipse/che-operator/pkg/deploy" + "github.com/eclipse/che-operator/pkg/util" + oauth "github.com/openshift/api/oauth/v1" + routev1 "github.com/openshift/api/route/v1" + "github.com/sirupsen/logrus" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/api/extensions/v1beta1" + rbac "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "time" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +var log = logf.Log.WithName("controller_che") +var ( + k8sclient = GetK8Client() +) + +// Add creates a new CheCluster Controller and adds it to the Manager. The Manager will set fields on the Controller +// and Start it when the Manager is Started. +func Add(mgr manager.Manager) error { + return add(mgr, newReconciler(mgr)) +} + +// newReconciler returns a new reconcile.Reconciler +func newReconciler(mgr manager.Manager) reconcile.Reconciler { + return &ReconcileChe{client: mgr.GetClient(), scheme: mgr.GetScheme()} +} + +// add adds a new Controller to mgr with r as the reconcile.Reconciler +func add(mgr manager.Manager, r reconcile.Reconciler) error { + isOpenShift, err := util.DetectOpenShift() + + if err != nil { + logrus.Errorf("An error occurred when detecting current infra: %s", err) + } + // Create a new controller + c, err := controller.New("che-controller", mgr, controller.Options{Reconciler: r}) + if err != nil { + return err + } + // register OpenShift routes in the scheme + if isOpenShift { + if err := routev1.AddToScheme(mgr.GetScheme()); err != nil { + logrus.Errorf("Failed to add OpenShift route to scheme: %", err) + } + if err := oauth.AddToScheme(mgr.GetScheme()); err != nil { + logrus.Errorf("Failed to add oAuth to scheme: %", err) + } + } + + // register RBAC in the scheme + if err := rbac.AddToScheme(mgr.GetScheme()); err != nil { + logrus.Errorf("Failed to add RBAC to scheme: %", err) + } + + // Watch for changes to primary resource CheCluster + err = c.Watch(&source.Kind{Type: &orgv1.CheCluster{}}, &handler.EnqueueRequestForObject{}) + if err != nil { + return err + } + + // Watch for changes to secondary resources and requeue the owner CheCluster + + if err = c.Watch(&source.Kind{Type: &corev1.Service{}}, &handler.EnqueueRequestForOwner{ + IsController: true, + OwnerType: &orgv1.CheCluster{}, + }); err != nil { + return err + } + + if err = c.Watch(&source.Kind{Type: &corev1.Secret{}}, &handler.EnqueueRequestForOwner{ + IsController: true, + OwnerType: &orgv1.CheCluster{}, + }); err != nil { + return err + } + + err = c.Watch(&source.Kind{Type: &corev1.ConfigMap{}}, &handler.EnqueueRequestForOwner{ + IsController: true, + OwnerType: &orgv1.CheCluster{}, + }) + if err != nil { + return err + } + + err = c.Watch(&source.Kind{Type: &rbac.Role{}}, &handler.EnqueueRequestForOwner{ + IsController: true, + OwnerType: &orgv1.CheCluster{}, + }) + if err != nil { + return err + } + + err = c.Watch(&source.Kind{Type: &rbac.RoleBinding{}}, &handler.EnqueueRequestForOwner{ + IsController: true, + OwnerType: &orgv1.CheCluster{}, + }) + if err != nil { + return err + } + + err = c.Watch(&source.Kind{Type: &corev1.ServiceAccount{}}, &handler.EnqueueRequestForOwner{ + IsController: true, + OwnerType: &orgv1.CheCluster{}, + }) + if err != nil { + return err + } + + if isOpenShift { + err = c.Watch(&source.Kind{Type: &routev1.Route{}}, &handler.EnqueueRequestForOwner{ + IsController: true, + OwnerType: &orgv1.CheCluster{}, + }) + if err != nil { + return err + } + } else { + err = c.Watch(&source.Kind{Type: &v1beta1.Ingress{}}, &handler.EnqueueRequestForOwner{ + IsController: true, + OwnerType: &orgv1.CheCluster{}, + }) + if err != nil { + return err + } + } + + err = c.Watch(&source.Kind{Type: &appsv1.Deployment{}}, &handler.EnqueueRequestForOwner{ + IsController: true, + OwnerType: &orgv1.CheCluster{}, + }) + if err != nil { + return err + } + + err = c.Watch(&source.Kind{Type: &corev1.PersistentVolumeClaim{}}, &handler.EnqueueRequestForOwner{ + IsController: true, + OwnerType: &orgv1.CheCluster{}, + }) + if err != nil { + return err + } + return nil +} + +var _ reconcile.Reconciler = &ReconcileChe{} + +// ReconcileChe reconciles a CheCluster object +type ReconcileChe struct { + // This client, initialized using mgr.Client() above, is a split client + // that reads objects from the cache and writes to the apiserver + client client.Client + scheme *runtime.Scheme + tests bool +} + +// Reconcile reads that state of the cluster for a CheCluster object and makes changes based on the state read +// and what is in the CheCluster.Spec. The Controller will requeue the Request to be processed again if the returned error is non-nil or +// Result.Requeue is true, otherwise upon completion it will remove the work from the queue. + +func (r *ReconcileChe) Reconcile(request reconcile.Request) (reconcile.Result, error) { + + // Fetch the CheCluster instance + tests := r.tests + var instance, err = r.GetCR(request) + if err != nil { + if errors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. + // Return and don't requeue + return reconcile.Result{}, nil + } + // Error reading the object - requeue the request. + return reconcile.Result{}, err + } + + isOpenShift, err := util.DetectOpenShift() + if err != nil { + logrus.Errorf("An error occurred when detecting current infra: %s", err) + } + // create a secret with router tls cert if self signed certs are in use + // requires cluster admin privileges + selfSignedCert := instance.Spec.Server.SelfSignedCert + if isOpenShift && selfSignedCert { + crt, err := k8sclient.GetDefaultRouterCert("openshift-ingress") + if err != nil { + logrus.Errorf("Default router tls secret not found. Self signed cert isn't added to CheCluster deployment") + return reconcile.Result{}, err + } else { + secret := deploy.NewSecret(instance, "self-signed-certificate", crt) + if err := r.CreateNewSecret(instance, secret); err != nil { + return reconcile.Result{}, err + } + } + } + if !tests { + deployment := &appsv1.Deployment{} + name := "che" + cheFlavor := instance.Spec.Server.CheFlavor + if cheFlavor == "codeready" { + name = cheFlavor + } + err = r.client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: instance.Namespace}, deployment) + if err != nil && instance.Status.CheClusterRunning != UnavailableStatus { + if err := r.SetCheUnavailableStatus(instance, request); err != nil { + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 1}, err + } + } + } + // create service accounts: + // che is the one which token is used to create workspace objects + // che-workspace is SA used by plugins like exec and terminal with limited privileges like view and exec + cheServiceAccount := deploy.NewServiceAccount(instance, "che") + if err := r.CreateServiceAccount(instance, cheServiceAccount); err != nil { + return reconcile.Result{}, err + } + workspaceServiceAccount := deploy.NewServiceAccount(instance, "che-workspace") + if err := r.CreateServiceAccount(instance, workspaceServiceAccount); err != nil { + return reconcile.Result{}, err + } + // create exec and view roles for CheCluster server and workspaces + execRole := deploy.NewRole(instance, "exec", []string{"pods/exec"}, []string{"*"}) + if err := r.CreateNewRole(instance, execRole); err != nil { + return reconcile.Result{}, err + } + viewRole := deploy.NewRole(instance, "view", []string{"pods"}, []string{"list"}) + if err := r.CreateNewRole(instance, viewRole); err != nil { + return reconcile.Result{}, err + } + // create RoleBindings for created (and existing ClusterRole) roles and service accounts + cheRoleBinding := deploy.NewRoleBinding(instance, "che", cheServiceAccount.Name, "edit", "ClusterRole") + if err := r.CreateNewRoleBinding(instance, cheRoleBinding); err != nil { + return reconcile.Result{}, err + } + execRoleBinding := deploy.NewRoleBinding(instance, "che-workspace-exec", workspaceServiceAccount.Name, execRole.Name, "Role") + if err = r.CreateNewRoleBinding(instance, execRoleBinding); err != nil { + return reconcile.Result{}, err + } + viewRoleBinding := deploy.NewRoleBinding(instance, "che-workspace-view", workspaceServiceAccount.Name, viewRole.Name, "Role") + if err := r.CreateNewRoleBinding(instance, viewRoleBinding); err != nil { + return reconcile.Result{}, err + } + if err := r.GenerateAndSaveFields(instance, request); err != nil { + instance, _ = r.GetCR(request) + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 1}, err + } + chePostgresPassword := instance.Spec.Database.ChePostgresPassword + keycloakPostgresPassword := instance.Spec.Auth.KeycloakPostgresPassword + keycloakAdminPassword := instance.Spec.Auth.KeycloakAdminPassword + + // Create Postgres resources and provisioning unless an external DB is used + externalDB := instance.Spec.Database.ExternalDB + if !externalDB { + // Create a new postgres service + postgresLabels := deploy.GetLabels(instance, "postgres") + if err := r.CreateService(instance, "postgres", postgresLabels, "postgres", 5432); err != nil { + return reconcile.Result{}, err + } + // Create a new Postgres PVC object + pvc := deploy.NewPvc(instance, "postgres-data", "1Gi", postgresLabels) + if err := r.CreatePVC(instance, pvc); err != nil { + return reconcile.Result{}, err + } + if !tests { + err = r.client.Get(context.TODO(), types.NamespacedName{Name: pvc.Name, Namespace: instance.Namespace}, pvc) + if pvc.Status.Phase != "Bound" { + k8sclient.GetPostgresStatus(pvc, instance.Namespace) + } + } + // Create a new Postgres deployment + postgresDeployment := deploy.NewPostgresDeployment(instance, chePostgresPassword) + if err := r.CreateNewDeployment(instance, postgresDeployment); err != nil { + return reconcile.Result{}, err + } + time.Sleep(time.Duration(1) * time.Second) + pgDeployment, err := r.GetEffectiveDeployment(instance, postgresDeployment.Name) + if err != nil { + logrus.Errorf("Failed to get %s deployment: %s", postgresDeployment.Name, err) + return reconcile.Result{}, err + } + if !tests { + if pgDeployment.Status.AvailableReplicas != 1 { + scaled := k8sclient.GetDeploymentStatus("postgres", instance.Namespace) + if !scaled { + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 5}, err + } + } + pgCommand := deploy.GetPostgresProvisionCommand(instance) + dbStatus := instance.Status.DbProvisoned + // provision Db and users for Che and Keycloak servers + if !dbStatus { + podToExec, err := k8sclient.GetDeploymentPod(pgDeployment.Name, instance.Namespace) + if err != nil { + return reconcile.Result{}, err + } + provisioned := ExecIntoPod(podToExec, pgCommand, "create Keycloak DB, user, privileges", instance.Namespace) + if provisioned { + for { + instance.Status.DbProvisoned = true + if err := r.UpdateCheCRStatus(instance, "status: provisioned with DB and user", "true"); err != nil { + instance, _ = r.GetCR(request) + } else { + break + } + } + } else { + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 5}, err + } + } + } + } + cheFlavor := util.GetValue(instance.Spec.Server.CheFlavor, deploy.DefaultCheFlavor) + ingressStrategy := util.GetValue(instance.Spec.K8SOnly.IngressStrategy, deploy.DefaultIngressStrategy) + ingressDomain := instance.Spec.K8SOnly.IngressDomain + tlsSupport := instance.Spec.Server.TlsSupport + protocol := "http" + if tlsSupport { + protocol = "https" + } + // create Che service and route + cheLabels := deploy.GetLabels(instance, util.GetValue(instance.Spec.Server.CheFlavor, deploy.DefaultCheFlavor)) + + if err := r.CreateService(instance, "che-host", cheLabels, "http", 8080); err != nil { + return reconcile.Result{}, err + } + if !isOpenShift { + cheIngress := deploy.NewIngress(instance, cheFlavor, "che-host", 8080) + if err := r.CreateNewIngress(instance, cheIngress); err != nil { + return reconcile.Result{}, err + } + cheHost := ingressDomain + if ingressStrategy == "multi-host" { + cheHost = cheFlavor + "-" + instance.Namespace + "." + ingressDomain + } + if len(instance.Spec.Server.CheHost) == 0 { + instance.Spec.Server.CheHost = cheHost + if err := r.UpdateCheCRSpec(instance, "CheHost URL", cheHost); err != nil { + instance, _ = r.GetCR(request) + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 1}, err + } + } + } else { + cheRoute := deploy.NewRoute(instance, cheFlavor, "che-host") + if tlsSupport { + cheRoute = deploy.NewTlsRoute(instance, cheFlavor, "che-host") + } + if err := r.CreateNewRoute(instance, cheRoute); err != nil { + return reconcile.Result{}, err + } + if len(instance.Spec.Server.CheHost) == 0 { + instance.Spec.Server.CheHost = cheRoute.Spec.Host + if len(cheRoute.Spec.Host) < 1 { + cheRoute := r.GetEffectiveRoute(instance, cheRoute.Name) + instance.Spec.Server.CheHost = cheRoute.Spec.Host + } + if err := r.UpdateCheCRSpec(instance, "CheHost URL", instance.Spec.Server.CheHost); err != nil { + instance, _ = r.GetCR(request) + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 1}, err + } + } + } + // create and provision Keycloak related objects + ExternalKeycloak := instance.Spec.Auth.ExternalKeycloak + if !ExternalKeycloak { + keycloakLabels := deploy.GetLabels(instance, "keycloak") + if err := r.CreateService(instance, "keycloak", keycloakLabels, "http", 8080); err != nil { + return reconcile.Result{}, err + } + // create Keycloak ingresses when on k8s + if !isOpenShift { + keycloakIngress := deploy.NewIngress(instance, "keycloak", "keycloak", 8080) + if err := r.CreateNewIngress(instance, keycloakIngress); err != nil { + return reconcile.Result{}, err + } + keycloakURL := protocol + "://" + ingressDomain + if ingressStrategy == "multi-host" { + keycloakURL = protocol + "://keycloak-" + instance.Namespace + "." + ingressDomain + } + if len(instance.Spec.Auth.KeycloakURL) == 0 { + instance.Spec.Auth.KeycloakURL = keycloakURL + if err := r.UpdateCheCRSpec(instance, "Keycloak URL", instance.Spec.Auth.KeycloakURL); err != nil { + instance, _ = r.GetCR(request) + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 1}, err + } + } + } else { + // create Keycloak route + keycloakRoute := deploy.NewRoute(instance, "keycloak", "keycloak") + if tlsSupport { + keycloakRoute = deploy.NewTlsRoute(instance, "keycloak", "keycloak") + } + if err = r.CreateNewRoute(instance, keycloakRoute); err != nil { + return reconcile.Result{}, err + } + keycloakURL := keycloakRoute.Spec.Host + if len(instance.Spec.Auth.KeycloakURL) == 0 { + instance.Spec.Auth.KeycloakURL = protocol + "://" + keycloakURL + if len(keycloakURL) < 1 { + keycloakURL := r.GetEffectiveRoute(instance, keycloakRoute.Name).Spec.Host + instance.Spec.Auth.KeycloakURL = protocol + "://" + keycloakURL + } + if err := r.UpdateCheCRSpec(instance, "Keycloak URL", instance.Spec.Auth.KeycloakURL); err != nil { + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 1}, err + } + instance.Status.KeycloakURL = protocol + "://" + keycloakURL + if err := r.UpdateCheCRStatus(instance, "status: Keycloak URL", instance.Spec.Auth.KeycloakURL); err != nil { + instance, _ = r.GetCR(request) + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 1}, err + } + } + } + keycloakDeployment := deploy.NewKeycloakDeployment(instance, keycloakPostgresPassword, keycloakAdminPassword, cheFlavor) + if err := r.CreateNewDeployment(instance, keycloakDeployment); err != nil { + return reconcile.Result{}, err + } + time.Sleep(time.Duration(1) * time.Second) + deployment, err := r.GetEffectiveDeployment(instance, keycloakDeployment.Name) + if err != nil { + logrus.Errorf("Failed to get %s deployment: %s", keycloakDeployment.Name, err) + return reconcile.Result{}, err + } + if !tests { + if deployment.Status.AvailableReplicas != 1 { + scaled := k8sclient.GetDeploymentStatus(keycloakDeployment.Name, instance.Namespace) + if !scaled { + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 5}, err + } + } + keycloakRealmClientStatus := instance.Status.KeycloakProvisoned + if !keycloakRealmClientStatus { + if err := r.CreateKyecloakResources(instance, request, keycloakDeployment.Name); err != nil { + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 5}, err + } + } + } + if isOpenShift { + doInstallOpenShiftoAuthProvider := instance.Spec.Auth.OpenShiftOauth + if doInstallOpenShiftoAuthProvider { + openShiftIdentityProviderStatus := instance.Status.OpenShiftoAuthProvisioned + if !openShiftIdentityProviderStatus { + if err := r.CreateIdentityProviderItems(instance, request, cheFlavor, keycloakDeployment.Name); err != nil { + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 5}, err + } + } + } + } + } + // create Che ConfigMap + cheHost := instance.Spec.Server.CheHost + cheEnv := deploy.GetConfigMapData(instance) + cheConfigMap := deploy.NewCheConfigMap(instance, cheEnv) + if err := r.CreateNewConfigMap(instance, cheConfigMap); err != nil { + return reconcile.Result{}, err + } + + // create a custom configmap that won't be synced with CR spec + // to be able to override envs and not clutter CR spec with fields + customCM := &corev1.ConfigMap{ + Data: map[string]string{"SAMPLE_KEY": "SAMPLE_VALUE"}, + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "custom", + Namespace: instance.Namespace, + Labels: cheLabels}} + if err := r.CreateNewConfigMap(instance, customCM); err != nil { + return reconcile.Result{}, err + } + + cmResourceVersion := cheConfigMap.ResourceVersion + // create Che deployment + cheImageRepo := util.GetValue(instance.Spec.Server.CheImage, deploy.DefaultCheServerImageRepo) + cheImageTag := util.GetValue(instance.Spec.Server.CheImageTag, deploy.DefaultCheServerImageTag) + if cheFlavor == "codeready" { + cheImageRepo = util.GetValue(instance.Spec.Server.CheImage, deploy.DefaultCodeReadyServerImageRepo) + cheImageTag = util.GetValue(instance.Spec.Server.CheImageTag, deploy.DefaultCodeReadyServerImageTag) + } + cheDeployment := deploy.NewCheDeployment(instance, cheImageRepo, cheImageTag, cmResourceVersion) + if err := r.CreateNewDeployment(instance, cheDeployment); err != nil { + return reconcile.Result{}, err + } + // sometimes Get cannot find deployment right away + time.Sleep(time.Duration(1) * time.Second) + deployment, err := r.GetEffectiveDeployment(instance, cheDeployment.Name) + if err != nil { + logrus.Errorf("Failed to get %s deployment: %s", cheDeployment.Name, err) + return reconcile.Result{}, err + } + + if !tests { + if deployment.Status.AvailableReplicas != 1 { + instance, _ := r.GetCR(request) + if err := r.SetCheUnavailableStatus(instance, request); err != nil { + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 1}, err + } + scaled := k8sclient.GetDeploymentStatus(cheDeployment.Name, instance.Namespace) + if !scaled { + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 5}, err + } + err = r.client.Get(context.TODO(), types.NamespacedName{Name: cheDeployment.Name, Namespace: instance.Namespace}, deployment) + if deployment.Status.AvailableReplicas == 1 { + if err := r.SetCheAvailableStatus(instance, request, protocol, cheHost); err != nil { + instance, _ = r.GetCR(request) + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 1}, err + } + if instance.Status.CheVersion != cheImageTag { + instance.Status.CheVersion = cheImageTag + if err := r.UpdateCheCRStatus(instance, "version", cheImageTag); err != nil { + instance, _ = r.GetCR(request) + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 1}, err + } + } + } + } + if deployment.Status.Replicas > 1 { + logrus.Infof("Deployment %s is in the rolling update state", cheDeployment.Name) + if err := r.SetCheRollingUpdateStatus(instance, request); err != nil { + instance, _ = r.GetCR(request) + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 1}, err + } + k8sclient.GetDeploymentRollingUpdateStatus(cheDeployment.Name, instance.Namespace) + deployment, _ := r.GetEffectiveDeployment(instance, cheDeployment.Name) + if deployment.Status.Replicas == 1 { + if err := r.SetCheAvailableStatus(instance, request, protocol, cheHost); err != nil { + instance, _ = r.GetCR(request) + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 1}, err + } + } + } + } + if deployment.Spec.Template.Spec.Containers[0].Image != cheDeployment.Spec.Template.Spec.Containers[0].Image { + if err := controllerutil.SetControllerReference(instance, deployment, r.scheme); err != nil { + logrus.Errorf("An error occurred: %s", err) + } + logrus.Infof("Updating %s %s with image %s:%s", cheDeployment.Name, cheDeployment.Kind, cheImageRepo, cheImageTag) + instance.Status.CheVersion = cheImageTag + if err := r.UpdateCheCRStatus(instance, "version", cheImageTag); err != nil { + instance, _ = r.GetCR(request) + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 1}, err + } + if err := r.client.Update(context.TODO(), cheDeployment); err != nil { + logrus.Errorf("Failed to update %s %s: %s", deployment.Kind, deployment.Name, err) + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 1}, err + + } + } + + // reconcile routes/ingresses before reconciling Che deployment + activeConfigMap := &corev1.ConfigMap{} + if err := r.client.Get(context.TODO(), types.NamespacedName{Name: "che", Namespace: instance.Namespace}, activeConfigMap); err != nil { + logrus.Errorf("ConfigMap %s not found: %s", activeConfigMap.Name, err) + } + if !tlsSupport && activeConfigMap.Data["CHE_INFRA_OPENSHIFT_TLS__ENABLED"] == "true" { + routesUpdated, err := r.ReconcileTLSObjects(instance, request, cheFlavor, tlsSupport, isOpenShift) + if err != nil { + logrus.Errorf("An error occurred when updating routes %s", err) + } + if routesUpdated { + logrus.Info("Routes have been updated with TLS config") + } + } + if tlsSupport && activeConfigMap.Data["CHE_INFRA_OPENSHIFT_TLS__ENABLED"] == "false" { + routesUpdated, err := r.ReconcileTLSObjects(instance, request, cheFlavor, tlsSupport, isOpenShift) + if err != nil { + logrus.Errorf("An error occurred when updating routes %s", err) + } + if routesUpdated { + logrus.Info("Routes have been updated with TLS config") + } + } + // Reconcile Che ConfigMap to align with CR spec + cmUpdated, err := r.UpdateConfigMap(instance) + if err != nil { + return reconcile.Result{}, err + } + // Delete OpenShift identity provider if OpenShift oAuth is false in spec + // but OpenShiftoAuthProvisioned is true in CR status, e.g. when oAuth has been turned on and then turned off + deleted, err := r.ReconcileIdentityProvider(instance) + if deleted { + instance.Status.OpenShiftoAuthProvisioned = false + if err := r.UpdateCheCRStatus(instance, "provisioned with OpenShift oAuth", "false"); err != nil { + instance, _ = r.GetCR(request) + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 1}, err + } + instance.Spec.Auth.OauthSecret = "" + if err := r.UpdateCheCRSpec(instance, "delete oAuth secret name", ""); err != nil { + instance, _ = r.GetCR(request) + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 1}, err + } + instance.Spec.Auth.OauthClientName = "" + if err := r.UpdateCheCRSpec(instance, "delete oAuth client name", ""); err != nil { + instance, _ = r.GetCR(request) + return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 1}, err + } + } + + if cmUpdated { + // sometimes an old cm resource version is returned ie get happens too fast - before server updates CM + time.Sleep(time.Duration(1) * time.Second) + cm := r.GetEffectiveConfigMap(instance, cheConfigMap.Name) + cmResourceVersion := cm.ResourceVersion + cheDeployment := deploy.NewCheDeployment(instance, cheImageRepo, cheImageTag, cmResourceVersion) + if err := controllerutil.SetControllerReference(instance, cheDeployment, r.scheme); err != nil { + logrus.Errorf("An error occurred: %s", err) + } + if err := r.client.Update(context.TODO(), cheDeployment); err != nil { + return reconcile.Result{}, err + } + } + return reconcile.Result{}, nil +} diff --git a/pkg/controller/che/che_controller_test.go b/pkg/controller/che/che_controller_test.go new file mode 100644 index 000000000..3140bdcf7 --- /dev/null +++ b/pkg/controller/che/che_controller_test.go @@ -0,0 +1,167 @@ +// +// 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 che + +import ( + "context" + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + routev1 "github.com/openshift/api/route/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + oauth "github.com/openshift/api/oauth/v1" + + "testing" +) + +func TestCheController(t *testing.T) { + + // Set the logger to development mode for verbose logs. + logf.SetLogger(logf.ZapLogger(true)) + + var ( + name = "eclipse-che" + namespace = "eclipse-che" + ) + + pgPod := &corev1.Pod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-pg-pod", + Namespace: "eclipse-che", + Labels: map[string]string{ + "component": "postgres", + }, + }, + } + + // A CheCluster custom resource with metadata and spec + cheCR := &orgv1.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: orgv1.CheClusterSpec{ + // todo add some spec to check controller ifs like external db, ssl etc + }, + } + // Objects to track in the fake client. + objs := []runtime.Object{ + cheCR, pgPod, + } + + route := &routev1.Route{} + oAuthClient := &oauth.OAuthClient{} + + // Register operator types with the runtime scheme + s := scheme.Scheme + s.AddKnownTypes(orgv1.SchemeGroupVersion, cheCR) + s.AddKnownTypes(routev1.SchemeGroupVersion, route) + s.AddKnownTypes(oauth.SchemeGroupVersion, oAuthClient) + + // Create a fake client to mock API calls + cl := fake.NewFakeClient(objs...) + tests := true + + // Create a ReconcileChe object with the scheme and fake client + r := &ReconcileChe{client: cl, scheme: s, tests: tests} + + // Mock request to simulate Reconcile() being called on an event for a + // watched resource . + req := reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: name, + Namespace: namespace, + }, + } + res, err := r.Reconcile(req) + if err != nil { + t.Fatalf("reconcile: (%v)", err) + } + // Check the result of reconciliation to make sure it has the desired state. + if res.Requeue { + t.Error("Reconcile did not requeue request as expected") + } + + // update CR and make sure Che configmap has been updated + cheCR.Spec.Server.TlsSupport = true + if err := cl.Update(context.TODO(), cheCR); err != nil { + t.Error("Failed to update CheCluster custom resource") + } + + // reconcile again + res, err = r.Reconcile(req) + if err != nil { + t.Fatalf("reconcile: (%v)", err) + } + + // get configmap + cm := &corev1.ConfigMap{} + if err := cl.Get(context.TODO(), types.NamespacedName{Name: "che", Namespace: cheCR.Namespace}, cm); err != nil { + t.Errorf("ConfigMap %s not found: %s", cm.Name, err) + } + + // run a few checks to make sure the operator reconciled tls routes and updated configmap + if cm.Data["CHE_INFRA_OPENSHIFT_TLS__ENABLED"] != "true" { + t.Errorf("ConfigMap wasn't updated. Extecting true, got: %s", cm.Data["CHE_INFRA_OPENSHIFT_TLS__ENABLED"]) + } + if err := cl.Get(context.TODO(), types.NamespacedName{Name: "che", Namespace: cheCR.Namespace}, route); err != nil { + t.Errorf("Route %s not found: %s", cm.Name, err) + } + if route.Spec.TLS.Termination != "edge" { + t.Errorf("Test failed as %s %s is not a TLS route", route.Kind, route.Name) + } + + // update CR and make sure Che configmap has been updated + cheCR.Spec.Auth.OpenShiftOauth = true + if err := cl.Update(context.TODO(), cheCR); err != nil { + t.Error("Failed to update CheCluster custom resource") + } + // reconcile again + res, err = r.Reconcile(req) + if err != nil { + t.Fatalf("reconcile: (%v)", err) + } + + // get configmap and check if identity provider name and workspace project name are correctly set + cm = &corev1.ConfigMap{} + if err := cl.Get(context.TODO(), types.NamespacedName{Name: "che", Namespace: cheCR.Namespace}, cm); err != nil { + t.Errorf("ConfigMap %s not found: %s", cm.Name, err) + } + if cm.Data["CHE_INFRA_OPENSHIFT_PROJECT"] != "" { + t.Errorf("ConfigMap wasn't updated properly. Extecting empty string, got: '%s'", cm.Data["CHE_INFRA_OPENSHIFT_PROJECT"]) + } + expectedIdentityProviderName := "openshift-v3" + if cm.Data["CHE_INFRA_OPENSHIFT_OAUTH__IDENTITY__PROVIDER"] != expectedIdentityProviderName { + t.Errorf("ConfigMap wasn't updated properly. Extecting '%s', got: '%s'", expectedIdentityProviderName, cm.Data["CHE_INFRA_OPENSHIFT_OAUTH__IDENTITY__PROVIDER"]) + } + + err = r.client.Get(context.TODO(), types.NamespacedName{Name: cheCR.Name, Namespace: cheCR.Namespace}, cheCR) + err = r.CreateIdentityProviderItems(cheCR,req, "che", "keycloak") + oAuthClientName := cheCR.Spec.Auth.OauthClientName + oauthSecret := cheCR.Spec.Auth.OauthSecret + if err = r.client.Get(context.TODO(), types.NamespacedName{Name: oAuthClientName, Namespace: ""}, oAuthClient); err != nil { + t.Errorf("Failed to Get oAuthClient %s: %s", oAuthClient.Name, err) + } + if oAuthClient.Secret != oauthSecret { + t.Errorf("Secrets do not match. Expecting %s, got %s", oauthSecret, oAuthClient.Secret) + } + +} diff --git a/pkg/controller/che/create.go b/pkg/controller/che/create.go new file mode 100644 index 000000000..42987322c --- /dev/null +++ b/pkg/controller/che/create.go @@ -0,0 +1,474 @@ +// +// 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 che + +import ( + "context" + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + "github.com/eclipse/che-operator/pkg/deploy" + "github.com/eclipse/che-operator/pkg/util" + oauth "github.com/openshift/api/oauth/v1" + routev1 "github.com/openshift/api/route/v1" + "github.com/sirupsen/logrus" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/api/extensions/v1beta1" + rbac "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "strings" +) + +func (r *ReconcileChe) CreateNewDeployment(instance *orgv1.CheCluster, deployment *appsv1.Deployment) error { + if err := controllerutil.SetControllerReference(instance, deployment, r.scheme); err != nil { + logrus.Errorf("An error occurred: %s", err) + return err + } + deploymentFound := &appsv1.Deployment{} + err := r.client.Get(context.TODO(), types.NamespacedName{Name: deployment.Name, Namespace: deployment.Namespace}, deploymentFound) + if err != nil && errors.IsNotFound(err) { + logrus.Infof("Creating a new object: %s, name: %s", deployment.Kind, deployment.Name) + err = r.client.Create(context.TODO(), deployment) + if err != nil { + logrus.Errorf("Failed to create %s %s: %s", deployment.Kind, deployment.Name, err) + return err + } + return nil + } else if err != nil { + logrus.Errorf("An error occurred: %s", err) + return err + } + return nil +} + +func (r *ReconcileChe) CreateNewConfigMap(instance *orgv1.CheCluster, configMap *corev1.ConfigMap) error { + if err := controllerutil.SetControllerReference(instance, configMap, r.scheme); err != nil { + logrus.Errorf("An error occurred: %s", err) + return err + } + configMapFound := &corev1.ConfigMap{} + err := r.client.Get(context.TODO(), types.NamespacedName{Name: configMap.Name, Namespace: configMap.Namespace}, configMapFound) + if err != nil && errors.IsNotFound(err) { + logrus.Infof("Creating a new object: %s, name: %s", configMap.Kind, configMap.Name) + err = r.client.Create(context.TODO(), configMap) + if err != nil { + logrus.Errorf("Failed to create %s %s: %s", configMap.Kind, configMap.Name, err) + return err + } + return nil + } else if err != nil { + logrus.Errorf("An error occurred: %s", err) + + return err + } + return nil +} + +func (r *ReconcileChe) CreateServiceAccount(cr *orgv1.CheCluster, serviceAccount *corev1.ServiceAccount) error { + if err := controllerutil.SetControllerReference(cr, serviceAccount, r.scheme); err != nil { + return err + } + serviceAccountFound := &corev1.ServiceAccount{} + err := r.client.Get(context.TODO(), types.NamespacedName{Name: serviceAccount.Name, Namespace: serviceAccount.Namespace}, serviceAccountFound) + if err != nil && errors.IsNotFound(err) { + logrus.Infof("Creating a new object: %s, name: %s", serviceAccount.Kind, serviceAccount.Name) + err = r.client.Create(context.TODO(), serviceAccount) + if err != nil { + logrus.Errorf("Failed to create %s %s: %s", serviceAccount.Name, serviceAccount.Kind, err) + return err + } + return nil + } else if err != nil { + return err + } + return nil +} + +func (r *ReconcileChe) CreateNewRole(instance *orgv1.CheCluster, role *rbac.Role) error { + if err := controllerutil.SetControllerReference(instance, role, r.scheme); err != nil { + return err + } + roleFound := &rbac.Role{} + err := r.client.Get(context.TODO(), types.NamespacedName{Name: role.Name, Namespace: role.Namespace}, roleFound) + if err != nil && errors.IsNotFound(err) { + logrus.Infof("Creating a new object: %s, name: %s", role.Kind, role.Name) + err = r.client.Create(context.TODO(), role) + if err != nil { + logrus.Errorf("Failed to create %s %s: %s", role.Name, role.Kind, err) + return err + } + return nil + } else if err != nil { + logrus.Errorf("An error occurred: %s", err) + return err + } + return nil +} + +func (r *ReconcileChe) CreateNewIngress(instance *orgv1.CheCluster, ingress *v1beta1.Ingress) error { + if err := controllerutil.SetControllerReference(instance, ingress, r.scheme); err != nil { + logrus.Errorf("An error occurred %s", err) + return err + } + ingressFound := &v1beta1.Ingress{} + err := r.client.Get(context.TODO(), types.NamespacedName{Name: ingress.Name, Namespace: ingress.Namespace}, ingressFound) + if err != nil && errors.IsNotFound(err) { + logrus.Infof("Creating a new object %s, name: %s", ingress.Kind, ingress.Name) + if err := r.client.Create(context.TODO(), ingress); err != nil { + logrus.Errorf("Failed to create %s %s: %s", ingress.Kind, ingress.Name, err) + return err + } + return nil + } else if err != nil { + logrus.Errorf("An error occurred %s", err) + + return err + } + return nil +} + +func (r *ReconcileChe) CreateNewRoute(instance *orgv1.CheCluster, route *routev1.Route) error { + if err := controllerutil.SetControllerReference(instance, route, r.scheme); err != nil { + logrus.Errorf("An error occurred %s", err) + return err + } + routeFound := &routev1.Route{} + err := r.client.Get(context.TODO(), types.NamespacedName{Name: route.Name, Namespace: route.Namespace}, routeFound) + if err != nil && errors.IsNotFound(err) { + logrus.Infof("Creating a new object %s, name: %s", route.Kind, route.Name) + err = r.client.Create(context.TODO(), route) + if err != nil { + logrus.Errorf("Failed to create %s %s: %s", route.Kind, route.Name, err) + return err + } + // Route created successfully - don't requeue + return nil + } else if err != nil { + logrus.Errorf("An error occurred %s", err) + return err + } + return nil +} + +func (r *ReconcileChe) CreateNewSecret(instance *orgv1.CheCluster, secret *corev1.Secret) error { + if err := controllerutil.SetControllerReference(instance, secret, r.scheme); err != nil { + logrus.Errorf("An error occurred: %s", err) + return err + } + deploymentFound := &corev1.Secret{} + err := r.client.Get(context.TODO(), types.NamespacedName{Name: secret.Name, Namespace: secret.Namespace}, deploymentFound) + if err != nil && errors.IsNotFound(err) { + logrus.Infof("Creating a new object: %s, name: %s", secret.Kind, secret.Name) + err = r.client.Create(context.TODO(), secret) + if err != nil { + logrus.Errorf("Failed to create %s %s: %s", secret.Kind, secret.Name, err) + return err + } + return nil + } else if err != nil { + logrus.Errorf("An error occurred: %s", err) + + return err + } + return nil +} + +func (r *ReconcileChe) CreateNewOauthClient(instance *orgv1.CheCluster, oAuthClient *oauth.OAuthClient) error { + if err := controllerutil.SetControllerReference(instance, oAuthClient, r.scheme); err != nil { + logrus.Errorf("An error occurred: %s", err) + return err + } + oAuthClientFound := &oauth.OAuthClient{} + err := r.client.Get(context.TODO(), types.NamespacedName{Name: oAuthClient.Name, Namespace: oAuthClient.Namespace}, oAuthClientFound) + if err != nil && errors.IsNotFound(err) { + logrus.Infof("Creating a new object: %s, name: %s", oAuthClient.Kind, oAuthClient.Name) + err = r.client.Create(context.TODO(), oAuthClient) + if err != nil { + logrus.Errorf("Failed to create %s %s: %s", oAuthClient.Kind, oAuthClient.Name, err) + return err + } + return nil + } else if err != nil { + logrus.Errorf("An error occurred: %s", err) + + return err + } + return nil +} + +// CreateService creates a service with a given name, port, selector and labels +func (r *ReconcileChe) CreateService(cr *orgv1.CheCluster, name string, labels map[string]string, portName string, portNumber int32) error { + service := deploy.NewService(cr, name, labels, portName, portNumber) + if err := controllerutil.SetControllerReference(cr, service, r.scheme); err != nil { + logrus.Errorf("An error occurred %s", err) + return err + } + serviceFound := &corev1.Service{} + err := r.client.Get(context.TODO(), types.NamespacedName{Name: service.Name, Namespace: service.Namespace}, serviceFound) + if err != nil && errors.IsNotFound(err) { + logrus.Infof("Creating a new object %s, name: %s", service.Kind, service.Name) + err = r.client.Create(context.TODO(), service) + if err != nil { + logrus.Errorf("Failed to create %s %s: %s", service.Kind, service.Name) + return err + } + return nil + } else if err != nil { + logrus.Errorf("An error occurred %s", err) + return err + } + return nil +} + +func (r *ReconcileChe) CreatePVC(instance *orgv1.CheCluster, pvc *corev1.PersistentVolumeClaim) error { + // Set CheCluster instance as the owner and controller + if err := controllerutil.SetControllerReference(instance, pvc, r.scheme); err != nil { + return err + } + pvcFound := &corev1.PersistentVolumeClaim{} + err := r.client.Get(context.TODO(), types.NamespacedName{Name: pvc.Name, Namespace: pvc.Namespace}, pvcFound) + if err != nil && errors.IsNotFound(err) { + logrus.Infof("Creating a new object %s, name: %s", pvc.Kind, pvc.Name) + err = r.client.Create(context.TODO(), pvc) + if err != nil { + return err + } + return nil + } else if err != nil { + return err + } + return nil + +} + +func (r *ReconcileChe) CreateNewRoleBinding(instance *orgv1.CheCluster, roleBinding *rbac.RoleBinding) error { + if err := controllerutil.SetControllerReference(instance, roleBinding, r.scheme); err != nil { + return err + } + roleBindingFound := &rbac.RoleBinding{} + err := r.client.Get(context.TODO(), types.NamespacedName{Name: roleBinding.Name, Namespace: roleBinding.Namespace}, roleBindingFound) + if err != nil && errors.IsNotFound(err) { + logrus.Infof("Creating a new object: %s, name: %s", roleBinding.Kind, roleBinding.Name) + err = r.client.Create(context.TODO(), roleBinding) + if err != nil { + logrus.Errorf("Failed to create %s %s: %s", roleBinding.Name, roleBinding.Kind, err) + return err + } + return nil + } else if err != nil { + logrus.Errorf("An error occurred: %s", err) + return err + } + return nil +} + +func (r *ReconcileChe) CreateIdentityProviderItems(instance *orgv1.CheCluster, request reconcile.Request, cheFlavor string, keycloakDeploymentName string) (err error) { + tests := r.tests + keycloakAdminPassword := instance.Spec.Auth.KeycloakAdminPassword + 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 := r.UpdateCheCRSpec(instance, "oAuthClient name", oAuthClientName); err != nil { + return err + } + } + oauthSecret := instance.Spec.Auth.OauthSecret + if len(oauthSecret) < 1 { + oauthSecret = util.GeneratePasswd(12) + instance.Spec.Auth.OauthSecret = oauthSecret + if err := r.UpdateCheCRSpec(instance, "oAuthC secret name", oauthSecret); err != nil { + return err + } + } + keycloakURL := instance.Spec.Auth.KeycloakURL + keycloakRealm := util.GetValue(instance.Spec.Auth.KeycloakRealm, cheFlavor) + oAuthClient := deploy.NewOAuthClient(oAuthClientName, oauthSecret, keycloakURL, keycloakRealm) + if err := r.CreateNewOauthClient(instance, oAuthClient); err != nil { + return err + } + + if !tests { + openShiftIdentityProviderCommand := deploy.GetOpenShiftIdentityProviderProvisionCommand(instance, oAuthClientName, oauthSecret, keycloakAdminPassword) + podToExec, err := k8sclient.GetDeploymentPod(keycloakDeploymentName, instance.Namespace) + if err != nil { + logrus.Errorf("Failed to retrieve pod name. Further exec will fail") + return err + } + provisioned := ExecIntoPod(podToExec, openShiftIdentityProviderCommand, "create OpenShift identity provider", instance.Namespace) + if provisioned { + for { + instance.Status.OpenShiftoAuthProvisioned = true + if err := r.UpdateCheCRStatus(instance, "status: provisioned with OpenShift identity provider", "true"); err != nil { + instance, _ = r.GetCR(request) + } else { + break + } + } + } + return nil + } + return nil +} + +func (r *ReconcileChe) GenerateAndSaveFields(instance *orgv1.CheCluster, request reconcile.Request) (err error) { + + chePostgresPassword := util.GetValue(instance.Spec.Database.ChePostgresPassword, util.GeneratePasswd(12)) + if len(instance.Spec.Database.ChePostgresPassword) < 1 { + instance.Spec.Database.ChePostgresPassword = chePostgresPassword + if err := r.UpdateCheCRSpec(instance, "auto-generated CheCluster DB password", "password-hidden"); err != nil { + return err + } + + } + keycloakPostgresPassword := util.GetValue(instance.Spec.Auth.KeycloakPostgresPassword, util.GeneratePasswd(12)) + if len(instance.Spec.Auth.KeycloakPostgresPassword) < 1 { + instance.Spec.Auth.KeycloakPostgresPassword = keycloakPostgresPassword + if err := r.UpdateCheCRSpec(instance, "auto-generated Keycloak DB password", "password-hidden"); err != nil { + return err + } + } + if len(instance.Spec.Auth.KeycloakAdminPassword) < 1 { + keycloakAdminPassword := util.GetValue(instance.Spec.Auth.KeycloakAdminPassword, util.GeneratePasswd(12)) + instance.Spec.Auth.KeycloakAdminPassword = keycloakAdminPassword + if err := r.UpdateCheCRSpec(instance, "Keycloak admin password", "password hidden"); err != nil { + return err + } + } + if len(instance.Spec.Auth.KeycloakAdminUserName) < 1 { + keycloakAdminUserName := util.GetValue(instance.Spec.Auth.KeycloakAdminUserName, "admin") + instance.Spec.Auth.KeycloakAdminUserName = keycloakAdminUserName + if err := r.UpdateCheCRSpec(instance, "Keycloak admin username", "password hidden"); err != nil { + return err + } + } + chePostgresUser := util.GetValue(instance.Spec.Database.ChePostgresUser, "pgche") + if len(instance.Spec.Database.ChePostgresUser) < 1 { + instance.Spec.Database.ChePostgresUser = chePostgresUser + if err := r.UpdateCheCRSpec(instance, "Postgres User", chePostgresUser); err != nil { + return err + } + } + chePostgresDb := util.GetValue(instance.Spec.Database.ChePostgresDb, "dbche") + if len(instance.Spec.Database.ChePostgresDb) < 1 { + instance.Spec.Database.ChePostgresDb = chePostgresDb + if err := r.UpdateCheCRSpec(instance, "Postgres DB", chePostgresDb); err != nil { + return err + } + } + chePostgresHostName := util.GetValue(instance.Spec.Database.ChePostgresDBHostname, deploy.DefaultChePostgresHostName) + if len(instance.Spec.Database.ChePostgresDBHostname) < 1 { + instance.Spec.Database.ChePostgresDBHostname = chePostgresHostName + if err := r.UpdateCheCRSpec(instance, "Postgres hostname", chePostgresHostName); err != nil { + return err + } + } + chePostgresPort := util.GetValue(instance.Spec.Database.ChePostgresPort, deploy.DefaultChePostgresPort) + if len(instance.Spec.Database.ChePostgresPort) < 1 { + instance.Spec.Database.ChePostgresPort = chePostgresPort + if err := r.UpdateCheCRSpec(instance, "Postgres port", chePostgresPort); err != nil { + return err + } + } + cheFlavor := util.GetValue(instance.Spec.Server.CheFlavor, deploy.DefaultCheFlavor) + if len(instance.Spec.Server.CheFlavor) < 1 { + instance.Spec.Server.CheFlavor = cheFlavor + if err := r.UpdateCheCRSpec(instance, "installation flavor", cheFlavor); err != nil { + return err + } + } + defaultPostgresImage := deploy.DefaultPostgresUpstreamImage + if cheFlavor == "codeready" { + defaultPostgresImage = deploy.DefaultPostgresImage + + } + postgresImage := util.GetValue(instance.Spec.Database.PostgresImage, defaultPostgresImage) + if len(instance.Spec.Database.PostgresImage) < 1 { + instance.Spec.Database.PostgresImage = postgresImage + if err := r.UpdateCheCRSpec(instance, "DB image:tag", postgresImage); err != nil { + return err + } + } + + defaultKeycloakImage := deploy.DefaultKeycloakUpstreamImage + if cheFlavor == "codeready" { + defaultKeycloakImage = deploy.DefaultKeycloakImage + + } + + keycloakImage := util.GetValue(instance.Spec.Auth.KeycloakImage, defaultKeycloakImage) + if len(instance.Spec.Auth.KeycloakImage) < 1 { + instance.Spec.Auth.KeycloakImage = keycloakImage + if err := r.UpdateCheCRSpec(instance, "Keycloak image:tag", keycloakImage); err != nil { + return err + } + } + keycloakRealm := util.GetValue(instance.Spec.Auth.KeycloakRealm, cheFlavor) + if len(instance.Spec.Auth.KeycloakRealm) < 1 { + instance.Spec.Auth.KeycloakRealm = keycloakRealm + if err := r.UpdateCheCRSpec(instance, "Keycloak realm", keycloakRealm); err != nil { + return err + } + } + keycloakClientId := util.GetValue(instance.Spec.Auth.KeycloakClientId, cheFlavor+"-public") + if len(instance.Spec.Auth.KeycloakClientId) < 1 { + instance.Spec.Auth.KeycloakClientId = keycloakClientId + if err := r.UpdateCheCRSpec(instance, "Keycloak client ID", keycloakClientId); err != nil { + return err + } + } + pluginRegistryUrl := util.GetValue(instance.Spec.Server.PluginRegistryUrl, deploy.DefaultPluginRegistryUrl) + if len(instance.Spec.Server.PluginRegistryUrl) < 1 { + instance.Spec.Server.PluginRegistryUrl = pluginRegistryUrl + if err := r.UpdateCheCRSpec(instance, "plugin registry URL", pluginRegistryUrl); err != nil { + return err + } + } + cheLogLevel := util.GetValue(instance.Spec.Server.CheLogLevel, deploy.DefaultCheLogLevel) + if len(instance.Spec.Server.CheLogLevel) < 1 { + instance.Spec.Server.CheLogLevel = cheLogLevel + if err := r.UpdateCheCRSpec(instance, "log level", cheLogLevel); err != nil { + return err + } + } + cheDebug := util.GetValue(instance.Spec.Server.CheDebug, deploy.DefaultCheDebug) + if len(instance.Spec.Server.CheDebug) < 1 { + instance.Spec.Server.CheDebug = cheDebug + if err := r.UpdateCheCRSpec(instance, "debug", cheDebug); err != nil { + return err + } + } + pvcStrategy := util.GetValue(instance.Spec.Storage.PvcStrategy, deploy.DefaultPvcStrategy) + if len(instance.Spec.Storage.PvcStrategy) < 1 { + instance.Spec.Storage.PvcStrategy = pvcStrategy + if err := r.UpdateCheCRSpec(instance, "pvc strategy", pvcStrategy); err != nil { + return err + } + } + pvcClaimSize := util.GetValue(instance.Spec.Storage.PvcClaimSize, deploy.DefaultPvcClaimSize) + if len(instance.Spec.Storage.PvcClaimSize) < 1 { + instance.Spec.Storage.PvcClaimSize = pvcClaimSize + if err := r.UpdateCheCRSpec(instance, "pvc claim size", pvcClaimSize); err != nil { + return err + } + } + pvcJobsImage := util.GetValue(instance.Spec.Storage.PvcJobsImage, deploy.DefaultPvcJobsImage) + if len(instance.Spec.Storage.PvcJobsImage) < 1 { + instance.Spec.Storage.PvcJobsImage = pvcJobsImage + if err := r.UpdateCheCRSpec(instance, "pvc jobs image", pvcJobsImage); err != nil { + return err + } + } + return nil +} diff --git a/pkg/controller/che/exec.go b/pkg/controller/che/exec.go new file mode 100644 index 000000000..aeb2c913f --- /dev/null +++ b/pkg/controller/che/exec.go @@ -0,0 +1,111 @@ +// +// 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 che + +import ( + "bytes" + "fmt" + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + "github.com/eclipse/che-operator/pkg/deploy" + "github.com/sirupsen/logrus" + "io" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/remotecommand" + "sigs.k8s.io/controller-runtime/pkg/client/config" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func ExecIntoPod(podName string, provisionCommand string, reason string, ns string) (provisioned bool) { + + command := []string{"/bin/bash", "-c", provisionCommand} + logrus.Infof("Running exec to %s in pod %s", reason, podName) + // print std if operator is run in debug mode (TODO) + _, stderr, err := k8sclient.RunExec(command, podName, ns) + if err != nil { + logrus.Errorf("Error exec'ing into pod: %v: , command: %s", err, command) + logrus.Errorf(stderr) + return false + } + logrus.Info("Exec successfully completed") + return true +} + +func (cl *k8s) RunExec(command []string, podName, namespace string) (string, string, error) { + + req := cl.clientset.CoreV1().RESTClient().Post(). + Resource("pods"). + Name(podName). + Namespace(namespace). + SubResource("exec") + + req.VersionedParams(&corev1.PodExecOptions{ + Command: command, + Stdin: false, + Stdout: true, + Stderr: true, + TTY: false, + }, scheme.ParameterCodec) + + cfg, _ := config.GetConfig() + exec, err := remotecommand.NewSPDYExecutor(cfg, "POST", req.URL()) + if err != nil { + return "", "", fmt.Errorf("error while creating executor: %v", err) + } + + var stdout, stderr bytes.Buffer + var stdin io.Reader + err = exec.Stream(remotecommand.StreamOptions{ + Stdin: stdin, + Stdout: &stdout, + Stderr: &stderr, + Tty: false, + }) + if err != nil { + return stdout.String(), stderr.String(), err + } + + return stdout.String(), stderr.String(), nil +} + +func (r *ReconcileChe) CreateKyecloakResources(instance *orgv1.CheCluster, request reconcile.Request, deploymentName string) (err error) { + cheHost := instance.Spec.Server.CheHost + keycloakProvisionCommand := deploy.GetKeycloakProvisionCommand(instance, cheHost) + podToExec, err := k8sclient.GetDeploymentPod(deploymentName, instance.Namespace) + if err != nil { + logrus.Errorf("Failed to retrieve pod name. Further exec will fail") + } + provisioned := ExecIntoPod(podToExec, keycloakProvisionCommand, "create realm, client and user", instance.Namespace) + if provisioned { + instance, err := r.GetCR(request) + if err != nil { + if errors.IsNotFound(err) { + logrus.Errorf("CR %s not found: %s", instance.Name, err) + return err + } + logrus.Errorf("Error when getting %s CR: %s", instance.Name, err) + return err + } + for { + instance.Status.KeycloakProvisoned = true + if err := r.UpdateCheCRStatus(instance, "status: provisioned with Keycloak", "true"); err != nil { + instance, _ = r.GetCR(request) + } else { + break + } + } + + return nil + } + return err +} diff --git a/pkg/controller/che/get.go b/pkg/controller/che/get.go new file mode 100644 index 000000000..43651d23e --- /dev/null +++ b/pkg/controller/che/get.go @@ -0,0 +1,78 @@ +// +// 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 che + +import ( + "context" + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + "github.com/sirupsen/logrus" + appsv1 "k8s.io/api/apps/v1" + routev1 "github.com/openshift/api/route/v1" + "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/types" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func(r *ReconcileChe) GetEffectiveDeployment(instance *orgv1.CheCluster, name string) (deployment *appsv1.Deployment, err error) { + deployment = &appsv1.Deployment{} + err = r.client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: instance.Namespace}, deployment) + if err != nil { + logrus.Errorf("Failed to get %s deployment: %s", name, err) + return nil, err + } + return deployment, nil +} + + +func(r *ReconcileChe) GetEffectiveIngress(instance *orgv1.CheCluster, name string) (ingress *v1beta1.Ingress) { + ingress = &v1beta1.Ingress{} + err := r.client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: instance.Namespace}, ingress) + if err != nil { + logrus.Errorf("Failed to get %s ingress: %s", name, err) + return nil + } + return ingress +} + + + +func(r *ReconcileChe) GetEffectiveRoute(instance *orgv1.CheCluster, name string) (route *routev1.Route) { + route = &routev1.Route{} + err := r.client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: instance.Namespace}, route) + if err != nil { + logrus.Errorf("Failed to get %s route: %s", name, err) + return nil + } + return route +} + +func (r *ReconcileChe) GetEffectiveConfigMap(instance *orgv1.CheCluster, name string) (configMap *corev1.ConfigMap) { + configMap = &corev1.ConfigMap{} + err := r.client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: instance.Namespace}, configMap) + if err != nil { + logrus.Errorf("Failed to get %s route: %s", name, err) + return nil + } + return configMap + +} + +func (r *ReconcileChe) GetCR(request reconcile.Request) (instance *orgv1.CheCluster, err error) { + instance = &orgv1.CheCluster{} + err = r.client.Get(context.TODO(), request.NamespacedName, instance) + if err != nil { + logrus.Errorf("Failed to get %s CR: %s", instance.Name, err) + return nil, err + } + return instance, nil +} \ No newline at end of file diff --git a/pkg/controller/che/k8s_helpers.go b/pkg/controller/che/k8s_helpers.go new file mode 100644 index 000000000..673af9277 --- /dev/null +++ b/pkg/controller/che/k8s_helpers.go @@ -0,0 +1,263 @@ +// +// 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 che + +import ( + "bytes" + "github.com/eclipse/che-operator/pkg/util" + "github.com/sirupsen/logrus" + "io" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/kubernetes" + "sigs.k8s.io/controller-runtime/pkg/client/config" +) + +type k8s struct { + clientset kubernetes.Interface +} + +func GetK8Client() *k8s { + tests := util.IsTestMode() + if !tests { + cfg, err := config.GetConfig() + if err != nil { + logrus.Errorf(err.Error()) + } + client := k8s{} + client.clientset, err = kubernetes.NewForConfig(cfg) + + if err != nil { + logrus.Errorf(err.Error()) + return nil + } + return &client + } + return nil +} + +// GetPostgresStatus waits for pvc.status.phase to be Bound +func (cl *k8s) GetPostgresStatus(pvc *corev1.PersistentVolumeClaim, ns string) { + var timeout int64 = 10 + listOptions := metav1.ListOptions{ + FieldSelector: fields.OneTermEqualSelector("metadata.name", pvc.Name).String(), + TimeoutSeconds: &timeout, + } + watcher, err := cl.clientset.CoreV1().PersistentVolumeClaims(ns).Watch(listOptions) + if err != nil { + log.Error(err, "An error occurred") + } + ch := watcher.ResultChan() + logrus.Infof("Waiting for PVC %s to be bound. Default timeout: %v seconds", pvc.Name, timeout) + + for event := range ch { + pvc, ok := event.Object.(*corev1.PersistentVolumeClaim) + if !ok { + log.Error(err, "Unexpected type") + } + + // check before watching in case pvc has been already bound + postgresPvc, err := cl.clientset.CoreV1().PersistentVolumeClaims(ns).Get(pvc.Name, metav1.GetOptions{}) + if err != nil { + logrus.Errorf("Failed to get %s pvc: %s", postgresPvc.Name, err) + break + } + if postgresPvc.Status.Phase == "Bound" { + volumeName := postgresPvc.Spec.VolumeName + logrus.Infof("PVC %s successfully bound to volume %s", postgresPvc.Name, volumeName) + break + } + + switch event.Type { + case watch.Error: + watcher.Stop() + case watch.Modified: + if postgresPvc.Status.Phase == "Bound" { + volumeName := postgresPvc.Spec.VolumeName + logrus.Infof("PVC %s successfully bound to volume %s", postgresPvc.Name, volumeName) + watcher.Stop() + } + + } + } + postgresPvc, err := cl.clientset.CoreV1().PersistentVolumeClaims(ns).Get(pvc.Name, metav1.GetOptions{}) + if postgresPvc.Status.Phase != "Bound" { + currentPvcPhase := postgresPvc.Status.Phase + logrus.Warnf("Timeout waiting for a PVC %s to be bound. Current phase is %s", postgresPvc.Name, currentPvcPhase) + logrus.Warn("Sometimes PVC can be bound only when the first consumer is created") + } +} + +func (cl *k8s) GetDeploymentRollingUpdateStatus(name string, ns string) { + api := cl.clientset.AppsV1() + var timeout int64 = 420 + listOptions := metav1.ListOptions{ + FieldSelector: fields.OneTermEqualSelector("metadata.name", name).String(), + TimeoutSeconds: &timeout, + } + watcher, err := api.Deployments(ns).Watch(listOptions) + if err != nil { + log.Error(err, "An error occurred") + } + ch := watcher.ResultChan() + logrus.Infof("Waiting for a successful rolling update of deployment %s. Default timeout: %v seconds", name, timeout) + for event := range ch { + dc, ok := event.Object.(*appsv1.Deployment) + if !ok { + log.Error(err, "Unexpected type") + } + // check before watching in case the deployment is already scaled to 1 + deployment, err := cl.clientset.AppsV1().Deployments(ns).Get(name, metav1.GetOptions{}) + if err != nil { + logrus.Errorf("Failed to get %s deployment: %s", deployment.Name, err) + break + } + if deployment.Status.Replicas == 1 { + logrus.Infof("Rolling update of '%s' deployment finished", deployment.Name) + break + } + switch event.Type { + case watch.Error: + watcher.Stop() + case watch.Modified: + if dc.Status.Replicas == 1 { + logrus.Infof("Rolling update of '%s' deployment finished", deployment.Name) + watcher.Stop() + } + + } + } +} + +// GetDeploymentStatus listens to deployment events and checks replicas once MODIFIED event is received +func (cl *k8s) GetDeploymentStatus(name string, ns string) (scaled bool) { + api := cl.clientset.AppsV1() + var timeout int64 = 420 + listOptions := metav1.ListOptions{ + FieldSelector: fields.OneTermEqualSelector("metadata.name", name).String(), + TimeoutSeconds: &timeout, + } + watcher, err := api.Deployments(ns).Watch(listOptions) + if err != nil { + log.Error(err, "An error occurred") + } + ch := watcher.ResultChan() + logrus.Infof("Waiting for deployment %s. Default timeout: %v seconds", name, timeout) + for event := range ch { + dc, ok := event.Object.(*appsv1.Deployment) + if !ok { + log.Error(err, "Unexpected type") + } + // check before watching in case the deployment is already scaled to 1 + deployment, err := cl.clientset.AppsV1().Deployments(ns).Get(name, metav1.GetOptions{}) + if err != nil { + logrus.Errorf("Failed to get %s deployment: %s", deployment.Name, err) + return false + } + if deployment.Status.AvailableReplicas == 1 { + logrus.Infof("Deployment '%s' successfully scaled to %v", deployment.Name, deployment.Status.AvailableReplicas) + return true + } + switch event.Type { + case watch.Error: + watcher.Stop() + case watch.Modified: + if dc.Status.AvailableReplicas == 1 { + logrus.Infof("Deployment '%s' successfully scaled to %v", deployment.Name, dc.Status.AvailableReplicas) + watcher.Stop() + return true + + } + } + } + dc, _ := cl.clientset.AppsV1().Deployments(ns).Get(name, metav1.GetOptions{}) + if dc.Status.AvailableReplicas != 1 { + logrus.Errorf("Failed to verify a successful %s deployment", name) + eventList := cl.GetEvents(name, ns).Items + for i := range eventList { + logrus.Errorf("Event message: %v", eventList[i].Message) + } + deploymentPod, err := cl.GetDeploymentPod(name, ns) + if err != nil { + return false + } + cl.GetPodLogs(deploymentPod, ns) + logrus.Errorf("Command to get deployment logs: kubectl logs deployment/%s -n=%s", name, ns) + logrus.Errorf("Get k8s events: kubectl get events "+ + "--field-selector "+ + "involvedObject.name=$(kubectl get pods -l=component=%s -n=%s"+ + " -o=jsonpath='{.items[0].metadata.name}') -n=%s", name, ns, ns) + return false + } + return true +} + +// GetEvents returns a list of events filtered by involvedObject +func (cl *k8s) GetEvents(deploymentName string, ns string) (list *corev1.EventList) { + eventListOptions := metav1.ListOptions{FieldSelector: fields.OneTermEqualSelector("involvedObject.fieldPath", "spec.containers{"+deploymentName+"}").String()} + deploymentEvents, _ := cl.clientset.CoreV1().Events(ns).List(eventListOptions) + return deploymentEvents +} + +// GetLogs prints stderr or stdout from a selected pod. Log size is capped at 60000 bytes +func (cl *k8s) GetPodLogs(podName string, ns string) () { + var limitBytes int64 = 60000 + req := cl.clientset.CoreV1().Pods(ns).GetLogs(podName, &corev1.PodLogOptions{LimitBytes: &limitBytes}, + ) + readCloser, err := req.Stream() + if err != nil { + logrus.Errorf("Pod error log: %v", err) + } else { + buf := new(bytes.Buffer) + _, err = io.Copy(buf, readCloser) + logrus.Infof("Pod log: %v", buf.String()) + } +} + +//GetDeploymentPod queries all pods is a selected namespace by LabelSelector +func (cl *k8s) GetDeploymentPod(name string, ns string) (podName string, err error) { + api := cl.clientset.CoreV1() + listOptions := metav1.ListOptions{ + LabelSelector: "component=" + name, + } + podList, _ := api.Pods(ns).List(listOptions) + podListItems := podList.Items + if len(podListItems) == 0 { + logrus.Errorf("Failed to find pod to exec into. List of pods: %s", podListItems) + return "", err + } + // expecting only one pod to be there so, taking the first one + // todo maybe add a unique label to deployments? + podName = podListItems[0].Name + return podName, nil +} + +// GetDefaultRouterCert retrieves secret with OpenShift router certificate and extracts it +// The cert is then used to create self-signed-certificate secret consumed by CheCluster server and workspaces +func (cl *k8s) GetDefaultRouterCert(ns string) (crt []byte, err error) { + options := metav1.GetOptions{} + secret, err := cl.clientset.CoreV1().Secrets(ns).Get("router-certs-default", options) + if err != nil { + // in 3.11 it's default namespace and router-certs secret + secret, err = cl.clientset.CoreV1().Secrets("default").Get("router-certs", options) + if err != nil { + logrus.Errorf("Failed to get a secret in both namespace %s and default: %s", ns, err) + return nil, err + } + } + secretData := secret.Data + crt = secretData["tls.crt"] + return crt, nil +} diff --git a/pkg/controller/che/k8s_helpers_test.go b/pkg/controller/che/k8s_helpers_test.go new file mode 100644 index 000000000..a726e7976 --- /dev/null +++ b/pkg/controller/che/k8s_helpers_test.go @@ -0,0 +1,104 @@ +// +// 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 che + +import ( + "github.com/sirupsen/logrus" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + "testing" +) + +var ( + fakeK8s = fakeClientSet() + namespace = "eclipse-che" +) + +func fakeClientSet() *k8s { + client := k8s{} + client.clientset = fake.NewSimpleClientset() + return &client +} + +func TestGetDeploymentPod(t *testing.T) { + + // create a fake pod + _, err := fakeK8s.clientset.CoreV1().Pods(namespace).Create(&corev1.Pod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-pod", + Namespace: namespace, + Labels: map[string]string{ + "component": "postgres", + }, + }, + }) + if err != nil { + panic(err) + } + pod, err := fakeK8s.GetDeploymentPod("postgres", namespace) + if err != nil { + t.Errorf("Failed to det deployment pod: %s", err) + } + if len(pod) == 0 { + t.Errorf("Test failed. No pods found by label") + } + logrus.Infof("Test passed. Pod %s found", pod) +} + + +func TestGetEvents(t *testing.T) { + + // fire up an event with fake-pod as involvedObject + message := "This is a fake event about a fake pod" + _, err := fakeK8s.clientset.CoreV1().Events(namespace).Create(&corev1.Event{ + TypeMeta: metav1.TypeMeta{ + Kind: "Event", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-event", + Namespace: "eclipse-che", + }, + InvolvedObject: corev1.ObjectReference{ + FieldPath: "spec.containers{fake-pod}", + Kind: "Pod", + }, + Message: message, + Reason: "Testing event filtering", + Type: "Normal", + }) + + if err != nil { + panic(err) + } + + events := fakeK8s.GetEvents("fake-pod", namespace) + fakePodEvents := events.Items + if len(fakePodEvents) == 0 { + logrus.Fatal("Test failed No events found") + } else { + logrus.Infof("Test passed. Found %v event", len(fakePodEvents)) + } + // test if event message matches + fakePodEventMessage := events.Items[0].Message + if len(fakePodEventMessage) != len(message) { + t.Errorf("Test failed. Message to be received: %s, but got %s ", message, fakePodEventMessage) + } else { + logrus.Infof("Test passed. Expected event message: %s. Received event message %s", message, fakePodEventMessage) + } +} + diff --git a/pkg/controller/che/status.go b/pkg/controller/che/status.go new file mode 100644 index 000000000..caa65eaa8 --- /dev/null +++ b/pkg/controller/che/status.go @@ -0,0 +1,72 @@ +// +// 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 che + +import ( + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + "github.com/eclipse/che-operator/pkg/deploy" + "github.com/eclipse/che-operator/pkg/util" + "github.com/sirupsen/logrus" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +const ( + AvailableStatus = "Available" + UnavailableStatus = "Unavailable" + RollingUpdateInProgressStatus = "Available: Rolling update in progress" +) + +func (r *ReconcileChe) SetCheAvailableStatus(instance *orgv1.CheCluster, request reconcile.Request, protocol string, cheHost string) (err error) { + cheFlavor := util.GetValue(instance.Spec.Server.CheFlavor, deploy.DefaultCheFlavor) + name := "Eclipse Che" + if cheFlavor == "codeready" { + name = "CodeReady Workspaces" + } + keycloakURL := instance.Spec.Auth.KeycloakURL + instance.Status.KeycloakURL = keycloakURL + if err := r.UpdateCheCRStatus(instance, "Keycloak URL status", keycloakURL); err != nil { + instance, _ = r.GetCR(request) + return err + } + instance.Status.CheClusterRunning = AvailableStatus + if err := r.UpdateCheCRStatus(instance, "status: "+name+" server", AvailableStatus); err != nil { + instance, _ = r.GetCR(request) + return err + } + instance.Status.CheURL = protocol + "://" + cheHost + if err := r.UpdateCheCRStatus(instance, name+" server URL", protocol+"://"+cheHost); err != nil { + instance, _ = r.GetCR(request) + return err + } + logrus.Infof(name+" is now available at: %s://%s", protocol, cheHost) + return nil + +} + +func (r *ReconcileChe) SetCheUnavailableStatus(instance *orgv1.CheCluster, request reconcile.Request) (err error) { + instance.Status.CheClusterRunning = UnavailableStatus + if err:= r.UpdateCheCRStatus(instance, "status: Che API", UnavailableStatus); err != nil { + instance, _ = r.GetCR(request) + return err + } + return nil +} + +func (r *ReconcileChe) SetCheRollingUpdateStatus(instance *orgv1.CheCluster, request reconcile.Request) (err error){ + + instance.Status.CheClusterRunning = RollingUpdateInProgressStatus + if err:= r.UpdateCheCRStatus(instance, "status", RollingUpdateInProgressStatus); err != nil { + instance, _ = r.GetCR(request) + return err + } + return nil +} diff --git a/pkg/controller/che/update.go b/pkg/controller/che/update.go new file mode 100644 index 000000000..bb74b60a2 --- /dev/null +++ b/pkg/controller/che/update.go @@ -0,0 +1,219 @@ +// +// 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 che + +import ( + "context" + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + "github.com/eclipse/che-operator/pkg/deploy" + "github.com/eclipse/che-operator/pkg/util" + oauth "github.com/openshift/api/oauth/v1" + routev1 "github.com/openshift/api/route/v1" + "github.com/sirupsen/logrus" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "reflect" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func (r *ReconcileChe) UpdateCheCRStatus(instance *orgv1.CheCluster, updatedField string, value string) (err error) { + logrus.Infof("Updating %s CR with %s: %s", instance.Name, updatedField, value) + err = r.client.Status().Update(context.TODO(), instance) + if err != nil { + logrus.Warnf("Failed to update %s CR. Fetching the latest CR version: %s", instance.Name, err) + return err + } + logrus.Infof("Custom resource %s updated", instance.Name) + return nil +} + +func (r *ReconcileChe) UpdateCheCRSpec1(instance *orgv1.CheCluster, updatedField string, value string) (err error) { + logrus.Infof("Updating %s CR with %s: %s", instance.Name, updatedField, value) + if err = r.client.Update(context.TODO(), instance); err != nil { + return err + } + logrus.Infof("Custom resource %s updated", instance.Name) + return nil +} + +func (r *ReconcileChe) UpdateCheCRSpec(instance *orgv1.CheCluster, updatedField string, value string) (err error) { + logrus.Infof("Updating %s CR with %s: %s", instance.Name, updatedField, value) + err = r.client.Update(context.TODO(), instance) + if err != nil { + logrus.Warnf("Failed to update %s CR: %s", instance.Name, err) + return err + } + logrus.Infof("Custom resource %s updated", instance.Name) + return nil +} + +// UpdateConfigMap compares existing ConfigMap retrieved from API with a current ConfigMap +// i.e. ConfigMap.Data consuming current CheCluster.Spec fields, and updates an existing +// ConfigMap with up-to-date .Data. +func (r *ReconcileChe) UpdateConfigMap(instance *orgv1.CheCluster) (updated bool, err error) { + + activeConfigMap := &corev1.ConfigMap{} + if err := r.client.Get(context.TODO(), types.NamespacedName{Name: "che", Namespace: instance.Namespace}, activeConfigMap); err != nil { + logrus.Errorf("ConfigMap %s not found: %s", activeConfigMap.Name, err) + } + // compare ConfigMap.Data with current CM on server + cheEnv := deploy.GetConfigMapData(instance) + cm := deploy.NewCheConfigMap(instance, cheEnv) + equal := reflect.DeepEqual(cm.Data, activeConfigMap.Data) + if !equal { + + logrus.Infof("Updating %s ConfigMap", activeConfigMap.Name) + if err := controllerutil.SetControllerReference(instance, cm, r.scheme); err != nil { + logrus.Errorf("Failed to set OwnersReference for %s %s: %s", activeConfigMap.Kind, activeConfigMap.Name, err) + return false, err + } + if err := r.client.Update(context.TODO(), cm); err != nil { + logrus.Errorf("Failed to update %s %s: %s", activeConfigMap.Kind, activeConfigMap.Name, err) + return false, err + } + return true, nil + } + return false, nil +} + +func (r *ReconcileChe) ReconcileTLSObjects(instance *orgv1.CheCluster, request reconcile.Request, cheFlavor string, tlsSupport bool, isOpenShift bool) (updated bool, err error) { + + // reconcile ingresses + if !isOpenShift { + ingressDomain := instance.Spec.K8SOnly.IngressDomain + ingressStrategy := util.GetValue(instance.Spec.K8SOnly.IngressStrategy, deploy.DefaultIngressStrategy) + currentCheIngress := r.GetEffectiveIngress(instance, cheFlavor) + if currentCheIngress == nil { + return false, err + } + protocol := "http" + logrus.Infof("Deleting ingress %s", currentCheIngress.Name) + if err := r.client.Delete(context.TODO(), currentCheIngress); err != nil { + logrus.Errorf("Failed to delete %s ingress: %s", currentCheIngress.Name, err) + return false, err + } + cheIngress := deploy.NewIngress(instance, cheFlavor, "che-host", 8080) + + if err := r.CreateNewIngress(instance, cheIngress); err != nil { + logrus.Errorf("Failed to create %s %s: %s", cheIngress.Name, cheIngress.Kind, err) + return false, err + } + currentKeycloakIngress := r.GetEffectiveIngress(instance, "keycloak") + if currentKeycloakIngress == nil { + return false, err + } else { + keycloakURL := protocol + "://" + ingressDomain + if ingressStrategy == "multi-host" { + keycloakURL = protocol + "://keycloak-" + instance.Namespace + "." + ingressDomain + } + instance.Spec.Auth.KeycloakURL = keycloakURL + if err := r.UpdateCheCRSpec(instance, "Keycloak URL", keycloakURL); err != nil { + return false, err + } + } + logrus.Infof("Deleting route %s", currentKeycloakIngress.Name) + if err := r.client.Delete(context.TODO(), currentKeycloakIngress); err != nil { + logrus.Errorf("Failed to delete %s ingress: %s", currentKeycloakIngress.Name, err) + return false, err + } + keycloakIngress := deploy.NewIngress(instance, "keycloak", "keycloak", 8080) + + if err := r.CreateNewIngress(instance, keycloakIngress); err != nil { + logrus.Errorf("Failed to create Keycloak ingress: %s", err) + return false, err + } + return true, nil + + } + protocol := "http" + currentCheRoute := r.GetEffectiveRoute(instance, cheFlavor) + if currentCheRoute == nil { + return false, err + + } + logrus.Infof("Deleting route %s", currentCheRoute.Name) + if err := r.client.Delete(context.TODO(), currentCheRoute); err != nil { + logrus.Errorf("Failed to delete %s route: %s", currentCheRoute.Name, err) + return false, err + } + cheRoute := deploy.NewRoute(instance, cheFlavor, "che-host") + + if tlsSupport { + cheRoute = deploy.NewTlsRoute(instance, cheFlavor, "che-host") + protocol = "https" + } + + if err := r.CreateNewRoute(instance, cheRoute); err != nil { + logrus.Errorf("Failed to create %s %s: %s", cheRoute.Name, cheRoute.Kind, err) + return false, err + } + + currentKeycloakRoute := &routev1.Route{} + if err := r.client.Get(context.TODO(), types.NamespacedName{Name: "keycloak", Namespace: instance.Namespace}, currentKeycloakRoute); err != nil { + logrus.Errorf("Failed to get %s route: %s", currentKeycloakRoute.Name, err) + return false, err + + } else { + keycloakURL := currentKeycloakRoute.Spec.Host + instance.Spec.Auth.KeycloakURL = protocol + "://" + keycloakURL + if err := r.UpdateCheCRSpec(instance, "Keycloak URL", protocol+"://"+keycloakURL); err != nil { + return false, err + } + } + + logrus.Infof("Deleting route %s", currentKeycloakRoute.Name) + if err := r.client.Delete(context.TODO(), currentKeycloakRoute); err != nil { + logrus.Errorf("Failed to delete %s route: %s", currentKeycloakRoute.Name, err) + return false, err + } + keycloakRoute := deploy.NewRoute(instance, "keycloak", "keycloak") + + if tlsSupport { + keycloakRoute = deploy.NewTlsRoute(instance, "keycloak", "keycloak") + } + if err := r.CreateNewRoute(instance, keycloakRoute); err != nil { + logrus.Errorf("Failed to create Keycloak route: %s", err) + return false, err + } + return true, nil +} + +func (r *ReconcileChe) ReconcileIdentityProvider(instance *orgv1.CheCluster) (deleted bool, err error) { + if instance.Spec.Auth.OpenShiftOauth == false && instance.Status.OpenShiftoAuthProvisioned == true { + keycloakAdminPassword := instance.Spec.Auth.KeycloakAdminPassword + keycloakDeployment := &appsv1.Deployment{} + 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 := deploy.GetDeleteOpenShiftIdentityProviderProvisionCommand(instance, keycloakAdminPassword) + podToExec, err := k8sclient.GetDeploymentPod(keycloakDeployment.Name, instance.Namespace) + if err != nil { + logrus.Errorf("Failed to retrieve pod name. Further exec will fail") + } + provisioned := ExecIntoPod(podToExec, deleteOpenShiftIdentityProviderProvisionCommand, "delete OpenShift identity provider", instance.Namespace) + if provisioned { + oAuthClient := &oauth.OAuthClient{} + oAuthClientName := instance.Spec.Auth.OauthClientName + if err := r.client.Get(context.TODO(), types.NamespacedName{Name: oAuthClientName, Namespace: ""}, oAuthClient); err != nil { + logrus.Errorf("%s %s not found: %s", oAuthClient.Name, err) + } + if err := r.client.Delete(context.TODO(), oAuthClient); err != nil { + logrus.Errorf("Failed to delete %s %s: %s", oAuthClient.Kind, oAuthClient.Name, err) + } + return true, nil + } + return false, err + } + return false, nil +} diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go new file mode 100644 index 000000000..7ea70d01e --- /dev/null +++ b/pkg/controller/controller.go @@ -0,0 +1,29 @@ +// +// 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 controller + +import ( + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +// AddToManagerFuncs is a list of functions to add all Controllers to the Manager +var AddToManagerFuncs []func(manager.Manager) error + +// AddToManager adds all Controllers to the Manager +func AddToManager(m manager.Manager) error { + for _, f := range AddToManagerFuncs { + if err := f(m); err != nil { + return err + } + } + return nil +} diff --git a/pkg/deploy/che_configmap.go b/pkg/deploy/che_configmap.go new file mode 100644 index 000000000..5198b6392 --- /dev/null +++ b/pkg/deploy/che_configmap.go @@ -0,0 +1,216 @@ +// +// 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 deploy + +import ( + "encoding/json" + "fmt" + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + "github.com/eclipse/che-operator/pkg/util" + "github.com/sirupsen/logrus" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func addMap(a map[string]string, b map[string]string) { + for k, v := range b { + a[k] = v + } +} + +type CheConfigMap struct { + CheHost string `json:"CHE_HOST"` + CheMultiUser string `json:"CHE_MULTIUSER"` + ChePort string `json:"CHE_PORT"` + CheApi string `json:"CHE_API"` + CheWebSocketEndpoint string `json:"CHE_WEBSOCKET_ENDPOINT"` + CheDebugServer string `json:"CHE_DEBUG_SERVER"` + CheInfrastructureActive string `json:"CHE_INFRASTRUCTURE_ACTIVE"` + BootstrapperBinaryUrl string `json:"CHE_INFRA_KUBERNETES_BOOTSTRAPPER_BINARY__URL"` + WorkspacesNamespace string `json:"CHE_INFRA_OPENSHIFT_PROJECT"` + PvcStrategy string `json:"CHE_INFRA_KUBERNETES_PVC_STRATEGY"` + PvcClaimSize string `json:"CHE_INFRA_KUBERNETES_PVC_QUANTITY"` + PvcJobsImage string `json:"CHE_INFRA_KUBERNETES_PVC_JOBS_IMAGE"` + PreCreateSubPaths string `json:"CHE_INFRA_KUBERNETES_PVC_PRECREATE__SUBPATHS"` + TlsSupport string `json:"CHE_INFRA_OPENSHIFT_TLS__ENABLED"` + K8STrustCerts string `json:"CHE_INFRA_KUBERNETES_TRUST__CERTS"` + DatabaseURL string `json:"CHE_JDBC_URL"` + DbUserName string `json:"CHE_JDBC_USERNAME"` + DbPassword string `json:"CHE_JDBC_PASSWORD"` + CheLogLevel string `json:"CHE_LOG_LEVEL"` + KeycloakURL string `json:"CHE_KEYCLOAK_AUTH__SERVER__URL"` + KeycloakRealm string `json:"CHE_KEYCLOAK_REALM"` + KeycloakClientId string `json:"CHE_KEYCLOAK_CLIENT__ID"` + OpenShiftIdentityProvider string `json:"CHE_INFRA_OPENSHIFT_OAUTH__IDENTITY__PROVIDER"` + ReloadStacksOnStart string `json:"CHE_PREDEFINED_STACKS_RELOAD__ON__START"` + WorkspaceServiceAccountName string `json:"CHE_INFRA_KUBERNETES_SERVICE__ACCOUNT__NAME"` + WorkspaceAutoStart string `json:"CHE_WORKSPACE_AUTO_START"` + UnrecoverableEvents string `json:"CHE_INFRA_KUBERNETES_WORKSPACE__UNRECOVERABLE__EVENTS"` + InactiveWorkspaceStopTimeout string `json:"CHE_WORKSPACE_AGENT_DEV_INACTIVE__STOP__TIMEOUT__MS"` + JavaOpts string `json:"JAVA_OPTS"` + WorkspaceJavaOpts string `json:"CHE_WORKSPACE_JAVA__OPTIONS"` + WorkspaceMavenOpts string `json:"CHE_WORKSPACE_MAVEN__OPTIONS"` + WorkspaceProxyJavaOpts string `json:"CHE_WORKSPACE_HTTP__PROXY__JAVA__OPTIONS"` + WorkspaceHttpProxy string `json:"CHE_WORKSPACE_HTTP__PROXY"` + WorkspaceHttpsProxy string `json:"CHE_WORKSPACE_HTTPS__PROXY"` + WorkspaceNoProxy string `json:"CHE_WORKSPACE_NO__PROXY"` + PluginRegistryUrl string `json:"CHE_WORKSPACE_PLUGIN__REGISTRY__URL"` + WebSocketEndpointMinor string `json:"CHE_WEBSOCKET_ENDPOINT__MINOR"` +} + +// GetConfigMapData gets env values from CR spec and returns a map with key:value +// which is used in CheCluster ConfigMap to configure CheCluster master behavior +func GetConfigMapData(cr *orgv1.CheCluster) (cheEnv map[string]string) { + cheHost := cr.Spec.Server.CheHost + keycloakURL := cr.Spec.Auth.KeycloakURL + isOpenShift, err := util.DetectOpenShift() + if err != nil { + logrus.Errorf("Failed to get current infra: %s", err) + } + cheFlavor := util.GetValue(cr.Spec.Server.CheFlavor, DefaultCheFlavor) + chePostgresPassword := cr.Spec.Database.ChePostgresPassword + infra := "kubernetes" + if isOpenShift { + infra = "openshift" + } + workspacesNamespace := cr.Namespace + tls := "false" + openShiftIdentityProviderId := "NULL" + openshiftOAuth := cr.Spec.Auth.OpenShiftOauth + if openshiftOAuth && isOpenShift { + workspacesNamespace = "" + openShiftIdentityProviderId = "openshift-v3" + } + tlsSupport := cr.Spec.Server.TlsSupport + protocol := "http" + wsprotocol := "ws" + if tlsSupport { + protocol = "https" + wsprotocol = "wss" + tls = "true" + } + proxyJavaOpts := "" + proxyUser := cr.Spec.Server.ProxyUser + proxyPassword := cr.Spec.Server.ProxyPassword + if len(cr.Spec.Server.ProxyURL) > 1 { + proxyJavaOpts = util.GenerateProxyJavaOpts(cr.Spec.Server.ProxyURL, cr.Spec.Server.ProxyPort, cr.Spec.Server.NonProxyHosts, proxyUser, proxyPassword) + } + cheWorkspaceHttpProxy := "" + cheWorkspaceNoProxy := "" + if len(cr.Spec.Server.ProxyURL) > 1 { + cheWorkspaceHttpProxy, cheWorkspaceNoProxy = util.GenerateProxyEnvs(cr.Spec.Server.ProxyURL, cr.Spec.Server.ProxyPort, cr.Spec.Server.NonProxyHosts, proxyUser, proxyPassword) + } + + ingressDomain := cr.Spec.K8SOnly.IngressDomain + tlsSecretName := cr.Spec.K8SOnly.TlsSecretName + pvcStrategy := util.GetValue(cr.Spec.Storage.PvcStrategy, DefaultPvcStrategy) + pvcClaimSize := util.GetValue(cr.Spec.Storage.PvcClaimSize, DefaultPvcClaimSize) + pvcJobsImage := util.GetValue(cr.Spec.Storage.PvcJobsImage, DefaultPvcJobsImage) + preCreateSubPaths := "true" + if !cr.Spec.Storage.PreCreateSubPaths { + preCreateSubPaths = "false" + } + chePostgresHostName := util.GetValue(cr.Spec.Database.ChePostgresDBHostname, DefaultChePostgresHostName) + chePostgresUser := util.GetValue(cr.Spec.Database.ChePostgresUser, DefaultChePostgresUser) + chePostgresPort := util.GetValue(cr.Spec.Database.ChePostgresPort, DefaultChePostgresPort) + chePostgresDb := util.GetValue(cr.Spec.Database.ChePostgresDb, DefaultChePostgresDb) + keycloakRealm := util.GetValue(cr.Spec.Auth.KeycloakRealm, cheFlavor) + keycloakClientId := util.GetValue(cr.Spec.Auth.KeycloakClientId, cheFlavor+"-public") + ingressStrategy := util.GetValue(cr.Spec.K8SOnly.IngressStrategy, DefaultIngressStrategy) + ingressClass := util.GetValue(cr.Spec.K8SOnly.IngressClass, DefaultIngressClass) + pluginRegistryUrl := util.GetValue(cr.Spec.Server.PluginRegistryUrl, DefaultPluginRegistryUrl) + cheLogLevel := util.GetValue(cr.Spec.Server.CheLogLevel, DefaultCheLogLevel) + cheDebug := util.GetValue(cr.Spec.Server.CheDebug, DefaultCheDebug) + + data := &CheConfigMap{ + CheMultiUser: "true", + CheHost: cheHost, + ChePort: "8080", + CheApi: protocol + "://" + cheHost + "/api", + CheWebSocketEndpoint: wsprotocol + "://" + cheHost + "/api/websocket", + WebSocketEndpointMinor: wsprotocol + "://" + cheHost + "/api/websocket-minor", + CheDebugServer: cheDebug, + CheInfrastructureActive: infra, + BootstrapperBinaryUrl: protocol + "://" + cheHost + "/agent-binaries/linux_amd64/bootstrapper/bootstrapper", + WorkspacesNamespace: workspacesNamespace, + PvcStrategy: pvcStrategy, + PvcClaimSize: pvcClaimSize, + PvcJobsImage: pvcJobsImage, + PreCreateSubPaths: preCreateSubPaths, + TlsSupport: tls, + K8STrustCerts: tls, + DatabaseURL: "jdbc:postgresql://" + chePostgresHostName + ":" + chePostgresPort + "/" + chePostgresDb, + DbUserName: chePostgresUser, + DbPassword: chePostgresPassword, + CheLogLevel: cheLogLevel, + KeycloakURL: keycloakURL + "/auth", + KeycloakRealm: keycloakRealm, + KeycloakClientId: keycloakClientId, + OpenShiftIdentityProvider: openShiftIdentityProviderId, + ReloadStacksOnStart: "true", + WorkspaceServiceAccountName: "che-workspace", + WorkspaceAutoStart: "true", + UnrecoverableEvents: "FailedMount,FailedScheduling,MountVolume.SetUp failed,Failed to pull image", + InactiveWorkspaceStopTimeout: "-1", + JavaOpts: "-XX:MaxRAMFraction=2 -XX:+UseParallelGC -XX:MinHeapFreeRatio=10 " + + "-XX:MaxHeapFreeRatio=20 -XX:GCTimeRatio=4 " + + "-XX:AdaptiveSizePolicyWeight=90 -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap " + + "-Dsun.zip.disableMemoryMapping=true -Xms20m " + proxyJavaOpts, + WorkspaceJavaOpts: "-XX:MaxRAM=150m -XX:MaxRAMFraction=2 -XX:+UseParallelGC " + + "-XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=20 -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 " + + "-Dsun.zip.disableMemoryMapping=true " + + "-Xms20m -Djava.security.egd=file:/dev/./urandom " + proxyJavaOpts, + WorkspaceProxyJavaOpts: proxyJavaOpts, + WorkspaceHttpProxy: cheWorkspaceHttpProxy, + WorkspaceHttpsProxy: cheWorkspaceHttpProxy, + WorkspaceNoProxy: cheWorkspaceNoProxy, + PluginRegistryUrl: pluginRegistryUrl, + } + + out, err := json.Marshal(data) + if err != nil { + fmt.Println(err) + + } + err = json.Unmarshal(out, &cheEnv) + + // k8s specific envs + k8sCheEnv := map[string]string{ + "CHE_INFRA_KUBERNETES_POD_SECURITY__CONTEXT_FS__GROUP": "0", + "CHE_INFRA_KUBERNETES_POD_SECURITY__CONTEXT_RUN__AS__USER": "0", + "CHE_INFRA_KUBERNETES_NAMESPACE": workspacesNamespace, + "CHE_INFRA_KUBERNETES_INGRESS_DOMAIN": ingressDomain, + "CHE_INFRA_KUBERNETES_SERVER__STRATEGY": ingressStrategy, + "CHE_INFRA_KUBERNETES_TLS__SECRET": tlsSecretName, + "CHE_INFRA_KUBERNETES_INGRESS_ANNOTATIONS__JSON": "{\"kubernetes.io/ingress.class\": " + ingressClass + ", \"nginx.ingress.kubernetes.io/rewrite-target\": \"/\",\"nginx.ingress.kubernetes.io/ssl-redirect\": " + tls + ",\"nginx.ingress.kubernetes.io/proxy-connect-timeout\": \"3600\",\"nginx.ingress.kubernetes.io/proxy-read-timeout\": \"3600\"}", + } + if !isOpenShift { + addMap(cheEnv, k8sCheEnv) + } + return cheEnv +} + +func NewCheConfigMap(cr *orgv1.CheCluster, cheEnv map[string]string) *corev1.ConfigMap { + labels := GetLabels(cr, util.GetValue(cr.Spec.Server.CheFlavor, DefaultCheFlavor)) + return &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "che", + Namespace: cr.Namespace, + Labels: labels, + }, + Data: cheEnv, + } +} diff --git a/pkg/deploy/che_configmap_test.go b/pkg/deploy/che_configmap_test.go new file mode 100644 index 000000000..05ce4561e --- /dev/null +++ b/pkg/deploy/che_configmap_test.go @@ -0,0 +1,39 @@ +// +// 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 deploy + +import ( + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + "strings" + "testing" +) + +func TestNewCheConfigMap(t *testing.T) { + + // since all values are retrieved from CR or auto-generated + // some of them are explicitly set for this test to avoid using fake kube + // and creating a CR with all spec fields pre-populated + cr := &orgv1.CheCluster{} + cr.Spec.Server.CheHost = "myhostname.com" + cr.Spec.Server.TlsSupport = true + cr.Spec.Auth.OpenShiftOauth = true + cheEnv := GetConfigMapData(cr) + testCm := NewCheConfigMap(cr, cheEnv) + identityProvider := testCm.Data["CHE_INFRA_OPENSHIFT_OAUTH__IDENTITY__PROVIDER"] + protocol := strings.Split(testCm.Data["CHE_INFRA_KUBERNETES_BOOTSTRAPPER_BINARY__URL"], "://")[0] + if identityProvider != "openshift-v3" { + t.Errorf("Test failed. Expecting identity provider to be 'openshift-v3' while got '%s'", identityProvider) + } + if protocol != "https" { + t.Errorf("Test failed. Expecting 'https' protocol, got '%s'", protocol) + } +} diff --git a/pkg/deploy/defaults.go b/pkg/deploy/defaults.go new file mode 100644 index 000000000..87bfba0a3 --- /dev/null +++ b/pkg/deploy/defaults.go @@ -0,0 +1,37 @@ +// +// 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 deploy + +const ( + DefaultCheServerImageRepo = "eclipse/che-server" + DefaultCodeReadyServerImageRepo = "registry.access.redhat.com/codeready-workspaces/server" + DefaultCheServerImageTag = "6.19.0" + DefaultCodeReadyServerImageTag = "1.0" + DefaultCheFlavor = "che" + DefaultChePostgresUser = "pgche" + DefaultChePostgresHostName = "postgres" + DefaultChePostgresPort = "5432" + DefaultChePostgresDb = "dbche" + DefaultPvcStrategy = "common" + DefaultPvcClaimSize = "1Gi" + DefaultIngressStrategy = "multi-host" + DefaultIngressClass = "nginx" + DefaultPluginRegistryUrl = "https://che-plugin-registry.openshift.io" + DefaultKeycloakAdminUserName = "admin" + DefaultCheLogLevel = "INFO" + DefaultCheDebug = "false" + DefaultPvcJobsImage = "registry.access.redhat.com/rhel7-minimal:7.6-154" + DefaultPostgresImage = "registry.access.redhat.com/rhscl/postgresql-96-rhel7:1-25" + DefaultPostgresUpstreamImage = "centos/postgresql-96-centos7:9.6" + DefaultKeycloakImage = "registry.access.redhat.com/redhat-sso-7/sso72-openshift:1.2-8" + DefaultKeycloakUpstreamImage = "eclipse/che-keycloak:6.19.0" +) diff --git a/pkg/deploy/deployment_che.go b/pkg/deploy/deployment_che.go new file mode 100644 index 000000000..cea9bcd0a --- /dev/null +++ b/pkg/deploy/deployment_che.go @@ -0,0 +1,151 @@ +// +// 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 deploy + +import ( + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + "github.com/eclipse/che-operator/pkg/util" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func NewCheDeployment(cr *orgv1.CheCluster, cheImage string, cheTag string, cmRevision string) *appsv1.Deployment { + labels := GetLabels(cr, util.GetValue(cr.Spec.Server.CheFlavor, DefaultCheFlavor)) + optionalEnv := true + cheFlavor := util.GetValue(cr.Spec.Server.CheFlavor, DefaultCheFlavor) + return &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + APIVersion: "apps/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: cheFlavor, + Namespace: cr.Namespace, + Labels: labels, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{MatchLabels: labels}, + Strategy: appsv1.DeploymentStrategy{ + Type: appsv1.RollingUpdateDeploymentStrategyType, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: "che", + Containers: []corev1.Container{ + { + Name: cheFlavor, + ImagePullPolicy: corev1.PullIfNotPresent, + Image: cheImage + ":" + cheTag, + Ports: []corev1.ContainerPort{ + { + Name: "http", + ContainerPort: 8080, + Protocol: "TCP", + }, + { + Name: "http-debug", + ContainerPort: 8000, + Protocol: "TCP", + }, + { + Name: "jgroups-ping", + ContainerPort: 8888, + Protocol: "TCP", + }, + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("512Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("1Gi"), + }, + }, + ReadinessProbe: &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/api/system/state", + Port: intstr.IntOrString{ + Type: intstr.Int, + IntVal: int32(8080), + }, + Scheme: corev1.URISchemeHTTP, + }, + }, + InitialDelaySeconds: 25, + FailureThreshold: 5, + TimeoutSeconds: 5, + }, + LivenessProbe: &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/api/system/state", + Port: intstr.IntOrString{ + Type: intstr.Int, + IntVal: int32(8080), + }, + Scheme: corev1.URISchemeHTTP, + }, + }, + InitialDelaySeconds: 50, + FailureThreshold: 3, + TimeoutSeconds: 3, + }, + EnvFrom: []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: "che"}, + }, + }, + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: "custom"}, + Optional: &optionalEnv, + }, + }, + }, + Env: []corev1.EnvVar{ + { + Name: "CM_REVISION", + Value: cmRevision, + }, + { + Name: "OPENSHIFT_KUBE_PING_NAMESPACE", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "metadata.namespace"}}, + }, + { + Name: "CHE_SELF__SIGNED__CERT", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + Key: "ca.crt", + LocalObjectReference: corev1.LocalObjectReference{ + Name: "self-signed-certificate", + }, + Optional: &optionalEnv, + }, + }, + }, + }}, + }, + }, + }, + }, + } +} diff --git a/pkg/deploy/deployment_keycloak.go b/pkg/deploy/deployment_keycloak.go new file mode 100644 index 000000000..6b2771ac8 --- /dev/null +++ b/pkg/deploy/deployment_keycloak.go @@ -0,0 +1,217 @@ +// +// 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 deploy + +import ( + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + "github.com/eclipse/che-operator/pkg/util" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func NewKeycloakDeployment(cr *orgv1.CheCluster, keycloakPostgresPassword string, keycloakAdminPassword string, cheFlavor string) *appsv1.Deployment { + keycloakName := "keycloak" + labels := GetLabels(cr, keycloakName) + keycloakImage := util.GetValue(cr.Spec.Auth.KeycloakImage, DefaultKeycloakImage) + trustpass := util.GeneratePasswd(12) + jbossDir := "/opt/eap" + if cheFlavor == "che" { + // writable dir in the upstream Keycloak image + jbossDir = "/scripts" + } + // add crt to Java trust store so that Keycloak can connect to k8s API + addCertToTrustStoreCommand := "keytool -importcert -alias HOSTDOMAIN" + + " -keystore " + jbossDir +"/openshift.jks" + + " -file /var/run/secrets/kubernetes.io/serviceaccount/ca.crt -storepass " + trustpass + " -noprompt" + + " && keytool -importkeystore -srckeystore $JAVA_HOME/jre/lib/security/cacerts" + + " -destkeystore " + jbossDir + "/openshift.jks" + + " -srcstorepass changeit -deststorepass " + trustpass + startCommand := "sed -i 's/WILDCARD/ANY/g' /opt/eap/bin/launch/keycloak-spi.sh && /opt/eap/bin/openshift-launch.sh -b 0.0.0.0" + // upstream Keycloak has a bit different mechanism of adding jks + changeConfigCommand := "echo -e \"embed-server --server-config=standalone.xml --std-out=echo \n" + + "/subsystem=keycloak-server/spi=truststore/:add \n" + + "/subsystem=keycloak-server/spi=truststore/provider=file/:add(properties={file => " + + "\"" + jbossDir + "/openshift.jks\", password => \"" + trustpass + "\", disabled => \"false\" },enabled=true) \n" + + "stop-embedded-server\" > /scripts/add_openshift_certificate.cli && " + + "/opt/jboss/keycloak/bin/jboss-cli.sh --file=/scripts/add_openshift_certificate.cli" + keycloakAdminUserName := util.GetValue(cr.Spec.Auth.KeycloakAdminUserName, DefaultKeycloakAdminUserName) + keycloakEnv := []corev1.EnvVar{ + { + Name: "PROXY_ADDRESS_FORWARDING", + Value: "true", + }, + { + Name: "KEYCLOAK_USER", + Value: keycloakAdminUserName, + }, + { + Name: "KEYCLOAK_PASSWORD", + Value: keycloakAdminPassword, + }, + { + Name: "DB_VENDOR", + Value: "POSTGRES", + }, + { + Name: "POSTGRES_PORT_5432_TCP_ADDR", + Value: "postgres", + }, + { + Name: "POSTGRES_PORT_5432_TCP_PORT", + Value: "5432", + }, + { + Name: "POSTGRES_DATABASE", + Value: "keycloak", + }, + { + Name: "POSTGRES_USER", + Value: "keycloak", + }, + { + Name: "POSTGRES_PASSWORD", + Value: keycloakPostgresPassword, + }, + } + if cheFlavor == "codeready" { + keycloakEnv = []corev1.EnvVar{ + { + Name: "PROXY_ADDRESS_FORWARDING", + Value: "true", + }, + { + Name: "DB_SERVICE_PREFIX_MAPPING", + Value: "keycloak-postgresql=DB", + }, + { + Name: "KEYCLOAK_POSTGRESQL_SERVICE_HOST", + Value: "postgres", + }, + { + Name: "KEYCLOAK_POSTGRESQL_SERVICE_PORT", + Value: "5432", + }, + { + Name: "DB_DATABASE", + Value: keycloakName, + }, + { + Name: "DB_USERNAME", + Value: keycloakName, + }, + { + Name: "DB_PASSWORD", + Value: keycloakPostgresPassword, + }, + { + Name: "SSO_ADMIN_USERNAME", + Value: keycloakAdminUserName, + }, + { + Name: "SSO_ADMIN_PASSWORD", + Value: keycloakAdminPassword, + }, + { + Name: "DB_VENDOR", + Value: "POSTGRES", + }, + { + Name: "SSO_TRUSTSTORE", + Value: "openshift.jks", + }, + { + Name: "SSO_TRUSTSTORE_DIR", + Value: jbossDir, + }, + { + Name: "SSO_TRUSTSTORE_PASSWORD", + Value: trustpass, + }, + } + } + command := addCertToTrustStoreCommand + " && " + changeConfigCommand + " && /opt/jboss/docker-entrypoint.sh -b 0.0.0.0" + if cheFlavor == "codeready" { + command = addCertToTrustStoreCommand + " && " + startCommand + } + return &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + APIVersion: "apps/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: keycloakName, + Namespace: cr.Namespace, + Labels: labels, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{MatchLabels: labels}, + Strategy: appsv1.DeploymentStrategy{ + Type: appsv1.RollingUpdateDeploymentStrategyType, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: keycloakName, + Image: keycloakImage, + ImagePullPolicy: corev1.PullIfNotPresent, + Command: []string{ + "/bin/sh", + }, + Args: []string{ + "-c", + command, + }, + Ports: []corev1.ContainerPort{ + { + Name: keycloakName, + ContainerPort: 8080, + Protocol: "TCP", + }, + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("512Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + }, + ReadinessProbe: &corev1.Probe{ + Handler: corev1.Handler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "auth/js/keycloak.js", + Port: intstr.IntOrString{ + Type: intstr.Int, + IntVal: int32(8080), + }, + Scheme: corev1.URISchemeHTTP, + }, + }, + InitialDelaySeconds: 25, + FailureThreshold: 10, + TimeoutSeconds: 5, + }, + Env: keycloakEnv, + }, + }, + }, + }, + }, + } +} diff --git a/pkg/deploy/deployment_postgres.go b/pkg/deploy/deployment_postgres.go new file mode 100644 index 000000000..faf5269db --- /dev/null +++ b/pkg/deploy/deployment_postgres.go @@ -0,0 +1,125 @@ +// +// 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 deploy + +import ( + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + "github.com/eclipse/che-operator/pkg/util" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func NewPostgresDeployment(cr *orgv1.CheCluster, chePostgresPassword string) *appsv1.Deployment { + chePostgresUser := util.GetValue(cr.Spec.Database.ChePostgresUser, "pgche") + chePostgresDb := util.GetValue(cr.Spec.Database.ChePostgresDb, "dbche") + postgresAdminPassword := util.GeneratePasswd(12) + postgresImage := util.GetValue(cr.Spec.Database.PostgresImage, DefaultPostgresImage) + name := "postgres" + labels := GetLabels(cr, name) + return &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + APIVersion: "apps/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "postgres", + Namespace: cr.Namespace, + Labels: labels, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{MatchLabels: labels}, + Strategy: appsv1.DeploymentStrategy{ + Type: appsv1.DeploymentStrategyType("Recreate"), + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{ + { + Name: name + "-data", + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: name + "-data", + }, + }, + }, + }, + Containers: []corev1.Container{ + { + Name: name, + Image: postgresImage, + ImagePullPolicy: corev1.PullIfNotPresent, + Ports: []corev1.ContainerPort{ + { + Name: name, + ContainerPort: 5432, + Protocol: "TCP", + }, + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("512Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("1Gi"), + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: name + "-data", + MountPath: "/var/lib/pgsql/data", + }, + }, + ReadinessProbe: &corev1.Probe{ + Handler: corev1.Handler{ + Exec: &corev1.ExecAction{ + Command: []string{ + "/bin/sh", + "-i", + "-c", + "psql -h 127.0.0.1 -U " + chePostgresUser + " -q -d " + chePostgresDb + " -c 'SELECT 1'", + }, + }, + }, + InitialDelaySeconds: 15, + FailureThreshold: 10, + SuccessThreshold: 1, + TimeoutSeconds: 5, + }, + Env: []corev1.EnvVar{ + { + Name: "POSTGRESQL_USER", + Value: chePostgresUser, + }, + { + Name: "POSTGRESQL_PASSWORD", + Value: chePostgresPassword, + }, + { + Name: "POSTGRESQL_DATABASE", + Value: chePostgresDb, + }, + { + Name: "POSTGRESQL_ADMIN_PASSWORD", + Value: postgresAdminPassword, + }, + }}, + }, + }, + }, + }, + } +} diff --git a/pkg/deploy/exec_commands.go b/pkg/deploy/exec_commands.go new file mode 100644 index 000000000..3063e768b --- /dev/null +++ b/pkg/deploy/exec_commands.go @@ -0,0 +1,134 @@ +// +// 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 deploy + +import ( + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + "github.com/eclipse/che-operator/pkg/util" + "github.com/sirupsen/logrus" + "io/ioutil" + "strings" +) + +func GetPostgresProvisionCommand(cr *orgv1.CheCluster) (command string) { + + chePostgresUser := util.GetValue(cr.Spec.Database.ChePostgresUser, DefaultChePostgresUser) + keycloakPostgresPassword := cr.Spec.Auth.KeycloakPostgresPassword + + command = "OUT=$(psql postgres -tAc \"SELECT 1 FROM pg_roles WHERE rolname='keycloak'\"); " + + "if [ $OUT -eq 1 ]; then echo \"DB exists\"; exit 0; fi " + + "&& psql -c \"CREATE USER keycloak WITH PASSWORD '" + keycloakPostgresPassword + "'\" " + + "&& psql -c \"CREATE DATABASE keycloak\" " + + "&& psql -c \"GRANT ALL PRIVILEGES ON DATABASE keycloak TO keycloak\" " + + "&& psql -c \"ALTER USER " + chePostgresUser + " WITH SUPERUSER\"" + + return command +} + +func GetKeycloakProvisionCommand(cr *orgv1.CheCluster, cheHost string) (command string) { + keycloakAdminUserName := util.GetValue(cr.Spec.Auth.KeycloakAdminUserName,"admin") + keycloakAdminPassword := util.GetValue(cr.Spec.Auth.KeycloakAdminPassword,"admin") + requiredActions := "" + updateAdminPassword := cr.Spec.Auth.UpdateAdminPassword + cheFlavor := util.GetValue(cr.Spec.Server.CheFlavor, DefaultCheFlavor) + keycloakRealm := util.GetValue(cr.Spec.Auth.KeycloakRealm, cheFlavor) + keycloakClientId := util.GetValue(cr.Spec.Auth.KeycloakClientId, cheFlavor+"-public") + + if updateAdminPassword { + requiredActions = "\"UPDATE_PASSWORD\"" + } + file, err := ioutil.ReadFile("/tmp/keycloak_provision") + if err != nil { + logrus.Errorf("Failed to find 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" + + } + str := string(file) + r := strings.NewReplacer("$script", script, + "$keycloakAdminUserName", keycloakAdminUserName, + "$keycloakAdminPassword", keycloakAdminPassword, + "$keycloakRealm", keycloakRealm, + "$realmDisplayName", realmDisplayName, + "$keycloakClientId", keycloakClientId, + "$keycloakTheme", keycloakTheme, + "$cheHost", cheHost, + "$requiredActions", requiredActions) + createRealmClientUserCommand := r.Replace(str) + command = createRealmClientUserCommand + if cheFlavor == "che" { + command = "cd /scripts && " +createRealmClientUserCommand + } + return command +} + +func GetOpenShiftIdentityProviderProvisionCommand(cr *orgv1.CheCluster, oAuthClientName string, oauthSecret string, keycloakAdminPassword string) (command string) { + cheFlavor := util.GetValue(cr.Spec.Server.CheFlavor, DefaultCheFlavor) + openShiftApiUrl, err := util.GetClusterPublicHostname() + 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) + openShiftApiUrl = "RECPLACE_ME" + } + + keycloakRealm := util.GetValue(cr.Spec.Auth.KeycloakRealm, cheFlavor) + keycloakAdminUserName := util.GetValue(cr.Spec.Auth.KeycloakAdminUserName, DefaultKeycloakAdminUserName) + script := "/opt/jboss/keycloak/bin/kcadm.sh" + if cheFlavor == "codeready" { + script = "/opt/eap/bin/kcadm.sh" + + } + + createOpenShiftIdentityProviderCommand := + script + " config credentials --server http://0.0.0.0:8080/auth " + + "--realm master --user " + keycloakAdminUserName + " --password " + keycloakAdminPassword + " && " + script + + " get identity-provider/instances/openshift-v3 -r " + keycloakRealm + "; " + + "if [ $? -eq 0 ]; then echo \"Provider exists\"; exit 0; fi && " + script + + " create identity-provider/instances -r " + keycloakRealm + + " -s alias=openshift-v3 -s providerId=openshift-v3 -s enabled=true -s storeToken=true" + + " -s addReadTokenRoleOnCreate=true -s config.useJwksUrl=true" + + " -s config.clientId=" + oAuthClientName + " -s config.clientSecret=" + oauthSecret + + " -s config.baseUrl=" + openShiftApiUrl + + " -s config.defaultScope=user:full" + command = createOpenShiftIdentityProviderCommand + if cheFlavor == "che" { + command = "cd /scripts && " + createOpenShiftIdentityProviderCommand + } + return command +} + +func GetDeleteOpenShiftIdentityProviderProvisionCommand(cr *orgv1.CheCluster, keycloakAdminPassword string) (command string) { + cheFlavor := util.GetValue(cr.Spec.Server.CheFlavor, DefaultCheFlavor) + keycloakRealm := util.GetValue(cr.Spec.Auth.KeycloakRealm, cheFlavor) + keycloakAdminUserName := util.GetValue(cr.Spec.Auth.KeycloakAdminUserName, DefaultKeycloakAdminUserName) + script := "/opt/jboss/keycloak/bin/kcadm.sh" + if cheFlavor == "codeready" { + script = "/opt/eap/bin/kcadm.sh" + + } + + deleteOpenShiftIdentityProviderCommand := + script + " config credentials --server http://0.0.0.0:8080/auth " + + "--realm master --user " + keycloakAdminUserName + " --password " + keycloakAdminPassword + " && " + + script + " delete identity-provider/instances/openshift-v3 -r " + keycloakRealm + command = deleteOpenShiftIdentityProviderCommand + if cheFlavor == "che" { + command = "cd /scripts && " + deleteOpenShiftIdentityProviderCommand + } + return command +} + diff --git a/pkg/deploy/ingress.go b/pkg/deploy/ingress.go new file mode 100644 index 000000000..d25fc93f3 --- /dev/null +++ b/pkg/deploy/ingress.go @@ -0,0 +1,101 @@ +// +// 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 deploy + +import ( + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + "github.com/eclipse/che-operator/pkg/util" + "k8s.io/api/extensions/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func NewIngress(cr *orgv1.CheCluster, name string, serviceName string, port int) *v1beta1.Ingress { + tlsSupport := cr.Spec.Server.TlsSupport + ingressStrategy := cr.Spec.K8SOnly.IngressStrategy + if len(ingressStrategy) < 1 { + ingressStrategy = "multi-host" + } + ingressDomain := cr.Spec.K8SOnly.IngressDomain + ingressClass := util.GetValue(cr.Spec.K8SOnly.IngressClass, DefaultIngressClass) + labels := GetLabels(cr, util.GetValue(cr.Spec.Server.CheFlavor, DefaultCheFlavor)) + tlsSecretName := cr.Spec.K8SOnly.TlsSecretName + if name == "keycloak" { + labels = GetLabels(cr, name) + } + tls := "false" + if tlsSupport { + tls = "true" + } + host := "" + path := "/" + if name == "keycloak" && ingressStrategy != "multi-host" { + path = "/auth" + } + if ingressStrategy == "multi-host" { + host = name + "-" + cr.Namespace + "." + ingressDomain + } else if ingressStrategy == "single-host" { + host = ingressDomain + } + tlsIngress := []v1beta1.IngressTLS{} + if tlsSupport { + tlsIngress = []v1beta1.IngressTLS{ + { + Hosts: []string{ + ingressDomain, + }, + SecretName: tlsSecretName, + }, + } + } + + return &v1beta1.Ingress{ + TypeMeta: metav1.TypeMeta{ + Kind: "Ingress", + APIVersion: v1beta1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: cr.Namespace, + Labels: labels, + Annotations: map[string]string{ + "kubernetes.io/ingress.class": ingressClass, + "nginx.ingress.kubernetes.io/proxy-read-timeout": "3600", + "nginx.ingress.kubernetes.io/proxy-connect-timeout": "3600", + "nginx.ingress.kubernetes.io/ssl-redirect": tls, + }, + }, + Spec: v1beta1.IngressSpec{ + TLS: tlsIngress, + Rules: []v1beta1.IngressRule{ + { + Host: host, + IngressRuleValue: v1beta1.IngressRuleValue{ + HTTP: &v1beta1.HTTPIngressRuleValue{ + + Paths: []v1beta1.HTTPIngressPath{ + { + Backend: v1beta1.IngressBackend{ + ServiceName: serviceName, + ServicePort: intstr.FromInt(port), + }, + Path: path, + }, + }, + }, + }, + }, + }, + }, + } +} + diff --git a/pkg/deploy/labels.go b/pkg/deploy/labels.go new file mode 100644 index 000000000..4677ea7c2 --- /dev/null +++ b/pkg/deploy/labels.go @@ -0,0 +1,22 @@ +// +// 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 deploy + +import ( + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" +) + +func GetLabels(cr *orgv1.CheCluster, component string) (labels map[string]string) { + cheFlavor := cr.Spec.Server.CheFlavor + labels = map[string]string{"app": cheFlavor, "component": component} + return labels +} diff --git a/pkg/deploy/oauthclient.go b/pkg/deploy/oauthclient.go new file mode 100644 index 000000000..f99dcb362 --- /dev/null +++ b/pkg/deploy/oauthclient.go @@ -0,0 +1,38 @@ +// +// 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 deploy + +import ( + oauth "github.com/openshift/api/oauth/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + + +func NewOAuthClient(name string, oauthSecret string, keycloakURL string, keycloakRealm string) *oauth.OAuthClient { + return &oauth.OAuthClient{ + TypeMeta: metav1.TypeMeta{ + Kind: "OAuthClient", + APIVersion: oauth.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{"app":"che"}, + }, + + Secret: oauthSecret, + RedirectURIs: []string{ + keycloakURL + "/auth/realms/" + keycloakRealm +"/broker/openshift-v3/endpoint", + }, + GrantMethod: oauth.GrantHandlerPrompt, + } + +} diff --git a/pkg/deploy/pvc.go b/pkg/deploy/pvc.go new file mode 100644 index 000000000..d977acd50 --- /dev/null +++ b/pkg/deploy/pvc.go @@ -0,0 +1,47 @@ +// +// 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 deploy + +import ( + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func NewPvc(cr *orgv1.CheCluster, name string, pvcClaimSize string, labels map[string]string) *corev1.PersistentVolumeClaim { + //value := true + return &corev1.PersistentVolumeClaim{ + TypeMeta: metav1.TypeMeta{ + Kind: "PersistentVolumeClaim", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: cr.Namespace, + Labels: labels, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + // todo Make configurable + corev1.ReadWriteOnce, + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceName(corev1.ResourceStorage): resource.MustParse(pvcClaimSize), + }, + }, + }, + } + +} + diff --git a/pkg/deploy/role.go b/pkg/deploy/role.go new file mode 100644 index 000000000..6c4c3e0b4 --- /dev/null +++ b/pkg/deploy/role.go @@ -0,0 +1,46 @@ +// +// 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 deploy + +import ( + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + "github.com/eclipse/che-operator/pkg/util" + rbac "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func NewRole(cr *orgv1.CheCluster, name string, resources []string, verbs []string) *rbac.Role { + labels := GetLabels(cr, util.GetValue(cr.Spec.Server.CheFlavor, DefaultCheFlavor)) + return &rbac.Role{ + TypeMeta: metav1.TypeMeta{ + Kind: "Role", + APIVersion: rbac.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: cr.Namespace, + Labels: labels, + }, + Rules: []rbac.PolicyRule{ + { + APIGroups: []string{ + "", + }, + Resources: resources, + Verbs: verbs, + }, + }, + } +} + + + diff --git a/pkg/deploy/rolebinding.go b/pkg/deploy/rolebinding.go new file mode 100644 index 000000000..9bedf764f --- /dev/null +++ b/pkg/deploy/rolebinding.go @@ -0,0 +1,47 @@ +// +// 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 deploy + +import ( + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + "github.com/eclipse/che-operator/pkg/util" + rbac "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func NewRoleBinding(cr *orgv1.CheCluster, name string, serviceAccountName string, roleName string, roleKind string) *rbac.RoleBinding { + labels := GetLabels(cr, util.GetValue(cr.Spec.Server.CheFlavor, DefaultCheFlavor)) + return &rbac.RoleBinding{ + TypeMeta: metav1.TypeMeta{ + Kind: "RoleBinding", + APIVersion: rbac.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: cr.Namespace, + Labels: labels, + }, + Subjects: []rbac.Subject{ + { + Kind: rbac.ServiceAccountKind, + Name: serviceAccountName, + Namespace: cr.Namespace, + }, + }, + RoleRef: rbac.RoleRef{ + Name: roleName, + Kind: roleKind, + APIGroup: "rbac.authorization.k8s.io", + }, + } +} + diff --git a/pkg/deploy/route.go b/pkg/deploy/route.go new file mode 100644 index 000000000..0e0950ff2 --- /dev/null +++ b/pkg/deploy/route.go @@ -0,0 +1,73 @@ +// +// 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 deploy + +import ( + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + "github.com/eclipse/che-operator/pkg/util" + routev1 "github.com/openshift/api/route/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func NewRoute(cr *orgv1.CheCluster, name string, serviceName string) *routev1.Route { + labels := GetLabels(cr, util.GetValue(cr.Spec.Server.CheFlavor, DefaultCheFlavor)) + if name == "keycloak" { + labels = GetLabels(cr, name) + } + return &routev1.Route{ + TypeMeta: metav1.TypeMeta{ + Kind: "Route", + APIVersion: routev1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: cr.Namespace, + Labels: labels, + + }, + Spec: routev1.RouteSpec{ + To: routev1.RouteTargetReference{ + Kind: "Service", + Name: serviceName, + }, + }, + } +} + +func NewTlsRoute(cr *orgv1.CheCluster, name string, serviceName string) *routev1.Route { + labels := GetLabels(cr, util.GetValue(cr.Spec.Server.CheFlavor, DefaultCheFlavor)) + if name == "keycloak" { + labels = GetLabels(cr, name) + } + return &routev1.Route{ + TypeMeta: metav1.TypeMeta{ + Kind: "Route", + APIVersion: routev1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: cr.Namespace, + Labels: labels, + + }, + Spec: routev1.RouteSpec{ + To: routev1.RouteTargetReference{ + Kind: "Service", + Name: serviceName, + }, + TLS: &routev1.TLSConfig{ + InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect, + Termination: routev1.TLSTerminationEdge, + }, + }, + } +} diff --git a/pkg/deploy/secret.go b/pkg/deploy/secret.go new file mode 100644 index 000000000..0abaf15a5 --- /dev/null +++ b/pkg/deploy/secret.go @@ -0,0 +1,37 @@ +// +// 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 deploy + +import ( + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + "github.com/eclipse/che-operator/pkg/util" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func NewSecret(cr *orgv1.CheCluster, name string, crt []byte) *corev1.Secret { + labels := GetLabels(cr, util.GetValue(cr.Spec.Server.CheFlavor, DefaultCheFlavor)) + return &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: cr.Namespace, + Labels: labels, + }, + Data: map[string][]byte{ + "ca.crt": crt, + }, + } +} diff --git a/pkg/deploy/service.go b/pkg/deploy/service.go new file mode 100644 index 000000000..0554a6bb1 --- /dev/null +++ b/pkg/deploy/service.go @@ -0,0 +1,45 @@ +// +// 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 deploy + +import ( + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func NewService(cr *orgv1.CheCluster, name string, labels map[string]string, portName string, portNumber int32) *corev1.Service { + + return &corev1.Service{ + TypeMeta: metav1.TypeMeta{ + Kind: "Service", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: cr.Namespace, + Labels: labels, + + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Name: portName, + Port: portNumber, + Protocol: "TCP", + }, + }, + Selector: labels, + }, + } +} + diff --git a/pkg/deploy/service_account.go b/pkg/deploy/service_account.go new file mode 100644 index 000000000..28cdc7b0b --- /dev/null +++ b/pkg/deploy/service_account.go @@ -0,0 +1,34 @@ +// +// 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 deploy + +import ( + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + "github.com/eclipse/che-operator/pkg/util" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func NewServiceAccount(cr *orgv1.CheCluster, name string) *corev1.ServiceAccount { + labels := GetLabels(cr, util.GetValue(cr.Spec.Server.CheFlavor, DefaultCheFlavor)) + return &corev1.ServiceAccount{ + TypeMeta: metav1.TypeMeta{ + Kind: "ServiceAccount", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: cr.Namespace, + Labels: labels, + }, + } +} diff --git a/pkg/util/util.go b/pkg/util/util.go new file mode 100644 index 000000000..56a163b26 --- /dev/null +++ b/pkg/util/util.go @@ -0,0 +1,143 @@ +// +// 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 util + +import ( + "crypto/tls" + "encoding/json" + "github.com/sirupsen/logrus" + "io/ioutil" + "k8s.io/client-go/discovery" + "math/rand" + "net/http" + "os" + "sigs.k8s.io/controller-runtime/pkg/client/config" + "strings" + "time" +) + +func GeneratePasswd(stringLength int) (passwd string) { + rand.Seed(time.Now().UnixNano()) + chars := []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz" + + "0123456789") + length := stringLength + buf := make([]rune, length) + for i := range buf { + buf[i] = chars[rand.Intn(len(chars))] + } + passwd = string(buf) + return passwd +} + +func DetectOpenShift() (bool, error) { + tests := IsTestMode() + if !tests { + kubeconfig, err := config.GetConfig() + if err != nil { + return false, err + } + discoveryClient, err := discovery.NewDiscoveryClientForConfig(kubeconfig) + if err != nil { + return false, err + } + apiList, err := discoveryClient.ServerGroups() + if err != nil { + return false, err + } + apiGroups := apiList.Groups + for i := 0; i < len(apiGroups); i++ { + if apiGroups[i].Name == "route.openshift.io" { + return true, nil + } + } + return false, nil + } + return true, nil +} + +func GetValue(key string, defaultValue string) (value string) { + + value = key + if len(key) < 1 { + value = defaultValue + } + return value +} + +func IsTestMode() (isTesting bool) { + + testMode := os.Getenv("MOCK_API") + if len(testMode) == 0 { + return false + } + return true +} + +// GetClusterPublicHostname is a hacky way to get OpenShift API public DNS/IP +// to be used in OpenShift oAuth provider as baseURL +func GetClusterPublicHostname() (hostname string, err error) { + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + client := &http.Client{} + kubeApi := os.Getenv("KUBERNETES_PORT_443_TCP_ADDR") + url := "https://" + kubeApi + "/.well-known/oauth-authorization-server" + req, err := http.NewRequest("GET", url, nil) + resp, err := client.Do(req) + if err != nil { + logrus.Errorf("An error occurred when getting API public hostname: %s", err) + return "", err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + logrus.Errorf("An error occurred when getting API public hostname: %s", err) + return "", err + } + var jsonData map[string]interface{} + err = json.Unmarshal(body, &jsonData) + if err != nil { + logrus.Errorf("An error occurred when unmarshalling: %s") + return "", err + } + hostname = jsonData["issuer"].(string) + return hostname, nil +} + +func GenerateProxyJavaOpts(proxyURL string, proxyPort string, nonProxyHosts string, proxyUser string, proxyPassword string) (javaOpts string) { + + proxyHost := strings.TrimLeft(proxyURL, "https://") + proxyUserPassword := "" + if len(proxyUser) > 1 && len(proxyPassword) > 1 { + proxyUserPassword = + " -Dhttp.proxyUser=" + proxyUser + " -Dhttp.proxyPassword=" + proxyPassword + + " -Dhttps.proxyUser=" + proxyUser + " -Dhttps.proxyPassword=" + proxyPassword + } + javaOpts = + " -Dhttp.proxyHost=" + proxyHost + " -Dhttp.proxyPort=" + proxyPort + + " -Dhttps.proxyHost=" + proxyHost + " -Dhttps.proxyPort=" + proxyPort + + " -Dhttp.nonProxyHosts='" + nonProxyHosts + "|172.30.0.1'" + proxyUserPassword + return javaOpts +} + +func GenerateProxyEnvs(proxyHost string, proxyPort string, nonProxyHosts string, proxyUser string, proxyPassword string) (proxyUrl string, noProxy string) { + proxyUrl = proxyHost + ":" + proxyPort + if len(proxyUser) > 1 && len(proxyPassword) > 1 { + protocol := strings.Split(proxyHost, "://")[0] + host := strings.Split(proxyHost, "://")[1] + proxyUrl = protocol + "://" + proxyUser + ":" + proxyPassword + "@" + host + ":" + proxyPort + } + + noProxy = strings.Replace(nonProxyHosts, "|", ",", -1) + + return proxyUrl, noProxy +} diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go new file mode 100644 index 000000000..dc8cd4072 --- /dev/null +++ b/pkg/util/util_test.go @@ -0,0 +1,99 @@ +// +// 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 util + +import ( + "reflect" + "testing" +) + +const ( + proxyHost = "https://myproxy.com" + proxyPort = "1234" + nonProxyHosts = "localhost|myhost.com" + proxyUser = "user" + proxyPassword = "password" + expectedProxyURLWithUsernamePassword = "https://user:password@myproxy.com:1234" + expectedProxyURLWithoutUsernamePassword = "https://myproxy.com:1234" + expectedNoProxy = "localhost,myhost.com" +) + +func TestGenerateProxyEnvs(t *testing.T) { + + proxyUrl, noProxy := GenerateProxyEnvs(proxyHost, proxyPort, nonProxyHosts, proxyUser, proxyPassword) + + if !reflect.DeepEqual(proxyUrl, expectedProxyURLWithUsernamePassword) { + t.Errorf("Test failed. Expected %s but got %s", expectedProxyURLWithUsernamePassword, proxyUrl) + } + + if !reflect.DeepEqual(noProxy, expectedNoProxy) { + t.Errorf("Test failed. Expected %s but got %s", expectedNoProxy, noProxy) + + } + + proxyUrl, _ = GenerateProxyEnvs(proxyHost, proxyPort, nonProxyHosts, "", proxyPassword) + if !reflect.DeepEqual(proxyUrl, expectedProxyURLWithoutUsernamePassword) { + t.Errorf("Test failed. Expected %s but got %s", expectedProxyURLWithoutUsernamePassword, proxyUrl) + } + +} + +func TestGenerateProxyJavaOpts(t *testing.T) { + + javaOpts := GenerateProxyJavaOpts(proxyHost, proxyPort, nonProxyHosts, proxyUser, proxyPassword) + expectedJavaOpts := " -Dhttp.proxyHost=myproxy.com -Dhttp.proxyPort=1234 -Dhttps.proxyHost=myproxy.com " + + "-Dhttps.proxyPort=1234 -Dhttp.nonProxyHosts='localhost|myhost.com|172.30.0.1' -Dhttp.proxyUser=user " + + "-Dhttp.proxyPassword=password -Dhttps.proxyUser=user -Dhttps.proxyPassword=password" + if !reflect.DeepEqual(javaOpts,expectedJavaOpts) { + t.Errorf("Test failed. Expected '%s' but got '%s'", expectedJavaOpts, javaOpts) + + } + + javaOpts = GenerateProxyJavaOpts(proxyHost, proxyPort, nonProxyHosts, "", proxyPassword) + expectedJavaOptsWithoutUsernamePassword := " -Dhttp.proxyHost=myproxy.com -Dhttp.proxyPort=1234 -Dhttps.proxyHost=myproxy.com " + + "-Dhttps.proxyPort=1234 -Dhttp.nonProxyHosts='localhost|myhost.com|172.30.0.1'" + + if !reflect.DeepEqual(javaOpts ,expectedJavaOptsWithoutUsernamePassword) { + t.Errorf("Test failed. Expected '%s' but got '%s'", expectedJavaOptsWithoutUsernamePassword, javaOpts) + + } +} + +func TestGeneratePasswd(t *testing.T) { + chars := 12 + passwd := GeneratePasswd(chars) + expectedCharsNumber := 12 + + if !reflect.DeepEqual(len(passwd), expectedCharsNumber) { + t.Errorf("Test failed. Expected %v chars, got %v chars", expectedCharsNumber, len(passwd)) + } + + passwd1 := GeneratePasswd(12) + if reflect.DeepEqual (passwd, passwd1) { + t.Errorf("Test failed. Passwords are identical, %s: %s", passwd, passwd1) + } +} + +func TestGetValue(t *testing.T) { + key := "myvalue" + defaultValue := "myDefaultValue" + var1 := GetValue(key, defaultValue) + var2 := GetValue("", defaultValue) + + if !reflect.DeepEqual(var1, key) { + t.Errorf("Test failed. Expected '%s', but got '%s'", key, var1) + } + + if !reflect.DeepEqual(var2, defaultValue) { + t.Errorf("Test failed. Expected '%s', but got '%s'", var2, defaultValue) + } +} diff --git a/version/version.go b/version/version.go new file mode 100644 index 000000000..e3e130bf9 --- /dev/null +++ b/version/version.go @@ -0,0 +1,5 @@ +package version + +var ( + Version = "0.0.1" +)