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 }