Skip to content

Commit b41d85c

Browse files
committed
[WIP] Add support for dedicated chaincode nodes
See #228 Signed-off-by: James Taylor <[email protected]>
1 parent 0dff17f commit b41d85c

File tree

11 files changed

+188
-32
lines changed

11 files changed

+188
-32
lines changed

cmd/run.go

Lines changed: 98 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,75 +5,141 @@ package cmd
55
import (
66
"context"
77
"os"
8-
"strconv"
98

109
"github.com/hyperledger-labs/fabric-builder-k8s/internal/builder"
1110
"github.com/hyperledger-labs/fabric-builder-k8s/internal/log"
1211
"github.com/hyperledger-labs/fabric-builder-k8s/internal/util"
13-
"k8s.io/apimachinery/pkg/api/validation"
12+
apivalidation "k8s.io/apimachinery/pkg/api/validation"
13+
"k8s.io/apimachinery/pkg/util/validation"
1414
)
1515

16-
func Run() int {
17-
const (
18-
expectedArgsLength = 3
19-
buildOutputDirectoryArg = 1
20-
runMetadataDirectoryArg = 2
21-
maximumKubeNamePrefixLength = 30
22-
)
23-
24-
debug, _ := strconv.ParseBool(util.GetOptionalEnv(util.DebugVariable, "false"))
25-
ctx := log.NewCmdContext(context.Background(), debug)
26-
logger := log.New(ctx)
27-
28-
if len(os.Args) != expectedArgsLength {
29-
logger.Println("Expected BUILD_OUTPUT_DIR and RUN_METADATA_DIR arguments")
30-
31-
return 1
32-
}
33-
34-
buildOutputDirectory := os.Args[buildOutputDirectoryArg]
35-
runMetadataDirectory := os.Args[runMetadataDirectoryArg]
36-
37-
logger.Debugf("Build output directory: %s", buildOutputDirectory)
38-
logger.Debugf("Run metadata directory: %s", runMetadataDirectory)
39-
16+
//nolint:nonamedreturns // using the ok bool convention to indicate errors
17+
func getPeerID(logger *log.CmdLogger) (peerID string, ok bool) {
4018
peerID, err := util.GetRequiredEnv(util.PeerIDVariable)
4119
if err != nil {
4220
logger.Printf("Expected %s environment variable\n", util.PeerIDVariable)
4321

44-
return 1
22+
return peerID, false
4523
}
4624

4725
logger.Debugf("%s=%s", util.PeerIDVariable, peerID)
4826

27+
return peerID, true
28+
}
29+
30+
func getKubeconfigPath(logger *log.CmdLogger) string {
4931
kubeconfigPath := util.GetOptionalEnv(util.KubeconfigPathVariable, "")
5032
logger.Debugf("%s=%s", util.KubeconfigPathVariable, kubeconfigPath)
5133

34+
return kubeconfigPath
35+
}
36+
37+
func getKubeNamespace(logger *log.CmdLogger) string {
5238
kubeNamespace := util.GetOptionalEnv(util.ChaincodeNamespaceVariable, "")
5339
logger.Debugf("%s=%s", util.ChaincodeNamespaceVariable, kubeNamespace)
5440

5541
if kubeNamespace == "" {
42+
var err error
43+
5644
kubeNamespace, err = util.GetKubeNamespace()
5745
if err != nil {
46+
logger.Debugf("Error getting namespace: %+v\n", util.DefaultNamespace, err)
5847
kubeNamespace = util.DefaultNamespace
5948
}
49+
50+
logger.Debugf("Using default namespace: %s\n", util.DefaultNamespace)
6051
}
6152

53+
return kubeNamespace
54+
}
55+
56+
//nolint:nonamedreturns // using the ok bool convention to indicate errors
57+
func getKubeNodeRole(logger *log.CmdLogger) (kubeNodeRole string, ok bool) {
58+
kubeNodeRole = util.GetOptionalEnv(util.ChaincodeNodeRoleVariable, "")
59+
logger.Debugf("%s=%s", util.ChaincodeNodeRoleVariable, kubeNodeRole)
60+
61+
// TODO: are valid taint values the same?!
62+
if msgs := validation.IsValidLabelValue(kubeNodeRole); len(msgs) > 0 {
63+
logger.Printf("The %s environment variable must be a valid Kubernetes label value: %s", util.ChaincodeNodeRoleVariable, msgs[0])
64+
65+
return kubeNodeRole, false
66+
}
67+
68+
return kubeNodeRole, true
69+
}
70+
71+
func getKubeServiceAccount(logger *log.CmdLogger) string {
6272
kubeServiceAccount := util.GetOptionalEnv(util.ChaincodeServiceAccountVariable, util.DefaultServiceAccountName)
6373
logger.Debugf("%s=%s", util.ChaincodeServiceAccountVariable, kubeServiceAccount)
6474

65-
kubeNamePrefix := util.GetOptionalEnv(util.ObjectNamePrefixVariable, util.DefaultObjectNamePrefix)
75+
return kubeServiceAccount
76+
}
77+
78+
//nolint:nonamedreturns // using the ok bool convention to indicate errors
79+
func getKubeNamePrefix(logger *log.CmdLogger) (kubeNamePrefix string, ok bool) {
80+
const maximumKubeNamePrefixLength = 30
81+
82+
kubeNamePrefix = util.GetOptionalEnv(util.ObjectNamePrefixVariable, util.DefaultObjectNamePrefix)
6683
logger.Debugf("%s=%s", util.ObjectNamePrefixVariable, kubeNamePrefix)
6784

6885
if len(kubeNamePrefix) > maximumKubeNamePrefixLength {
69-
logger.Printf("The FABRIC_K8S_BUILDER_OBJECT_NAME_PREFIX environment variable must be a maximum of 30 characters")
86+
logger.Printf("The %s environment variable must be a maximum of 30 characters", util.ObjectNamePrefixVariable)
87+
88+
return kubeNamePrefix, false
89+
}
90+
91+
if msgs := apivalidation.NameIsDNS1035Label(kubeNamePrefix, true); len(msgs) > 0 {
92+
logger.Printf("The %s environment variable must be a valid DNS-1035 label: %s", util.ObjectNamePrefixVariable, msgs[0])
93+
94+
return kubeNamePrefix, false
95+
}
96+
97+
return kubeNamePrefix, true
98+
}
99+
100+
func Run() int {
101+
const (
102+
expectedArgsLength = 3
103+
buildOutputDirectoryArg = 1
104+
runMetadataDirectoryArg = 2
105+
)
106+
107+
debug := util.GetOptionalEnv(util.DebugVariable, "false")
108+
ctx := log.NewCmdContext(context.Background(), debug == "true")
109+
logger := log.New(ctx)
110+
111+
if len(os.Args) != expectedArgsLength {
112+
logger.Println("Expected BUILD_OUTPUT_DIR and RUN_METADATA_DIR arguments")
113+
114+
return 1
115+
}
116+
117+
buildOutputDirectory := os.Args[buildOutputDirectoryArg]
118+
runMetadataDirectory := os.Args[runMetadataDirectoryArg]
119+
120+
logger.Debugf("Build output directory: %s", buildOutputDirectory)
121+
logger.Debugf("Run metadata directory: %s", runMetadataDirectory)
122+
123+
//nolint:varnamelen // using the ok bool convention to indicate errors
124+
var ok bool
125+
126+
peerID, ok := getPeerID(logger)
127+
if !ok {
128+
return 1
129+
}
130+
131+
kubeconfigPath := getKubeconfigPath(logger)
132+
kubeNamespace := getKubeNamespace(logger)
70133

134+
kubeNodeRole, ok := getKubeNodeRole(logger)
135+
if !ok {
71136
return 1
72137
}
73138

74-
if msgs := validation.NameIsDNS1035Label(kubeNamePrefix, true); len(msgs) > 0 {
75-
logger.Printf("The FABRIC_K8S_BUILDER_OBJECT_NAME_PREFIX environment variable must be a valid DNS-1035 label: %s", msgs[0])
139+
kubeServiceAccount := getKubeServiceAccount(logger)
76140

141+
kubeNamePrefix, ok := getKubeNamePrefix(logger)
142+
if !ok {
77143
return 1
78144
}
79145

@@ -83,6 +149,7 @@ func Run() int {
83149
PeerID: peerID,
84150
KubeconfigPath: kubeconfigPath,
85151
KubeNamespace: kubeNamespace,
152+
KubeNodeRole: kubeNodeRole,
86153
KubeServiceAccount: kubeServiceAccount,
87154
KubeNamePrefix: kubeNamePrefix,
88155
}

cmd/run/main_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,28 @@ var _ = Describe("Main", func() {
4343
),
4444
)
4545

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+
4668
DescribeTable("Running the run command produces the correct error for invalid FABRIC_K8S_BUILDER_OBJECT_NAME_PREFIX environment variable values",
4769
func(kubeNamePrefixValue, expectedErrorMessage string) {
4870
args := []string{"BUILD_OUTPUT_DIR", "RUN_METADATA_DIR"}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Dedicated nodes
2+
3+
TBC
4+
5+
The `FABRIC_K8S_BUILDER_NODE_ROLE` environment variable can be used to...
6+
7+
For example, if `FABRIC_K8S_BUILDER_NODE_ROLE` is set to `chaincode`, ... using the following command.
8+
9+
```shell
10+
kubectl label nodes node1 fabric-builder-k8s-role=chaincode
11+
kubectl taint nodes node1 fabric-builder-k8s-role=chaincode:NoSchedule
12+
```
13+
14+
More complex requirements should be handled with Dynamic Admission Control using a Mutating Webhook.
15+
For example, it looks like the namespace-node-affinity webhook could be used to assign node affinity and tolerations to all pods in the FABRIC_K8S_BUILDER_NAMESPACE namespace.

docs/configuring/overview.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ External builders are configured in the `core.yaml` file, for example:
1414
- CORE_PEER_ID
1515
- FABRIC_K8S_BUILDER_DEBUG
1616
- FABRIC_K8S_BUILDER_NAMESPACE
17+
- FABRIC_K8S_BUILDER_NODE_ROLE
1718
- FABRIC_K8S_BUILDER_OBJECT_NAME_PREFIX
1819
- FABRIC_K8S_BUILDER_SERVICE_ACCOUNT
1920
- KUBERNETES_SERVICE_HOST
@@ -30,6 +31,7 @@ The k8s builder is configured using the following environment variables.
3031
| ------------------------------------- | -------------------------------- | ---------------------------------------------------- |
3132
| CORE_PEER_ID | | The Fabric peer ID (required) |
3233
| FABRIC_K8S_BUILDER_NAMESPACE | The peer namespace or `default` | The Kubernetes namespace to run chaincode with |
34+
| FABRIC_K8S_BUILDER_NODE_ROLE | | TBC |
3335
| FABRIC_K8S_BUILDER_OBJECT_NAME_PREFIX | `hlfcc` | Eye-catcher prefix for Kubernetes object names |
3436
| FABRIC_K8S_BUILDER_SERVICE_ACCOUNT | `default` | The Kubernetes service account to run chaincode with |
3537
| FABRIC_K8S_BUILDER_DEBUG | `false` | Set to `true` to enable k8s builder debug messages |

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ require (
4949
k8s.io/component-base v0.31.1 // indirect
5050
mvdan.cc/sh/v3 v3.7.0 // indirect
5151
sigs.k8s.io/controller-runtime v0.19.0 // indirect
52+
sigs.k8s.io/e2e-framework v0.5.0 // indirect
5253
)
5354

5455
require (

internal/builder/run.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type Run struct {
1616
PeerID string
1717
KubeconfigPath string
1818
KubeNamespace string
19+
KubeNodeRole string
1920
KubeServiceAccount string
2021
KubeNamePrefix string
2122
}
@@ -73,6 +74,7 @@ func (r *Run) Run(ctx context.Context) error {
7374
kubeObjectName,
7475
r.KubeNamespace,
7576
r.KubeServiceAccount,
77+
r.KubeNodeRole,
7678
r.PeerID,
7779
chaincodeData,
7880
imageData,

internal/util/env.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
const (
1111
builderVariablePrefix = "FABRIC_K8S_BUILDER_"
1212
ChaincodeNamespaceVariable = builderVariablePrefix + "NAMESPACE"
13+
ChaincodeNodeRoleVariable = builderVariablePrefix + "NODE_ROLE"
1314
ObjectNamePrefixVariable = builderVariablePrefix + "OBJECT_NAME_PREFIX"
1415
ChaincodeServiceAccountVariable = builderVariablePrefix + "SERVICE_ACCOUNT"
1516
DebugVariable = builderVariablePrefix + "DEBUG"

internal/util/k8s.go

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ func CreateChaincodeJob(
365365
ctx context.Context,
366366
logger *log.CmdLogger,
367367
jobsClient typedBatchv1.JobInterface,
368-
objectName, namespace, serviceAccount, peerID string,
368+
objectName, namespace, serviceAccount, nodeRole, peerID string,
369369
chaincodeData *ChaincodeJSON,
370370
imageData *ImageJSON,
371371
) (*batchv1.Job, error) {
@@ -381,6 +381,41 @@ func CreateChaincodeJob(
381381
return nil, fmt.Errorf("error getting chaincode job definition for chaincode ID %s: %w", chaincodeData.ChaincodeID, err)
382382
}
383383

384+
if nodeRole != "" {
385+
logger.Debugf(
386+
"Adding node affinity and toleration to job definition for chaincode ID %s: %s",
387+
chaincodeData.ChaincodeID,
388+
nodeRole,
389+
)
390+
391+
jobDefinition.Spec.Template.Spec.Affinity = &apiv1.Affinity{
392+
NodeAffinity: &apiv1.NodeAffinity{
393+
RequiredDuringSchedulingIgnoredDuringExecution: &apiv1.NodeSelector{
394+
NodeSelectorTerms: []apiv1.NodeSelectorTerm{
395+
{
396+
MatchExpressions: []apiv1.NodeSelectorRequirement{
397+
{
398+
Key: "fabric-builder-k8s-role",
399+
Operator: apiv1.NodeSelectorOpIn,
400+
Values: []string{nodeRole},
401+
},
402+
},
403+
},
404+
},
405+
},
406+
},
407+
}
408+
409+
jobDefinition.Spec.Template.Spec.Tolerations = []apiv1.Toleration{
410+
{
411+
Key: "fabric-builder-k8s-role",
412+
Operator: apiv1.TolerationOpEqual,
413+
Value: nodeRole,
414+
Effect: apiv1.TaintEffectNoSchedule,
415+
},
416+
}
417+
}
418+
384419
jobName := jobDefinition.ObjectMeta.Name
385420

386421
logger.Debugf(

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ nav:
106106
- Kubernetes permissions: configuring/kubernetes-permissions.md
107107
- Kubernetes namespace: configuring/kubernetes-namespace.md
108108
- Kubernetes service account: configuring/kubernetes-service-account.md
109+
- Dedicated nodes: configuring/dedicated-nodes.md
109110
- Tutorials:
110111
- Developing and debuging chaincode: tutorials/develop-chaincode.md
111112
- Creating a chaincode package: tutorials/package-chaincode.md

test/integration/main_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@ func TestMain(m *testing.M) {
3030

3131
testenv.Setup(
3232
envfuncs.CreateCluster(kind.NewProvider(), clusterName),
33+
// envfuncs.CreateClusterWithConfig(kind.NewProvider(), clusterName, "testdata/kind-config.yaml", kind.WithImage("kindest/node:v1.22.2")),
3334
envfuncs.CreateNamespace(envCfg.Namespace()),
3435
)
3536

3637
testenv.Finish(
3738
envfuncs.DeleteNamespace(envCfg.Namespace()),
39+
// envfuncs.ExportClusterLogs(kindClusterName, "./logs"),
3840
envfuncs.DestroyCluster(clusterName),
3941
)
4042

0 commit comments

Comments
 (0)