Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions docs/configuring/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ externalBuilders:
- FABRIC_K8S_BUILDER_OBJECT_NAME_PREFIX
- FABRIC_K8S_BUILDER_SERVICE_ACCOUNT
- FABRIC_K8S_BUILDER_START_TIMEOUT
- FABRIC_K8S_BUILDER_NAME_SERVERS
- FABRIC_K8S_BUILDER_CUSTOM_ANNOTATIONS
- KUBERNETES_SERVICE_HOST
- KUBERNETES_SERVICE_PORT
```
Expand All @@ -41,14 +43,16 @@ For more information, see [Configuring external builders and launchers](https://

The k8s builder is configured using the following environment variables.

| Name | Default | Description |
| ------------------------------------- | -------------------------------- | ---------------------------------------------------- |
| CORE_PEER_ID | | The Fabric peer ID (required) |
| FABRIC_K8S_BUILDER_NAMESPACE | The peer namespace or `default` | The Kubernetes namespace to run chaincode with |
| FABRIC_K8S_BUILDER_NODE_ROLE | | Use dedicated Kubernetes nodes to run chaincode |
| FABRIC_K8S_BUILDER_OBJECT_NAME_PREFIX | `hlfcc` | Eye-catcher prefix for Kubernetes object names |
| FABRIC_K8S_BUILDER_SERVICE_ACCOUNT | `default` | The Kubernetes service account to run chaincode with |
| FABRIC_K8S_BUILDER_START_TIMEOUT | `3m` | The timeout when waiting for chaincode pods to start |
| FABRIC_K8S_BUILDER_DEBUG | `false` | Set to `true` to enable k8s builder debug messages |
| Name | Default | Description |
| ---------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------- |
| CORE_PEER_ID | | The Fabric peer ID (required) |
| FABRIC_K8S_BUILDER_NAMESPACE | The peer namespace or `default` | The Kubernetes namespace to run chaincode with |
| FABRIC_K8S_BUILDER_NODE_ROLE | | Use dedicated Kubernetes nodes to run chaincode |
| FABRIC_K8S_BUILDER_OBJECT_NAME_PREFIX | `hlfcc` | Eye-catcher prefix for Kubernetes object names |
| FABRIC_K8S_BUILDER_SERVICE_ACCOUNT | `default` | The Kubernetes service account to run chaincode with |
| FABRIC_K8S_BUILDER_START_TIMEOUT | `3m` | The timeout when waiting for chaincode pods to start |
| FABRIC_K8S_BUILDER_NAME_SERVERS | | Custom DNS nameserver IP for chaincode pods (optional, enables custom DNS) |
| FABRIC_K8S_BUILDER_CUSTOM_ANNOTATIONS | | Custom annotations for chaincode pods (optional, comma-separated key=value pairs)|
| FABRIC_K8S_BUILDER_DEBUG | `false` | Set to `true` to enable k8s builder debug messages |

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.
4 changes: 4 additions & 0 deletions internal/builder/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ type Run struct {
KubeServiceAccount string
KubeNamePrefix string
ChaincodeStartTimeout time.Duration
NameServers string
CustomAnnotations map[string]string
}

func (r *Run) Run(ctx context.Context) error {
Expand Down Expand Up @@ -78,6 +80,8 @@ func (r *Run) Run(ctx context.Context) error {
r.KubeServiceAccount,
r.KubeNodeRole,
r.PeerID,
r.NameServers,
r.CustomAnnotations,
chaincodeData,
imageData,
)
Expand Down
24 changes: 24 additions & 0 deletions internal/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,25 @@ func getChaincodeStartTimeout(logger *log.CmdLogger) (chaincodeStartTimeoutDurat
return chaincodeStartTimeoutDuration, true
}

func getNameServers(logger *log.CmdLogger) string {
nameServers := util.GetOptionalEnv(util.NameServersVariable, "")
logger.Debugf("%s=%s", util.NameServersVariable, nameServers)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be some validation for the nameserver variable. I think it's something like up to 3 IP addresses but I don't know for sure/the details.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes it can be 3 servers
nameservers: a list of IP addresses that will be used as DNS servers for the Pod. There can be at most 3 IP addresses specified.
but it's admin responsibility to identify the servers , we can add validation / Warning about length
do you agree ?


return nameServers
}

func getCustomAnnotations(logger *log.CmdLogger) map[string]string {
annotationsStr := util.GetOptionalEnv(util.CustomAnnotationsVariable, "")
logger.Debugf("%s=%s", util.CustomAnnotationsVariable, annotationsStr)

annotations := util.ParseAnnotations(annotationsStr)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be some validation for the annotations somewhere. The builder does label validation for example.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what type of validation we can add for this annotation ?

if len(annotations) > 0 {
logger.Debugf("Parsed custom annotations: %v", annotations)
}

return annotations
}

func Run() {
const (
expectedArgsLength = 3
Expand Down Expand Up @@ -164,6 +183,9 @@ func Run() {
os.Exit(1)
}

nameServers := getNameServers(logger)
customAnnotations := getCustomAnnotations(logger)

run := &builder.Run{
BuildOutputDirectory: buildOutputDirectory,
RunMetadataDirectory: runMetadataDirectory,
Expand All @@ -174,6 +196,8 @@ func Run() {
KubeServiceAccount: kubeServiceAccount,
KubeNamePrefix: kubeNamePrefix,
ChaincodeStartTimeout: chaincodeStartTimeout,
NameServers: nameServers,
CustomAnnotations: customAnnotations,
}

if err := run.Run(ctx); err != nil {
Expand Down
33 changes: 33 additions & 0 deletions internal/util/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package util
import (
"fmt"
"os"
"strings"
)

const (
Expand All @@ -14,6 +15,8 @@ const (
ObjectNamePrefixVariable = builderVariablePrefix + "OBJECT_NAME_PREFIX"
ChaincodeServiceAccountVariable = builderVariablePrefix + "SERVICE_ACCOUNT"
ChaincodeStartTimeoutVariable = builderVariablePrefix + "START_TIMEOUT"
NameServersVariable = builderVariablePrefix + "NAME_SERVERS"
CustomAnnotationsVariable = builderVariablePrefix + "CUSTOM_ANNOTATIONS"
DebugVariable = builderVariablePrefix + "DEBUG"
KubeconfigPathVariable = "KUBECONFIG_PATH"
PeerIDVariable = "CORE_PEER_ID"
Expand All @@ -34,3 +37,33 @@ func GetRequiredEnv(key string) (string, error) {

return "", fmt.Errorf("environment variable not set: %s", key)
}

// ParseAnnotations parses a comma-separated list of key=value pairs into a map.
// Example input: "sidecar.istio.io/inject=true,app=myapp"
// Returns empty map if input is empty or invalid entries are skipped.
func ParseAnnotations(annotationsStr string) map[string]string {
annotations := make(map[string]string)

if annotationsStr == "" {
return annotations
}

pairs := strings.Split(annotationsStr, ",")
for _, pair := range pairs {
pair = strings.TrimSpace(pair)
if pair == "" {
continue
}

parts := strings.SplitN(pair, "=", 2)
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
if key != "" {
annotations[key] = value
}
}
}

return annotations
}
20 changes: 17 additions & 3 deletions internal/util/k8s.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,8 @@ func getAnnotations(peerID string, chaincodeData *ChaincodeJSON) map[string]stri

func getChaincodeJobSpec(
imageData *ImageJSON,
namespace, serviceAccount, objectName, peerID string,
namespace, serviceAccount, objectName, peerID, nameServers string,
customAnnotations map[string]string,
chaincodeData *ChaincodeJSON,
) (*batchv1.Job, error) {
chaincodeImage := imageData.Name + "@" + imageData.Digest
Expand All @@ -281,6 +282,11 @@ func getChaincodeJobSpec(
}

annotations := getAnnotations(peerID, chaincodeData)

// Merge custom annotations if provided
for key, value := range customAnnotations {
annotations[key] = value
}

return &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Expand Down Expand Up @@ -429,7 +435,8 @@ func CreateChaincodeJob(
ctx context.Context,
logger *log.CmdLogger,
jobsClient typedBatchv1.JobInterface,
objectName, namespace, serviceAccount, nodeRole, peerID string,
objectName, namespace, serviceAccount, nodeRole, peerID, nameServers string,
customAnnotations map[string]string,
chaincodeData *ChaincodeJSON,
imageData *ImageJSON,
) (*batchv1.Job, error) {
Expand All @@ -439,6 +446,8 @@ func CreateChaincodeJob(
serviceAccount,
objectName,
peerID,
nameServers,
customAnnotations,
chaincodeData,
)
if err != nil {
Expand All @@ -451,7 +460,12 @@ func CreateChaincodeJob(
chaincodeData.ChaincodeID,
nodeRole,
)

if nameServers != "" {
jobDefinition.Spec.Template.Spec.DNSPolicy = apiv1.DNSNone
jobDefinition.Spec.Template.Spec.DNSConfig = &apiv1.PodDNSConfig{
Nameservers: []string{nameServers},
}
}
jobDefinition.Spec.Template.Spec.Affinity = &apiv1.Affinity{
NodeAffinity: &apiv1.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &apiv1.NodeSelector{
Expand Down
63 changes: 63 additions & 0 deletions internal/util/k8s_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,67 @@ var _ = Describe("K8s", func() {
Expect(name).To(Equal("hlf-k8sbuilder-ftw-fabfabfabfabcarfabfabfabfabcar-b46p74k4ygwh6"))
})
})

Describe("ParseAnnotations", func() {
It("should return empty map for empty string", func() {
result := util.ParseAnnotations("")
Expect(result).To(BeEmpty())
})

It("should parse single annotation", func() {
result := util.ParseAnnotations("sidecar.istio.io/inject=true")
Expect(result).To(HaveLen(1))
Expect(result["sidecar.istio.io/inject"]).To(Equal("true"))
})

It("should parse multiple annotations", func() {
result := util.ParseAnnotations("sidecar.istio.io/inject=true,app=myapp,version=1.0")
Expect(result).To(HaveLen(3))
Expect(result["sidecar.istio.io/inject"]).To(Equal("true"))
Expect(result["app"]).To(Equal("myapp"))
Expect(result["version"]).To(Equal("1.0"))
})

It("should handle annotations with spaces", func() {
result := util.ParseAnnotations(" sidecar.istio.io/inject = true , app = myapp ")
Expect(result).To(HaveLen(2))
Expect(result["sidecar.istio.io/inject"]).To(Equal("true"))
Expect(result["app"]).To(Equal("myapp"))
})

It("should skip invalid entries without equals sign", func() {
result := util.ParseAnnotations("sidecar.istio.io/inject=true,invalidentry,app=myapp")
Expect(result).To(HaveLen(2))
Expect(result["sidecar.istio.io/inject"]).To(Equal("true"))
Expect(result["app"]).To(Equal("myapp"))
})

It("should skip empty entries", func() {
result := util.ParseAnnotations("sidecar.istio.io/inject=true,,app=myapp")
Expect(result).To(HaveLen(2))
Expect(result["sidecar.istio.io/inject"]).To(Equal("true"))
Expect(result["app"]).To(Equal("myapp"))
})

It("should handle annotations with empty values", func() {
result := util.ParseAnnotations("sidecar.istio.io/inject=,app=myapp")
Expect(result).To(HaveLen(2))
Expect(result["sidecar.istio.io/inject"]).To(Equal(""))
Expect(result["app"]).To(Equal("myapp"))
})

It("should handle annotations with equals signs in values", func() {
result := util.ParseAnnotations("config=key=value,app=myapp")
Expect(result).To(HaveLen(2))
Expect(result["config"]).To(Equal("key=value"))
Expect(result["app"]).To(Equal("myapp"))
})

It("should skip entries with empty keys", func() {
result := util.ParseAnnotations("=value,app=myapp")
Expect(result).To(HaveLen(1))
Expect(result["app"]).To(Equal("myapp"))
})
})

})