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 }