├── .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 | Creative Commons License
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 | [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/9817/badge)](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 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 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 | --------------------------------------------------------------------------------