diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 91ea521..ae18db7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,7 +25,7 @@ We use [pull requests](http://help.github.com/pull-requests/) to deliver changes 1. [Fork the repository](https://guides.github.com/activities/forking/#fork) and create a new branch from `main`. 2. If you've added code that should be tested, add tests! 3. If you've added any new features or made breaking changes, update the documentation. -4. Ensure all the tests pass. +4. Ensure all the tests pass, using `go test -v ./...`. 5. Include a descriptive message, and the [Developer Certificate of Origin (DCO) sign-off](https://github.com/probot/dco#how-it-works) on all commit messages. 6. [Issue a pull request](https://guides.github.com/activities/forking/#making-a-pull-request)! 7. [GitHub Actions](https://github.com/hyperledger-labs/fabric-builder-k8s/actions) builds must succeed before the pull request can be reviewed and merged. @@ -34,6 +34,12 @@ We use [pull requests](http://help.github.com/pull-requests/) to deliver changes Please to try to be consistent with the rest of the code and conform to linting rules where they are provided. +To run the linter, use the following command. + +```shell +golangci-lint run ./... +``` + ## Development environment There is a [Visual Studio Code Dev Container](https://code.visualstudio.com/docs/devcontainers/containers) which should help develop and test the k8s builder in a consistent development environment. @@ -41,19 +47,19 @@ It includes a preconfigured nano Fabric test network and minikube which can be u Build your latest k8s builder changes. -``` +```shell GOBIN="${PWD}"/.fabric/builders/k8s_builder/bin go install ./cmd/... ``` [Configure kubernetes](./docs/KUBERNETES_CONFIG.md) and export the kubeconfig path. -``` +```shell export KUBECONFIG_PATH="${HOME}/.kube/config" ``` Start the Fabric test network in the `.fabric/test-network-nano-bash` directory. -``` +```shell ./network.sh start ``` @@ -67,7 +73,7 @@ curl -fsSL \ Set up the environment for running peer commands and check everything is working. -``` +```shell . ./peer1admin.sh peer channel list ``` diff --git a/cmd/run/main_test.go b/cmd/run/main_test.go index 86a8cf4..a112639 100644 --- a/cmd/run/main_test.go +++ b/cmd/run/main_test.go @@ -4,8 +4,8 @@ import ( "fmt" "os" "os/exec" + "time" - "github.com/bitfield/script" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/gbytes" @@ -68,7 +68,7 @@ var _ = Describe("Main", func() { ) It( - "should start a chaincode pod using the supplied configuration environment variables", + "should start a chaincode job using the supplied configuration environment variables", Label("kind"), func() { homedir, err := os.UserHomeDir() @@ -95,21 +95,28 @@ var _ = Describe("Main", func() { ).Should(gbytes.Say(`run \[\d+\] DEBUG: FABRIC_K8S_BUILDER_SERVICE_ACCOUNT=chaincode`)) Eventually( session.Err, - ).Should(gbytes.Say(`run \[\d+\]: Running chaincode ID CHAINCODE_LABEL:CHAINCODE_HASH in kubernetes pod chaincode/hlfcc-chaincodelabel-f15ukm9v906aq`)) + ).Should(gbytes.Say(`run \[\d+\]: Running chaincode ID CHAINCODE_LABEL:6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45 with kubernetes job chaincode/hlfcc-chaincodelabel-piihcaj6ryttc`)) - pipe := script.Exec( - "kubectl wait --for=condition=ready pod --timeout=120s --namespace=chaincode -l fabric-builder-k8s-peerid=core-peer-id-abcdefghijklmnopqrstuvwxyz-0123456789", - ) - _, err = pipe.Stdout() + waitArgs := []string{ + "wait", + "--for=condition=ready", + "job", + "--timeout=120s", + "--namespace=chaincode", + "-l", + "fabric-builder-k8s-cchash=N6MMJOZJIFDXCMJO3XI2QE7O6WB56IJBYI24I6LXSLYUDJDNJNCQ", + } + waitCommand := exec.Command("kubectl", waitArgs...) + waitSession, err := gexec.Start(waitCommand, GinkgoWriter, GinkgoWriter) Expect(err).NotTo(HaveOccurred()) - Expect(pipe.ExitStatus()).To(Equal(0)) + Eventually(waitSession).WithTimeout(240 * time.Second).Should(gexec.Exit(0)) descArgs := []string{ "describe", - "pod", + "job", "--namespace=chaincode", "-l", - "fabric-builder-k8s-peerid=core-peer-id-abcdefghijklmnopqrstuvwxyz-0123456789", + "fabric-builder-k8s-cchash=N6MMJOZJIFDXCMJO3XI2QE7O6WB56IJBYI24I6LXSLYUDJDNJNCQ", } descCommand := exec.Command("kubectl", descArgs...) descSession, err := gexec.Start(descCommand, GinkgoWriter, GinkgoWriter) @@ -117,11 +124,13 @@ var _ = Describe("Main", func() { Eventually(descSession).Should(gexec.Exit(0)) Eventually(descSession.Out).Should(gbytes.Say(`Namespace:\s+chaincode`)) - Eventually(descSession.Out).Should(gbytes.Say(`fabric-builder-k8s-mspid=MSPID`)) Eventually( descSession.Out, - ).Should(gbytes.Say(`fabric-builder-k8s-ccid:\s+CHAINCODE_LABEL:CHAINCODE_HASH`)) - Eventually(descSession.Out).Should(gbytes.Say(`CORE_CHAINCODE_ID_NAME:\s+CHAINCODE_LABEL:CHAINCODE_HASH`)) + ).Should(gbytes.Say(`fabric-builder-k8s-ccid:\s+CHAINCODE_LABEL:6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45`)) + Eventually(descSession.Out).Should(gbytes.Say(`fabric-builder-k8s-mspid:\s+MSPID`)) + Eventually(descSession.Out).Should(gbytes.Say(`fabric-builder-k8s-peeraddress:\s+PEER_ADDRESS`)) + Eventually(descSession.Out).Should(gbytes.Say(`fabric-builder-k8s-peerid:\s+core-peer-id-abcdefghijklmnopqrstuvwxyz-0123456789`)) + Eventually(descSession.Out).Should(gbytes.Say(`CORE_CHAINCODE_ID_NAME:\s+CHAINCODE_LABEL:6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45`)) Eventually(descSession.Out).Should(gbytes.Say(`CORE_PEER_ADDRESS:\s+PEER_ADDRESS`)) Eventually(descSession.Out).Should(gbytes.Say(`CORE_PEER_LOCALMSPID:\s+MSPID`)) }, diff --git a/cmd/run/testdata/validchaincode/chaincode.json b/cmd/run/testdata/validchaincode/chaincode.json index 53ffb72..764268b 100644 --- a/cmd/run/testdata/validchaincode/chaincode.json +++ b/cmd/run/testdata/validchaincode/chaincode.json @@ -1,5 +1,5 @@ { - "chaincode_id": "CHAINCODE_LABEL:CHAINCODE_HASH", + "chaincode_id": "CHAINCODE_LABEL:6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45", "peer_address": "PEER_ADDRESS", "client_cert": "CLIENT_CERT", "client_key": "CLIENT_KEY", diff --git a/internal/builder/run.go b/internal/builder/run.go index 2708abd..3e10128 100644 --- a/internal/builder/run.go +++ b/internal/builder/run.go @@ -34,7 +34,7 @@ func (r *Run) Run(ctx context.Context) error { return err } - kubeObjectName := util.GetValidRfc1035LabelName(r.KubeNamePrefix, r.PeerID, chaincodeData) + kubeObjectName := util.GetValidRfc1035LabelName(r.KubeNamePrefix, r.PeerID, chaincodeData, util.ObjectNameSuffixLength+1) clientset, err := util.GetKubeClientset(logger, r.KubeconfigPath) if err != nil { @@ -64,12 +64,12 @@ func (r *Run) Run(ctx context.Context) error { ) } - podsClient := clientset.CoreV1().Pods(r.KubeNamespace) + jobsClient := clientset.BatchV1().Jobs(r.KubeNamespace) - pod, err := util.CreateChaincodePod( + job, err := util.CreateChaincodeJob( ctx, logger, - podsClient, + jobsClient, kubeObjectName, r.KubeNamespace, r.KubeServiceAccount, @@ -82,11 +82,13 @@ func (r *Run) Run(ctx context.Context) error { } logger.Printf( - "Running chaincode ID %s in kubernetes pod %s/%s", + "Running chaincode ID %s with kubernetes job %s/%s", chaincodeData.ChaincodeID, - pod.Namespace, - pod.Name, + job.Namespace, + job.Name, ) - return util.WaitForChaincodePod(ctx, logger, podsClient, pod, chaincodeData.ChaincodeID) + batchClient := clientset.BatchV1().RESTClient() + + return util.WaitForChaincodeJob(ctx, logger, batchClient, job, chaincodeData.ChaincodeID) } diff --git a/internal/util/k8s.go b/internal/util/k8s.go index 4aa2593..e7999eb 100644 --- a/internal/util/k8s.go +++ b/internal/util/k8s.go @@ -6,6 +6,7 @@ import ( "context" "encoding/base32" "encoding/base64" + "encoding/hex" "fmt" "hash/fnv" "os" @@ -14,22 +15,27 @@ import ( "time" "github.com/hyperledger-labs/fabric-builder-k8s/internal/log" + batchv1 "k8s.io/api/batch/v1" apiv1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/rand" "k8s.io/apimachinery/pkg/watch" applycorev1 "k8s.io/client-go/applyconfigurations/core/v1" "k8s.io/client-go/kubernetes" + typedBatchv1 "k8s.io/client-go/kubernetes/typed/batch/v1" v1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/clientcmd" watchtools "k8s.io/client-go/tools/watch" + "k8s.io/utils/ptr" ) const ( namespacePath = "/var/run/secrets/kubernetes.io/serviceaccount/namespace" + jobTTL = 5 * time.Minute + + ObjectNameSuffixLength int = 5 // Defaults. DefaultNamespace string = "default" @@ -44,154 +50,98 @@ const ( TLSClientRootCertFile string = "/etc/hyperledger/fabric/peer.crt" ) -func waitForPod( +func waitForJob( ctx context.Context, - timeout time.Duration, - podsClient v1.PodInterface, - podName, namespace string, + // jobsClient typedBatchv1.JobInterface, + client cache.Getter, + jobName, namespace string, conditionFunc watchtools.ConditionFunc, -) (*apiv1.PodStatus, error) { - fieldSelector := fields.OneTermEqualSelector("metadata.name", podName).String() - - listWatch := &cache.ListWatch{ - ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { - options.FieldSelector = fieldSelector +) (*batchv1.JobStatus, error) { + fieldSelector := fields.OneTermEqualSelector("metadata.name", jobName) + listWatch := cache.NewListWatchFromClient(client, "jobs", namespace, fieldSelector) - return podsClient.List(context.TODO(), options) - }, - WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { - options.FieldSelector = fieldSelector - - return podsClient.Watch(context.TODO(), options) - }, - } - - // TODO it might be nice to use NewListWatchFromClient instead but not sure what - // client to give it to avoid forbidden errors for pod list. - // var client kubernetes.Interface - // listWatch := cache.NewListWatchFromClient(client, "pods", namespace, fieldSelector) - - ctx, cancel := watchtools.ContextWithOptionalTimeout(ctx, timeout) + ctx, cancel := watchtools.ContextWithOptionalTimeout(ctx, 0) defer cancel() - event, err := watchtools.UntilWithSync(ctx, listWatch, &apiv1.Pod{}, nil, conditionFunc) + event, err := watchtools.UntilWithSync(ctx, listWatch, &batchv1.Job{}, nil, conditionFunc) if err != nil { return nil, err } if event == nil { - return nil, fmt.Errorf("no events received for pod %s/%s", namespace, podName) + return nil, fmt.Errorf("no events received for job %s/%s", namespace, jobName) } - pod, ok := event.Object.(*apiv1.Pod) + job, ok := event.Object.(*batchv1.Job) if !ok { - return nil, fmt.Errorf("unexpected object while watching pod %s/%s", namespace, podName) + return nil, fmt.Errorf("event contained unexpected object %T while watching job %s/%s", job, namespace, jobName) } - return &pod.Status, nil + return &job.Status, nil } -func waitForPodRunning( +func waitForJobTermination( ctx context.Context, - timeout time.Duration, - podsClient v1.PodInterface, - podName, namespace string, -) (*apiv1.PodStatus, error) { - podRunningCondition := func(event watch.Event) (bool, error) { - pod, ok := event.Object.(*apiv1.Pod) - if !ok { - return false, fmt.Errorf( - "unexpected object while watching pod %s/%s", - namespace, - podName, - ) - } - - phase := pod.Status.Phase - if phase == apiv1.PodRunning { - return true, nil - } - - return false, nil - } - - return waitForPod(ctx, timeout, podsClient, podName, namespace, podRunningCondition) -} - -func waitForPodTermination( - ctx context.Context, - timeout time.Duration, - podsClient v1.PodInterface, - podName, namespace string, -) (*apiv1.PodStatus, error) { - podTerminationCondition := func(event watch.Event) (bool, error) { + // jobsClient typedBatchv1.JobInterface, + client cache.Getter, + jobName, namespace string, +) (*batchv1.JobStatus, error) { + jobTerminationCondition := func(event watch.Event) (bool, error) { if event.Type == watch.Deleted { return true, nil } - pod, ok := event.Object.(*apiv1.Pod) + job, ok := event.Object.(*batchv1.Job) if !ok { return false, fmt.Errorf( - "unexpected object while watching pod %s/%s", + "event contained unexpected object %T while watching job %s/%s", + job, namespace, - podName, + jobName, ) } - phase := pod.Status.Phase - if phase != apiv1.PodRunning { - return true, nil + for _, c := range job.Status.Conditions { + if c.Type == batchv1.JobComplete && c.Status == "True" { + return true, nil + } else if c.Type == batchv1.JobFailed && c.Status == "True" { + return true, fmt.Errorf("job %s/%s failed for reason %s: %s", namespace, jobName, c.Reason, c.Message) + } } return false, nil } - return waitForPod(ctx, timeout, podsClient, podName, namespace, podTerminationCondition) + return waitForJob(ctx, client, jobName, namespace, jobTerminationCondition) } -func WaitForChaincodePod( +func WaitForChaincodeJob( ctx context.Context, logger *log.CmdLogger, - podsClient v1.PodInterface, - pod *apiv1.Pod, + // jobsClient typedBatchv1.JobInterface, + client cache.Getter, + job *batchv1.Job, chaincodeID string, ) error { - logger.Debugf("Waiting for pod %s/%s for chaincode ID %s", pod.Namespace, pod.Name, chaincodeID) - - _, err := waitForPodRunning(ctx, time.Minute, podsClient, pod.Name, pod.Namespace) - if err != nil { - return fmt.Errorf( - "error waiting for chaincode pod %s/%s for chaincode ID %s: %w", - pod.Namespace, - pod.Name, - chaincodeID, - err, - ) - } + logger.Debugf("Waiting for job %s/%s to terminate for chaincode ID %s", job.Namespace, job.Name, chaincodeID) - status, err := waitForPodTermination(ctx, 0, podsClient, pod.Name, pod.Namespace) + _, err := waitForJobTermination(ctx, client, job.Name, job.Namespace) if err != nil { return fmt.Errorf( - "error waiting for chaincode pod %s/%s to terminate for chaincode ID %s: %w", - pod.Namespace, - pod.Name, + "error waiting for chaincode job %s/%s to terminate for chaincode ID %s: %w", + job.Namespace, + job.Name, chaincodeID, err, ) } - if status != nil { - return fmt.Errorf( - "chaincode pod %s/%s for chaincode ID %s terminated %s: %s", - pod.Namespace, - pod.Name, - chaincodeID, - status.Reason, - status.Message, - ) - } - - return fmt.Errorf("unexpected chaincode pod termination for chaincode ID %s", chaincodeID) + return fmt.Errorf( + "chaincode job %s/%s for chaincode ID %s terminated", + job.Namespace, + job.Name, + chaincodeID, + ) } // GetKubeClientset returns a client object for a provided kubeconfig filepath @@ -226,104 +176,140 @@ func GetKubeNamespace() (string, error) { return string(namespace), nil } -func getChaincodePodObject( +func getLabels(chaincodeData *ChaincodeJSON) (map[string]string, error) { + packageID := NewChaincodePackageID(chaincodeData.ChaincodeID) + + packageHashBytes, err := hex.DecodeString(packageID.Hash) + if err != nil { + return nil, fmt.Errorf("error decoding chaincode package hash %s: %w", packageID.Hash, err) + } + + encodedPackageHash := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(packageHashBytes) + + return map[string]string{ + "app.kubernetes.io/name": "hyperledger-fabric", + "app.kubernetes.io/component": "chaincode", + "app.kubernetes.io/created-by": "fabric-builder-k8s", + "app.kubernetes.io/managed-by": "fabric-builder-k8s", + "fabric-builder-k8s-cchash": encodedPackageHash, + }, nil +} + +func getAnnotations(peerID string, chaincodeData *ChaincodeJSON) map[string]string { + return map[string]string{ + "fabric-builder-k8s-ccid": chaincodeData.ChaincodeID, + "fabric-builder-k8s-mspid": chaincodeData.MspID, + "fabric-builder-k8s-peeraddress": chaincodeData.PeerAddress, + "fabric-builder-k8s-peerid": peerID, + } +} + +func getChaincodeJobSpec( imageData *ImageJSON, - namespace, serviceAccount, podName, peerID string, + namespace, serviceAccount, objectName, peerID string, chaincodeData *ChaincodeJSON, -) *apiv1.Pod { +) (*batchv1.Job, error) { chaincodeImage := imageData.Name + "@" + imageData.Digest - return &apiv1.Pod{ + jobName := objectName + "-" + rand.String(ObjectNameSuffixLength) + + labels, err := getLabels(chaincodeData) + if err != nil { + return nil, fmt.Errorf("error getting chaincode job labels for chaincode ID %s: %w", chaincodeData.ChaincodeID, err) + } + + annotations := getAnnotations(peerID, chaincodeData) + + return &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ - Name: podName, - Namespace: namespace, - Labels: map[string]string{ - "app.kubernetes.io/name": "fabric", - "app.kubernetes.io/component": "chaincode", - "app.kubernetes.io/created-by": "fabric-builder-k8s", - "app.kubernetes.io/managed-by": "fabric-builder-k8s", - "fabric-builder-k8s-mspid": chaincodeData.MspID, - "fabric-builder-k8s-peerid": peerID, - }, - Annotations: map[string]string{ - "fabric-builder-k8s-ccid": chaincodeData.ChaincodeID, - }, + Name: jobName, + Namespace: namespace, + Labels: labels, + Annotations: annotations, }, - Spec: apiv1.PodSpec{ - ServiceAccountName: serviceAccount, - Containers: []apiv1.Container{ - { - Name: "main", - Image: chaincodeImage, - VolumeMounts: []apiv1.VolumeMount{ + Spec: batchv1.JobSpec{ + Template: apiv1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + Annotations: annotations, + }, + Spec: apiv1.PodSpec{ + ServiceAccountName: serviceAccount, + Containers: []apiv1.Container{ { - Name: "certs", - MountPath: "/etc/hyperledger/fabric", - ReadOnly: true, + Name: "chaincode", + Image: chaincodeImage, + VolumeMounts: []apiv1.VolumeMount{ + { + Name: "certs", + MountPath: "/etc/hyperledger/fabric", + ReadOnly: true, + }, + }, + Env: []apiv1.EnvVar{ + { + Name: "CORE_CHAINCODE_ID_NAME", + Value: chaincodeData.ChaincodeID, + }, + { + Name: "CORE_PEER_ADDRESS", + Value: chaincodeData.PeerAddress, + }, + { + Name: "CORE_PEER_TLS_ENABLED", + Value: "true", // TODO only if there are certs? + }, + { + Name: "CORE_PEER_TLS_ROOTCERT_FILE", + Value: TLSClientRootCertFile, + }, + { + Name: "CORE_TLS_CLIENT_KEY_PATH", + Value: TLSClientKeyPath, + }, + { + Name: "CORE_TLS_CLIENT_CERT_PATH", + Value: TLSClientCertPath, + }, + { + Name: "CORE_TLS_CLIENT_KEY_FILE", + Value: TLSClientKeyFile, + }, + { + Name: "CORE_TLS_CLIENT_CERT_FILE", + Value: TLSClientCertFile, + }, + { + Name: "CORE_PEER_LOCALMSPID", + Value: chaincodeData.MspID, + }, + }, }, }, - Env: []apiv1.EnvVar{ - { - Name: "CORE_CHAINCODE_ID_NAME", - Value: chaincodeData.ChaincodeID, - }, - { - Name: "CORE_PEER_ADDRESS", - Value: chaincodeData.PeerAddress, - }, - { - Name: "CORE_PEER_TLS_ENABLED", - Value: "true", // TODO only if there are certs? - }, + RestartPolicy: apiv1.RestartPolicyNever, + Volumes: []apiv1.Volume{ { - Name: "CORE_PEER_TLS_ROOTCERT_FILE", - Value: TLSClientRootCertFile, - }, - { - Name: "CORE_TLS_CLIENT_KEY_PATH", - Value: TLSClientKeyPath, - }, - { - Name: "CORE_TLS_CLIENT_CERT_PATH", - Value: TLSClientCertPath, - }, - { - Name: "CORE_TLS_CLIENT_KEY_FILE", - Value: TLSClientKeyFile, - }, - { - Name: "CORE_TLS_CLIENT_CERT_FILE", - Value: TLSClientCertFile, - }, - { - Name: "CORE_PEER_LOCALMSPID", - Value: chaincodeData.MspID, - }, - }, - }, - }, - RestartPolicy: apiv1.RestartPolicyNever, - Volumes: []apiv1.Volume{ - { - Name: "certs", - VolumeSource: apiv1.VolumeSource{ - Secret: &apiv1.SecretVolumeSource{ - SecretName: podName, + Name: "certs", + VolumeSource: apiv1.VolumeSource{ + Secret: &apiv1.SecretVolumeSource{ + SecretName: objectName, + }, + }, }, }, }, }, + BackoffLimit: ptr.To[int32](0), + TTLSecondsAfterFinished: ptr.To[int32](int32(jobTTL / time.Second)), }, - } + }, nil } func getChaincodeSecretApplyConfiguration( secretName, namespace, peerID string, chaincodeData *ChaincodeJSON, -) *applycorev1.SecretApplyConfiguration { - annotations := map[string]string{ - "fabric-builder-k8s-ccid": chaincodeData.ChaincodeID, - } +) (*applycorev1.SecretApplyConfiguration, error) { + annotations := getAnnotations(peerID, chaincodeData) data := map[string]string{ "peer.crt": chaincodeData.RootCert, @@ -333,13 +319,9 @@ func getChaincodeSecretApplyConfiguration( "client.key": base64.StdEncoding.EncodeToString([]byte(chaincodeData.ClientKey)), } - labels := map[string]string{ - "app.kubernetes.io/name": "fabric", - "app.kubernetes.io/component": "chaincode", - "app.kubernetes.io/created-by": "fabric-builder-k8s", - "app.kubernetes.io/managed-by": "fabric-builder-k8s", - "fabric-builder-k8s-mspid": chaincodeData.MspID, - "fabric-builder-k8s-peerid": peerID, + labels, err := getLabels(chaincodeData) + if err != nil { + return nil, fmt.Errorf("error getting chaincode secret labels for chaincode ID %s: %w", chaincodeData.ChaincodeID, err) } return applycorev1. @@ -347,7 +329,7 @@ func getChaincodeSecretApplyConfiguration( WithAnnotations(annotations). WithLabels(labels). WithStringData(data). - WithType(apiv1.SecretTypeOpaque) + WithType(apiv1.SecretTypeOpaque), nil } func ApplyChaincodeSecrets( @@ -357,7 +339,10 @@ func ApplyChaincodeSecrets( secretName, namespace, peerID string, chaincodeData *ChaincodeJSON, ) error { - secret := getChaincodeSecretApplyConfiguration(secretName, namespace, peerID, chaincodeData) + secret, err := getChaincodeSecretApplyConfiguration(secretName, namespace, peerID, chaincodeData) + if err != nil { + return fmt.Errorf("error getting chaincode secret definition for chaincode ID %s: %w", chaincodeData.ChaincodeID, err) + } result, err := secretsClient.Apply( ctx, @@ -365,7 +350,7 @@ func ApplyChaincodeSecrets( metav1.ApplyOptions{FieldManager: "fabric-builder-k8s"}, ) if err != nil { - return err + return fmt.Errorf("error applying chaincode secret definition for chaincode ID %s: %w", chaincodeData.ChaincodeID, err) } logger.Debugf( @@ -378,67 +363,15 @@ func ApplyChaincodeSecrets( return nil } -func deleteChaincodePod( - ctx context.Context, - logger *log.CmdLogger, - podsClient v1.PodInterface, - podName, namespace string, - chaincodeData *ChaincodeJSON, -) error { - logger.Debugf( - "Deleting any existing chaincode pod for chaincode ID %s: %s/%s", - chaincodeData.ChaincodeID, - namespace, - podName, - ) - - err := podsClient.Delete(ctx, podName, metav1.DeleteOptions{}) - if err != nil { - if errors.IsNotFound(err) { - logger.Debugf( - "No existing chaincode pod for chaincode ID %s: %s/%s", - chaincodeData.ChaincodeID, - namespace, - podName, - ) - - return nil - } - - return err - } - - logger.Debugf( - "Waiting for existing chaincode pod to terminate for chaincode ID %s: %s/%s", - chaincodeData.ChaincodeID, - namespace, - podName, - ) - - _, err = waitForPodTermination(ctx, time.Minute, podsClient, podName, namespace) - if err != nil { - return err - } - - logger.Debugf( - "Existing chaincode pod deleted for chaincode ID %s: %s/%s", - chaincodeData.ChaincodeID, - namespace, - podName, - ) - - return nil -} - -func CreateChaincodePod( +func CreateChaincodeJob( ctx context.Context, logger *log.CmdLogger, - podsClient v1.PodInterface, + jobsClient typedBatchv1.JobInterface, objectName, namespace, serviceAccount, peerID string, chaincodeData *ChaincodeJSON, imageData *ImageJSON, -) (*apiv1.Pod, error) { - podDefinition := getChaincodePodObject( +) (*batchv1.Job, error) { + jobDefinition, err := getChaincodeJobSpec( imageData, namespace, serviceAccount, @@ -446,29 +379,23 @@ func CreateChaincodePod( peerID, chaincodeData, ) - - err := deleteChaincodePod(ctx, logger, podsClient, objectName, namespace, chaincodeData) if err != nil { - return nil, fmt.Errorf( - "unable to delete existing chaincode pod %s/%s for chaincode ID %s: %w", - namespace, - objectName, - chaincodeData.ChaincodeID, - err, - ) + return nil, fmt.Errorf("error getting chaincode job definition for chaincode ID %s: %w", chaincodeData.ChaincodeID, err) } + jobName := jobDefinition.ObjectMeta.Name + logger.Debugf( - "Creating chaincode pod for chaincode ID %s: %s/%s", + "Creating chaincode job for chaincode ID %s: %s/%s", chaincodeData.ChaincodeID, namespace, - objectName, + jobName, ) - pod, err := podsClient.Create(ctx, podDefinition, metav1.CreateOptions{}) + job, err := jobsClient.Create(ctx, jobDefinition, metav1.CreateOptions{}) if err != nil { return nil, fmt.Errorf( - "unable to create chaincode pod %s/%s for chaincode ID %s: %w", + "error creating chaincode job %s/%s for chaincode ID %s: %w", namespace, objectName, chaincodeData.ChaincodeID, @@ -477,18 +404,18 @@ func CreateChaincodePod( } logger.Debugf( - "Created chaincode pod for chaincode ID %s: %s/%s", + "Created chaincode job for chaincode ID %s: %s/%s", chaincodeData.ChaincodeID, - pod.Namespace, - pod.Name, + job.Namespace, + job.Name, ) - return pod, nil + return job, nil } // GetValidRfc1035LabelName returns a valid RFC 1035 label name with the format // --. -func GetValidRfc1035LabelName(prefix, peerID string, chaincodeData *ChaincodeJSON) string { +func GetValidRfc1035LabelName(prefix, peerID string, chaincodeData *ChaincodeJSON, suffixLen int) string { const ( maxRfc1035LabelLength = 63 labelSeparators = 2 @@ -500,7 +427,7 @@ func GetValidRfc1035LabelName(prefix, peerID string, chaincodeData *ChaincodeJSO runHash.Write([]byte(chaincodeData.PeerAddress)) runHash.Write([]byte(chaincodeData.MspID)) runHash.Write([]byte(chaincodeData.ChaincodeID)) - suffix := strings.ToLower(base32.HexEncoding.WithPadding(base32.NoPadding).EncodeToString(runHash.Sum(nil))) + runHashString := strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(runHash.Sum(nil))) // Remove unsafe characters from the chaincode package label packageID := NewChaincodePackageID(chaincodeData.ChaincodeID) @@ -508,11 +435,12 @@ func GetValidRfc1035LabelName(prefix, peerID string, chaincodeData *ChaincodeJSO safeLabel := re.ReplaceAllString(strings.ToLower(packageID.Label), "") // Make sure the chaincode package label fits in the space available, - // taking in to account the prefix, suffix, and two '-' separators - maxLabelLength := maxRfc1035LabelLength - len(prefix) - len(suffix) - labelSeparators + // taking in to account the prefix, runHashString, two '-' separators, + // and any required space for a suffix + maxLabelLength := maxRfc1035LabelLength - len(prefix) - len(runHashString) - labelSeparators - suffixLen if maxLabelLength < len(safeLabel) { safeLabel = safeLabel[:maxLabelLength] } - return prefix + "-" + safeLabel + "-" + suffix + return prefix + "-" + safeLabel + "-" + runHashString } diff --git a/internal/util/k8s_test.go b/internal/util/k8s_test.go index 6ab4a62..ccad4d9 100644 --- a/internal/util/k8s_test.go +++ b/internal/util/k8s_test.go @@ -14,17 +14,27 @@ var _ = Describe("K8s", func() { PeerAddress: "peer0.org1.example.com", MspID: "CongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaOrgMsp", } - name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "CongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaOrgPeer0", chaincodeData) + name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "CongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaOrgPeer0", chaincodeData, 0) Expect(len(name)).To(Equal(63)) }) + It("should return names with a maximum of 57 characters if a 6 character suffix is required", func() { + chaincodeData := &util.ChaincodeJSON{ + ChaincodeID: "fabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcar:cffa266294278404e5071cb91150d550dc0bf855149908a170b1169d6160004b", + PeerAddress: "peer0.org1.example.com", + MspID: "CongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaOrgMsp", + } + name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "CongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaOrgPeer0", chaincodeData, 6) + Expect(len(name)).To(Equal(57)) + }) + It("should return names which starts with an alphabetic character", func() { chaincodeData := &util.ChaincodeJSON{ ChaincodeID: "fabcar:cffa266294278404e5071cb91150d550dc0bf855149908a170b1169d6160004b", PeerAddress: "peer0.org1.example.com", MspID: "GreenCongaOrg", } - name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "GreenCongaOrgPeer0", chaincodeData) + name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "GreenCongaOrgPeer0", chaincodeData, 0) Expect(name).To(MatchRegexp("^[a-z]")) }) @@ -34,7 +44,7 @@ var _ = Describe("K8s", func() { PeerAddress: "peer0.org1.example.com", MspID: "BlueCongaOrg", } - name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "BlueCongaOrgPeer0", chaincodeData) + name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "BlueCongaOrgPeer0", chaincodeData, 0) Expect(name).To(MatchRegexp("[a-z0-9]$")) }) @@ -44,7 +54,7 @@ var _ = Describe("K8s", func() { PeerAddress: "peer0.org1.example.com", MspID: "BlueCongaOrg", } - name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "BlueCongaOrgPeer0", chaincodeData) + name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "BlueCongaOrgPeer0", chaincodeData, 0) Expect(name).To(MatchRegexp("^(?:[a-z0-9]|-)+$")) }) @@ -59,8 +69,8 @@ var _ = Describe("K8s", func() { PeerAddress: "peer0.org2.example.org", MspID: "BlueCongaOrg", } - name1 := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "GreenCongaOrgPeer0", chaincodeData1) - name2 := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "BlueCongaOrgPeer0", chaincodeData2) + name1 := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "GreenCongaOrgPeer0", chaincodeData1, 0) + name2 := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "BlueCongaOrgPeer0", chaincodeData2, 0) Expect(name1).NotTo(Equal(name2)) }) @@ -75,8 +85,8 @@ var _ = Describe("K8s", func() { PeerAddress: "peer0.org1.example.com", MspID: "RedCongaOrg", } - name1 := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "RedCongaOrg", chaincodeData1) - name2 := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "RedCongaOrg", chaincodeData2) + name1 := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "RedCongaOrg", chaincodeData1, 0) + name2 := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "RedCongaOrg", chaincodeData2, 0) Expect(name1).NotTo(Equal(name2)) }) @@ -86,8 +96,8 @@ var _ = Describe("K8s", func() { PeerAddress: "peer0.org1.example.com", MspID: "CongaOrg", } - name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "CongaOrgPeer0", chaincodeData) - Expect(name).To(Equal("hlf-k8sbuilder-ftw-fabcar-iufmagu14f8q4")) + name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "CongaOrgPeer0", chaincodeData, 0) + Expect(name).To(Equal("hlf-k8sbuilder-ftw-fabcar-s6pwkq6bepi2e")) }) It("should return names which start with the specified prefix and a safe version of the chaincode label", func() { @@ -96,18 +106,18 @@ var _ = Describe("K8s", func() { PeerAddress: "peer0.org1.example.com", MspID: "CongaOrg", } - name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "CongaOrgPeer0", chaincodeData) + name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "CongaOrgPeer0", chaincodeData, 0) Expect(name).To(HavePrefix("hlf-k8sbuilder-ftw" + "-fabcar-")) }) - It("should return names which end with a 13 character extended hex hash string", func() { + It("should return names which end with a 13 character lowercase base32 encoded hash string", func() { chaincodeData := &util.ChaincodeJSON{ ChaincodeID: "fabcar:cffa266294278404e5071cb91150d550dc0bf855149908a170b1169d6160004b", PeerAddress: "peer0.org1.example.com", MspID: "CongaOrg", } - name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "CongaOrgPeer0", chaincodeData) - Expect(name).To(MatchRegexp("-[0-9a-v]{13}$")) + name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "CongaOrgPeer0", chaincodeData, 0) + Expect(name).To(MatchRegexp("-[a-z2-7]{13}$")) }) It("should return names with the full prefix and hash, and a truncated chaincode label", func() { @@ -116,8 +126,8 @@ var _ = Describe("K8s", func() { PeerAddress: "peer0.org1.example.com", MspID: "CongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaOrgMsp", } - name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "CongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaOrgPeer0", chaincodeData) - Expect(name).To(Equal("hlf-k8sbuilder-ftw-fabfabfabfabcarfabfabfabfabcar-1sufvsaso6m7u")) + name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "CongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaOrgPeer0", chaincodeData, 0) + Expect(name).To(Equal("hlf-k8sbuilder-ftw-fabfabfabfabcarfabfabfabfabcar-b46p74k4ygwh6")) }) }) })