Skip to content

Commit

Permalink
Make sure Kubernetes object labels are valid
Browse files Browse the repository at this point in the history
Base32 encode the package hash and validate the chaincode label in the
build phase

Move labels that cannot be guaranteed to be valid to annotations
instead

Closes #89

Signed-off-by: James Taylor <jamest@uk.ibm.com>
  • Loading branch information
jt-nti committed May 24, 2024
1 parent 91576a5 commit 7f78c09
Show file tree
Hide file tree
Showing 17 changed files with 162 additions and 70 deletions.
22 changes: 17 additions & 5 deletions cmd/build/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,26 @@ var _ = Describe("Main", func() {

Eventually(session).Should(gexec.Exit(expectedErrorCode))
},
Entry("When the image.json file exists", 0, func() []string {
return []string{"./testdata/validimage", "CHAINCODE_METADATA_DIR", tempDir}
Entry("When the image.json and metadata.json files exist", 0, func() []string {
return []string{"./testdata/ccsrc/validimage", "./testdata/ccmetadata/validmetadata", tempDir}
}),
Entry("When the image.json file does not exist", 1, func() []string {
return []string{"CHAINCODE_SOURCE_DIR", "CHAINCODE_METADATA_DIR", "BUILD_OUTPUT_DIR"}
return []string{"CHAINCODE_SOURCE_DIR", "./testdata/ccmetadata/validmetadata", "BUILD_OUTPUT_DIR"}
}),
Entry("When the image.json file is invalid", 1, func() []string {
return []string{"./testdata/invalidimage", "CHAINCODE_METADATA_DIR", tempDir}
return []string{"./testdata/invalidimage", "./testdata/ccmetadata/validmetadata", "BUILD_OUTPUT_DIR"}
}),
Entry("When the metadata.json file does not exist", 1, func() []string {
return []string{"./testdata/validimage", "CHAINCODE_METADATA_DIR", "BUILD_OUTPUT_DIR"}
}),
Entry("When the metadata.json file is invalid", 1, func() []string {
return []string{"./testdata/validimage", "./testdata/ccmetadata/invalidmetadata", "BUILD_OUTPUT_DIR"}
}),
Entry("When the metadata.json contains an invalid label", 1, func() []string {
return []string{"./testdata/validimage", "./testdata/ccmetadata/invalidlabel", "BUILD_OUTPUT_DIR"}
}),
Entry("When the metadata.json contains an invalid label length", 1, func() []string {
return []string{"./testdata/validimage", "./testdata/ccmetadata/invalidlabellength", "BUILD_OUTPUT_DIR"}
}),
Entry("When too few arguments are provided", 1, func() []string {
return []string{"CHAINCODE_SOURCE_DIR"}
Expand All @@ -47,7 +59,7 @@ var _ = Describe("Main", func() {
)

It("should copy chaincode metadata to the build output directory", func() {
args := []string{"./testdata/withmetadata", "CHAINCODE_METADATA_DIR", tempDir}
args := []string{"./testdata/ccsrc/withmetadata", "./testdata/ccmetadata/validmetadata", tempDir}
command := exec.Command(buildCmdPath, args...)
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred())
Expand Down
4 changes: 4 additions & 0 deletions cmd/build/testdata/ccmetadata/invalidlabel/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "k8s",
"label": "42"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "k8s",
"label": "fabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcar"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
type=k8s
4 changes: 4 additions & 0 deletions cmd/build/testdata/ccmetadata/validmetadata/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "k8s",
"label": "basic"
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
16 changes: 10 additions & 6 deletions cmd/run/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,10 @@ 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 in kubernetes pod chaincode/hlfcc-chaincodelabel-f887209uhojj2`))

pipe := script.Exec(
"kubectl wait --for=condition=ready pod --timeout=120s --namespace=chaincode -l fabric-builder-k8s-peerid=core-peer-id-abcdefghijklmnopqrstuvwxyz-0123456789",
"kubectl wait --for=condition=ready pod --timeout=120s --namespace=chaincode -l fabric-builder-k8s-cclabel=CHAINCODE_LABEL",
)
_, err = pipe.Stdout()
Expect(err).NotTo(HaveOccurred())
Expand All @@ -109,19 +109,23 @@ var _ = Describe("Main", func() {
"pod",
"--namespace=chaincode",
"-l",
"fabric-builder-k8s-peerid=core-peer-id-abcdefghijklmnopqrstuvwxyz-0123456789",
"fabric-builder-k8s-cclabel=CHAINCODE_LABEL",
}
descCommand := exec.Command("kubectl", descArgs...)
descSession, err := gexec.Start(descCommand, GinkgoWriter, GinkgoWriter)
Expect(err).NotTo(HaveOccurred())

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`))
},
Expand Down
2 changes: 1 addition & 1 deletion cmd/run/testdata/validchaincode/chaincode.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
16 changes: 10 additions & 6 deletions docs/concepts/chaincode-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ From version 2.0, Hyperledger Fabric supports External Builders and Launchers to

External Builders and Launchers are a collection of executables — `detect`, `build`, `release` and `run` — that the peer calls in order to build, launch, and discover chaincode. The k8s builder executables do the following.

| Executable | Description |
| ----------- | ----------------------------------------------------------------------------------------------------------------- |
| detect | Detects chaincode packages with a type of `k8s` |
| build | No-op (a chaincode image must be built and published to a container registry before it can be deployed to Fabric) |
| release | No-op |
| run | Starts a Kubernetes pod using the chaincode image identified by an immutable digest |
| Executable | Description |
| ----------- | ----------------------------------------------------------------------------------- |
| detect | Detects chaincode packages with a type of `k8s` |
| build | Checks that the chaincode label is a valid Kubernetes object label[^1] |
| release | No-op |
| run | Starts a Kubernetes pod using the chaincode image identified by an immutable digest |

[^1]:
The k8s builder *does not* build the chaincode image.
A chaincode image must be built and published to a container registry before it can be deployed to Fabric using the k8s builder.

For more information about Fabric builders, see the [External Builders and Launchers](https://hyperledger-fabric.readthedocs.io/en/latest/cc_launcher.html) documentation.
2 changes: 2 additions & 0 deletions docs/concepts/chaincode-package.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ The k8s builder will detect chaincode packages which have a type of `k8s`. For e
}
```

The k8s builder uses the chaincode label to label Kubernetes objects, so it must be a [valid Kubernetes label value](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set).

## code.tar.gz

Unlike other chaincode packages, the source artifacts in a k8s chaincode package do not contain the chaincode source files.
Expand Down
17 changes: 16 additions & 1 deletion internal/builder/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ package builder

import (
"context"
"fmt"

"github.com/hyperledger-labs/fabric-builder-k8s/internal/log"
"github.com/hyperledger-labs/fabric-builder-k8s/internal/util"
"k8s.io/apimachinery/pkg/util/validation"
)

type Build struct {
Expand All @@ -19,7 +21,20 @@ func (b *Build) Run(ctx context.Context) error {
logger := log.New(ctx)
logger.Debugln("Building chaincode...")

err := util.CopyImageJSON(logger, b.ChaincodeSourceDirectory, b.BuildOutputDirectory)
metadata, err := util.ReadMetadataJSON(logger, b.ChaincodeMetadataDirectory)
if err != nil {
return err
}

if errs := validation.IsDNS1035Label(metadata.Label); len(errs) != 0 {
return fmt.Errorf(
"chaincode label '%s' must be a valid RFC1035 label: %v",
metadata.Label,
errs,
)
}

err = util.CopyImageJSON(logger, b.ChaincodeSourceDirectory, b.BuildOutputDirectory)
if err != nil {
return err
}
Expand Down
23 changes: 3 additions & 20 deletions internal/builder/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,27 @@ package builder

import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/hyperledger-labs/fabric-builder-k8s/internal/log"
"github.com/hyperledger-labs/fabric-builder-k8s/internal/util"
)

type Detect struct {
ChaincodeSourceDirectory string
ChaincodeMetadataDirectory string
}

type metadata struct {
Label string `json:"label"`
Type string `json:"type"`
}

var ErrUnsupportedChaincodeType = errors.New("chaincode type not supported")

func (d *Detect) Run(ctx context.Context) error {
logger := log.New(ctx)
logger.Debugln("Checking chaincode type...")

mdpath := filepath.Join(d.ChaincodeMetadataDirectory, "metadata.json")

mdbytes, err := os.ReadFile(mdpath)
if err != nil {
return fmt.Errorf("unable to read %s: %w", mdpath, err)
}

var metadata metadata

err = json.Unmarshal(mdbytes, &metadata)
metadata, err := util.ReadMetadataJSON(logger, d.ChaincodeMetadataDirectory)
if err != nil {
return fmt.Errorf("unable to process %s: %w", mdpath, err)
return err
}

if strings.ToLower(metadata.Type) == "k8s" {
Expand Down
31 changes: 31 additions & 0 deletions internal/util/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,16 @@ type ImageJSON struct {
Digest string `json:"digest"`
}

// MetadataJSON represents the metadata.json file in the k8s chaincode package.
type MetadataJSON struct {
Label string `json:"label"`
Type string `json:"type"`
}

const (
ChaincodeFile = "chaincode.json"
ImageFile = "image.json"
MetadataFile = "metadata.json"
MetadataDir = "META-INF"
)

Expand Down Expand Up @@ -77,3 +84,27 @@ func ReadImageJSON(logger *log.CmdLogger, dir string) (*ImageJSON, error) {

return &imageData, nil
}

// ReadMetadataJSON reads and parses the metadata.json file in the provided directory.
func ReadMetadataJSON(logger *log.CmdLogger, dir string) (*MetadataJSON, error) {
metadataJSONPath := filepath.Join(dir, MetadataFile)
logger.Debugf("Reading %s...", metadataJSONPath)

metadataJSONContents, err := os.ReadFile(metadataJSONPath)
if err != nil {
return nil, fmt.Errorf("unable to read %s: %w", metadataJSONPath, err)
}

var metadata MetadataJSON
if err := json.Unmarshal(metadataJSONContents, &metadata); err != nil {
return nil, fmt.Errorf("unable to parse %s: %w", metadataJSONPath, err)
}

logger.Debugf("Label: %s\nType: %s\n", metadata.Label, metadata.Type)

if len(metadata.Label) == 0 || len(metadata.Type) == 0 {
return nil, fmt.Errorf("%s file must contain 'label' and 'type'", metadataJSONPath)
}

return &metadata, nil
}
Loading

0 comments on commit 7f78c09

Please sign in to comment.