From 37d74881f7cbb8584c4d77e6f2d11cc081abe51c Mon Sep 17 00:00:00 2001 From: James Taylor Date: Wed, 8 May 2024 11:01:46 +0000 Subject: [PATCH] Run chaincode using jobs See #119 Signed-off-by: James Taylor --- CONTRIBUTING.md | 18 +- cmd/run/main_test.go | 29 +-- internal/builder/run.go | 18 +- internal/util/k8s.go | 390 ++++++++++++++------------------------ internal/util/k8s_test.go | 42 ++-- 5 files changed, 209 insertions(+), 288 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 91ea521..ddc1369 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. +[Configure kubernetes permissions](https://labs.hyperledger.org/fabric-builder-k8s/configuring/kubernetes-permissions/) 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 9888514..61174f2 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,18 +95,25 @@ 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:6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45 in kubernetes pod chaincode/hlfcc-chaincodelabel-f887209uhojj2`)) + ).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-cclabel=CHAINCODE_LABEL", - ) - _, err = pipe.Stdout() + waitArgs := []string{ + "wait", + "--for=jsonpath=.status.ready=1", + "job", + "--timeout=120s", + "--namespace=chaincode", + "-l", + "fabric-builder-k8s-cclabel=CHAINCODE_LABEL", + } + 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-cclabel=CHAINCODE_LABEL", @@ -123,9 +130,7 @@ var _ = Describe("Main", func() { 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_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/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 cc860f5..aed324b 100644 --- a/internal/util/k8s.go +++ b/internal/util/k8s.go @@ -15,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" @@ -45,154 +50,95 @@ const ( TLSClientRootCertFile string = "/etc/hyperledger/fabric/peer.crt" ) -func waitForPod( +func waitForJob( ctx context.Context, - timeout time.Duration, - podsClient v1.PodInterface, - podName, namespace string, + 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 - - return podsClient.List(context.TODO(), options) - }, - WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { - options.FieldSelector = fieldSelector - - return podsClient.Watch(context.TODO(), options) - }, - } +) (*batchv1.JobStatus, error) { + fieldSelector := fields.OneTermEqualSelector("metadata.name", jobName) + listWatch := cache.NewListWatchFromClient(client, "jobs", namespace, fieldSelector) - // 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 &pod.Status, nil -} - -func waitForPodRunning( - 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 nil, fmt.Errorf("event contained unexpected object %T while watching job %s/%s", job, namespace, jobName) } - return waitForPod(ctx, timeout, podsClient, podName, namespace, podRunningCondition) + return &job.Status, nil } -func waitForPodTermination( +func waitForJobTermination( ctx context.Context, - timeout time.Duration, - podsClient v1.PodInterface, - podName, namespace string, -) (*apiv1.PodStatus, error) { - podTerminationCondition := func(event watch.Event) (bool, error) { + 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, + 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) + logger.Debugf("Waiting for job %s/%s to terminate for chaincode ID %s", job.Namespace, job.Name, chaincodeID) - _, err := waitForPodRunning(ctx, time.Minute, 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 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, ) } - status, err := waitForPodTermination(ctx, 0, podsClient, pod.Name, pod.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, - 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 @@ -256,13 +202,15 @@ func getAnnotations(peerID string, chaincodeData *ChaincodeJSON) map[string]stri } } -func getChaincodePodObject( +func getChaincodeJobSpec( imageData *ImageJSON, - namespace, serviceAccount, podName, peerID string, + namespace, serviceAccount, objectName, peerID string, chaincodeData *ChaincodeJSON, -) (*apiv1.Pod, error) { +) (*batchv1.Job, error) { chaincodeImage := imageData.Name + "@" + imageData.Digest + 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) @@ -270,77 +218,87 @@ func getChaincodePodObject( annotations := getAnnotations(peerID, chaincodeData) - return &apiv1.Pod{ + return &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ - Name: podName, + 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? - }, - { - Name: "CORE_PEER_TLS_ROOTCERT_FILE", - Value: TLSClientRootCertFile, - }, - { - Name: "CORE_TLS_CLIENT_KEY_PATH", - Value: TLSClientKeyPath, - }, - { - Name: "CORE_TLS_CLIENT_CERT_PATH", - Value: TLSClientCertPath, - }, + RestartPolicy: apiv1.RestartPolicyNever, + Volumes: []apiv1.Volume{ { - 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 } @@ -390,7 +348,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( @@ -403,67 +361,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, err := getChaincodePodObject( +) (*batchv1.Job, error) { + jobDefinition, err := getChaincodeJobSpec( imageData, namespace, serviceAccount, @@ -472,31 +378,22 @@ func CreateChaincodePod( chaincodeData, ) if err != nil { - return nil, fmt.Errorf("error getting chaincode pod definition for chaincode ID %s: %w", chaincodeData.ChaincodeID, err) + return nil, fmt.Errorf("error getting chaincode job definition for chaincode ID %s: %w", chaincodeData.ChaincodeID, err) } - 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, - ) - } + 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, @@ -505,18 +402,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 { +// -- and space for a suffix if required. +func GetValidRfc1035LabelName(prefix, peerID string, chaincodeData *ChaincodeJSON, suffixLen int) string { const ( maxRfc1035LabelLength = 63 labelSeparators = 2 @@ -528,7 +425,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) @@ -536,11 +433,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")) }) }) })