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..cccc6265f1 100644 --- a/controllers/storagecluster/provider_server.go +++ b/controllers/storagecluster/provider_server.go @@ -10,22 +10,28 @@ 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/runtime" + "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" "sigs.k8s.io/controller-runtime/pkg/reconcile" + openshiftv1 "github.com/openshift/api/template/v1" ocsv1 "github.com/red-hat-storage/ocs-operator/v4/api/v1" "github.com/red-hat-storage/ocs-operator/v4/controllers/util" "github.com/red-hat-storage/ocs-operator/v4/services/provider/server" ) const ( - ocsProviderServerName = "ocs-provider-server" - providerAPIServerImage = "PROVIDER_API_SERVER_IMAGE" + ocsProviderServerName = "ocs-provider-server" + providerAPIServerImage = "PROVIDER_API_SERVER_IMAGE" + onboardingSecretGeneratorImage = "ONBOARDING_SECRET_GENERATOR_IMAGE" + jobTemplateName = "onboarding-secret-generator" ocsProviderServicePort = int32(50051) ocsProviderServiceNodePort = int32(31659) @@ -63,6 +69,17 @@ func (o *ocsProviderServer) ensureCreated(r *StorageClusterReconciler, instance return res, nil } + onboardingTokenJob := &batchv1.Job{} + + err := r.Client.Get(context.TODO(), types.NamespacedName{Namespace: instance.Namespace, Name: jobTemplateName}, onboardingTokenJob) + + if err != nil && errors.IsNotFound(err) { + r.Log.Info("onboarding job not fund") + createOnboardingSecretTemplate(instance) + } else if onboardingTokenJob.Status.Conditions[len(onboardingTokenJob.Status.Conditions)-1].Type != batchv1.JobComplete { + createOnboardingSecretTemplate(instance) + } + return reconcile.Result{}, nil } @@ -434,3 +451,55 @@ func RandomString(l int) string { return string(bytes) } + +func createOnboardingSecretTemplate(sc *ocsv1.StorageCluster) *openshiftv1.Template { + + return &openshiftv1.Template{ + ObjectMeta: metav1.ObjectMeta{ + Name: jobTemplateName, + Namespace: sc.Namespace, + }, + Objects: []runtime.RawExtension{ + { + Object: onboardingSecretGenratorJob(sc, jobTemplateName), + }, + }, + } +} + +func onboardingSecretGenratorJob(sc *ocsv1.StorageCluster, jobTemplateName string) *batchv1.Job { + + // Annotation template.alpha.openshift.io/wait-for-ready ensures template readiness + annotations := map[string]string{ + "template.alpha.openshift.io/wait-for-ready": "true", + } + + job := &batchv1.Job{ + TypeMeta: metav1.TypeMeta{ + Kind: "Job", + APIVersion: "template.openshift.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: jobTemplateName, + Namespace: sc.Namespace, + Annotations: annotations, + }, + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyOnFailure, + ServiceAccountName: jobTemplateName, + Containers: []corev1.Container{ + { + Name: jobTemplateName, + Image: os.Getenv(onboardingSecretGeneratorImage), + Command: []string{"/usr/local/bin/onboarding-secret-generator"}, + }, + }, + }, + }, + }, + } + + return job +} diff --git a/deploy/ocs-operator/manifests/ocs-operator.clusterserviceversion.yaml b/deploy/ocs-operator/manifests/ocs-operator.clusterserviceversion.yaml index a997793373..273e8068c7 100644 --- a/deploy/ocs-operator/manifests/ocs-operator.clusterserviceversion.yaml +++ b/deploy/ocs-operator/manifests/ocs-operator.clusterserviceversion.yaml @@ -3086,6 +3086,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/deploy/ocs-operator/manifests/onboarding-secret-generator-binding.yaml b/deploy/ocs-operator/manifests/onboarding-secret-generator-binding.yaml new file mode 100644 index 0000000000..3b19bc5348 --- /dev/null +++ b/deploy/ocs-operator/manifests/onboarding-secret-generator-binding.yaml @@ -0,0 +1,11 @@ +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: onboarding-secret-generator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: onboarding-secret-generator +subjects: +- kind: ServiceAccount + name: onboarding-secret-generator diff --git a/deploy/ocs-operator/manifests/onboarding-secret-generator-role.yaml b/deploy/ocs-operator/manifests/onboarding-secret-generator-role.yaml new file mode 100644 index 0000000000..06146068f5 --- /dev/null +++ b/deploy/ocs-operator/manifests/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/hack/source-manifests.sh b/hack/source-manifests.sh index 73fa2299bd..5261ecce91 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" --extra-service-accounts=ocs-metrics-exporter + $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=ocs-metrics-exporter,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..0334fdf1b8 --- /dev/null +++ b/onboarding/main.go @@ -0,0 +1,159 @@ +package main + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "os" + + "github.com/red-hat-storage/ocs-operator/v4/controllers/util" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" + errruntime "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + apiruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/klog/v2" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" +) + +const ( + onboardingTicketPublicKeySecretName = "onboarding-ticket-public-key" + onboardingTicketPrivateKeySecretName = "onboarding-ticket-private-key" +) + +var ( + ctx = context.Background() + serverNamespace = os.Getenv(util.OperatorNamespaceEnvVar) + privatePem, publicPem string +) + +func createSecret(cl client.Client, onboardingTicketSecretName, pemStr string) { + + desiredSecret := getSecret(onboardingTicketSecretName, pemStr) + actualSecret := &corev1.Secret{} + + err := cl.Get(ctx, client.ObjectKeyFromObject(desiredSecret), actualSecret) + + if err != nil && errruntime.IsNotFound(err) { + err = cl.Create(ctx, desiredSecret) + if err != nil { + klog.Error(err, "Failed to create secret.") + } + } else if err != nil { + klog.Error(err, "Failed to get secret.") + + } +} + +func getSecret(onboardingTicketSecretName, pemStr string) *corev1.Secret { + + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: onboardingTicketSecretName, + Namespace: serverNamespace, + }, + Immutable: ptr.To(true), + StringData: map[string]string{ + "data": pemStr, + }, + } +} + +func newClient() (client.Client, error) { + scheme := apiruntime.NewScheme() + utilruntime.Must(corev1.AddToScheme(scheme)) + + config, err := config.GetConfig() + if err != nil { + klog.Error(err, "failed to get config check if KUBECOFIG is set.") + return nil, err + } + client, err := client.New(config, client.Options{Scheme: scheme}) + if err != nil { + klog.Error(err, "failed to create controller-runtime client") + return nil, err + } + + return client, nil +} + +func convertRsaPrivateKeyAsPemStr(privateKey *rsa.PrivateKey) string { + privteKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) + privateKeyPem := pem.EncodeToMemory( + &pem.Block{ + Type: "RSA 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: "RSA PUBLIC KEY", + Bytes: publicKeyBytes, + }, + ) + + return string(publicKeyPem), nil +} + +func genratePrivateAndPublicKeys() { + + // Generate RSA key. + var err error + privateKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + panic(err) + } + + publicKey := &privateKey.PublicKey + + // Export the keys to pem string + //similar to key.pem & pubkey.pem + privatePem = convertRsaPrivateKeyAsPemStr(privateKey) + publicPem, err = convertRsaPublicKeyAsPemStr(publicKey) + + if err != nil { + panic(err) + } + +} + +func main() { + + client, err := newClient() + + if err != nil { + klog.Error(err, "failed to create controller-runtime client") + return + } + + // 1. Check public key secret exist or not + secret := &v1.Secret{} + err = client.Get(ctx, types.NamespacedName{Name: onboardingTicketPublicKeySecretName, + Namespace: serverNamespace}, secret) + + if err != nil { + if errruntime.IsNotFound(err) { + genratePrivateAndPublicKeys() + createSecret(client, onboardingTicketPublicKeySecretName, publicPem) + createSecret(client, onboardingTicketPrivateKeySecretName, privatePem) + } else { + klog.Error(err, "failed to get secret. ") + } + + } + +} diff --git a/rbac/onboarding-secret-generator-binding.yaml b/rbac/onboarding-secret-generator-binding.yaml new file mode 100644 index 0000000000..3b19bc5348 --- /dev/null +++ b/rbac/onboarding-secret-generator-binding.yaml @@ -0,0 +1,11 @@ +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: onboarding-secret-generator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: onboarding-secret-generator +subjects: +- kind: ServiceAccount + name: onboarding-secret-generator 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/tools/csv-merger/csv-merger.go b/tools/csv-merger/csv-merger.go index 00736539b5..ec54141ab5 100644 --- a/tools/csv-merger/csv-merger.go +++ b/tools/csv-merger/csv-merger.go @@ -154,6 +154,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{