diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f14d8c59..b0a77477 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,19 +34,22 @@ jobs: CHIA_IMAGE_TAG=$(curl -fsSL https://latest.cmm.io/chia) EXPORTER_IMAGE_TAG=$(curl -fsSL https://latest.cmm.io/chia-exporter) HEALTHCHECK_IMAGE_TAG=$(curl -fsSL https://latest.cmm.io/chia-healthcheck) + DBPULL_IMAGE_TAG=$(curl -fsSL https://latest.cmm.io/chia-db-pull) # Verify all variables are set - if [ -z "$CHIA_IMAGE_TAG" ] || [ -z "$EXPORTER_IMAGE_TAG" ] || [ -z "$HEALTHCHECK_IMAGE_TAG" ]; then + if [ -z "$CHIA_IMAGE_TAG" ] || [ -z "$EXPORTER_IMAGE_TAG" ] || [ -z "$HEALTHCHECK_IMAGE_TAG" ] || [ -z "$DBPULL_IMAGE_TAG" ]; then echo "Error: Failed to fetch one or more version tags" echo "CHIA_IMAGE_TAG=$CHIA_IMAGE_TAG" echo "EXPORTER_IMAGE_TAG=$EXPORTER_IMAGE_TAG" echo "HEALTHCHECK_IMAGE_TAG=$HEALTHCHECK_IMAGE_TAG" + echo "DBPULL_IMAGE_TAG=$DBPULL_IMAGE_TAG" exit 1 fi echo "CHIA_IMAGE_TAG=$CHIA_IMAGE_TAG" >> $GITHUB_ENV echo "EXPORTER_IMAGE_TAG=$EXPORTER_IMAGE_TAG" >> $GITHUB_ENV echo "HEALTHCHECK_IMAGE_TAG=$HEALTHCHECK_IMAGE_TAG" >> $GITHUB_ENV + echo "DBPULL_IMAGE_TAG=$DBPULL_IMAGE_TAG" >> $GITHUB_ENV - name: Build Container uses: Chia-Network/actions/docker/build@main @@ -56,3 +59,4 @@ jobs: "CHIA_IMAGE_TAG=${{ env.CHIA_IMAGE_TAG }}" "EXPORTER_IMAGE_TAG=${{ env.EXPORTER_IMAGE_TAG }}" "HEALTHCHECK_IMAGE_TAG=${{ env.HEALTHCHECK_IMAGE_TAG }}" + "DBPULL_IMAGE_TAG=${{ env.DBPULL_IMAGE_TAG }}" diff --git a/.golangci.yml b/.golangci.yml index e2c303dc..43eed99b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -25,6 +25,10 @@ linters: - dupl - lll path: internal/* + # ChiaNode Reconcile function complexity has become too high + - linters: + - gocyclo + path: internal/controller/chianode/controller.go paths: - third_party$ - builtin$ diff --git a/Dockerfile b/Dockerfile index d3489ade..d357b1c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,10 +4,12 @@ FROM golang:1 AS builder ARG CHIA_IMAGE_TAG=latest ARG EXPORTER_IMAGE_TAG=latest ARG HEALTHCHECK_IMAGE_TAG=latest +ARG DBPULL_IMAGE_TAG=latest ENV CHIA_IMAGE_TAG=${CHIA_IMAGE_TAG} ENV EXPORTER_IMAGE_TAG=${EXPORTER_IMAGE_TAG} ENV HEALTHCHECK_IMAGE_TAG=${HEALTHCHECK_IMAGE_TAG} +ENV DBPULL_IMAGE_TAG=${DBPULL_IMAGE_TAG} WORKDIR /workspace # Copy the Go Modules manifests diff --git a/Makefile b/Makefile index 2dd60d44..09f693ac 100644 --- a/Makefile +++ b/Makefile @@ -7,12 +7,14 @@ ENVTEST_K8S_VERSION = 1.30.0 CHIA_IMAGE_TAG ?= latest EXPORTER_IMAGE_TAG ?= latest HEALTHCHECK_IMAGE_TAG ?= latest +DBPULL_IMAGE_TAG ?= latest CHIA_OPERATOR_VERSION ?= latest LD_FLAGS := \ -X 'github.com/chia-network/chia-operator/internal/controller/common/consts.DefaultChiaImageTag=$(CHIA_IMAGE_TAG)' \ -X 'github.com/chia-network/chia-operator/internal/controller/common/consts.DefaultChiaExporterImageTag=$(EXPORTER_IMAGE_TAG)' \ - -X 'github.com/chia-network/chia-operator/internal/controller/common/consts.DefaultChiaHealthcheckImageTag=$(HEALTHCHECK_IMAGE_TAG)' + -X 'github.com/chia-network/chia-operator/internal/controller/common/consts.DefaultChiaHealthcheckImageTag=$(HEALTHCHECK_IMAGE_TAG)' \ + -X 'github.com/chia-network/chia-operator/internal/controller/common/consts.DefaultChiaDBPullImageTag=$(DBPULL_IMAGE_TAG)' # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) diff --git a/api/v1/chianode_types.go b/api/v1/chianode_types.go index 36b71afd..abac8f90 100644 --- a/api/v1/chianode_types.go +++ b/api/v1/chianode_types.go @@ -6,6 +6,7 @@ package v1 import ( appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -20,6 +21,12 @@ type ChiaNodeSpec struct { // +optional ChiaHealthcheckConfig SpecChiaHealthcheck `json:"chiaHealthcheck,omitempty"` + // ChiaDBPullConfig defines the configuration options available to an optional chia-db-pull init container + // that downloads a chia blockchain database from an S3-compatible bucket into CHIA_ROOT before the chia + // container starts. + // +optional + ChiaDBPullConfig SpecChiaDBPull `json:"chiaDBPull,omitempty"` + // Replicas is the desired number of replicas of the given Statefulset. defaults to 1. // +optional // +kubebuilder:default=1 @@ -30,6 +37,55 @@ type ChiaNodeSpec struct { UpdateStrategy *appsv1.StatefulSetUpdateStrategy `json:"updateStrategy,omitempty"` } +// SpecChiaDBPull defines the desired state of an optional chia-db-pull init container +type SpecChiaDBPull struct { + // Enabled defines whether a chia-db-pull init container should run before the chia container. + // Defaults to false. + // +optional + Enabled *bool `json:"enabled,omitempty"` + + // Image defines the image to use for the chia-db-pull init container + // +optional + Image *string `json:"image,omitempty"` + + // S3Prefix is the S3 URI prefix the chia-db-pull container will download the database from. + // Required when Enabled is true. Mapped to the S3_PREFIX env var. + // +optional + S3Prefix string `json:"s3Prefix,omitempty"` + + // Network is the chia network name the database belongs to. Mapped to the NETWORK env var. + // If unset, the operator derives the value from the surrounding chia config: it first looks + // for a "network" key in the ChiaNetwork ConfigMap referenced by spec.chia.chiaNetwork, then + // falls back to spec.chia.network. If neither is set, no NETWORK env var is emitted and + // chia-db-pull will use its own default (mainnet). + // +optional + Network *string `json:"network,omitempty"` + + // MinHeight is the minimum block height the downloaded database should be at. Mapped to the MIN_HEIGHT env var. + // +optional + MinHeight *int64 `json:"minHeight,omitempty"` + + // AWSCredentialsSecret is the name of a kubernetes Secret in the same namespace whose keys will be loaded + // into the chia-db-pull container as environment variables via envFrom. + // Use this to inject AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (or any other credentials) without putting them in plaintext. + // +optional + AWSCredentialsSecret *string `json:"awsCredentialsSecret,omitempty"` + + // AdditionalEnv contain a list of additional environment variables to be supplied to the chia-db-pull container. + // These variables will be placed at the end of the environment variable list in the resulting container, + // this means they overwrite variables of the same name created by the operator in the container env. + // +optional + AdditionalEnv *[]corev1.EnvVar `json:"additionalEnv,omitempty"` + + // Resources defines the compute resources (limits/requests) for the chia-db-pull container. + // +optional + Resources *corev1.ResourceRequirements `json:"resources,omitempty"` + + // SecurityContext defines the security context for the chia-db-pull container + // +optional + SecurityContext *corev1.SecurityContext `json:"securityContext,omitempty"` +} + // ChiaNodeSpecChia defines the desired state of Chia component configuration type ChiaNodeSpecChia struct { CommonSpecChia `json:",inline"` diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index f938e3c7..6dc5cd11 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -1036,6 +1036,7 @@ func (in *ChiaNodeSpec) DeepCopyInto(out *ChiaNodeSpec) { in.CommonSpec.DeepCopyInto(&out.CommonSpec) in.ChiaConfig.DeepCopyInto(&out.ChiaConfig) in.ChiaHealthcheckConfig.DeepCopyInto(&out.ChiaHealthcheckConfig) + in.ChiaDBPullConfig.DeepCopyInto(&out.ChiaDBPullConfig) if in.UpdateStrategy != nil { in, out := &in.UpdateStrategy, &out.UpdateStrategy *out = new(appsv1.StatefulSetUpdateStrategy) @@ -2229,6 +2230,67 @@ func (in *Service) DeepCopy() *Service { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SpecChiaDBPull) DeepCopyInto(out *SpecChiaDBPull) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.Image != nil { + in, out := &in.Image, &out.Image + *out = new(string) + **out = **in + } + if in.Network != nil { + in, out := &in.Network, &out.Network + *out = new(string) + **out = **in + } + if in.MinHeight != nil { + in, out := &in.MinHeight, &out.MinHeight + *out = new(int64) + **out = **in + } + if in.AWSCredentialsSecret != nil { + in, out := &in.AWSCredentialsSecret, &out.AWSCredentialsSecret + *out = new(string) + **out = **in + } + if in.AdditionalEnv != nil { + in, out := &in.AdditionalEnv, &out.AdditionalEnv + *out = new([]corev1.EnvVar) + if **in != nil { + in, out := *in, *out + *out = make([]corev1.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = new(corev1.ResourceRequirements) + (*in).DeepCopyInto(*out) + } + if in.SecurityContext != nil { + in, out := &in.SecurityContext, &out.SecurityContext + *out = new(corev1.SecurityContext) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SpecChiaDBPull. +func (in *SpecChiaDBPull) DeepCopy() *SpecChiaDBPull { + if in == nil { + return nil + } + out := new(SpecChiaDBPull) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SpecChiaExporter) DeepCopyInto(out *SpecChiaExporter) { *out = *in diff --git a/config/crd/bases/k8s.chia.net_chianodes.yaml b/config/crd/bases/k8s.chia.net_chianodes.yaml index d047ce5f..25473844 100644 --- a/config/crd/bases/k8s.chia.net_chianodes.yaml +++ b/config/crd/bases/k8s.chia.net_chianodes.yaml @@ -2197,6 +2197,460 @@ spec: required: - caSecretName type: object + chiaDBPull: + description: |- + ChiaDBPullConfig defines the configuration options available to an optional chia-db-pull init container + that downloads a chia blockchain database from an S3-compatible bucket into CHIA_ROOT before the chia + container starts. + properties: + additionalEnv: + description: |- + AdditionalEnv contain a list of additional environment variables to be supplied to the chia-db-pull container. + These variables will be placed at the end of the environment variable list in the resulting container, + this means they overwrite variables of the same name created by the operator in the container env. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + awsCredentialsSecret: + description: |- + AWSCredentialsSecret is the name of a kubernetes Secret in the same namespace whose keys will be loaded + into the chia-db-pull container as environment variables via envFrom. + Use this to inject AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY (or any other credentials) without putting them in plaintext. + type: string + enabled: + description: |- + Enabled defines whether a chia-db-pull init container should run before the chia container. + Defaults to false. + type: boolean + image: + description: Image defines the image to use for the chia-db-pull + init container + type: string + minHeight: + description: MinHeight is the minimum block height the downloaded + database should be at. Mapped to the MIN_HEIGHT env var. + format: int64 + type: integer + network: + description: |- + Network is the chia network name the database belongs to. Mapped to the NETWORK env var. + If unset, the operator derives the value from the surrounding chia config: it first looks + for a "network" key in the ChiaNetwork ConfigMap referenced by spec.chia.chiaNetwork, then + falls back to spec.chia.network. If neither is set, no NETWORK env var is emitted and + chia-db-pull will use its own default (mainnet). + type: string + resources: + description: Resources defines the compute resources (limits/requests) + for the chia-db-pull container. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + s3Prefix: + description: |- + S3Prefix is the S3 URI prefix the chia-db-pull container will download the database from. + Required when Enabled is true. Mapped to the S3_PREFIX env var. + type: string + securityContext: + description: SecurityContext defines the security context for + the chia-db-pull container + properties: + allowPrivilegeEscalation: + description: |- + AllowPrivilegeEscalation controls whether a process can gain more + privileges than its parent process. This bool directly controls if + the no_new_privs flag will be set on the container process. + AllowPrivilegeEscalation is true always when the container is: + 1) run as Privileged + 2) has CAP_SYS_ADMIN + Note that this field cannot be set when spec.os.name is windows. + type: boolean + appArmorProfile: + description: |- + appArmorProfile is the AppArmor options to use by this container. If set, this profile + overrides the pod's appArmorProfile. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile loaded on the node that should be used. + The profile must be preconfigured on the node to work. + Must match the loaded name of the profile. + Must be set if and only if type is "Localhost". + type: string + type: + description: |- + type indicates which kind of AppArmor profile will be applied. + Valid options are: + Localhost - a profile pre-loaded on the node. + RuntimeDefault - the container runtime's default profile. + Unconfined - no AppArmor enforcement. + type: string + required: + - type + type: object + capabilities: + description: |- + The capabilities to add/drop when running containers. + Defaults to the default set of capabilities granted by the container runtime. + Note that this field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + x-kubernetes-list-type: atomic + type: object + privileged: + description: |- + Run container in privileged mode. + Processes in privileged containers are essentially equivalent to root on the host. + Defaults to false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + procMount: + description: |- + procMount denotes the type of proc mount to use for the containers. + The default value is Default which uses the container runtime defaults for + readonly paths and masked paths. + This requires the ProcMountType feature flag to be enabled. + Note that this field cannot be set when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: |- + Whether this container has a read-only root filesystem. + Default is false. + Note that this field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: |- + The GID to run the entrypoint of the container process. + Uses runtime default if unset. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: |- + Indicates that the container must run as a non-root user. + If true, the Kubelet will validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start the container if it does. + If unset or false, no such validation will be performed. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: |- + The UID to run the entrypoint of the container process. + Defaults to user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: |- + The SELinux context to be applied to the container. + If unspecified, the container runtime will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: |- + The seccomp options to use by this container. If seccomp options are + provided at both the pod & container level, the container options + override the pod options. + Note that this field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: |- + localhostProfile indicates a profile defined in a file on the node should be used. + The profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's configured seccomp profile location. + Must be set if type is "Localhost". Must NOT be set for any other type. + type: string + type: + description: |- + type indicates which kind of seccomp profile will be applied. + Valid options are: + + Localhost - a profile defined in a file on the node should be used. + RuntimeDefault - the container runtime default profile should be used. + Unconfined - no profile should be applied. + type: string + required: + - type + type: object + windowsOptions: + description: |- + The Windows specific settings applied to all containers. + If unspecified, the options from the PodSecurityContext will be used. + If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: |- + GMSACredentialSpec is where the GMSA admission webhook + (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the + GMSA credential spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of the + GMSA credential spec to use. + type: string + hostProcess: + description: |- + HostProcess determines if a container should be run as a 'Host Process' container. + All of a Pod's containers must have the same effective HostProcess value + (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). + In addition, if HostProcess is true then HostNetwork must also be set to true. + type: boolean + runAsUserName: + description: |- + The UserName in Windows to run the entrypoint of the container process. + Defaults to the user specified in image metadata if unspecified. + May also be set in PodSecurityContext. If set in both SecurityContext and + PodSecurityContext, the value specified in SecurityContext takes precedence. + type: string + type: object + type: object + type: object chiaExporter: description: ChiaExporterConfig defines the configuration options available to Chia component containers diff --git a/docs/chianode.md b/docs/chianode.md index 453e1f90..259014d2 100644 --- a/docs/chianode.md +++ b/docs/chianode.md @@ -87,6 +87,51 @@ spec: This specifies two trusted CIDRs, where if the IP address of a full_node peer is discovered to be within one of these two CIDR ranges, chia will consider that a trusted peer. +## chia-db-pull init container + +ChiaNode supports an optional first-class `chia-db-pull` init container that downloads a chia blockchain database from an S3-compatible bucket into `CHIA_ROOT` before the chia container starts. This can dramatically reduce sync time for fresh nodes. + +A minimal example: + +```yaml +spec: + chia: + network: "testnet11" # NETWORK is auto-derived from this + chiaDBPull: + enabled: true + s3Prefix: "s3://chia-blockchain-sqlite-backups/testnet11/" +``` + +The S3 bucket + path specified by `chiaDBPull.s3Prefix` must contain a `blockchain_v2_${NETWORK}.sqlite` file. If one is not found within the `s3Prefix` the init container will fail and the node won't start. The `height-to-hash` and `sub-epoch-summaries` cache files may also optionally be picked up by this init container, the init container won't fail if they're missing, however. + +The network variable is only required by chia-db-pull if the target network is a testnet. If the target network is a testnet, the operator derives the network variable the same way the chia container's network variable is derived. In order of precedence the network variable is either pulled from `spec.chiaDBPull.network`, a ChiaNetwork specified in `spec.chia.chiaNetwork`, or `spec.chia.network`. If none are set, no network variable is emitted and chia-db-pull falls back to its own default (mainnet). When using `spec.chia.testnet: true` you must configure one of the methods for setting the network variable so chia-db-pull knows which testnet to pull. + +A more full example using a Secret for AWS credentials and a min-height threshold: + +```yaml +spec: + chia: + network: "testnet11" + chiaDBPull: + enabled: true + s3Prefix: "s3://chia-blockchain-sqlite-backups/testnet11/" + network: "testnet11" # optional override; derived from spec.chia when omitted + minHeight: 123456 + awsCredentialsSecret: aws-creds +``` + +The Secret referenced by `awsCredentialsSecret` is mounted via `envFrom`, so its keys (e.g. `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, optionally `AWS_SESSION_TOKEN`) become environment variables on the init container without needing to be in the CR in plaintext. + +If you are running on EKS with IRSA (or another mechanism where the pod's ServiceAccount provides AWS credentials), simply omit `awsCredentialsSecret` and set the appropriate `serviceAccountName` on the ChiaNode. + +When `chiaDBPull.enabled` is `true`, `chiaDBPull.s3Prefix` is required; the controller will refuse to reconcile and emit an event otherwise. + +See the [chia-db-pull README](https://github.com/Chia-Network/chia-db-pull) for more details on each of these settings. + +### Note on ordering with `spec.initContainers` + +The first-class `chia-db-pull` container is appended to the StatefulSet's init container list **after** any containers defined in `spec.initContainers`. This means manually-defined init containers run first (good for things like clearing peer caches), and `chia-db-pull` is the last init container before the main chia container starts. + ## More Info This page contains documentation specific to this resource. Please see the rest of the documentation for information on more available configurations. diff --git a/internal/controller/chianode/assemblers.go b/internal/controller/chianode/assemblers.go index 1c7de023..445c4e54 100644 --- a/internal/controller/chianode/assemblers.go +++ b/internal/controller/chianode/assemblers.go @@ -328,6 +328,11 @@ func assembleStatefulset(ctx context.Context, node k8schianetv1.ChiaNode, fullNo stateful.Spec.Template.Spec.Volumes = append(stateful.Spec.Template.Spec.Volumes, init.Volumes...) } + // Append the first-class chia-db-pull init container last so any user-defined init containers run first. + if kube.ChiaDBPullEnabled(node.Spec.ChiaDBPullConfig) { + stateful.Spec.Template.Spec.InitContainers = append(stateful.Spec.Template.Spec.InitContainers, assembleChiaDBPullContainer(node, networkData)) + } + // Get Sidecar Containers stateful.Spec.Template.Spec.Containers = append(stateful.Spec.Template.Spec.Containers, kube.GetExtraContainers(node.Spec.Sidecars, chiaContainer)...) // Add Sidecar Container Volumes @@ -465,3 +470,34 @@ func assembleChiaHealthcheckContainer(node k8schianetv1.ChiaNode) corev1.Contain return kube.AssembleChiaHealthcheckContainer(input) } + +func assembleChiaDBPullContainer(node k8schianetv1.ChiaNode, networkData *map[string]string) corev1.Container { + input := kube.AssembleChiaDBPullContainerInputs{ + Image: node.Spec.ChiaDBPullConfig.Image, + ImagePullPolicy: node.Spec.ImagePullPolicy, + S3Prefix: node.Spec.ChiaDBPullConfig.S3Prefix, + Network: node.Spec.ChiaDBPullConfig.Network, + MinHeight: node.Spec.ChiaDBPullConfig.MinHeight, + AWSCredentialsSecret: node.Spec.ChiaDBPullConfig.AWSCredentialsSecret, + AdditionalEnv: node.Spec.ChiaDBPullConfig.AdditionalEnv, + } + + // If the user didn't explicitly set chiaDBPull.network, derive it from the surrounding chia config + // the same way the chia container's "network" env var is derived: a ChiaNetwork ConfigMap + // "network" key wins over an inline CommonSpecChia.Network value. + if input.Network == nil || *input.Network == "" { + if resolved := kube.ResolveChiaNetwork(node.Spec.ChiaConfig.CommonSpecChia, networkData); resolved != "" { + input.Network = &resolved + } + } + + if node.Spec.ChiaDBPullConfig.SecurityContext != nil { + input.SecurityContext = node.Spec.ChiaDBPullConfig.SecurityContext + } + + if node.Spec.ChiaDBPullConfig.Resources != nil { + input.ResourceRequirements = *node.Spec.ChiaDBPullConfig.Resources + } + + return kube.AssembleChiaDBPullContainer(input) +} diff --git a/internal/controller/chianode/controller.go b/internal/controller/chianode/controller.go index bb106031..609cc16d 100644 --- a/internal/controller/chianode/controller.go +++ b/internal/controller/chianode/controller.go @@ -74,6 +74,13 @@ func (r *ChiaNodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c metrics.ChiaNodes.Add(1.0) } + // Validate the chia-db-pull init container config before doing any other work. + if kube.ChiaDBPullEnabled(node.Spec.ChiaDBPullConfig) && node.Spec.ChiaDBPullConfig.S3Prefix == "" { + err := fmt.Errorf("chiaDBPull.enabled is true but chiaDBPull.s3Prefix is empty") + r.Recorder.Event(&node, corev1.EventTypeWarning, "Failed", "chiaDBPull is enabled but s3Prefix is not set -- the chia-db-pull init container requires an S3 prefix.") + return ctrl.Result{}, fmt.Errorf("ChiaNodeReconciler ChiaNode=%s %v", req.NamespacedName, err) + } + // Check for ChiaNetwork, retrieve matching ConfigMap if specified networkData, err := kube.GetChiaNetworkData(ctx, r.Client, node.Spec.ChiaConfig.CommonSpecChia, node.Namespace) if err != nil { diff --git a/internal/controller/chianode/helpers_test.go b/internal/controller/chianode/helpers_test.go index e9bd9723..954c7ec3 100644 --- a/internal/controller/chianode/helpers_test.go +++ b/internal/controller/chianode/helpers_test.go @@ -280,3 +280,110 @@ func TestGetChiaVolumesAndTemplates(t *testing.T) { func stringPtr(s string) *string { return &s } + +func TestAssembleChiaDBPullContainer(t *testing.T) { + network := "testnet11" + minHeight := int64(123456) + credsSecret := "aws-creds" + + node := k8schianetv1.ChiaNode{ + Spec: k8schianetv1.ChiaNodeSpec{ + CommonSpec: k8schianetv1.CommonSpec{ + ImagePullPolicy: corev1.PullIfNotPresent, + }, + ChiaDBPullConfig: k8schianetv1.SpecChiaDBPull{ + S3Prefix: "s3://test/", + Network: &network, + MinHeight: &minHeight, + AWSCredentialsSecret: &credsSecret, + }, + }, + } + + cont := assembleChiaDBPullContainer(node, nil) + + assert.Equal(t, "chia-db-pull", cont.Name) + assert.Equal(t, corev1.PullIfNotPresent, cont.ImagePullPolicy) + assert.NotEmpty(t, cont.Image, "Image should default when not set") + + envByName := map[string]string{} + for _, e := range cont.Env { + envByName[e.Name] = e.Value + } + assert.Equal(t, "/chia-data", envByName["CHIA_ROOT"]) + assert.Equal(t, "s3://test/", envByName["S3_PREFIX"]) + assert.Equal(t, "testnet11", envByName["NETWORK"]) + assert.Equal(t, "123456", envByName["MIN_HEIGHT"]) + + assert.Len(t, cont.EnvFrom, 1) + assert.Equal(t, credsSecret, cont.EnvFrom[0].SecretRef.Name) + + assert.Len(t, cont.VolumeMounts, 1) + assert.Equal(t, "chiaroot", cont.VolumeMounts[0].Name) + assert.Equal(t, "/chia-data", cont.VolumeMounts[0].MountPath) +} + +func TestAssembleChiaDBPullContainer_DerivesNetwork(t *testing.T) { + chiaNet := "testnet11" + configmapNet := "testnet-from-cm" + + envByName := func(c corev1.Container) map[string]string { + m := map[string]string{} + for _, e := range c.Env { + m[e.Name] = e.Value + } + return m + } + + t.Run("from CommonSpecChia.Network when chiaDBPull.network unset", func(t *testing.T) { + node := k8schianetv1.ChiaNode{ + Spec: k8schianetv1.ChiaNodeSpec{ + ChiaConfig: k8schianetv1.ChiaNodeSpecChia{ + CommonSpecChia: k8schianetv1.CommonSpecChia{Network: &chiaNet}, + }, + ChiaDBPullConfig: k8schianetv1.SpecChiaDBPull{S3Prefix: "s3://test/"}, + }, + } + assert.Equal(t, "testnet11", envByName(assembleChiaDBPullContainer(node, nil))["NETWORK"]) + }) + + t.Run("ChiaNetwork ConfigMap data overrides inline chia.network", func(t *testing.T) { + networkData := map[string]string{"network": configmapNet} + node := k8schianetv1.ChiaNode{ + Spec: k8schianetv1.ChiaNodeSpec{ + ChiaConfig: k8schianetv1.ChiaNodeSpecChia{ + CommonSpecChia: k8schianetv1.CommonSpecChia{Network: &chiaNet}, + }, + ChiaDBPullConfig: k8schianetv1.SpecChiaDBPull{S3Prefix: "s3://test/"}, + }, + } + assert.Equal(t, configmapNet, envByName(assembleChiaDBPullContainer(node, &networkData))["NETWORK"]) + }) + + t.Run("explicit chiaDBPull.network wins over derivation", func(t *testing.T) { + networkData := map[string]string{"network": configmapNet} + explicit := "explicit-net" + node := k8schianetv1.ChiaNode{ + Spec: k8schianetv1.ChiaNodeSpec{ + ChiaConfig: k8schianetv1.ChiaNodeSpecChia{ + CommonSpecChia: k8schianetv1.CommonSpecChia{Network: &chiaNet}, + }, + ChiaDBPullConfig: k8schianetv1.SpecChiaDBPull{ + S3Prefix: "s3://test/", + Network: &explicit, + }, + }, + } + assert.Equal(t, "explicit-net", envByName(assembleChiaDBPullContainer(node, &networkData))["NETWORK"]) + }) + + t.Run("no NETWORK env when nothing is resolvable", func(t *testing.T) { + node := k8schianetv1.ChiaNode{ + Spec: k8schianetv1.ChiaNodeSpec{ + ChiaDBPullConfig: k8schianetv1.SpecChiaDBPull{S3Prefix: "s3://test/"}, + }, + } + _, hasNetwork := envByName(assembleChiaDBPullContainer(node, nil))["NETWORK"] + assert.False(t, hasNetwork, "NETWORK env should be omitted when no network is resolvable") + }) +} diff --git a/internal/controller/chianode_test.go b/internal/controller/chianode_test.go index cd583a8a..d27fa24e 100644 --- a/internal/controller/chianode_test.go +++ b/internal/controller/chianode_test.go @@ -13,6 +13,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" apiv1 "github.com/chia-network/chia-operator/api/v1" ) @@ -40,6 +41,10 @@ var _ = Describe("ChiaNode controller", func() { ChiaConfig: apiv1.ChiaNodeSpecChia{ CASecretName: "test-secret", }, + ChiaDBPullConfig: apiv1.SpecChiaDBPull{ + Enabled: ptr.To(true), + S3Prefix: "s3://test/", + }, }, } expect := &apiv1.ChiaNode{ @@ -52,6 +57,10 @@ var _ = Describe("ChiaNode controller", func() { Enabled: nil, DNSHostname: nil, }, + ChiaDBPullConfig: apiv1.SpecChiaDBPull{ + Enabled: ptr.To(true), + S3Prefix: "s3://test/", + }, CommonSpec: apiv1.CommonSpec{ ImagePullPolicy: "Always", ChiaExporterConfig: apiv1.SpecChiaExporter{ diff --git a/internal/controller/common/consts/consts.go b/internal/controller/common/consts/consts.go index c1663339..63f69129 100644 --- a/internal/controller/common/consts/consts.go +++ b/internal/controller/common/consts/consts.go @@ -55,6 +55,12 @@ var ( // DefaultChiaHealthcheckImageTag contains the default tag name for the chia-healthcheck image DefaultChiaHealthcheckImageTag = "latest" + + // DefaultChiaDBPullImageName contains the default image name for the chia-db-pull init container image + DefaultChiaDBPullImageName = "ghcr.io/chia-network/chia-db-pull" + + // DefaultChiaDBPullImageTag contains the default tag name for the chia-db-pull init container image + DefaultChiaDBPullImageTag = "latest" ) const ( diff --git a/internal/controller/common/kube/assemblers.go b/internal/controller/common/kube/assemblers.go index 48e28b33..7189c7f7 100644 --- a/internal/controller/common/kube/assemblers.go +++ b/internal/controller/common/kube/assemblers.go @@ -2,6 +2,7 @@ package kube import ( "fmt" + "strconv" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -311,3 +312,80 @@ func AssembleChiaHealthcheckProbe(input AssembleChiaHealthcheckProbeInputs) *cor return &probe } + +// AssembleChiaDBPullContainerInputs contains configuration inputs to the AssembleChiaDBPullContainer function +type AssembleChiaDBPullContainerInputs struct { + Image *string + ImagePullPolicy corev1.PullPolicy + S3Prefix string + Network *string + MinHeight *int64 + AWSCredentialsSecret *string + AdditionalEnv *[]corev1.EnvVar + ResourceRequirements corev1.ResourceRequirements + SecurityContext *corev1.SecurityContext +} + +// AssembleChiaDBPullContainer assembles the chia-db-pull init container spec. +// The container shares the chiaroot volume with the main chia container so that the downloaded +// blockchain database lands in CHIA_ROOT before chia starts. +func AssembleChiaDBPullContainer(input AssembleChiaDBPullContainerInputs) corev1.Container { + container := corev1.Container{ + Name: "chia-db-pull", + SecurityContext: input.SecurityContext, + ImagePullPolicy: input.ImagePullPolicy, + Env: []corev1.EnvVar{ + { + Name: "CHIA_ROOT", + Value: "/chia-data", + }, + { + Name: "S3_PREFIX", + Value: input.S3Prefix, + }, + }, + Resources: input.ResourceRequirements, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "chiaroot", + MountPath: "/chia-data", + }, + }, + } + + if input.Image != nil && *input.Image != "" { + container.Image = *input.Image + } else { + container.Image = fmt.Sprintf("%s:%s", consts.DefaultChiaDBPullImageName, consts.DefaultChiaDBPullImageTag) + } + + if input.Network != nil && *input.Network != "" { + container.Env = append(container.Env, corev1.EnvVar{ + Name: "NETWORK", + Value: *input.Network, + }) + } + + if input.MinHeight != nil { + container.Env = append(container.Env, corev1.EnvVar{ + Name: "MIN_HEIGHT", + Value: strconv.FormatInt(*input.MinHeight, 10), + }) + } + + if input.AWSCredentialsSecret != nil && *input.AWSCredentialsSecret != "" { + container.EnvFrom = append(container.EnvFrom, corev1.EnvFromSource{ + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: *input.AWSCredentialsSecret, + }, + }, + }) + } + + if input.AdditionalEnv != nil { + container.Env = append(container.Env, *input.AdditionalEnv...) + } + + return container +} diff --git a/internal/controller/common/kube/assemblers_test.go b/internal/controller/common/kube/assemblers_test.go index 9d6a92d6..c96f60be 100644 --- a/internal/controller/common/kube/assemblers_test.go +++ b/internal/controller/common/kube/assemblers_test.go @@ -574,3 +574,118 @@ func TestAssembleChiaHealthcheckProbe_Full(t *testing.T) { }) require.Equal(t, expected, *actual) } + +func TestAssembleChiaDBPullContainer_Minimal(t *testing.T) { + expected := corev1.Container{ + Name: "chia-db-pull", + Image: "test:latest", + ImagePullPolicy: "Always", + Env: []corev1.EnvVar{ + { + Name: "CHIA_ROOT", + Value: "/chia-data", + }, + { + Name: "S3_PREFIX", + Value: "s3://test/", + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "chiaroot", + MountPath: "/chia-data", + }, + }, + } + actual := AssembleChiaDBPullContainer(AssembleChiaDBPullContainerInputs{ + Image: &expected.Image, + ImagePullPolicy: expected.ImagePullPolicy, + S3Prefix: "s3://test/", + }) + require.Equal(t, expected, actual) +} + +func TestAssembleChiaDBPullContainer_DefaultImage(t *testing.T) { + actual := AssembleChiaDBPullContainer(AssembleChiaDBPullContainerInputs{ + S3Prefix: "s3://test/", + }) + require.Equal(t, consts.DefaultChiaDBPullImageName+":"+consts.DefaultChiaDBPullImageTag, actual.Image) +} + +func TestAssembleChiaDBPullContainer_Full(t *testing.T) { + network := "testnet11" + minHeight := int64(123456) + credsSecret := "aws-creds" + expected := corev1.Container{ + Name: "chia-db-pull", + Image: "test:latest", + ImagePullPolicy: "Always", + Env: []corev1.EnvVar{ + { + Name: "CHIA_ROOT", + Value: "/chia-data", + }, + { + Name: "S3_PREFIX", + Value: "s3://test/", + }, + { + Name: "NETWORK", + Value: network, + }, + { + Name: "MIN_HEIGHT", + Value: "123456", + }, + { + Name: "EXTRA", + Value: "yes", + }, + }, + EnvFrom: []corev1.EnvFromSource{ + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: credsSecret, + }, + }, + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "chiaroot", + MountPath: "/chia-data", + }, + }, + SecurityContext: &corev1.SecurityContext{ + Capabilities: &corev1.Capabilities{ + Add: []corev1.Capability{ + "NET_BIND_SERVICE", + }, + }, + }, + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + "CPU": resource.MustParse("200m"), + "Memory": resource.MustParse("512Mi"), + }, + Requests: corev1.ResourceList{ + "CPU": resource.MustParse("100m"), + "Memory": resource.MustParse("256Mi"), + }, + }, + } + additional := []corev1.EnvVar{{Name: "EXTRA", Value: "yes"}} + actual := AssembleChiaDBPullContainer(AssembleChiaDBPullContainerInputs{ + Image: &expected.Image, + ImagePullPolicy: expected.ImagePullPolicy, + S3Prefix: "s3://test/", + Network: &network, + MinHeight: &minHeight, + AWSCredentialsSecret: &credsSecret, + AdditionalEnv: &additional, + ResourceRequirements: expected.Resources, + SecurityContext: expected.SecurityContext, + }) + require.Equal(t, expected, actual) +} diff --git a/internal/controller/common/kube/helpers.go b/internal/controller/common/kube/helpers.go index 73f51139..2afbde94 100644 --- a/internal/controller/common/kube/helpers.go +++ b/internal/controller/common/kube/helpers.go @@ -371,3 +371,29 @@ func ChiaExporterEnabled(in k8schianetv1.SpecChiaExporter) bool { } return *in.Enabled } + +// ChiaDBPullEnabled returns true if the first-class chia-db-pull init container was enabled. +// Defaults to disabled, since this init container only makes sense when an S3 prefix is configured. +func ChiaDBPullEnabled(in k8schianetv1.SpecChiaDBPull) bool { + if in.Enabled == nil { + return false + } + return *in.Enabled +} + +// ResolveChiaNetwork returns the chia network name implied by a CommonSpecChia and optional +// ChiaNetwork ConfigMap data, in the same priority order the chia container's "network" env var +// is resolved from: a "network" key in the ChiaNetwork ConfigMap overrides the inline +// CommonSpecChia.Network field. Returns "" when neither is set (which the caller should treat +// as "let the consumer use its own default", typically mainnet). +func ResolveChiaNetwork(commonSpecChia k8schianetv1.CommonSpecChia, networkData *map[string]string) string { + if networkData != nil { + if v, ok := (*networkData)["network"]; ok && v != "" { + return v + } + } + if commonSpecChia.Network != nil && *commonSpecChia.Network != "" { + return *commonSpecChia.Network + } + return "" +} diff --git a/internal/controller/common/kube/helpers_test.go b/internal/controller/common/kube/helpers_test.go index 8bbc074e..5b1ba098 100644 --- a/internal/controller/common/kube/helpers_test.go +++ b/internal/controller/common/kube/helpers_test.go @@ -409,6 +409,50 @@ func TestChiaExporterEnabled(t *testing.T) { require.Equal(t, false, actual, "expected exporter disabled, set to false") } +func TestResolveChiaNetwork(t *testing.T) { + // Empty when nothing is set + got := ResolveChiaNetwork(k8schianetv1.CommonSpecChia{}, nil) + require.Equal(t, "", got) + + // Falls back to commonSpecChia.Network + net := "testnet11" + got = ResolveChiaNetwork(k8schianetv1.CommonSpecChia{Network: &net}, nil) + require.Equal(t, "testnet11", got) + + // ChiaNetwork ConfigMap "network" key takes precedence over inline Network + configmapNetwork := "testnetX" + override := map[string]string{"network": configmapNetwork} + got = ResolveChiaNetwork(k8schianetv1.CommonSpecChia{Network: &net}, &override) + require.Equal(t, configmapNetwork, got) + + // Empty value in the ConfigMap is ignored (falls through to inline) + emptyOverride := map[string]string{"network": ""} + got = ResolveChiaNetwork(k8schianetv1.CommonSpecChia{Network: &net}, &emptyOverride) + require.Equal(t, "testnet11", got) +} + +func TestChiaDBPullEnabled(t *testing.T) { + // False case - default false (opposite of healthcheck/exporter) + actual := ChiaDBPullEnabled(k8schianetv1.SpecChiaDBPull{ + Enabled: nil, + }) + require.Equal(t, false, actual, "expected chia-db-pull disabled by default") + + // True case - set to true + enabled := true + actual = ChiaDBPullEnabled(k8schianetv1.SpecChiaDBPull{ + Enabled: &enabled, + }) + require.Equal(t, true, actual, "expected chia-db-pull enabled, set to true") + + // False case - set to false + disabled := false + actual = ChiaDBPullEnabled(k8schianetv1.SpecChiaDBPull{ + Enabled: &disabled, + }) + require.Equal(t, false, actual, "expected chia-db-pull disabled, set to false") +} + func TestGetCommonChiaEnv(t *testing.T) { testCases := []struct { name string