diff --git a/Makefile b/Makefile index 01885ca..1d295bb 100644 --- a/Makefile +++ b/Makefile @@ -97,4 +97,4 @@ swagger: .PHONY: timoni-build timoni-build: - @timoni build podinfo ./timoni/podinfo -f ./timoni/podinfo/test_values.cue + @timoni build podinfo ./timoni/podinfo -f ./timoni/podinfo/debug_values.cue diff --git a/timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/image.cue b/timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/image.cue index 3c6b93c..ec500aa 100644 --- a/timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/image.cue +++ b/timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/image.cue @@ -3,7 +3,10 @@ package v1alpha1 -import "strings" +import ( + "encoding/base64" + "strings" +) // Image defines the schema for OCI image reference used in Kubernetes PodSpec container image. #Image: { @@ -22,6 +25,10 @@ import "strings" // Spec: https://github.com/opencontainers/image-spec/blob/main/descriptor.md#digests. digest!: string + // PullPolicy defines the pull policy for the image. + // By default, it is set to IfNotPresent. + pullPolicy: *"IfNotPresent" | "Always" | "Never" + // Reference is the image address computed from repository, tag and digest // in the format [REPOSITORY]:[TAG]@[DIGEST]. reference: string @@ -42,3 +49,53 @@ import "strings" reference: "\(repository):latest" } } + +// ImagePullSecret is a generator for Kubernetes Secrets of type kubernetes.io/dockerconfigjson. +// Spec: https://kubernetes.io/docs/concepts/configuration/secret/#docker-config-secrets. +#ImagePullSecret: { + // Metadata is the Kubernetes object's metadata generated by Timoni. + meta=metadata: #Metadata + + // Registry is the hostname of the container registry in the format [HOST[:PORT_NUMBER]]. + registry!: string + + // Username is the username used to authenticate to the container registry. + username!: string + + // Password is the password used to authenticate to the container registry. + password!: string + + // Optional suffix used to generate the Secret name. + suffix: *"" | string + + let auth = base64.Encode(null, username+":"+password) + + // The object is a read-only struct that contains the generated + // Kubernetes Secret of type kubernetes.io/dockerconfigjson. + object: { + apiVersion: "v1" + kind: "Secret" + type: "kubernetes.io/dockerconfigjson" + metadata: { + name: meta.name + suffix + namespace: meta.namespace + labels: meta.labels + if meta.annotations != _|_ { + annotations: meta.annotations + } + } + stringData: { + ".dockerconfigjson": #""" + { + "auths": { + "\#(registry)": { + "username": "\#(username)", + "password": "\#(password)", + "auth": "\#(auth)" + } + } + } + """# + } + } +} diff --git a/timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/instance.cue b/timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/instance.cue new file mode 100644 index 0000000..ad96b06 --- /dev/null +++ b/timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/instance.cue @@ -0,0 +1,27 @@ +// Copyright 2023 Stefan Prodan +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import "strings" + +// InstanceName defines the schema for the name of a Timoni instance. +// The instance name is used as a Kubernetes label value and must be 63 characters or less. +#InstanceName: string & =~"^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$" & strings.MinRunes(1) & strings.MaxRunes(63) + +// InstanceNamespace defines the schema for the namespace of a Timoni instance. +// The instance namespace is used as a Kubernetes label value and must be 63 characters or less. +#InstanceNamespace: string & =~"^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$" & strings.MinRunes(1) & strings.MaxRunes(63) + +// InstanceOwnerReference defines the schema for Kubernetes labels used to denote ownership. +#InstanceOwnerReference: { + #Name: "instance.timoni.sh/name" + #Namespace: "instance.timoni.sh/namespace" +} + +// InstanceModule defines the schema for the Module of a Timoni instance. +#InstanceModule: { + url: string & =~"^((oci|file)://.*)$" + version: *"latest" | string + digest?: string +} diff --git a/timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/metadata.cue b/timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/metadata.cue index 2b6cfe8..bcb7e43 100644 --- a/timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/metadata.cue +++ b/timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/metadata.cue @@ -5,6 +5,19 @@ package v1alpha1 import "strings" +// Annotations defines the schema for Kubernetes object metadata annotations. +#Annotations: {[string & strings.MaxRunes(253)]: string} + +// Labels defines the schema for Kubernetes object metadata labels. +#Labels: {[string & strings.MaxRunes(253)]: string & =~"^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$" & strings.MaxRunes(63)} + +#StdLabelName: "app.kubernetes.io/name" +#StdLabelVersion: "app.kubernetes.io/version" +#StdLabelPartOf: "app.kubernetes.io/part-of" +#StdLabelManagedBy: "app.kubernetes.io/managed-by" +#StdLabelComponent: "app.kubernetes.io/component" +#StdLabelInstance: "app.kubernetes.io/instance" + // Metadata defines the schema for Kubernetes object metadata. #Metadata: { // Version should be in the strict semver format. Is required when creating resources. @@ -13,24 +26,90 @@ import "strings" // 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 - name!: string & =~"^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$" & strings.MaxRunes(63) + name!: #InstanceName // Namespace defines the space within which each name must be unique. // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces - namespace!: string & =~"^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$" & strings.MaxRunes(63) + namespace!: #InstanceNamespace // 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 - annotations?: {[string & =~"^(([A-Za-z0-9][-A-Za-z0-9_./]*)?[A-Za-z0-9])?$" & strings.MaxRunes(63)]: string} + annotations?: #Annotations // 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 - labels: {[string & =~"^(([A-Za-z0-9][-A-Za-z0-9_./]*)?[A-Za-z0-9])?$" & strings.MaxRunes(63)]: string & =~"^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$" & strings.MaxRunes(63)} + labels: #Labels - // Standard Kubernetes labels: app name and version. + // Standard Kubernetes labels: app name, version and managed-by. labels: { - "app.kubernetes.io/name": name - "app.kubernetes.io/version": #Version + "\(#StdLabelName)": name + "\(#StdLabelVersion)": #Version + "\(#StdLabelManagedBy)": "timoni" + } + + // LabelSelector selects Pods based on the app.kubernetes.io/name label. + #LabelSelector: #Labels & { + "\(#StdLabelName)": name + } +} + +// MetaComponent generates the Kubernetes object metadata for a module namespaced component. +// The metadata.name is composed of the instance name and the component name. +// The metadata.labels contain the app.kubernetes.io/component label. +#MetaComponent: { + // Meta is the Kubernetes object's metadata generated by Timoni. + #Meta!: #Metadata + + // Component is the name of the component used + // as a suffix for the generate object name. + #Component!: string & strings.MaxRunes(30) + + name: #Meta.name + "-" + #Component + namespace: #Meta.namespace + + labels: #Meta.labels + labels: "\(#StdLabelComponent)": #Component + + annotations?: #Annotations + if #Meta.annotations != _|_ { + annotations: #Meta.annotations + } + + // LabelSelector selects Pods based on the app.kubernetes.io/name + // and app.kubernetes.io/component labels. + #LabelSelector: #Labels & { + "\(#StdLabelComponent)": #Component + "\(#StdLabelName)": #Meta.name + } +} + +// MetaClusterComponent generates the Kubernetes object metadata for a module non-namespaced component. +// The metadata.name is composed of the instance name and the component name. +// The metadata.namespace is unset. +// The metadata.labels contain the app.kubernetes.io/component label. +#MetaClusterComponent: { + // Meta is the Kubernetes object's metadata generated by Timoni. + #Meta!: #Metadata + + // Component is the name of the component used + // as a suffix for the generate object name. + #Component!: string & strings.MaxRunes(30) + + name: #Meta.name + "-" + #Component + + labels: #Meta.labels + labels: "\(#StdLabelComponent)": #Component + + annotations?: #Annotations + if #Meta.annotations != _|_ { + annotations: #Meta.annotations + } + + // LabelSelector selects Pods based on the app.kubernetes.io/name + // and app.kubernetes.io/component labels. + #LabelSelector: #Labels & { + "\(#StdLabelComponent)": #Component + "\(#StdLabelName)": #Meta.name } } diff --git a/timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/requirements.cue b/timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/requirements.cue new file mode 100644 index 0000000..d3b5573 --- /dev/null +++ b/timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/requirements.cue @@ -0,0 +1,40 @@ +// Copyright 2023 Stefan Prodan +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + "strconv" + "strings" +) + +// CPUQuantity is a string that is validated as a quantity of CPU, such as 100m or 2000m. +#CPUQuantity: string & =~"^[1-9]\\d*m$" + +// MemoryQuantity is a string that is validated as a quantity of memory, such as 128Mi or 2Gi. +#MemoryQuantity: string & =~"^[1-9]\\d*(Mi|Gi)$" + +// ResourceRequirement defines the schema for the CPU and Memory resource requirements. +#ResourceRequirement: { + cpu?: #CPUQuantity + memory?: #MemoryQuantity +} + +// ResourceRequirements defines the schema for the compute resource requirements of a container. +// More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/. +#ResourceRequirements: { + // Limits describes the maximum amount of compute resources allowed. + limits?: #ResourceRequirement + + // Requests describes the minimum amount of compute resources required. + // Requests cannot exceed Limits. + requests?: #ResourceRequirement & { + if limits != _|_ { + if limits.cpu != _|_ { + _lc: strconv.Atoi(strings.Split(limits.cpu, "m")[0]) + _rc: strconv.Atoi(strings.Split(requests.cpu, "m")[0]) + #cpu: int & >=_rc & _lc + } + } + } +} diff --git a/timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/selector.cue b/timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/selector.cue index 3e8306b..ed141f4 100644 --- a/timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/selector.cue +++ b/timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/selector.cue @@ -3,19 +3,17 @@ package v1alpha1 -import "strings" - // Selector defines the schema for Kubernetes Pod label selector used in Deployments, Services, Jobs, etc. #Selector: { // 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 - #Name!: string & =~"^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$" & strings.MinRunes(1) & strings.MaxRunes(63) + #Name!: #InstanceName // 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 - labels: {[string & =~"^(([A-Za-z0-9][-A-Za-z0-9_./]*)?[A-Za-z0-9])?$" & strings.MaxRunes(63)]: string & =~"^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?$" & strings.MaxRunes(63)} + labels: #Labels // Standard Kubernetes label: app name. - labels: "app.kubernetes.io/name": #Name + labels: "\(#StdLabelName)": #Name } diff --git a/timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/semver.cue b/timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/semver.cue new file mode 100644 index 0000000..ecd1e39 --- /dev/null +++ b/timoni/podinfo/cue.mod/pkg/timoni.sh/core/v1alpha1/semver.cue @@ -0,0 +1,29 @@ +// Copyright 2023 Stefan Prodan +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + "strconv" + "strings" +) + +// SemVer validates the input version string and extracts the major and minor version numbers. +// When Minimum is set, the major and minor parts must be greater or equal to the minimum +// or a validation error is returned. +#SemVer: { + // Input version string in strict semver format. + #Version!: string & =~"^\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$" + + // Minimum is the minimum allowed MAJOR.MINOR version. + #Minimum: *"0.0.0" | string & =~"^\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$" + + let minMajor = strconv.Atoi(strings.Split(#Minimum, ".")[0]) + let minMinor = strconv.Atoi(strings.Split(#Minimum, ".")[1]) + + major: int & >=minMajor + major: strconv.Atoi(strings.Split(#Version, ".")[0]) + + minor: int & >=minMinor + minor: strconv.Atoi(strings.Split(#Version, ".")[1]) +} diff --git a/timoni/podinfo/templates/config.cue b/timoni/podinfo/templates/config.cue index 5f05f32..8d4554d 100644 --- a/timoni/podinfo/templates/config.cue +++ b/timoni/podinfo/templates/config.cue @@ -7,45 +7,70 @@ import ( // Config defines the schema and defaults for the Instance values. #Config: { - // UI setting + // Podinfo optional UI setting. ui: { color: *"#34577c" | string message?: string backend?: string } - // Runtime version info + // Runtime version info automatically set at apply-time. moduleVersion!: string kubeVersion!: string - // Metadata (common to all resources) + // The minimum Kubernetes version to 1.20. + clusterVersion: timoniv1.#SemVer & {#Version: kubeVersion, #Minimum: "1.20.0"} + + // Kubernetes metadata common to all resources. metadata: timoniv1.#Metadata & {#Version: moduleVersion} - // Label selector (common to all resources) + // Label selector common to all resources. selector: timoniv1.#Selector & {#Name: metadata.name} - // Deployment + // The number of pods replicas. + // By default, the number of replicas is 1. replicas: *1 | int & >=0 - // Pod - podAnnotations?: {[ string]: string} + // The image allows setting the container image repository, + // tag, digest and pull policy. + // The default image repository and tag is set in `values.cue`. + image!: timoniv1.#Image + + // The resources allows setting the container resource requirements. + // By default, the container requests 10m CPU and 32Mi memory. + resources: timoniv1.#ResourceRequirements & { + requests: { + cpu: *"10m" | timoniv1.#CPUQuantity + memory: *"32Mi" | timoniv1.#MemoryQuantity + } + } + + // The securityContext allows setting the container security context. + securityContext?: corev1.#SecurityContext + + // Pod optinal settings. + podAnnotations?: {[string]: string} podSecurityContext?: corev1.#PodSecurityContext imagePullSecrets?: [...corev1.LocalObjectReference] - tolerations?: [ ...corev1.#Toleration] - affinity?: corev1.#Affinity + tolerations?: [...corev1.#Toleration] topologySpreadConstraints?: [...corev1.#TopologySpreadConstraint] - // Container - image: timoniv1.#Image - imagePullPolicy: *"IfNotPresent" | string - resources?: corev1.#ResourceRequirements - securityContext?: corev1.#SecurityContext + // Pod affinity rules, by default, pods are scheduled on Linux nodes. + affinity: *{ + nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: [{ + matchExpressions: [{ + key: corev1.#LabelOSStable + operator: "In" + values: ["linux"] + }] + }] + } | corev1.#Affinity // Service service: { - port: *80 | int & >0 & <=65535 - annotations?: {[ string]: string} - labels?: {[ string]: string} + port: *80 | int & >0 & <=65535 + annotations?: timoniv1.#Annotations + labels?: timoniv1.#Labels } // HorizontalPodAutoscaler (optional) @@ -59,12 +84,12 @@ import ( // Ingress (optional) ingress: { - enabled: *false | bool - tls: *false | bool - host: *"podinfo.local" | string - annotations?: {[ string]: string} - labels?: {[ string]: string} - className?: string + enabled: *false | bool + tls: *false | bool + host: *"podinfo.local" | string + className?: string + annotations?: timoniv1.#Annotations + labels?: timoniv1.#Labels } // ServiceMonitor (optional) @@ -91,9 +116,9 @@ import ( config: #Config objects: { - "\(config.metadata.name)-sa": #ServiceAccount & {_config: config} - "\(config.metadata.name)-svc": #Service & {_config: config} - "\(config.metadata.name)-deploy": #Deployment & {_config: config} + "\(config.metadata.name)-sa": #ServiceAccount & {_config: config} + "\(config.metadata.name)-svc": #Service & {_config: config} + "\(config.metadata.name)-deploy": #Deployment & {_config: config} if config.autoscaling.enabled { "\(config.metadata.name)-hpa": #HorizontalPodAutoscaler & {_config: config} diff --git a/timoni/podinfo/templates/deployment.cue b/timoni/podinfo/templates/deployment.cue index bf75f40..05ff1ea 100644 --- a/timoni/podinfo/templates/deployment.cue +++ b/timoni/podinfo/templates/deployment.cue @@ -10,7 +10,7 @@ import ( apiVersion: "apps/v1" kind: "Deployment" metadata: _config.metadata - spec: appsv1.#DeploymentSpec & { + spec: appsv1.#DeploymentSpec & { if !_config.autoscaling.enabled { replicas: _config.replicas } @@ -38,7 +38,7 @@ import ( { name: _config.metadata.name image: _config.image.reference - imagePullPolicy: _config.imagePullPolicy + imagePullPolicy: _config.image.pullPolicy ports: [ { name: "http" diff --git a/timoni/podinfo/templates/job.cue b/timoni/podinfo/templates/job.cue index e19593a..75eeea6 100644 --- a/timoni/podinfo/templates/job.cue +++ b/timoni/podinfo/templates/job.cue @@ -13,20 +13,20 @@ import ( _config: #Config apiVersion: "batch/v1" kind: "Job" - metadata: name: "\(_config.metadata.name)-test" - metadata: namespace: _config.metadata.namespace - metadata: labels: _config.metadata.labels + metadata: timoniv1.#MetaComponent & { + #Meta: _config.metadata + #Component: "test" + } metadata: annotations: timoniv1.Action.Force spec: batchv1.#JobSpec & { template: corev1.#PodTemplateSpec & { - metadata: labels: _config.metadata.labels let _checksum = uuid.SHA1(uuid.ns.DNS, yaml.Marshal(_config)) metadata: annotations: "timoni.sh/checksum": "\(_checksum)" spec: { containers: [{ name: "curl" image: _config.test.image.reference - imagePullPolicy: _config.imagePullPolicy + imagePullPolicy: _config.test.image.pullPolicy command: [ "curl", "-v", diff --git a/timoni/podinfo/timoni.cue b/timoni/podinfo/timoni.cue index d4e3079..ac3559b 100644 --- a/timoni/podinfo/timoni.cue +++ b/timoni/podinfo/timoni.cue @@ -5,9 +5,6 @@ package main import ( - "strconv" - "strings" - templates "timoni.sh/podinfo/templates" ) @@ -39,16 +36,12 @@ timoni: { } } - // Enforce minimum Kubernetes version. - kubeMinorVersion: int & >=23 - kubeMinorVersion: strconv.Atoi(strings.Split(instance.config.kubeVersion, ".")[1]) - // Pass Kubernetes resources outputted by the instance // to Timoni's multi-step apply. - apply: app: [ for obj in instance.objects {obj}] + apply: app: [for obj in instance.objects {obj}] // Conditionally run tests after an install or upgrade. if instance.config.test.enabled { - apply: test: [ for obj in instance.tests {obj}] + apply: test: [for obj in instance.tests {obj}] } }