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 }