diff --git a/modules/workload/base/common.k b/modules/workload/base/common.k new file mode 100644 index 0000000..baf0f0d --- /dev/null +++ b/modules/workload/base/common.k @@ -0,0 +1,36 @@ +import container as c +import secret as sec +import kam.v1.workload as wl + +schema WorkloadBase(wl.Workload): + """ WorkloadBase defines set of attributes shared by different workload profile, e.g Service + and Job. You can inherit this Schema to reuse these common attributes. + + Attributes + ---------- + containers: {str:c.Container}, default is Undefined, required. + Containers defines the templates of containers to be ran. + More info: https://kubernetes.io/docs/concepts/containers + secrets: {str:sec.Secret}, default is Undefined, optional. + Secrets can be used to store small amount of sensitive data e.g. password, token. + replicas: int, optional. + Number of container replicas based on this configuration that should be ran. + labels: {str:str}, default is Undefined, optional. + Labels are key/value pairs that are attached to the workload. + annotations: {str:str}, default is Undefined, optional. + Annotations are key/value pairs that attach arbitrary non-identifying metadata to the workload. + """ + + # The templates of containers to be ran. + containers: {str:c.Container} + + # Secrets store small amount of sensitive data e.g. a password, a token, or a key. + secrets?: {str:sec.Secret} + + # The number of containers that should be ran. + replicas?: int + + ###### Other metadata info + # Labels and annotations can be used to attach arbitrary metadata as key-value pairs to resources. + labels?: {str:str} + annotations?: {str:str} \ No newline at end of file diff --git a/modules/workload/base/container/container.k b/modules/workload/base/container/container.k new file mode 100644 index 0000000..f4988fc --- /dev/null +++ b/modules/workload/base/container/container.k @@ -0,0 +1,146 @@ +import container.probe as p +import container.lifecycle as lc + +import regex + +schema Container: + """ Container describes how the Application's tasks are expected to be run. Depending on + the replicas parameter 1 or more containers can be created from each template. + + Attributes + ---------- + image: str, default is Undefined, required. + Image refers to the Docker image name to run for this container. + More info: https://kubernetes.io/docs/concepts/containers/images + command: [str], default is Undefined, optional. + Entrypoint array. Not executed within a shell. + Command will overwrite the ENTRYPOINT value set in the Dockfile, otherwise the Docker + image's ENTRYPOINT is used if this is not provided. + args: [str], default is Undefined, optional. + Arguments to the entrypoint. + Args will overwrite the CMD value set in the Dockfile, otherwise the Docker + image's CMD is used if this is not provided. + env: {str:str}, default is Undefined, optional. + List of environment variables to set in the container. + The value of the environment variable may be static text or a value from a secret. + workingDir: str, default is Undefined, optional. + The working directory of the running process defined in entrypoint. + Default container runtime will be used if this is not specified. + resources: {str:str}, default is Undefined, optional. + Map of resource requirements the container should run with. + The resources parameter is a dict with the key being the resource name and the value being + the resource value. + files: {str:FileSpec}, default is Undefined, optional. + List of files to create in the container. + The files parameter is a dict with the key being the file name in the container and the value + being the target file specification. + dirs: {str:str}, default is Undefined, optional. + Collection of volumes mount into the container's filesystem. + The dirs parameter is a dict with the key being the folder name in the container and the value + being the referenced volume. + livenessProbe: p.Probe, default is Undefined, optional. + LivenessProbe indicates if a running process is healthy. + Container will be restarted if the probe fails. + readinessProbe: p.Probe, default is Undefined, optional. + ReadinessProbe indicates whether an application is available to handle requests. + startupProbe: p.Probe, default is Undefined, optional. + StartupProbe indicates that the container has started for the first time. + Container will be restarted if the probe fails. + lifecycle: lc.Lifecycle, default is Undefined, optional. + Lifecycle refers to actions that the management system should take in response to container lifecycle events. + + Examples + -------- + import catalog.workload.container as c + + web = c.Container { + image: "nginx:latest" + command: ["/bin/sh", "-c", "echo hi"] + env: { + "name": "value" + } + resources: { + "cpu": "2" + "memory": "4Gi" + } + } + """ + + # Image to run for this container. + image: str + + # Entrypoint array. + # The image's ENTRYPOINT is used if this is not provided. + command?: [str] + # Arguments to the entrypoint. + # The image's CMD is used if this is not provided. + args?: [str] + # Collection of environment variables to set in the container. + # The value of environment variable may be static text or a value from a secret. + env?: {str:str} + # The current working directory of the running process defined in entrypoint. + workingDir?: str + + # Resource requirements for this container. + resources?: {str:str} + + # Files configures one or more files to be created in the container. + files?: {str:FileSpec} + # Dirs configures one or more volumes to be mounted to the specified folder. + dirs?: {str:str} + + # Liveness probe for this container. + # Liveness probe indicates if a running process is healthy. + livenessProbe?: p.Probe + # Readiness probe for this container. + # Readiness probe indicates whether an application is available to handle requests. + readinessProbe?: p.Probe + # Startup probe for this container. + # Startup probe indicates that the container has started for the first time. + startupProbe?: p.Probe + + # Lifecycle configures actions which should be taken response to container lifecycle + # events. + lifecycle?: lc.Lifecycle + + check: + all e in env { + regex.match(e, r"^[-._a-zA-Z][-._a-zA-Z0-9]*$") + } if env, "a valid environment variable name must consist of alphabetic characters, digits, '_', '-', or '.', and must not start with a digit" + +schema FileSpec: + """ FileSpec defines the target file in a Container. + + Attributes + ---------- + content: str, default is Undefined, optional. + File content in plain text. + contentFrom: str, default is Undefined, optional. + Source for the file content, reference to a secret of configmap value. + mode: str, default is Undefined, optional. + Mode bits used to set permissions on this file, must be an octal value + between 0000 and 0777 or a decimal value between 0 and 511 + + Examples + -------- + import catalog.workload.container as c + + tmpFile = c.FileSpec { + content: "some file contents" + mode: "0777" + } + """ + + # The content of target file in plain text. + content?: str + + # Source for the file content, might be a reference to a secret value. + contentFrom?: str + + # Mode bits used to set permissions on this file. + # Defaults to 0644. + mode: str = "0644" + + check: + not content or not contentFrom, "content and contentFrom are mutually exclusive" + regex.match(mode, r"^[0-7]{3,4}$"), "valid mode must between 0000 and 0777, both inclusive" diff --git a/modules/workload/base/container/lifecycle/lifecycle.k b/modules/workload/base/container/lifecycle/lifecycle.k new file mode 100644 index 0000000..8331876 --- /dev/null +++ b/modules/workload/base/container/lifecycle/lifecycle.k @@ -0,0 +1,36 @@ +import container.probe as p + +schema Lifecycle: + """ Lifecycle describes actions that the management system should take in response + to container lifecycle events. + + Attributes + ---------- + preStop: p.Exec | p.Http, default is Undefined, optional. + The action to be taken before a container is terminated due to an API request or + management event such as liveness/startup probe failure, preemption, resource contention, etc. + More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks + postStart: p.Exec | p.Http, default is Undefined, optional. + The action to be taken after a container is created. + More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks + + Examples + -------- + import catalog.workload.container.probe as p + import catalog.workload.container.lifecycle as lc + + lifecycleHook = lc.Lifecycle { + preStop: p.Exec { + command: ["preStop.sh"] + } + postStart: p.Http { + url: "http://localhost:80" + } + } + """ + + # The action to be taken before a container is terminated. + preStop?: p.Exec | p.Http + + # The action to be taken after a container is created. + postStart?: p.Exec | p.Http diff --git a/modules/workload/base/container/probe/probe.k b/modules/workload/base/container/probe/probe.k new file mode 100644 index 0000000..457dc40 --- /dev/null +++ b/modules/workload/base/container/probe/probe.k @@ -0,0 +1,142 @@ +import regex + +schema Probe: + """ Probe describes a health check to be performed against a container to determine whether it is + alive or ready to receive traffic. There are three probe types: readiness, liveness, and startup. + + Attributes + ---------- + probeHandler: Exec | Http | Tcp, default is Undefined, required. + The action taken to determine the alive or health of a container + initialDelaySeconds: int, default is Undefined, optional. + The number of seconds before health checking is activated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + timeoutSeconds: int, default is Undefined, optional. + The number of seconds after which the probe times out. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes + periodSeconds: int, default is Undefined, optional. + How often (in seconds) to perform the probe. + successThreshold: int, default is Undefined, optional. + Minimum consecutive successes for the probe to be considered successful after having failed. + failureThreshold: int, default is Undefined, optional. + Minimum consecutive failures for the probe to be considered failed after having succeeded. + terminationGracePeriod: int, default is Undefined, optional. + Duration in seconds before terminate gracefully upon probe failure. + + Examples + -------- + import catalog.workload.container.probe as p + + probe = p.Probe { + probeHandler: p.Http { + path: "/healthz" + } + initialDelaySeconds: 10 + } + """ + + # The action taken to determine the health of a container + probeHandler: Exec | Http | Tcp + + # Number of seconds after the container has started before liveness probes are initiated. + initialDelaySeconds?: int + + # Number of seconds after which the probe times out. + timeoutSeconds?: int + + # How often (in seconds) to perform the probe. + periodSeconds?: int + + # Minimum consecutive successes for the probe to be considered successful after having failed. + successThreshold?: int + + # Minimum consecutive failures for the probe to be considered failed after having succeeded. + failureThreshold?: int + + # Duration in seconds before terminate gracefully upon probe failure. + terminationGracePeriod?: int + + check: + initialDelaySeconds >= 0 if initialDelaySeconds, "initialDelaySeconds must be greater than or equal to 0" + timeoutSeconds >= 0 if timeoutSeconds, "timeoutSeconds must be greater than or equal to 0" + periodSeconds >= 0 if periodSeconds, "periodSeconds must be greater than or equal to 0" + successThreshold >= 0 if successThreshold, "successThreshold must be greater than or equal to 0" + failureThreshold >= 0 if failureThreshold, "failureThreshold must be greater than or equal to 0" + terminationGracePeriod >= 0 if terminationGracePeriod, "terminationGracePeriod must be greater than or equal to 0" + +schema Exec: + """ Exec describes a "run in container" action. + + Attributes + ---------- + command: [str], default is Undefined, required. + The command line to execute inside the container. + + Examples + -------- + import catalog.workload.container.probe as p + + execProbe = p.Exec { + command: ["probe.sh"] + } + """ + + # The command line to execute inside the container. + # Exit status of 0 is treated as live/healthy and non-zero is unhealthy. + command: [str] + + check: + len(command) > 0, "command must be specified" + +schema Http: + """ Http describes an action based on HTTP Get requests. + + Attributes + ---------- + url: str, default is Undefined, required. + The full qualified url to send HTTP requests. + headers: {str:str}, default is Undefined, optional. + Collection of custom headers to set in the request + + Examples + -------- + import catalog.workload.container.probe as p + + httpProbe = p.Http { + url: "http://localhost:80" + headers: { + "X-HEADER": "VALUE" + } + } + """ + + # The full qualified url to send HTTP requests. + url: str + + # Custom headers to set in the request. + headers?: {str:str} + + check: + all header in headers { + regex.match(header, r"^[-A-Za-z0-9]+$") + } if headers, "a valid HTTP header must consist of alphanumeric characters or '-' e.g X-Header-Name" + +schema Tcp: + """ Tcp describes an action based on opening a socket. + + Attributes + ---------- + url: str, default is Undefined, required. + The full qualified url to open a socket. + + Examples + -------- + import catalog.workload.container.probe as p + + tcpProbe = p.Tcp { + url: "tcp://localhost:1234" + } + """ + + # The full qualified url to open a socket. + url: str diff --git a/modules/workload/base/kcl.mod b/modules/workload/base/kcl.mod new file mode 100644 index 0000000..36bc882 --- /dev/null +++ b/modules/workload/base/kcl.mod @@ -0,0 +1,6 @@ +[package] +name = "workload_base" +version = "0.1.0-beta" + +[dependencies] +kam = { git = "https://github.com/KusionStack/kam.git", tag = "0.2.0-beta" } diff --git a/modules/workload/base/kcl.mod.lock b/modules/workload/base/kcl.mod.lock new file mode 100644 index 0000000..8cb39cf --- /dev/null +++ b/modules/workload/base/kcl.mod.lock @@ -0,0 +1,8 @@ +[dependencies] + [dependencies.kam] + name = "kam" + full_name = "kam_0.2.0-beta" + version = "0.2.0-beta" + sum = "SktUX5eD9/fp4gQJc1fyXy5kxAFPEChMKlMe0pn2y9Y=" + url = "https://github.com/KusionStack/kam.git" + git_tag = "0.2.0-beta" diff --git a/modules/workload/base/secret/secret.k b/modules/workload/base/secret/secret.k new file mode 100644 index 0000000..b72f059 --- /dev/null +++ b/modules/workload/base/secret/secret.k @@ -0,0 +1,59 @@ +import regex + +# mapping between secret type and valid data key +SECRET_TYPE_DATA_MAPPING: {str:[str]} = { + "basic": ["username", "password"] + "token": ["token"] + "certificate": ["tls.crt", "tls.key"] + # empty array means no pre-defined data keys + "opaque": [] + "external": [] +} + +schema Secret: + """ Secrets are used to provide data that is considered sensitive like passwords, API keys, + TLS certificates, tokens or other credentials. + + Attributes + ---------- + type: str, default is Undefined, required. + Type of secret, used to facilitate programmatic handling of secret data. + params: {str:str}, default is Undefined, optional. + Collection of parameters used to facilitate programmatic handling of secret data. + data: {str:str}, default is Undefined, optional. + Data contains the non-binary secret data in string form. + immutable: bool, default is Undefined, optional. + Immutable, if set to true, ensures that data stored in the Secret cannot be updated. + + Examples + -------- + import catalog.workload.secret as sec + + basicAuth = sec.Secret { + type: "basic" + data: { + "username": "" + "password": "" + } + } + """ + + # Types of secrets available to use. + type: "basic" | "token" | "opaque" | "certificate" | "external" + + # Params defines extra parameters used to customize secret handling. + params?: {str:str} + + # Data defines the keys and data that will be used by secret. + data?: {str:str} + + # If immutable set to true, ensures that data stored in the Secret cannot be updated. + immutable?: bool + + check: + all k in data { + regex.match(k, r"[A-Za-z0-9_.-]*") + } if data, "a valid secret data key must consist of alphanumeric characters, '-', '_' or '.'" + all k in data { + k in SECRET_TYPE_DATA_MAPPING[type] if len(SECRET_TYPE_DATA_MAPPING[type]) > 0 + } if data, "a valid secret data key name must be one of ${SECRET_TYPE_DATA_MAPPING[type]} for ${type} type secret" \ No newline at end of file diff --git a/modules/workload/job/job.k b/modules/workload/job/job.k new file mode 100644 index 0000000..cbb7fee --- /dev/null +++ b/modules/workload/job/job.k @@ -0,0 +1,32 @@ +import workload_base.common as c + +schema Job(c.WorkloadBase): + """ Job is a kind of workload profile that describes how to run your application code. This + is typically used for tasks that take from a few seconds to a few days to complete. + + Attributes + ---------- + schedule: str, default is Undefined, required. + The scheduling strategy in Cron format. More info: https://en.wikipedia.org/wiki/Cron. + + Examples + -------- + Instantiate a job with busybox image and runs every hour + + import catalog.workload as wl + import catalog.workload.container as c + + echoJob : wl.Job { + containers: { + "busybox": c.Container{ + image: "busybox:1.28" + command: ["/bin/sh", "-c", "echo hello"] + } + } + schedule: "0 * * * *" + } + """ + + # The scheduling strategy in Cron format. + # More info: https://en.wikipedia.org/wiki/Cron. + schedule: str diff --git a/modules/workload/job/kcl.mod b/modules/workload/job/kcl.mod new file mode 100644 index 0000000..bc5e2fe --- /dev/null +++ b/modules/workload/job/kcl.mod @@ -0,0 +1,7 @@ +[package] +name = "job" +version = "0.1.0-beta" + +[dependencies] +workload_base = { oci = "oci://ghcr.io/kusionstack/workload_base", tag = "0.1.0-beta" } +kam = { git = "https://github.com/KusionStack/kam.git", tag = "0.2.0-beta" } \ No newline at end of file diff --git a/modules/workload/job/kcl.mod.lock b/modules/workload/job/kcl.mod.lock new file mode 100644 index 0000000..4facbcd --- /dev/null +++ b/modules/workload/job/kcl.mod.lock @@ -0,0 +1,16 @@ +[dependencies] + [dependencies.kam] + name = "kam" + full_name = "kam_0.2.0-beta" + version = "0.2.0-beta" + sum = "SktUX5eD9/fp4gQJc1fyXy5kxAFPEChMKlMe0pn2y9Y=" + url = "https://github.com/KusionStack/kam.git" + git_tag = "0.2.0-beta" + [dependencies.workload_base] + name = "workload_base" + full_name = "workload_base_0.1.0-beta" + version = "0.1.0-beta" + sum = "ja0SPn10+m4wlu0jWcBI5wksAy/01MraZnyToan6p6s=" + reg = "ghcr.io" + repo = "kusionstack/workload_base" + oci_tag = "0.1.0-beta" diff --git a/modules/workload/service/kcl.mod b/modules/workload/service/kcl.mod new file mode 100644 index 0000000..6e8974c --- /dev/null +++ b/modules/workload/service/kcl.mod @@ -0,0 +1,7 @@ +[package] +name = "service" +version = "0.1.0-beta" + +[dependencies] +workload_base = { oci = "oci://ghcr.io/kusionstack/workload_base", tag = "0.1.0-beta" } +kam = { git = "https://github.com/KusionStack/kam.git", tag = "0.2.0-beta" } \ No newline at end of file diff --git a/modules/workload/service/kcl.mod.lock b/modules/workload/service/kcl.mod.lock new file mode 100644 index 0000000..4facbcd --- /dev/null +++ b/modules/workload/service/kcl.mod.lock @@ -0,0 +1,16 @@ +[dependencies] + [dependencies.kam] + name = "kam" + full_name = "kam_0.2.0-beta" + version = "0.2.0-beta" + sum = "SktUX5eD9/fp4gQJc1fyXy5kxAFPEChMKlMe0pn2y9Y=" + url = "https://github.com/KusionStack/kam.git" + git_tag = "0.2.0-beta" + [dependencies.workload_base] + name = "workload_base" + full_name = "workload_base_0.1.0-beta" + version = "0.1.0-beta" + sum = "ja0SPn10+m4wlu0jWcBI5wksAy/01MraZnyToan6p6s=" + reg = "ghcr.io" + repo = "kusionstack/workload_base" + oci_tag = "0.1.0-beta" diff --git a/modules/workload/service/service.k b/modules/workload/service/service.k new file mode 100644 index 0000000..cc2a7c1 --- /dev/null +++ b/modules/workload/service/service.k @@ -0,0 +1,24 @@ +import workload_base.common as c + +schema Service(c.WorkloadBase): + """ Service is a kind of workload profile that describes how to run your application code. This + is typically used for long-running web applications that should "never" go down, and handle + short-lived latency-sensitive web requests, or events. + + Attributes + ---------- + + Examples + -------- + # Instantiate a long-running service and its image is "nginx:v1" + + import catalog.workload.base.container as c + + nginxSvc : Service { + containers: { + "nginx": c.Container { + image: "nginx:v1" + } + } + } + """ \ No newline at end of file