diff --git a/blueprints/starter/README.md b/blueprints/starter/README.md index bd425519..0d2d9581 100644 --- a/blueprints/starter/README.md +++ b/blueprints/starter/README.md @@ -41,19 +41,38 @@ timoni -n default delete blueprint ## Configuration -| Key | Type | Default | Description | -|--------------------------|----------------------------------|--------------------|----------------------------------------------------------------------------------------------------------------------------------------------| -| `image: tag:` | `string` | `` | Container image tag | -| `image: digest:` | `string` | `""` | Container image digest, takes precedence over `tag` when specified | -| `image: repository:` | `string` | `docker.io/nginx` | Container image repository | -| `image: pullPolicy:` | `string` | `IfNotPresent` | [Kubernetes image pull policy](https://kubernetes.io/docs/concepts/containers/images/#image-pull-policy) | -| `metadata: labels:` | `{[ string]: string}` | `{}` | Common labels for all resources | -| `metadata: annotations:` | `{[ string]: string}` | `{}` | Common annotations for all resources | -| `pod: annotations:` | `{[ string]: string}` | `{}` | Annotations applied to pods | -| `pod: affinity:` | `corev1.#Affinity` | `{}` | [Kubernetes affinity and anti-affinity](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity) | -| `pod: imagePullSecrets:` | `[...timoniv1.#ObjectReference]` | `[]` | [Kubernetes image pull secrets](https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod) | -| `replicas:` | `int` | `1` | Kubernetes deployment replicas | -| `resources:` | `timoniv1.#ResourceRequirements` | `{}` | [Kubernetes resource requests and limits](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers) | -| `securityContext:` | `corev1.#SecurityContext` | `{}` | [Kubernetes container security context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context) | -| `service: annotations:` | `{[ string]: string}` | `{}` | Annotations applied to the Kubernetes Service | -| `service: port:` | `int` | `80` | Kubernetes Service HTTP port | +### General values + +| KEY | TYPE | DESCRIPTION | +|----------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `metadata: name:` | `"module-name"` | Name must be unique within a namespace. Is required when creating resources. Name is primarily intended for creation idempotence and configuration definition. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names | +| `metadata: namespace:` | `"default"` | Namespace defines the space within which each name must be unique. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces | +| `metadata: annotations:` | `{}` | Annotations is an unstructured key value map stored with a resource that may be set to store and retrieve arbitrary metadata. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations The annotations allows adding `metadata.annotations` to all resources. | +| `metadata: labels:` | `{ "app.kubernetes.io/name": "module-name" "app.kubernetes.io/version": "0.0.0-devel" "app.kubernetes.io/managed-by": "timoni"}` | Map of string keys and values that can be used to organize and categorize (scope and select) objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels Standard Kubernetes labels: app name, version and managed-by. The labels allows adding `metadata.labels` to all resources. The `app.kubernetes.io/name` and `app.kubernetes.io/version` labels are automatically generated and can't be overwritten. | +| `selector: labels:` | `{ "app.kubernetes.io/name": "module-name"}` | Map of string keys and values that can be used to organize and categorize (scope and select) objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels Standard Kubernetes label: app name. | +| `image: repository:` | `*"docker.io/nginx" \| string` | Repository is the address of a container registry repository. An image repository is made up of slash-separated name components, optionally prefixed by a registry hostname and port in the format [HOST[:PORT_NUMBER]/]PATH. | +| `image: tag:` | `*"1-alpine" \| strings.MaxRunes(128)` | Tag identifies an image in the repository. A tag name may contain lowercase and uppercase characters, digits, underscores, periods and dashes. A tag name may not start with a period or a dash and may contain a maximum of 128 characters. | +| `image: digest:` | `*"" \| string` | Digest uniquely and immutably identifies an image in the repository. Spec: https://github.com/opencontainers/image-spec/blob/main/descriptor.md#digests. | +| `image: pullPolicy:` | `*"IfNotPresent" \| "Always" \| "Never"` | PullPolicy defines the pull policy for the image. By default, it is set to IfNotPresent. | +| `pod: annotations:` | `{}` | | +| `pod: affinity:` | `*{ nodeAffinity: { requiredDuringSchedulingIgnoredDuringExecution: { nodeSelectorTerms: [{ matchExpressions: [{ key: "kubernetes.io/os" operator: "In" values: ["linux"] }] }] } }} \| { nodeAffinity: { requiredDuringSchedulingIgnoredDuringExecution: { nodeSelectorTerms: [{ matchExpressions: [{ key: "kubernetes.io/os" operator: "In" values: ["linux"] }] }] } }} \| {}` | | +| `pod: imagePullSecrets:` | `[...{ name!: strings.MaxRunes(256)}]` | | +| `resources: limits:` | `{}` | Limits describes the maximum amount of compute resources allowed. | +| `resources: requests: cpu:` | `*"10m" \| =~"^[1-9]\\d*m$"` | | +| `resources: requests: memory:` | `*"32Mi" \| =~"^[1-9]\\d*(Mi\|Gi)$"` | | +| `replicas:` | `*1 \| >0 & int` | The number of pods replicas. By default, the number of replicas is 1. | +| `securityContext: capabilities: drop:` | `*["ALL"] \| [string]` | Removed capabilities | +| `securityContext: capabilities: add:` | `*["CHOWN", "NET_BIND_SERVICE", "SETGID", "SETUID"] \| [string]` | Added capabilities | +| `securityContext: privileged:` | `*false \| true` | 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. | +| `securityContext: seLinuxOptions:` | `null \| {}` | 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. | +| `securityContext: windowsOptions:` | `null \| {}` | 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. | +| `securityContext: runAsUser:` | `null \| int64` | 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. | +| `securityContext: runAsGroup:` | `null \| int64` | 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. | +| `securityContext: runAsNonRoot:` | `null \| bool` | 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. | +| `securityContext: readOnlyRootFilesystem:` | `null \| bool` | 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. | +| `securityContext: allowPrivilegeEscalation:` | `*false \| true` | 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. | +| `securityContext: procMount:` | `null \| string` | procMount denotes the type of proc mount to use for the containers. The default is DefaultProcMount 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. | +| `securityContext: seccompProfile:` | `null \| { type: string}` | 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. | +| `service: annotations:` | `{}` | | +| `service: port:` | `*80 \| uint16 & >0` | | + diff --git a/blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/image.cue b/blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/image.cue index 1535ea43..8378bb08 100644 --- a/blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/image.cue +++ b/blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/image.cue @@ -30,6 +30,7 @@ import ( // Reference is the image address computed from repository, tag and digest // in the format [REPOSITORY]:[TAG]@[DIGEST]. + // +nodoc reference: string if digest != "" && tag != "" { diff --git a/blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/metadata.cue b/blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/metadata.cue index 188ff505..afed90dc 100644 --- a/blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/metadata.cue +++ b/blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/metadata.cue @@ -43,13 +43,17 @@ import "strings" // Standard Kubernetes labels: app name, version and managed-by. labels: { - (#StdLabelName): name - (#StdLabelVersion): #Version + // +nodoc + (#StdLabelName): name + // +nodoc + (#StdLabelVersion): #Version + // +nodoc (#StdLabelManagedBy): "timoni" } // LabelSelector selects Pods based on the app.kubernetes.io/name label. #LabelSelector: #Labels & { + // +nodoc (#StdLabelName): name } @@ -74,6 +78,7 @@ import "strings" namespace: #Meta.namespace labels: #Meta.labels + // +nodoc labels: (#StdLabelComponent): #Component annotations?: #Annotations @@ -84,8 +89,10 @@ import "strings" // LabelSelector selects Pods based on the app.kubernetes.io/name // and app.kubernetes.io/component labels. #LabelSelector: #Labels & { + // +nodoc (#StdLabelComponent): #Component - (#StdLabelName): #Meta.name + // +nodoc + (#StdLabelName): #Meta.name } } @@ -104,6 +111,7 @@ import "strings" name: #Meta.name + "-" + #Component labels: #Meta.labels + // +nodoc labels: (#StdLabelComponent): #Component annotations?: #Annotations @@ -114,7 +122,9 @@ import "strings" // LabelSelector selects Pods based on the app.kubernetes.io/name // and app.kubernetes.io/component labels. #LabelSelector: #Labels & { + // +nodoc (#StdLabelComponent): #Component - (#StdLabelName): #Meta.name + // +nodoc + (#StdLabelName): #Meta.name } } diff --git a/blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/selector.cue b/blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/selector.cue index 9c4f2384..87bd377e 100644 --- a/blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/selector.cue +++ b/blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/selector.cue @@ -15,5 +15,8 @@ package v1alpha1 labels: #Labels // Standard Kubernetes label: app name. - labels: (#StdLabelName): #Name + labels: { + // +nodoc + (#StdLabelName): #Name + } } diff --git a/blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/semver.cue b/blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/semver.cue index ecd1e397..14dc8ab8 100644 --- a/blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/semver.cue +++ b/blueprints/starter/cue.mod/pkg/timoni.sh/core/v1alpha1/semver.cue @@ -21,9 +21,11 @@ import ( let minMajor = strconv.Atoi(strings.Split(#Minimum, ".")[0]) let minMinor = strconv.Atoi(strings.Split(#Minimum, ".")[1]) + // +nodoc major: int & >=minMajor major: strconv.Atoi(strings.Split(#Version, ".")[0]) + // +nodoc minor: int & >=minMinor minor: strconv.Atoi(strings.Split(#Version, ".")[1]) } diff --git a/blueprints/starter/templates/config.cue b/blueprints/starter/templates/config.cue index 0e784603..7ed1330a 100644 --- a/blueprints/starter/templates/config.cue +++ b/blueprints/starter/templates/config.cue @@ -9,13 +9,16 @@ import ( #Config: { // The kubeVersion is a required field, set at apply-time // via timoni.cue by querying the user's Kubernetes API. + // +nodoc kubeVersion!: string // Using the kubeVersion you can enforce a minimum Kubernetes minor version. // By default, the minimum Kubernetes version is set to 1.20. + // +nodoc clusterVersion: timoniv1.#SemVer & {#Version: kubeVersion, #Minimum: "1.20.0"} // The moduleVersion is set from the user-supplied module version. // This field is used for the `app.kubernetes.io/version` label. + // +nodoc moduleVersion!: string // The Kubernetes metadata common to all resources. diff --git a/cmd/timoni/mod_show_config.go b/cmd/timoni/mod_show_config.go index b32534e5..f3d4cda6 100644 --- a/cmd/timoni/mod_show_config.go +++ b/cmd/timoni/mod_show_config.go @@ -40,13 +40,13 @@ import ( var configShowModCmd = &cobra.Command{ Use: "config [MODULE PATH]", - Short: "Output the #Config structure of a local module", + Short: "Output the `#Config` structure of a local module", Long: `The config command parses the local module configuration structure and outputs the information to stdout.`, Example: ` # print the config of a module in the current directory timoni mod show config - # output the config to a file, if the file is markdown, the table will overwrite a table in a Configuration section or - # be appended to the end of the file + # output the config to a file, if the file is markdown, the table will overwrite a table in the ## Configuration section + # under the sub heading ### General values or be appended to the end of the file timoni mod show config --output ./README.md `, RunE: runConfigShowModCmd, @@ -115,7 +115,7 @@ func runConfigShowModCmd(cmd *cobra.Command, args []string) error { configShowModArgs.pkg.String(), ) - if err := builder.WriteSchemaFile(); err != nil { + if err = builder.WriteSchemaFile(); err != nil { return err } @@ -124,17 +124,16 @@ func runConfigShowModCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("build failed: %w", err) } - buildResult, err := builder.Build() + _, err = builder.Build() if err != nil { return describeErr(f.GetModuleRoot(), "validation failed", err) } - - rows, err := builder.GetConfigDoc(buildResult) + rows, err := builder.GetConfigDoc() if err != nil { return describeErr(f.GetModuleRoot(), "failed to get config structure", err) } - header := []string{"Key", "Type", "Default", "Description"} + header := []string{"Key", "Type", "Description"} if configShowModArgs.output == "" { printMarkDownTable(rootCmd.OutOrStdout(), header, rows) @@ -158,7 +157,10 @@ func writeFile(readFile string, header []string, rows [][]string, f fetcher.Fetc var tableBuffer bytes.Buffer tableWriter := bufio.NewWriter(&tableBuffer) printMarkDownTable(tableWriter, header, rows) - tableWriter.Flush() + err := tableWriter.Flush() + if err != nil { + return "", describeErr(f.GetModuleRoot(), "Unable to create the temporary output file", err) + } // get a temporary file name tmpFileName := readFile + ".tmp" // open the input file @@ -166,7 +168,6 @@ func writeFile(readFile string, header []string, rows [][]string, f fetcher.Fetc if err != nil { if errors.Is(err, fs.ErrNotExist) { inputFile, err = os.Create(readFile) - if err != nil { return "", describeErr(f.GetModuleRoot(), "Unable to create the temporary output file", err) } @@ -174,20 +175,31 @@ func writeFile(readFile string, header []string, rows [][]string, f fetcher.Fetc return "", describeErr(f.GetModuleRoot(), "Unable to create the temporary output file", err) } } - defer inputFile.Close() + fileClose := func(file *os.File) { + err = file.Close() + if err != nil { + fmt.Println(describeErr(f.GetModuleRoot(), "Unable to create the temporary output file", err)) + } + } + defer fileClose(inputFile) // open the output file outputFile, err := os.Create(tmpFileName) if err != nil { return "", describeErr(f.GetModuleRoot(), "Unable to create the temporary output file", err) } - defer outputFile.Close() + defer fileClose(outputFile) // Create the scanner and writer inputScanner := bufio.NewScanner(inputFile) outputWriter := bufio.NewWriter(outputFile) var configSection bool + var generalValuesSection bool var foundTable bool + regex, err := regexp.Compile(`^\|.*\|$`) + if err != nil { + return "", describeErr(f.GetModuleRoot(), "Unalbe to compile the regex check", err) + } // Scan the input file line by line to find the table and replace it or append it to the end for inputScanner.Scan() { @@ -197,29 +209,45 @@ func writeFile(readFile string, header []string, rows [][]string, f fetcher.Fetc if !configSection && line == "## Configuration" { configSection = true } - - matched, err := regexp.MatchString(`^\|.*\|$`, line) - if err != nil { - return "", describeErr(f.GetModuleRoot(), "Regex Match for table content failed", err) + if configSection && !generalValuesSection && line == "### General values" { + generalValuesSection = true } - if configSection && !foundTable && matched { + matched := regex.MatchString(line) + if configSection && generalValuesSection && !foundTable && matched { + // Write the new table foundTable = true - outputWriter.WriteString(tableBuffer.String() + "\n") - } else if configSection && foundTable && matched { - } else if configSection && foundTable && !matched { + _, err = outputWriter.WriteString(tableBuffer.String() + "\n") + if err != nil { + return "", describeErr(f.GetModuleRoot(), "Unable to create the temporary output file", err) + } + } else if configSection && generalValuesSection && foundTable && !matched { + // not a match for configuraton section so we set to false configSection = false + generalValuesSection = false + } else if configSection && generalValuesSection && foundTable && matched { + // do nothing here as we don't want to write the old table data } else { - outputWriter.WriteString(line + "\n") + // write the existing data + _, err = outputWriter.WriteString(line + "\n") + if err != nil { + return "", describeErr(f.GetModuleRoot(), "Unable to create the temporary output file", err) + } } } else { - outputWriter.WriteString(line + "\n") + _, err = outputWriter.WriteString(line + "\n") + if err != nil { + return "", describeErr(f.GetModuleRoot(), "Unable to create the temporary output file", err) + } } } // If no table was found, append it to the end of the file if !foundTable { - outputWriter.WriteString("\n" + tableBuffer.String()) + _, err = outputWriter.WriteString("\n" + tableBuffer.String()) + if err != nil { + return "", describeErr(f.GetModuleRoot(), "Unable to create the temporary output file", err) + } } err = outputWriter.Flush() diff --git a/cmd/timoni/testdata/module/README.md b/cmd/timoni/testdata/module/README.md index 59ddfb26..52515460 100644 --- a/cmd/timoni/testdata/module/README.md +++ b/cmd/timoni/testdata/module/README.md @@ -41,15 +41,17 @@ timoni -n module delete module ## Configuration -| KEY | TYPE | DEFAULT | DESCRIPTION | -|------------------------------|----------|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `client: enabled:` | `bool` | `true` | | -| `client: image: repository:` | `string` | `"cgr.dev/chainguard/timoni"` | Repository is the address of a container registry repository. An image repository is made up of slash-separated name components, optionally prefixed by a registry hostname and port in the format [HOST[:PORT_NUMBER]/]PATH. | -| `client: image: tag:` | `string` | `"latest-dev"` | Tag identifies an image in the repository. A tag name may contain lowercase and uppercase characters, digits, underscores, periods and dashes. A tag name may not start with a period or a dash and may contain a maximum of 128 characters. | -| `client: image: digest:` | `string` | `"sha256:b49fbaac0eedc22c1cfcd26684707179cccbed0df205171bae3e1bae61326a10"` | Digest uniquely and immutably identifies an image in the repository. Spec: https://github.com/opencontainers/image-spec/blob/main/descriptor.md#digests. | -| `client: image: pullPolicy:` | `string` | `"IfNotPresent"` | PullPolicy defines the pull policy for the image. By default, it is set to IfNotPresent. | -| `server: enabled:` | `bool` | `true` | | -| `domain:` | `string` | `"example.internal"` | | -| `globals: enabled:` | `bool` | `false` | | -| `team:` | `string` | `"test"` | | +### General values + +| KEY | TYPE | DESCRIPTION | +|------------------------------|----------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `client: enabled:` | `*true \| bool` | | +| `client: image: repository:` | `*"cgr.dev/chainguard/timoni" \| string` | Repository is the address of a container registry repository. An image repository is made up of slash-separated name components, optionally prefixed by a registry hostname and port in the format [HOST[:PORT_NUMBER]/]PATH. | +| `client: image: tag:` | `*"latest-dev" \| strings.MaxRunes(128)` | Tag identifies an image in the repository. A tag name may contain lowercase and uppercase characters, digits, underscores, periods and dashes. A tag name may not start with a period or a dash and may contain a maximum of 128 characters. | +| `client: image: digest:` | `*"sha256:b49fbaac0eedc22c1cfcd26684707179cccbed0df205171bae3e1bae61326a10" \| string` | Digest uniquely and immutably identifies an image in the repository. Spec: https://github.com/opencontainers/image-spec/blob/main/descriptor.md#digests. | +| `client: image: pullPolicy:` | `*"IfNotPresent" \| "Always" \| "Never"` | PullPolicy defines the pull policy for the image. By default, it is set to IfNotPresent. | +| `server: enabled:` | `*true \| bool` | | +| `domain:` | `*"example.internal" \| string` | | +| `globals: enabled:` | `*false \| bool` | | +| `team:` | `"test"` | | diff --git a/cmd/timoni/testdata/module/templates/config.cue b/cmd/timoni/testdata/module/templates/config.cue index 7a0be13a..b82645fd 100644 --- a/cmd/timoni/testdata/module/templates/config.cue +++ b/cmd/timoni/testdata/module/templates/config.cue @@ -23,11 +23,9 @@ import ( "app.kubernetes.io/team": team } - // +nodoc client: { enabled: *true | bool - // +nodoc image: timoniv1.#Image & { repository: *"cgr.dev/chainguard/timoni" | string tag: *"latest-dev" | string @@ -35,13 +33,11 @@ import ( } } - // +nodoc server: { enabled: *true | bool } domain: *"example.internal" | string - // +nodoc globals: { enabled: *false | bool } diff --git a/examples/minimal/README.md b/examples/minimal/README.md index 980a5bbf..987a404e 100644 --- a/examples/minimal/README.md +++ b/examples/minimal/README.md @@ -43,25 +43,50 @@ timoni -n default delete minimal ### General values -| Key | Type | Default | Description | -|------------------------------|-----------------------------------------|----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------| -| `image: tag:` | `string` | `` | Container image tag | -| `image: digest:` | `string` | `` | Container image digest, takes precedence over `tag` when specified | -| `image: repository:` | `string` | `cgr.dev/chainguard/nginx` | Container image repository | -| `image: pullPolicy:` | `string` | `IfNotPresent` | [Kubernetes image pull policy](https://kubernetes.io/docs/concepts/containers/images/#image-pull-policy) | -| `metadata: labels:` | `{[ string]: string}` | `{}` | Common labels for all resources | -| `metadata: annotations:` | `{[ string]: string}` | `{}` | Common annotations for all resources | -| `podAnnotations:` | `{[ string]: string}` | `{}` | Annotations applied to pods | -| `imagePullSecrets:` | `[...timoniv1.ObjectReference]` | `[]` | [Kubernetes image pull secrets](https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod) | -| `tolerations:` | `[ ...corev1.#Toleration]` | `[]` | [Kubernetes toleration](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration) | -| `affinity:` | `corev1.#Affinity` | `{}` | [Kubernetes affinity and anti-affinity](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity) | -| `resources:` | `timoniv1.#ResourceRequirements` | `{}` | [Kubernetes resource requests and limits](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers) | -| `topologySpreadConstraints:` | `[...corev1.#TopologySpreadConstraint]` | `[]` | [Kubernetes pod topology spread constraints](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints) | -| `podSecurityContext:` | `corev1.#PodSecurityContext` | `{}` | [Kubernetes pod security context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context) | -| `securityContext:` | `corev1.#SecurityContext` | `{}` | [Kubernetes container security context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context) | -| `service: annotations:` | `{[ string]: string}` | `{}` | Annotations applied to the Kubernetes Service | -| `service: port:` | `int` | `80` | Kubernetes Service HTTP port | -| `test: enabled:` | `bool` | `false` | Run end-to-end tests at install and upgrades | +| KEY | TYPE | DESCRIPTION | +|----------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `clusterVersion:` | `{ major: 1 minor: 27}` | Using the kubeVersion you can enforce a minimum Kubernetes minor version. By default, the minimum Kubernetes version is set to 1.20. | +| `moduleVersion:` | `"0.0.0-devel"` | The moduleVersion is set from the user-supplied module version. This field is used for the `app.kubernetes.io/version` label. | +| `metadata: name:` | `"module-name"` | Name must be unique within a namespace. Is required when creating resources. Name is primarily intended for creation idempotence and configuration definition. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names | +| `metadata: namespace:` | `"default"` | Namespace defines the space within which each name must be unique. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces | +| `metadata: annotations:` | `{}` | Annotations is an unstructured key value map stored with a resource that may be set to store and retrieve arbitrary metadata. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations The annotations allows adding `metadata.annotations` to all resources. | +| `metadata: labels:` | `{ "app.kubernetes.io/name": "module-name" "app.kubernetes.io/version": "0.0.0-devel" "app.kubernetes.io/managed-by": "timoni"}` | Map of string keys and values that can be used to organize and categorize (scope and select) objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels Standard Kubernetes labels: app name, version and managed-by. The labels allows adding `metadata.labels` to all resources. The `app.kubernetes.io/name` and `app.kubernetes.io/version` labels are automatically generated and can't be overwritten. | +| `metadata: finalizers:` | `[...string]` | Finalizers are namespaced keys that tell Kubernetes to wait until specific conditions are met before it fully deletes resources marked for deletion. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/ | +| `selector: labels:` | `{ "app.kubernetes.io/name": "module-name"}` | Map of string keys and values that can be used to organize and categorize (scope and select) objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels Standard Kubernetes label: app name. | +| `image: repository:` | `"cgr.dev/chainguard/nginx"` | Repository is the address of a container registry repository. An image repository is made up of slash-separated name components, optionally prefixed by a registry hostname and port in the format [HOST[:PORT_NUMBER]/]PATH. | +| `image: tag:` | `"1.25.3"` | Tag identifies an image in the repository. A tag name may contain lowercase and uppercase characters, digits, underscores, periods and dashes. A tag name may not start with a period or a dash and may contain a maximum of 128 characters. | +| `image: digest:` | `"sha256:3dd8fa303f77d7eb6ce541cb05009a5e8723bd7e3778b95131ab4a2d12fadb8f"` | Digest uniquely and immutably identifies an image in the repository. Spec: https://github.com/opencontainers/image-spec/blob/main/descriptor.md#digests. | +| `image: pullPolicy:` | `*"IfNotPresent" \| "Always" \| "Never"` | PullPolicy defines the pull policy for the image. By default, it is set to IfNotPresent. | +| `resources: limits:` | `{}` | Limits describes the maximum amount of compute resources allowed. | +| `resources: requests: cpu:` | `*"10m" \| =~"^[1-9]\\d*m$"` | | +| `resources: requests: memory:` | `*"32Mi" \| =~"^[1-9]\\d*(Mi\|Gi)$"` | | +| `replicas:` | `*1 \| int & >0` | The number of pods replicas. By default, the number of replicas is 1. | +| `securityContext: allowPrivilegeEscalation:` | `*false \| true` | 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. | +| `securityContext: capabilities: add:` | `*["CHOWN", "NET_BIND_SERVICE", "SETGID", "SETUID"] \| [string]` | Added capabilities | +| `securityContext: capabilities: drop:` | `*["ALL"] \| [string]` | Removed capabilities | +| `securityContext: privileged:` | `*false \| true` | 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. | +| `securityContext: readOnlyRootFilesystem:` | `null \| bool` | 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. | +| `securityContext: runAsGroup:` | `null \| int64` | 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. | +| `securityContext: runAsNonRoot:` | `null \| bool` | 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. | +| `securityContext: runAsUser:` | `null \| int64` | 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. | +| `securityContext: seLinuxOptions:` | `null \| {}` | 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. | +| `securityContext: windowsOptions:` | `null \| {}` | 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. | +| `securityContext: procMount:` | `null \| string` | procMount denotes the type of proc mount to use for the containers. The default is DefaultProcMount 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. | +| `securityContext: seccompProfile:` | `null \| { type: string}` | 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. | +| `service: annotations:` | `{}` | | +| `service: port:` | `*80 \| uint16 & >0` | | +| `podAnnotations:` | `{}` | Pod optional settings. | +| `podSecurityContext:` | `{}` | | +| `imagePullSecrets:` | `[...{ name!: strings.MaxRunes(256)}]` | | +| `tolerations:` | `[...{}]` | | +| `affinity:` | `{}` | | +| `topologySpreadConstraints:` | `[...{ maxSkew: int32 topologyKey: string whenUnsatisfiable: string}]` | | +| `test: enabled:` | `*false \| bool` | | +| `test: image: repository:` | `"cgr.dev/chainguard/curl"` | Repository is the address of a container registry repository. An image repository is made up of slash-separated name components, optionally prefixed by a registry hostname and port in the format [HOST[:PORT_NUMBER]/]PATH. | +| `test: image: tag:` | `"latest"` | Tag identifies an image in the repository. A tag name may contain lowercase and uppercase characters, digits, underscores, periods and dashes. A tag name may not start with a period or a dash and may contain a maximum of 128 characters. | +| `test: image: digest:` | `""` | Digest uniquely and immutably identifies an image in the repository. Spec: https://github.com/opencontainers/image-spec/blob/main/descriptor.md#digests. | +| `test: image: pullPolicy:` | `*"IfNotPresent" \| "Always" \| "Never"` | PullPolicy defines the pull policy for the image. By default, it is set to IfNotPresent. | +| `message:` | `"Hello World"` | App settings. | #### Recommended values diff --git a/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/action.cue b/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/action.cue index 2c579e99..3aadc316 100644 --- a/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/action.cue +++ b/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/action.cue @@ -18,6 +18,10 @@ Action: { Keep: { "action.timoni.sh/prune": ActionStatus.Disabled } + // DisableWaiting annotation is for excluding resouces from Timoni's readiness check. + DisableWaiting: { + "action.timoni.sh/wait": ActionStatus.Disabled + } } ActionStatus: { diff --git a/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/image.cue b/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/image.cue index 1535ea43..8378bb08 100644 --- a/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/image.cue +++ b/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/image.cue @@ -30,6 +30,7 @@ import ( // Reference is the image address computed from repository, tag and digest // in the format [REPOSITORY]:[TAG]@[DIGEST]. + // +nodoc reference: string if digest != "" && tag != "" { diff --git a/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/metadata.cue b/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/metadata.cue index 188ff505..16e38afe 100644 --- a/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/metadata.cue +++ b/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/metadata.cue @@ -43,13 +43,17 @@ import "strings" // Standard Kubernetes labels: app name, version and managed-by. labels: { - (#StdLabelName): name - (#StdLabelVersion): #Version + // +nodoc + (#StdLabelName): name + // +nodoc + (#StdLabelVersion): #Version + // +nodoc (#StdLabelManagedBy): "timoni" } // LabelSelector selects Pods based on the app.kubernetes.io/name label. #LabelSelector: #Labels & { + // +nodoc (#StdLabelName): name } @@ -74,7 +78,10 @@ import "strings" namespace: #Meta.namespace labels: #Meta.labels - labels: (#StdLabelComponent): #Component + labels: { + // +nodoc + (#StdLabelComponent): #Component + } annotations?: #Annotations if #Meta.annotations != _|_ { @@ -84,8 +91,10 @@ import "strings" // LabelSelector selects Pods based on the app.kubernetes.io/name // and app.kubernetes.io/component labels. #LabelSelector: #Labels & { + // +nodoc (#StdLabelComponent): #Component - (#StdLabelName): #Meta.name + // +nodoc + (#StdLabelName): #Meta.name } } @@ -104,7 +113,10 @@ import "strings" name: #Meta.name + "-" + #Component labels: #Meta.labels - labels: (#StdLabelComponent): #Component + labels: { + // +nodoc + (#StdLabelComponent): #Component + } annotations?: #Annotations if #Meta.annotations != _|_ { @@ -114,7 +126,9 @@ import "strings" // LabelSelector selects Pods based on the app.kubernetes.io/name // and app.kubernetes.io/component labels. #LabelSelector: #Labels & { + // +nodoc (#StdLabelComponent): #Component - (#StdLabelName): #Meta.name + // +nodoc + (#StdLabelName): #Meta.name } } diff --git a/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/selector.cue b/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/selector.cue index 9c4f2384..87bd377e 100644 --- a/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/selector.cue +++ b/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/selector.cue @@ -15,5 +15,8 @@ package v1alpha1 labels: #Labels // Standard Kubernetes label: app name. - labels: (#StdLabelName): #Name + labels: { + // +nodoc + (#StdLabelName): #Name + } } diff --git a/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/semver.cue b/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/semver.cue index ecd1e397..14dc8ab8 100644 --- a/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/semver.cue +++ b/examples/minimal/cue.mod/pkg/timoni.sh/core/v1alpha1/semver.cue @@ -21,9 +21,11 @@ import ( let minMajor = strconv.Atoi(strings.Split(#Minimum, ".")[0]) let minMinor = strconv.Atoi(strings.Split(#Minimum, ".")[1]) + // +nodoc major: int & >=minMajor major: strconv.Atoi(strings.Split(#Version, ".")[0]) + // +nodoc minor: int & >=minMinor minor: strconv.Atoi(strings.Split(#Version, ".")[1]) } diff --git a/examples/minimal/templates/config.cue b/examples/minimal/templates/config.cue index 5769b01f..5ace35ea 100644 --- a/examples/minimal/templates/config.cue +++ b/examples/minimal/templates/config.cue @@ -9,6 +9,7 @@ import ( #Config: { // The kubeVersion is a required field, set at apply-time // via timoni.cue by querying the user's Kubernetes API. + // +nodoc kubeVersion!: string // Using the kubeVersion you can enforce a minimum Kubernetes minor version. // By default, the minimum Kubernetes version is set to 1.20. diff --git a/examples/redis/README.md b/examples/redis/README.md index 2800cb63..2ef7b0c2 100644 --- a/examples/redis/README.md +++ b/examples/redis/README.md @@ -97,19 +97,46 @@ timoni -n default delete redis ### General values -| Key | Type | Default | Description | -|------------------------------|-----------------------------------------|----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------| -| `image: tag:` | `string` | `` | Container image tag | -| `image: digest:` | `string` | `` | Container image digest, takes precedence over `tag` when specified | -| `image: repository:` | `string` | `cgr.dev/chainguard/redis` | Container image repository | -| `image: pullPolicy:` | `string` | `IfNotPresent` | [Kubernetes image pull policy](https://kubernetes.io/docs/concepts/containers/images/#image-pull-policy) | -| `metadata: labels:` | `{[ string]: string}` | `{}` | Common labels for all resources | -| `metadata: annotations:` | `{[ string]: string}` | `{}` | Common annotations for all resources | -| `podAnnotations:` | `{[ string]: string}` | `{}` | Annotations applied to pods | -| `imagePullSecrets:` | `[...corev1.LocalObjectReference]` | `[]` | [Kubernetes image pull secrets](https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod) | -| `tolerations:` | `[ ...corev1.#Toleration]` | `[]` | [Kubernetes toleration](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration) | -| `affinity:` | `corev1.#Affinity` | `{}` | [Kubernetes affinity and anti-affinity](https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity) | -| `resources:` | `corev1.#ResourceRequirements` | `{}` | [Kubernetes resource requests and limits](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers) | -| `topologySpreadConstraints:` | `[...corev1.#TopologySpreadConstraint]` | `[]` | [Kubernetes pod topology spread constraints](https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints) | -| `podSecurityContext:` | `corev1.#PodSecurityContext` | `{runAsUser: 1001}` | [Kubernetes pod security context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context) | -| `securityContext:` | `corev1.#SecurityContext` | `{runAsNonRoot: true}` | [Kubernetes container security context](https://kubernetes.io/docs/tasks/configure-pod-container/security-context) | +| KEY | TYPE | DESCRIPTION | +|----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `kubeVersion:` | `"1.27.5"` | The kubeVersion is a required field, set at apply-time via timoni.cue by querying the user's Kubernetes API. | +| `clusterVersion: major:` | `1` | | +| `clusterVersion: minor:` | `27` | | +| `moduleVersion:` | `"0.0.0-devel"` | The moduleVersion is set from the user-supplied module version. This field is used for the `app.kubernetes.io/version` label. | +| `metadata: name:` | `"module-name"` | Name must be unique within a namespace. Is required when creating resources. Name is primarily intended for creation idempotence and configuration definition. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names | +| `metadata: namespace:` | `"default"` | Namespace defines the space within which each name must be unique. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces | +| `metadata: annotations:` | `{}` | Annotations is an unstructured key value map stored with a resource that may be set to store and retrieve arbitrary metadata. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations The annotations allows adding `metadata.annotations` to all resources. | +| `metadata: labels: "app.kubernetes.io/name":` | `"module-name"` | | +| `metadata: labels: "app.kubernetes.io/version":` | `"0.0.0-devel"` | | +| `metadata: labels: "app.kubernetes.io/managed-by":` | `"timoni"` | | +| `metadata: finalizers:` | `[...string]` | Finalizers are namespaced keys that tell Kubernetes to wait until specific conditions are met before it fully deletes resources marked for deletion. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/ | +| `maxmemory:` | `*512 \| int & >=64` | Redis config | +| `readonly: replicas:` | `*1 \| int & >=0` | | +| `persistence: enabled:` | `*true \| bool` | | +| `persistence: storageClass:` | `*"standard" \| string` | | +| `persistence: size:` | `*"8Gi" \| string` | | +| `password:` | `=~"^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$"` | | +| `image: repository:` | `"cgr.dev/chainguard/redis"` | Repository is the address of a container registry repository. An image repository is made up of slash-separated name components, optionally prefixed by a registry hostname and port in the format [HOST[:PORT_NUMBER]/]PATH. | +| `image: tag:` | `"7.2.4"` | Tag identifies an image in the repository. A tag name may contain lowercase and uppercase characters, digits, underscores, periods and dashes. A tag name may not start with a period or a dash and may contain a maximum of 128 characters. | +| `image: digest:` | `"sha256:6cf3066237a604fcf3e716aa9423cdd15383db49049bb594a337a97ca862ca50"` | Digest uniquely and immutably identifies an image in the repository. Spec: https://github.com/opencontainers/image-spec/blob/main/descriptor.md#digests. | +| `image: pullPolicy:` | `*"IfNotPresent" \| "Always" \| "Never"` | PullPolicy defines the pull policy for the image. By default, it is set to IfNotPresent. | +| `image: reference:` | `"cgr.dev/chainguard/redis:7.2.4@sha256:6cf3066237a604fcf3e716aa9423cdd15383db49049bb594a337a97ca862ca50"` | Reference is the image address computed from repository, tag and digest in the format [REPOSITORY]:[TAG]@[DIGEST]. | +| `imagePullSecrets:` | `[...corev1.LocalObjectReference]` | | +| `resources: limits: cpu:` | `=~"^[1-9]\\d*m$"` | | +| `resources: limits: memory:` | `*"544Mi" \| =~"^[1-9]\\d*(Mi\|Gi)$"` | | +| `resources: requests: cpu:` | `*"100m" \| =~"^[1-9]\\d*m$"` | | +| `resources: requests: memory:` | `*"64Mi" \| =~"^[1-9]\\d*(Mi\|Gi)$"` | | +| `resources: claims:` | `[...#ResourceClaim]` | Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. This is an alpha field and requires enabling the DynamicResourceAllocation feature gate. This field is immutable. +listType=map +listMapKey=name +featureGate=DynamicResourceAllocation | +| `podSecurityContext:` | `*{ fsGroup: 1001 runAsUser: 1001 runAsGroup: 1001} \| {}` | Security (common to all deployments) | +| `securityContext:` | `*{ allowPrivilegeEscalation: false readOnlyRootFilesystem: true runAsNonRoot: true capabilities: { drop: ["ALL"] } seccompProfile: { type: "RuntimeDefault" }} \| {}` | | +| `affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms:` | `[{ matchExpressions: [{ key: "kubernetes.io/os" operator: "In" values: ["linux"] }]}]` | Required. A list of node selector terms. The terms are ORed. | +| `affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution:` | `[...{ weight: int32 preference: {}}]` | The scheduler will prefer to schedule pods to nodes that satisfy the affinity expressions specified by this field, but it may choose a node that violates one or more of the expressions. The node that is most preferred is the one with the greatest sum of weights, i.e. for each node that meets all of the scheduling requirements (resource request, requiredDuringScheduling affinity expressions, etc.), compute a sum by iterating through the elements of this field and adding "weight" to the sum if the node matches the corresponding matchExpressions; the node(s) with the highest sum are the most preferred. | +| `affinity: podAffinity:` | `null \| {}` | Describes pod affinity scheduling rules (e.g. co-locate this pod in the same node, zone, etc. as some other pod(s)). | +| `affinity: podAntiAffinity:` | `null \| {}` | Describes pod anti-affinity scheduling rules (e.g. avoid putting this pod in the same node, zone, etc. as some other pod(s)). | +| `podAnnotations:` | `{}` | Pod optional settings (common to all deployments) | +| `tolerations:` | `[...{}]` | | +| `topologySpreadConstraints:` | `[...{ maxSkew: int32 topologyKey: string whenUnsatisfiable: string}]` | | +| `service: port:` | `*6379 \| uint16 & >0` | Service | +| `clusterDomain:` | `"cluster.local"` | | +| `test: enabled:` | `*false \| bool` | | + diff --git a/internal/engine/get_config.go b/internal/engine/get_config.go new file mode 100644 index 00000000..d1b3e7f3 --- /dev/null +++ b/internal/engine/get_config.go @@ -0,0 +1,189 @@ +/* +Copyright 2023 Stefan Prodan + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package engine + +import ( + "errors" + "fmt" + "regexp" + "strings" + + "cuelang.org/go/cue" + "cuelang.org/go/cue/ast" + "cuelang.org/go/cue/load" + + apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" +) + +// GetConfigDoc extracts the config structure from the module. +func (b *ModuleBuilder) GetConfigDoc() ([][]string, error) { + var value cue.Value + + cfg := &load.Config{ + ModuleRoot: b.moduleRoot, + Package: b.pkgName, + Dir: b.pkgPath, + DataFiles: true, + Tags: []string{ + "name=" + b.name, + "namespace=" + b.namespace, + }, + TagVars: map[string]load.TagVar{ + "moduleVersion": { + Func: func() (ast.Expr, error) { + return ast.NewString(b.moduleVersion), nil + }, + }, + "kubeVersion": { + Func: func() (ast.Expr, error) { + return ast.NewString(b.kubeVersion), nil + }, + }, + }, + } + + modInstances := load.Instances([]string{}, cfg) + if len(modInstances) == 0 { + return nil, errors.New("no instances found") + } + + modInstance := modInstances[0] + if modInstance.Err != nil { + return nil, fmt.Errorf("instance error: %w", modInstance.Err) + } + + value = b.ctx.BuildInstance(modInstance) + if value.Err() != nil { + return nil, value.Err() + } + + cfgValues := value.LookupPath(cue.ParsePath(apiv1.ConfigValuesSelector.String())) + if cfgValues.Err() != nil { + return nil, fmt.Errorf("lookup %s failed: %w", apiv1.ConfigValuesSelector, cfgValues.Err()) + } + + rows, err := iterateFields(cfgValues) + if err != nil { + return nil, err + } + + return rows, nil +} + +func iterateFields(v cue.Value) ([][]string, error) { + var rows [][]string + + fields, err := v.Fields( + cue.Optional(true), + cue.Concrete(true), + cue.Docs(true), + ) + if err != nil { + return nil, fmt.Errorf("Cue Fields Error: %w", err) + } + + for fields.Next() { + v := fields.Value() + _, noDoc := hasNoDoc(v) + + if noDoc { + continue + } + + // We are chekcing if the field is a struct and not optional and is concrete before we iterate through it + // this allows for definition of default values as full structs without generating output for each + // field in the struct where it doesn't make sense e.g. + // + // - annotations?: {[string]: string} + // - affinity: corev1.Affinity | *{nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: [...]} + if v.IncompleteKind() == cue.StructKind && !fields.IsOptional() && v.IsConcrete() { + //if _, ok := v.Default(); v.IncompleteKind() == cue.StructKind && !fields.IsOptional() && ok { + // Assume we want to use the field + useField := true + iRows, err := iterateFields(v) + + if err != nil { + return nil, err + } + + for _, row := range iRows { + if len(row) > 0 { + // If we have a row with more than 0 elements, we don't want to use the field and should use the child rows instead + useField = false + rows = append(rows, row) + } + } + + if useField { + rows = append(rows, getField(v)) + } + } else { + rows = append(rows, getField(v)) + } + } + + return rows, nil +} + +func hasNoDoc(v cue.Value) (string, bool) { + var noDoc bool + var doc string + + for _, d := range v.Doc() { + if line := len(d.List) - 1; line >= 0 { + switch d.List[line].Text { + case "// +nodoc": + noDoc = true + break + } + } + + doc += d.Text() + doc = strings.ReplaceAll(doc, "\n", " ") + doc = strings.ReplaceAll(doc, "+required", "") + doc = strings.ReplaceAll(doc, "+optional", "") + } + + return doc, noDoc +} + +func getField(v cue.Value) []string { + var row []string + labelDomain := regexp.MustCompile(`^([a-zA-Z0-9-_.]+)?(".+")?$`) + doc, noDoc := hasNoDoc(v) + + if !noDoc { + fieldType := strings.ReplaceAll(fmt.Sprintf("%v", v), "\n", "") + fieldType = strings.ReplaceAll(fieldType, "|", "\\|") + fieldType = strings.ReplaceAll(fieldType, "\":", "\": ") + fieldType = strings.ReplaceAll(fieldType, "\":[", "\": [") + fieldType = strings.ReplaceAll(fieldType, "},", "}, ") + + if len(fieldType) == 0 { + fieldType = " " + } + + field := strings.Replace(v.Path().String(), "timoni.instance.config.", "", 1) + match := labelDomain.FindStringSubmatch(field) + + row = append(row, fmt.Sprintf("`%s:`", strings.ReplaceAll(match[1], ".", ": ")+match[2])) + row = append(row, fmt.Sprintf("`%s`", fieldType)) + row = append(row, fmt.Sprintf("%s", doc)) + } + + return row +} diff --git a/internal/engine/module_builder.go b/internal/engine/module_builder.go index 29b9bbfb..3c2ba998 100644 --- a/internal/engine/module_builder.go +++ b/internal/engine/module_builder.go @@ -22,9 +22,7 @@ import ( "os" "path/filepath" "reflect" - "regexp" "slices" - "strings" "cuelang.org/go/cue" "cuelang.org/go/cue/ast" @@ -316,60 +314,3 @@ func (b *ModuleBuilder) GetContainerImages(value cue.Value) ([]string, error) { return images, nil } - -// GetConfigDoc extracts the config structure from the module. -func (b *ModuleBuilder) GetConfigDoc(value cue.Value) ([][]string, error) { - cfgValues := value.LookupPath(cue.ParsePath(apiv1.ConfigValuesSelector.String())) - if cfgValues.Err() != nil { - return nil, fmt.Errorf("lookup %s failed: %w", apiv1.ConfigValuesSelector, cfgValues.Err()) - } - - labelDomain := regexp.MustCompile(`^([a-zA-Z0-9-_.]+)?(".+")?$`) - - var rows [][]string - configDataInfo := func(v cue.Value) bool { - var row []string - var noDoc bool - var doc string - - for _, d := range v.Doc() { - if line := len(d.List) - 1; line >= 0 { - switch d.List[line].Text { - case "// +nodoc": - noDoc = true - break - } - } - - doc += d.Text() - doc = strings.ReplaceAll(doc, "\n", " ") - doc = strings.ReplaceAll(doc, "+required", "") - doc = strings.ReplaceAll(doc, "+optional", "") - } - - if !noDoc { - defaultVal, _ := v.Default() - valueBytes, _ := defaultVal.MarshalJSON() - valueType := strings.ReplaceAll(v.IncompleteKind().String(), "|", "\\|") - - value := strings.ReplaceAll(string(valueBytes), "\":", "\": ") - value = strings.ReplaceAll(value, "\":[", "\": [") - value = strings.ReplaceAll(value, "},", "}, ") - value = strings.ReplaceAll(value, "|", "\\|") - - field := strings.Replace(v.Path().String(), "timoni.instance.config.", "", 1) - match := labelDomain.FindStringSubmatch(field) - - row = append(row, fmt.Sprintf("`%s:`", strings.ReplaceAll(match[1], ".", ": ")+match[2])) - row = append(row, fmt.Sprintf("`%s`", valueType)) - row = append(row, fmt.Sprintf("`%s`", value)) - row = append(row, fmt.Sprintf("%s", doc)) - rows = append(rows, row) - } - - return true - } - - cfgValues.Walk(configDataInfo, nil) - return rows, nil -} diff --git a/schemas/timoni.sh/core/v1alpha1/image.cue b/schemas/timoni.sh/core/v1alpha1/image.cue index 1535ea43..8378bb08 100644 --- a/schemas/timoni.sh/core/v1alpha1/image.cue +++ b/schemas/timoni.sh/core/v1alpha1/image.cue @@ -30,6 +30,7 @@ import ( // Reference is the image address computed from repository, tag and digest // in the format [REPOSITORY]:[TAG]@[DIGEST]. + // +nodoc reference: string if digest != "" && tag != "" { diff --git a/schemas/timoni.sh/core/v1alpha1/metadata.cue b/schemas/timoni.sh/core/v1alpha1/metadata.cue index 188ff505..16e38afe 100644 --- a/schemas/timoni.sh/core/v1alpha1/metadata.cue +++ b/schemas/timoni.sh/core/v1alpha1/metadata.cue @@ -43,13 +43,17 @@ import "strings" // Standard Kubernetes labels: app name, version and managed-by. labels: { - (#StdLabelName): name - (#StdLabelVersion): #Version + // +nodoc + (#StdLabelName): name + // +nodoc + (#StdLabelVersion): #Version + // +nodoc (#StdLabelManagedBy): "timoni" } // LabelSelector selects Pods based on the app.kubernetes.io/name label. #LabelSelector: #Labels & { + // +nodoc (#StdLabelName): name } @@ -74,7 +78,10 @@ import "strings" namespace: #Meta.namespace labels: #Meta.labels - labels: (#StdLabelComponent): #Component + labels: { + // +nodoc + (#StdLabelComponent): #Component + } annotations?: #Annotations if #Meta.annotations != _|_ { @@ -84,8 +91,10 @@ import "strings" // LabelSelector selects Pods based on the app.kubernetes.io/name // and app.kubernetes.io/component labels. #LabelSelector: #Labels & { + // +nodoc (#StdLabelComponent): #Component - (#StdLabelName): #Meta.name + // +nodoc + (#StdLabelName): #Meta.name } } @@ -104,7 +113,10 @@ import "strings" name: #Meta.name + "-" + #Component labels: #Meta.labels - labels: (#StdLabelComponent): #Component + labels: { + // +nodoc + (#StdLabelComponent): #Component + } annotations?: #Annotations if #Meta.annotations != _|_ { @@ -114,7 +126,9 @@ import "strings" // LabelSelector selects Pods based on the app.kubernetes.io/name // and app.kubernetes.io/component labels. #LabelSelector: #Labels & { + // +nodoc (#StdLabelComponent): #Component - (#StdLabelName): #Meta.name + // +nodoc + (#StdLabelName): #Meta.name } } diff --git a/schemas/timoni.sh/core/v1alpha1/selector.cue b/schemas/timoni.sh/core/v1alpha1/selector.cue index 9c4f2384..87bd377e 100644 --- a/schemas/timoni.sh/core/v1alpha1/selector.cue +++ b/schemas/timoni.sh/core/v1alpha1/selector.cue @@ -15,5 +15,8 @@ package v1alpha1 labels: #Labels // Standard Kubernetes label: app name. - labels: (#StdLabelName): #Name + labels: { + // +nodoc + (#StdLabelName): #Name + } } diff --git a/schemas/timoni.sh/core/v1alpha1/semver.cue b/schemas/timoni.sh/core/v1alpha1/semver.cue index ecd1e397..14dc8ab8 100644 --- a/schemas/timoni.sh/core/v1alpha1/semver.cue +++ b/schemas/timoni.sh/core/v1alpha1/semver.cue @@ -21,9 +21,11 @@ import ( let minMajor = strconv.Atoi(strings.Split(#Minimum, ".")[0]) let minMinor = strconv.Atoi(strings.Split(#Minimum, ".")[1]) + // +nodoc major: int & >=minMajor major: strconv.Atoi(strings.Split(#Version, ".")[0]) + // +nodoc minor: int & >=minMinor minor: strconv.Atoi(strings.Split(#Version, ".")[1]) }