github.com/IBM-Blockchain/fabric-operator@v1.0.4/pkg/initializer/common/enroller/hsmdaemonenroller.go (about)

     1  /*
     2   * Copyright contributors to the Hyperledger Fabric Operator project
     3   *
     4   * SPDX-License-Identifier: Apache-2.0
     5   *
     6   * Licensed under the Apache License, Version 2.0 (the "License");
     7   * you may not use this file except in compliance with the License.
     8   * You may obtain a copy of the License at:
     9   *
    10   * 	  http://www.apache.org/licenses/LICENSE-2.0
    11   *
    12   * Unless required by applicable law or agreed to in writing, software
    13   * distributed under the License is distributed on an "AS IS" BASIS,
    14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15   * See the License for the specific language governing permissions and
    16   * limitations under the License.
    17   */
    18  
    19  package enroller
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"path/filepath"
    25  	"time"
    26  
    27  	current "github.com/IBM-Blockchain/fabric-operator/api/v1beta1"
    28  	"github.com/IBM-Blockchain/fabric-operator/pkg/initializer/common/config"
    29  	k8sclient "github.com/IBM-Blockchain/fabric-operator/pkg/k8s/controllerclient"
    30  	jobv1 "github.com/IBM-Blockchain/fabric-operator/pkg/manager/resources/job"
    31  	"github.com/IBM-Blockchain/fabric-operator/pkg/util"
    32  	"github.com/pkg/errors"
    33  
    34  	batchv1 "k8s.io/api/batch/v1"
    35  	corev1 "k8s.io/api/core/v1"
    36  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    37  	"k8s.io/apimachinery/pkg/runtime"
    38  	"k8s.io/apimachinery/pkg/types"
    39  	"k8s.io/apimachinery/pkg/util/wait"
    40  )
    41  
    42  // HSMDaemonEnroller is responsible for enrolling with CAs to generate cryptographic materical
    43  // for fabric nodes
    44  type HSMDaemonEnroller struct {
    45  	CAClient HSMCAClient
    46  	Client   k8sclient.Client
    47  	Instance Instance
    48  	Timeouts HSMEnrollJobTimeouts
    49  	Scheme   *runtime.Scheme
    50  	Config   *config.HSMConfig
    51  }
    52  
    53  // NewHSMDaemonEnroller initializes and returns a pointer to HSMDaemonEnroller
    54  func NewHSMDaemonEnroller(cfg *current.Enrollment, instance Instance, caclient HSMCAClient, client k8sclient.Client, scheme *runtime.Scheme, timeouts HSMEnrollJobTimeouts, hsmConfig *config.HSMConfig) *HSMDaemonEnroller {
    55  	return &HSMDaemonEnroller{
    56  		CAClient: caclient,
    57  		Client:   client,
    58  		Instance: instance,
    59  		Scheme:   scheme,
    60  		Timeouts: timeouts,
    61  		Config:   hsmConfig,
    62  	}
    63  }
    64  
    65  // GetEnrollmentRequest returns the enrollment request defined on the ca client
    66  func (e *HSMDaemonEnroller) GetEnrollmentRequest() *current.Enrollment {
    67  	return e.CAClient.GetEnrollmentRequest()
    68  }
    69  
    70  // ReadKey is no-op method on HSM
    71  func (e *HSMDaemonEnroller) ReadKey() ([]byte, error) {
    72  	return nil, nil
    73  }
    74  
    75  // PingCA uses the ca client do ping the CA
    76  func (e *HSMDaemonEnroller) PingCA(timeout time.Duration) error {
    77  	return e.CAClient.PingCA(timeout)
    78  }
    79  
    80  // Enroll reaches out the CA to get back a signed certificate
    81  func (e *HSMDaemonEnroller) Enroll() (*config.Response, error) {
    82  	log.Info(fmt.Sprintf("Enrolling using HSM Daemon"))
    83  	// Deleting CA client config is an unfortunate requirement since the ca client
    84  	// config map was not properly deleted after a successfull reenrollment request.
    85  	// This is problematic when recreating a resource with same name, as it will
    86  	// try to use old settings in the config map, which might no longer apply, thus
    87  	// it must be removed if found before proceeding.
    88  	if err := deleteCAClientConfig(e.Client, e.Instance); err != nil {
    89  		return nil, err
    90  	}
    91  
    92  	e.CAClient.SetHSMLibrary(filepath.Join("/hsm/lib", filepath.Base(e.Config.Library.FilePath)))
    93  	if err := createRootTLSSecret(e.Client, e.CAClient, e.Scheme, e.Instance); err != nil {
    94  		return nil, err
    95  	}
    96  
    97  	if err := createCAClientConfig(e.Client, e.CAClient, e.Scheme, e.Instance); err != nil {
    98  		return nil, err
    99  	}
   100  
   101  	job := e.initHSMJob(e.Instance, e.Timeouts)
   102  	if err := e.Client.Create(context.TODO(), job.Job, k8sclient.CreateOption{
   103  		Owner:  e.Instance,
   104  		Scheme: e.Scheme,
   105  	}); err != nil {
   106  		return nil, errors.Wrap(err, "failed to create HSM ca initialization job")
   107  	}
   108  	log.Info(fmt.Sprintf("Job '%s' created", job.GetName()))
   109  
   110  	if err := job.WaitUntilActive(e.Client); err != nil {
   111  		return nil, err
   112  	}
   113  	log.Info(fmt.Sprintf("Job '%s' active", job.GetName()))
   114  
   115  	if err := job.WaitUntilContainerFinished(e.Client, CertGen); err != nil {
   116  		return nil, err
   117  	}
   118  	log.Info(fmt.Sprintf("Job '%s' finished", job.GetName()))
   119  
   120  	status, err := job.ContainerStatus(e.Client, CertGen)
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  
   125  	log.Info(fmt.Sprintf("Job status at finish '%s'", status))
   126  
   127  	switch status {
   128  	case jobv1.FAILED:
   129  		return nil, fmt.Errorf("Job '%s' finished unsuccessfully, not cleaning up pods to allow for error evaluation", job.GetName())
   130  	case jobv1.COMPLETED:
   131  		if err := job.Delete(e.Client); err != nil {
   132  			return nil, err
   133  		}
   134  
   135  		if err := deleteRootTLSSecret(e.Client, e.Instance); err != nil {
   136  			return nil, err
   137  		}
   138  
   139  		if err := deleteCAClientConfig(e.Client, e.Instance); err != nil {
   140  			return nil, err
   141  		}
   142  	}
   143  
   144  	name := fmt.Sprintf("ecert-%s-signcert", e.Instance.GetName())
   145  	err = wait.Poll(2*time.Second, 30*time.Second, func() (bool, error) {
   146  		sec := &corev1.Secret{}
   147  		log.Info(fmt.Sprintf("Waiting for secret '%s' to be created", name))
   148  		err = e.Client.Get(context.TODO(), types.NamespacedName{
   149  			Name:      name,
   150  			Namespace: e.Instance.GetNamespace(),
   151  		}, sec)
   152  		if err != nil {
   153  			return false, nil
   154  		}
   155  
   156  		return true, nil
   157  	})
   158  	if err != nil {
   159  		return nil, fmt.Errorf("failed to create secret '%s'", name)
   160  	}
   161  
   162  	if err := setControllerReferences(e.Client, e.Scheme, e.Instance); err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	return &config.Response{}, nil
   167  }
   168  
   169  const (
   170  	// HSMClient is the name of container that contain the HSM client library
   171  	HSMClient = "hsm-client"
   172  	// CertGen is the name of container that runs the command to generate the certificate for the CA
   173  	CertGen = "certgen"
   174  )
   175  
   176  func (e *HSMDaemonEnroller) initHSMJob(instance Instance, timeouts HSMEnrollJobTimeouts) *jobv1.Job {
   177  	hsmConfig := e.Config
   178  	req := e.CAClient.GetEnrollmentRequest()
   179  
   180  	hsmLibraryPath := hsmConfig.Library.FilePath
   181  	hsmLibraryName := filepath.Base(hsmLibraryPath)
   182  
   183  	jobName := fmt.Sprintf("%s-enroll", instance.GetName())
   184  
   185  	f := false
   186  	t := true
   187  	user := int64(0)
   188  	backoffLimit := int32(0)
   189  	mountPath := "/shared"
   190  	pvcVolumeName := fmt.Sprintf("%s-pvc-volume", instance.GetName())
   191  
   192  	k8sJob := &batchv1.Job{
   193  		ObjectMeta: metav1.ObjectMeta{
   194  			Name:      jobName,
   195  			Namespace: instance.GetNamespace(),
   196  			Labels: map[string]string{
   197  				"name":  jobName,
   198  				"owner": instance.GetName(),
   199  			},
   200  		},
   201  		Spec: batchv1.JobSpec{
   202  			BackoffLimit: &backoffLimit,
   203  			Template: corev1.PodTemplateSpec{
   204  				Spec: corev1.PodSpec{
   205  					ServiceAccountName: instance.GetName(),
   206  					ImagePullSecrets:   util.AppendImagePullSecretIfMissing(instance.GetPullSecrets(), hsmConfig.BuildPullSecret()),
   207  					RestartPolicy:      corev1.RestartPolicyNever,
   208  					InitContainers: []corev1.Container{
   209  						{
   210  							Name:            HSMClient,
   211  							Image:           hsmConfig.Library.Image,
   212  							ImagePullPolicy: corev1.PullAlways,
   213  							Command: []string{
   214  								"sh",
   215  								"-c",
   216  								fmt.Sprintf("mkdir -p %s/hsm && dst=\"%s/hsm/%s\" && echo \"Copying %s to ${dst}\" && mkdir -p $(dirname $dst) && cp -r %s $dst", mountPath, mountPath, hsmLibraryName, hsmLibraryPath, hsmLibraryPath),
   217  							},
   218  							SecurityContext: &corev1.SecurityContext{
   219  								RunAsUser:                &user,
   220  								RunAsNonRoot:             &f,
   221  								AllowPrivilegeEscalation: &t,
   222  							},
   223  							VolumeMounts: []corev1.VolumeMount{
   224  								{
   225  									Name:      "shared",
   226  									MountPath: mountPath,
   227  								},
   228  							},
   229  							Resources: instance.GetResource(current.INIT),
   230  						},
   231  					},
   232  					Containers: []corev1.Container{
   233  						{
   234  							Name:            CertGen,
   235  							Image:           instance.EnrollerImage(),
   236  							ImagePullPolicy: corev1.PullAlways,
   237  							SecurityContext: &corev1.SecurityContext{
   238  								RunAsUser:  &user,
   239  								Privileged: &t,
   240  							},
   241  							Env: hsmConfig.GetEnvs(),
   242  							Command: []string{
   243  								"sh",
   244  								"-c",
   245  							},
   246  							Args: []string{
   247  								fmt.Sprintf(config.DAEMON_CHECK_CMD+" && /usr/local/bin/enroller node enroll %s %s %s %s %s %s %s %s %s", e.CAClient.GetHomeDir(), "/tmp/fabric-ca-client-config.yaml", req.CAHost, req.CAPort, req.CAName, instance.GetName(), instance.GetNamespace(), req.EnrollID, req.EnrollSecret),
   248  							},
   249  							VolumeMounts: []corev1.VolumeMount{
   250  								{
   251  									Name:      "tlscertfile",
   252  									MountPath: fmt.Sprintf("%s/tlsCert.pem", e.CAClient.GetHomeDir()),
   253  									SubPath:   "tlsCert.pem",
   254  								},
   255  								{
   256  									Name:      "clientconfig",
   257  									MountPath: fmt.Sprintf("/tmp/%s", "fabric-ca-client-config.yaml"),
   258  									SubPath:   "fabric-ca-client-config.yaml",
   259  								},
   260  								{
   261  									Name:      "shared",
   262  									MountPath: "/hsm/lib",
   263  									SubPath:   "hsm",
   264  								},
   265  								{
   266  									Name:      "shared",
   267  									MountPath: "/shared",
   268  								},
   269  							},
   270  						},
   271  					},
   272  					Volumes: []corev1.Volume{
   273  						{
   274  							Name: "shared",
   275  							VolumeSource: corev1.VolumeSource{
   276  								EmptyDir: &corev1.EmptyDirVolumeSource{
   277  									Medium: corev1.StorageMediumMemory,
   278  								},
   279  							},
   280  						},
   281  						{
   282  							Name: "tlscertfile",
   283  							VolumeSource: corev1.VolumeSource{
   284  								Secret: &corev1.SecretVolumeSource{
   285  									SecretName: fmt.Sprintf("%s-init-roottls", instance.GetName()),
   286  								},
   287  							},
   288  						},
   289  						{
   290  							Name: "clientconfig",
   291  							VolumeSource: corev1.VolumeSource{
   292  								ConfigMap: &corev1.ConfigMapVolumeSource{
   293  									LocalObjectReference: corev1.LocalObjectReference{
   294  										Name: fmt.Sprintf("%s-init-config", instance.GetName()),
   295  									},
   296  								},
   297  							},
   298  						},
   299  						{
   300  							Name: pvcVolumeName,
   301  							VolumeSource: corev1.VolumeSource{
   302  								PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
   303  									ClaimName: instance.PVCName(),
   304  								},
   305  							},
   306  						},
   307  					},
   308  				},
   309  			},
   310  		},
   311  	}
   312  
   313  	job := jobv1.New(k8sJob, &jobv1.Timeouts{
   314  		WaitUntilActive:   timeouts.JobStart.Get(),
   315  		WaitUntilFinished: timeouts.JobCompletion.Get(),
   316  	})
   317  
   318  	job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, hsmConfig.GetVolumes()...)
   319  	job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, hsmConfig.GetVolumeMounts()...)
   320  
   321  	// If daemon settings are configured in HSM config, create a sidecar that is running the daemon image
   322  	if e.Config.Daemon != nil {
   323  		// Certain token information requires to be stored in persistent store, the administrator
   324  		// responsible for configuring HSM sets the HSM config to point to the path where the PVC
   325  		// needs to be mounted.
   326  		var pvcMount *corev1.VolumeMount
   327  		for _, vm := range e.Config.MountPaths {
   328  			if vm.UsePVC {
   329  				pvcMount = &corev1.VolumeMount{
   330  					Name:      pvcVolumeName,
   331  					MountPath: vm.MountPath,
   332  				}
   333  			}
   334  		}
   335  
   336  		// Add daemon container to the deployment
   337  		config.AddDaemonContainer(e.Config, job, instance.GetResource(current.HSMDAEMON), pvcMount)
   338  
   339  		// If a pvc mount has been configured in HSM config, set the volume mount on the CertGen container
   340  		if pvcMount != nil {
   341  			job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, *pvcMount)
   342  		}
   343  	}
   344  
   345  	return job
   346  }