├── .dockerignore
├── requirements.txt
├── samples
├── node-contract
│ ├── .eslintignore
│ ├── .gitignore
│ ├── .dockerignore
│ ├── .eslintrc.json
│ ├── .prettierrc.js
│ ├── tsconfig.json
│ ├── src
│ │ └── index.ts
│ ├── Dockerfile
│ └── package.json
├── java-contract
│ ├── .dockerignore
│ ├── .gitignore
│ ├── gradle
│ │ └── wrapper
│ │ │ ├── gradle-wrapper.jar
│ │ │ └── gradle-wrapper.properties
│ ├── .gitattributes
│ ├── settings.gradle
│ ├── Dockerfile
│ ├── src
│ │ └── main
│ │ │ └── java
│ │ │ └── org
│ │ │ └── example
│ │ │ └── fabric
│ │ │ └── SampleContract.java
│ ├── build.gradle
│ ├── gradlew.bat
│ └── gradlew
├── README.md
└── go-contract
│ ├── main.go
│ ├── Dockerfile
│ ├── go.mod
│ └── go.sum
├── cmd
├── detect
│ ├── testdata
│ │ ├── invalidfile
│ │ │ └── metadata.json
│ │ ├── validtype
│ │ │ └── metadata.json
│ │ └── invalidtype
│ │ │ └── metadata.json
│ ├── main.go
│ ├── detect_suite_test.go
│ └── main_test.go
├── build
│ ├── testdata
│ │ ├── ccsrc
│ │ │ ├── withmetadata
│ │ │ │ ├── META-INF
│ │ │ │ │ └── test
│ │ │ │ │ │ └── test.txt
│ │ │ │ └── image.json
│ │ │ ├── invalidimage
│ │ │ │ └── image.json
│ │ │ └── validimage
│ │ │ │ └── image.json
│ │ └── ccmetadata
│ │ │ ├── invalidmetadata
│ │ │ └── metadata.json
│ │ │ ├── invalidlabel
│ │ │ └── metadata.json
│ │ │ ├── validmetadata
│ │ │ └── metadata.json
│ │ │ └── invalidlabellength
│ │ │ └── metadata.json
│ ├── main.go
│ ├── build_suite_test.go
│ └── main_test.go
├── release
│ ├── testdata
│ │ ├── buildwithindexes
│ │ │ ├── META-INF
│ │ │ │ └── statedb
│ │ │ │ │ └── couchdb
│ │ │ │ │ ├── test.txt
│ │ │ │ │ ├── indexed
│ │ │ │ │ ├── test.txt
│ │ │ │ │ ├── indexOwner.json
│ │ │ │ │ └── subdir
│ │ │ │ │ │ └── indexOwner.json
│ │ │ │ │ ├── indexes
│ │ │ │ │ ├── test.txt
│ │ │ │ │ ├── indexOwner.json
│ │ │ │ │ └── subdir
│ │ │ │ │ │ └── indexOwner.json
│ │ │ │ │ ├── subdir
│ │ │ │ │ └── indexes
│ │ │ │ │ │ ├── test.txt
│ │ │ │ │ │ ├── indexOwner.json
│ │ │ │ │ │ └── subdir
│ │ │ │ │ │ └── indexOwner.json
│ │ │ │ │ ├── indexOwner.json
│ │ │ │ │ ├── collections
│ │ │ │ │ ├── fabCarCollection
│ │ │ │ │ │ ├── subdir
│ │ │ │ │ │ │ └── indexes
│ │ │ │ │ │ │ │ └── indexOwner.json
│ │ │ │ │ │ └── indexes
│ │ │ │ │ │ │ └── indexOwner.json
│ │ │ │ │ └── assetCollection
│ │ │ │ │ │ └── indexes
│ │ │ │ │ │ └── indexOwner.json
│ │ │ │ │ └── collectionsd
│ │ │ │ │ ├── fabCarCollection
│ │ │ │ │ ├── subdir
│ │ │ │ │ │ └── indexes
│ │ │ │ │ │ │ └── indexOwner.json
│ │ │ │ │ └── indexes
│ │ │ │ │ │ └── indexOwner.json
│ │ │ │ │ └── assetCollection
│ │ │ │ │ └── indexes
│ │ │ │ │ └── indexOwner.json
│ │ │ └── image.json
│ │ └── buildwithoutindexes
│ │ │ └── image.json
│ ├── main.go
│ ├── release_suite_test.go
│ └── main_test.go
└── run
│ ├── testdata
│ ├── validimage
│ │ └── image.json
│ └── validchaincode
│ │ └── chaincode.json
│ ├── main.go
│ ├── run_suite_test.go
│ └── main_test.go
├── docs
├── getting-started
│ ├── requirements.md
│ ├── faqs.md
│ ├── install.md
│ └── demo.md
├── about
│ ├── community.md
│ └── objectives.md
├── configuring
│ ├── kubernetes-namespace.md
│ ├── dedicated-nodes.md
│ ├── kubernetes-service-account.md
│ ├── kubernetes-permissions.md
│ └── overview.md
├── concepts
│ ├── chaincode-image.md
│ ├── chaincode-builder.md
│ ├── chaincode-package.md
│ └── chaincode-job.md
├── index.md
├── tutorials
│ ├── package-chaincode.md
│ ├── fabric-operator.md
│ ├── bevel-operator.md
│ └── develop-chaincode.md
└── assets
│ └── Hyperledger_Fabric_Icon.svg
├── .devcontainer
├── Dockerfile
├── postCreateCommand.sh
└── devcontainer.json
├── CODE_OF_CONDUCT.md
├── .github
├── workflows
│ ├── status-checks-docker.yml
│ ├── status-checks.yml
│ ├── go-contract-image.yml
│ ├── java-contract-image.yml
│ ├── peer-image.yml
│ ├── node-contract-image.yml
│ ├── golangci-lint.yml
│ ├── mkdocs.yml
│ ├── go.yml
│ ├── ossf-scorecard.yml
│ └── docker-build.yml
└── dependabot.yml
├── internal
├── util
│ ├── util_suite_test.go
│ ├── fabric.go
│ ├── env.go
│ ├── fabric_test.go
│ ├── files.go
│ ├── copy.go
│ └── k8s_test.go
├── builder
│ ├── release.go
│ ├── detect.go
│ ├── build.go
│ └── run.go
├── cmd
│ ├── release.go
│ ├── detect.go
│ ├── build.go
│ └── run.go
└── log
│ └── log.go
├── .gitignore
├── test
├── integration
│ ├── testdata
│ │ ├── kind-config.yaml
│ │ ├── dedicated_node_unavailable.txtar
│ │ ├── chaincode_service_account.txtar
│ │ ├── chaincode_name_prefix.txtar
│ │ ├── dedicated_node_available.txtar
│ │ └── run_chaincode.txtar
│ ├── main_test.go
│ └── integration_test.go
└── testscript_helpers.go
├── MAINTAINERS.md
├── Dockerfile
├── README.md
├── .golangci.yml
├── mkdocs.yml
├── go.mod
└── CONTRIBUTING.md
/.dockerignore:
--------------------------------------------------------------------------------
1 | docs
2 | samples
3 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | mkdocs-material
2 | mike
3 |
--------------------------------------------------------------------------------
/samples/node-contract/.eslintignore:
--------------------------------------------------------------------------------
1 | dist/
2 |
--------------------------------------------------------------------------------
/cmd/detect/testdata/invalidfile/metadata.json:
--------------------------------------------------------------------------------
1 | type=k8s
2 |
--------------------------------------------------------------------------------
/samples/node-contract/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 |
--------------------------------------------------------------------------------
/cmd/build/testdata/ccsrc/withmetadata/META-INF/test/test.txt:
--------------------------------------------------------------------------------
1 | Testing
--------------------------------------------------------------------------------
/samples/java-contract/.dockerignore:
--------------------------------------------------------------------------------
1 | .gradle/
2 | bin/
3 | build/
4 |
--------------------------------------------------------------------------------
/cmd/build/testdata/ccmetadata/invalidmetadata/metadata.json:
--------------------------------------------------------------------------------
1 | type=k8s
2 |
--------------------------------------------------------------------------------
/samples/node-contract/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 |
--------------------------------------------------------------------------------
/cmd/release/testdata/buildwithindexes/META-INF/statedb/couchdb/test.txt:
--------------------------------------------------------------------------------
1 | Testing
2 |
--------------------------------------------------------------------------------
/samples/node-contract/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./node_modules/gts/"
3 | }
4 |
--------------------------------------------------------------------------------
/cmd/release/testdata/buildwithindexes/META-INF/statedb/couchdb/indexed/test.txt:
--------------------------------------------------------------------------------
1 | Testing
2 |
--------------------------------------------------------------------------------
/cmd/release/testdata/buildwithindexes/META-INF/statedb/couchdb/indexes/test.txt:
--------------------------------------------------------------------------------
1 | Testing
2 |
--------------------------------------------------------------------------------
/cmd/release/testdata/buildwithindexes/META-INF/statedb/couchdb/subdir/indexes/test.txt:
--------------------------------------------------------------------------------
1 | Testing
2 |
--------------------------------------------------------------------------------
/cmd/detect/testdata/validtype/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "k8s",
3 | "label": "basic"
4 | }
5 |
--------------------------------------------------------------------------------
/cmd/detect/testdata/invalidtype/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "external",
3 | "label": "basic"
4 | }
5 |
--------------------------------------------------------------------------------
/samples/node-contract/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | ...require('gts/.prettierrc.json')
3 | }
4 |
--------------------------------------------------------------------------------
/cmd/build/testdata/ccmetadata/invalidlabel/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "k8s",
3 | "label": "42"
4 | }
5 |
--------------------------------------------------------------------------------
/cmd/build/testdata/ccmetadata/validmetadata/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "k8s",
3 | "label": "basic"
4 | }
5 |
--------------------------------------------------------------------------------
/docs/getting-started/requirements.md:
--------------------------------------------------------------------------------
1 | # Requirements
2 |
3 | The k8s builder will work with Hyperledger Fabric 2.0 or later.
4 |
--------------------------------------------------------------------------------
/cmd/build/testdata/ccsrc/invalidimage/image.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ghcr.io/hyperledger/asset-transfer-basic",
3 | "tag": "1.0"
4 | }
5 |
--------------------------------------------------------------------------------
/cmd/run/testdata/validimage/image.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nginx",
3 | "digest": "sha256:da3cc3053314be9ca3871307366f6e30ce2b11e1ea6a72e5957244d99b2515bf"
4 | }
5 |
--------------------------------------------------------------------------------
/samples/java-contract/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore Gradle project-specific cache directory
2 | .gradle
3 |
4 | # Ignore Gradle build output directory
5 | build
6 |
7 | bin
8 |
--------------------------------------------------------------------------------
/cmd/release/testdata/buildwithindexes/META-INF/statedb/couchdb/indexOwner.json:
--------------------------------------------------------------------------------
1 | {"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"}
2 |
--------------------------------------------------------------------------------
/samples/java-contract/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hyperledger-labs/fabric-builder-k8s/HEAD/samples/java-contract/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/cmd/release/testdata/buildwithindexes/META-INF/statedb/couchdb/indexed/indexOwner.json:
--------------------------------------------------------------------------------
1 | {"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"}
2 |
--------------------------------------------------------------------------------
/cmd/release/testdata/buildwithindexes/META-INF/statedb/couchdb/indexes/indexOwner.json:
--------------------------------------------------------------------------------
1 | {"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"}
2 |
--------------------------------------------------------------------------------
/cmd/release/testdata/buildwithindexes/META-INF/statedb/couchdb/indexed/subdir/indexOwner.json:
--------------------------------------------------------------------------------
1 | {"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"}
2 |
--------------------------------------------------------------------------------
/cmd/release/testdata/buildwithindexes/META-INF/statedb/couchdb/indexes/subdir/indexOwner.json:
--------------------------------------------------------------------------------
1 | {"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"}
2 |
--------------------------------------------------------------------------------
/cmd/release/testdata/buildwithindexes/META-INF/statedb/couchdb/subdir/indexes/indexOwner.json:
--------------------------------------------------------------------------------
1 | {"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"}
2 |
--------------------------------------------------------------------------------
/cmd/release/testdata/buildwithindexes/META-INF/statedb/couchdb/subdir/indexes/subdir/indexOwner.json:
--------------------------------------------------------------------------------
1 | {"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"}
2 |
--------------------------------------------------------------------------------
/cmd/build/testdata/ccsrc/validimage/image.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ghcr.io/hyperledger/asset-transfer-basic",
3 | "digest": "sha256:b35962f000d26ad046d4102f22d70a1351692fc69a9ddead89dfa13aefb942a7"
4 | }
5 |
--------------------------------------------------------------------------------
/cmd/build/testdata/ccsrc/withmetadata/image.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ghcr.io/hyperledger/asset-transfer-basic",
3 | "digest": "sha256:b35962f000d26ad046d4102f22d70a1351692fc69a9ddead89dfa13aefb942a7"
4 | }
5 |
--------------------------------------------------------------------------------
/cmd/release/testdata/buildwithindexes/image.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ghcr.io/hyperledger/asset-transfer-basic",
3 | "digest": "sha256:b35962f000d26ad046d4102f22d70a1351692fc69a9ddead89dfa13aefb942a7"
4 | }
5 |
--------------------------------------------------------------------------------
/cmd/release/testdata/buildwithoutindexes/image.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ghcr.io/hyperledger/asset-transfer-basic",
3 | "digest": "sha256:b35962f000d26ad046d4102f22d70a1351692fc69a9ddead89dfa13aefb942a7"
4 | }
5 |
--------------------------------------------------------------------------------
/cmd/run/main.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package main
4 |
5 | import "github.com/hyperledger-labs/fabric-builder-k8s/internal/cmd"
6 |
7 | func main() {
8 | cmd.Run()
9 | }
10 |
--------------------------------------------------------------------------------
/cmd/build/main.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package main
4 |
5 | import "github.com/hyperledger-labs/fabric-builder-k8s/internal/cmd"
6 |
7 | func main() {
8 | cmd.Build()
9 | }
10 |
--------------------------------------------------------------------------------
/cmd/detect/main.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package main
4 |
5 | import "github.com/hyperledger-labs/fabric-builder-k8s/internal/cmd"
6 |
7 | func main() {
8 | cmd.Detect()
9 | }
10 |
--------------------------------------------------------------------------------
/samples/java-contract/.gitattributes:
--------------------------------------------------------------------------------
1 | #
2 | # https://help.github.com/articles/dealing-with-line-endings/
3 | #
4 | # These are explicitly windows files and should use crlf
5 | *.bat text eol=crlf
6 |
7 |
--------------------------------------------------------------------------------
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/devcontainers/go:1-1.24-bookworm
2 |
3 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
4 | && apt-get -y install --no-install-recommends \
5 | xxd
6 |
--------------------------------------------------------------------------------
/cmd/release/main.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package main
4 |
5 | import "github.com/hyperledger-labs/fabric-builder-k8s/internal/cmd"
6 |
7 | func main() {
8 | cmd.Release()
9 | }
10 |
--------------------------------------------------------------------------------
/cmd/release/testdata/buildwithindexes/META-INF/statedb/couchdb/collections/fabCarCollection/subdir/indexes/indexOwner.json:
--------------------------------------------------------------------------------
1 | {"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"}
2 |
--------------------------------------------------------------------------------
/cmd/release/testdata/buildwithindexes/META-INF/statedb/couchdb/collectionsd/fabCarCollection/subdir/indexes/indexOwner.json:
--------------------------------------------------------------------------------
1 | {"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"}
2 |
--------------------------------------------------------------------------------
/cmd/build/testdata/ccmetadata/invalidlabellength/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "k8s",
3 | "label": "fabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcar"
4 | }
5 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of conduct
2 |
3 | This project follows the [Linux Foundation Decentralized Trust code of conduct](https://lf-decentralized-trust.github.io/governance/governing-documents/code-of-conduct.html). Please review these guidelines before participating.
4 |
--------------------------------------------------------------------------------
/.github/workflows/status-checks-docker.yml:
--------------------------------------------------------------------------------
1 | name: Skip docker status checks
2 |
3 | on:
4 | workflow_call:
5 |
6 | permissions: read-all
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - run: 'echo "No docker build required"'
13 |
--------------------------------------------------------------------------------
/samples/java-contract/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/internal/util/util_suite_test.go:
--------------------------------------------------------------------------------
1 | package util_test
2 |
3 | import (
4 | "testing"
5 |
6 | . "github.com/onsi/ginkgo/v2"
7 | . "github.com/onsi/gomega"
8 | )
9 |
10 | func TestUtil(t *testing.T) {
11 | RegisterFailHandler(Fail)
12 | RunSpecs(t, "Util Suite")
13 | }
14 |
--------------------------------------------------------------------------------
/cmd/run/testdata/validchaincode/chaincode.json:
--------------------------------------------------------------------------------
1 | {
2 | "chaincode_id": "CHAINCODE_LABEL:6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45",
3 | "peer_address": "PEER_ADDRESS",
4 | "client_cert": "CLIENT_CERT",
5 | "client_key": "CLIENT_KEY",
6 | "root_cert": "ROOT_CERT",
7 | "mspid": "MSPID"
8 | }
9 |
--------------------------------------------------------------------------------
/cmd/release/testdata/buildwithindexes/META-INF/statedb/couchdb/collections/fabCarCollection/indexes/indexOwner.json:
--------------------------------------------------------------------------------
1 | {
2 | "index": {
3 | "fields": [
4 | "model",
5 | "owner"
6 | ]
7 | },
8 | "ddoc": "indexOwnerDoc",
9 | "name": "indexOwner",
10 | "type": "json"
11 | }
12 |
--------------------------------------------------------------------------------
/cmd/release/testdata/buildwithindexes/META-INF/statedb/couchdb/collectionsd/fabCarCollection/indexes/indexOwner.json:
--------------------------------------------------------------------------------
1 | {
2 | "index": {
3 | "fields": [
4 | "model",
5 | "owner"
6 | ]
7 | },
8 | "ddoc": "indexOwnerDoc",
9 | "name": "indexOwner",
10 | "type": "json"
11 | }
12 |
--------------------------------------------------------------------------------
/samples/node-contract/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./node_modules/gts/tsconfig-google.json",
3 | "compilerOptions": {
4 | "experimentalDecorators": true,
5 | "emitDecoratorMetadata": true,
6 | "outDir": "dist"
7 | },
8 | "include": [
9 | "src/**/*.ts",
10 | "test/**/*.ts"
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/cmd/release/testdata/buildwithindexes/META-INF/statedb/couchdb/collections/assetCollection/indexes/indexOwner.json:
--------------------------------------------------------------------------------
1 | {
2 | "index": {
3 | "fields": [
4 | "objectType",
5 | "owner"
6 | ]
7 | },
8 | "ddoc": "indexOwnerDoc",
9 | "name": "indexOwner",
10 | "type": "json"
11 | }
12 |
--------------------------------------------------------------------------------
/cmd/release/testdata/buildwithindexes/META-INF/statedb/couchdb/collectionsd/assetCollection/indexes/indexOwner.json:
--------------------------------------------------------------------------------
1 | {
2 | "index": {
3 | "fields": [
4 | "objectType",
5 | "owner"
6 | ]
7 | },
8 | "ddoc": "indexOwnerDoc",
9 | "name": "indexOwner",
10 | "type": "json"
11 | }
12 |
--------------------------------------------------------------------------------
/docs/about/community.md:
--------------------------------------------------------------------------------
1 | # Community
2 |
3 | You can find [community discussion related to the Kubernetes Builder on GitHub](https://github.com/hyperledger-labs/fabric-builder-k8s/discussions) and on the [#fabric-kubernetes](https://discord.com/channels/905194001349627914/945796983795384331) channel on Linux Foundation Decentralized Trust Discord ([invite link](https://discord.gg/hyperledger)).
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Dependency directories (remove the comment below to include it)
15 | # vendor/
16 | .idea/
17 |
18 | # mkdocs documentation
19 | /site
20 |
21 | .fabric
22 |
23 | *.log
24 |
--------------------------------------------------------------------------------
/samples/java-contract/settings.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * This file was generated by the Gradle 'init' task.
3 | *
4 | * The settings file is used to specify which projects to include in your build.
5 | *
6 | * Detailed information about configuring a multi-project build in Gradle can be found
7 | * in the user manual at https://docs.gradle.org/7.3/userguide/multi_project_builds.html
8 | */
9 |
10 | rootProject.name = 'sample-contract'
11 |
--------------------------------------------------------------------------------
/test/integration/testdata/kind-config.yaml:
--------------------------------------------------------------------------------
1 | kind: Cluster
2 | apiVersion: kind.x-k8s.io/v1alpha4
3 | nodes:
4 | - role: control-plane
5 | - role: worker
6 | - role: worker
7 | kubeadmConfigPatches:
8 | - |
9 | kind: JoinConfiguration
10 | nodeRegistration:
11 | kubeletExtraArgs:
12 | node-labels: "fabric-builder-k8s-role=chaincode"
13 | register-with-taints: "fabric-builder-k8s-role=chaincode:NoSchedule"
14 |
--------------------------------------------------------------------------------
/docs/configuring/kubernetes-namespace.md:
--------------------------------------------------------------------------------
1 | # Kubernetes namespace
2 |
3 | By default, the k8s builder starts chaincode pods in the same namespace as the peer, or the `default` namespace if the peer is running outside Kubernetes.
4 |
5 | The `FABRIC_K8S_BUILDER_NAMESPACE` environment variable can be used to start chaincode pods in a different namespace.
6 |
7 | For example, if `FABRIC_K8S_BUILDER_NAMESPACE` is set to `hlf-chaincode`, create the required namespace using the following command.
8 |
9 | ```shell
10 | kubectl create namespace hlf-chaincode
11 | ```
12 |
--------------------------------------------------------------------------------
/samples/README.md:
--------------------------------------------------------------------------------
1 | # Sample contracts
2 |
3 | The main purpose of these samples is to demonstrate basic `Dockerfile`s for deploying Go, Java, and Node.js chaincode with the k8s builder.
4 |
5 | The samples can be built with:
6 |
7 | ```shell
8 | docker build . -t sample-contract
9 | ```
10 |
11 | You will need a digest to create a chaincode package for the k8s builder, which is only created when the docker image is published to a registry.
12 | For example, to publish to a local registry:
13 |
14 | ```shell
15 | docker tag sample-contract localhost:5000/sample-contract
16 | docker push localhost:5000/sample-contract
17 | ```
18 |
--------------------------------------------------------------------------------
/docs/getting-started/faqs.md:
--------------------------------------------------------------------------------
1 | # Frequently Asked Questions
2 |
3 | ## Chaincode
4 |
5 | ### Are private chaincode images supported?
6 |
7 | Yes. For more information, see [Kubernetes service account](../configuring/kubernetes-service-account.md)
8 |
9 | ### Will chaincode work in multi-architecture Fabric networks?
10 |
11 | Yes. Build multi-architecture chaincode images if you have a multi-architecture Fabric network.
12 |
13 | ### Can every chaincode be launched in a different namespace?
14 |
15 | No. The k8s builder configuration is defined for each peer, so all chaincode launched by the same peer will run in the same namespace.
16 |
--------------------------------------------------------------------------------
/.github/workflows/status-checks.yml:
--------------------------------------------------------------------------------
1 | name: Skip status checks
2 |
3 | on:
4 | pull_request:
5 | paths:
6 | - '*.md'
7 | - 'docs/**'
8 | - 'samples/**'
9 |
10 | permissions: read-all
11 |
12 | jobs:
13 | lint:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - run: 'echo "No lint required"'
17 |
18 | build:
19 | runs-on: ${{ matrix.os }}
20 | strategy:
21 | matrix:
22 | os: [ubuntu-latest]
23 | goarch: [amd64]
24 | steps:
25 | - run: 'echo "No build required"'
26 |
27 | docker_build:
28 | name: Docker build
29 | uses: ./.github/workflows/status-checks-docker.yml
30 |
--------------------------------------------------------------------------------
/samples/node-contract/src/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: Apache-2.0
3 | */
4 |
5 | import {Context, Contract, Transaction} from 'fabric-contract-api';
6 | import {TextDecoder, TextEncoder} from 'util';
7 |
8 | const encoder = new TextEncoder();
9 | const decoder = new TextDecoder();
10 |
11 | class SampleContract extends Contract {
12 | @Transaction()
13 | async PutValue(ctx: Context, key: string, value: string) {
14 | await ctx.stub.putState(key, encoder.encode(value));
15 | }
16 |
17 | @Transaction()
18 | async GetValue(ctx: Context, key: string) {
19 | const value = await ctx.stub.getState(key);
20 | return decoder.decode(value);
21 | }
22 | }
23 |
24 | exports.contracts = [SampleContract];
25 |
--------------------------------------------------------------------------------
/cmd/build/build_suite_test.go:
--------------------------------------------------------------------------------
1 | package main_test
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | . "github.com/onsi/ginkgo/v2"
8 | . "github.com/onsi/gomega"
9 | "github.com/onsi/gomega/gexec"
10 | )
11 |
12 | //nolint:gochecknoglobals // not sure how to avoid this
13 | var buildCmdPath string
14 |
15 | func TestBuild(t *testing.T) {
16 | RegisterFailHandler(Fail)
17 | RunSpecs(t, "Build Suite")
18 | }
19 |
20 | var _ = BeforeSuite(func() {
21 | SetDefaultEventuallyTimeout(2 * time.Second)
22 |
23 | var err error
24 | buildCmdPath, err = gexec.Build("github.com/hyperledger-labs/fabric-builder-k8s/cmd/build")
25 | Expect(err).NotTo(HaveOccurred())
26 | })
27 |
28 | var _ = AfterSuite(func() {
29 | gexec.CleanupBuildArtifacts()
30 | })
31 |
--------------------------------------------------------------------------------
/cmd/run/run_suite_test.go:
--------------------------------------------------------------------------------
1 | package main_test
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | . "github.com/onsi/ginkgo/v2"
8 | . "github.com/onsi/gomega"
9 | "github.com/onsi/gomega/gexec"
10 | )
11 |
12 | //nolint:gochecknoglobals // not sure how to avoid this
13 | var (
14 | runCmdPath string
15 | )
16 |
17 | func TestRun(t *testing.T) {
18 | RegisterFailHandler(Fail)
19 | RunSpecs(t, "Run Suite")
20 | }
21 |
22 | var _ = BeforeSuite(func() {
23 | SetDefaultEventuallyTimeout(2 * time.Second)
24 |
25 | var err error
26 | runCmdPath, err = gexec.Build("github.com/hyperledger-labs/fabric-builder-k8s/cmd/run")
27 | Expect(err).NotTo(HaveOccurred())
28 | })
29 |
30 | var _ = AfterSuite(func() {
31 | gexec.CleanupBuildArtifacts()
32 | })
33 |
--------------------------------------------------------------------------------
/cmd/detect/detect_suite_test.go:
--------------------------------------------------------------------------------
1 | package main_test
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | . "github.com/onsi/ginkgo/v2"
8 | . "github.com/onsi/gomega"
9 | "github.com/onsi/gomega/gexec"
10 | )
11 |
12 | //nolint:gochecknoglobals // not sure how to avoid this
13 | var detectCmdPath string
14 |
15 | func TestDetect(t *testing.T) {
16 | RegisterFailHandler(Fail)
17 | RunSpecs(t, "Detect Suite")
18 | }
19 |
20 | var _ = BeforeSuite(func() {
21 | SetDefaultEventuallyTimeout(2 * time.Second)
22 |
23 | var err error
24 | detectCmdPath, err = gexec.Build("github.com/hyperledger-labs/fabric-builder-k8s/cmd/detect")
25 | Expect(err).NotTo(HaveOccurred())
26 | })
27 |
28 | var _ = AfterSuite(func() {
29 | gexec.CleanupBuildArtifacts()
30 | })
31 |
--------------------------------------------------------------------------------
/cmd/release/release_suite_test.go:
--------------------------------------------------------------------------------
1 | package main_test
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | . "github.com/onsi/ginkgo/v2"
8 | . "github.com/onsi/gomega"
9 | "github.com/onsi/gomega/gexec"
10 | )
11 |
12 | //nolint:gochecknoglobals // not sure how to avoid this
13 | var releaseCmdPath string
14 |
15 | func TestRelease(t *testing.T) {
16 | RegisterFailHandler(Fail)
17 | RunSpecs(t, "Release Suite")
18 | }
19 |
20 | var _ = BeforeSuite(func() {
21 | SetDefaultEventuallyTimeout(2 * time.Second)
22 |
23 | var err error
24 | releaseCmdPath, err = gexec.Build("github.com/hyperledger-labs/fabric-builder-k8s/cmd/release")
25 | Expect(err).NotTo(HaveOccurred())
26 | })
27 |
28 | var _ = AfterSuite(func() {
29 | gexec.CleanupBuildArtifacts()
30 | })
31 |
--------------------------------------------------------------------------------
/MAINTAINERS.md:
--------------------------------------------------------------------------------
1 | ## Maintainers
2 |
3 | ### Active Maintainers
4 |
5 | | Name | GitHub | email |
6 | |---------------|----------|---------------------|
7 | | Josh Kneubuhl | jkneubuh | jkneubuh@us.ibm.com |
8 | | James Taylor | jt-nti | jamest@uk.ibm.com |
9 |
10 | ### Retired Maintainers
11 |
12 | | Name | GitHub | email |
13 | |------|--------|-------|
14 | | | | |
15 |
16 | ---
17 | 
This work is licensed under a Creative Commons Attribution 4.0 International License
18 |
--------------------------------------------------------------------------------
/.github/workflows/go-contract-image.yml:
--------------------------------------------------------------------------------
1 | name: Go Contract Image CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'main'
7 | tags:
8 | - 'v*'
9 | paths:
10 | - 'samples/go-contract/**'
11 | pull_request:
12 | branches:
13 | - 'main'
14 | paths:
15 | - 'samples/go-contract/**'
16 |
17 | permissions: read-all
18 |
19 | jobs:
20 | docker_build:
21 | name: Docker build
22 | permissions:
23 | contents: write
24 | packages: write
25 | id-token: write
26 | uses: ./.github/workflows/docker-build.yml
27 | with:
28 | platforms: linux/amd64,linux/arm64
29 | image-name: ghcr.io/hyperledger-labs/fabric-builder-k8s/sample-go-contract
30 | path: samples/go-contract
31 | chaincode-label: go-contract
32 |
--------------------------------------------------------------------------------
/internal/util/fabric.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package util
4 |
5 | import "strings"
6 |
7 | type ChaincodePackageID struct {
8 | Label string
9 | Hash string
10 | }
11 |
12 | // NewChaincodePackageID returns a ChaincodePackageID created from the provided string.
13 | func NewChaincodePackageID(chaincodeID string) *ChaincodePackageID {
14 | substrings := strings.Split(chaincodeID, ":")
15 |
16 | // If it doesn't look like a label and a hash, don't try and guess which is which
17 | if len(substrings) == 1 {
18 | return &ChaincodePackageID{
19 | Label: "",
20 | Hash: "",
21 | }
22 | }
23 |
24 | return &ChaincodePackageID{
25 | Label: strings.Join(substrings[:len(substrings)-1], ":"),
26 | Hash: substrings[len(substrings)-1],
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.github/workflows/java-contract-image.yml:
--------------------------------------------------------------------------------
1 | name: Java Contract Image CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'main'
7 | tags:
8 | - 'v*'
9 | paths:
10 | - 'samples/java-contract/**'
11 | pull_request:
12 | branches:
13 | - 'main'
14 | paths:
15 | - 'samples/java-contract/**'
16 |
17 | permissions: read-all
18 |
19 | jobs:
20 | docker_build:
21 | name: Docker build
22 | permissions:
23 | contents: write
24 | packages: write
25 | id-token: write
26 | uses: ./.github/workflows/docker-build.yml
27 | with:
28 | platforms: linux/amd64
29 | image-name: ghcr.io/hyperledger-labs/fabric-builder-k8s/sample-java-contract
30 | path: samples/java-contract
31 | chaincode-label: java-contract
32 |
--------------------------------------------------------------------------------
/.github/workflows/peer-image.yml:
--------------------------------------------------------------------------------
1 | name: Peer Image CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'main'
7 | tags:
8 | - 'v*'
9 | paths-ignore:
10 | - '*.md'
11 | - 'docs/**'
12 | - 'samples/**'
13 | pull_request:
14 | branches:
15 | - 'main'
16 | paths-ignore:
17 | - '*.md'
18 | - 'docs/**'
19 | - 'samples/**'
20 |
21 | permissions: read-all
22 |
23 | jobs:
24 | docker_build:
25 | name: Docker build
26 | permissions:
27 | contents: write
28 | packages: write
29 | id-token: write
30 | uses: ./.github/workflows/docker-build.yml
31 | with:
32 | platforms: linux/amd64,linux/arm64
33 | image-name: ghcr.io/hyperledger-labs/fabric-builder-k8s/k8s-fabric-peer
34 | path: .
35 |
--------------------------------------------------------------------------------
/.github/workflows/node-contract-image.yml:
--------------------------------------------------------------------------------
1 | name: Node Contract Image CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'main'
7 | tags:
8 | - 'v*'
9 | paths:
10 | - 'samples/node-contract/**'
11 | pull_request:
12 | branches:
13 | - 'main'
14 | paths:
15 | - 'samples/node-contract/**'
16 |
17 | permissions: read-all
18 |
19 | jobs:
20 | docker_build:
21 | name: Docker build
22 | permissions:
23 | contents: write
24 | packages: write
25 | id-token: write
26 | uses: ./.github/workflows/docker-build.yml
27 | with:
28 | platforms: linux/amd64,linux/arm64
29 | image-name: ghcr.io/hyperledger-labs/fabric-builder-k8s/sample-node-contract
30 | path: samples/node-contract
31 | chaincode-label: node-contract
32 |
--------------------------------------------------------------------------------
/.github/workflows/golangci-lint.yml:
--------------------------------------------------------------------------------
1 | name: golangci-lint
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | tags: [ v** ]
7 | paths-ignore:
8 | - '*.md'
9 | - 'docs/**'
10 | - 'samples/**'
11 | pull_request:
12 | branches: [ main ]
13 | paths-ignore:
14 | - '*.md'
15 | - 'docs/**'
16 | - 'samples/**'
17 |
18 | permissions: read-all
19 |
20 | jobs:
21 | golangci:
22 | name: lint
23 | runs-on: ubuntu-latest
24 | steps:
25 | - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
26 | with:
27 | go-version: 1.24
28 | - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
29 | - name: golangci-lint
30 | uses: golangci/golangci-lint-action@0a35821d5c230e903fcfe077583637dea1b27b47 # v9.0.0
31 | with:
32 | version: v2.1
33 |
--------------------------------------------------------------------------------
/docs/concepts/chaincode-image.md:
--------------------------------------------------------------------------------
1 | # Chaincode image
2 |
3 | Unlike the traditional built-in chaincode language support for Go, Java, and Node.js, the k8s builder *does not* build a chaincode Docker image using Docker-in-Docker.
4 | Instead, a chaincode Docker image must be built and published before it can be used with the k8s builder.
5 |
6 | The chaincode will have access to the following environment variables:
7 |
8 | - CORE_CHAINCODE_ID_NAME
9 | - CORE_PEER_ADDRESS
10 | - CORE_PEER_TLS_ENABLED
11 | - CORE_PEER_TLS_ROOTCERT_FILE
12 | - CORE_TLS_CLIENT_KEY_PATH
13 | - CORE_TLS_CLIENT_CERT_PATH
14 | - CORE_TLS_CLIENT_KEY_FILE
15 | - CORE_TLS_CLIENT_CERT_FILE
16 | - CORE_PEER_LOCALMSPID
17 |
18 | See the [sample contracts for Go, Java, and Node.js](https://github.com/hyperledger-labs/fabric-builder-k8s/tree/main/samples) for basic docker images which will work with the k8s builder.
19 |
--------------------------------------------------------------------------------
/samples/node-contract/Dockerfile:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: Apache-2.0
2 |
3 | ARG NODE_VER=16
4 | ARG ALPINE_VER=3.14
5 |
6 | FROM node:${NODE_VER}-alpine${ALPINE_VER} AS build
7 |
8 | RUN apk add --no-cache \
9 | dumb-init
10 |
11 | FROM node:${NODE_VER}-alpine${ALPINE_VER}
12 |
13 | LABEL org.opencontainers.image.title="Sample Node Contract"
14 | LABEL org.opencontainers.image.description="Sample Hyperledger Fabric Node contract for Kubernetes chaincode builder"
15 | LABEL org.opencontainers.image.source="https://github.com/hyperledger-labs/fabric-builder-k8s/samples/node-contract"
16 |
17 | COPY --from=build /usr/bin/dumb-init /usr/bin/dumb-init
18 |
19 | WORKDIR /usr/src/app
20 |
21 | COPY package*.json ./
22 |
23 | RUN npm ci
24 |
25 | COPY . .
26 |
27 | RUN npm run compile
28 |
29 | ENTRYPOINT ["/usr/bin/dumb-init", "--"]
30 | CMD ["sh", "-c", "exec npm start -- --peer.address $CORE_PEER_ADDRESS"]
31 |
--------------------------------------------------------------------------------
/internal/builder/release.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package builder
4 |
5 | import (
6 | "context"
7 |
8 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/log"
9 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/util"
10 | )
11 |
12 | type Release struct {
13 | BuildOutputDirectory string
14 | ReleaseOutputDirectory string
15 | }
16 |
17 | func (r *Release) Run(ctx context.Context) error {
18 | logger := log.New(ctx)
19 | logger.Debugln("Releasing chaincode...")
20 |
21 | // If CouchDB index definitions are required for the chaincode, release is
22 | // responsible for placing the indexes into the statedb/couchdb/
23 | // directory under RELEASE_OUTPUT_DIR. The indexes must have a .json
24 | // extension.
25 | err := util.CopyIndexFiles(logger, r.BuildOutputDirectory, r.ReleaseOutputDirectory)
26 | if err != nil {
27 | return err
28 | }
29 |
30 | return nil
31 | }
32 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for more information:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 | # https://containers.dev/guide/dependabot
6 |
7 | version: 2
8 | updates:
9 | - package-ecosystem: "devcontainers"
10 | directory: "/"
11 | schedule:
12 | interval: weekly
13 | - package-ecosystem: "github-actions"
14 | directory: "/"
15 | schedule:
16 | interval: weekly
17 | - package-ecosystem: "gomod"
18 | directories:
19 | - "/"
20 | - "/samples/go-contract"
21 | schedule:
22 | interval: weekly
23 | - package-ecosystem: "npm"
24 | directory: "/samples/node-contract"
25 | schedule:
26 | interval: weekly
27 | - package-ecosystem: "gradle"
28 | directory: "/samples/java-contract"
29 | schedule:
30 | interval: weekly
31 |
--------------------------------------------------------------------------------
/docs/configuring/dedicated-nodes.md:
--------------------------------------------------------------------------------
1 | # Dedicated nodes
2 |
3 | By default, the k8s builder does not implement any Kubernetes node scheduling strategies.
4 |
5 | The `FABRIC_K8S_BUILDER_NODE_ROLE` environment variable can be used to schedule chaincode on dedicated Kubernetes nodes.
6 | Chaincode pods will be configured with an affinity for nodes with the `fabric-builder-k8s-role=` label, and will tolerate nodes with the `fabric-builder-k8s-role=:NoSchedule` taint.
7 |
8 | For example, if `FABRIC_K8S_BUILDER_NODE_ROLE` is set to `chaincode`, use the following `kubectl` commands to configure a dedicated chaincode node `ccnode`.
9 |
10 | ```shell
11 | kubectl label nodes ccnode fabric-builder-k8s-role=chaincode
12 | kubectl taint nodes ccnode fabric-builder-k8s-role=chaincode:NoSchedule
13 | ```
14 |
15 | More complex requirements should be handled with Dynamic Admission Control using a Mutating Webhook.
16 | For example, you could use a webhook to assign node affinity and tolerations to all pods in a `chaincode` namespace.
17 |
--------------------------------------------------------------------------------
/internal/builder/detect.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package builder
4 |
5 | import (
6 | "context"
7 | "errors"
8 | "strings"
9 |
10 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/log"
11 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/util"
12 | )
13 |
14 | type Detect struct {
15 | ChaincodeSourceDirectory string
16 | ChaincodeMetadataDirectory string
17 | }
18 |
19 | var ErrUnsupportedChaincodeType = errors.New("chaincode type not supported")
20 |
21 | func (d *Detect) Run(ctx context.Context) error {
22 | logger := log.New(ctx)
23 | logger.Debugln("Checking chaincode type...")
24 |
25 | metadata, err := util.ReadMetadataJSON(logger, d.ChaincodeMetadataDirectory)
26 | if err != nil {
27 | return err
28 | }
29 |
30 | if strings.ToLower(metadata.Type) == "k8s" {
31 | logger.Printf("Detected k8s chaincode: %s", metadata.Label)
32 |
33 | return nil
34 | }
35 |
36 | logger.Debugf("Chaincode type not supported: %s", metadata.Type)
37 |
38 | return ErrUnsupportedChaincodeType
39 | }
40 |
--------------------------------------------------------------------------------
/samples/node-contract/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "k8s-contract",
3 | "version": "1.0.0",
4 | "description": "Sample k8s builder contract",
5 | "main": "dist/index.js",
6 | "files": [
7 | "dist"
8 | ],
9 | "engines": {
10 | "node": ">=16",
11 | "npm": ">=8"
12 | },
13 | "scripts": {
14 | "test": "echo \"Error: no test specified\" && exit 1",
15 | "lint": "gts lint",
16 | "clean": "gts clean",
17 | "compile": "tsc",
18 | "fix": "gts fix",
19 | "pretest": "npm run compile",
20 | "postcompile": "npm run lint",
21 | "start": "set -x && fabric-chaincode-node start",
22 | "debug": "set -x && fabric-chaincode-node server --chaincode-address=$CHAINCODE_SERVER_ADDRESS --chaincode-id=$CORE_CHAINCODE_ID_NAME"
23 | },
24 | "author": "Hyperledger",
25 | "license": "Apache-2.0",
26 | "devDependencies": {
27 | "@types/node": "^25.0.2",
28 | "gts": "^6.0.2",
29 | "typescript": "^5.9.3"
30 | },
31 | "dependencies": {
32 | "fabric-contract-api": "^2.5.6",
33 | "fabric-shim": "^2.5.8"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/samples/java-contract/Dockerfile:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: Apache-2.0
2 |
3 | ARG JAVA_VER=11
4 |
5 | FROM eclipse-temurin:${JAVA_VER}-jdk-alpine AS build
6 |
7 | RUN apk add --no-cache \
8 | dumb-init
9 |
10 | WORKDIR /usr/src/app
11 |
12 | COPY ./gradle/wrapper/ ./gradle/wrapper/
13 | COPY gradlew .
14 | RUN ./gradlew --version
15 |
16 | COPY . .
17 |
18 | RUN ./gradlew jar
19 |
20 | FROM eclipse-temurin:${JAVA_VER}-jre-alpine
21 |
22 | LABEL org.opencontainers.image.title="Sample Java Contract"
23 | LABEL org.opencontainers.image.description="Sample Hyperledger Fabric Java contract for Kubernetes chaincode builder"
24 | LABEL org.opencontainers.image.source="https://github.com/hyperledger-labs/fabric-builder-k8s/samples/java-contract"
25 |
26 | WORKDIR /var/hyperledger/java-contract
27 |
28 | COPY --from=build /usr/bin/dumb-init /usr/bin/dumb-init
29 | COPY --from=build /usr/src/app/build/libs/sample-contract.jar ./sample-contract.jar
30 |
31 | ENTRYPOINT ["/usr/bin/dumb-init", "--"]
32 | CMD ["sh", "-c", "exec /opt/java/openjdk/bin/java -jar ./sample-contract.jar --peer.address=$CORE_PEER_ADDRESS"]
33 |
--------------------------------------------------------------------------------
/samples/go-contract/main.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package main
4 |
5 | import (
6 | "github.com/hyperledger/fabric-contract-api-go/v2/contractapi"
7 | )
8 |
9 | type SampleContract struct {
10 | contractapi.Contract
11 | }
12 |
13 | // PutValue - Adds a key value pair to the world state
14 | func (sc *SampleContract) PutValue(
15 | ctx contractapi.TransactionContextInterface,
16 | key string,
17 | value string,
18 | ) error {
19 | return ctx.GetStub().PutState(key, []byte(value))
20 | }
21 |
22 | // GetValue - Gets the value for a key from the world state
23 | func (sc *SampleContract) GetValue(
24 | ctx contractapi.TransactionContextInterface,
25 | key string,
26 | ) (string, error) {
27 | bytes, err := ctx.GetStub().GetState(key)
28 |
29 | if err != nil {
30 | return "", nil
31 | }
32 |
33 | return string(bytes), nil
34 | }
35 |
36 | func main() {
37 | SampleContract := new(SampleContract)
38 |
39 | cc, err := contractapi.NewChaincode(SampleContract)
40 |
41 | if err != nil {
42 | panic(err.Error())
43 | }
44 |
45 | if err := cc.Start(); err != nil {
46 | panic(err.Error())
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/internal/util/env.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package util
4 |
5 | import (
6 | "fmt"
7 | "os"
8 | )
9 |
10 | const (
11 | builderVariablePrefix = "FABRIC_K8S_BUILDER_"
12 | ChaincodeNamespaceVariable = builderVariablePrefix + "NAMESPACE"
13 | ChaincodeNodeRoleVariable = builderVariablePrefix + "NODE_ROLE"
14 | ObjectNamePrefixVariable = builderVariablePrefix + "OBJECT_NAME_PREFIX"
15 | ChaincodeServiceAccountVariable = builderVariablePrefix + "SERVICE_ACCOUNT"
16 | ChaincodeStartTimeoutVariable = builderVariablePrefix + "START_TIMEOUT"
17 | DebugVariable = builderVariablePrefix + "DEBUG"
18 | KubeconfigPathVariable = "KUBECONFIG_PATH"
19 | PeerIDVariable = "CORE_PEER_ID"
20 | )
21 |
22 | func GetOptionalEnv(key, defaultValue string) string {
23 | if value, ok := os.LookupEnv(key); ok {
24 | return value
25 | }
26 |
27 | return defaultValue
28 | }
29 |
30 | func GetRequiredEnv(key string) (string, error) {
31 | if value, ok := os.LookupEnv(key); ok {
32 | return value, nil
33 | }
34 |
35 | return "", fmt.Errorf("environment variable not set: %s", key)
36 | }
37 |
--------------------------------------------------------------------------------
/samples/go-contract/Dockerfile:
--------------------------------------------------------------------------------
1 | # SPDX-License-Identifier: Apache-2.0
2 |
3 | ARG GO_VER=1.24.0
4 | ARG ALPINE_VER=3.21
5 |
6 | FROM golang:${GO_VER}-alpine${ALPINE_VER} AS build
7 |
8 | RUN apk add --no-cache \
9 | bash \
10 | binutils-gold \
11 | dumb-init \
12 | gcc \
13 | git \
14 | make \
15 | musl-dev
16 |
17 | ADD . $GOPATH/src/github.com/hyperledger-labs/fabric-builder-k8s/samples/go-contract
18 | WORKDIR $GOPATH/src/github.com/hyperledger-labs/fabric-builder-k8s/samples/go-contract
19 |
20 | RUN go install ./...
21 |
22 | FROM golang:${GO_VER}-alpine${ALPINE_VER}
23 |
24 | LABEL org.opencontainers.image.title="Sample Go Contract"
25 | LABEL org.opencontainers.image.description="Sample Hyperledger Fabric Go contract for Kubernetes chaincode builder"
26 | LABEL org.opencontainers.image.source="https://github.com/hyperledger-labs/fabric-builder-k8s/samples/go-contract"
27 |
28 | COPY --from=build /usr/bin/dumb-init /usr/bin/dumb-init
29 | COPY --from=build /go/bin/go-contract /usr/bin/go-contract
30 |
31 | WORKDIR /var/hyperledger/go-contract
32 | ENTRYPOINT ["/usr/bin/dumb-init", "--"]
33 | CMD ["sh", "-c", "exec /usr/bin/go-contract -peer.address=$CORE_PEER_ADDRESS"]
34 |
--------------------------------------------------------------------------------
/docs/getting-started/install.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | Installing the k8s builder is part of the process of creating a production Fabric peer.
4 |
5 | For more information, see the [Checklist for a production peer](https://hyperledger-fabric.readthedocs.io/en/latest/deploypeer/peerchecklist.html#chaincode-externalbuilders) Fabric documentation.
6 |
7 | ## Sample peer image
8 |
9 | A sample [k8s-fabric-peer image](https://github.com/hyperledger-labs/fabric-builder-k8s/pkgs/container/fabric-builder-k8s%2Fk8s-fabric-peer) is available. The `k8s-fabric-peer` is based on the [hyperledger/fabric-peer](https://hub.docker.com/r/hyperledger/fabric-peer) with the k8s builder preconfigured.
10 |
11 | ## Prebuilt binaries
12 |
13 | Prebuilt binaries are available to download from the [releases page](https://github.com/hyperledger-labs/fabric-builder-k8s/releases).
14 |
15 | ## Install from source
16 |
17 | To install from source in `/opt/hyperledger/k8s_builder`, use the `go install` command.
18 |
19 | ```shell
20 | mkdir -p /opt/hyperledger/k8s_builder/bin
21 | cd /opt/hyperledger/k8s_builder/bin
22 | GOBIN="${PWD}" go install github.com/hyperledger-labs/fabric-builder-k8s/cmd/...@v0.13.0
23 | ```
24 |
--------------------------------------------------------------------------------
/samples/go-contract/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/hyperledger-labs/fabric-builder-k8s/samples/go-contract
2 |
3 | go 1.24.0
4 |
5 | require github.com/hyperledger/fabric-contract-api-go/v2 v2.2.0
6 |
7 | require (
8 | github.com/go-openapi/jsonpointer v0.21.0 // indirect
9 | github.com/go-openapi/jsonreference v0.21.0 // indirect
10 | github.com/go-openapi/spec v0.21.0 // indirect
11 | github.com/go-openapi/swag v0.23.0 // indirect
12 | github.com/hyperledger/fabric-chaincode-go/v2 v2.0.0 // indirect
13 | github.com/hyperledger/fabric-protos-go-apiv2 v0.3.4 // indirect
14 | github.com/josharian/intern v1.0.0 // indirect
15 | github.com/mailru/easyjson v0.7.7 // indirect
16 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
17 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
18 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect
19 | golang.org/x/net v0.38.0 // indirect
20 | golang.org/x/sys v0.31.0 // indirect
21 | golang.org/x/text v0.23.0 // indirect
22 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
23 | google.golang.org/grpc v1.67.0 // indirect
24 | google.golang.org/protobuf v1.36.1 // indirect
25 | gopkg.in/yaml.v3 v3.0.1 // indirect
26 | )
27 |
--------------------------------------------------------------------------------
/test/integration/testdata/dedicated_node_unavailable.txtar:
--------------------------------------------------------------------------------
1 | env CORE_PEER_ID=core-peer-id-abcdefghijklmnopqrstuvwxyz-0123456789
2 | env FABRIC_K8S_BUILDER_NAMESPACE=$TESTENV_NAMESPACE
3 | env FABRIC_K8S_BUILDER_NODE_ROLE=unavailable
4 | env FABRIC_K8S_BUILDER_START_TIMEOUT=30s
5 | env FABRIC_K8S_BUILDER_DEBUG=true
6 |
7 | # the builder should time out if the chaincode cannot be scheduled
8 | ! exec run build_output_dir run_metadata_dir
9 |
10 | stderr '^run \[\d+\]: Error running chaincode: error waiting for chaincode job testns--[a-z0-9]{24}\/hlfcc-nodeunavailablechaincodelabel-g4dgk4a4w4hos-[a-z0-9]{5} to start for chaincode ID NODE_UNAVAILABLE_CHAINCODE_LABEL:6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45: timed out waiting for the condition$'
11 |
12 | -- build_output_dir/image.json --
13 | {
14 | "name": "nginx",
15 | "digest": "sha256:da3cc3053314be9ca3871307366f6e30ce2b11e1ea6a72e5957244d99b2515bf"
16 | }
17 |
18 | -- run_metadata_dir/chaincode.json --
19 | {
20 | "chaincode_id": "NODE_UNAVAILABLE_CHAINCODE_LABEL:6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45",
21 | "peer_address": "PEER_ADDRESS",
22 | "client_cert": "CLIENT_CERT",
23 | "client_key": "CLIENT_KEY",
24 | "root_cert": "ROOT_CERT",
25 | "mspid": "MSPID"
26 | }
27 |
--------------------------------------------------------------------------------
/internal/builder/build.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package builder
4 |
5 | import (
6 | "context"
7 | "fmt"
8 |
9 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/log"
10 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/util"
11 | "k8s.io/apimachinery/pkg/util/validation"
12 | )
13 |
14 | type Build struct {
15 | ChaincodeSourceDirectory string
16 | ChaincodeMetadataDirectory string
17 | BuildOutputDirectory string
18 | }
19 |
20 | func (b *Build) Run(ctx context.Context) error {
21 | logger := log.New(ctx)
22 | logger.Debugln("Building chaincode...")
23 |
24 | metadata, err := util.ReadMetadataJSON(logger, b.ChaincodeMetadataDirectory)
25 | if err != nil {
26 | return err
27 | }
28 |
29 | if errs := validation.IsDNS1035Label(metadata.Label); len(errs) != 0 {
30 | return fmt.Errorf(
31 | "chaincode label '%s' must be a valid RFC1035 label: %v",
32 | metadata.Label,
33 | errs,
34 | )
35 | }
36 |
37 | err = util.CopyImageJSON(logger, b.ChaincodeSourceDirectory, b.BuildOutputDirectory)
38 | if err != nil {
39 | return err
40 | }
41 |
42 | err = util.CopyMetadataDir(logger, b.ChaincodeSourceDirectory, b.BuildOutputDirectory)
43 | if err != nil {
44 | return err
45 | }
46 |
47 | return nil
48 | }
49 |
--------------------------------------------------------------------------------
/test/integration/testdata/chaincode_service_account.txtar:
--------------------------------------------------------------------------------
1 | env CORE_PEER_ID=core-peer-id-abcdefghijklmnopqrstuvwxyz-0123456789
2 | env FABRIC_K8S_BUILDER_NAMESPACE=$TESTENV_NAMESPACE
3 | env FABRIC_K8S_BUILDER_SERVICE_ACCOUNT=chaincode
4 | env FABRIC_K8S_BUILDER_DEBUG=true
5 |
6 | # the builder should create a chaincode job
7 | exec run build_output_dir run_metadata_dir &builder&
8 |
9 | jobinfo SERVICE_ACCOUNT_CHAINCODE_LABEL 6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45
10 |
11 | # the chaincode job should start a chaincode pod
12 | podinfo SERVICE_ACCOUNT_CHAINCODE_LABEL 6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45
13 |
14 | # the chaincode pod should have the expected service account
15 | stdout -count=1 '^Pod service account: chaincode$'
16 |
17 | kill builder
18 |
19 | -- build_output_dir/image.json --
20 | {
21 | "name": "nginx",
22 | "digest": "sha256:da3cc3053314be9ca3871307366f6e30ce2b11e1ea6a72e5957244d99b2515bf"
23 | }
24 |
25 | -- run_metadata_dir/chaincode.json --
26 | {
27 | "chaincode_id": "SERVICE_ACCOUNT_CHAINCODE_LABEL:6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45",
28 | "peer_address": "PEER_ADDRESS",
29 | "client_cert": "CLIENT_CERT",
30 | "client_key": "CLIENT_KEY",
31 | "root_cert": "ROOT_CERT",
32 | "mspid": "MSPID"
33 | }
34 |
--------------------------------------------------------------------------------
/internal/cmd/release.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package cmd
4 |
5 | import (
6 | "context"
7 | "os"
8 | "strconv"
9 |
10 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/builder"
11 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/log"
12 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/util"
13 | )
14 |
15 | func Release() {
16 | const (
17 | expectedArgsLength = 3
18 | buildOutputDirectoryArg = 1
19 | releaseOutputDirectoryArg = 2
20 | )
21 |
22 | debug, _ := strconv.ParseBool(util.GetOptionalEnv(util.DebugVariable, "false"))
23 | ctx := log.NewCmdContext(context.Background(), debug)
24 | logger := log.New(ctx)
25 |
26 | if len(os.Args) != expectedArgsLength {
27 | logger.Println("Expected BUILD_OUTPUT_DIR and RELEASE_OUTPUT_DIR arguments")
28 |
29 | os.Exit(1)
30 | }
31 |
32 | buildOutputDirectory := os.Args[buildOutputDirectoryArg]
33 | releaseOutputDirectory := os.Args[releaseOutputDirectoryArg]
34 |
35 | logger.Debugf("Build output directory: %s", buildOutputDirectory)
36 | logger.Debugf("Release output directory: %s", releaseOutputDirectory)
37 |
38 | release := &builder.Release{
39 | BuildOutputDirectory: buildOutputDirectory,
40 | ReleaseOutputDirectory: releaseOutputDirectory,
41 | }
42 |
43 | if err := release.Run(ctx); err != nil {
44 | logger.Printf("Error releasing chaincode: %+v", err)
45 |
46 | os.Exit(1)
47 | }
48 |
49 | os.Exit(0)
50 | }
51 |
--------------------------------------------------------------------------------
/test/integration/testdata/chaincode_name_prefix.txtar:
--------------------------------------------------------------------------------
1 | env CORE_PEER_ID=core-peer-id-abcdefghijklmnopqrstuvwxyz-0123456789
2 | env FABRIC_K8S_BUILDER_NAMESPACE=$TESTENV_NAMESPACE
3 | env FABRIC_K8S_BUILDER_OBJECT_NAME_PREFIX=conga
4 | env FABRIC_K8S_BUILDER_DEBUG=true
5 |
6 | # the builder should create a chaincode job
7 | exec run build_output_dir run_metadata_dir &builder&
8 |
9 | jobinfo PREFIXED_CHAINCODE_LABEL 6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45
10 |
11 | # the chaincode job should have the expected name
12 | stdout -count=1 '^Job name: conga-prefixedchaincodelabel-4wihu4ltdr45k-[a-z0-9]{5}$'
13 |
14 | # the chaincode job should start a chaincode pod
15 | podinfo PREFIXED_CHAINCODE_LABEL 6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45
16 |
17 | # the chaincode pod should have the expected name
18 | stdout -count=1 '^Pod name: conga-prefixedchaincodelabel-4wihu4ltdr45k-[a-z0-9]{5}-[a-z0-9]{5}$'
19 |
20 | kill builder
21 |
22 | -- build_output_dir/image.json --
23 | {
24 | "name": "nginx",
25 | "digest": "sha256:da3cc3053314be9ca3871307366f6e30ce2b11e1ea6a72e5957244d99b2515bf"
26 | }
27 |
28 | -- run_metadata_dir/chaincode.json --
29 | {
30 | "chaincode_id": "PREFIXED_CHAINCODE_LABEL:6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45",
31 | "peer_address": "PEER_ADDRESS",
32 | "client_cert": "CLIENT_CERT",
33 | "client_key": "CLIENT_KEY",
34 | "root_cert": "ROOT_CERT",
35 | "mspid": "MSPID"
36 | }
37 |
--------------------------------------------------------------------------------
/docs/configuring/kubernetes-service-account.md:
--------------------------------------------------------------------------------
1 | # Kubernetes service account
2 |
3 | Chaincode pods are created with a service account defined by the `FABRIC_K8S_BUILDER_SERVICE_ACCOUNT` environment variable, or the `default` service account if the variable is not set.
4 |
5 | If your chaincode images are published to registries which require credentials, you will need to add image pull secrets to the service account.
6 |
7 | For example, follow these steps if `FABRIC_K8S_BUILDER_NAMESPACE` and `FABRIC_K8S_BUILDER_SERVICE_ACCOUNT` are both set to `hlf-chaincode`.
8 |
9 | Create the `hlf-chaincode` service account.
10 |
11 | ```shell
12 | kubectl create serviceaccount hlf-chaincode --namespace=hlf-chaincode
13 | ```
14 |
15 | Create an imagePullSecret.
16 |
17 | ```shell
18 | kubectl create secret docker-registry hlf-fabregistry-key --namespace=hlf-chaincode \
19 | --docker-server=DOCKER_SERVER \
20 | --docker-username=DOCKER_USERNAME \
21 | --docker-password=DOCKER_PASSWORD \
22 | --docker-email=DOCKER_EMAIL
23 | ```
24 |
25 | Add the image pull secret to the service account.
26 |
27 | ```shell
28 | kubectl patch serviceaccount hlf-chaincode --namespace=hlf-chaincode \
29 | -p '{"imagePullSecrets": [{"name": "hlf-fabregistry-key"}]}'
30 | ```
31 |
32 | See the Kubernetes [Configure Service Accounts for Pods](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account) documentation for details.
33 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | The Kubernetes external [chaincode builder](concepts/chaincode-builder.md) for Hyperledger Fabric (k8s builder) is an alternative to Fabric's legacy built in Docker chaincode builder, which does not work in a Kubernetes deployment, and the preconfigured chaincode-as-a-service builder, which is more suited to chaincode development and test.
4 |
5 | With the k8s builder, the Fabric administrator is responsible for preparing a [chaincode image](concepts/chaincode-image.md), publishing to a container registry, and preparing a [chaincode package](concepts/chaincode-package.md) with coordinates of the contract's immutable image digest.
6 | When Fabric detects the installation of a `type=k8s` contract, the builder assumes full ownership of the lifecycle of pods, containers, and network linkages necessary to communicate securely with the peer.
7 |
8 |
9 | Advantages:
10 |
11 | 🚀 Chaincode runs _immediately_ on channel commit.
12 |
13 | ✨ Avoids the complexity and administrative burdens associated with Chaincode-as-a-Service.
14 |
15 | 🔥 Pre-published chaincode images avoid code-compilation errors at deployment time.
16 |
17 | 🏗️ Pre-published chaincode images encourage modern, industry accepted CI/CD best practices.
18 |
19 | 🛡️ Pre-published chaincode images remove any and all dependencies on a root-level _docker daemon_.
20 |
21 | 🕵️ Pre-published chaincode images provide traceability and change management features (e.g. Git commit hash as image tag)
22 |
--------------------------------------------------------------------------------
/samples/java-contract/src/main/java/org/example/fabric/SampleContract.java:
--------------------------------------------------------------------------------
1 | /*
2 | * SPDX-License-Identifier: Apache-2.0
3 | */
4 |
5 | package org.example.fabric;
6 |
7 | import static java.nio.charset.StandardCharsets.UTF_8;
8 |
9 | import org.hyperledger.fabric.contract.Context;
10 | import org.hyperledger.fabric.contract.ContractInterface;
11 | import org.hyperledger.fabric.contract.annotation.Contract;
12 | import org.hyperledger.fabric.contract.annotation.Default;
13 | import org.hyperledger.fabric.contract.annotation.Transaction;
14 |
15 | @Contract(name = "sample")
16 | @Default
17 | public final class SampleContract implements ContractInterface {
18 |
19 | /**
20 | * Adds a key value pair to the world state.
21 | *
22 | * @param ctx the transaction context
23 | * @param key the key
24 | * @param value the value
25 | */
26 | @Transaction(intent = Transaction.TYPE.SUBMIT)
27 | public void PutValue(final Context ctx, final String key, final String value) {
28 | ctx.getStub().putState(key, value.getBytes());
29 | }
30 |
31 | /**
32 | * Gets the value for a key from the world state.
33 | *
34 | * @param ctx the transaction context
35 | * @param key the key
36 | * @return the value
37 | */
38 | @Transaction(intent = Transaction.TYPE.EVALUATE)
39 | public String GetValue(final Context ctx, final String key) {
40 | return new String(ctx.getStub().getState(key), UTF_8);
41 | }
42 | }
43 |
44 |
--------------------------------------------------------------------------------
/docs/concepts/chaincode-builder.md:
--------------------------------------------------------------------------------
1 | # Chaincode builder
2 |
3 | From version 2.0, Hyperledger Fabric supports External Builders and Launchers to manage the process of building and launching chaincode, rather than being limited to the peer's built in Docker based build and launch process.
4 |
5 | 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.
6 |
7 | | Executable | Description |
8 | | ----------- | ----------------------------------------------------------------------------------- |
9 | | detect | Detects chaincode packages with a type of `k8s` |
10 | | build | Checks that the chaincode label is a valid Kubernetes object label[^1] |
11 | | release | No-op |
12 | | run | Starts a Kubernetes pod using the chaincode image identified by an immutable digest |
13 |
14 | [^1]:
15 | The k8s builder *does not* build the chaincode image.
16 | A chaincode image must be built and published to a container registry before it can be deployed to Fabric using the k8s builder.
17 |
18 | For more information about Fabric builders, see the [External Builders and Launchers](https://hyperledger-fabric.readthedocs.io/en/latest/cc_launcher.html) documentation.
19 |
--------------------------------------------------------------------------------
/internal/util/fabric_test.go:
--------------------------------------------------------------------------------
1 | package util_test
2 |
3 | import (
4 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/util"
5 | . "github.com/onsi/ginkgo/v2"
6 | . "github.com/onsi/gomega"
7 | )
8 |
9 | var _ = Describe("Fabric", func() {
10 | DescribeTable("NewChaincodePackageID return a new ChaincodePackageID with the expected label and hash values",
11 | func(chaincodeID, expectedLabel, expectedHash string) {
12 | packageID := util.NewChaincodePackageID(chaincodeID)
13 | Expect(packageID.Label).To(Equal(expectedLabel), "The ChaincodePackageID should include the expected label")
14 | Expect(packageID.Hash).To(Equal(expectedHash), "The ChaincodePackageID should include the expected hash")
15 | },
16 | Entry("When the chaincode ID only contains one colon", "fabcar:cffa266294278404e5071cb91150d550dc0bf855149908a170b1169d6160004b", "fabcar", "cffa266294278404e5071cb91150d550dc0bf855149908a170b1169d6160004b"),
17 | // The rest are a bit of a guess since I'm not sure the package ID format is defined in detail anywhere
18 | Entry("When the chaincode ID contains more than one colon", "fab:car:cffa266294278404e5071cb91150d550dc0bf855149908a170b1169d6160004b", "fab:car", "cffa266294278404e5071cb91150d550dc0bf855149908a170b1169d6160004b"),
19 | Entry("When the chaincode ID contains a double colon", "fab::car:cffa266294278404e5071cb91150d550dc0bf855149908a170b1169d6160004b", "fab::car", "cffa266294278404e5071cb91150d550dc0bf855149908a170b1169d6160004b"),
20 | Entry("When the chaincode ID is an empty string", "", "", ""),
21 | Entry("When the chaincode ID does not contain a colon", "fabcar", "", ""),
22 | )
23 | })
24 |
--------------------------------------------------------------------------------
/samples/java-contract/build.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * This file was generated by the Gradle 'init' task.
3 | *
4 | * This generated file contains a sample Java application project to get you started.
5 | * For more details take a look at the 'Building Java & JVM projects' chapter in the Gradle
6 | * User Manual available at https://docs.gradle.org/7.3/userguide/building_java_projects.html
7 | */
8 |
9 | plugins {
10 | // Apply the application plugin to add support for building a CLI application in Java.
11 | id 'application'
12 | }
13 |
14 | repositories {
15 | mavenCentral()
16 | maven {
17 | url 'https://jitpack.io'
18 | }
19 | }
20 |
21 | dependencies {
22 | implementation 'org.hyperledger.fabric-chaincode-java:fabric-chaincode-shim:2.5.7'
23 |
24 | // Use JUnit Jupiter for testing.
25 | testImplementation 'org.junit.jupiter:junit-jupiter:6.0.1'
26 |
27 | // This dependency is used by the application.
28 | implementation 'com.google.guava:guava:33.5.0-jre'
29 | }
30 |
31 | application {
32 | // Define the main class for the application.
33 | mainClass = 'org.hyperledger.fabric.contract.ContractRouter'
34 | }
35 |
36 | tasks.named('test') {
37 | // Use JUnit Platform for unit tests.
38 | useJUnitPlatform()
39 | }
40 |
41 | jar {
42 | duplicatesStrategy = DuplicatesStrategy.WARN
43 |
44 | manifest {
45 | attributes 'Main-Class': rootProject.application.mainClass
46 | }
47 |
48 | from {
49 | configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
50 | }
51 |
52 | exclude 'META-INF/LICENSE*', 'META-INF/*.RSA', 'META-INF/*.SF','META-INF/*.DSA'
53 | }
54 |
--------------------------------------------------------------------------------
/test/integration/testdata/dedicated_node_available.txtar:
--------------------------------------------------------------------------------
1 | env CORE_PEER_ID=core-peer-id-abcdefghijklmnopqrstuvwxyz-0123456789
2 | env FABRIC_K8S_BUILDER_NAMESPACE=$TESTENV_NAMESPACE
3 | env FABRIC_K8S_BUILDER_NODE_ROLE=chaincode
4 | env FABRIC_K8S_BUILDER_DEBUG=true
5 |
6 | # the builder should create a chaincode job
7 | exec run build_output_dir run_metadata_dir &builder&
8 |
9 | jobinfo NODE_AVAILABLE_CHAINCODE_LABEL 6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45
10 |
11 | # the chaincode job should have the expected name
12 | stdout -count=1 '^Job name: hlfcc-nodeavailablechaincodelabel-7qvrnafpestwm-[a-z0-9]{5}$'
13 |
14 | # the chaincode job should start a chaincode pod
15 | podinfo NODE_AVAILABLE_CHAINCODE_LABEL 6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45
16 |
17 | # The chaincode pod should have the expected affinity
18 | stdout -count=1 '^Pod affinity: fabric-builder-k8s-role=\[chaincode\] op=In$'
19 |
20 | # The chaincode pod should have the expected toleration
21 | stdout -count=1 '^Pod toleration: fabric-builder-k8s-role=chaincode:NoSchedule op=Equal for s$'
22 |
23 | # wait builder
24 | kill builder
25 |
26 | -- build_output_dir/image.json --
27 | {
28 | "name": "nginx",
29 | "digest": "sha256:da3cc3053314be9ca3871307366f6e30ce2b11e1ea6a72e5957244d99b2515bf"
30 | }
31 |
32 | -- run_metadata_dir/chaincode.json --
33 | {
34 | "chaincode_id": "NODE_AVAILABLE_CHAINCODE_LABEL:6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45",
35 | "peer_address": "PEER_ADDRESS",
36 | "client_cert": "CLIENT_CERT",
37 | "client_key": "CLIENT_KEY",
38 | "root_cert": "ROOT_CERT",
39 | "mspid": "MSPID"
40 | }
41 |
--------------------------------------------------------------------------------
/internal/cmd/detect.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package cmd
4 |
5 | import (
6 | "context"
7 | "errors"
8 | "os"
9 | "strconv"
10 |
11 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/builder"
12 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/log"
13 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/util"
14 | )
15 |
16 | func Detect() {
17 | const (
18 | expectedArgsLength = 3
19 | chaincodeSourceDirectoryArg = 1
20 | chaincodeMetadataDirectoryArg = 2
21 | )
22 |
23 | debug, _ := strconv.ParseBool(util.GetOptionalEnv(util.DebugVariable, "false"))
24 | ctx := log.NewCmdContext(context.Background(), debug)
25 | logger := log.New(ctx)
26 |
27 | if len(os.Args) != expectedArgsLength {
28 | logger.Println("Expected CHAINCODE_SOURCE_DIR and CHAINCODE_METADATA_DIR arguments")
29 |
30 | os.Exit(1)
31 | }
32 |
33 | chaincodeSourceDirectory := os.Args[chaincodeSourceDirectoryArg]
34 | chaincodeMetadataDirectory := os.Args[chaincodeMetadataDirectoryArg]
35 |
36 | logger.Debugf("Chaincode source directory: %s", chaincodeSourceDirectory)
37 | logger.Debugf("Chaincode metadata directory: %s", chaincodeMetadataDirectory)
38 |
39 | detect := &builder.Detect{
40 | ChaincodeSourceDirectory: chaincodeSourceDirectory,
41 | ChaincodeMetadataDirectory: chaincodeMetadataDirectory,
42 | }
43 |
44 | if err := detect.Run(ctx); err != nil {
45 | if !errors.Is(err, builder.ErrUnsupportedChaincodeType) {
46 | // don't spam the peer log if it's just chaincode we don't recognise
47 | logger.Printf("Error detecting chaincode: %+v", err)
48 | }
49 |
50 | os.Exit(1)
51 | }
52 |
53 | os.Exit(0)
54 | }
55 |
--------------------------------------------------------------------------------
/internal/cmd/build.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package cmd
4 |
5 | import (
6 | "context"
7 | "os"
8 | "strconv"
9 |
10 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/builder"
11 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/log"
12 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/util"
13 | )
14 |
15 | func Build() {
16 | const (
17 | expectedArgsLength = 4
18 | chaincodeSourceDirectoryArg = 1
19 | chaincodeMetadataDirectoryArg = 2
20 | buildOutputDirectoryArg = 3
21 | )
22 |
23 | debug, _ := strconv.ParseBool(util.GetOptionalEnv(util.DebugVariable, "false"))
24 | ctx := log.NewCmdContext(context.Background(), debug)
25 | logger := log.New(ctx)
26 |
27 | if len(os.Args) != expectedArgsLength {
28 | logger.Println(
29 | "Expected CHAINCODE_SOURCE_DIR, CHAINCODE_METADATA_DIR and BUILD_OUTPUT_DIR arguments",
30 | )
31 |
32 | os.Exit(1)
33 | }
34 |
35 | chaincodeSourceDirectory := os.Args[chaincodeSourceDirectoryArg]
36 | chaincodeMetadataDirectory := os.Args[chaincodeMetadataDirectoryArg]
37 | buildOutputDirectory := os.Args[buildOutputDirectoryArg]
38 |
39 | logger.Debugf("Chaincode source directory: %s", chaincodeSourceDirectory)
40 | logger.Debugf("Chaincode metadata directory: %s", chaincodeMetadataDirectory)
41 | logger.Debugf("Build output directory: %s", buildOutputDirectory)
42 |
43 | build := &builder.Build{
44 | ChaincodeSourceDirectory: chaincodeSourceDirectory,
45 | ChaincodeMetadataDirectory: chaincodeMetadataDirectory,
46 | BuildOutputDirectory: buildOutputDirectory,
47 | }
48 |
49 | if err := build.Run(ctx); err != nil {
50 | logger.Printf("Error building chaincode: %+v", err)
51 |
52 | os.Exit(1)
53 | }
54 |
55 | os.Exit(0)
56 | }
57 |
--------------------------------------------------------------------------------
/docs/configuring/kubernetes-permissions.md:
--------------------------------------------------------------------------------
1 | # Kubernetes permissions
2 |
3 | The k8s builder needs sufficient permissions to manage chaincode pods on behalf of the Fabric `peer`.
4 |
5 | | Resource | Permissions |
6 | | -------- | -------------------------------- |
7 | | jobs | get, list, watch, create |
8 | | pods | get, list, watch, create, delete |
9 | | secrets | create, patch |
10 |
11 | For example, follow these steps if the builder will be running in the `default` namespace using the `default` service account.
12 |
13 | Create a `fabric-builder-role` role.
14 |
15 | ```shell
16 | cat < core.yaml
35 |
36 | FROM hyperledger/fabric-peer:${HLF_VERSION}
37 |
38 | LABEL org.opencontainers.image.title="K8s Hyperledger Fabric Peer"
39 | LABEL org.opencontainers.image.description="Hyperledger Fabric Peer with a preconfigured Kubernetes chaincode builder"
40 | LABEL org.opencontainers.image.source="https://github.com/hyperledger-labs/fabric-builder-k8s"
41 |
42 | COPY --from=core core.yaml ${FABRIC_CFG_PATH}
43 | COPY --from=build /go/bin/ /opt/hyperledger/k8s_builder/bin/
44 |
--------------------------------------------------------------------------------
/.devcontainer/postCreateCommand.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | #
3 | # SPDX-License-Identifier: Apache-2.0
4 | #
5 | set -eu
6 |
7 | mkdir -p "${HOME}"/.local/bin
8 |
9 | export GOENV_OS=$(go env GOOS)
10 | export GOENV_ARCH=$(go env GOARCH)
11 | export UNAME_KERNAL=$(uname -s)
12 |
13 | #
14 | # Install k9s
15 | #
16 | curl -sSL https://github.com/derailed/k9s/releases/download/v0.32.4/k9s_${UNAME_KERNAL}_${GOENV_ARCH}.tar.gz | tar -zxf - -C "${HOME}/.local/bin/" k9s && chmod +x "${HOME}/.local/bin/k9s"
17 |
18 | #
19 | # Install yq
20 | #
21 | curl -sSLo "${HOME}/.local/bin/yq" https://github.com/mikefarah/yq/releases/download/v4.43.1/yq_${GOENV_OS}_${GOENV_ARCH} && chmod +x "${HOME}/.local/bin/yq"
22 |
23 | #
24 | # Install fabric binaries and the nano test network
25 | #
26 | rm -r "${PWD}"/.fabric || true
27 | mkdir "${PWD}"/.fabric
28 | cd .fabric
29 |
30 | curl -sSLO https://raw.githubusercontent.com/hyperledger/fabric/main/scripts/install-fabric.sh && chmod +x install-fabric.sh
31 | ./install-fabric.sh binary
32 |
33 | export FABRIC_SAMPLES_COMMIT=0db64487e5e89a81d68e6871af3f0907c67e7d75
34 | curl -sSL "https://github.com/hyperledger/fabric-samples/archive/${FABRIC_SAMPLES_COMMIT}.tar.gz" | tar -xzf - --strip-components=1 fabric-samples-${FABRIC_SAMPLES_COMMIT}/test-network-nano-bash
35 |
36 | cd ..
37 |
38 | #
39 | # Add k8s builder config to fabric core.yaml
40 | #
41 | # To install the k8s builder use the following command:
42 | # GOBIN="${PWD}"/.fabric/builders/k8s_builder/bin go install ./cmd/...
43 | #
44 | export FABRIC_K8S_BUILDER_PATH="${PWD}/.fabric/builders/k8s_builder"
45 | mkdir -p "${FABRIC_K8S_BUILDER_PATH}/bin"
46 |
47 | yq -i '.chaincode.externalBuilders += { "name": "k8s_builder", "path": "${FABRIC_K8S_BUILDER_PATH}" | envsubst(ne), "propagateEnvironment": [ "CORE_PEER_ID", "KUBECONFIG_PATH", "FABRIC_K8S_BUILDER_DEBUG" ] }' .fabric/config/core.yaml
48 |
--------------------------------------------------------------------------------
/docs/about/objectives.md:
--------------------------------------------------------------------------------
1 | # Objectives
2 |
3 | The aim is for the k8s builder to work as closely as possible with the existing [Fabric chaincode lifecycle](https://hyperledger-fabric.readthedocs.io/en/latest/chaincode_lifecycle.html), making sensible compromises for deploying chaincode on Kubernetes within those limitations.
4 | (The assumption being that there are more people with Kubernetes skills than are familiar with the inner workings of Fabric!)
5 |
6 | The two key principles are:
7 |
8 | 1. **The contents of the chaincode package must uniquely identify the chaincode functions executed on the ledger:**
9 | In the case of the k8s builder the chaincode source code is not actually inside the package.
10 | In order not to break the Fabric chaincode lifecycle, the chaincode image must be specified using an immutable `@digest`, not `:label` which can be altered post commit.
11 | See [Pull an image by digest (immutable identifier)](https://docs.docker.com/engine/reference/commandline/pull/#pull-an-image-by-digest-immutable-identifier) for more details.
12 |
13 | 2. **The Fabric peer manages the chaincode process, not Kubernetes:**
14 | Running the chaincode in server mode, i.e. allowing the peer to initiate the gRPC connection, would make it possible to leave Kubernetes to manage the chaincode process by creating a chaincode deployment.
15 | Unfortunately due to limitations in Fabric's builder and launcher implementation, that is not possible and the peer expects to control the chaincode process.
16 |
17 | ## Status
18 |
19 | The k8s builder is [close to a version 1 release](https://github.com/hyperledger-labs/fabric-builder-k8s/milestone/1) and has been tested in a number of Kubernetes environments, deployment platforms, and provides semantic-revision aware [release tags](https://github.com/hyperledger-labs/fabric-builder-k8s/tags) for the external builder binaries.
20 |
21 | The current status should be considered as STABLE and any bugs or enhancements delivered as GitHub Issues in conjunction with community PRs.
22 |
--------------------------------------------------------------------------------
/.github/workflows/mkdocs.yml:
--------------------------------------------------------------------------------
1 | name: Docs build
2 |
3 | on:
4 | pull_request:
5 | branches: ["main"]
6 | paths:
7 | - 'docs/**'
8 | push:
9 | branches: ["main"]
10 | paths:
11 | - 'docs/**'
12 | workflow_dispatch:
13 |
14 | permissions: read-all
15 |
16 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
17 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
18 | concurrency:
19 | group: "pages"
20 | cancel-in-progress: false
21 |
22 | # Default to bash
23 | defaults:
24 | run:
25 | shell: bash
26 |
27 | jobs:
28 | # Build job
29 | build:
30 | runs-on: ubuntu-latest
31 | steps:
32 | - name: Checkout
33 | uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
34 | - name: Setup Python
35 | uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
36 | with:
37 | python-version: 3.x
38 | cache: "pip"
39 | - name: Setup Pages
40 | id: pages
41 | uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5
42 | - name: Install requirements
43 | run: pip install -r requirements.txt
44 | - name: Build with mkdocs
45 | run: mkdocs build --strict
46 | - name: Upload artifact
47 | uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0
48 | with:
49 | path: ./site
50 |
51 | # Deployment job
52 | deploy:
53 | if: github.event_name == 'push'
54 | permissions:
55 | contents: read
56 | pages: write
57 | id-token: write
58 | environment:
59 | name: github-pages
60 | url: ${{ steps.deployment.outputs.page_url }}
61 | runs-on: ubuntu-latest
62 | needs: build
63 | steps:
64 | - name: Deploy to GitHub Pages
65 | id: deployment
66 | uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
67 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # fabric-builder-k8s
2 |
3 | [](https://www.bestpractices.dev/projects/9817)
4 |
5 | The Kubernetes [external chaincode builder](https://hyperledger-fabric.readthedocs.io/en/latest/cc_launcher.html) for Hyperledger Fabric (k8s builder) is an alternative to Fabric's legacy built in Docker chaincode builder, which does not work in a Kubernetes deployment, and the preconfigured chaincode-as-a-service builder, which is more suited to chaincode development and test.
6 |
7 | For more information, including how to deploy your first chaincode with the k8s builder, see the [k8s builder documentation](https://labs.hyperledger.org/fabric-builder-k8s/).
8 |
9 | To find out how to report issues, suggest enhancements and contribute to the k8s builder project, see the [contributing guide](CONTRIBUTING.md).
10 |
11 | ## Overview
12 |
13 | With the k8s builder, the Fabric administrator is responsible for preparing a [chaincode image](https://labs.hyperledger.org/fabric-builder-k8s/concepts/chaincode-image/), publishing to a container registry, and preparing a [chaincode package](https://labs.hyperledger.org/fabric-builder-k8s/concepts/chaincode-package/) with coordinates of the contract's immutable image digest.
14 | When Fabric detects the installation of a `type=k8s` contract, the builder assumes full ownership of the lifecycle of pods, containers, and network linkages necessary to communicate securely with the peer.
15 |
16 | Advantages:
17 |
18 | 🚀 Chaincode runs _immediately_ on channel commit.
19 |
20 | ✨ Avoids the complexity and administrative burdens associated with Chaincode-as-a-Service.
21 |
22 | 🔥 Pre-published chaincode images avoid code-compilation errors at deployment time.
23 |
24 | 🏗️ Pre-published chaincode images encourage modern, industry accepted CI/CD best practices.
25 |
26 | 🛡️ Pre-published chaincode images remove any and all dependencies on a root-level _docker daemon_.
27 |
28 | 🕵️ Pre-published chaincode images provide traceability and change management features (e.g. Git commit hash as image tag)
29 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the
2 | // README at: https://github.com/devcontainers/templates/tree/main/src/go
3 | {
4 | "name": "Fabric k8s builder",
5 |
6 | // Use a Dockerfile. More info: https://containers.dev/guide/dockerfile
7 | "build": {
8 | "dockerfile": "Dockerfile"
9 | },
10 |
11 | // Features to add to the dev container. More info: https://containers.dev/features.
12 | "features": {
13 | "ghcr.io/devcontainers/features/docker-in-docker:2": {
14 | "enableNonRootDocker": true,
15 | "moby": true,
16 | "azureDnsAutoDetection": true,
17 | "installDockerBuildx": true,
18 | "installDockerComposeSwitch": false,
19 | "version": "latest",
20 | "dockerDashComposeVersion": "none"
21 | },
22 | "ghcr.io/devcontainers/features/kubectl-helm-minikube:1": {
23 | "version": "latest",
24 | "helm": "none",
25 | "minikube": "none"
26 | },
27 | "ghcr.io/devcontainers-extra/features/kind:1": {
28 | "version": "latest"
29 | },
30 | "ghcr.io/devcontainers/features/python:1": {
31 | "installTools": true,
32 | "version": "latest"
33 | },
34 | "ghcr.io/devcontainers-extra/features/mkdocs:2": {
35 | "version": "latest",
36 | "plugins": "mkdocs-material mike pymdown-extensions mkdocstrings[crystal,python] mkdocs-monorepo-plugin mkdocs-pdf-export-plugin mkdocs-awesome-pages-plugin"
37 | }
38 | },
39 |
40 | // Use 'forwardPorts' to make a list of ports inside the container available locally.
41 | // "forwardPorts": [],
42 |
43 | // Use 'postCreateCommand' to run commands after the container is created.
44 | "postCreateCommand": ".devcontainer/postCreateCommand.sh",
45 |
46 | // Configure tool-specific properties.
47 | // "customizations": {},
48 |
49 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
50 | // "remoteUser": "root"
51 |
52 | "containerEnv": {
53 | "FABRIC_K8S_BUILDER_DEBUG": "true",
54 | "CORE_PEER_CHAINCODEADDRESS_HOST_OVERRIDE": "dockerhost",
55 | "CORE_PEER_CHAINCODELISTENADDRESS_HOST_OVERRIDE": "0.0.0.0"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/docs/tutorials/package-chaincode.md:
--------------------------------------------------------------------------------
1 | # Creating a chaincode package
2 |
3 | To create a k8s chaincode package file, start by creating an `image.json` file.
4 | For example,
5 |
6 | ```shell
7 | cat << IMAGEJSON-EOF > image.json
8 | {
9 | "name": "ghcr.io/hyperledger-labs/go-contract",
10 | "digest": "sha256:802c336235cc1e7347e2da36c73fa2e4b6437cfc6f52872674d1e23f23bba63b"
11 | }
12 | IMAGEJSON-EOF
13 | ```
14 |
15 | The k8s builder uses digests because these are immutable, unlike tags.
16 | The docker inspect command can be used to find the digest if required.
17 |
18 | ```shell
19 | docker pull ghcr.io/hyperledger-labs/go-contract:v0.7.2
20 | docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/hyperledger-labs/go-contract:v0.7.2 | cut -d'@' -f2
21 | ```
22 |
23 | Create a `code.tar.gz` archive containing the `image.json` file.
24 |
25 | ```shell
26 | tar -czf code.tar.gz image.json
27 | ```
28 |
29 | Create a `metadata.json` file for the chaincode package.
30 | For example,
31 |
32 | ```shell
33 | cat << METADATAJSON-EOF > metadata.json
34 | {
35 | "type": "k8s",
36 | "label": "go-contract"
37 | }
38 | METADATAJSON-EOF
39 | ```
40 |
41 | Create the final chaincode package archive.
42 |
43 | ```shell
44 | tar -czf go-contract.tgz metadata.json code.tar.gz
45 | ```
46 |
47 | Ideally the chaincode package should be created in the same CI/CD pipeline which builds the docker image.
48 | There is an example [package-k8s-chaincode-action](https://github.com/hyperledgendary/package-k8s-chaincode-action) GitHub Action which can create the required k8s chaincode package.
49 |
50 | The GitHub Action repository includes a basic shell script which can also be used for automating the process above outside GitHub workflows.
51 | For example, to create a basic k8s chaincode package using the `pkgk8scc.sh` helper script.
52 |
53 | ```shell
54 | curl -fsSL https://raw.githubusercontent.com/hyperledgendary/package-k8s-chaincode-action/main/pkgk8scc.sh -o pkgk8scc.sh && chmod u+x pkgk8scc.sh
55 | ./pkgk8scc.sh -l go-contract -n ghcr.io/hyperledger-labs/go-contract -d sha256:802c336235cc1e7347e2da36c73fa2e4b6437cfc6f52872674d1e23f23bba63b
56 | ```
57 |
--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Go
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | tags: [ v** ]
7 | paths-ignore:
8 | - '*.md'
9 | - 'docs/**'
10 | - 'samples/**'
11 | pull_request:
12 | branches: [ main ]
13 | paths-ignore:
14 | - '*.md'
15 | - 'docs/**'
16 | - 'samples/**'
17 |
18 | permissions: read-all
19 |
20 | jobs:
21 |
22 | build:
23 | runs-on: ${{ matrix.os }}
24 | strategy:
25 | matrix:
26 | os: [ubuntu-latest, macOS-13]
27 | goarch: [amd64, arm64]
28 |
29 | permissions:
30 | contents: write
31 |
32 | env:
33 | GOARCH: ${{ matrix.goarch }}
34 |
35 | steps:
36 | - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
37 |
38 | - name: Set up Go
39 | uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
40 | with:
41 | go-version: 1.24
42 |
43 | - name: Build
44 | run: go build -v ./...
45 |
46 | # Testing the compiled binary will not work without emulation on arm
47 | - name: Test
48 | if: ${{ matrix.goarch != 'arm64' }}
49 | run: go test -v ./...
50 | env:
51 | FABRIC_K8S_BUILDER_DEBUG: 'true'
52 |
53 | - name: Package
54 | run: |
55 | CGO_ENABLED=0 go build -v ./cmd/build
56 | CGO_ENABLED=0 go build -v ./cmd/detect
57 | CGO_ENABLED=0 go build -v ./cmd/release
58 | CGO_ENABLED=0 go build -v ./cmd/run
59 | export GOOS=$(go env GOOS)
60 | tar -czvf fabric-builder-k8s-${GOOS}-${GOARCH}.tgz build detect release run
61 | ls -l fabric-builder-k8s-${GOOS}-${GOARCH}.tgz
62 |
63 | - name: Rename package
64 | if: startsWith(github.ref, 'refs/tags/v')
65 | run: |
66 | export GOOS=$(go env GOOS)
67 | mv fabric-builder-k8s-${GOOS}-${GOARCH}.tgz fabric-builder-k8s-${GITHUB_REF_NAME}-${GOOS}-${GOARCH}.tgz
68 |
69 | - name: Upload package
70 | run: |
71 | export GOOS=$(go env GOOS)
72 | gh release upload $GITHUB_REF_NAME fabric-builder-k8s-${GITHUB_REF_NAME}-${GOOS}-${GOARCH}.tgz
73 | if: startsWith(github.ref, 'refs/tags/v')
74 | env:
75 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
76 |
--------------------------------------------------------------------------------
/cmd/detect/main_test.go:
--------------------------------------------------------------------------------
1 | package main_test
2 |
3 | import (
4 | "os/exec"
5 |
6 | . "github.com/onsi/ginkgo/v2"
7 | . "github.com/onsi/gomega"
8 | "github.com/onsi/gomega/gbytes"
9 | "github.com/onsi/gomega/gexec"
10 | )
11 |
12 | var _ = Describe("Main", func() {
13 | DescribeTable(
14 | "Running the detect command produces the correct error code",
15 | func(expectedErrorCode int, args ...string) {
16 | command := exec.Command(detectCmdPath, args...)
17 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
18 | Expect(err).NotTo(HaveOccurred())
19 |
20 | Eventually(session).Should(gexec.Exit(expectedErrorCode))
21 | },
22 | Entry(
23 | "When the metadata contains a valid type",
24 | 0,
25 | "CHAINCODE_SOURCE_DIR",
26 | "./testdata/validtype",
27 | ),
28 | Entry(
29 | "When the metadata contains an invalid type",
30 | 1,
31 | "CHAINCODE_SOURCE_DIR",
32 | "./testdata/invalidtype",
33 | ),
34 | Entry(
35 | "When the metadata contents are invalid",
36 | 1,
37 | "CHAINCODE_SOURCE_DIR",
38 | "./testdata/invalidfile",
39 | ),
40 | Entry(
41 | "When the metadata does not exist",
42 | 1,
43 | "CHAINCODE_SOURCE_DIR",
44 | "CHAINCODE_METADATA_DIR",
45 | ),
46 | Entry("When too few arguments are provided", 1, "CHAINCODE_SOURCE_DIR"),
47 | Entry(
48 | "When too many arguments are provided",
49 | 1,
50 | "CHAINCODE_SOURCE_DIR",
51 | "CHAINCODE_METADATA_DIR",
52 | "UNEXPECTED_ARGUMENT",
53 | ),
54 | )
55 |
56 | It("Logs the label when a supported chaincode is detected", func() {
57 | command := exec.Command(detectCmdPath, "CHAINCODE_SOURCE_DIR", "./testdata/validtype")
58 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
59 | Expect(err).NotTo(HaveOccurred())
60 |
61 | Eventually(session.Err).Should(gbytes.Say(`detect \[\d+\]: Detected k8s chaincode: basic`))
62 | })
63 |
64 | It("Does not log an error when an unsupported chaincode is detected", func() {
65 | command := exec.Command(detectCmdPath, "CHAINCODE_SOURCE_DIR", "./testdata/invalidtype")
66 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
67 | Expect(err).NotTo(HaveOccurred())
68 |
69 | Eventually(session.Err).ShouldNot(gbytes.Say(`detect \[\d+\]:`))
70 | })
71 | })
72 |
--------------------------------------------------------------------------------
/docs/concepts/chaincode-package.md:
--------------------------------------------------------------------------------
1 | # Chaincode package
2 |
3 | From version 2.0, [Hyperledger Fabric chaincode packages](https://hyperledger-fabric.readthedocs.io/en/latest/cc_launcher.html#chaincode-packages) are `.tgz` files which contain two files:
4 |
5 | - `metadata.json` — the chaincode label and type
6 | - `code.tar.gz` — source artifacts for the chaincode
7 |
8 | Chaincode packages are used by the `peer lifecycle chaincode` command as part of the Fabric chaincode lifecycle to deploy chaincode. For example,
9 |
10 | ```shell
11 | peer lifecycle chaincode install go-contract.tgz
12 | ```
13 |
14 | For more information, see the [Fabric chaincode lifecycle](https://hyperledger-fabric.readthedocs.io/en/latest/chaincode_lifecycle.html) documentation.
15 |
16 | ## metadata.json
17 |
18 | The k8s builder will detect chaincode packages which have a type of `k8s`. For example,
19 |
20 | ```json
21 | {
22 | "type": "k8s",
23 | "label": "go-contract"
24 | }
25 | ```
26 |
27 | 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).
28 |
29 | ## code.tar.gz
30 |
31 | Unlike other chaincode packages, the source artifacts in a k8s chaincode package do not contain the chaincode source files.
32 | Instead, the `code.tar.gz` file contains an `image.json` file which defines which chaincode image should be used.
33 |
34 | The `code.tar.gz` file can also contain CouchDB indexes. For more information, see the [CouchDB indexes](https://hyperledger-fabric.readthedocs.io/en/latest/couchdb_as_state_database.html#couchdb-indexes) Fabric documentation.
35 |
36 | ## image.json
37 |
38 | The chaincode image must be built and published before creating the `image.json` file. The `image.json` contains the chaincode image name, and the immutable digest of the published image. For more information, see [Pull an image by digest (immutable identifier)](https://docs.docker.com/engine/reference/commandline/pull/#pull-an-image-by-digest-immutable-identifier). For example.
39 |
40 | ```json
41 | {
42 | "name": "ghcr.io/hyperledger-labs/go-contract",
43 | "digest": "sha256:802c336235cc1e7347e2da36c73fa2e4b6437cfc6f52872674d1e23f23bba63b"
44 | }
45 | ```
46 |
--------------------------------------------------------------------------------
/docs/concepts/chaincode-job.md:
--------------------------------------------------------------------------------
1 | # Chaincode job
2 |
3 | The k8s builder runs chaincode images using a long running [Kubernetes job](https://kubernetes.io/docs/concepts/workloads/controllers/job/). Using jobs instead of bare pods [enables Kubernetes to clean up chaincode pods automatically](https://kubernetes.io/docs/concepts/workloads/controllers/ttlafterfinished/).
4 |
5 | The k8s builder uses labels and annotations to help identify the Kubernetes objects it creates.
6 |
7 | ## Labels
8 |
9 | Kubernetes objects created by the k8s builder have the following labels.
10 |
11 | app.kubernetes.io/name[^1]
12 |
13 | : The name of the application, `hyperledger-fabric`
14 |
15 | app.kubernetes.io/component[^1]
16 |
17 | : The application component, `chaincode`
18 |
19 | app.kubernetes.io/created-by[^1]
20 |
21 | : The tool that created the object, `fabric-builder-k8s`
22 |
23 | app.kubernetes.io/managed-by[^1]
24 |
25 | : The tool used to manage the application, `fabric-builder-k8s`
26 |
27 | fabric-builder-k8s-cclabel
28 |
29 | : The chaincode label, e.g. `mycc`
30 |
31 | fabric-builder-k8s-cchash
32 |
33 | : Base32 encoded chaincode hash, e.g. `U7FELJ6MQXY5RHEQLN3VSIBWD3IITI3E4EVJW3KVXJ24SZO522UQ`
34 |
35 | The chaincode hash is base32 encoded so that it fits in the maximum number of characters allowed for a Kubernetes label value. For example, if you have the chaincode package ID, use the following commands to base32 encode the chaincode hash.
36 |
37 | ```shell
38 | echo $PACKAGE_ID | cut -d':' -f2 | xxd -r -p | base32 | tr -d '='
39 | ```
40 |
41 | [^1]:
42 | Kubernetes defines [recommended labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/) to describe applications and instances of applications.
43 |
44 | ## Annotations
45 |
46 | Kubernetes objects created by the k8s builder have the following annotations.
47 |
48 | fabric-builder-k8s-ccid
49 |
50 | : The full chaincode package ID, e.g. `mycc:a7ca45a7cc85f1d89c905b775920361ed089a364e12a9b6d55ba75c965ddd6a9`
51 |
52 | fabric-builder-k8s-mspid
53 |
54 | : The membership service provider ID, e.g. `DigiBank`
55 |
56 | fabric-builder-k8s-peeraddress
57 |
58 | : The peer address, e.g. `peer0.digibank.example.com`
59 |
60 | fabric-builder-k8s-peerid
61 |
62 | : The peer ID, e.g. `peer0`
63 |
64 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | linters:
3 | default: none
4 | enable:
5 | - asciicheck
6 | - bidichk
7 | - bodyclose
8 | - containedctx
9 | - contextcheck
10 | - copyloopvar
11 | - cyclop
12 | - decorder
13 | - dogsled
14 | - dupl
15 | - durationcheck
16 | - errcheck
17 | - errchkjson
18 | - errname
19 | - errorlint
20 | - exhaustive
21 | - forbidigo
22 | - forcetypeassert
23 | - funlen
24 | - gochecknoglobals
25 | - gochecknoinits
26 | - gocognit
27 | - goconst
28 | - gocritic
29 | - gocyclo
30 | - godot
31 | - goheader
32 | - gomoddirectives
33 | - gomodguard
34 | - goprintffuncname
35 | - gosec
36 | - govet
37 | - grouper
38 | - importas
39 | - ineffassign
40 | - ireturn
41 | - maintidx
42 | - makezero
43 | - misspell
44 | - mnd
45 | - nakedret
46 | - nestif
47 | - nilerr
48 | - nilnil
49 | - nlreturn
50 | - noctx
51 | - nolintlint
52 | - nonamedreturns
53 | - nosprintfhostport
54 | - prealloc
55 | - predeclared
56 | - promlinter
57 | - revive
58 | - rowserrcheck
59 | - sqlclosecheck
60 | - staticcheck
61 | - testpackage
62 | - thelper
63 | - tparallel
64 | - unconvert
65 | - unparam
66 | - unused
67 | - varnamelen
68 | - wastedassign
69 | - whitespace
70 | - wsl
71 | settings:
72 | errorlint:
73 | errorf: true
74 | funlen:
75 | lines: 100
76 | nolintlint:
77 | require-explanation: true
78 | require-specific: true
79 | exclusions:
80 | generated: lax
81 | presets:
82 | - comments
83 | - common-false-positives
84 | - legacy
85 | - std-error-handling
86 | rules:
87 | - linters:
88 | - revive
89 | path: _test\.go
90 | text: dot-imports
91 | paths:
92 | - third_party$
93 | - builtin$
94 | - examples$
95 | formatters:
96 | enable:
97 | - gci
98 | - gofmt
99 | - gofumpt
100 | - goimports
101 | exclusions:
102 | generated: lax
103 | paths:
104 | - third_party$
105 | - builtin$
106 | - examples$
107 |
--------------------------------------------------------------------------------
/test/integration/main_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | //go:build linux
4 | // +build linux
5 |
6 | package integration_test
7 |
8 | import (
9 | "context"
10 | "fmt"
11 | "testing"
12 |
13 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/cmd"
14 | "github.com/hyperledger-labs/fabric-builder-k8s/test"
15 | "github.com/rogpeppe/go-internal/testscript"
16 | batchv1 "k8s.io/api/batch/v1"
17 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
18 | "sigs.k8s.io/e2e-framework/klient/k8s/resources"
19 | "sigs.k8s.io/e2e-framework/pkg/env"
20 | "sigs.k8s.io/e2e-framework/pkg/envconf"
21 | "sigs.k8s.io/e2e-framework/pkg/envfuncs"
22 | "sigs.k8s.io/e2e-framework/support/kind"
23 | )
24 |
25 | //nolint:gochecknoglobals // not sure how to avoid this
26 | var (
27 | testenv env.Environment
28 | )
29 |
30 | func TestMain(m *testing.M) {
31 | envCfg := envconf.New().WithRandomNamespace()
32 | testenv = env.NewWithConfig(envCfg)
33 | clusterName := envconf.RandomName("test-cluster", 16)
34 |
35 | testenv.Setup(
36 | envfuncs.CreateClusterWithConfig(kind.NewProvider(), clusterName, "testdata/kind-config.yaml"),
37 | envfuncs.CreateNamespace(envCfg.Namespace()),
38 | )
39 |
40 | testenv.Finish(
41 | envfuncs.DeleteNamespace(envCfg.Namespace()),
42 | // envfuncs.ExportClusterLogs(kindClusterName, "./logs"),
43 | envfuncs.DestroyCluster(clusterName),
44 | )
45 |
46 | testenv.AfterEachTest(func(ctx context.Context, cfg *envconf.Config, t *testing.T) (context.Context, error) { //nolint:thelper // *testing.T must be last param for TestEnvFunc
47 | t.Helper()
48 |
49 | t.Logf("Deleting jobs after test %s", t.Name())
50 |
51 | client, err := cfg.NewClient()
52 | if err != nil {
53 | return ctx, fmt.Errorf("delete jobs func: %w", err)
54 | }
55 |
56 | jobs := new(batchv1.JobList)
57 |
58 | err = client.Resources(cfg.Namespace()).List(ctx, jobs)
59 | if err != nil {
60 | return ctx, fmt.Errorf("delete jobs func: %w", err)
61 | }
62 |
63 | for _, job := range jobs.Items {
64 | if err := client.Resources().Delete(ctx, &job, resources.WithDeletePropagation(string(metav1.DeletePropagationBackground))); err != nil {
65 | return ctx, fmt.Errorf("delete jobs func: %w", err)
66 | }
67 | }
68 |
69 | return ctx, nil
70 | })
71 |
72 | wm := test.NewWrappedM(m, testenv)
73 | testscript.Main(wm, map[string]func(){
74 | "run": cmd.Run,
75 | })
76 | }
77 |
--------------------------------------------------------------------------------
/internal/builder/run.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package builder
4 |
5 | import (
6 | "context"
7 | "fmt"
8 | "time"
9 |
10 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/log"
11 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/util"
12 | )
13 |
14 | type Run struct {
15 | BuildOutputDirectory string
16 | RunMetadataDirectory string
17 | PeerID string
18 | KubeconfigPath string
19 | KubeNamespace string
20 | KubeNodeRole string
21 | KubeServiceAccount string
22 | KubeNamePrefix string
23 | ChaincodeStartTimeout time.Duration
24 | }
25 |
26 | func (r *Run) Run(ctx context.Context) error {
27 | logger := log.New(ctx)
28 | logger.Debugln("Running chaincode...")
29 |
30 | imageData, err := util.ReadImageJSON(logger, r.BuildOutputDirectory)
31 | if err != nil {
32 | return err
33 | }
34 |
35 | chaincodeData, err := util.ReadChaincodeJSON(logger, r.RunMetadataDirectory)
36 | if err != nil {
37 | return err
38 | }
39 |
40 | kubeObjectName := util.GetValidRfc1035LabelName(r.KubeNamePrefix, r.PeerID, chaincodeData, util.ObjectNameSuffixLength+1)
41 |
42 | clientset, err := util.GetKubeClientset(logger, r.KubeconfigPath)
43 | if err != nil {
44 | return fmt.Errorf(
45 | "unable to connect kubernetes client for chaincode ID %s: %w",
46 | chaincodeData.ChaincodeID,
47 | err,
48 | )
49 | }
50 |
51 | secretsClient := clientset.CoreV1().Secrets(r.KubeNamespace)
52 |
53 | err = util.ApplyChaincodeSecrets(
54 | ctx,
55 | logger,
56 | secretsClient,
57 | kubeObjectName,
58 | r.KubeNamespace,
59 | r.PeerID,
60 | chaincodeData,
61 | )
62 | if err != nil {
63 | return fmt.Errorf(
64 | "unable to create kubernetes secret for chaincode ID %s: %w",
65 | chaincodeData.ChaincodeID,
66 | err,
67 | )
68 | }
69 |
70 | jobsClient := clientset.BatchV1().Jobs(r.KubeNamespace)
71 |
72 | job, err := util.CreateChaincodeJob(
73 | ctx,
74 | logger,
75 | jobsClient,
76 | kubeObjectName,
77 | r.KubeNamespace,
78 | r.KubeServiceAccount,
79 | r.KubeNodeRole,
80 | r.PeerID,
81 | chaincodeData,
82 | imageData,
83 | )
84 | if err != nil {
85 | return err
86 | }
87 |
88 | logger.Printf(
89 | "Running chaincode ID %s with kubernetes job %s/%s",
90 | chaincodeData.ChaincodeID,
91 | job.Namespace,
92 | job.Name,
93 | )
94 |
95 | batchClient := clientset.BatchV1().RESTClient()
96 |
97 | return util.WaitForChaincodeJob(ctx, logger, batchClient, job, chaincodeData.ChaincodeID, r.ChaincodeStartTimeout)
98 | }
99 |
--------------------------------------------------------------------------------
/cmd/build/main_test.go:
--------------------------------------------------------------------------------
1 | package main_test
2 |
3 | import (
4 | "os/exec"
5 | "path/filepath"
6 |
7 | . "github.com/onsi/ginkgo/v2"
8 | . "github.com/onsi/gomega"
9 | "github.com/onsi/gomega/gexec"
10 | )
11 |
12 | var _ = Describe("Main", func() {
13 | var tempDir string
14 | BeforeEach(func() {
15 | tempDir = GinkgoT().TempDir()
16 | })
17 |
18 | DescribeTable("Running the build command produces the correct error code",
19 | func(expectedErrorCode int, getArgs func() []string) {
20 | args := getArgs()
21 | command := exec.Command(buildCmdPath, args...)
22 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
23 | Expect(err).NotTo(HaveOccurred())
24 |
25 | Eventually(session).Should(gexec.Exit(expectedErrorCode))
26 | },
27 | Entry("When the image.json and metadata.json files exist", 0, func() []string {
28 | return []string{"./testdata/ccsrc/validimage", "./testdata/ccmetadata/validmetadata", tempDir}
29 | }),
30 | Entry("When the image.json file does not exist", 1, func() []string {
31 | return []string{"CHAINCODE_SOURCE_DIR", "./testdata/ccmetadata/validmetadata", "BUILD_OUTPUT_DIR"}
32 | }),
33 | Entry("When the image.json file is invalid", 1, func() []string {
34 | return []string{"./testdata/invalidimage", "./testdata/ccmetadata/validmetadata", "BUILD_OUTPUT_DIR"}
35 | }),
36 | Entry("When the metadata.json file does not exist", 1, func() []string {
37 | return []string{"./testdata/validimage", "CHAINCODE_METADATA_DIR", "BUILD_OUTPUT_DIR"}
38 | }),
39 | Entry("When the metadata.json file is invalid", 1, func() []string {
40 | return []string{"./testdata/validimage", "./testdata/ccmetadata/invalidmetadata", "BUILD_OUTPUT_DIR"}
41 | }),
42 | Entry("When the metadata.json contains an invalid label", 1, func() []string {
43 | return []string{"./testdata/validimage", "./testdata/ccmetadata/invalidlabel", "BUILD_OUTPUT_DIR"}
44 | }),
45 | Entry("When the metadata.json contains an invalid label length", 1, func() []string {
46 | return []string{"./testdata/validimage", "./testdata/ccmetadata/invalidlabellength", "BUILD_OUTPUT_DIR"}
47 | }),
48 | Entry("When too few arguments are provided", 1, func() []string {
49 | return []string{"CHAINCODE_SOURCE_DIR"}
50 | }),
51 | Entry("When too many arguments are provided", 1, func() []string {
52 | return []string{
53 | "CHAINCODE_SOURCE_DIR",
54 | "CHAINCODE_METADATA_DIR",
55 | "BUILD_OUTPUT_DIR",
56 | "UNEXPECTED_ARGUMENT",
57 | }
58 | }),
59 | )
60 |
61 | It("should copy chaincode metadata to the build output directory", func() {
62 | args := []string{"./testdata/ccsrc/withmetadata", "./testdata/ccmetadata/validmetadata", tempDir}
63 | command := exec.Command(buildCmdPath, args...)
64 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
65 | Expect(err).NotTo(HaveOccurred())
66 |
67 | Eventually(session).Should(gexec.Exit(0))
68 |
69 | indexPath := filepath.Join(tempDir, "META-INF", "test", "test.txt")
70 | Expect(indexPath).To(BeARegularFile())
71 | })
72 | })
73 |
--------------------------------------------------------------------------------
/samples/java-contract/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/docs/assets/Hyperledger_Fabric_Icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
--------------------------------------------------------------------------------
/docs/configuring/overview.md:
--------------------------------------------------------------------------------
1 | # Configuration overview
2 |
3 | Fabric peers must be configured to use the k8s external builder, and to propagate the required environment variables to configure the builder.
4 |
5 | ## Fabric peer configuration
6 |
7 | External builders are configured in the `core.yaml` file, for example:
8 |
9 | ```yaml
10 | externalBuilders:
11 | - name: k8s_builder
12 | path: /opt/hyperledger/k8s_builder
13 | propagateEnvironment:
14 | - CORE_PEER_ID
15 | - FABRIC_K8S_BUILDER_DEBUG
16 | - FABRIC_K8S_BUILDER_NAMESPACE
17 | - FABRIC_K8S_BUILDER_NODE_ROLE
18 | - FABRIC_K8S_BUILDER_OBJECT_NAME_PREFIX
19 | - FABRIC_K8S_BUILDER_SERVICE_ACCOUNT
20 | - FABRIC_K8S_BUILDER_START_TIMEOUT
21 | - KUBERNETES_SERVICE_HOST
22 | - KUBERNETES_SERVICE_PORT
23 | ```
24 |
25 | If you are only planning to use the k8s builder and do not need to fallback to the legacy Docker build process for any chaincode, check your `core.yaml` file for the `vm.endpoint` Docker endpoint configuration shown below and remove it if necessary.
26 |
27 | ```yaml
28 | vm:
29 | # Endpoint of the vm management system. For docker can be one of the following in general
30 | # unix:///var/run/docker.sock
31 | # http://localhost:2375
32 | # https://localhost:2376
33 | # If you utilize external chaincode builders and don't need the default Docker chaincode builder,
34 | # the endpoint should be unconfigured so that the peer's Docker health checker doesn't get registered.
35 | endpoint: unix:///var/run/docker.sock
36 | ```
37 |
38 | For more information, see [Configuring external builders and launchers](https://hyperledger-fabric.readthedocs.io/en/latest/cc_launcher.html#configuring-external-builders-and-launchers) in the Fabric documentation.
39 |
40 | ## Environment variables
41 |
42 | The k8s builder is configured using the following environment variables.
43 |
44 | | Name | Default | Description |
45 | | ------------------------------------- | -------------------------------- | ---------------------------------------------------- |
46 | | CORE_PEER_ID | | The Fabric peer ID (required) |
47 | | FABRIC_K8S_BUILDER_NAMESPACE | The peer namespace or `default` | The Kubernetes namespace to run chaincode with |
48 | | FABRIC_K8S_BUILDER_NODE_ROLE | | Use dedicated Kubernetes nodes to run chaincode |
49 | | FABRIC_K8S_BUILDER_OBJECT_NAME_PREFIX | `hlfcc` | Eye-catcher prefix for Kubernetes object names |
50 | | FABRIC_K8S_BUILDER_SERVICE_ACCOUNT | `default` | The Kubernetes service account to run chaincode with |
51 | | FABRIC_K8S_BUILDER_START_TIMEOUT | `3m` | The timeout when waiting for chaincode pods to start |
52 | | FABRIC_K8S_BUILDER_DEBUG | `false` | Set to `true` to enable k8s builder debug messages |
53 |
54 | The k8s builder can be run in cluster using the `KUBERNETES_SERVICE_HOST` and `KUBERNETES_SERVICE_PORT` environment variables, or it can connect using a `KUBECONFIG_PATH` environment variable.
55 |
--------------------------------------------------------------------------------
/docs/getting-started/demo.md:
--------------------------------------------------------------------------------
1 | # Quick start
2 |
3 | The [fabric-samples](https://github.com/hyperledger/fabric-samples/) Kubernetes test network includes support for the k8s builder and provides the quickest way to get started.
4 |
5 | First create a directory to download all the required files and run the demo.
6 |
7 | ```shell
8 | mkdir k8s-builder-demo
9 | cd k8s-builder-demo
10 | ```
11 |
12 | Now follow the steps below to deploy your first smart contract using the k8s builder!
13 |
14 | ## Download the Kubernetes test network
15 |
16 | Download the sample Kubernetes test network (fabric-samples isn't tagged so we'll use a known good commit).
17 |
18 | ```shell
19 | export FABRIC_SAMPLES_COMMIT=1058f9ffe16add583d1a11342deb5a9df3e5b72c
20 | curl -sSL "https://github.com/hyperledger/fabric-samples/archive/${FABRIC_SAMPLES_COMMIT}.tar.gz" | \
21 | tar -xzf - --strip-components=1 \
22 | fabric-samples-${FABRIC_SAMPLES_COMMIT}/test-network-k8s
23 | ```
24 |
25 | ## Configure the Kubernetes test network
26 |
27 | Set the following environment variables to enable the k8s builder and define which version to use.
28 |
29 | ```shell
30 | export TEST_NETWORK_CHAINCODE_BUILDER="k8s"
31 | export TEST_NETWORK_K8S_CHAINCODE_BUILDER_VERSION="0.15.1"
32 | ```
33 |
34 | ## Download chaincode samples
35 |
36 | The Kubernetes test network instructions deploy the `asset-transfer-basic` sample. The `asset-transfer-basic` sample may work with the k8s builder in some environments however it is better to download and use the [samples provided with the k8s builder](https://github.com/hyperledger-labs/fabric-builder-k8s/tree/main/samples) instead.
37 |
38 | ```shell
39 | curl -sSL "https://github.com/hyperledger-labs/fabric-builder-k8s/archive/refs/tags/v${TEST_NETWORK_K8S_CHAINCODE_BUILDER_VERSION}.tar.gz" | \
40 | tar -xzf - --strip-components=2 fabric-builder-k8s-${TEST_NETWORK_K8S_CHAINCODE_BUILDER_VERSION}/samples
41 | ```
42 |
43 | ## Start the Kubernetes test network
44 |
45 | In the `test-network-k8s` directory, follow the [Kubernetes test network](https://github.com/hyperledger/fabric-samples/tree/main/test-network-k8s) instructions to launch the network, and create a channel. Stop before deploying the `asset-transfer-basic` smart contract.
46 |
47 | ## Deploy a sample contract
48 |
49 | Use the Kubernetes test network script to deploy one of the k8s builder's sample contracts.
50 |
51 | ```shell
52 | ./network chaincode deploy sample-contract ../go-contract
53 | ```
54 |
55 | You can query the chaincode metadata to confirm that the sample was deployed successfully.
56 |
57 | ```shell
58 | ./network chaincode query sample-contract '{"Args":["org.hyperledger.fabric:GetMetadata"]}'
59 | ```
60 |
61 | Use the `kubectl` command to inspect chaincode jobs.
62 |
63 | ```shell
64 | kubectl -n test-network describe jobs -l app.kubernetes.io/created-by=fabric-builder-k8s
65 | ```
66 |
67 | ## Running transactions
68 |
69 | Use the following commands to invoke and query transactions on the sample contract.
70 |
71 | ```shell
72 | ./network chaincode invoke sample-contract '{"Args":["PutValue","asset1","green"]}'
73 | ./network chaincode query sample-contract '{"Args":["GetValue","asset1"]}'
74 | ```
75 |
76 | ## Cleaning up
77 |
78 | Follow the [Kubernetes test network](https://github.com/hyperledger/fabric-samples/tree/main/test-network-k8s) instructions to tear down the network.
79 |
--------------------------------------------------------------------------------
/.github/workflows/ossf-scorecard.yml:
--------------------------------------------------------------------------------
1 | # This workflow uses actions that are not certified by GitHub. They are provided
2 | # by a third-party and are governed by separate terms of service, privacy
3 | # policy, and support documentation.
4 |
5 | name: Scorecard supply-chain security
6 | on:
7 | # For Branch-Protection check. Only the default branch is supported. See
8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
9 | branch_protection_rule:
10 | # To guarantee Maintained check is occasionally updated. See
11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
12 | schedule:
13 | - cron: '30 3 * * 2'
14 | push:
15 | branches: [ "main" ]
16 |
17 | # Declare default permissions as read only.
18 | permissions: read-all
19 |
20 | jobs:
21 | analysis:
22 | name: Scorecard analysis
23 | runs-on: ubuntu-latest
24 | permissions:
25 | # Needed to upload the results to code-scanning dashboard.
26 | security-events: write
27 | # Needed to publish results and get a badge (see publish_results below).
28 | id-token: write
29 | # Uncomment the permissions below if installing in a private repository.
30 | # contents: read
31 | # actions: read
32 |
33 | steps:
34 | - name: "Checkout code"
35 | uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
36 | with:
37 | persist-credentials: false
38 |
39 | - name: "Run analysis"
40 | uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
41 | with:
42 | results_file: results.sarif
43 | results_format: sarif
44 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
45 | # - you want to enable the Branch-Protection check on a *public* repository, or
46 | # - you are installing Scorecard on a *private* repository
47 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional.
48 | # repo_token: ${{ secrets.SCORECARD_TOKEN }}
49 |
50 | # Public repositories:
51 | # - Publish results to OpenSSF REST API for easy access by consumers
52 | # - Allows the repository to include the Scorecard badge.
53 | # - See https://github.com/ossf/scorecard-action#publishing-results.
54 | # For private repositories:
55 | # - `publish_results` will always be set to `false`, regardless
56 | # of the value entered here.
57 | publish_results: true
58 |
59 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
60 | # format to the repository Actions tab.
61 | - name: "Upload artifact"
62 | uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
63 | with:
64 | name: SARIF file
65 | path: results.sarif
66 | retention-days: 5
67 |
68 | # Upload the results to GitHub's code scanning dashboard (optional).
69 | # Commenting out will disable upload of results to your repo's Code Scanning dashboard
70 | - name: "Upload to code-scanning"
71 | uses: github/codeql-action/upload-sarif@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
72 | with:
73 | sarif_file: results.sarif
74 |
--------------------------------------------------------------------------------
/internal/log/log.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package log
4 |
5 | import (
6 | "context"
7 | "fmt"
8 | "log"
9 | "os"
10 | )
11 |
12 | type logContextKeyType string
13 |
14 | const (
15 | cmdKey logContextKeyType = "cmd"
16 | debugKey logContextKeyType = "debug"
17 | pidKey logContextKeyType = "pid"
18 | )
19 |
20 | type minimalLogger interface {
21 | Print(v ...interface{})
22 | Printf(format string, v ...interface{})
23 | Println(v ...interface{})
24 | }
25 |
26 | type CmdLogger struct {
27 | infoLogger minimalLogger
28 | debugLogger minimalLogger
29 | }
30 |
31 | type nilLogger struct{}
32 |
33 | func (nl *nilLogger) Print(_ ...interface{}) {
34 | // do nothing
35 | }
36 |
37 | func (nl *nilLogger) Printf(_ string, _ ...interface{}) {
38 | // do nothing
39 | }
40 |
41 | func (nl *nilLogger) Println(_ ...interface{}) {
42 | // do nothing
43 | }
44 |
45 | const (
46 | flags = 0
47 | )
48 |
49 | func New(ctx context.Context) *CmdLogger {
50 | cmd, _ := CmdFromContext(ctx)
51 | pid, _ := PidFromContext(ctx)
52 |
53 | infoPrefix := fmt.Sprintf("%s [%v]: ", cmd, pid)
54 | infoLogger := log.New(os.Stderr, infoPrefix, flags)
55 |
56 | var debugLogger minimalLogger
57 |
58 | if DebugFromContext(ctx) {
59 | debugPrefix := fmt.Sprintf("%s [%v] DEBUG: ", cmd, pid)
60 | debugLogger = log.New(os.Stderr, debugPrefix, flags)
61 | } else {
62 | debugLogger = &nilLogger{}
63 | }
64 |
65 | cl := &CmdLogger{
66 | infoLogger: infoLogger,
67 | debugLogger: debugLogger,
68 | }
69 |
70 | return cl
71 | }
72 |
73 | // NewCmdContext returns a new Context with program name, process id, and
74 | // debug values.
75 | func NewCmdContext(ctx context.Context, debug bool) context.Context {
76 | cmdValue := os.Args[0]
77 | cmdContext := context.WithValue(ctx, cmdKey, cmdValue)
78 |
79 | pidValue := os.Getpid()
80 | cmdContext = context.WithValue(cmdContext, pidKey, pidValue)
81 |
82 | cmdContext = context.WithValue(cmdContext, debugKey, debug)
83 |
84 | return cmdContext
85 | }
86 |
87 | // CmdFromContext returns the program name value from the provided Context.
88 | func CmdFromContext(ctx context.Context) (string, bool) {
89 | cmd, ok := ctx.Value(cmdKey).(string)
90 |
91 | return cmd, ok
92 | }
93 |
94 | // PidFromContext returns the process ID value from the provided Context.
95 | func PidFromContext(ctx context.Context) (int, bool) {
96 | pid, ok := ctx.Value(pidKey).(int)
97 |
98 | return pid, ok
99 | }
100 |
101 | // DebugFromContext returns if debug is enabled in the provided Context.
102 | func DebugFromContext(ctx context.Context) bool {
103 | if d, ok := ctx.Value(debugKey).(bool); ok {
104 | return d
105 | }
106 |
107 | return false
108 | }
109 |
110 | func (cl *CmdLogger) Print(v ...interface{}) {
111 | cl.infoLogger.Print(v...)
112 | }
113 |
114 | func (cl *CmdLogger) Printf(format string, v ...interface{}) {
115 | cl.infoLogger.Printf(format, v...)
116 | }
117 |
118 | func (cl *CmdLogger) Println(v ...interface{}) {
119 | cl.infoLogger.Println(v...)
120 | }
121 |
122 | func (cl *CmdLogger) Debug(v ...interface{}) {
123 | cl.debugLogger.Print(v...)
124 | }
125 |
126 | func (cl *CmdLogger) Debugf(format string, v ...interface{}) {
127 | cl.debugLogger.Printf(format, v...)
128 | }
129 |
130 | func (cl *CmdLogger) Debugln(v ...interface{}) {
131 | cl.debugLogger.Println(v...)
132 | }
133 |
--------------------------------------------------------------------------------
/test/integration/testdata/run_chaincode.txtar:
--------------------------------------------------------------------------------
1 | env CORE_PEER_ID=core-peer-id-abcdefghijklmnopqrstuvwxyz-0123456789
2 | env FABRIC_K8S_BUILDER_NAMESPACE=$TESTENV_NAMESPACE
3 | env FABRIC_K8S_BUILDER_DEBUG=true
4 |
5 | # the builder should create a chaincode job
6 | exec run build_output_dir run_metadata_dir &builder&
7 |
8 | jobinfo RUN_CHAINCODE_LABEL 6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45
9 |
10 | # the chaincode job should have the expected name
11 | stdout -count=1 '^Job name: hlfcc-runchaincodelabel-uyg2zc6uzes7g-[a-z0-9]{5}$'
12 |
13 | # the chaincode job should have the expected labels
14 | stdout -count=1 '^Job label: app\.kubernetes\.io/created-by=fabric-builder-k8s$'
15 | stdout -count=1 '^Job label: app\.kubernetes\.io/managed-by=fabric-builder-k8s$'
16 | stdout -count=1 '^Job label: app\.kubernetes\.io/name=hyperledger-fabric$'
17 | stdout -count=1 '^Job label: fabric-builder-k8s-cchash=N6MMJOZJIFDXCMJO3XI2QE7O6WB56IJBYI24I6LXSLYUDJDNJNCQ$'
18 | stdout -count=1 '^Job label: fabric-builder-k8s-cclabel=RUN_CHAINCODE_LABEL$'
19 | stdout -count=1 '^Job label: app\.kubernetes\.io/component=chaincode$'
20 |
21 | # the chaincode job should have the expected annotations
22 | stdout -count=1 'Job annotation: fabric-builder-k8s-peerid=core-peer-id-abcdefghijklmnopqrstuvwxyz-0123456789$'
23 | stdout -count=1 'Job annotation: fabric-builder-k8s-ccid=RUN_CHAINCODE_LABEL:6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45$'
24 | stdout -count=1 'Job annotation: fabric-builder-k8s-mspid=MSPID$'
25 | stdout -count=1 'Job annotation: fabric-builder-k8s-peeraddress=PEER_ADDRESS$'
26 |
27 | # the chaincode job should start a chaincode pod
28 | podinfo RUN_CHAINCODE_LABEL 6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45
29 |
30 | # the chaincode pod should have the expected name
31 | stdout -count=1 '^Pod name: hlfcc-runchaincodelabel-uyg2zc6uzes7g-[a-z0-9]{5}-[a-z0-9]{5}$'
32 |
33 | # the chaincode pod should have the expected labels
34 | stdout -count=1 '^Pod label: app\.kubernetes\.io/created-by=fabric-builder-k8s$'
35 | stdout -count=1 '^Pod label: app\.kubernetes\.io/managed-by=fabric-builder-k8s$'
36 | stdout -count=1 '^Pod label: app\.kubernetes\.io/name=hyperledger-fabric$'
37 | stdout -count=1 '^Pod label: fabric-builder-k8s-cchash=N6MMJOZJIFDXCMJO3XI2QE7O6WB56IJBYI24I6LXSLYUDJDNJNCQ$'
38 | stdout -count=1 '^Pod label: fabric-builder-k8s-cclabel=RUN_CHAINCODE_LABEL$'
39 | stdout -count=1 '^Pod label: app\.kubernetes\.io/component=chaincode$'
40 |
41 | # the chaincode pod should have the expected annotations
42 | stdout -count=1 'Pod annotation: fabric-builder-k8s-peerid=core-peer-id-abcdefghijklmnopqrstuvwxyz-0123456789$'
43 | stdout -count=1 'Pod annotation: fabric-builder-k8s-ccid=RUN_CHAINCODE_LABEL:6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45$'
44 | stdout -count=1 'Pod annotation: fabric-builder-k8s-mspid=MSPID$'
45 | stdout -count=1 'Pod annotation: fabric-builder-k8s-peeraddress=PEER_ADDRESS$'
46 |
47 | kill builder
48 |
49 | -- build_output_dir/image.json --
50 | {
51 | "name": "nginx",
52 | "digest": "sha256:da3cc3053314be9ca3871307366f6e30ce2b11e1ea6a72e5957244d99b2515bf"
53 | }
54 |
55 | -- run_metadata_dir/chaincode.json --
56 | {
57 | "chaincode_id": "RUN_CHAINCODE_LABEL:6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45",
58 | "peer_address": "PEER_ADDRESS",
59 | "client_cert": "CLIENT_CERT",
60 | "client_key": "CLIENT_KEY",
61 | "root_cert": "ROOT_CERT",
62 | "mspid": "MSPID"
63 | }
64 |
--------------------------------------------------------------------------------
/.github/workflows/docker-build.yml:
--------------------------------------------------------------------------------
1 | name: Docker CI
2 |
3 | on:
4 | workflow_call:
5 | inputs:
6 | platforms:
7 | description: 'List of target platforms for Docker build.'
8 | required: true
9 | type: string
10 | image-name:
11 | description: 'A Docker image name passed from the caller workflow.'
12 | required: true
13 | type: string
14 | path:
15 | description: 'A path containing a Dockerfile passed from the caller workflow.'
16 | required: true
17 | type: string
18 | chaincode-label:
19 | description: 'An optional chaincode label passed from the caller workflow. If present, will prepare a chaincode package.'
20 | required: false
21 | type: string
22 |
23 | jobs:
24 | build:
25 | permissions:
26 | contents: write
27 | packages: write
28 | id-token: write
29 |
30 | runs-on: ubuntu-latest
31 | outputs:
32 | image_digest: ${{ steps.push.outputs.digest }}
33 |
34 | steps:
35 | - name: Checkout
36 | uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
37 | - name: Docker meta
38 | id: meta
39 | uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
40 | with:
41 | images: |
42 | ${{ inputs.image-name }}
43 | tags: |
44 | type=semver,pattern={{version}}
45 | type=semver,pattern={{major}}.{{minor}}
46 | type=semver,pattern={{major}}
47 | type=sha,format=long
48 | - name: Set up QEMU
49 | uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
50 | - name: Set up Docker Buildx
51 | uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
52 | - name: Login to GitHub Container Registry
53 | uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
54 | with:
55 | registry: ghcr.io
56 | username: ${{ github.repository_owner }}
57 | password: ${{ secrets.GITHUB_TOKEN }}
58 | - name: Build and push
59 | id: push
60 | uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
61 | with:
62 | context: ${{ inputs.path }}
63 | platforms: ${{ inputs.platforms }}
64 | push: ${{ github.event_name != 'pull_request' }}
65 | tags: ${{ steps.meta.outputs.tags }}
66 | labels: ${{ steps.meta.outputs.labels }}
67 |
68 | package:
69 | if: inputs.chaincode-label != '' && needs.build.outputs.image_digest != ''
70 | needs: build
71 | runs-on: ubuntu-latest
72 |
73 | steps:
74 | - name: Checkout
75 | uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
76 |
77 | - name: Create package
78 | uses: hyperledgendary/package-k8s-chaincode-action@ba10aea43e3d4f7991116527faf96e3c2b07abc7
79 | with:
80 | chaincode-label: ${{ inputs.chaincode-label }}
81 | chaincode-image: ${{ inputs.image-name }}
82 | chaincode-digest: ${{ needs.build.outputs.image_digest }}
83 |
84 | - name: Rename package
85 | if: startsWith(github.ref, 'refs/tags/v')
86 | run: mv ${CHAINCODE_LABEL}.tgz ${CHAINCODE_LABEL}-${CHAINCODE_VERSION}.tgz
87 | env:
88 | CHAINCODE_LABEL: ${{ inputs.chaincode-label }}
89 | CHAINCODE_VERSION: ${{ github.ref_name }}
90 |
91 | - name: Upload package
92 | run: gh release upload $GITHUB_REF_NAME ${CHAINCODE_LABEL}-${CHAINCODE_VERSION}.tgz
93 | if: startsWith(github.ref, 'refs/tags/v')
94 | env:
95 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
96 | CHAINCODE_LABEL: ${{ inputs.chaincode-label }}
97 | CHAINCODE_VERSION: ${{ github.ref_name }}
98 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Kubernetes Builder
2 | site_description: Kubernetes external chaincode builder for Hyperledger Fabric
3 | repo_name: fabric-builder-k8s
4 | repo_url: https://github.com/hyperledger-labs/fabric-builder-k8s
5 | docs_dir: docs
6 | theme:
7 | name: material
8 | logo: assets/Hyperledger_Fabric_White.svg
9 | favicon: assets/Hyperledger_Fabric_Icon.svg
10 | icon:
11 | repo: fontawesome/brands/github
12 | palette:
13 | # Palette toggle for automatic mode
14 | - media: "(prefers-color-scheme)"
15 | toggle:
16 | icon: material/brightness-auto
17 | name: Switch to light mode
18 | # Palette toggle for light mode
19 | - media: "(prefers-color-scheme: light)"
20 | scheme: default
21 | toggle:
22 | icon: material/brightness-7
23 | name: Switch to dark mode
24 | # Palette toggle for dark mode
25 | - media: "(prefers-color-scheme: dark)"
26 | scheme: slate
27 | toggle:
28 | icon: material/brightness-4
29 | name: Switch to system preference
30 | features:
31 | - content.code.copy
32 | - navigation.expand
33 | - navigation.footer
34 | - navigation.instant
35 | - navigation.tabs
36 | - navigation.tabs.sticky
37 | - navigation.top
38 | - navigation.tracking
39 | - toc.follow
40 | - toc.integrate
41 | markdown_extensions:
42 | - abbr
43 | - admonition
44 | - attr_list
45 | - def_list
46 | - footnotes
47 | - md_in_html
48 | - toc:
49 | permalink: true
50 | toc_depth: 3
51 | - pymdownx.arithmatex:
52 | generic: true
53 | - pymdownx.betterem:
54 | smart_enable: all
55 | - pymdownx.caret
56 | - pymdownx.details
57 | - pymdownx.emoji:
58 | emoji_generator: !!python/name:materialx.emoji.to_svg
59 | emoji_index: !!python/name:material.extensions.emoji.twemoji
60 | - pymdownx.highlight:
61 | anchor_linenums: true
62 | - pymdownx.inlinehilite
63 | - pymdownx.keys
64 | - pymdownx.magiclink:
65 | repo_url_shorthand: true
66 | user: squidfunk
67 | repo: mkdocs-material
68 | - pymdownx.mark
69 | - pymdownx.smartsymbols
70 | - pymdownx.superfences:
71 | custom_fences:
72 | - name: mermaid
73 | class: mermaid
74 | format: !!python/name:pymdownx.superfences.fence_code_format
75 | - pymdownx.tabbed:
76 | alternate_style: true
77 | - pymdownx.tasklist:
78 | custom_checkbox: true
79 | - pymdownx.tilde
80 | plugins:
81 | - search
82 | - mike
83 | extra:
84 | version:
85 | provider: mike
86 | social:
87 | - icon: fontawesome/brands/discord
88 | link: https://discord.gg/hyperledger
89 | name: Hyperledger Discord
90 | nav:
91 | - About:
92 | - Introduction: index.md
93 | - Objectives: about/objectives.md
94 | - Community: about/community.md
95 | - Getting Started:
96 | - Quick Start: getting-started/demo.md
97 | - Requirements: getting-started/requirements.md
98 | - Installation: getting-started/install.md
99 | - FAQs: getting-started/faqs.md
100 | - Concepts:
101 | - Chaincode builder: concepts/chaincode-builder.md
102 | - Chaincode image: concepts/chaincode-image.md
103 | - Chaincode package: concepts/chaincode-package.md
104 | - Chaincode job: concepts/chaincode-job.md
105 | - Configuring:
106 | - Configuration overview: configuring/overview.md
107 | - Kubernetes permissions: configuring/kubernetes-permissions.md
108 | - Kubernetes namespace: configuring/kubernetes-namespace.md
109 | - Kubernetes service account: configuring/kubernetes-service-account.md
110 | - Dedicated nodes: configuring/dedicated-nodes.md
111 | - Tutorials:
112 | - Developing and debugging chaincode: tutorials/develop-chaincode.md
113 | - Creating a chaincode package: tutorials/package-chaincode.md
114 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/hyperledger-labs/fabric-builder-k8s
2 |
3 | go 1.24.0
4 |
5 | require (
6 | github.com/onsi/ginkgo/v2 v2.27.2
7 | github.com/onsi/gomega v1.38.2
8 | github.com/otiai10/copy v1.14.1
9 | github.com/rogpeppe/go-internal v1.14.1
10 | k8s.io/api v0.34.2
11 | k8s.io/apimachinery v0.34.2
12 | k8s.io/client-go v0.34.2
13 | sigs.k8s.io/e2e-framework v0.6.0
14 | )
15 |
16 | require (
17 | github.com/Masterminds/semver/v3 v3.4.0 // indirect
18 | github.com/beorn7/perks v1.0.1 // indirect
19 | github.com/blang/semver/v4 v4.0.0 // indirect
20 | github.com/cespare/xxhash/v2 v2.3.0 // indirect
21 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect
22 | github.com/evanphx/json-patch/v5 v5.9.0 // indirect
23 | github.com/fxamacker/cbor/v2 v2.9.0 // indirect
24 | github.com/go-openapi/jsonpointer v0.21.0 // indirect
25 | github.com/go-openapi/jsonreference v0.20.4 // indirect
26 | github.com/go-openapi/swag v0.23.0 // indirect
27 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
28 | github.com/google/gnostic-models v0.7.0 // indirect
29 | github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
30 | github.com/google/uuid v1.6.0 // indirect
31 | github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
32 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
33 | github.com/josharian/intern v1.0.0 // indirect
34 | github.com/mailru/easyjson v0.7.7 // indirect
35 | github.com/moby/spdystream v0.5.0 // indirect
36 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
37 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
38 | github.com/otiai10/mint v1.6.3 // indirect
39 | github.com/pkg/errors v0.9.1 // indirect
40 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
41 | github.com/prometheus/client_golang v1.19.1 // indirect
42 | github.com/prometheus/client_model v0.6.1 // indirect
43 | github.com/prometheus/common v0.55.0 // indirect
44 | github.com/prometheus/procfs v0.15.1 // indirect
45 | github.com/spf13/cobra v1.8.1 // indirect
46 | github.com/vladimirvivien/gexe v0.4.1 // indirect
47 | github.com/x448/float16 v0.8.4 // indirect
48 | go.opentelemetry.io/otel v1.28.0 // indirect
49 | go.opentelemetry.io/otel/trace v1.28.0 // indirect
50 | go.yaml.in/yaml/v2 v2.4.2 // indirect
51 | go.yaml.in/yaml/v3 v3.0.4 // indirect
52 | golang.org/x/mod v0.27.0 // indirect
53 | golang.org/x/sync v0.16.0 // indirect
54 | golang.org/x/tools v0.36.0 // indirect
55 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
56 | k8s.io/component-base v0.32.1 // indirect
57 | sigs.k8s.io/controller-runtime v0.20.0 // indirect
58 | sigs.k8s.io/randfill v1.0.0 // indirect
59 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
60 | )
61 |
62 | require (
63 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
64 | github.com/go-logr/logr v1.4.3 // indirect
65 | github.com/gogo/protobuf v1.3.2 // indirect
66 | github.com/google/go-cmp v0.7.0 // indirect
67 | github.com/json-iterator/go v1.1.12 // indirect
68 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
69 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
70 | github.com/spf13/pflag v1.0.6 // indirect
71 | golang.org/x/net v0.43.0 // indirect
72 | golang.org/x/oauth2 v0.27.0 // indirect
73 | golang.org/x/sys v0.35.0 // indirect
74 | golang.org/x/term v0.34.0 // indirect
75 | golang.org/x/text v0.28.0 // indirect
76 | golang.org/x/time v0.9.0 // indirect
77 | google.golang.org/protobuf v1.36.7 // indirect
78 | gopkg.in/inf.v0 v0.9.1 // indirect
79 | gopkg.in/yaml.v3 v3.0.1 // indirect
80 | k8s.io/klog/v2 v2.130.1 // indirect
81 | k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
82 | k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
83 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
84 | sigs.k8s.io/yaml v1.6.0 // indirect
85 | )
86 |
--------------------------------------------------------------------------------
/internal/util/files.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package util
4 |
5 | import (
6 | "encoding/json"
7 | "fmt"
8 | "os"
9 | "path/filepath"
10 |
11 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/log"
12 | )
13 |
14 | // ChaincodeJSON represents the chaincode.json file that is supplied by Fabric in
15 | // the RUN_METADATA_DIR.
16 | type ChaincodeJSON struct {
17 | ChaincodeID string `json:"chaincode_id"`
18 | PeerAddress string `json:"peer_address"`
19 | ClientCert string `json:"client_cert"`
20 | ClientKey string `json:"client_key"`
21 | RootCert string `json:"root_cert"`
22 | MspID string `json:"mspid"`
23 | }
24 |
25 | // ImageJSON represents the image.json file in the k8s chaincode package.
26 | type ImageJSON struct {
27 | Name string `json:"name"`
28 | Digest string `json:"digest"`
29 | }
30 |
31 | // MetadataJSON represents the metadata.json file in the k8s chaincode package.
32 | type MetadataJSON struct {
33 | Label string `json:"label"`
34 | Type string `json:"type"`
35 | }
36 |
37 | const (
38 | ChaincodeFile = "chaincode.json"
39 | ImageFile = "image.json"
40 | MetadataFile = "metadata.json"
41 | MetadataDir = "META-INF"
42 | )
43 |
44 | // ReadChaincodeJSON reads and parses the chaincode.json file in the provided directory.
45 | func ReadChaincodeJSON(logger *log.CmdLogger, dir string) (*ChaincodeJSON, error) {
46 | chaincodeJSONPath := filepath.Join(dir, ChaincodeFile)
47 | logger.Debugf("Reading %s...", chaincodeJSONPath)
48 |
49 | chaincodeJSONContents, err := os.ReadFile(chaincodeJSONPath)
50 | if err != nil {
51 | return nil, fmt.Errorf("unable to read %s: %w", chaincodeJSONPath, err)
52 | }
53 |
54 | var chaincodeData ChaincodeJSON
55 | if err := json.Unmarshal(chaincodeJSONContents, &chaincodeData); err != nil {
56 | return nil, fmt.Errorf("unable to parse %s: %w", chaincodeJSONPath, err)
57 | }
58 |
59 | logger.Debugf("Chaincode ID: %s\n", chaincodeData.ChaincodeID)
60 |
61 | return &chaincodeData, nil
62 | }
63 |
64 | // ReadImageJSON reads and parses the image.json file in the provided directory.
65 | func ReadImageJSON(logger *log.CmdLogger, dir string) (*ImageJSON, error) {
66 | imageJSONPath := filepath.Join(dir, ImageFile)
67 | logger.Debugf("Reading %s...", imageJSONPath)
68 |
69 | imageJSONContents, err := os.ReadFile(imageJSONPath)
70 | if err != nil {
71 | return nil, fmt.Errorf("unable to read %s: %w", imageJSONPath, err)
72 | }
73 |
74 | var imageData ImageJSON
75 | if err := json.Unmarshal(imageJSONContents, &imageData); err != nil {
76 | return nil, fmt.Errorf("unable to parse %s: %w", imageJSONPath, err)
77 | }
78 |
79 | logger.Debugf("Image name: %s\nImage digest: %s\n", imageData.Name, imageData.Digest)
80 |
81 | if len(imageData.Name) == 0 || len(imageData.Digest) == 0 {
82 | return nil, fmt.Errorf("%s file must contain 'name' and 'digest'", imageJSONPath)
83 | }
84 |
85 | return &imageData, nil
86 | }
87 |
88 | // ReadMetadataJSON reads and parses the metadata.json file in the provided directory.
89 | func ReadMetadataJSON(logger *log.CmdLogger, dir string) (*MetadataJSON, error) {
90 | metadataJSONPath := filepath.Join(dir, MetadataFile)
91 | logger.Debugf("Reading %s...", metadataJSONPath)
92 |
93 | metadataJSONContents, err := os.ReadFile(metadataJSONPath)
94 | if err != nil {
95 | return nil, fmt.Errorf("unable to read %s: %w", metadataJSONPath, err)
96 | }
97 |
98 | var metadata MetadataJSON
99 | if err := json.Unmarshal(metadataJSONContents, &metadata); err != nil {
100 | return nil, fmt.Errorf("unable to parse %s: %w", metadataJSONPath, err)
101 | }
102 |
103 | logger.Debugf("Label: %s\nType: %s\n", metadata.Label, metadata.Type)
104 |
105 | if len(metadata.Label) == 0 || len(metadata.Type) == 0 {
106 | return nil, fmt.Errorf("%s file must contain 'label' and 'type'", metadataJSONPath)
107 | }
108 |
109 | return &metadata, nil
110 | }
111 |
--------------------------------------------------------------------------------
/test/integration/integration_test.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | //go:build linux
4 | // +build linux
5 |
6 | package integration_test
7 |
8 | import (
9 | "context"
10 | "testing"
11 |
12 | "github.com/hyperledger-labs/fabric-builder-k8s/test"
13 | "github.com/rogpeppe/go-internal/testscript"
14 | v1 "k8s.io/api/core/v1"
15 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16 | "sigs.k8s.io/e2e-framework/pkg/envconf"
17 | "sigs.k8s.io/e2e-framework/pkg/features"
18 | )
19 |
20 | func TestRunChaincode(t *testing.T) {
21 | testenv.Test(t, features.NewWithDescription(t.Name()+"Feature", "the builder should run chaincode in the specified namespace").
22 | Assess(t.Name()+"Assessment", func(ctx context.Context, t *testing.T, _ *envconf.Config) context.Context {
23 | t.Helper()
24 |
25 | testscript.Run(t, test.NewTestscriptParams(t, "testdata/run_chaincode.txtar", testenv))
26 |
27 | return ctx
28 | }).Feature())
29 | }
30 |
31 | func TestRunChaincodeWithNamePrefix(t *testing.T) {
32 | testenv.Test(t, features.NewWithDescription(t.Name()+"Feature", "the builder should run chaincode using kubernetes object names with the specified prefix").
33 | Assess(t.Name()+"Assessment", func(ctx context.Context, t *testing.T, _ *envconf.Config) context.Context {
34 | t.Helper()
35 |
36 | testscript.Run(t, test.NewTestscriptParams(t, "testdata/chaincode_name_prefix.txtar", testenv))
37 |
38 | return ctx
39 | }).Feature())
40 | }
41 |
42 | func TestRunChaincodeWithServiceAccount(t *testing.T) {
43 | testenv.Test(t, features.NewWithDescription(t.Name()+"Feature", "the builder should run chaincode in with the specified service account").
44 | Setup(func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
45 | t.Helper()
46 |
47 | t.Logf("Creating service account before test %s", t.Name())
48 | serviceAccount := &v1.ServiceAccount{
49 | ObjectMeta: metav1.ObjectMeta{Name: "chaincode", Namespace: cfg.Namespace()},
50 | }
51 | client, err := cfg.NewClient()
52 | if err != nil {
53 | t.Fatal(err)
54 | }
55 | if err := client.Resources().Create(ctx, serviceAccount); err != nil {
56 | t.Fatal(err)
57 | }
58 |
59 | return ctx
60 | }).
61 | Assess(t.Name()+"Assessment", func(ctx context.Context, t *testing.T, _ *envconf.Config) context.Context {
62 | t.Helper()
63 |
64 | testscript.Run(t, test.NewTestscriptParams(t, "testdata/chaincode_service_account.txtar", testenv))
65 |
66 | return ctx
67 | }).
68 | Teardown(func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
69 | t.Helper()
70 |
71 | t.Logf("Deleting service account after test %s", t.Name())
72 | serviceAccount := &v1.ServiceAccount{
73 | ObjectMeta: metav1.ObjectMeta{Name: "chaincode", Namespace: cfg.Namespace()},
74 | }
75 | client, err := cfg.NewClient()
76 | if err != nil {
77 | t.Fatal(err)
78 | }
79 | if err := client.Resources().Delete(ctx, serviceAccount); err != nil {
80 | t.Fatal(err)
81 | }
82 |
83 | return ctx
84 | }).Feature())
85 | }
86 |
87 | func TestRunChaincodeWithAvailableNodeRole(t *testing.T) {
88 | testenv.Test(t, features.NewWithDescription(t.Name()+"Feature", "the builder should run chaincode on a dedicated kubernetes node").
89 | Assess(t.Name()+"Assessment", func(ctx context.Context, t *testing.T, _ *envconf.Config) context.Context {
90 | t.Helper()
91 |
92 | testscript.Run(t, test.NewTestscriptParams(t, "testdata/dedicated_node_available.txtar", testenv))
93 |
94 | return ctx
95 | }).Feature())
96 | }
97 |
98 | func TestRunChaincodeWithoutAvailableNodeRole(t *testing.T) {
99 | testenv.Test(t, features.NewWithDescription(t.Name()+"Feature", "the builder should fail to run chaincode if a kubernetes node with the required role is not available").
100 | Assess(t.Name()+"Assessment", func(ctx context.Context, t *testing.T, _ *envconf.Config) context.Context {
101 | t.Helper()
102 |
103 | testscript.Run(t, test.NewTestscriptParams(t, "testdata/dedicated_node_unavailable.txtar", testenv))
104 |
105 | return ctx
106 | }).Feature())
107 | }
108 |
--------------------------------------------------------------------------------
/cmd/release/main_test.go:
--------------------------------------------------------------------------------
1 | package main_test
2 |
3 | import (
4 | "os/exec"
5 | "path/filepath"
6 |
7 | . "github.com/onsi/ginkgo/v2"
8 | . "github.com/onsi/gomega"
9 | "github.com/onsi/gomega/gexec"
10 | )
11 |
12 | var _ = Describe("Main", func() {
13 | var tempDir string
14 | BeforeEach(func() {
15 | tempDir = GinkgoT().TempDir()
16 | })
17 |
18 | DescribeTable("Running the release command produces the correct error code",
19 | func(expectedErrorCode int, getArgs func() []string) {
20 | args := getArgs()
21 | command := exec.Command(releaseCmdPath, args...)
22 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
23 | Expect(err).NotTo(HaveOccurred())
24 |
25 | Eventually(session).Should(gexec.Exit(expectedErrorCode))
26 | },
27 | Entry("When there is no chaincode metadata", 0, func() []string {
28 | return []string{"./testdata/buildwithoutindexes", "RELEASE_OUTPUT_DIR"}
29 | }),
30 | Entry("When there is chaincode metadata", 0, func() []string {
31 | return []string{"./testdata/buildwithindexes", tempDir}
32 | }),
33 | Entry("When too few arguments are provided", 1, func() []string {
34 | return []string{"BUILD_OUTPUT_DIR"}
35 | }),
36 | Entry("When too many arguments are provided", 1, func() []string {
37 | return []string{"BUILD_OUTPUT_DIR", "RELEASE_OUTPUT_DIR", "UNEXPECTED_ARGUMENT"}
38 | }),
39 | )
40 |
41 | It("should only copy .json CouchDB index definitions to the release output directory", func() {
42 | args := []string{"./testdata/buildwithindexes", tempDir}
43 | command := exec.Command(releaseCmdPath, args...)
44 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
45 | Expect(err).NotTo(HaveOccurred())
46 |
47 | Eventually(session).Should(gexec.Exit(0))
48 |
49 | indexPath := filepath.Join(tempDir, "statedb", "couchdb", "indexes", "indexOwner.json")
50 | Expect(indexPath).To(BeARegularFile())
51 |
52 | assetPrivateDataCollectionIndexPath := filepath.Join(tempDir, "statedb", "couchdb", "collections", "assetCollection", "indexes", "indexOwner.json")
53 | Expect(assetPrivateDataCollectionIndexPath).To(BeARegularFile(), "Private data index should be copied")
54 |
55 | fabCarPrivateDataCollectionIndexPath := filepath.Join(tempDir, "statedb", "couchdb", "collections", "fabCarCollection", "indexes", "indexOwner.json")
56 | Expect(fabCarPrivateDataCollectionIndexPath).To(BeARegularFile(), "Private data index should be copied")
57 |
58 | textPath := filepath.Join(tempDir, "statedb", "couchdb", "indexes", "test.txt")
59 | Expect(textPath).NotTo(BeAnExistingFile(), "Unexpected files should not be copied")
60 |
61 | subdirPath := filepath.Join(
62 | tempDir,
63 | "statedb",
64 | "couchdb",
65 | "indexes",
66 | "subdir",
67 | "indexOwner.json",
68 | )
69 | Expect(subdirPath).NotTo(BeAnExistingFile(), "Files outside indexes directory should not be copied")
70 |
71 | privateDataCollectionSubdirPath := filepath.Join(
72 | tempDir,
73 | "statedb",
74 | "couchdb",
75 | "collections",
76 | "fabCarCollection",
77 | "subdir",
78 | "indexes",
79 | "indexOwner.json",
80 | )
81 | Expect(privateDataCollectionSubdirPath).NotTo(BeAnExistingFile(), "Files outside indexes directory should not be copied")
82 |
83 | collectionsdCollectionPath := filepath.Join(
84 | tempDir,
85 | "statedb",
86 | "couchdb",
87 | "collectionsd",
88 | "fabCarCollection",
89 | "indexes",
90 | "indexOwner.json",
91 | )
92 | Expect(collectionsdCollectionPath).NotTo(BeAnExistingFile(), "Files outside indexes directory should not be copied")
93 |
94 | indexedCollectionSubdirPath := filepath.Join(
95 | tempDir,
96 | "statedb",
97 | "couchdb",
98 | "indexed",
99 | "indexes",
100 | "indexOwner.json",
101 | )
102 | Expect(indexedCollectionSubdirPath).NotTo(BeAnExistingFile(), "Files outside indexes directory should not be copied")
103 |
104 | rootIndexOwnerJSONFile := filepath.Join(
105 | tempDir,
106 | "statedb",
107 | "couchdb",
108 | "indexOwner.json",
109 | )
110 | Expect(rootIndexOwnerJSONFile).NotTo(BeAnExistingFile(), "Files outside indexes directory should not be copied")
111 |
112 | roottestTXTFile := filepath.Join(
113 | tempDir,
114 | "statedb",
115 | "couchdb",
116 | "test.txt",
117 | )
118 | Expect(roottestTXTFile).NotTo(BeAnExistingFile(), "Files outside indexes directory should not be copied")
119 | })
120 | })
121 |
--------------------------------------------------------------------------------
/docs/tutorials/fabric-operator.md:
--------------------------------------------------------------------------------
1 | # Fabric Operator
2 |
3 | The [Fabric Operator](https://github.com/hyperledger-labs/fabric-operator) includes support for the k8s builder through its `TEST_NETWORK_PEER_IMAGE` and `TEST_NETWORK_PEER_IMAGE_LABEL` environment variables.
4 |
5 | ## Create a Sample Network
6 |
7 | Before following [the Fabric Operator sample network instructions](https://github.com/hyperledger-labs/fabric-operator/tree/main/sample-network), export the following environment variables to use the k8s builder peer image:
8 |
9 | ```shell
10 | export TEST_NETWORK_PEER_IMAGE=ghcr.io/hyperledger-labs/k8s-fabric-peer
11 | export TEST_NETWORK_PEER_IMAGE_LABEL=v0.6.0
12 | ```
13 |
14 | To create a kind-based sample network using a [fabric-devenv](https://github.com/hyperledgendary/fabric-devenv) VM, run the following commands in the `fabric-operator/sample-network` directory:
15 |
16 | ```shell
17 | export PATH=$PWD:$PWD/bin:$PATH
18 | export TEST_NETWORK_KUBE_DNS_DOMAIN=test-network
19 | export TEST_NETWORK_INGRESS_DOMAIN=localho.st
20 | network kind
21 | network cluster init
22 | network up
23 | network channel create
24 | ```
25 |
26 | See the [full Fabric Operator sample network guide](https://github.com/hyperledger-labs/fabric-operator/tree/main/sample-network#k8s-chaincode-builder) for more details, prereqs, and alternative cluster options.
27 |
28 | ## Set the `peer` CLI environment
29 |
30 | Set the `peer` command environment, e.g. for org1, peer1, run the following commands in the `fabric-operator/sample-network` directory:
31 |
32 | ```shell
33 | export FABRIC_CFG_PATH=${PWD}/temp/config
34 | export CORE_PEER_LOCALMSPID=Org1MSP
35 | export CORE_PEER_ADDRESS=test-network-org1-peer1-peer.${TEST_NETWORK_INGRESS_DOMAIN}:443
36 | export CORE_PEER_TLS_ENABLED=true
37 | export CORE_PEER_MSPCONFIGPATH=${PWD}/temp/enrollments/org1/users/org1admin/msp
38 | export CORE_PEER_TLS_ROOTCERT_FILE=${PWD}/temp/channel-msp/peerOrganizations/org1/msp/tlscacerts/tlsca-signcert.pem
39 | ```
40 |
41 | ## Download a chaincode package
42 |
43 | The [sample contracts for Go, Java, and Node.js](https://github.com/hyperledger-labs/fabric-builder-k8s/tree/main/samples) publish a Docker image which the k8s builder can use _and_ a chaincode package file which can be used with the `peer lifecycle chaincode install` command.
44 | Use of a pre-generated chaincode package .tgz greatly simplifies the deployment, aligning with standard industry practices for CI/CD and git-ops workflows.
45 |
46 | Download a sample chaincode package, e.g. for the Go contract:
47 |
48 | ```shell
49 | curl -fsSL \
50 | https://github.com/hyperledger-labs/fabric-builder-k8s/releases/download/v0.7.2/go-contract-v0.7.2.tgz \
51 | -o go-contract-v0.7.2.tgz
52 | ```
53 |
54 | ## Deploying chaincode
55 |
56 | Deploy the chaincode package as usual, starting by installing the k8s chaincode package.
57 |
58 | ```shell
59 | peer lifecycle chaincode install go-contract-v0.7.2.tgz
60 | ```
61 |
62 | Export a `PACKAGE_ID` environment variable for use in the following commands.
63 |
64 | ```shell
65 | export PACKAGE_ID=$(peer lifecycle chaincode calculatepackageid go-contract-v0.7.2.tgz) && echo $PACKAGE_ID
66 | ```
67 |
68 | Note: the `PACKAGE_ID` must match the chaincode code package identifier shown by the `peer lifecycle chaincode install` command.
69 |
70 | Approve the chaincode:
71 |
72 | ```shell
73 | peer lifecycle \
74 | chaincode approveformyorg \
75 | --channelID mychannel \
76 | --name sample-contract \
77 | --version 1 \
78 | --package-id ${PACKAGE_ID} \
79 | --sequence 1 \
80 | --orderer test-network-org0-orderersnode1-orderer.${TEST_NETWORK_INGRESS_DOMAIN}:443 \
81 | --tls --cafile ${PWD}/temp/channel-msp/ordererOrganizations/org0/orderers/org0-orderersnode1/tls/signcerts/tls-cert.pem \
82 | --connTimeout 15s
83 | ```
84 |
85 | Commit the chaincode.
86 |
87 | ```shell
88 | peer lifecycle \
89 | chaincode commit \
90 | --channelID mychannel \
91 | --name sample-contract \
92 | --version 1 \
93 | --sequence 1 \
94 | --orderer test-network-org0-orderersnode1-orderer.${TEST_NETWORK_INGRESS_DOMAIN}:443 \
95 | --tls --cafile ${PWD}/temp/channel-msp/ordererOrganizations/org0/orderers/org0-orderersnode1/tls/signcerts/tls-cert.pem \
96 | --connTimeout 15s
97 | ```
98 |
99 | Inspect chaincode pods.
100 |
101 | ```shell
102 | kubectl -n test-network describe pods -l app.kubernetes.io/created-by=fabric-builder-k8s
103 | ```
104 |
105 | ## Running transactions
106 |
107 | Query the chaincode metadata!
108 |
109 | ```shell
110 | network chaincode query sample-contract '{"Args":["org.hyperledger.fabric:GetMetadata"]}'
111 | ```
112 |
--------------------------------------------------------------------------------
/docs/tutorials/bevel-operator.md:
--------------------------------------------------------------------------------
1 | # Hyperledger Fabric Operator
2 |
3 | The k8s builder can be used with the [Hyperledger Fabric Operator](https://github.com/hyperledger-labs/hlf-operator) by following the instructions below.
4 |
5 | ## Create a Demo Network
6 |
7 | Follow the [Hyperledger Fabric Operator getting started instructions](https://labs.hyperledger.org/hlf-operator/docs/getting-started) with the following modifications (TBC)...
8 |
9 | Configure the hfl-operator to use the k8s builder peer image:
10 |
11 | ```shell
12 | export PEER_IMAGE=ghcr.io/hyperledger-labs/k8s-fabric-peer
13 | export PEER_VERSION=v0.6.0
14 | ```
15 |
16 | After creating the peer, patch it to include the k8s builder configuration:
17 |
18 | ```shell
19 | kubectl patch peer org1-peer0 --type=json --patch-file=/dev/stdin <<-EOF
20 | [
21 | {
22 | "op" : "add",
23 | "path" : "/spec/externalBuilders/-",
24 | "value" : {
25 | "name" : "k8s_builder",
26 | "path" : "/opt/hyperledger/k8s_builder",
27 | "propagateEnvironment" : [
28 | "CORE_PEER_ID",
29 | "FABRIC_K8S_BUILDER_DEBUG",
30 | "FABRIC_K8S_BUILDER_NAMESPACE",
31 | "FABRIC_K8S_BUILDER_SERVICE_ACCOUNT",
32 | "KUBERNETES_SERVICE_HOST",
33 | "KUBERNETES_SERVICE_PORT"
34 | ]
35 | }
36 | }
37 | ]
38 | EOF
39 | ```
40 |
41 | Note: the configuration change does not get picked up without restarting the pods:
42 |
43 | ```shell
44 | kubectl scale deployment org1-peer0 --replicas=0 -n default
45 | kubectl scale deployment org1-peer0 --replicas=1 -n default
46 | ```
47 |
48 | TODO: Is there a better way to create peers with the builder pre-configured?
49 |
50 | Ensure the k8s builder has the required permissions to manage chaincode pods:
51 |
52 | ```shell
53 | cat <
145 |
146 | See our [Code of Conduct Guidelines](./CODE_OF_CONDUCT.md).
147 |
148 |
--------------------------------------------------------------------------------
/internal/cmd/run.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package cmd
4 |
5 | import (
6 | "context"
7 | "os"
8 | "time"
9 |
10 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/builder"
11 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/log"
12 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/util"
13 | apivalidation "k8s.io/apimachinery/pkg/api/validation"
14 | "k8s.io/apimachinery/pkg/util/validation"
15 | )
16 |
17 | //nolint:nonamedreturns // using the ok bool convention to indicate errors
18 | func getPeerID(logger *log.CmdLogger) (peerID string, ok bool) {
19 | peerID, err := util.GetRequiredEnv(util.PeerIDVariable)
20 | if err != nil {
21 | logger.Printf("Expected %s environment variable\n", util.PeerIDVariable)
22 |
23 | return peerID, false
24 | }
25 |
26 | logger.Debugf("%s=%s", util.PeerIDVariable, peerID)
27 |
28 | return peerID, true
29 | }
30 |
31 | func getKubeconfigPath(logger *log.CmdLogger) string {
32 | kubeconfigPath := util.GetOptionalEnv(util.KubeconfigPathVariable, "")
33 | logger.Debugf("%s=%s", util.KubeconfigPathVariable, kubeconfigPath)
34 |
35 | return kubeconfigPath
36 | }
37 |
38 | func getKubeNamespace(logger *log.CmdLogger) string {
39 | kubeNamespace := util.GetOptionalEnv(util.ChaincodeNamespaceVariable, "")
40 | logger.Debugf("%s=%s", util.ChaincodeNamespaceVariable, kubeNamespace)
41 |
42 | if kubeNamespace == "" {
43 | var err error
44 |
45 | kubeNamespace, err = util.GetKubeNamespace()
46 | if err != nil {
47 | logger.Debugf("Error getting namespace: %+v\n", util.DefaultNamespace, err)
48 | kubeNamespace = util.DefaultNamespace
49 | }
50 |
51 | logger.Debugf("Using default namespace: %s\n", util.DefaultNamespace)
52 | }
53 |
54 | return kubeNamespace
55 | }
56 |
57 | //nolint:nonamedreturns // using the ok bool convention to indicate errors
58 | func getKubeNodeRole(logger *log.CmdLogger) (kubeNodeRole string, ok bool) {
59 | kubeNodeRole = util.GetOptionalEnv(util.ChaincodeNodeRoleVariable, "")
60 | logger.Debugf("%s=%s", util.ChaincodeNodeRoleVariable, kubeNodeRole)
61 |
62 | // TODO: are valid taint values the same?!
63 | if msgs := validation.IsValidLabelValue(kubeNodeRole); len(msgs) > 0 {
64 | logger.Printf("The %s environment variable must be a valid Kubernetes label value: %s", util.ChaincodeNodeRoleVariable, msgs[0])
65 |
66 | return kubeNodeRole, false
67 | }
68 |
69 | return kubeNodeRole, true
70 | }
71 |
72 | func getKubeServiceAccount(logger *log.CmdLogger) string {
73 | kubeServiceAccount := util.GetOptionalEnv(util.ChaincodeServiceAccountVariable, util.DefaultServiceAccountName)
74 | logger.Debugf("%s=%s", util.ChaincodeServiceAccountVariable, kubeServiceAccount)
75 |
76 | return kubeServiceAccount
77 | }
78 |
79 | //nolint:nonamedreturns // using the ok bool convention to indicate errors
80 | func getKubeNamePrefix(logger *log.CmdLogger) (kubeNamePrefix string, ok bool) {
81 | const maximumKubeNamePrefixLength = 30
82 |
83 | kubeNamePrefix = util.GetOptionalEnv(util.ObjectNamePrefixVariable, util.DefaultObjectNamePrefix)
84 | logger.Debugf("%s=%s", util.ObjectNamePrefixVariable, kubeNamePrefix)
85 |
86 | if len(kubeNamePrefix) > maximumKubeNamePrefixLength {
87 | logger.Printf("The %s environment variable must be a maximum of 30 characters", util.ObjectNamePrefixVariable)
88 |
89 | return kubeNamePrefix, false
90 | }
91 |
92 | if msgs := apivalidation.NameIsDNS1035Label(kubeNamePrefix, true); len(msgs) > 0 {
93 | logger.Printf("The %s environment variable must be a valid DNS-1035 label: %s", util.ObjectNamePrefixVariable, msgs[0])
94 |
95 | return kubeNamePrefix, false
96 | }
97 |
98 | return kubeNamePrefix, true
99 | }
100 |
101 | //nolint:nonamedreturns // using the ok bool convention to indicate errors
102 | func getChaincodeStartTimeout(logger *log.CmdLogger) (chaincodeStartTimeoutDuration time.Duration, ok bool) {
103 | chaincodeStartTimeout := util.GetOptionalEnv(util.ChaincodeStartTimeoutVariable, util.DefaultStartTimeout)
104 | logger.Debugf("%s=%s", util.ChaincodeStartTimeoutVariable, chaincodeStartTimeout)
105 |
106 | chaincodeStartTimeoutDuration, err := time.ParseDuration(chaincodeStartTimeout)
107 | if err != nil {
108 | logger.Printf("The %s environment variable must be a valid Go duration string, e.g. 3m40s: %v", util.ChaincodeStartTimeoutVariable, err)
109 |
110 | return 0 * time.Minute, false
111 | }
112 |
113 | return chaincodeStartTimeoutDuration, true
114 | }
115 |
116 | func Run() {
117 | const (
118 | expectedArgsLength = 3
119 | buildOutputDirectoryArg = 1
120 | runMetadataDirectoryArg = 2
121 | )
122 |
123 | debug := util.GetOptionalEnv(util.DebugVariable, "false")
124 | ctx := log.NewCmdContext(context.Background(), debug == "true")
125 | logger := log.New(ctx)
126 |
127 | if len(os.Args) != expectedArgsLength {
128 | logger.Println("Expected BUILD_OUTPUT_DIR and RUN_METADATA_DIR arguments")
129 |
130 | os.Exit(1)
131 | }
132 |
133 | buildOutputDirectory := os.Args[buildOutputDirectoryArg]
134 | runMetadataDirectory := os.Args[runMetadataDirectoryArg]
135 |
136 | logger.Debugf("Build output directory: %s", buildOutputDirectory)
137 | logger.Debugf("Run metadata directory: %s", runMetadataDirectory)
138 |
139 | //nolint:varnamelen // using the ok bool convention to indicate errors
140 | var ok bool
141 |
142 | peerID, ok := getPeerID(logger)
143 | if !ok {
144 | os.Exit(1)
145 | }
146 |
147 | kubeconfigPath := getKubeconfigPath(logger)
148 | kubeNamespace := getKubeNamespace(logger)
149 |
150 | kubeNodeRole, ok := getKubeNodeRole(logger)
151 | if !ok {
152 | os.Exit(1)
153 | }
154 |
155 | kubeServiceAccount := getKubeServiceAccount(logger)
156 |
157 | kubeNamePrefix, ok := getKubeNamePrefix(logger)
158 | if !ok {
159 | os.Exit(1)
160 | }
161 |
162 | chaincodeStartTimeout, ok := getChaincodeStartTimeout(logger)
163 | if !ok {
164 | os.Exit(1)
165 | }
166 |
167 | run := &builder.Run{
168 | BuildOutputDirectory: buildOutputDirectory,
169 | RunMetadataDirectory: runMetadataDirectory,
170 | PeerID: peerID,
171 | KubeconfigPath: kubeconfigPath,
172 | KubeNamespace: kubeNamespace,
173 | KubeNodeRole: kubeNodeRole,
174 | KubeServiceAccount: kubeServiceAccount,
175 | KubeNamePrefix: kubeNamePrefix,
176 | ChaincodeStartTimeout: chaincodeStartTimeout,
177 | }
178 |
179 | if err := run.Run(ctx); err != nil {
180 | logger.Printf("Error running chaincode: %+v", err)
181 |
182 | os.Exit(1)
183 | }
184 |
185 | os.Exit(0)
186 | }
187 |
--------------------------------------------------------------------------------
/internal/util/copy.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package util
4 |
5 | import (
6 | "fmt"
7 | "os"
8 | "path/filepath"
9 | "strings"
10 |
11 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/log"
12 | "github.com/otiai10/copy"
13 | )
14 |
15 | // CopyImageJSON validates and copies the chaincode image file.
16 | func CopyImageJSON(logger *log.CmdLogger, src, dest string) error {
17 | imageSrcPath := filepath.Join(src, ImageFile)
18 | imageDestPath := filepath.Join(dest, ImageFile)
19 |
20 | logger.Debugf("Copying chaincode image file from %s to %s", imageSrcPath, imageDestPath)
21 |
22 | err := copy.Copy(imageSrcPath, imageDestPath)
23 | if err != nil {
24 | return fmt.Errorf(
25 | "failed to copy chaincode image file from %s to %s: %w",
26 | imageSrcPath,
27 | imageDestPath,
28 | err,
29 | )
30 | }
31 |
32 | logger.Debugf("Verifying chaincode image file %s", imageDestPath)
33 |
34 | _, err = ReadImageJSON(logger, dest)
35 | if err != nil {
36 | return err
37 | }
38 |
39 | return nil
40 | }
41 |
42 | // CopyIndexFiles copies CouchDB index definitions from source to destination directories.
43 | func CopyIndexFiles(logger *log.CmdLogger, src, dest string) error {
44 | indexDir := filepath.Join("statedb", "couchdb")
45 | indexSrcDir := filepath.Join(src, MetadataDir, indexDir)
46 | indexDestDir := filepath.Join(dest, indexDir)
47 |
48 | logger.Debugf("Copying couchdb index files from %s to %s", indexSrcDir, indexDestDir)
49 |
50 | _, err := os.Lstat(indexSrcDir)
51 | if err != nil {
52 | if os.IsNotExist(err) {
53 | // indexes are optional
54 | return nil
55 | }
56 |
57 | return err
58 | }
59 |
60 | opt := copy.Options{
61 | Skip: func(info os.FileInfo, src, _ string) (bool, error) {
62 | logger.Debugf("Checking source copy path: %s", src)
63 | if info.IsDir() {
64 | skip, err := skipFolder(logger, indexSrcDir, src)
65 | if err != nil {
66 | return skip, fmt.Errorf(
67 | "error checking if the folder is eligible to have a couchdb index: %s, %s: %w",
68 | indexSrcDir,
69 | src,
70 | err,
71 | )
72 | }
73 |
74 | return skip, nil
75 | }
76 |
77 | skip, err := skipFile(logger, indexSrcDir, src)
78 | if err != nil {
79 | return skip, fmt.Errorf(
80 | "error checking if the file is eligible to have a couchdb index: %s, %s: %w",
81 | indexSrcDir,
82 | src,
83 | err,
84 | )
85 | }
86 |
87 | return skip, nil
88 | },
89 | }
90 |
91 | if err := copy.Copy(indexSrcDir, indexDestDir, opt); err != nil {
92 | return fmt.Errorf(
93 | "failed to copy CouchDB index definitions from %s to %s: %w",
94 | indexSrcDir,
95 | indexDestDir,
96 | err,
97 | )
98 | }
99 |
100 | return nil
101 | }
102 |
103 | // CopyMetadataDir copies all chaincode metadata from source to destination directories.
104 | func CopyMetadataDir(logger *log.CmdLogger, src, dest string) error {
105 | metadataSrcDir := filepath.Join(src, MetadataDir)
106 | metadataDestDir := filepath.Join(dest, MetadataDir)
107 |
108 | logger.Debugf("Copying chaincode metadata from %s to %s", metadataSrcDir, metadataDestDir)
109 |
110 | fileInfo, err := os.Lstat(metadataSrcDir)
111 | if err != nil {
112 | if os.IsNotExist(err) {
113 | // metadata is optional
114 | return nil
115 | }
116 |
117 | return err
118 | }
119 |
120 | if !fileInfo.IsDir() {
121 | return fmt.Errorf("chaincode metadata path %s is not a directory: %w", metadataSrcDir, err)
122 | }
123 |
124 | if err := copy.Copy(metadataSrcDir, metadataDestDir); err != nil {
125 | return fmt.Errorf(
126 | "failed to copy chaincode metadata from %s to %s: %w",
127 | metadataSrcDir,
128 | metadataDestDir,
129 | err,
130 | )
131 | }
132 |
133 | return nil
134 | }
135 |
136 | // skipFile checks if the file will need to be skipped during indexes copy.
137 | func skipFile(logger *log.CmdLogger, indexSrcDir, src string) (bool, error) {
138 | path, err := filepath.Rel(indexSrcDir, src)
139 | if err != nil {
140 | logger.Debugf("error verifying relative path from: %s, src: %s", indexSrcDir, src)
141 |
142 | return true, fmt.Errorf(
143 | "error verifying relative path from %s to %s: %w",
144 | indexSrcDir,
145 | src,
146 | err,
147 | )
148 | }
149 |
150 | if len(strings.Split(path, string(filepath.Separator))) == 1 { // JSON is in root couchdb folder
151 | logger.Debugf("The JSON file in the root couchdb index folder, should skip: %s, src: %s", path, src)
152 |
153 | return true, nil
154 | }
155 |
156 | if strings.HasSuffix(src, ".json") {
157 | logger.Debugf("The JSON file is valid, should copy: %s, src: %s", path, src)
158 |
159 | return false, nil
160 | }
161 |
162 | logger.Debugf("The JSON file is invalid, should skip: %s, src: %s", path, src)
163 |
164 | return true, nil
165 | }
166 |
167 | // skipFolder checks if the folder will need to be skipped during indexes copy.
168 | func skipFolder(logger *log.CmdLogger, indexSrcDir, src string) (bool, error) {
169 | path, err := filepath.Rel(indexSrcDir, src)
170 | if err != nil {
171 | logger.Debugf("failed resolve relative path: %s, src: %s", indexSrcDir, src)
172 |
173 | return true, fmt.Errorf("failed resolve relative path %s to %s: %w", indexSrcDir, src, err)
174 | }
175 |
176 | matchContainsPublicIndexFolder, _ := filepath.Match("indexes", path)
177 | matchContainsPrivateDataCollectionFolder, _ := filepath.Match("collections", path)
178 | matchPrivateDataCollectionFolder, _ := filepath.Match("collections/*", path)
179 | matchPrivateDataCollectionIndexFolder, _ := filepath.Match("collections/*/indexes", path)
180 | relativeFoldersLength := len(strings.Split(path, string(filepath.Separator)))
181 |
182 | logger.Debugf("Calculated relative path: %s. Total relative folders: %d", path, relativeFoldersLength)
183 | logger.Debugf("Match pattern 'index': %t", matchContainsPublicIndexFolder)
184 | logger.Debugf("Match pattern 'collections': %t", matchContainsPrivateDataCollectionFolder)
185 | logger.Debugf("Match pattern 'collections/*': %t", matchPrivateDataCollectionFolder)
186 | logger.Debugf("Match pattern 'collections/*/indexes': %t", matchPrivateDataCollectionIndexFolder)
187 |
188 | switch {
189 | case relativeFoldersLength == 1 && (!matchContainsPublicIndexFolder && !matchContainsPrivateDataCollectionFolder):
190 | logger.Debugf("Should skip folder")
191 |
192 | return true, nil
193 |
194 | case relativeFoldersLength == 2 && (!matchPrivateDataCollectionFolder):
195 | logger.Debugf("Should skip folder")
196 |
197 | return true, nil
198 |
199 | case relativeFoldersLength == 3 && (!matchPrivateDataCollectionIndexFolder):
200 | logger.Debugf("Should skip folder")
201 |
202 | return true, nil
203 |
204 | default:
205 | logger.Debugf("Should not skip folder")
206 |
207 | return false, nil
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/cmd/run/main_test.go:
--------------------------------------------------------------------------------
1 | package main_test
2 |
3 | import (
4 | "os"
5 | "os/exec"
6 |
7 | . "github.com/onsi/ginkgo/v2"
8 | . "github.com/onsi/gomega"
9 | "github.com/onsi/gomega/gbytes"
10 | "github.com/onsi/gomega/gexec"
11 | )
12 |
13 | var _ = Describe("Main", func() {
14 | It("should return an error if the CORE_PEER_ID environment variable is not set", func() {
15 | args := []string{"BUILD_OUTPUT_DIR", "RUN_METADATA_DIR"}
16 | command := exec.Command(runCmdPath, args...)
17 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
18 | Expect(err).NotTo(HaveOccurred())
19 |
20 | Eventually(session).Should(gexec.Exit(1))
21 | Eventually(
22 | session.Err,
23 | ).Should(gbytes.Say(`run \[\d+\]: Expected CORE_PEER_ID environment variable`))
24 | })
25 |
26 | DescribeTable("Running the run command with the wrong arguments produces the correct error",
27 | func(args ...string) {
28 | command := exec.Command(runCmdPath, args...)
29 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
30 | Expect(err).NotTo(HaveOccurred())
31 |
32 | Eventually(session).Should(gexec.Exit(1))
33 | Eventually(
34 | session.Err,
35 | ).Should(gbytes.Say(`run \[\d+\]: Expected BUILD_OUTPUT_DIR and RUN_METADATA_DIR arguments`))
36 | },
37 | Entry("When too few arguments are provided", "BUILD_OUTPUT_DIR"),
38 | Entry(
39 | "When too many arguments are provided",
40 | "BUILD_OUTPUT_DIR",
41 | "RUN_METADATA_DIR",
42 | "UNEXPECTED_ARGUMENT",
43 | ),
44 | )
45 |
46 | DescribeTable("Running the run command produces the correct error for invalid FABRIC_K8S_BUILDER_NODE_ROLE environment variable values",
47 | func(kubeNodeRoleValue, expectedErrorMessage string) {
48 | args := []string{"BUILD_OUTPUT_DIR", "RUN_METADATA_DIR"}
49 | command := exec.Command(runCmdPath, args...)
50 | command.Env = append(os.Environ(),
51 | "CORE_PEER_ID=core-peer-id-abcdefghijklmnopqrstuvwxyz-0123456789",
52 | "FABRIC_K8S_BUILDER_NODE_ROLE="+kubeNodeRoleValue,
53 | )
54 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
55 | Expect(err).NotTo(HaveOccurred())
56 |
57 | Eventually(session).Should(gexec.Exit(1))
58 | Eventually(
59 | session.Err,
60 | ).Should(gbytes.Say(expectedErrorMessage))
61 | },
62 | Entry("When the FABRIC_K8S_BUILDER_NODE_ROLE is too long", "long-node-role-is-looooooooooooooooooooooooooooooooooooooooooong", `run \[\d+\]: The FABRIC_K8S_BUILDER_NODE_ROLE environment variable must be a valid Kubernetes label value: must be no more than 63 characters`),
63 | Entry("When the FABRIC_K8S_BUILDER_NODE_ROLE contains invalid characters", "invalid*value", `run \[\d+\]: The FABRIC_K8S_BUILDER_NODE_ROLE environment variable must be a valid Kubernetes label value: a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '\.', and must start and end with an alphanumeric character`),
64 | Entry("When the FABRIC_K8S_BUILDER_NODE_ROLE does not start with an alphanumeric character", ".role", `run \[\d+\]: The FABRIC_K8S_BUILDER_NODE_ROLE environment variable must be a valid Kubernetes label value: a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '\.', and must start and end with an alphanumeric character`),
65 | Entry("When the FABRIC_K8S_BUILDER_NODE_ROLE does not end with an alphanumeric character", "role-", `run \[\d+\]: The FABRIC_K8S_BUILDER_NODE_ROLE environment variable must be a valid Kubernetes label value: a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '\.', and must start and end with an alphanumeric character`),
66 | )
67 |
68 | DescribeTable("Running the run command produces the correct error for invalid FABRIC_K8S_BUILDER_OBJECT_NAME_PREFIX environment variable values",
69 | func(kubeNamePrefixValue, expectedErrorMessage string) {
70 | args := []string{"BUILD_OUTPUT_DIR", "RUN_METADATA_DIR"}
71 | command := exec.Command(runCmdPath, args...)
72 | command.Env = append(os.Environ(),
73 | "CORE_PEER_ID=core-peer-id-abcdefghijklmnopqrstuvwxyz-0123456789",
74 | "FABRIC_K8S_BUILDER_OBJECT_NAME_PREFIX="+kubeNamePrefixValue,
75 | )
76 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
77 | Expect(err).NotTo(HaveOccurred())
78 |
79 | Eventually(session).Should(gexec.Exit(1))
80 | Eventually(
81 | session.Err,
82 | ).Should(gbytes.Say(expectedErrorMessage))
83 | },
84 | Entry("When the FABRIC_K8S_BUILDER_OBJECT_NAME_PREFIX is too long", "long-prefix-is-looooooooooooooooooooong", `run \[\d+\]: The FABRIC_K8S_BUILDER_OBJECT_NAME_PREFIX environment variable must be a maximum of 30 characters`),
85 | Entry("When the FABRIC_K8S_BUILDER_OBJECT_NAME_PREFIX contains invalid characters", "invalid/PREFIX*", `run \[\d+\]: The FABRIC_K8S_BUILDER_OBJECT_NAME_PREFIX environment variable must be a valid DNS-1035 label: a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character`),
86 | Entry("When the FABRIC_K8S_BUILDER_OBJECT_NAME_PREFIX starts with a number", "1prefix", `run \[\d+\]: The FABRIC_K8S_BUILDER_OBJECT_NAME_PREFIX environment variable must be a valid DNS-1035 label: a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character`),
87 | Entry("When the FABRIC_K8S_BUILDER_OBJECT_NAME_PREFIX starts with a dash", "-prefix", `run \[\d+\]: The FABRIC_K8S_BUILDER_OBJECT_NAME_PREFIX environment variable must be a valid DNS-1035 label: a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character`),
88 | )
89 |
90 | DescribeTable("Running the run command produces the correct error for invalid FABRIC_K8S_BUILDER_START_TIMEOUT environment variable values",
91 | func(chaincodeStartTimeoutValue, expectedErrorMessage string) {
92 | args := []string{"BUILD_OUTPUT_DIR", "RUN_METADATA_DIR"}
93 | command := exec.Command(runCmdPath, args...)
94 | command.Env = append(os.Environ(),
95 | "CORE_PEER_ID=core-peer-id-abcdefghijklmnopqrstuvwxyz-0123456789",
96 | "FABRIC_K8S_BUILDER_START_TIMEOUT="+chaincodeStartTimeoutValue,
97 | )
98 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
99 | Expect(err).NotTo(HaveOccurred())
100 |
101 | Eventually(session).Should(gexec.Exit(1))
102 | Eventually(
103 | session.Err,
104 | ).Should(gbytes.Say(expectedErrorMessage))
105 | },
106 | Entry("When the FABRIC_K8S_BUILDER_START_TIMEOUT is missing a duration unit", "3", `run \[\d+\]: The FABRIC_K8S_BUILDER_START_TIMEOUT environment variable must be a valid Go duration string, e\.g\. 3m40s: time: missing unit in duration "3"`),
107 | Entry("When the FABRIC_K8S_BUILDER_START_TIMEOUT is not a valid duration string", "three minutes", `run \[\d+\]: The FABRIC_K8S_BUILDER_START_TIMEOUT environment variable must be a valid Go duration string, e\.g\. 3m40s: time: invalid duration "three minutes"`),
108 | )
109 | })
110 |
--------------------------------------------------------------------------------
/internal/util/k8s_test.go:
--------------------------------------------------------------------------------
1 | package util_test
2 |
3 | import (
4 | "github.com/hyperledger-labs/fabric-builder-k8s/internal/util"
5 | . "github.com/onsi/ginkgo/v2"
6 | . "github.com/onsi/gomega"
7 | )
8 |
9 | var _ = Describe("K8s", func() {
10 | Describe("GetValidRfc1035LabelName", func() {
11 | It("should return names with a maximum of 63 characters", func() {
12 | chaincodeData := &util.ChaincodeJSON{
13 | ChaincodeID: "fabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcar:cffa266294278404e5071cb91150d550dc0bf855149908a170b1169d6160004b",
14 | PeerAddress: "peer0.org1.example.com",
15 | MspID: "CongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaOrgMsp",
16 | }
17 | name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "CongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaOrgPeer0", chaincodeData, 0)
18 | Expect(len(name)).To(Equal(63))
19 | })
20 |
21 | It("should return names with a maximum of 57 characters if a 6 character suffix is required", func() {
22 | chaincodeData := &util.ChaincodeJSON{
23 | ChaincodeID: "fabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcar:cffa266294278404e5071cb91150d550dc0bf855149908a170b1169d6160004b",
24 | PeerAddress: "peer0.org1.example.com",
25 | MspID: "CongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaOrgMsp",
26 | }
27 | name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "CongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaOrgPeer0", chaincodeData, 6)
28 | Expect(len(name)).To(Equal(57))
29 | })
30 |
31 | It("should return names which starts with an alphabetic character", func() {
32 | chaincodeData := &util.ChaincodeJSON{
33 | ChaincodeID: "fabcar:cffa266294278404e5071cb91150d550dc0bf855149908a170b1169d6160004b",
34 | PeerAddress: "peer0.org1.example.com",
35 | MspID: "GreenCongaOrg",
36 | }
37 | name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "GreenCongaOrgPeer0", chaincodeData, 0)
38 | Expect(name).To(MatchRegexp("^[a-z]"))
39 | })
40 |
41 | It("should return names which end with an alphanumeric character", func() {
42 | chaincodeData := &util.ChaincodeJSON{
43 | ChaincodeID: "fabcar:cffa266294278404e5071cb91150d550dc0bf855149908a170b1169d6160004b",
44 | PeerAddress: "peer0.org1.example.com",
45 | MspID: "BlueCongaOrg",
46 | }
47 | name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "BlueCongaOrgPeer0", chaincodeData, 0)
48 | Expect(name).To(MatchRegexp("[a-z0-9]$"))
49 | })
50 |
51 | It("should return names which only contains lowercase alphanumeric characters or '-'", func() {
52 | chaincodeData := &util.ChaincodeJSON{
53 | ChaincodeID: "FAB/CAR*:cffa266294278404e5071cb91150d550dc0bf855149908a170b1169d6160004b",
54 | PeerAddress: "peer0.org1.example.com",
55 | MspID: "BlueCongaOrg",
56 | }
57 | name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "BlueCongaOrgPeer0", chaincodeData, 0)
58 | Expect(name).To(MatchRegexp("^(?:[a-z0-9]|-)+$"))
59 | })
60 |
61 | It("should return different names for the same package IDs", func() {
62 | chaincodeData1 := &util.ChaincodeJSON{
63 | ChaincodeID: "fabcar:cffa266294278404e5071cb91150d550dc0bf855149908a170b1169d6160004b",
64 | PeerAddress: "peer0.org1.example.com",
65 | MspID: "GreenCongaOrg",
66 | }
67 | chaincodeData2 := &util.ChaincodeJSON{
68 | ChaincodeID: "fabcar:cffa266294278404e5071cb91150d550dc0bf855149908a170b1169d6160004b",
69 | PeerAddress: "peer0.org2.example.org",
70 | MspID: "BlueCongaOrg",
71 | }
72 | name1 := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "GreenCongaOrgPeer0", chaincodeData1, 0)
73 | name2 := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "BlueCongaOrgPeer0", chaincodeData2, 0)
74 | Expect(name1).NotTo(Equal(name2))
75 | })
76 |
77 | It("should return different names for different package IDs", func() {
78 | chaincodeData1 := &util.ChaincodeJSON{
79 | ChaincodeID: "fabcar:cffa266294278404e5071cb91150d550dc0bf855149908a170b1169d6160004b",
80 | PeerAddress: "peer0.org1.example.com",
81 | MspID: "RedCongaOrg",
82 | }
83 | chaincodeData2 := &util.ChaincodeJSON{
84 | ChaincodeID: "go-contract:6f98c4bb29414771312eddd1a813eef583df2121c235c4797792f141a46d4b45",
85 | PeerAddress: "peer0.org1.example.com",
86 | MspID: "RedCongaOrg",
87 | }
88 | name1 := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "RedCongaOrg", chaincodeData1, 0)
89 | name2 := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "RedCongaOrg", chaincodeData2, 0)
90 | Expect(name1).NotTo(Equal(name2))
91 | })
92 |
93 | It("should return deterministic names", func() {
94 | chaincodeData := &util.ChaincodeJSON{
95 | ChaincodeID: "fabcar:cffa266294278404e5071cb91150d550dc0bf855149908a170b1169d6160004b",
96 | PeerAddress: "peer0.org1.example.com",
97 | MspID: "CongaOrg",
98 | }
99 | name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "CongaOrgPeer0", chaincodeData, 0)
100 | Expect(name).To(Equal("hlf-k8sbuilder-ftw-fabcar-s6pwkq6bepi2e"))
101 | })
102 |
103 | It("should return names which start with the specified prefix and a safe version of the chaincode label", func() {
104 | chaincodeData := &util.ChaincodeJSON{
105 | ChaincodeID: "FAB/CAR*:cffa266294278404e5071cb91150d550dc0bf855149908a170b1169d6160004b",
106 | PeerAddress: "peer0.org1.example.com",
107 | MspID: "CongaOrg",
108 | }
109 | name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "CongaOrgPeer0", chaincodeData, 0)
110 | Expect(name).To(HavePrefix("hlf-k8sbuilder-ftw" + "-fabcar-"))
111 | })
112 |
113 | It("should return names which end with a 13 character lowercase base32 encoded hash string", func() {
114 | chaincodeData := &util.ChaincodeJSON{
115 | ChaincodeID: "fabcar:cffa266294278404e5071cb91150d550dc0bf855149908a170b1169d6160004b",
116 | PeerAddress: "peer0.org1.example.com",
117 | MspID: "CongaOrg",
118 | }
119 | name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "CongaOrgPeer0", chaincodeData, 0)
120 | Expect(name).To(MatchRegexp("-[a-z2-7]{13}$"))
121 | })
122 |
123 | It("should return names with the full prefix and hash, and a truncated chaincode label", func() {
124 | chaincodeData := &util.ChaincodeJSON{
125 | ChaincodeID: "fabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcarfabfabfabfabcar:cffa266294278404e5071cb91150d550dc0bf855149908a170b1169d6160004b",
126 | PeerAddress: "peer0.org1.example.com",
127 | MspID: "CongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaOrgMsp",
128 | }
129 | name := util.GetValidRfc1035LabelName("hlf-k8sbuilder-ftw", "CongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaCongaOrgPeer0", chaincodeData, 0)
130 | Expect(name).To(Equal("hlf-k8sbuilder-ftw-fabfabfabfabcarfabfabfabfabcar-b46p74k4ygwh6"))
131 | })
132 | })
133 | })
134 |
--------------------------------------------------------------------------------
/test/testscript_helpers.go:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: Apache-2.0
2 |
3 | package test
4 |
5 | import (
6 | "encoding/base32"
7 | "encoding/hex"
8 | "fmt"
9 | "os"
10 | "strconv"
11 | "testing"
12 | "time"
13 |
14 | "github.com/rogpeppe/go-internal/testscript"
15 | batchv1 "k8s.io/api/batch/v1"
16 | v1 "k8s.io/api/core/v1"
17 | "k8s.io/apimachinery/pkg/labels"
18 | "sigs.k8s.io/e2e-framework/klient/k8s/resources"
19 | "sigs.k8s.io/e2e-framework/klient/wait"
20 | "sigs.k8s.io/e2e-framework/klient/wait/conditions"
21 | "sigs.k8s.io/e2e-framework/pkg/env"
22 | "sigs.k8s.io/e2e-framework/pkg/envconf"
23 | )
24 |
25 | const (
26 | waitInterval = 2 * time.Second
27 | shortWaitTimeout = 30 * time.Second
28 | longWaitTimeout = 5 * time.Minute
29 | jobinfoArgs = 2
30 | podinfoArgs = 2
31 | )
32 |
33 | type WrappedM struct {
34 | m *testing.M
35 | testenv env.Environment
36 | }
37 |
38 | func (w WrappedM) Run() int {
39 | return w.testenv.Run(w.m)
40 | }
41 |
42 | func NewWrappedM(m *testing.M, testenv env.Environment) WrappedM {
43 | return WrappedM{
44 | m: m,
45 | testenv: testenv,
46 | }
47 | }
48 |
49 | func getChaincodePackageLabels(script *testscript.TestScript, args []string) (string, string) {
50 | cclabel := args[0]
51 | cchash := args[1]
52 |
53 | packageHashBytes, err := hex.DecodeString(cchash)
54 | if err != nil {
55 | script.Fatalf("error decoding chaincode package hash %v: %v", cchash, err)
56 | }
57 |
58 | encodedPackageHash := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(packageHashBytes)
59 |
60 | return cclabel, encodedPackageHash
61 | }
62 |
63 | func getConfig(script *testscript.TestScript) *envconf.Config {
64 | testenv, ok := script.Value("testenv").(env.Environment)
65 | if !ok {
66 | script.Logf("could not get testenv")
67 | }
68 |
69 | cfg := testenv.EnvConf()
70 |
71 | return cfg
72 | }
73 |
74 | func waitForChaincodeJob(script *testscript.TestScript, cfg *envconf.Config, cclabel string, cchash string) *batchv1.Job {
75 | script.Logf("Waiting for job to be created for chaincode %s", cclabel)
76 |
77 | jobs := &batchv1.JobList{}
78 |
79 | err := wait.For(conditions.New(cfg.Client().Resources()).ResourceListN(jobs, 1,
80 | resources.WithLabelSelector(
81 | labels.FormatLabels(map[string]string{
82 | "fabric-builder-k8s-cclabel": cclabel,
83 | "fabric-builder-k8s-cchash": cchash,
84 | }))), wait.WithInterval(waitInterval), wait.WithTimeout(shortWaitTimeout))
85 | if err != nil {
86 | script.Fatalf("failed waiting for job to be created for chaincode %s: %v", cclabel, err)
87 | }
88 |
89 | job := &jobs.Items[0]
90 |
91 | return job
92 | }
93 |
94 | func waitForChaincodePod(script *testscript.TestScript, cfg *envconf.Config, jobname string) *v1.Pod {
95 | script.Logf("Waiting for pod to be created for chaincode job %s", jobname)
96 |
97 | pods := &v1.PodList{}
98 |
99 | err := wait.For(conditions.New(cfg.Client().Resources()).ResourceListN(pods, 1,
100 | resources.WithLabelSelector(
101 | labels.FormatLabels(map[string]string{
102 | "batch.kubernetes.io/job-name": jobname,
103 | }))), wait.WithInterval(waitInterval), wait.WithTimeout(shortWaitTimeout))
104 | if err != nil {
105 | script.Fatalf("failed waiting for pod to be created for chaincode job %s: %v", jobname, err)
106 | }
107 |
108 | pod := &pods.Items[0]
109 | podname := pod.GetName()
110 |
111 | script.Logf("Waiting for pod %s to start", podname)
112 |
113 | err = wait.For(conditions.New(cfg.Client().Resources()).PodReady(pod), wait.WithInterval(waitInterval), wait.WithTimeout(longWaitTimeout))
114 | if err != nil {
115 | script.Fatalf("failed to wait for chaincode pod %s to reach Ready condition: %v", podname, err)
116 | }
117 |
118 | return pod
119 | }
120 |
121 | func jobInfoCmd(script *testscript.TestScript, _ bool, args []string) {
122 | if len(args) != jobinfoArgs {
123 | script.Fatalf("usage: jobinfo chaincode_label chaincode_hash")
124 | }
125 |
126 | cclabel, cchash := getChaincodePackageLabels(script, args)
127 |
128 | cfg := getConfig(script)
129 |
130 | job := waitForChaincodeJob(script, cfg, cclabel, cchash)
131 |
132 | var err error
133 | _, err = script.Stdout().Write(fmt.Appendf(nil, "Job name: %s\n", job.GetName()))
134 | script.Check(err)
135 |
136 | _, err = script.Stdout().Write(fmt.Appendf(nil, "Job namespace: %s\n", job.GetNamespace()))
137 | script.Check(err)
138 |
139 | for k, v := range job.GetLabels() {
140 | _, err = script.Stdout().Write(fmt.Appendf(nil, "Job label: %s=%s\n", k, v))
141 | script.Check(err)
142 | }
143 |
144 | for k, v := range job.GetAnnotations() {
145 | _, err = script.Stdout().Write(fmt.Appendf(nil, "Job annotation: %s=%s\n", k, v))
146 | script.Check(err)
147 | }
148 | }
149 |
150 | func podInfoCmd(script *testscript.TestScript, _ bool, args []string) {
151 | if len(args) != podinfoArgs {
152 | script.Fatalf("usage: podinfo chaincode_label chaincode_hash")
153 | }
154 |
155 | cclabel, cchash := getChaincodePackageLabels(script, args)
156 |
157 | cfg := getConfig(script)
158 |
159 | job := waitForChaincodeJob(script, cfg, cclabel, cchash)
160 | jobname := job.GetName()
161 |
162 | pod := waitForChaincodePod(script, cfg, jobname)
163 | podname := pod.GetName()
164 |
165 | var err error
166 | _, err = script.Stdout().Write(fmt.Appendf(nil, "Pod name: %s\n", podname))
167 | script.Check(err)
168 |
169 | _, err = script.Stdout().Write(fmt.Appendf(nil, "Pod namespace: %s\n", pod.GetNamespace()))
170 | script.Check(err)
171 |
172 | _, err = script.Stdout().Write(fmt.Appendf(nil, "Pod service account: %s\n", pod.Spec.ServiceAccountName))
173 | script.Check(err)
174 |
175 | for k, v := range pod.GetLabels() {
176 | _, err = script.Stdout().Write(fmt.Appendf(nil, "Pod label: %s=%s\n", k, v))
177 | script.Check(err)
178 | }
179 |
180 | for k, v := range pod.GetAnnotations() {
181 | _, err = script.Stdout().Write(fmt.Appendf(nil, "Pod annotation: %s=%s\n", k, v))
182 | script.Check(err)
183 | }
184 |
185 | if pod.Spec.Affinity != nil && pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution != nil {
186 | for _, t := range pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms {
187 | for _, e := range t.MatchExpressions {
188 | _, err = script.Stdout().Write(fmt.Appendf(nil, "Pod affinity: %v=%v op=%v\n", e.Key, e.Values, e.Operator))
189 | script.Check(err)
190 | }
191 | }
192 | }
193 |
194 | for _, t := range pod.Spec.Tolerations {
195 | _, err = script.Stdout().Write(fmt.Appendf(nil, "Pod toleration: %v=%v:%v op=%v for %vs\n", t.Key, t.Value, t.Effect, t.Operator, t.TolerationSeconds))
196 | script.Check(err)
197 | }
198 | }
199 |
200 | func setupTestscriptEnv(t *testing.T, tsenv *testscript.Env, testenv env.Environment) error {
201 | t.Helper()
202 |
203 | tsenv.Setenv("KUBECONFIG_PATH", testenv.EnvConf().KubeconfigFile())
204 | tsenv.Setenv("TESTENV_NAMESPACE", testenv.EnvConf().Namespace())
205 | tsenv.Values["testenv"] = testenv
206 |
207 | return nil
208 | }
209 |
210 | func NewTestscriptParams(t *testing.T, scriptfile string, testenv env.Environment) testscript.Params {
211 | t.Helper()
212 |
213 | keepWorkDirs, _ := strconv.ParseBool(os.Getenv("KEEP_TESTSCRIPT_DIRS"))
214 |
215 | params := testscript.Params{
216 | Files: []string{scriptfile},
217 | RequireExplicitExec: true,
218 | Setup: func(e *testscript.Env) error { return setupTestscriptEnv(t, e, testenv) },
219 | TestWork: keepWorkDirs,
220 | Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){
221 | "jobinfo": jobInfoCmd,
222 | "podinfo": podInfoCmd,
223 | },
224 | }
225 |
226 | return params
227 | }
228 |
--------------------------------------------------------------------------------
/samples/java-contract/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
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 | # https://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 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/docs/tutorials/develop-chaincode.md:
--------------------------------------------------------------------------------
1 | # Developing and debugging chaincode
2 |
3 | Publishing a chaincode Docker image and using the image digest to deploy the chaincode works well with the [Fabric chaincode lifecycle](https://hyperledger-fabric.readthedocs.io/en/latest/chaincode_lifecycle.html), however it is not as convenient while developing and debugging chaincode.
4 |
5 | This tutorial describes how to debug chaincode using the chaincode-as-a-service (CCAAS) builder before you are ready to deploy it in a production environment with the k8s builder.
6 |
7 | With CCAAS you still have to go through the chaincode lifecycle once to tell Fabric where the chaincode is running but after that you can stop, update, restart, and debug the chaincode all without needing to repeat the chaincode lifecycle steps as you would do normally.
8 |
9 | Like the k8s builder, CCAAS packages do not contain any chaincode source code.
10 | Unlike the k8s builder, CCAAS packages do not uniquely identify any specific chaincode: they only contain details a Fabric peer needs to connect to a chaincode instance running in server mode.
11 | This is unlikely to be acceptable in a production environment but it is ideal in a development environment.
12 |
13 | First create a directory to download all the required files and run the demo.
14 |
15 | ```shell
16 | mkdir hlf-debug-demo
17 | cd hlf-debug-demo
18 | ```
19 |
20 | Now follow the steps below to debug your first smart contract using the CCAAS builder!
21 |
22 | ## Setup the nano test network
23 |
24 | In this tutorial, we'll use the [fabric-samples](https://github.com/hyperledger/fabric-samples/) nano test network because it's ideally suited to a light weight development environment.
25 | There is also a [test-network CCAAS tutorial](https://github.com/hyperledger/fabric-samples/blob/main/test-network/CHAINCODE_AS_A_SERVICE_TUTORIAL.md) if you would prefer to use the Docker based test network.
26 |
27 | Start by downloading the sample nano test network (fabric-samples isn't tagged so we'll use a known good commit).
28 |
29 | ```shell
30 | export FABRIC_SAMPLES_COMMIT=0db64487e5e89a81d68e6871af3f0907c67e7d75
31 | curl -sSL "https://github.com/hyperledger/fabric-samples/archive/${FABRIC_SAMPLES_COMMIT}.tar.gz" | tar -xzf - --strip-components=1 fabric-samples-${FABRIC_SAMPLES_COMMIT}/test-network-nano-bash
32 | ```
33 |
34 | You will also need to install the Fabric binaries, which include the CCAAS builder and default configuration files.
35 |
36 | ```shell
37 | curl -sSLO https://raw.githubusercontent.com/hyperledger/fabric/main/scripts/install-fabric.sh && chmod +x install-fabric.sh
38 | ./install-fabric.sh binary
39 | ```
40 |
41 | The default `core.yaml` file provided with the Fabric binaries needs to be updated with the correct CCAAS builder location, since it assumes that the peer will be running in a docker container. Either update the `ccaas_builder` in `externalBuilders` with the absolute path to the `builders/ccaas` directory using your preferred editor, or use the following [yq](https://github.com/mikefarah/yq) command to update the path.
42 |
43 | ```shell
44 | yq -i '( .chaincode.externalBuilders[] | select(.name == "ccaas_builder") | .path ) = strenv(PWD) + "/builders/ccaas"' config/core.yaml
45 | ```
46 |
47 | Now start the nano test network!
48 |
49 | ```shell
50 | cd test-network-nano-bash
51 | ./network.sh start
52 | ```
53 |
54 | ## Deploy a CCAAS chaincode package
55 |
56 | Open a new shell, change to the `hlf-debug-demo/test-network-nano-bash` directory, and check that the nano test network is running.
57 |
58 | ```shell
59 | . ./peer1admin.sh
60 | peer channel list
61 | ```
62 |
63 | Sourcing the `peer1admin.sh` script sets up the environment for running `peer` commands.
64 | You'll need to repeat this step in any new shell you want to run `peer` commands in, for example to deploy chaincode or run transactions.
65 |
66 | Before deploying a CCAAS chaincode package, we need to create a suitable chaincode package.
67 | Start by creating a `connection.json` file.
68 | You'll need to use the same `address` when starting the chaincode later.
69 |
70 | ```shell
71 | cat << CONNECTIONJSON_EOF > connection.json
72 | {
73 | "address": "127.0.0.1:9999",
74 | "dial_timeout": "10s",
75 | "tls_required": false
76 | }
77 | CONNECTIONJSON_EOF
78 | ```
79 |
80 | Next, create a `metadata.json` file for the chaincode package.
81 | The CCAAS builder provided with Fabric will detect the type of `ccaas`.
82 |
83 | ```shell
84 | cat << METADATAJSON_EOF > metadata.json
85 | {
86 | "type": "ccaas",
87 | "label": "dev-contract"
88 | }
89 | METADATAJSON_EOF
90 | ```
91 |
92 | Create the chaincode package archive.
93 |
94 | ```shell
95 | tar -czf code.tar.gz connection.json
96 | tar -czf dev-contract.tgz metadata.json code.tar.gz
97 | ```
98 |
99 | Now follow the Fabric chaincode lifecycle to deploy the CCAAS chaincode.
100 | Start by installing the chaincode package you just created.
101 |
102 | ```shell
103 | peer lifecycle chaincode install dev-contract.tgz
104 | ```
105 |
106 | Set the CHAINCODE_ID environment variable for use in subsequent commands.
107 |
108 | ```shell
109 | export CHAINCODE_ID=$(peer lifecycle chaincode calculatepackageid dev-contract.tgz) && echo $CHAINCODE_ID
110 | ```
111 |
112 | Approve and commit the chaincode.
113 |
114 | ```shell
115 | peer lifecycle chaincode approveformyorg -o 127.0.0.1:6050 --channelID mychannel --name dev-contract --version 1 --package-id $CHAINCODE_ID --sequence 1 --tls --cafile "${PWD}"/crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/tls/ca.crt
116 | peer lifecycle chaincode commit -o 127.0.0.1:6050 --channelID mychannel --name dev-contract --version 1 --sequence 1 --tls --cafile "${PWD}"/crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/tls/ca.crt
117 | ```
118 |
119 | ## Debugging the chaincode
120 |
121 | Normally Fabric peers start chaincode automatically but with CCAAS you are responsible for starting the chaincode in server mode manually, which is why it is so useful for debugging.
122 |
123 | Importantly, chaincode can be started in server mode without any code changes in recent Fabric versions, making it easy to move from CCAAS in a development environment to using the k8s builder in a production environment.
124 |
125 | Go and Java chaincode will start in server mode if `CHAINCODE_SERVER_ADDRESS` and `CORE_CHAINCODE_ID_NAME` environment variables are configured.
126 | The `CHAINCODE_SERVER_ADDRESS` must match the `address` field from the `connection.json` file in the chaincode package.
127 | The `CORE_CHAINCODE_ID_NAME` can be found using the `peer lifecycle chaincode calculatepackageid` command.
128 |
129 | Node.js chaincode can be started in server mode using the `server` command and providing `--chaincode-address` and `--chaincode-id` command line arguments.
130 | The sample Node.js contract includes a `debug` script in `package.json` which uses the same `CHAINCODE_SERVER_ADDRESS` and `CORE_CHAINCODE_ID_NAME` environment variables for these command line arguments as Go and Java chaincode.
131 |
132 | For example, use the following commands to export the required environment variables.
133 |
134 | ```shell
135 | export CHAINCODE_SERVER_ADDRESS=127.0.0.1:9999
136 | export CORE_CHAINCODE_ID_NAME=$(peer lifecycle chaincode calculatepackageid dev-contract.tgz)
137 | ```
138 |
139 | The following commands can then be used to start each sample in server mode without a debugger attached.
140 |
141 | In the `samples/go-contract` directory:
142 |
143 | ```shell
144 | CORE_PEER_TLS_ENABLED=false go run main.go
145 | ```
146 |
147 | In the `samples/java-contract` directory:
148 |
149 | ```shell
150 | ./gradlew jar
151 | CORE_PEER_TLS_ENABLED=false java -jar ./build/libs/sample-contract.jar
152 | ```
153 |
154 | In the `samples/node-contract` directory:
155 |
156 | ```shell
157 | npm install
158 | npm run compile
159 | npm run debug
160 | ```
161 |
162 | Change to the `hlf-debug-demo/test-network-nano-bash` directory in a new shell, and check everything is working using the `GetMetadata` transaction.
163 |
164 | ```shell
165 | . ./peer1admin.sh
166 | peer chaincode query -C mychannel -n dev-contract -c '{"Args":["org.hyperledger.fabric:GetMetadata"]}'
167 | ```
168 |
169 | Now it's time to attach a debugger, set breakpoints and start running transactions!
170 | Exactly how you debug chaincode will depend on your preferred debugger.
171 | For example, [VS Code includes a built-in debugger for Node.js](https://code.visualstudio.com/docs/nodejs/nodejs-debugging).
172 |
173 | Try adding breakpoints to the transactions defined in the samples and then invoke the `PutValue` transaction using the following command.
174 |
175 | ```shell
176 | peer chaincode invoke -o 127.0.0.1:6050 -C mychannel -n dev-contract -c '{"Args":["PutValue","asset1","green"]}' --waitForEvent --tls --cafile "${PWD}"/crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/tls/ca.crt
177 | ```
178 |
179 | Query the `GetValue` transaction using the following command.
180 |
181 | ```shell
182 | peer chaincode query -C mychannel -n dev-contract -c '{"Args":["GetValue","asset1"]}'
183 | ```
184 |
185 | ## Next steps
186 |
187 | Take a look at the [Fabric Full Stack Development Workshop](https://github.com/hyperledger/fabric-samples/blob/main/full-stack-asset-transfer-guide/README.md) for an in-depth introduction to the entire Fabric development process, from smart contract and client application development, to cloud native deployment.
188 |
--------------------------------------------------------------------------------