Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate a Markdown Table from a Module Configuration #318

Merged
merged 7 commits into from
Jan 25, 2024

Conversation

Nalum
Copy link
Contributor

@Nalum Nalum commented Jan 14, 2024

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

Fix: #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 stefanprodan added area/cli CLI related issues and pull requests area/engine CUE engine related issues and pull requests labels Jan 14, 2024
@stefanprodan
Copy link
Owner

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
Copy link
Contributor Author

Nalum commented Jan 14, 2024

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
Copy link
Contributor Author

Nalum commented Jan 14, 2024

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
Copy link
Contributor Author

Nalum commented Jan 14, 2024

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
Copy link
Contributor Author

Nalum commented Jan 14, 2024

Should I put Description before Default?

@stefanprodan
Copy link
Owner

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
Copy link
Contributor Author

Nalum commented Jan 14, 2024

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

@stefanprodan
Copy link
Owner

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
Copy link
Owner

@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
Copy link
Contributor Author

Nalum commented Jan 15, 2024

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

@Nalum Nalum marked this pull request as ready for review January 15, 2024 15:04
@Nalum
Copy link
Contributor Author

Nalum commented Jan 22, 2024

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

@stefanprodan
Copy link
Owner

I would go with timoni mod show config

Add description column and format list/struct as single line of JSON
Run MarshalJSON for all values
Add field comment to the description column
Remove +optional and +required from doc strings
Format field path in cue syntax and add spaces to json defaults to allow the string to break across multiple lines

Signed-off-by: Luke Mallon (Nalum) <[email protected]>
The output flag will take a file name and read the file line by line,
write each line to a new temporary file. If the file is a markdown file
we are looking for a table which will be replaced with the new table
generated from the module configuration data. If a table is not found or
the file is not markdown then the generated table is appended to the end
of the file. The temporary file is then used to replace the existing
file.

Add logic to look for specific markdown header and only work with the table under that
Better replace for json structure
Fix formatting for types with pipe values
Use string replace on correct variable

Signed-off-by: Luke Mallon (Nalum) <[email protected]>
Test reading and writing to the output file

Signed-off-by: Luke Mallon (Nalum) <[email protected]>
@Nalum Nalum marked this pull request as draft January 23, 2024 10:54
@Nalum
Copy link
Contributor Author

Nalum commented Jan 24, 2024

@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.

if strings.HasSuffix(strings.TrimSpace(d.Text()), "+nodoc") {
noDoc = true
break
} else if strings.HasSuffix(strings.TrimSpace(d.Text()), "+required") {
required = true
}

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.

@Nalum Nalum marked this pull request as ready for review January 25, 2024 10:58
Copy link
Owner

@stefanprodan stefanprodan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please set the copyright year to 2024 for the mod_show Go files please.

Signed-off-by: Luke Mallon (Nalum) <[email protected]>
Signed-off-by: Luke Mallon (Nalum) <[email protected]>
Signed-off-by: Luke Mallon (Nalum) <[email protected]>
Copy link
Owner

@stefanprodan stefanprodan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Thanks @Nalum awesome contribution 🏅

@stefanprodan stefanprodan merged commit 8f40b1a into stefanprodan:main Jan 25, 2024
4 checks passed
@Nalum Nalum deleted the config-to-markdown branch January 25, 2024 13:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/cli CLI related issues and pull requests area/engine CUE engine related issues and pull requests
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Update Readme file with values available in the module
2 participants