github.com/IBM-Blockchain/fabric-operator@v1.0.4/pkg/initializer/ca/hsm.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 initializer
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"path/filepath"
    25  	"strings"
    26  	"time"
    27  
    28  	"github.com/pkg/errors"
    29  
    30  	current "github.com/IBM-Blockchain/fabric-operator/api/v1beta1"
    31  	v1 "github.com/IBM-Blockchain/fabric-operator/pkg/apis/ca/v1"
    32  	"github.com/IBM-Blockchain/fabric-operator/pkg/apis/common"
    33  	caconfig "github.com/IBM-Blockchain/fabric-operator/pkg/initializer/ca/config"
    34  	"github.com/IBM-Blockchain/fabric-operator/pkg/initializer/common/config"
    35  	controller "github.com/IBM-Blockchain/fabric-operator/pkg/k8s/controllerclient"
    36  	"github.com/IBM-Blockchain/fabric-operator/pkg/util"
    37  	"github.com/IBM-Blockchain/fabric-operator/pkg/util/image"
    38  
    39  	batchv1 "k8s.io/api/batch/v1"
    40  	corev1 "k8s.io/api/core/v1"
    41  	"k8s.io/apimachinery/pkg/api/resource"
    42  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    43  	"k8s.io/apimachinery/pkg/labels"
    44  	"k8s.io/apimachinery/pkg/runtime"
    45  	"k8s.io/apimachinery/pkg/types"
    46  	"k8s.io/apimachinery/pkg/util/wait"
    47  
    48  	k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
    49  	"sigs.k8s.io/yaml"
    50  )
    51  
    52  // HSMInitJobTimeouts defines timeouts properties
    53  type HSMInitJobTimeouts struct {
    54  	JobStart      common.Duration `json:"jobStart" yaml:"jobStart"`
    55  	JobCompletion common.Duration `json:"jobCompletion" yaml:"jobCompletion"`
    56  }
    57  
    58  // HSM implements the ability to initialize HSM CA
    59  type HSM struct {
    60  	Config   *config.HSMConfig
    61  	Timeouts HSMInitJobTimeouts
    62  	Client   controller.Client
    63  	Scheme   *runtime.Scheme
    64  }
    65  
    66  // Create creates the crypto and config materical to initialize an HSM based CA
    67  func (h *HSM) Create(instance *current.IBPCA, overrides *v1.ServerConfig, ca IBPCA) (*Response, error) {
    68  	log.Info(fmt.Sprintf("Creating job to initialize ca '%s'", instance.GetName()))
    69  
    70  	if err := ca.OverrideServerConfig(overrides); err != nil {
    71  		return nil, err
    72  	}
    73  
    74  	if err := createCACryptoSecret(h.Client, h.Scheme, instance, ca); err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	if err := createCAConfigMap(h.Client, h.Scheme, instance, h.Config.Library.FilePath, ca); err != nil {
    79  		return nil, err
    80  	}
    81  
    82  	dbConfig, err := getDBConfig(instance, ca.GetType())
    83  	if err != nil {
    84  		return nil, errors.Wrapf(err, "failed get DB config for CA '%s'", instance.GetName())
    85  	}
    86  
    87  	job := initHSMCAJob(instance, h.Config, dbConfig, ca.GetType())
    88  	setPathsOnJob(h.Config, job)
    89  
    90  	if err := h.Client.Create(context.TODO(), job, controller.CreateOption{
    91  		Owner:  instance,
    92  		Scheme: h.Scheme,
    93  	}); err != nil {
    94  		return nil, errors.Wrap(err, "failed to create HSM ca initialization job")
    95  	}
    96  
    97  	log.Info(fmt.Sprintf("Job '%s' created", job.GetName()))
    98  
    99  	// Wait for job to start and pod to go into running state
   100  	if err := h.waitForJobToBeActive(job); err != nil {
   101  		return nil, err
   102  	}
   103  
   104  	status, err := h.waitForJobPodToFinish(job)
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  
   109  	log.Info(fmt.Sprintf("Job '%s' finished", job.GetName()))
   110  
   111  	if status.Phase != corev1.PodSucceeded {
   112  		return nil, fmt.Errorf("failed to init '%s' check job '%s' pods for errors", instance.GetName(), job.GetName())
   113  	}
   114  
   115  	// For posterity, job is only deleted if successful, not deleting on failure allows logs to be
   116  	// available for review.
   117  	//
   118  	// Don't need to cleanup/delete CACrypto Secret and CAConfig config map created earlier,
   119  	// as the job will update these resources.
   120  	if err := h.deleteJob(job); err != nil {
   121  		return nil, err
   122  	}
   123  
   124  	if ca.GetType().Is(caconfig.EnrollmentCA) {
   125  		if err := updateCAConfigMap(h.Client, h.Scheme, instance, ca); err != nil {
   126  			return nil, errors.Wrapf(err, "failed to update CA configmap for CA %s", instance.GetName())
   127  		}
   128  	}
   129  
   130  	return nil, nil
   131  }
   132  
   133  func createCACryptoSecret(client controller.Client, scheme *runtime.Scheme, instance *current.IBPCA, ca IBPCA) error {
   134  	crypto, err := ca.ParseCrypto()
   135  	if err != nil {
   136  		return err
   137  	}
   138  
   139  	var name string
   140  	switch ca.GetType() {
   141  	case caconfig.EnrollmentCA:
   142  		name = fmt.Sprintf("%s-ca-crypto", instance.GetName())
   143  	case caconfig.TLSCA:
   144  		name = fmt.Sprintf("%s-tlsca-crypto", instance.GetName())
   145  	}
   146  
   147  	secret := &corev1.Secret{
   148  		ObjectMeta: metav1.ObjectMeta{
   149  			Name:      name,
   150  			Namespace: instance.GetNamespace(),
   151  		},
   152  		Data: crypto,
   153  	}
   154  
   155  	if err := client.Create(context.TODO(), secret, controller.CreateOption{
   156  		Owner:  instance,
   157  		Scheme: scheme,
   158  	}); err != nil {
   159  		return errors.Wrap(err, "failed to create initialization crypto secret")
   160  	}
   161  
   162  	return nil
   163  }
   164  
   165  func createCAConfigMap(client controller.Client, scheme *runtime.Scheme, instance *current.IBPCA, library string, ca IBPCA) error {
   166  	serverConfig := ca.GetServerConfig()
   167  	serverConfig.CAConfig.CSP.PKCS11.Library = filepath.Join("/hsm/lib", filepath.Base(library))
   168  
   169  	ca.SetMountPaths()
   170  	configBytes, err := ca.ConfigToBytes()
   171  	if err != nil {
   172  		return err
   173  	}
   174  
   175  	var name string
   176  	switch ca.GetType() {
   177  	case caconfig.EnrollmentCA:
   178  		name = fmt.Sprintf("%s-ca-config", instance.GetName())
   179  	case caconfig.TLSCA:
   180  		name = fmt.Sprintf("%s-tlsca-config", instance.GetName())
   181  	}
   182  
   183  	cm := &corev1.ConfigMap{
   184  		ObjectMeta: metav1.ObjectMeta{
   185  			Name:      name,
   186  			Namespace: instance.GetNamespace(),
   187  			Labels:    instance.GetLabels(),
   188  			OwnerReferences: []metav1.OwnerReference{
   189  				{
   190  					Kind:       "IBPCA",
   191  					APIVersion: "ibp.com/v1beta1",
   192  					Name:       instance.GetName(),
   193  					UID:        instance.GetUID(),
   194  				},
   195  			},
   196  		},
   197  		BinaryData: map[string][]byte{
   198  			"fabric-ca-server-config.yaml": configBytes,
   199  		},
   200  	}
   201  
   202  	if err := client.Create(context.TODO(), cm, controller.CreateOption{
   203  		Owner:  instance,
   204  		Scheme: scheme,
   205  	}); err != nil {
   206  		return errors.Wrap(err, "failed to create initialization config map secret")
   207  	}
   208  
   209  	return nil
   210  }
   211  
   212  func updateCAConfigMap(client controller.Client, scheme *runtime.Scheme, instance *current.IBPCA, ca IBPCA) error {
   213  	serverConfig := ca.GetServerConfig()
   214  	serverConfig.CAfiles = []string{"/data/tlsca/fabric-ca-server-config.yaml"}
   215  
   216  	configBytes, err := ca.ConfigToBytes()
   217  	if err != nil {
   218  		return err
   219  	}
   220  
   221  	name := fmt.Sprintf("%s-ca-config", instance.GetName())
   222  
   223  	cm := &corev1.ConfigMap{
   224  		ObjectMeta: metav1.ObjectMeta{
   225  			Name:      name,
   226  			Namespace: instance.GetNamespace(),
   227  			Labels:    instance.GetLabels(),
   228  			OwnerReferences: []metav1.OwnerReference{
   229  				{
   230  					Kind:       "IBPCA",
   231  					APIVersion: "ibp.com/v1beta1",
   232  					Name:       instance.GetName(),
   233  					UID:        instance.GetUID(),
   234  				},
   235  			},
   236  		},
   237  		BinaryData: map[string][]byte{
   238  			"fabric-ca-server-config.yaml": configBytes,
   239  		},
   240  	}
   241  
   242  	if err := client.Update(context.TODO(), cm, controller.UpdateOption{
   243  		Owner:  instance,
   244  		Scheme: scheme,
   245  	}); err != nil {
   246  		return errors.Wrapf(err, "failed to update config map '%s'", name)
   247  	}
   248  
   249  	return nil
   250  }
   251  
   252  func (h *HSM) waitForJobToBeActive(job *batchv1.Job) error {
   253  	err := wait.Poll(2*time.Second, h.Timeouts.JobStart.Duration, func() (bool, error) {
   254  		log.Info(fmt.Sprintf("Waiting for job '%s' to start", job.GetName()))
   255  
   256  		j := &batchv1.Job{}
   257  		err := h.Client.Get(context.TODO(), types.NamespacedName{
   258  			Name:      job.GetName(),
   259  			Namespace: job.GetNamespace(),
   260  		}, j)
   261  		if err != nil {
   262  			return false, nil
   263  		}
   264  
   265  		if j.Status.Active >= int32(1) {
   266  			return true, nil
   267  		}
   268  
   269  		return false, nil
   270  	})
   271  	if err != nil {
   272  		return errors.Wrap(err, "job failed to start")
   273  	}
   274  	return nil
   275  }
   276  
   277  func (h *HSM) waitForJobPodToFinish(job *batchv1.Job) (*corev1.PodStatus, error) {
   278  	var err error
   279  	var status *corev1.PodStatus
   280  
   281  	err = wait.Poll(2*time.Second, h.Timeouts.JobCompletion.Duration, func() (bool, error) {
   282  		log.Info(fmt.Sprintf("Waiting for job pod '%s' to finish", job.GetName()))
   283  
   284  		status, err = h.podStatus(job)
   285  		if err != nil {
   286  			log.Info(fmt.Sprintf("job pod err: %s", err))
   287  			return false, nil
   288  		}
   289  
   290  		if status.Phase == corev1.PodFailed || status.Phase == corev1.PodSucceeded {
   291  			return true, nil
   292  		}
   293  
   294  		return false, nil
   295  	})
   296  	if err != nil {
   297  		return nil, errors.Wrapf(err, "pod for job '%s' failed to finish", job.GetName())
   298  	}
   299  
   300  	return status, nil
   301  }
   302  
   303  func (h *HSM) podStatus(job *batchv1.Job) (*corev1.PodStatus, error) {
   304  	labelSelector, err := labels.Parse(fmt.Sprintf("job-name=%s", job.GetName()))
   305  	if err != nil {
   306  		return nil, err
   307  	}
   308  
   309  	opts := &k8sclient.ListOptions{
   310  		LabelSelector: labelSelector,
   311  	}
   312  
   313  	pods := &corev1.PodList{}
   314  	if err := h.Client.List(context.TODO(), pods, opts); err != nil {
   315  		return nil, err
   316  	}
   317  
   318  	if len(pods.Items) != 1 {
   319  		return nil, errors.New("incorrect number of job pods found")
   320  	}
   321  
   322  	for _, pod := range pods.Items {
   323  		for _, containerStatus := range pod.Status.ContainerStatuses {
   324  			if containerStatus.State.Waiting != nil || containerStatus.State.Running != nil {
   325  				return &pod.Status, nil
   326  			}
   327  		}
   328  
   329  		return &pod.Status, nil
   330  	}
   331  
   332  	return nil, errors.New("unable to get pod status")
   333  }
   334  
   335  func (h *HSM) deleteJob(job *batchv1.Job) error {
   336  	if err := h.Client.Delete(context.TODO(), job); err != nil {
   337  		return err
   338  	}
   339  
   340  	// TODO: Need to investigate why job is not adding controller reference to job pod,
   341  	// this manual cleanup should not be required
   342  	podList := &corev1.PodList{}
   343  	if err := h.Client.List(context.TODO(), podList, k8sclient.MatchingLabels{"job-name": job.Name}); err != nil {
   344  		return errors.Wrap(err, "failed to list job pods")
   345  	}
   346  
   347  	for _, pod := range podList.Items {
   348  		podListItem := pod
   349  		if err := h.Client.Delete(context.TODO(), &podListItem); err != nil {
   350  			return errors.Wrapf(err, "failed to delete pod '%s'", podListItem.Name)
   351  		}
   352  	}
   353  
   354  	return nil
   355  }
   356  
   357  func setPathsOnJob(hsmConfig *config.HSMConfig, job *batchv1.Job) {
   358  	job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, hsmConfig.GetVolumes()...)
   359  	job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, hsmConfig.GetVolumeMounts()...)
   360  }
   361  
   362  func getDBConfig(instance *current.IBPCA, caType caconfig.Type) (*v1.CAConfigDB, error) {
   363  	var rawMessage *[]byte
   364  	switch caType {
   365  	case caconfig.EnrollmentCA:
   366  		if instance.Spec.ConfigOverride != nil && instance.Spec.ConfigOverride.CA != nil {
   367  			rawMessage = &instance.Spec.ConfigOverride.CA.Raw
   368  		}
   369  	case caconfig.TLSCA:
   370  		if instance.Spec.ConfigOverride != nil && instance.Spec.ConfigOverride.TLSCA != nil {
   371  			rawMessage = &instance.Spec.ConfigOverride.TLSCA.Raw
   372  		}
   373  	}
   374  
   375  	if rawMessage == nil {
   376  		return &v1.CAConfigDB{}, nil
   377  	}
   378  
   379  	caOverrides := &v1.ServerConfig{}
   380  	err := yaml.Unmarshal(*rawMessage, caOverrides)
   381  	if err != nil {
   382  		return nil, err
   383  	}
   384  
   385  	return caOverrides.CAConfig.DB, nil
   386  }
   387  
   388  func initHSMCAJob(instance *current.IBPCA, hsmConfig *config.HSMConfig, dbConfig *v1.CAConfigDB, caType caconfig.Type) *batchv1.Job {
   389  	var typ string
   390  
   391  	switch caType {
   392  	case caconfig.EnrollmentCA:
   393  		typ = "ca"
   394  	case caconfig.TLSCA:
   395  		typ = "tlsca"
   396  	}
   397  
   398  	cryptoMountPath := fmt.Sprintf("/crypto/%s", typ)
   399  	homeDir := fmt.Sprintf("/tmp/data/%s/%s", instance.GetName(), typ)
   400  	secretName := fmt.Sprintf("%s-%s-crypto", instance.GetName(), typ)
   401  	jobName := fmt.Sprintf("%s-%s-init", instance.GetName(), typ)
   402  
   403  	hsmLibraryPath := hsmConfig.Library.FilePath
   404  	hsmLibraryName := filepath.Base(hsmLibraryPath)
   405  
   406  	f := false
   407  	user := int64(0)
   408  	backoffLimit := int32(0)
   409  	mountPath := "/shared"
   410  	job := &batchv1.Job{
   411  		ObjectMeta: metav1.ObjectMeta{
   412  			Name:      jobName,
   413  			Namespace: instance.GetNamespace(),
   414  			Labels: map[string]string{
   415  				"name":  jobName,
   416  				"owner": instance.GetName(),
   417  			},
   418  		},
   419  		Spec: batchv1.JobSpec{
   420  			BackoffLimit: &backoffLimit,
   421  			Template: corev1.PodTemplateSpec{
   422  				Spec: corev1.PodSpec{
   423  					ServiceAccountName: instance.GetName(),
   424  					ImagePullSecrets:   util.AppendImagePullSecretIfMissing(instance.GetPullSecrets(), hsmConfig.BuildPullSecret()),
   425  					RestartPolicy:      corev1.RestartPolicyNever,
   426  					InitContainers: []corev1.Container{
   427  						corev1.Container{
   428  							Name:            "hsm-client",
   429  							Image:           hsmConfig.Library.Image,
   430  							ImagePullPolicy: corev1.PullAlways,
   431  							Command: []string{
   432  								"sh",
   433  								"-c",
   434  								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),
   435  							},
   436  							SecurityContext: &corev1.SecurityContext{
   437  								RunAsUser:    &user,
   438  								RunAsNonRoot: &f,
   439  							},
   440  							VolumeMounts: []corev1.VolumeMount{
   441  								corev1.VolumeMount{
   442  									Name:      "shared",
   443  									MountPath: mountPath,
   444  								},
   445  							},
   446  							Resources: corev1.ResourceRequirements{
   447  								Requests: corev1.ResourceList{
   448  									corev1.ResourceCPU:              resource.MustParse("0.1"),
   449  									corev1.ResourceMemory:           resource.MustParse("100Mi"),
   450  									corev1.ResourceEphemeralStorage: resource.MustParse("100Mi"),
   451  								},
   452  								Limits: corev1.ResourceList{
   453  									corev1.ResourceCPU:              resource.MustParse("1"),
   454  									corev1.ResourceMemory:           resource.MustParse("500Mi"),
   455  									corev1.ResourceEphemeralStorage: resource.MustParse("1Gi"),
   456  								},
   457  							},
   458  						},
   459  					},
   460  					Containers: []corev1.Container{
   461  						corev1.Container{
   462  							Name: "init",
   463  							Image: image.Format(
   464  								instance.Spec.Images.EnrollerImage,
   465  								instance.Spec.Images.EnrollerTag,
   466  							),
   467  							ImagePullPolicy: corev1.PullAlways,
   468  							SecurityContext: &corev1.SecurityContext{
   469  								RunAsUser:    &user,
   470  								RunAsNonRoot: &f,
   471  							},
   472  							Command: []string{
   473  								"sh",
   474  								"-c",
   475  								fmt.Sprintf("/usr/local/bin/enroller ca %s %s %s %s %s %s", instance.GetName(), instance.GetNamespace(), homeDir, cryptoMountPath, secretName, caType),
   476  							},
   477  							Env: hsmConfig.GetEnvs(),
   478  							VolumeMounts: []corev1.VolumeMount{
   479  								corev1.VolumeMount{
   480  									Name:      "shared",
   481  									MountPath: "/hsm/lib",
   482  									SubPath:   "hsm",
   483  								},
   484  								corev1.VolumeMount{
   485  									Name:      "caconfig",
   486  									MountPath: fmt.Sprintf("/tmp/data/%s/%s/fabric-ca-server-config.yaml", instance.GetName(), typ),
   487  									SubPath:   "fabric-ca-server-config.yaml",
   488  								},
   489  							},
   490  						},
   491  					},
   492  					Volumes: []corev1.Volume{
   493  						corev1.Volume{
   494  							Name: "shared",
   495  							VolumeSource: corev1.VolumeSource{
   496  								EmptyDir: &corev1.EmptyDirVolumeSource{
   497  									Medium: corev1.StorageMediumMemory,
   498  								},
   499  							},
   500  						},
   501  						corev1.Volume{
   502  							Name: "caconfig",
   503  							VolumeSource: corev1.VolumeSource{
   504  								ConfigMap: &corev1.ConfigMapVolumeSource{
   505  									LocalObjectReference: corev1.LocalObjectReference{
   506  										Name: fmt.Sprintf("%s-%s-config", instance.GetName(), typ),
   507  									},
   508  								},
   509  							},
   510  						},
   511  					},
   512  				},
   513  			},
   514  		},
   515  	}
   516  
   517  	if dbConfig == nil {
   518  		return job
   519  	}
   520  
   521  	// If using postgres with TLS enabled need to mount trusted root TLS certificate for databae server
   522  	if strings.ToLower(dbConfig.Type) == "postgres" {
   523  		if dbConfig.TLS.IsEnabled() {
   524  			job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts,
   525  				corev1.VolumeMount{
   526  					Name:      "cacrypto",
   527  					MountPath: fmt.Sprintf("/crypto/%s/db-certfile0.pem", typ),
   528  					SubPath:   "db-certfile0.pem",
   529  				})
   530  
   531  			job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes,
   532  				corev1.Volume{
   533  					Name: "cacrypto",
   534  					VolumeSource: corev1.VolumeSource{
   535  						Secret: &corev1.SecretVolumeSource{
   536  							SecretName: fmt.Sprintf("%s-%s-crypto", instance.GetName(), typ),
   537  							Items: []corev1.KeyToPath{
   538  								corev1.KeyToPath{
   539  									Key:  "db-certfile0.pem",
   540  									Path: "db-certfile0.pem",
   541  								},
   542  							},
   543  						},
   544  					},
   545  				},
   546  			)
   547  		}
   548  	}
   549  
   550  	return job
   551  }