diff --git a/Dockerfile b/Dockerfile index bba2b4db24..344c09d30d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,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 # Build stage 2 @@ -19,6 +20,7 @@ FROM registry.access.redhat.com/ubi9/ubi-minimal 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/ RUN chmod +x /usr/local/bin/ocs-operator /usr/local/bin/provider-api diff --git a/controllers/storagecluster/provider_server.go b/controllers/storagecluster/provider_server.go index 3b21f99bfe..5dc51e8549 100644 --- a/controllers/storagecluster/provider_server.go +++ b/controllers/storagecluster/provider_server.go @@ -10,9 +10,11 @@ import ( "go.uber.org/multierr" appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -24,8 +26,11 @@ import ( ) const ( - ocsProviderServerName = "ocs-provider-server" - providerAPIServerImage = "PROVIDER_API_SERVER_IMAGE" + ocsProviderServerName = "ocs-provider-server" + providerAPIServerImage = "PROVIDER_API_SERVER_IMAGE" + onboardingSecretGeneratorImage = "ONBOARDING_SECRET_GENERATOR_IMAGE" + onboardingJobName = "onboarding-secret-generator" + onboardingTicketPublicKeySecretName = "onboarding-ticket-key" ocsProviderServicePort = int32(50051) ocsProviderServiceNodePort = int32(31659) @@ -63,6 +68,12 @@ func (o *ocsProviderServer) ensureCreated(r *StorageClusterReconciler, instance return res, nil } + if res, err := o.createJob(r, instance); err != nil { + return reconcile.Result{}, err + } else if !res.IsZero() { + return res, nil + } + return reconcile.Result{}, nil } @@ -434,3 +445,64 @@ func RandomString(l int) string { return string(bytes) } + +func getOnboardingJobObject(instance *ocsv1.StorageCluster) *batchv1.Job { + + return &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: onboardingJobName, + Namespace: instance.Namespace, + }, + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyOnFailure, + ServiceAccountName: onboardingJobName, + Containers: []corev1.Container{ + { + Name: onboardingJobName, + Image: os.Getenv(onboardingSecretGeneratorImage), + Command: []string{"/usr/local/bin/onboarding-secret-generator"}, + Env: []corev1.EnvVar{ + { + Name: util.OperatorNamespaceEnvVar, + Value: os.Getenv(util.OperatorNamespaceEnvVar), + }, + }, + }, + }, + }, + }, + }, + } +} + +func (o *ocsProviderServer) createJob(r *StorageClusterReconciler, instance *ocsv1.StorageCluster) (reconcile.Result, error) { + if os.Getenv(onboardingSecretGeneratorImage) == "" { + err := fmt.Errorf("onboardingSecretGeneratorImage env var is not set.") + r.Log.Error(err, "No value for env variable", onboardingSecretGeneratorImage) + return reconcile.Result{}, err + } else { + + actualSecret := &corev1.Secret{} + // Creating the job only if public is not found + err := r.Client.Get(context.Background(), types.NamespacedName{Name: onboardingTicketPublicKeySecretName, + Namespace: instance.Namespace}, actualSecret) + + if err != nil && errors.IsNotFound(err) { + onboardingSecretGeneratorJob := getOnboardingJobObject(instance) + err = r.Client.Create(context.Background(), onboardingSecretGeneratorJob) + if err != nil { + r.Log.Error(err, "Failed to create job.") + return reconcile.Result{}, err + } + } + if err != nil { + r.Log.Error(err, "failed to ensure secret") + return reconcile.Result{}, err + } + + } + r.Log.Info("Job is running as desired") + return reconcile.Result{}, nil +} diff --git a/controllers/storagecluster/storagecluster_controller.go b/controllers/storagecluster/storagecluster_controller.go index 1f3e2feb41..3e285a1468 100644 --- a/controllers/storagecluster/storagecluster_controller.go +++ b/controllers/storagecluster/storagecluster_controller.go @@ -184,6 +184,14 @@ func (r *StorageClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { }, ) + onboardingSecretPredicates := builder.WithPredicates( + predicate.NewPredicateFuncs( + func(client client.Object) bool { + return client.GetName() == onboardingTicketPublicKeySecretName + }, + ), + ) + builder := ctrl.NewControllerManagedBy(mgr). For(&ocsv1.StorageCluster{}, builder.WithPredicates(scPredicate)). Owns(&cephv1.CephCluster{}). @@ -199,6 +207,11 @@ func (r *StorageClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { }, }, enqueueStorageClusterRequest, + ). + Watches( + &corev1.Secret{}, + enqueueStorageClusterRequest, + onboardingSecretPredicates, ) if os.Getenv("SKIP_NOOBAA_CRD_WATCH") != "true" { builder.Owns(&nbv1.NooBaa{}) diff --git a/deploy/ocs-operator/manifests/ocs-operator.clusterserviceversion.yaml b/deploy/ocs-operator/manifests/ocs-operator.clusterserviceversion.yaml index dd6474477d..efbebd2f7d 100644 --- a/deploy/ocs-operator/manifests/ocs-operator.clusterserviceversion.yaml +++ b/deploy/ocs-operator/manifests/ocs-operator.clusterserviceversion.yaml @@ -3077,6 +3077,8 @@ spec: value: docker.io/centos/postgresql-12-centos8 - name: PROVIDER_API_SERVER_IMAGE value: quay.io/ocs-dev/ocs-operator:latest + - name: ONBOARDING_SECRET_GENERATOR_IMAGE + value: quay.io/ocs-dev/ocs-operator:latest - name: OPERATOR_NAMESPACE valueFrom: fieldRef: diff --git a/hack/source-manifests.sh b/hack/source-manifests.sh index 1236cde8e9..44c1544f57 100755 --- a/hack/source-manifests.sh +++ b/hack/source-manifests.sh @@ -69,7 +69,7 @@ function gen_ocs_csv() { pushd config/manager $KUSTOMIZE edit set image ocs-dev/ocs-operator="$OCS_IMAGE" popd - $KUSTOMIZE build config/manifests/ocs-operator | $OPERATOR_SDK generate bundle -q --overwrite=false --output-dir deploy/ocs-operator --kustomize-dir config/manifests/ocs-operator --package ocs-operator --version "$CSV_VERSION" + $KUSTOMIZE build config/manifests/ocs-operator | $OPERATOR_SDK generate bundle -q --overwrite=false --output-dir deploy/ocs-operator --kustomize-dir config/manifests/ocs-operator --package ocs-operator --version "$CSV_VERSION" --extra-service-accounts=onboarding-secret-generator mv deploy/ocs-operator/manifests/*clusterserviceversion.yaml $OCS_CSV cp config/crd/bases/* $ocs_crds_outdir } diff --git a/onboarding/main.go b/onboarding/main.go new file mode 100644 index 0000000000..5988a708d9 --- /dev/null +++ b/onboarding/main.go @@ -0,0 +1,124 @@ +package main + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "os" + + "github.com/red-hat-storage/ocs-operator/v4/controllers/util" + "golang.org/x/net/context" + corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/klog/v2" + runtime "sigs.k8s.io/controller-runtime/pkg/client/config" +) + +const ( + onboardingTicketPublicKeySecretName = "onboarding-ticket-key" //Name of existing public key which is used ocs-operator + onboardingTicketPrivateKeySecretName = "onboarding-ticket-private-key" + serviceAccountName = "onboarding-secret-generator" +) + +func main() { + clientset, err := newClient() + if err != nil { + klog.Error(err, "failed to create controller-runtime client") + return + } + + operatorNamespace, err := util.GetOperatorNamespace() + if err != nil { + klog.Error(err, "unable to get operator namespace") + os.Exit(1) + } + + // 1. Check public key secret exist or not + _, err = clientset.CoreV1().Secrets(operatorNamespace).Get(context.TODO(), onboardingTicketPublicKeySecretName, metav1.GetOptions{}) + + if err != nil && kerrors.IsNotFound(err) { + // Generate RSA key. + var err error + privateKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + klog.Error(err, "unable to generate private") + os.Exit(1) + } + + publicKey := &privateKey.PublicKey + // Export the keys to pem string + privatePem := convertRsaPrivateKeyAsPemStr(privateKey) + publicPem, err := convertRsaPublicKeyAsPemStr(publicKey) + + if err != nil { + klog.Error(err, "failed to convert public key to pem str") + os.Exit(1) + } + + privateSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: onboardingTicketPrivateKeySecretName, + Namespace: operatorNamespace, + Annotations: map[string]string{"kubernetes.io/service-account.name": serviceAccountName}, + }, + Type: "kubernetes.io/service-account-token", + StringData: map[string]string{ + "key": privatePem, + }, + } + + _, err = clientset.CoreV1().Secrets(operatorNamespace).Create(context.Background(), privateSecret, metav1.CreateOptions{}) + + if err != nil { + klog.Error(err, "Failed to create private secret.") + os.Exit(1) + } + publicSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: onboardingTicketPublicKeySecretName, + Namespace: operatorNamespace, + }, + StringData: map[string]string{ + "key": publicPem, + }, + } + + _, err = clientset.CoreV1().Secrets(operatorNamespace).Create(context.Background(), publicSecret, metav1.CreateOptions{}) + if err != nil { + klog.Error(err, "Failed to create public secret.") + os.Exit(1) + } + + } + +} + +func newClient() (*kubernetes.Clientset, error) { + config := runtime.GetConfigOrDie() + clientset, err := kubernetes.NewForConfig(config) + + if err != nil { + klog.Error(err, "failed to get clientset") + } + + return clientset, nil +} + +func convertRsaPrivateKeyAsPemStr(privateKey *rsa.PrivateKey) string { + privteKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) + privateKeyPem := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privteKeyBytes}) + return string(privateKeyPem) +} + +func convertRsaPublicKeyAsPemStr(publicKey *rsa.PublicKey) (string, error) { + publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey) + if err != nil { + return "", err + } + publicKeyPem := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: publicKeyBytes}) + + return string(publicKeyPem), nil +} diff --git a/rbac/onboarding-secret-generator-binding.yaml b/rbac/onboarding-secret-generator-binding.yaml new file mode 100644 index 0000000000..428418f1fe --- /dev/null +++ b/rbac/onboarding-secret-generator-binding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: onboarding-secret-generator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: onboarding-secret-generator +subjects: +- kind: ServiceAccount + name: onboarding-secret-generator + namespace: openshift-storage diff --git a/rbac/onboarding-secret-generator-role.yaml b/rbac/onboarding-secret-generator-role.yaml new file mode 100644 index 0000000000..06146068f5 --- /dev/null +++ b/rbac/onboarding-secret-generator-role.yaml @@ -0,0 +1,13 @@ +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: onboarding-secret-generator +rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - create diff --git a/rbac/onboarding-secret-generator-sa.yaml b/rbac/onboarding-secret-generator-sa.yaml new file mode 100644 index 0000000000..d2e935ca0e --- /dev/null +++ b/rbac/onboarding-secret-generator-sa.yaml @@ -0,0 +1,5 @@ +kind: ServiceAccount +apiVersion: v1 +metadata: + name: onboarding-secret-generator +type: kubernetes.io/service-account-token diff --git a/tools/csv-merger/csv-merger.go b/tools/csv-merger/csv-merger.go index cc418fff1e..c7cfc2d1e3 100644 --- a/tools/csv-merger/csv-merger.go +++ b/tools/csv-merger/csv-merger.go @@ -156,6 +156,10 @@ func unmarshalCSV(filePath string) *csvv1.ClusterServiceVersion { Name: "PROVIDER_API_SERVER_IMAGE", Value: *ocsContainerImage, }, + { + Name: "ONBOARDING_SECRET_GENERATOR_IMAGE", + Value: *ocsContainerImage, + }, { Name: util.OperatorNamespaceEnvVar, ValueFrom: &corev1.EnvVarSource{