github.com/IBM-Blockchain/fabric-operator@v1.0.4/pkg/initializer/common/enroller/hsmenroller.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  	"github.com/hyperledger/fabric-ca/lib"
    28  	"github.com/pkg/errors"
    29  
    30  	current "github.com/IBM-Blockchain/fabric-operator/api/v1beta1"
    31  	"github.com/IBM-Blockchain/fabric-operator/pkg/apis/common"
    32  	"github.com/IBM-Blockchain/fabric-operator/pkg/initializer/common/config"
    33  	k8sclient "github.com/IBM-Blockchain/fabric-operator/pkg/k8s/controllerclient"
    34  	jobv1 "github.com/IBM-Blockchain/fabric-operator/pkg/manager/resources/job"
    35  	"github.com/IBM-Blockchain/fabric-operator/pkg/util"
    36  
    37  	batchv1 "k8s.io/api/batch/v1"
    38  	corev1 "k8s.io/api/core/v1"
    39  	"k8s.io/apimachinery/pkg/api/resource"
    40  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    41  	"k8s.io/apimachinery/pkg/runtime"
    42  	"k8s.io/apimachinery/pkg/types"
    43  	"k8s.io/apimachinery/pkg/util/wait"
    44  
    45  	"sigs.k8s.io/controller-runtime/pkg/client"
    46  	"sigs.k8s.io/yaml"
    47  )
    48  
    49  //go:generate counterfeiter -o mocks/instance.go -fake-name Instance . Instance
    50  type Instance interface {
    51  	metav1.Object
    52  	EnrollerImage() string
    53  	GetPullSecrets() []corev1.LocalObjectReference
    54  	PVCName() string
    55  	GetResource(current.Component) corev1.ResourceRequirements
    56  }
    57  
    58  //go:generate counterfeiter -o mocks/hsmcaclient.go -fake-name HSMCAClient . HSMCAClient
    59  type HSMCAClient interface {
    60  	GetEnrollmentRequest() *current.Enrollment
    61  	GetHomeDir() string
    62  	PingCA(time.Duration) error
    63  	SetHSMLibrary(string)
    64  	GetConfig() *lib.ClientConfig
    65  }
    66  
    67  type HSMEnrollJobTimeouts struct {
    68  	JobStart      common.Duration `json:"jobStart" yaml:"jobStart"`
    69  	JobCompletion common.Duration `json:"jobCompletion" yaml:"jobCompletion"`
    70  }
    71  
    72  type HSMEnroller struct {
    73  	CAClient HSMCAClient
    74  	Client   k8sclient.Client
    75  	Instance Instance
    76  	Timeouts HSMEnrollJobTimeouts
    77  	Scheme   *runtime.Scheme
    78  	Config   *config.HSMConfig
    79  }
    80  
    81  func NewHSMEnroller(cfg *current.Enrollment, instance Instance, caclient HSMCAClient, client k8sclient.Client, scheme *runtime.Scheme, timeouts HSMEnrollJobTimeouts, hsmConfig *config.HSMConfig) *HSMEnroller {
    82  	return &HSMEnroller{
    83  		CAClient: caclient,
    84  		Client:   client,
    85  		Instance: instance,
    86  		Scheme:   scheme,
    87  		Timeouts: timeouts,
    88  		Config:   hsmConfig,
    89  	}
    90  }
    91  
    92  func (e *HSMEnroller) GetEnrollmentRequest() *current.Enrollment {
    93  	return e.CAClient.GetEnrollmentRequest()
    94  }
    95  
    96  func (e *HSMEnroller) ReadKey() ([]byte, error) {
    97  	return nil, nil
    98  }
    99  
   100  func (e *HSMEnroller) PingCA(timeout time.Duration) error {
   101  	return e.CAClient.PingCA(timeout)
   102  }
   103  
   104  func (e *HSMEnroller) Enroll() (*config.Response, error) {
   105  	// Deleting CA client config is an unfortunate requirement since the ca client
   106  	// config map was not properly deleted after a successfull reenrollment request.
   107  	// This is problematic when recreating a resource with same name, as it will
   108  	// try to use old settings in the config map, which might no longer apply, thus
   109  	// it must be removed if found before proceeding.
   110  	if err := deleteCAClientConfig(e.Client, e.Instance); err != nil {
   111  		return nil, err
   112  	}
   113  
   114  	e.CAClient.SetHSMLibrary(filepath.Join("/hsm/lib", filepath.Base(e.Config.Library.FilePath)))
   115  	if err := createRootTLSSecret(e.Client, e.CAClient, e.Scheme, e.Instance); err != nil {
   116  		return nil, err
   117  	}
   118  
   119  	if err := createCAClientConfig(e.Client, e.CAClient, e.Scheme, e.Instance); err != nil {
   120  		return nil, err
   121  	}
   122  
   123  	job := e.initHSMJob(e.Instance, e.Timeouts)
   124  	if err := e.Client.Create(context.TODO(), job.Job, k8sclient.CreateOption{
   125  		Owner:  e.Instance,
   126  		Scheme: e.Scheme,
   127  	}); err != nil {
   128  		return nil, errors.Wrap(err, "failed to create HSM ca initialization job")
   129  	}
   130  	log.Info(fmt.Sprintf("Job '%s' created", job.GetName()))
   131  
   132  	if err := job.WaitUntilActive(e.Client); err != nil {
   133  		return nil, err
   134  	}
   135  	log.Info(fmt.Sprintf("Job '%s' active", job.GetName()))
   136  
   137  	if err := job.WaitUntilFinished(e.Client); err != nil {
   138  		return nil, err
   139  	}
   140  	log.Info(fmt.Sprintf("Job '%s' finished", job.GetName()))
   141  
   142  	status, err := job.Status(e.Client)
   143  	if err != nil {
   144  		return nil, err
   145  	}
   146  
   147  	switch status {
   148  	case jobv1.FAILED:
   149  		return nil, fmt.Errorf("Job '%s' finished unsuccessfully, not cleaning up pods to allow for error evaluation", job.GetName())
   150  	case jobv1.COMPLETED:
   151  		if err := job.Delete(e.Client); err != nil {
   152  			return nil, err
   153  		}
   154  
   155  		if err := deleteRootTLSSecret(e.Client, e.Instance); err != nil {
   156  			return nil, err
   157  		}
   158  
   159  		if err := deleteCAClientConfig(e.Client, e.Instance); err != nil {
   160  			return nil, err
   161  		}
   162  	}
   163  
   164  	name := fmt.Sprintf("ecert-%s-signcert", e.Instance.GetName())
   165  	err = wait.Poll(2*time.Second, 30*time.Second, func() (bool, error) {
   166  		sec := &corev1.Secret{}
   167  		log.Info(fmt.Sprintf("Waiting for secret '%s' to be created", name))
   168  		err = e.Client.Get(context.TODO(), types.NamespacedName{
   169  			Name:      name,
   170  			Namespace: e.Instance.GetNamespace(),
   171  		}, sec)
   172  		if err != nil {
   173  			return false, nil
   174  		}
   175  
   176  		return true, nil
   177  	})
   178  	if err != nil {
   179  		return nil, fmt.Errorf("failed to create secret '%s'", name)
   180  	}
   181  
   182  	if err := setControllerReferences(e.Client, e.Scheme, e.Instance); err != nil {
   183  		return nil, err
   184  	}
   185  
   186  	return &config.Response{}, nil
   187  }
   188  
   189  func setControllerReferences(client k8sclient.Client, scheme *runtime.Scheme, instance Instance) error {
   190  	if err := setControllerReferenceFor(fmt.Sprintf("ecert-%s-signcert", instance.GetName()), false, client, scheme, instance); err != nil {
   191  		return err
   192  	}
   193  
   194  	if err := setControllerReferenceFor(fmt.Sprintf("ecert-%s-cacerts", instance.GetName()), false, client, scheme, instance); err != nil {
   195  		return err
   196  	}
   197  
   198  	if err := setControllerReferenceFor(fmt.Sprintf("ecert-%s-admincerts", instance.GetName()), true, client, scheme, instance); err != nil {
   199  		return err
   200  	}
   201  
   202  	if err := setControllerReferenceFor(fmt.Sprintf("ecert-%s-intercerts", instance.GetName()), true, client, scheme, instance); err != nil {
   203  		return err
   204  	}
   205  
   206  	return nil
   207  }
   208  
   209  func setControllerReferenceFor(name string, skipIfNotFound bool, client k8sclient.Client, scheme *runtime.Scheme, instance Instance) error {
   210  	nn := types.NamespacedName{
   211  		Name:      name,
   212  		Namespace: instance.GetNamespace(),
   213  	}
   214  
   215  	sec := &corev1.Secret{}
   216  	if err := client.Get(context.TODO(), nn, sec); err != nil {
   217  		if skipIfNotFound {
   218  			return nil
   219  		}
   220  
   221  		return err
   222  	}
   223  
   224  	if err := client.Update(context.TODO(), sec, k8sclient.UpdateOption{
   225  		Owner:  instance,
   226  		Scheme: scheme,
   227  	}); err != nil {
   228  		return errors.Wrapf(err, "failed to update secret '%s' with controller reference", instance.GetName())
   229  	}
   230  
   231  	return nil
   232  }
   233  
   234  func createRootTLSSecret(client k8sclient.Client, caClient HSMCAClient, scheme *runtime.Scheme, instance Instance) error {
   235  	tlsCertBytes, err := caClient.GetEnrollmentRequest().GetCATLSBytes()
   236  	if err != nil {
   237  		return err
   238  	}
   239  
   240  	secret := &corev1.Secret{
   241  		ObjectMeta: metav1.ObjectMeta{
   242  			Name:      fmt.Sprintf("%s-init-roottls", instance.GetName()),
   243  			Namespace: instance.GetNamespace(),
   244  		},
   245  		Data: map[string][]byte{
   246  			"tlsCert.pem": tlsCertBytes,
   247  		},
   248  	}
   249  
   250  	if err := client.Create(context.TODO(), secret, k8sclient.CreateOption{
   251  		Owner:  instance,
   252  		Scheme: scheme,
   253  	}); err != nil {
   254  		return errors.Wrap(err, "failed to create secret")
   255  	}
   256  
   257  	return nil
   258  }
   259  
   260  func deleteRootTLSSecret(client k8sclient.Client, instance Instance) error {
   261  	secret := &corev1.Secret{
   262  		ObjectMeta: metav1.ObjectMeta{
   263  			Name:      fmt.Sprintf("%s-init-roottls", instance.GetName()),
   264  			Namespace: instance.GetNamespace(),
   265  		},
   266  	}
   267  
   268  	if err := client.Delete(context.TODO(), secret); err != nil {
   269  		return errors.Wrap(err, "failed to delete secret")
   270  	}
   271  
   272  	return nil
   273  }
   274  
   275  func createCAClientConfig(client k8sclient.Client, caClient HSMCAClient, scheme *runtime.Scheme, instance Instance) error {
   276  	configBytes, err := yaml.Marshal(caClient.GetConfig())
   277  	if err != nil {
   278  		return err
   279  	}
   280  
   281  	cm := &corev1.ConfigMap{
   282  		ObjectMeta: metav1.ObjectMeta{
   283  			Name:      fmt.Sprintf("%s-init-config", instance.GetName()),
   284  			Namespace: instance.GetNamespace(),
   285  		},
   286  		BinaryData: map[string][]byte{
   287  			"fabric-ca-client-config.yaml": configBytes,
   288  		},
   289  	}
   290  
   291  	if err := client.Create(context.TODO(), cm, k8sclient.CreateOption{
   292  		Owner:  instance,
   293  		Scheme: scheme,
   294  	}); err != nil {
   295  		return errors.Wrap(err, "failed to create config map")
   296  	}
   297  
   298  	return nil
   299  }
   300  
   301  func deleteCAClientConfig(k8sClient k8sclient.Client, instance Instance) error {
   302  	cm := &corev1.ConfigMap{
   303  		ObjectMeta: metav1.ObjectMeta{
   304  			Name:      fmt.Sprintf("%s-init-config", instance.GetName()),
   305  			Namespace: instance.GetNamespace(),
   306  		},
   307  	}
   308  
   309  	if err := k8sClient.Delete(context.TODO(), cm); client.IgnoreNotFound(err) != nil {
   310  		return errors.Wrap(err, "failed to delete config map")
   311  	}
   312  
   313  	return nil
   314  }
   315  
   316  func (e *HSMEnroller) initHSMJob(instance Instance, timeouts HSMEnrollJobTimeouts) *jobv1.Job {
   317  	hsmConfig := e.Config
   318  	req := e.CAClient.GetEnrollmentRequest()
   319  
   320  	hsmLibraryPath := hsmConfig.Library.FilePath
   321  	hsmLibraryName := filepath.Base(hsmLibraryPath)
   322  
   323  	jobName := fmt.Sprintf("%s-enroll", instance.GetName())
   324  
   325  	f := false
   326  	user := int64(0)
   327  	backoffLimit := int32(0)
   328  	mountPath := "/shared"
   329  
   330  	k8sJob := &batchv1.Job{
   331  		ObjectMeta: metav1.ObjectMeta{
   332  			Name:      jobName,
   333  			Namespace: instance.GetNamespace(),
   334  			Labels: map[string]string{
   335  				"name":  jobName,
   336  				"owner": instance.GetName(),
   337  			},
   338  		},
   339  		Spec: batchv1.JobSpec{
   340  			BackoffLimit: &backoffLimit,
   341  			Template: corev1.PodTemplateSpec{
   342  				Spec: corev1.PodSpec{
   343  					ServiceAccountName: instance.GetName(),
   344  					ImagePullSecrets:   util.AppendImagePullSecretIfMissing(instance.GetPullSecrets(), hsmConfig.BuildPullSecret()),
   345  					RestartPolicy:      corev1.RestartPolicyNever,
   346  					InitContainers: []corev1.Container{
   347  						corev1.Container{
   348  							Name:            "hsm-client",
   349  							Image:           hsmConfig.Library.Image,
   350  							ImagePullPolicy: corev1.PullAlways,
   351  							Command: []string{
   352  								"sh",
   353  								"-c",
   354  								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),
   355  							},
   356  							SecurityContext: &corev1.SecurityContext{
   357  								RunAsUser:    &user,
   358  								RunAsNonRoot: &f,
   359  							},
   360  							VolumeMounts: []corev1.VolumeMount{
   361  								corev1.VolumeMount{
   362  									Name:      "shared",
   363  									MountPath: mountPath,
   364  								},
   365  							},
   366  							Resources: corev1.ResourceRequirements{
   367  								Requests: corev1.ResourceList{
   368  									corev1.ResourceCPU:              resource.MustParse("0.1"),
   369  									corev1.ResourceMemory:           resource.MustParse("100Mi"),
   370  									corev1.ResourceEphemeralStorage: resource.MustParse("100Mi"),
   371  								},
   372  								Limits: corev1.ResourceList{
   373  									corev1.ResourceCPU:              resource.MustParse("1"),
   374  									corev1.ResourceMemory:           resource.MustParse("500Mi"),
   375  									corev1.ResourceEphemeralStorage: resource.MustParse("1Gi"),
   376  								},
   377  							},
   378  						},
   379  					},
   380  					Containers: []corev1.Container{
   381  						corev1.Container{
   382  							Name:            "init",
   383  							Image:           instance.EnrollerImage(),
   384  							ImagePullPolicy: corev1.PullAlways,
   385  							SecurityContext: &corev1.SecurityContext{
   386  								RunAsUser:    &user,
   387  								RunAsNonRoot: &f,
   388  							},
   389  							Env: hsmConfig.GetEnvs(),
   390  							Command: []string{
   391  								"sh",
   392  								"-c",
   393  								fmt.Sprintf("/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),
   394  							},
   395  							VolumeMounts: []corev1.VolumeMount{
   396  								corev1.VolumeMount{
   397  									Name:      "tlscertfile",
   398  									MountPath: fmt.Sprintf("%s/tlsCert.pem", e.CAClient.GetHomeDir()),
   399  									SubPath:   "tlsCert.pem",
   400  								},
   401  								corev1.VolumeMount{
   402  									Name:      "clientconfig",
   403  									MountPath: fmt.Sprintf("/tmp/%s", "fabric-ca-client-config.yaml"),
   404  									SubPath:   "fabric-ca-client-config.yaml",
   405  								},
   406  								corev1.VolumeMount{
   407  									Name:      "shared",
   408  									MountPath: "/hsm/lib",
   409  									SubPath:   "hsm",
   410  								},
   411  							},
   412  						},
   413  					},
   414  					Volumes: []corev1.Volume{
   415  						corev1.Volume{
   416  							Name: "shared",
   417  							VolumeSource: corev1.VolumeSource{
   418  								EmptyDir: &corev1.EmptyDirVolumeSource{
   419  									Medium: corev1.StorageMediumMemory,
   420  								},
   421  							},
   422  						},
   423  						corev1.Volume{
   424  							Name: "tlscertfile",
   425  							VolumeSource: corev1.VolumeSource{
   426  								Secret: &corev1.SecretVolumeSource{
   427  									SecretName: fmt.Sprintf("%s-init-roottls", instance.GetName()),
   428  								},
   429  							},
   430  						},
   431  						corev1.Volume{
   432  							Name: "clientconfig",
   433  							VolumeSource: corev1.VolumeSource{
   434  								ConfigMap: &corev1.ConfigMapVolumeSource{
   435  									LocalObjectReference: corev1.LocalObjectReference{
   436  										Name: fmt.Sprintf("%s-init-config", instance.GetName()),
   437  									},
   438  								},
   439  							},
   440  						},
   441  					},
   442  				},
   443  			},
   444  		},
   445  	}
   446  
   447  	job := jobv1.New(k8sJob, &jobv1.Timeouts{
   448  		WaitUntilActive:   timeouts.JobStart.Get(),
   449  		WaitUntilFinished: timeouts.JobCompletion.Get(),
   450  	})
   451  
   452  	job.Spec.Template.Spec.Volumes = append(job.Spec.Template.Spec.Volumes, hsmConfig.GetVolumes()...)
   453  	job.Spec.Template.Spec.Containers[0].VolumeMounts = append(job.Spec.Template.Spec.Containers[0].VolumeMounts, hsmConfig.GetVolumeMounts()...)
   454  
   455  	return job
   456  }