diff --git a/Dockerfile b/Dockerfile index 344c09d30d..6a0e697db1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,7 @@ ARG LDFLAGS RUN GOOS="$GOOS" GOARCH="$GOARCH" go build -ldflags "$LDFLAGS" -tags netgo,osusergo -o ocs-operator main.go RUN GOOS="$GOOS" GOARCH="$GOARCH" go build -tags netgo,osusergo -o provider-api services/provider/main.go RUN GOOS="$GOOS" GOARCH="$GOARCH" go build -tags netgo,osusergo -o onboarding-secret-generator onboarding/main.go +RUN GOOS="$GOOS" GOARCH="$GOARCH" go build -tags netgo,osusergo -o ux-backend-server services/ux-backend/main.go # Build stage 2 @@ -22,6 +23,7 @@ COPY --from=builder workspace/ocs-operator /usr/local/bin/ocs-operator COPY --from=builder workspace/provider-api /usr/local/bin/provider-api COPY --from=builder workspace/onboarding-secret-generator /usr/local/bin/onboarding-secret-generator COPY --from=builder workspace/metrics/deploy/*rules*.yaml /ocs-prometheus-rules/ +COPY --from=builder workspace/ux-backend-server /usr/local/bin/ux-backend-server RUN chmod +x /usr/local/bin/ocs-operator /usr/local/bin/provider-api diff --git a/controllers/ocsinitialization/ocsinitialization_controller.go b/controllers/ocsinitialization/ocsinitialization_controller.go index 225ac3d015..e969995f5a 100644 --- a/controllers/ocsinitialization/ocsinitialization_controller.go +++ b/controllers/ocsinitialization/ocsinitialization_controller.go @@ -15,6 +15,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -26,7 +27,10 @@ import ( // operatorNamespace is the namespace the operator is running in var operatorNamespace string -const wrongNamespacedName = "Ignoring this resource. Only one should exist, and this one has the wrong name and/or namespace." +const ( + wrongNamespacedName = "Ignoring this resource. Only one should exist, and this one has the wrong name and/or namespace." + random30CharacterString = "KP7TThmSTZegSGmHuPKLnSaaAHSG3RSgqw6akBj0oVk" +) // InitNamespacedName returns a NamespacedName for the singleton instance that // should exist. @@ -159,6 +163,18 @@ func (r *OCSInitializationReconciler) Reconcile(ctx context.Context, request rec return reconcile.Result{}, err } + err = r.reconcileUXBackendSecret(instance) + if err != nil { + r.Log.Error(err, "Failed to ensure uxbackend secret") + return reconcile.Result{}, err + } + + err = r.reconcileUXBackendService(instance) + if err != nil { + r.Log.Error(err, "Failed to ensure uxbackend service") + return reconcile.Result{}, err + } + reason := ocsv1.ReconcileCompleted message := ocsv1.ReconcileCompletedMessage util.SetCompleteCondition(&instance.Status.Conditions, reason, message) @@ -175,6 +191,8 @@ func (r *OCSInitializationReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&ocsv1.OCSInitialization{}). + Owns(&corev1.Service{}). + Owns(&corev1.Secret{}). // Watcher for storagecluster required to update // ocs-operator-config configmap if storagecluster spec changes Watches( @@ -327,3 +345,81 @@ func (r *OCSInitializationReconciler) getEnableNFSKeyValue() string { return "false" } + +func (r *OCSInitializationReconciler) reconcileUXBackendSecret(initialData *ocsv1.OCSInitialization) error { + + var err error + + secret := &corev1.Secret{} + secret.Name = "ux-backend-proxy" + secret.Namespace = initialData.Namespace + + _, err = ctrl.CreateOrUpdate(r.ctx, r.Client, secret, func() error { + + if err := ctrl.SetControllerReference(initialData, secret, r.Scheme); err != nil { + return err + } + + secret.StringData = map[string]string{ + "session_secret": random30CharacterString, + } + + return nil + }) + + if err != nil { + r.Log.Error(err, "Failed to create/update ux-backend secret") + return err + } + + r.Log.Info("Secret creation succeeded", "Name", secret.Name) + + return nil +} + +func (r *OCSInitializationReconciler) reconcileUXBackendService(initialData *ocsv1.OCSInitialization) error { + + var err error + + service := &corev1.Service{} + service.Name = "ux-backend-proxy" + service.Namespace = initialData.Namespace + + _, err = ctrl.CreateOrUpdate(r.ctx, r.Client, service, func() error { + + if err := ctrl.SetControllerReference(initialData, service, r.Scheme); err != nil { + return err + } + + service.Annotations = map[string]string{ + "service.beta.openshift.io/serving-cert-secret-name": "ux-cert-secret", + } + service.Spec = corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Name: "proxy", + Port: 8888, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.IntOrString{ + Type: intstr.Int, + IntVal: 8888, + }, + }, + }, + Selector: map[string]string{"app": "ux-backend-server"}, + SessionAffinity: "None", + Type: "ClusterIP", + } + + return nil + + }) + + if err != nil { + r.Log.Error(err, "Failed to create/update ux-backend service") + return err + } + r.Log.Info("Service creation succeeded", "Name", service.Name) + + return nil +} diff --git a/deploy/ocs-operator/manifests/ocs-operator.clusterserviceversion.yaml b/deploy/ocs-operator/manifests/ocs-operator.clusterserviceversion.yaml index e54a7182b0..a2e484b40c 100644 --- a/deploy/ocs-operator/manifests/ocs-operator.clusterserviceversion.yaml +++ b/deploy/ocs-operator/manifests/ocs-operator.clusterserviceversion.yaml @@ -3079,6 +3079,8 @@ spec: value: quay.io/ocs-dev/ocs-operator:latest - name: ONBOARDING_SECRET_GENERATOR_IMAGE value: quay.io/ocs-dev/ocs-operator:latest + - name: UX_BACKEND_SERVER_IMAGE + value: quay.io/ocs-dev/ocs-operator:latest - name: OPERATOR_NAMESPACE valueFrom: fieldRef: @@ -3252,6 +3254,79 @@ spec: name: rook-config - emptyDir: {} name: default-config-dir + - name: ux-backend-server + spec: + replicas: 1 + selector: + matchLabels: + app: ux-backend-server + app.kubernetes.io/component: ux-backend-server + app.kubernetes.io/name: ux-backend-server + strategy: + type: Recreate + template: + metadata: + labels: + app: ux-backend-server + app.kubernetes.io/component: ux-backend-server + app.kubernetes.io/name: ux-backend-server + spec: + containers: + - command: + - /usr/local/bin/ux-backend-server + env: + - name: ONBOARDING_TOKEN_LIFETIME + - name: UX_BACKEND_PORT + image: quay.io/ocs-dev/ocs-operator:latest + imagePullPolicy: IfNotPresent + name: ux-backend-server + ports: + - containerPort: 8080 + resources: {} + volumeMounts: + - mountPath: /etc/private-key + name: onboarding-private-key + - mountPath: /etc/tls/private + name: ux-cert-secret + - args: + - -provider=openshift + - -https-address=:8888 + - -http-address= + - -email-domain=* + - -upstream=https://localhost:8080/onboarding-tokens + - -tls-cert=/etc/tls/private/tls.crt + - -tls-key=/etc/tls/private/tls.key + - -cookie-secret-file=/etc/proxy/secrets/session_secret + - -openshift-service-account=ux-backend-server + - -openshift-ca=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt + image: quay.io/openshift/origin-oauth-proxy:latest + imagePullPolicy: IfNotPresent + name: oauth-proxy + ports: + - containerPort: 8888 + resources: {} + volumeMounts: + - mountPath: /etc/proxy/secrets + name: ux-proxy-secret + - mountPath: /etc/tls/private + name: ux-cert-secret + serviceAccountName: ux-backend-server + tolerations: + - effect: NoSchedule + key: node.ocs.openshift.io/storage + operator: Equal + value: "true" + volumes: + - name: onboarding-private-key + secret: + optional: true + secretName: onboarding-private-key + - name: ux-proxy-secret + secret: + secretName: ux-backend-proxy + - name: ux-cert-secret + secret: + secretName: ux-cert-secret permissions: - rules: - apiGroups: @@ -3571,4 +3646,6 @@ spec: name: ocs-must-gather - image: quay.io/ocs-dev/ocs-metrics-exporter:latest name: ocs-metrics-exporter + - image: quay.io/openshift/origin-oauth-proxy:latest + name: ux-backend-oauth-image version: 4.15.0 diff --git a/deploy/ocs-operator/manifests/onboarding-secret-generator-binding.yaml b/deploy/ocs-operator/manifests/onboarding-secret-generator-binding.yaml index 428418f1fe..344ce37876 100644 --- a/deploy/ocs-operator/manifests/onboarding-secret-generator-binding.yaml +++ b/deploy/ocs-operator/manifests/onboarding-secret-generator-binding.yaml @@ -1,10 +1,10 @@ apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding +kind: RoleBinding metadata: name: onboarding-secret-generator roleRef: apiGroup: rbac.authorization.k8s.io - kind: ClusterRole + kind: Role name: onboarding-secret-generator subjects: - kind: ServiceAccount diff --git a/deploy/ocs-operator/manifests/onboarding-secret-generator-role.yaml b/deploy/ocs-operator/manifests/onboarding-secret-generator-role.yaml index 06146068f5..cae160c88c 100644 --- a/deploy/ocs-operator/manifests/onboarding-secret-generator-role.yaml +++ b/deploy/ocs-operator/manifests/onboarding-secret-generator-role.yaml @@ -1,4 +1,4 @@ -kind: ClusterRole +kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: name: onboarding-secret-generator diff --git a/deploy/ocs-operator/manifests/ux_backend_role.yaml b/deploy/ocs-operator/manifests/ux_backend_role.yaml new file mode 100644 index 0000000000..f89b32672e --- /dev/null +++ b/deploy/ocs-operator/manifests/ux_backend_role.yaml @@ -0,0 +1,16 @@ +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: ux-backend-server +rules: +- apiGroups: + - "" + resources: + - secrets + resourceNames: + - onboarding-private-key + - ux-cert-secret + - ux-backend-proxy + verbs: + - get + - list diff --git a/deploy/ocs-operator/manifests/ux_backend_role_binding.yaml b/deploy/ocs-operator/manifests/ux_backend_role_binding.yaml new file mode 100644 index 0000000000..1bcdeedc72 --- /dev/null +++ b/deploy/ocs-operator/manifests/ux_backend_role_binding.yaml @@ -0,0 +1,12 @@ +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: ux-backend-server +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: ux-backend-server +subjects: +- kind: ServiceAccount + name: ux-backend-server + namespace: openshift-storage diff --git a/deploy/ocs-operator/manifests/ux_backend_sa.yaml b/deploy/ocs-operator/manifests/ux_backend_sa.yaml new file mode 100644 index 0000000000..50e442ae3e --- /dev/null +++ b/deploy/ocs-operator/manifests/ux_backend_sa.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ux-backend-server diff --git a/hack/common.sh b/hack/common.sh index f26960b23c..e3303bcc3e 100644 --- a/hack/common.sh +++ b/hack/common.sh @@ -62,6 +62,7 @@ DEFAULT_OPERATOR_IMAGE_NAME="ocs-operator" DEFAULT_OPERATOR_BUNDLE_NAME="ocs-operator-bundle" DEFAULT_FILE_BASED_CATALOG_NAME="ocs-operator-catalog" DEFAULT_METRICS_EXPORTER_IMAGE_NAME="ocs-metrics-exporter" +DEFAULT_UX_BACKEND_OAUTH_IMAGE_NAME="openshift/origin-oauth-proxy" IMAGE_REGISTRY="${IMAGE_REGISTRY:-${DEFAULT_IMAGE_REGISTRY}}" REGISTRY_NAMESPACE="${REGISTRY_NAMESPACE:-${DEFAULT_REGISTRY_NAMESPACE}}" @@ -69,17 +70,20 @@ OPERATOR_IMAGE_NAME="${OPERATOR_IMAGE_NAME:-${DEFAULT_OPERATOR_IMAGE_NAME}}" OPERATOR_BUNDLE_NAME="${OPERATOR_BUNDLE_NAME:-${DEFAULT_OPERATOR_BUNDLE_NAME}}" FILE_BASED_CATALOG_NAME="${FILE_BASED_CATALOG_NAME:-${DEFAULT_FILE_BASED_CATALOG_NAME}}" METRICS_EXPORTER_IMAGE_NAME="${METRICS_EXPORTER_IMAGE_NAME:-${DEFAULT_METRICS_EXPORTER_IMAGE_NAME}}" +UX_BACKEND_OAUTH_IMAGE_NAME="${UX_BACKEND_OAUTH_IMAGE_NAME:-${DEFAULT_UX_BACKEND_OAUTH_IMAGE_NAME}}" IMAGE_TAG="${IMAGE_TAG:-${DEFAULT_IMAGE_TAG}}" DEFAULT_OPERATOR_FULL_IMAGE_NAME="${IMAGE_REGISTRY}/${REGISTRY_NAMESPACE}/${OPERATOR_IMAGE_NAME}:${IMAGE_TAG}" DEFAULT_BUNDLE_FULL_IMAGE_NAME="${IMAGE_REGISTRY}/${REGISTRY_NAMESPACE}/${OPERATOR_BUNDLE_NAME}:${IMAGE_TAG}" DEFAULT_FILE_BASED_CATALOG_FULL_IMAGE_NAME="${IMAGE_REGISTRY}/${REGISTRY_NAMESPACE}/${FILE_BASED_CATALOG_NAME}:${IMAGE_TAG}" DEFAULT_METRICS_EXPORTER_FULL_IMAGE_NAME="${IMAGE_REGISTRY}/${REGISTRY_NAMESPACE}/${METRICS_EXPORTER_IMAGE_NAME}:${IMAGE_TAG}" +DEFAULT_UX_BACKEND_OAUTH_FULL_IMAGE_NAME="${IMAGE_REGISTRY}/${UX_BACKEND_OAUTH_IMAGE_NAME}:${IMAGE_TAG}" OPERATOR_FULL_IMAGE_NAME="${OPERATOR_FULL_IMAGE_NAME:-${DEFAULT_OPERATOR_FULL_IMAGE_NAME}}" BUNDLE_FULL_IMAGE_NAME="${BUNDLE_FULL_IMAGE_NAME:-${DEFAULT_BUNDLE_FULL_IMAGE_NAME}}" FILE_BASED_CATALOG_FULL_IMAGE_NAME="${FILE_BASED_CATALOG_FULL_IMAGE_NAME:-${DEFAULT_FILE_BASED_CATALOG_FULL_IMAGE_NAME}}" METRICS_EXPORTER_FULL_IMAGE_NAME="${METRICS_EXPORTER_FULL_IMAGE_NAME:-${DEFAULT_METRICS_EXPORTER_FULL_IMAGE_NAME}}" +UX_BACKEND_OAUTH_FULL_IMAGE_NAME="${UX_BACKEND_OAUTH_FULL_IMAGE_NAME:-${DEFAULT_UX_BACKEND_OAUTH_FULL_IMAGE_NAME}}" NOOBAA_BUNDLE_FULL_IMAGE_NAME="quay.io/noobaa/noobaa-operator-bundle:master-20231217" diff --git a/hack/generate-latest-csv.sh b/hack/generate-latest-csv.sh index c654d3a7a0..4cead50bd4 100755 --- a/hack/generate-latest-csv.sh +++ b/hack/generate-latest-csv.sh @@ -14,6 +14,7 @@ export NOOBAA_DB_IMAGE=${NOOBAA_DB_IMAGE:-${LATEST_NOOBAA_DB_IMAGE}} export CEPH_IMAGE=${CEPH_IMAGE:-${LATEST_CEPH_IMAGE}} export OCS_IMAGE=${OCS_IMAGE:-${OPERATOR_FULL_IMAGE_NAME}} export OCS_METRICS_EXPORTER_IMAGE=${OCS_METRICS_EXPORTER_IMAGE:-${METRICS_EXPORTER_FULL_IMAGE_NAME}} +export UX_BACKEND_OAUTH_IMAGE=${UX_BACKEND_OAUTH_IMAGE:-${UX_BACKEND_OAUTH_FULL_IMAGE_NAME}} export OCS_MUST_GATHER_IMAGE=${OCS_MUST_GATHER_IMAGE:-${LATEST_MUST_GATHER_IMAGE}} export ROOK_CSIADDONS_IMAGE=${ROOK_CSIADDONS_IMAGE:-${LATEST_ROOK_CSIADDONS_IMAGE}} @@ -25,6 +26,7 @@ echo -e "\tNOOBAA_CORE_IMAGE=$NOOBAA_CORE_IMAGE" echo -e "\tNOOBAA_DB_IMAGE=$NOOBAA_DB_IMAGE" echo -e "\tOCS_IMAGE=$OCS_IMAGE" echo -e "\tOCS_METRICS_EXPORTER_IMAGE=$OCS_METRICS_EXPORTER_IMAGE" +echo -e "\tUX_BACKEND_OAUTH_IMAGE=$UX_BACKEND_OAUTH_IMAGE" echo -e "\tOCS_MUST_GATHER_IMAGE=$OCS_MUST_GATHER_IMAGE" echo -e "\tROOK_CSIADDONS_IMAGE=$ROOK_CSIADDONS_IMAGE" diff --git a/hack/generate-unified-csv.sh b/hack/generate-unified-csv.sh index c6b43b11ec..9cb7901f54 100755 --- a/hack/generate-unified-csv.sh +++ b/hack/generate-unified-csv.sh @@ -61,6 +61,7 @@ $CSV_MERGER \ --noobaa-db-image="$NOOBAA_DB_IMAGE" \ --ocs-image="$OCS_IMAGE" \ --ocs-metrics-exporter-image="$OCS_METRICS_EXPORTER_IMAGE" \ + --ux-backend-oauth-image="$UX_BACKEND_OAUTH_IMAGE" \ --ocs-must-gather-image="$OCS_MUST_GATHER_IMAGE" \ --crds-directory="$OUTDIR_CRDS" \ --manifests-directory=$BUNDLEMANIFESTS_DIR \ diff --git a/onboarding/main.go b/onboarding/main.go index 5988a708d9..170feb4e46 100644 --- a/onboarding/main.go +++ b/onboarding/main.go @@ -18,9 +18,9 @@ import ( ) const ( - onboardingTicketPublicKeySecretName = "onboarding-ticket-key" //Name of existing public key which is used ocs-operator - onboardingTicketPrivateKeySecretName = "onboarding-ticket-private-key" - serviceAccountName = "onboarding-secret-generator" + onboardingTicketPublicKeySecretName = "onboarding-ticket-key" //Name of existing public key which is used ocs-operator + onboardingPrivateKeySecretName = "onboarding-private-key" + serviceAccountName = "onboarding-secret-generator" ) func main() { @@ -60,7 +60,7 @@ func main() { privateSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: onboardingTicketPrivateKeySecretName, + Name: onboardingPrivateKeySecretName, Namespace: operatorNamespace, Annotations: map[string]string{"kubernetes.io/service-account.name": serviceAccountName}, }, diff --git a/rbac/onboarding-secret-generator-binding.yaml b/rbac/onboarding-secret-generator-binding.yaml index 428418f1fe..344ce37876 100644 --- a/rbac/onboarding-secret-generator-binding.yaml +++ b/rbac/onboarding-secret-generator-binding.yaml @@ -1,10 +1,10 @@ apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding +kind: RoleBinding metadata: name: onboarding-secret-generator roleRef: apiGroup: rbac.authorization.k8s.io - kind: ClusterRole + kind: Role name: onboarding-secret-generator subjects: - kind: ServiceAccount diff --git a/rbac/onboarding-secret-generator-role.yaml b/rbac/onboarding-secret-generator-role.yaml index 06146068f5..cae160c88c 100644 --- a/rbac/onboarding-secret-generator-role.yaml +++ b/rbac/onboarding-secret-generator-role.yaml @@ -1,4 +1,4 @@ -kind: ClusterRole +kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: name: onboarding-secret-generator diff --git a/rbac/ux_backend_role.yaml b/rbac/ux_backend_role.yaml new file mode 100644 index 0000000000..f89b32672e --- /dev/null +++ b/rbac/ux_backend_role.yaml @@ -0,0 +1,16 @@ +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: ux-backend-server +rules: +- apiGroups: + - "" + resources: + - secrets + resourceNames: + - onboarding-private-key + - ux-cert-secret + - ux-backend-proxy + verbs: + - get + - list diff --git a/rbac/ux_backend_role_binding.yaml b/rbac/ux_backend_role_binding.yaml new file mode 100644 index 0000000000..1bcdeedc72 --- /dev/null +++ b/rbac/ux_backend_role_binding.yaml @@ -0,0 +1,12 @@ +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: ux-backend-server +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: ux-backend-server +subjects: +- kind: ServiceAccount + name: ux-backend-server + namespace: openshift-storage diff --git a/rbac/ux_backend_sa.yaml b/rbac/ux_backend_sa.yaml new file mode 100644 index 0000000000..50e442ae3e --- /dev/null +++ b/rbac/ux_backend_sa.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ux-backend-server diff --git a/services/provider/server/server.go b/services/provider/server/server.go index 206f3229d7..8dbad0c101 100644 --- a/services/provider/server/server.go +++ b/services/provider/server/server.go @@ -24,6 +24,7 @@ import ( ocsVersion "github.com/red-hat-storage/ocs-operator/v4/version" rookCephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1" + sharedTypes "github.com/red-hat-storage/ocs-operator/v4/services/types" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" @@ -60,11 +61,6 @@ type OCSProviderServer struct { namespace string } -type onboardingTicket struct { - ID string `json:"id"` - ExpirationDate int64 `json:"expirationDate,string"` -} - func NewOCSProviderServer(ctx context.Context, namespace string) (*OCSProviderServer, error) { client, err := newClient() if err != nil { @@ -445,7 +441,7 @@ func validateTicket(ticket string, pubKey *rsa.PublicKey) error { return fmt.Errorf("failed to decode onboarding ticket: %v", err) } - var ticketData onboardingTicket + var ticketData sharedTypes.OnboardingTicket err = json.Unmarshal(message, &ticketData) if err != nil { return fmt.Errorf("failed to unmarshal onboarding ticket message. %v", err) diff --git a/services/types/types.go b/services/types/types.go new file mode 100644 index 0000000000..be67b0fdb5 --- /dev/null +++ b/services/types/types.go @@ -0,0 +1,6 @@ +package types + +type OnboardingTicket struct { + ID string `json:"id"` + ExpirationDate int64 `json:"expirationDate,string"` +} diff --git a/services/ux-backend/handlers/onboarding_tokens.go b/services/ux-backend/handlers/onboarding_tokens.go new file mode 100644 index 0000000000..0f3e6392b5 --- /dev/null +++ b/services/ux-backend/handlers/onboarding_tokens.go @@ -0,0 +1,120 @@ +package handler + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "net/http" + "os" + "time" + + "github.com/google/uuid" + "github.com/red-hat-storage/ocs-operator/v4/services/types" + "k8s.io/klog/v2" +) + +const onboardingPrivateKeyFilePath = "/etc/private-key/key" + +func OnboardingTokensHandler(w http.ResponseWriter, r *http.Request, tokenLifetimeInHours int) { + + var err error + switch r.Method { + case "POST": + + onboardingToken, err := generateOnboardingToken(tokenLifetimeInHours) + if err != nil { + klog.Errorf("failed to get onboardig token: %v", err) + w.WriteHeader(http.StatusInternalServerError) + w.Header().Set("Content-Type", "text/text") + _, err = w.Write([]byte("Failed to generate token")) + + if err != nil { + klog.Errorf("failed write data to response writer, %v", err) + } + return + } + + klog.Info("onboarding token generated successfully") + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "text/text") + + _, err = w.Write([]byte(onboardingToken)) + if err != nil { + klog.Errorf("failed write data to response writer: %v", err) + return + } + + default: + klog.Info("Only POST method should be used to send data to this endpoint /onboarding-tokens") + w.WriteHeader(http.StatusMethodNotAllowed) + w.Header().Set("Content-Type", "text/text") + _, err = w.Write([]byte(fmt.Sprintf("Unsupported method : %s", r.Method))) + if err != nil { + klog.Errorf("failed write data to response writer: %v", err) + } + return + } +} + +func generateOnboardingToken(tokenLifetimeInHours int) (string, error) { + + tokenExpirationDate := time.Now(). + Add(time.Duration(tokenLifetimeInHours) * time.Hour). + Unix() + + payload, err := json.Marshal(types.OnboardingTicket{ + ID: uuid.New().String(), + ExpirationDate: tokenExpirationDate, + }) + + if err != nil { + return "", fmt.Errorf("failed to marshal the payload: %v", err) + } + + encodedPayload := base64.StdEncoding.EncodeToString(payload) + // Before signing, we need to hash our message + // The hash is what we actually sign + msgHash := sha256.New() + _, err = msgHash.Write(payload) + if err != nil { + return "", fmt.Errorf("failed to hash onboarding token payload: %v", err) + } + + privateKey, err := readAndDecodeOnboardingPrivateKey() + if err != nil { + return "", fmt.Errorf("failed to read and decode private key: %v", err) + } + + msgHashSum := msgHash.Sum(nil) + signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, msgHashSum) + if err != nil { + return "", fmt.Errorf("failed to sign private key: %v", err) + } + + encodedSignature := base64.StdEncoding.EncodeToString(signature) + return fmt.Sprintf("%s.%s", encodedPayload, encodedSignature), nil +} + +func readAndDecodeOnboardingPrivateKey() (*rsa.PrivateKey, error) { + + pemString, err := os.ReadFile(onboardingPrivateKeyFilePath) + if err != nil { + return nil, fmt.Errorf("failed to read onboarding private key: %v", err) + } + + // In order to generate the signature, we provide a random number generator, + // our private key, the hashing algorithm that we used, and the hash sum + // of our message + Block, _ := pem.Decode(pemString) + privateKey, err := x509.ParsePKCS1PrivateKey(Block.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse private key: %v", err) + } + return privateKey, nil +} diff --git a/services/ux-backend/main.go b/services/ux-backend/main.go new file mode 100644 index 0000000000..1b073ee475 --- /dev/null +++ b/services/ux-backend/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "os" + "strconv" + + handler "github.com/red-hat-storage/ocs-operator/v4/services/ux-backend/handlers" + "k8s.io/klog/v2" +) + +type serverConfig struct { + listenPort int + tokenLifetimeInHours int +} + +func loadAndValidateServerConfig() (*serverConfig, error) { + var config serverConfig + + var err error + defaultTokenLifetimeInHours := 48 + tokenLifetimeInHoursAsString := os.Getenv("ONBOARDING_TOKEN_LIFETIME") + if tokenLifetimeInHoursAsString == "" { + klog.Infof("No user-defined token lifetime provided, defaulting to %d ", defaultTokenLifetimeInHours) + config.tokenLifetimeInHours = defaultTokenLifetimeInHours + } else if config.tokenLifetimeInHours, err = strconv.Atoi(tokenLifetimeInHoursAsString); err != nil { + return nil, fmt.Errorf("Malformed user-defined Token lifetime: %s. shutting down: %v", tokenLifetimeInHoursAsString, err) + } + + klog.Infof("generated tokens will be valid for %d hours", config.tokenLifetimeInHours) + + defaultListeningPort := 8080 + listenPortAsString := os.Getenv("UX_BACKEND_PORT") + if listenPortAsString == "" { + klog.Infof("No user-defined server listening port provided, defaulting to %d ", defaultListeningPort) + config.listenPort = defaultListeningPort + } else if config.listenPort, err = strconv.Atoi(listenPortAsString); err != nil { + return nil, fmt.Errorf("Malformed user-defined listening port: %s. shutting down: %v", listenPortAsString, err) + } + + return &config, nil +} + +func main() { + + klog.Info("Starting ux backend server") + + config, err := loadAndValidateServerConfig() + if err != nil { + klog.Errorf("failed to load server config: %v", err) + os.Exit(-1) + } + http.HandleFunc("/onboarding-tokens", func(w http.ResponseWriter, r *http.Request) { + handler.OnboardingTokensHandler(w, r, config.tokenLifetimeInHours) + + }) + + klog.Info("ux backend server listening on port ", config.listenPort) + + log.Fatal(http.ListenAndServeTLS( + fmt.Sprintf("%s%d", ":", config.listenPort), + "/etc/tls/private/tls.crt", + "/etc/tls/private/tls.key", + nil, + )) + +} diff --git a/tools/csv-merger/csv-merger.go b/tools/csv-merger/csv-merger.go index c7cfc2d1e3..ce96d216f4 100644 --- a/tools/csv-merger/csv-merger.go +++ b/tools/csv-merger/csv-merger.go @@ -22,6 +22,7 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -45,6 +46,7 @@ var ( noobaaDBContainerImage = flag.String("noobaa-db-image", "", "db container image for noobaa") ocsContainerImage = flag.String("ocs-image", "", "ocs operator container image") ocsMetricsExporterImage = flag.String("ocs-metrics-exporter-image", "", "ocs metrics exporter container image") + uxBackendOauthImage = flag.String("ux-backend-oauth-image", "", "ux backend oauth container image") ocsMustGatherImage = flag.String("ocs-must-gather-image", "", "ocs-must-gather image") rookCsiAddonsImage = flag.String("rook-csiaddons-image", "", "csi-addons container image") @@ -160,6 +162,10 @@ func unmarshalCSV(filePath string) *csvv1.ClusterServiceVersion { Name: "ONBOARDING_SECRET_GENERATOR_IMAGE", Value: *ocsContainerImage, }, + { + Name: "UX_BACKEND_SERVER_IMAGE", + Value: *ocsContainerImage, + }, { Name: util.OperatorNamespaceEnvVar, ValueFrom: &corev1.EnvVarSource{ @@ -546,6 +552,12 @@ func generateUnifiedCSV() *csvv1.ClusterServiceVersion { } + uxBackendStrategySpec := csvv1.StrategyDeploymentSpec{ + Name: "ux-backend-server", + Spec: getUXBackendServerDeployment(), + } + templateStrategySpec.DeploymentSpecs = append(templateStrategySpec.DeploymentSpecs, uxBackendStrategySpec) + // Add tolerations to deployments for i := range templateStrategySpec.DeploymentSpecs { d := &templateStrategySpec.DeploymentSpecs[i] @@ -825,6 +837,12 @@ func injectCSVRelatedImages(r *unstructured.Unstructured) error { "image": *ocsMetricsExporterImage, }) } + if *uxBackendOauthImage != "" { + relatedImages = append(relatedImages, map[string]interface{}{ + "name": "ux-backend-oauth-image", + "image": *uxBackendOauthImage, + }) + } return unstructured.SetNestedSlice(r.Object, relatedImages, "spec", "relatedImages") } @@ -910,6 +928,124 @@ func copyManifests() { } } +func getUXBackendServerDeployment() appsv1.DeploymentSpec { + replica := int32(1) + ptrToTrue := true + deployment := appsv1.DeploymentSpec{ + Replicas: &replica, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app.kubernetes.io/component": "ux-backend-server", + "app.kubernetes.io/name": "ux-backend-server", + "app": "ux-backend-server", + }, + }, + Strategy: appsv1.DeploymentStrategy{Type: appsv1.RecreateDeploymentStrategyType}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app.kubernetes.io/component": "ux-backend-server", + "app.kubernetes.io/name": "ux-backend-server", + "app": "ux-backend-server", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "ux-backend-server", + VolumeMounts: []corev1.VolumeMount{ + { + Name: "onboarding-private-key", + MountPath: "/etc/private-key", + }, + { + Name: "ux-cert-secret", + MountPath: "/etc/tls/private", + }, + }, + Image: *ocsContainerImage, + ImagePullPolicy: "IfNotPresent", + Command: []string{"/usr/local/bin/ux-backend-server"}, + Ports: []corev1.ContainerPort{ + { + ContainerPort: 8080, + }, + }, + Env: []corev1.EnvVar{ + { + Name: "ONBOARDING_TOKEN_LIFETIME", + Value: os.Getenv("ONBOARDING_TOKEN_LIFETIME"), + }, + { + Name: "UX_BACKEND_PORT", + Value: os.Getenv("UX_BACKEND_PORT"), + }, + }, + }, + { + Name: "oauth-proxy", + VolumeMounts: []corev1.VolumeMount{ + { + Name: "ux-proxy-secret", + MountPath: "/etc/proxy/secrets", + }, + { + Name: "ux-cert-secret", + MountPath: "/etc/tls/private", + }, + }, + Image: *uxBackendOauthImage, + ImagePullPolicy: "IfNotPresent", + Args: []string{"-provider=openshift", + "-https-address=:8888", + "-http-address=", "-email-domain=*", + "-upstream=https://localhost:8080/onboarding-tokens", + "-tls-cert=/etc/tls/private/tls.crt", + "-tls-key=/etc/tls/private/tls.key", + "-cookie-secret-file=/etc/proxy/secrets/session_secret", + "-openshift-service-account=ux-backend-server", + "-openshift-ca=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"}, + Ports: []corev1.ContainerPort{ + { + ContainerPort: 8888, + }, + }, + }, + }, + Volumes: []corev1.Volume{ + { + Name: "onboarding-private-key", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "onboarding-private-key", + Optional: &ptrToTrue, + }, + }, + }, + { + Name: "ux-proxy-secret", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "ux-backend-proxy", + }, + }, + }, + { + Name: "ux-cert-secret", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "ux-cert-secret", + }, + }, + }, + }, + ServiceAccountName: "ux-backend-server", + }, + }, + } + return deployment +} + func main() { flag.Parse() @@ -935,6 +1071,9 @@ func main() { log.Fatal("--crds-directory is required") } else if *outputDir == "" { log.Fatal("--olm-bundle-directory is required") + } else if *uxBackendOauthImage == "" { + // this image can be used quay.io/openshift/origin-oauth-proxy:4.14 + log.Fatal("--ux-backend-oauth-image is required") } // start with a fresh output directory if it already exists