stefanprodan / timoni

Timoni is a package manager for Kubernetes, powered by CUE and inspired by Helm.
https://timoni.sh
Apache License 2.0
1.53k stars 68 forks source link

Generate a Markdown Table from a Module Configuration #318

Closed Nalum closed 8 months ago

Nalum commented 8 months ago

This adds support for creating a markdown table generated from the configuration cue structure in a module with timoni mod show config.

Fix: https://github.com/stefanprodan/timoni/issues/168

The command will output the table to os.Stdout by default, however you can provide a file name to the flag -o or --output where the table will be written to. If the file is markdown (i.e. has the file extention .md or .markdown) we look for the heading ## Configuration and then a table within this heading which will be replaced. If the file is not markdown the table will be appended to the file.

The below is an example of output generated from the blueprint module and you can look at the cert-manager-module README for an example of something with more complexity.

KEY TYPE DEFAULT DESCRIPTION
timoni: instance: config: struct {"kubeVersion": "1.27.5","clusterVersion": {"major": 1,"minor": 27}, "moduleVersion": "0.0.0-devel","metadata": {"name": "module-name","namespace": "default","labels": {"app.kubernetes.io/name": "module-name","app.kubernetes.io/version": "0.0.0-devel","app.kubernetes.io/managed-by": "timoni"}}, "selector": {"labels": {"app.kubernetes.io/name": "module-name"}}, "image": {"repository": "docker.io/nginx","tag": "1-alpine","digest": "","pullPolicy": "IfNotPresent","reference": "docker.io/nginx:1-alpine"}, "pod": {"affinity": {"nodeAffinity": {"requiredDuringSchedulingIgnoredDuringExecution": {"nodeSelectorTerms": [{"matchExpressions": [{"key": "kubernetes.io/os","operator": "In","values": ["linux"]}]}]}}}}, "resources": {"requests": {"cpu": "10m","memory": "32Mi"}}, "replicas": 1,"securityContext": {"capabilities": {"drop": ["ALL"],"add": ["CHOWN","NET_BIND_SERVICE","SETGID","SETUID"]}, "privileged": false,"allowPrivilegeEscalation": false}, "service": {"port": 80}} The user-supplied values are merged with the default values at runtime by Timoni. These values are injected at runtime by Timoni.
kubeVersion: string "1.27.5" The kubeVersion is a required field, set at apply-time via timoni.cue by querying the user's Kubernetes API.
clusterVersion: struct {"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.
clusterVersion: major: int 1
clusterVersion: minor: int 27
moduleVersion: string "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: struct {"name": "module-name","namespace": "default","labels": {"app.kubernetes.io/name": "module-name","app.kubernetes.io/version": "0.0.0-devel","app.kubernetes.io/managed-by": "timoni"}} The Kubernetes metadata common to all resources. The metadata.name and metadata.namespace fields are set from the user-supplied instance name and namespace.
metadata: name: string "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: string "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: labels: struct {"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: labels: "app.kubernetes.io/name": string "module-name"
metadata: labels: "app.kubernetes.io/version": string "0.0.0-devel"
metadata: labels: "app.kubernetes.io/managed-by": string "timoni"
selector: struct {"labels": {"app.kubernetes.io/name": "module-name"}} The selector allows adding label selectors to Deployments and Services. The app.kubernetes.io/name label selector is automatically generated from the instance name and can't be overwritten.
selector: labels: struct {"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.
selector: labels: "app.kubernetes.io/name": string "module-name"
image: struct {"repository": "docker.io/nginx","tag": "1-alpine","digest": "","pullPolicy": "IfNotPresent","reference": "docker.io/nginx:1-alpine"} The image allows setting the container image repository, tag, digest and pull policy.
image: repository: string "docker.io/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: string "1-alpine" 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: string "IfNotPresent" PullPolicy defines the pull policy for the image. By default, it is set to IfNotPresent.
image: reference: string "docker.io/nginx:1-alpine" Reference is the image address computed from repository, tag and digest in the format [REPOSITORY]:[TAG]@[DIGEST].
pod: struct {"affinity": {"nodeAffinity": {"requiredDuringSchedulingIgnoredDuringExecution": {"nodeSelectorTerms": [{"matchExpressions": [{"key": "kubernetes.io/os","operator": "In","values": ["linux"]}]}]}}}} The pod allows setting the Kubernetes Pod annotations, image pull secrets, affinity and anti-affinity rules. By default, pods are scheduled on Linux nodes.
pod: affinity: struct {"nodeAffinity": {"requiredDuringSchedulingIgnoredDuringExecution": {"nodeSelectorTerms": [{"matchExpressions": [{"key": "kubernetes.io/os","operator": "In","values": ["linux"]}]}]}}}
resources: struct {"requests": {"cpu": "10m","memory": "32Mi"}} The resources allows setting the container resource requirements. By default, the container requests 10m CPU and 32Mi memory.
resources: requests: struct {"cpu": "10m","memory": "32Mi"} Requests describes the minimum amount of compute resources required. Requests cannot exceed Limits.
resources: requests: cpu: string "10m"
resources: requests: memory: string "32Mi"
replicas: int 1 The number of pods replicas. By default, the number of replicas is 1.
securityContext: struct {"capabilities": {"drop": ["ALL"],"add": ["CHOWN","NET_BIND_SERVICE","SETGID","SETUID"]}, "privileged": false,"allowPrivilegeEscalation": false} The securityContext allows setting the container security context. By default, the container is denined privilege escalation.
securityContext: capabilities: struct {"drop": ["ALL"],"add": ["CHOWN","NET_BIND_SERVICE","SETGID","SETUID"]} The capabilities to add/drop when running containers. Defaults to the default set of capabilities granted by the container runtime. Note that this field cannot be set when spec.os.name is windows.
securityContext: capabilities: drop: list ["ALL"] Removed capabilities
securityContext: capabilities: add: list ["CHOWN","NET_BIND_SERVICE","SETGID","SETUID"] Added capabilities
securityContext: privileged: bool false 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: allowPrivilegeEscalation: bool false 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.
service: struct {"port": 80} The service allows setting the Kubernetes Service annotations and port. By default, the HTTP port is 80.
service: port: int 80
stefanprodan commented 8 months ago

I've tried to render the markdown from your example and looks like the multi-line values break into rows. Maybe we need to wrap the multi-line into code block "````"?

Nalum commented 8 months ago

Yeah, I'll try fix that shortly. I was in the process of getting it into a single line, will try the code block syntax.

Nalum commented 8 months ago

Using a code block didn't work, still split across multiple lines. But marshaling to a single line of JSON works, not very readable though...

| FIELD                                                                    | TYPE           | DEFAULT                                                                                                                                                                                                                                                                                                                                                 | DESCRIPTION |
|--------------------------------------------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|
| `logLevel`                                                               | `int`          | `2`                                                                                                                                                                                                                                                                                                                                                     |             |
| `rbac.enabled`                                                           | `bool`         | `true`                                                                                                                                                                                                                                                                                                                                                  |             |
| `controller.monitoring.serviceMonitor.targetPort`                        | `(int|string)` | `http-metrics`                                                                                                                                                                                                                                                                                                                                          |             |
| `controller.volumeMounts`                                                | `list`         | `[{"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount","name":"serviceaccount-token","readOnly":true}]`                                                                                                                                                                                                                                         |             |
| `controller.volumes`                                                     | `list`         | `[{"name":"serviceaccount-token","projected":{"defaultMode":444,"sources":[{"serviceAccountToken":{"expirationSeconds":3607,"path":"token"}},{"configMap":{"name":"kube-root-ca.crt","items":[{"key":"ca.crt","path":"ca.crt"}]}},{"downwardAPI":{"items":[{"path":"namespace","fieldRef":{"apiVersion":"v1","fieldPath":"metadata.namespace"}}]}}]}}]` |             |
| `webhook.hostNetwork`                                                    | `bool`         | `false`                                                                                                                                                                                                                                                                                                                                                 |             |
| `webhook.securePort`                                                     | `int`          | `10250`                                                                                                                                                                                                                                                                                                                                                 |             |
| `webhook.timeoutSeconds`                                                 | `int`          | `10`                                                                                                                                                                                                                                                                                                                                                    |             |
| `webhook.image.repository`                                               | `string`       | `quay.io/jetstack/cert-manager-webhook`                                                                                                                                                                                                                                                                                                                 |             |
| `webhook.image.tag`                                                      | `string`       | `v1.13.2`                                                                                                                                                                                                                                                                                                                                               |             |
| `webhook.image.digest`                                                   | `string`       | `sha256:0a9470447ebf1d3ff1c172e19268be12dc26125ff83320d456f6826c677c0ed2`                                                                                                                                                                                                                                                                               |             |
| `webhook.image.pullPolicy`                                               | `string`       | `IfNotPresent`                                                                                                                                                                                                                                                                                                                                          |             |
| `webhook.networkPolicy`                                                  | `struct`       | `{"ingress":[{"from":[{"ipBlock":{"cidr":"0.0.0.0/0"}}]}],"egress":[{"ports":[{"port":80,"protocol":"TCP"},{"port":443,"protocol":"TCP"},{"port":53,"protocol":"TCP"},{"port":53,"protocol":"UDP"},{"port":6443,"protocol":"TCP"}],"to":[{"ipBlock":{"cidr":"0.0.0.0/0"}}]}]}`                                                                          |             |
| `webhook.volumeMounts`                                                   | `list`         | `[{"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount","name":"serviceaccount-token","readOnly":true}]`                                                                                                                                                                                                                                         |             |
| `webhook.volumes`                                                        | `list`         | `[{"name":"serviceaccount-token","projected":{"defaultMode":444,"sources":[{"serviceAccountToken":{"expirationSeconds":3607,"path":"token"}},{"configMap":{"name":"kube-root-ca.crt","items":[{"key":"ca.crt","path":"ca.crt"}]}},{"downwardAPI":{"items":[{"path":"namespace","fieldRef":{"apiVersion":"v1","fieldPath":"metadata.namespace"}}]}}]}}]` |             |
| `caInjector.automountServiceAccountToken`                                | `bool`         | `false`                                                                                                                                                                                                                                                                                                                                                 |             |
| `test.startupAPICheck.replicas`                                          | `int`          | `1`                                                                                                                                                                                                                                                                                                                                                     |             |
| `test.startupAPICheck.securityContext.runAsNonRoot`                      | `bool`         | `true`                                                                                                                                                                                                                                                                                                                                                  |             |
| `test.startupAPICheck.securityContext.seccompProfile.type`               | `string`       | `RuntimeDefault`                                                                                                                                                                                                                                                                                                                                        |             |
| `test.startupAPICheck.serviceAccount.automountServiceAccountToken`       | `bool`         | `false`                                                                                                                                                                                                                                                                                                                                                 |             |
| `test.startupAPICheck.service.type`                                      | `string`       | `ClusterIP`                                                                                                                                                                                                                                                                                                                                             |             |
| `test.startupAPICheck.volumeMounts`                                      | `list`         | `[{"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount","name":"serviceaccount-token","readOnly":true}]`                                                                                                                                                                                                                                         |             |
| `test.startupAPICheck.volumes`                                           | `list`         | `[{"name":"serviceaccount-token","projected":{"defaultMode":444,"sources":[{"serviceAccountToken":{"expirationSeconds":3607,"path":"token"}},{"configMap":{"name":"kube-root-ca.crt","items":[{"key":"ca.crt","path":"ca.crt"}]}},{"downwardAPI":{"items":[{"path":"namespace","fieldRef":{"apiVersion":"v1","fieldPath":"metadata.namespace"}}]}}]}}]` |             |

In the field names do we want to have them separated by : as they would be in cue? e.g. test: startupAPICheck: service: type:

Nalum commented 8 months ago

Figured out how to pull the comment associated with a config field:

FIELD TYPE DEFAULT DESCRIPTION
test.enabled bool true
test.startupAPICheck.backoffLimit int 4
test.startupAPICheck.automountServiceAccountToken bool false automountServiceAccountToken indicates whether a service account token should be automatically mounted.
test.startupAPICheck.containerSecurityContext.capabilities.drop list ["ALL"] Removed capabilities +optional
test.startupAPICheck.containerSecurityContext.runAsNonRoot bool true 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. +optional
test.startupAPICheck.containerSecurityContext.readOnlyRootFilesystem bool true 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. +optional
test.startupAPICheck.containerSecurityContext.allowPrivilegeEscalation bool false 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. +optional
test.startupAPICheck.enableServiceLinks bool false enableServiceLinks indicates whether information about services should be injected into pod's environment variables, matching the syntax of Docker links.
test.startupAPICheck.image.repository string "quay.io/jetstack/cert-manager-ctl" 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.startupAPICheck.image.tag string "v1.13.2" 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.startupAPICheck.image.digest string "sha256:4d9fce2c050eaadabedac997d9bd4a003341e9172c3f48fae299d94fa5f03435" Digest uniquely and immutably identifies an image in the repository. Spec: https://github.com/opencontainers/image-spec/blob/main/descriptor.md#digests.
test.startupAPICheck.image.pullPolicy string "IfNotPresent" PullPolicy defines the pull policy for the image. By default, it is set to IfNotPresent.
test.startupAPICheck.timeout string "1m" Timeout for 'kubectl check api' command
Nalum commented 8 months ago

Should I put Description before Default?

stefanprodan commented 8 months ago

I think we should be using : as separat for fields so it's valid CUE. Also let's remove +optional and +required if these are in a dedicated line comment.

Nalum commented 8 months ago

Should this also be updated to have -o (markdown|yaml|json)?

stefanprodan commented 8 months ago

I would go for --output README.md and when a .md file is specified, I would replace the existing table if it exists at the end of the doc, if not, I would append it.

stefanprodan commented 8 months ago

@Nalum can you please update the PR description and put there not in code blocks, but as markdown the cert-manager table in its latest form.

Nalum commented 8 months ago

@stefanprodan will do, just going to update the logic a bit more.

Nalum commented 8 months ago

@stefanprodan are you happy with the command structure, timoni mod config, or do you have something else you'd prefer?

stefanprodan commented 8 months ago

I would go with timoni mod show config

Nalum commented 8 months ago

@stefanprodan I have updated this so now you can set +nodoc and +required, ~however I cannot identify these as a separate dedicated line so have used strings.HasSuffix:~ ~Actually I could look for \n+nodoc\n$ or \n+required\n$...~ scratch that, I've found another way.

https://github.com/stefanprodan/timoni/blob/ae652b01c53bce60f6f8c173992758409460bf57/internal/engine/module_builder.go#L328-L333

I've also had to update the timoni core api files and the timoni.cue file with +nodoc comments to hide certain fields as they were not being hidden by adding it to the #Config in templates. I'm happy to apply this to where needed in this PR or another if you're happy with this.