nyl 0.2.2__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. {nyl-0.2.2 → nyl-0.4.0}/.github/workflows/python.yaml +5 -0
  2. {nyl-0.2.2 → nyl-0.4.0}/PKG-INFO +7 -1
  3. {nyl-0.2.2 → nyl-0.4.0}/README.md +6 -0
  4. {nyl-0.2.2 → nyl-0.4.0}/argocd-cmp/Dockerfile +15 -2
  5. {nyl-0.2.2 → nyl-0.4.0}/docs/content/reference/configuration/profiles.md +21 -4
  6. nyl-0.4.0/docs/content/reference/glossary.md +27 -0
  7. nyl-0.2.2/docs/content/reference/templating/inlining.md → nyl-0.4.0/docs/content/reference/templating/inlining/helmcharts.md +5 -12
  8. nyl-0.4.0/docs/content/reference/templating/inlining/overview.md +10 -0
  9. nyl-0.4.0/docs/content/reference/templating/inlining/postprocessor.md +120 -0
  10. {nyl-0.2.2 → nyl-0.4.0}/pyproject.toml +1 -1
  11. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/commands/template.py +6 -0
  12. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/generator/__init__.py +9 -3
  13. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/generator/dispatch_test.py +4 -0
  14. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/generator/helmchart.py +1 -1
  15. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/profiles/config.py +14 -1
  16. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/profiles/kubeconfig.py +11 -2
  17. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/resources/placeholder.py +0 -32
  18. nyl-0.4.0/src/nyl/resources/postprocessor.py +128 -0
  19. nyl-0.4.0/src/nyl/resources/postprocessor_test.py +133 -0
  20. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/tools/kubernetes.py +16 -0
  21. {nyl-0.2.2 → nyl-0.4.0}/.envrc +0 -0
  22. {nyl-0.2.2 → nyl-0.4.0}/.github/workflows/docker.yaml +0 -0
  23. {nyl-0.2.2 → nyl-0.4.0}/.github/workflows/docs.yaml +0 -0
  24. {nyl-0.2.2 → nyl-0.4.0}/.gitignore +0 -0
  25. {nyl-0.2.2 → nyl-0.4.0}/.kraken.py +0 -0
  26. {nyl-0.2.2 → nyl-0.4.0}/argocd-cmp/README.md +0 -0
  27. {nyl-0.2.2 → nyl-0.4.0}/argocd-cmp/plugin.yaml +0 -0
  28. {nyl-0.2.2 → nyl-0.4.0}/argocd-with-nyl/.envrc +0 -0
  29. {nyl-0.2.2 → nyl-0.4.0}/argocd-with-nyl/.secrets.yaml +0 -0
  30. {nyl-0.2.2 → nyl-0.4.0}/argocd-with-nyl/.sops.yaml +0 -0
  31. {nyl-0.2.2 → nyl-0.4.0}/argocd-with-nyl/README.md +0 -0
  32. {nyl-0.2.2 → nyl-0.4.0}/argocd-with-nyl/argocd.yaml +0 -0
  33. {nyl-0.2.2 → nyl-0.4.0}/argocd-with-nyl/nyl-project.yaml +0 -0
  34. {nyl-0.2.2 → nyl-0.4.0}/argocd-with-nyl/nyl-secrets.yaml +0 -0
  35. {nyl-0.2.2 → nyl-0.4.0}/docs/content/index.md +0 -0
  36. {nyl-0.2.2 → nyl-0.4.0}/docs/content/reference/applysets.md +0 -0
  37. {nyl-0.2.2 → nyl-0.4.0}/docs/content/reference/argocd-plugin.md +0 -0
  38. {nyl-0.2.2 → nyl-0.4.0}/docs/content/reference/cluster-connectivity.md +0 -0
  39. {nyl-0.2.2 → nyl-0.4.0}/docs/content/reference/configuration/environment.md +0 -0
  40. {nyl-0.2.2 → nyl-0.4.0}/docs/content/reference/configuration/projects.md +0 -0
  41. {nyl-0.2.2 → nyl-0.4.0}/docs/content/reference/configuration/secrets.md +0 -0
  42. {nyl-0.2.2 → nyl-0.4.0}/docs/content/reference/configuration-api/profiles.md +0 -0
  43. {nyl-0.2.2 → nyl-0.4.0}/docs/content/reference/configuration-api/secrets.md +0 -0
  44. {nyl-0.2.2 → nyl-0.4.0}/docs/content/reference/templating/basics.md +0 -0
  45. {nyl-0.2.2 → nyl-0.4.0}/docs/content/reference/templating/components.md +0 -0
  46. {nyl-0.2.2 → nyl-0.4.0}/docs/content/reference/templating/secrets.md +0 -0
  47. {nyl-0.2.2 → nyl-0.4.0}/docs/mkdocs.yml +0 -0
  48. {nyl-0.2.2 → nyl-0.4.0}/docs/pyproject.toml +0 -0
  49. {nyl-0.2.2 → nyl-0.4.0}/docs/uv.lock +0 -0
  50. {nyl-0.2.2 → nyl-0.4.0}/example/manifests/postgres.yaml +0 -0
  51. {nyl-0.2.2 → nyl-0.4.0}/example/nyl-profiles.yaml +0 -0
  52. {nyl-0.2.2 → nyl-0.4.0}/example/nyl-secrets.yaml +0 -0
  53. {nyl-0.2.2 → nyl-0.4.0}/example/secrets.yaml +0 -0
  54. {nyl-0.2.2 → nyl-0.4.0}/src/kubernetes/README.md +0 -0
  55. {nyl-0.2.2 → nyl-0.4.0}/src/kubernetes/__init__.pyi +0 -0
  56. {nyl-0.2.2 → nyl-0.4.0}/src/kubernetes/dynamic/__init__.pyi +0 -0
  57. {nyl-0.2.2 → nyl-0.4.0}/src/kubernetes/dynamic/client.pyi +0 -0
  58. {nyl-0.2.2 → nyl-0.4.0}/src/kubernetes/dynamic/discovery.pyi +0 -0
  59. {nyl-0.2.2 → nyl-0.4.0}/src/kubernetes/dynamic/exceptions.pyi +0 -0
  60. {nyl-0.2.2 → nyl-0.4.0}/src/kubernetes/dynamic/resource.pyi +0 -0
  61. {nyl-0.2.2 → nyl-0.4.0}/src/kubernetes/watch/__init__.pyi +0 -0
  62. {nyl-0.2.2 → nyl-0.4.0}/src/kubernetes/watch/watch.pyi +0 -0
  63. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/__init__.py +0 -0
  64. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/commands/__init__.py +0 -0
  65. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/commands/argocd.py +0 -0
  66. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/commands/crds.py +0 -0
  67. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/commands/new.py +0 -0
  68. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/commands/profile.py +0 -0
  69. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/commands/run.py +0 -0
  70. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/commands/secrets.py +0 -0
  71. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/commands/tools/__init__.py +0 -0
  72. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/commands/tools/bcrypt.py +0 -0
  73. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/commands/tools/sops.py +0 -0
  74. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/commands/tun.py +0 -0
  75. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/generator/components.py +0 -0
  76. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/generator/dispatch.py +0 -0
  77. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/generator/statefulsecret.py +0 -0
  78. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/profiles/__init__.py +0 -0
  79. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/profiles/config_test.py +0 -0
  80. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/profiles/tunnel.py +0 -0
  81. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/project/config.py +0 -0
  82. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/resources/__init__.py +0 -0
  83. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/resources/applyset.py +0 -0
  84. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/resources/applyset_test.py +0 -0
  85. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/resources/helmchart.py +0 -0
  86. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/resources/statefulsecret.py +0 -0
  87. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/secrets/__init__.py +0 -0
  88. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/secrets/config.py +0 -0
  89. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/secrets/config_test.py +0 -0
  90. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/secrets/null.py +0 -0
  91. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/secrets/sops.py +0 -0
  92. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/secrets/sops_test.py +0 -0
  93. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/templating.py +0 -0
  94. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/templating_test.py +0 -0
  95. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/tools/fs.py +0 -0
  96. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/tools/fs_test.py +0 -0
  97. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/tools/kubectl.py +0 -0
  98. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/tools/kvstore.py +0 -0
  99. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/tools/loads.py +0 -0
  100. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/tools/logging.py +0 -0
  101. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/tools/shell.py +0 -0
  102. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/tools/testing.py +0 -0
  103. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/tools/typer.py +0 -0
  104. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/tools/types.py +0 -0
  105. {nyl-0.2.2 → nyl-0.4.0}/src/nyl/tools/yaml.py +0 -0
  106. {nyl-0.2.2 → nyl-0.4.0}/tests/__init__.py +0 -0
  107. {nyl-0.2.2 → nyl-0.4.0}/tests/conftest.py +0 -0
  108. {nyl-0.2.2 → nyl-0.4.0}/uv.lock +0 -0
@@ -38,6 +38,11 @@ jobs:
38
38
  - name: Run ${{ matrix.step }}
39
39
  run: krakenw run fmt lint
40
40
 
41
+ - name: Install the Kyverno CLI (required for some Nyl features)
42
+ uses: kyverno/action-install-cli@v0.2.0
43
+ with:
44
+ release: 'v1.13.0'
45
+
41
46
  # TODO: Until krakenw run test works (currently fails because of doctest modules)
42
47
  - name: Run tests
43
48
  run: uv run pytest -vv
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nyl
3
- Version: 0.2.2
3
+ Version: 0.4.0
4
4
  Summary: Advanced templating engine for Kubernetes and GitOps.
5
5
  Author-email: Niklas Rosenstein <rosensteinniklas@gmail.com>
6
6
  License: MIT
@@ -31,6 +31,12 @@ Requires Python 3.11 or newer.
31
31
 
32
32
  $ uvx nyl
33
33
 
34
+ For some features, additional programs must be available:
35
+
36
+ * [kubectl](https://kubernetes.io/de/docs/reference/kubectl/) for most operations
37
+ * [kyverno](https://kyverno.io/docs/kyverno-cli/) when using the Nyl `PostProcessor` resource
38
+ * [sops](https://github.com/getsops/sops) when using the SOPS secrets provider
39
+
34
40
  ## Local development
35
41
 
36
42
  Install the project with [Uv](https://docs.astral.sh/uv/).
@@ -9,6 +9,12 @@ Requires Python 3.11 or newer.
9
9
 
10
10
  $ uvx nyl
11
11
 
12
+ For some features, additional programs must be available:
13
+
14
+ * [kubectl](https://kubernetes.io/de/docs/reference/kubectl/) for most operations
15
+ * [kyverno](https://kyverno.io/docs/kyverno-cli/) when using the Nyl `PostProcessor` resource
16
+ * [sops](https://github.com/getsops/sops) when using the SOPS secrets provider
17
+
12
18
  ## Local development
13
19
 
14
20
  Install the project with [Uv](https://docs.astral.sh/uv/).
@@ -29,7 +29,7 @@ RUN \
29
29
  case "$(uname -m)" in \
30
30
  x86_64) BIN_ARCH='amd64' ;; \
31
31
  aarch64) BIN_ARCH='arm64' ;; \
32
- *) echo "Unsupported architecture: $ARCH" && exit 1 ;; \
32
+ *) echo "Unsupported architecture: $(uname -m)" && exit 1 ;; \
33
33
  esac \
34
34
  && printf "%s" "$BIN_ARCH" > /BINARY_ARCH
35
35
 
@@ -57,12 +57,24 @@ RUN curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/s
57
57
  FROM fetch-bin-base AS sops-bin
58
58
  # Find SOPS releases at https://github.com/getsops/sops/releases
59
59
  ARG SOPS_VERSION=v3.9.0
60
- RUN curl -LO https://github.com/getsops/sops/releases/download/${SOPS_VERSION}/sops-${SOPS_VERSION}.linux.$(cat /BINARY_ARCH) \
60
+ RUN curl -fLO https://github.com/getsops/sops/releases/download/${SOPS_VERSION}/sops-${SOPS_VERSION}.linux.$(cat /BINARY_ARCH) \
61
61
  && mv sops-${SOPS_VERSION}.linux.$(cat /BINARY_ARCH) /usr/local/bin/sops \
62
62
  && chmod +x /usr/local/bin/sops \
63
63
  && upx /usr/local/bin/sops \
64
64
  && sops --version
65
65
 
66
+ FROM fetch-bin-base AS kyverno-bin
67
+ ARG KYVERNO_VERSION=1.13.0
68
+ RUN \
69
+ case "$(uname -m)" in \
70
+ x86_64) KYVERNO_ARCH='x86_64' ;; \
71
+ aarch64) KYVERNO_ARCH='arm64' ;; \
72
+ *) echo "Unsupported architecture: $(uname -m)" && exit 1 ;; \
73
+ esac \
74
+ && curl -fLO https://github.com/kyverno/kyverno/releases/download/v${KYVERNO_VERSION}/kyverno-cli_v${KYVERNO_VERSION}_linux_${KYVERNO_ARCH}.tar.gz \
75
+ && tar -xvf kyverno-cli_v${KYVERNO_VERSION}_linux_${KYVERNO_ARCH}.tar.gz \
76
+ && mv kyverno /usr/local/bin/ \
77
+ && upx /usr/local/bin/kyverno
66
78
 
67
79
  #: Build the ArgoCD CMP server image.
68
80
  #: ----------------------------------
@@ -73,6 +85,7 @@ COPY --from=argocd-bin /usr/local/bin/argocd /usr/local/bin/argocd
73
85
  RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-cmp-server && chown 999:999 /usr/local/bin/argocd-cmp-server
74
86
  COPY --from=helm-bin /usr/local/bin/helm /usr/local/bin/helm
75
87
  COPY --from=sops-bin /usr/local/bin/sops /usr/local/bin/sops
88
+ COPY --from=kyverno-bin /usr/local/bin/kyverno /usr/local/bin/kyverno
76
89
  COPY --from=build /opt/nyl /opt/nyl
77
90
  RUN ln -s /opt/nyl/.venv/bin/nyl /usr/local/bin/nyl
78
91
  COPY --chown=999 argocd-cmp/plugin.yaml /home/argocd/cmp-server/config/plugin.yaml
@@ -45,9 +45,12 @@ machine and use that to connect to the Kubernetes cluster.
45
45
  ```toml title="nyl-profile.toml"
46
46
  [default.kubeconfig]
47
47
  type = "ssh"
48
+ sudo = false # default, set to true to use `sudo cat <path>`
48
49
  user = "root"
49
50
  host = "mycluster.example.com"
51
+ port = 22 # default
50
52
  path = "/etc/rancher/k3s/k3s.yaml"
53
+ # replace_apiserver_hostname
51
54
 
52
55
  [default.tunnel]
53
56
  type = "ssh"
@@ -61,9 +64,12 @@ machine and use that to connect to the Kubernetes cluster.
61
64
  default:
62
65
  kubeconfig:
63
66
  type: ssh
67
+ sudo: false
64
68
  user: root
65
69
  host: mycluster.example.com
70
+ port: 22
66
71
  path: /etc/rancher/k3s/k3s.yaml
72
+ replace_apiserver_hostname: null
67
73
  tunnel:
68
74
  type: ssh
69
75
  user: root
@@ -77,9 +83,12 @@ machine and use that to connect to the Kubernetes cluster.
77
83
  "default": {
78
84
  "kubeconfig": {
79
85
  "type": "ssh",
86
+ "sudo": false,
80
87
  "user": "root",
81
88
  "host": "mycluster.example.com",
82
- "path": "/etc/rancher/k3s/k3s.yaml"
89
+ "port": 22,
90
+ "path": "/etc/rancher/k3s/k3s.yaml",
91
+ "replace_apiserver_hostname": null
83
92
  },
84
93
  "tunnel": {
85
94
  "type": "ssh",
@@ -100,7 +109,7 @@ is reachable from your local machine.
100
109
  user = "root"
101
110
  host = "mycluster.example.com"
102
111
  path = "/etc/rancher/k3s/k3s.yaml"
103
- replace_apiserver_hostname = "mycluster.example.com"
112
+ replace_apiserver_hostname = "reachable-address.com"
104
113
  ```
105
114
 
106
115
  === "YAML"
@@ -112,7 +121,7 @@ is reachable from your local machine.
112
121
  user: root
113
122
  host: mycluster.example.com
114
123
  path: /etc/rancher/k3s/k3s.yaml
115
- replace_apiserver_hostname: mycluster.example.com
124
+ replace_apiserver_hostname: reachable-address.com
116
125
  ```
117
126
 
118
127
  === "JSON"
@@ -125,11 +134,19 @@ is reachable from your local machine.
125
134
  "user": "root",
126
135
  "host": "mycluster.example.com",
127
136
  "path": "/etc/rancher/k3s/k3s.yaml",
128
- "replace_apiserver_hostname": "mycluster.example.com"
137
+ "replace_apiserver_hostname": "reachable-address.com"
129
138
  }
130
139
  }
131
140
  ```
132
141
 
142
+ !!! note
143
+
144
+ When retrieving a Kubeconfig via SSH that has `127.0.0.1`, `0.0.0.0` or `localhost` as the hostname in the `server`
145
+ field, and `replace_apiserver_hostname` is not specified, Nyl will automatically replace it with the `host` field
146
+ specified that was used to execute the remote command to read the Kubeconfig. This is because we expect the two
147
+ hosts to be the same, and you cannot reach the Kubernetes API server through your local machine without using tunnel
148
+ (which you can use, see [Tunnel management](#tunnel-management)).
149
+
133
150
  If you are you are already setup with a kubeconfig file, you can specify the path to the file directly or have it
134
151
  automatically use your `~/.kube/config` file/`KUBECONFIG` environment variable. You may specify the context to use
135
152
  from that kubeconfig file which ensures Nyl interacts with the correct cluster, even if your
@@ -0,0 +1,27 @@
1
+ # Glossary
2
+
3
+ [Nyl-inline]: ./templating/inlining/overview.md
4
+
5
+ ### Manifest
6
+
7
+ A manifest is a file that may define zero or more Kubernetes or [Nyl inline resources][Nyl-inline]. Nyl understands
8
+ individual manifests, i.e. YAML files, and certain behaviours operate on that level, such as Kubernetes Namespace
9
+ auto-filling and resource post-processing applying only to all resources defined in the same manifest as the
10
+ [`PostProcessor`](./templating/inlining/postprocessor.md).
11
+
12
+ ### Profile
13
+
14
+ ...
15
+
16
+ ### Project
17
+
18
+ ...
19
+
20
+ ### Resource
21
+
22
+ A resource is a YAML document that follows the schema of a Kubernetes API resource or [Nyl inline resource][Nyl-inline].
23
+ An example would be a Kubernetes `ConfigMap`, `Deployment`, `Pod` or a Nyl `HelmChart` or `PostProcessor`.
24
+
25
+ ### Secrets provider
26
+
27
+ ...
@@ -2,19 +2,11 @@
2
2
  weight: 5
3
3
  ---
4
4
 
5
- # Inlining
5
+ # HelmChart
6
6
 
7
- Nyl supports inlining of Kubernetes resources generated from templates in your configuration. Resources that are
8
- inlined by Nyl are typically in the `inline.nyl.io/v1` API group, however Nyl also has the concept of
9
- [components](./components.md) which effectively allow you to define your own API groups and resource kinds for
10
- inlined resources.
11
-
12
- ## Helm charts
13
-
14
- Nyl currently only supports inlining of Helm charts. Helm charts are inlined by specifying a `HelmChart` resource in
15
- your configuration. Just like any other resource, the Helm chart resource values can be templated using Nyl's
16
- [structured templating](../templating/basics.md) (for example to inject secrets), and are then rendered to Kubernetes
17
- resources that are inlined in your configuration.
7
+ Nyl allows you to inline Helm charts by specifying a `HelmChart` resource in your manifest. Just like any other
8
+ resource, the Helm chart resource values can be templated using Nyl's [structured templating](../templating/basics.md)
9
+ (for example to inject secrets), and are then rendered to Kubernetes resources that are inlined in your configuration.
18
10
 
19
11
  __Example__
20
12
 
@@ -28,6 +20,7 @@ spec:
28
20
  chart:
29
21
  repository: https://charts.bitnami.com/bitnami
30
22
  name: nginx
23
+ # version: "..."
31
24
  values:
32
25
  controller:
33
26
  service:
@@ -0,0 +1,10 @@
1
+ ---
2
+ title: Overview
3
+ ---
4
+
5
+ # Inlining Overview
6
+
7
+ Nyl supports inlining of Kubernetes resources generated from templates in your configuration. Resources that are
8
+ inlined by Nyl are typically in the `inline.nyl.io/v1` API group, however Nyl also has the concept of
9
+ [components](./components.md) which effectively allow you to define your own API groups and resource kinds for
10
+ inlined resources.
@@ -0,0 +1,120 @@
1
+ # PostProcessor
2
+
3
+ The `inline.nyl.io/v1/PostProcessor` resource can be used to configure post-processing steps for all Kubernetes
4
+ resources defined in the same [Manifest](../../glossary.md#manifest) as the `PostProcessor`. This resource does
5
+ not accept a `metadata` field.
6
+
7
+ ## Prerequisites
8
+
9
+ * You need the [`kyverno` CLI](https://kyverno.io/docs/kyverno-cli/) installed
10
+
11
+ ## API Spec
12
+
13
+ ```yaml
14
+ apiVersion: inline.nyl.io/v1
15
+ kind: PostProcessor
16
+ spec:
17
+ kyverno:
18
+ # A list of files that each contain a Kyverno policy resource, usually a `ClusterPolicy`. The paths are first
19
+ # considered relative to the manifest that this resource is defined in, and will then be searched in the project
20
+ # search path.
21
+ policyFiles:
22
+ - path/to/policy.yaml
23
+
24
+ # A mapping of policy names (what you would give as a filename) and a YAML document that represents the Kyverno
25
+ # policy resource (usually a `ClusterPolicy`).
26
+ inlinePolicies:
27
+ my-policy:
28
+ apiVersion: kyverno.io/v1
29
+ kind: ClusterPolicy
30
+ metadata:
31
+ name: enforce-pod-security-context
32
+ spec: {} # ...
33
+ ```
34
+
35
+ ## Example
36
+
37
+ If you're deploying to hardened RKE2, your pods must have a specific `securityContext` configuration in order to be
38
+ allowed by the PodSecurityPolicy. The application's Helm charts that you deploy may have options to inject the
39
+ required options, but if they are not you're usually out of luck unless you fork the Helm chart, or materialize
40
+ the resources and edit them in your project.
41
+
42
+ With the Nyl `PostProcessor`, you can apply Kyverno policies to validate or mutate the resources in a
43
+ [manifest](../../glossary.md#manifest).
44
+
45
+ === "Manifest"
46
+
47
+ ```yaml title="forgejo.yaml"
48
+ ---
49
+ apiVersion: inline.nyl.io/v1
50
+ kind: PostProcessor
51
+ spec:
52
+ kyverno:
53
+ policyFiles:
54
+ - ./policies/security-profile.yaml
55
+
56
+ ---
57
+ apiVersion: v1
58
+ kind: Namespace
59
+ metadata:
60
+ name: forgejo
61
+
62
+ ---
63
+ apiVersion: inline.nyl.io/v1
64
+ kind: HelmChart
65
+ metadata:
66
+ name: forgejo
67
+ namespace: forgejo
68
+ spec:
69
+ chart:
70
+ repository: oci://code.forgejo.org/forgejo-helm # https://artifacthub.io/packages/helm/forgejo-helm/forgejo
71
+ name: forgejo
72
+ version: "10.0.1"
73
+ values: {}
74
+ ```
75
+
76
+ === "Kyverno Policy"
77
+
78
+ ```yaml title="policies/security-profile.yaml"
79
+ apiVersion: kyverno.io/v1
80
+ kind: ClusterPolicy
81
+ metadata:
82
+ name: enforce-security-context
83
+ spec:
84
+ validationFailureAction: enforce
85
+ rules:
86
+ - name: mutate-pod-security-context
87
+ match:
88
+ resources:
89
+ kinds:
90
+ - Pod
91
+ mutate:
92
+ patchStrategicMerge: &podSpec
93
+ spec:
94
+ securityContext:
95
+ runAsNonRoot: true
96
+ seccompProfile:
97
+ type: RuntimeDefault
98
+ containers: &containers
99
+ - (name): "*"
100
+ securityContext:
101
+ runAsNonRoot: true
102
+ allowPrivilegeEscalation: false
103
+ capabilities:
104
+ drop:
105
+ - "ALL"
106
+ initContainers: *containers
107
+ - name: mutate-deployment-security-context
108
+ match:
109
+ resources:
110
+ kinds:
111
+ - Deployment
112
+ mutate:
113
+ patchStrategicMerge:
114
+ spec:
115
+ template: *podSpec
116
+ ```
117
+
118
+ Running `nyl template forgejo.yaml` will use the `kyverno` CLI to apply the policy to the manifests generated by
119
+ the Helm chart. Note that the post processing happens at the very end after all other Kubernetes manifests have
120
+ been generated.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nyl"
7
- version = "0.2.2" # sed'ed on publish in CI
7
+ version = "0.4.0" # sed'ed on publish in CI
8
8
  description = "Advanced templating engine for Kubernetes and GitOps."
9
9
  authors = [
10
10
  {name = "Niklas Rosenstein", email = "rosensteinniklas@gmail.com"},
@@ -5,6 +5,7 @@ from pathlib import Path
5
5
  from textwrap import indent
6
6
  from typing import Optional
7
7
  from loguru import logger
8
+ from nyl.resources.postprocessor import PostProcessor
8
9
  from typer import Argument, Option
9
10
  from nyl.tools import yaml
10
11
  from nyl.generator import reconcile_generator
@@ -177,8 +178,13 @@ def template(
177
178
  generator,
178
179
  source.manifests,
179
180
  on_generated=lambda m: template_engine.evaluate(Manifests([m])),
181
+ skip_resources=[PostProcessor],
180
182
  )
181
183
 
184
+ # Find any PostProcessor resources and apply them.
185
+ source.manifests, post_processors = PostProcessor.extract_from_list(source.manifests)
186
+ source.manifests = PostProcessor.apply_all(source.manifests, post_processors, source.file)
187
+
182
188
  # Find the namespaces that are defined in the file. If we find any manifests without a namespace, we will
183
189
  # inject that namespace name into them. Also find the applyset defined in the file.
184
190
  namespaces: set[str] = set()
@@ -4,8 +4,9 @@ This package contains everything related to the generation of Kubernetes manifes
4
4
 
5
5
  from abc import ABC, abstractmethod
6
6
  from collections.abc import Callable
7
- from typing import Any, ClassVar, Generic, TypeVar
7
+ from typing import Any, ClassVar, Generic, Sequence, TypeVar
8
8
 
9
+ from nyl.resources import NylResource
9
10
  from stablehash import stablehash
10
11
 
11
12
  from nyl.tools.types import Manifest, Manifests
@@ -37,6 +38,7 @@ def reconcile_generator(
37
38
  generator: Generator[Manifest],
38
39
  manifests: Manifests,
39
40
  on_generated: Callable[[Manifest], Manifests],
41
+ skip_resources: Sequence[type[NylResource]] = (),
40
42
  ) -> Manifests:
41
43
  """
42
44
  Recursively reconcile all Nyl resources in the manifests using the given generator.
@@ -46,6 +48,7 @@ def reconcile_generator(
46
48
  manifests: The list of manifests to reconcile.
47
49
  on_generated: A callback to call on each generated manifest, giving the opportunity to modify it.
48
50
  This is used to pass the manifest through the `structured-templates` engine.
51
+ skip_resources: A list of Nyl resources to ignore.
49
52
  """
50
53
 
51
54
  queue = Manifests(manifests)
@@ -63,8 +66,11 @@ def reconcile_generator(
63
66
  result.append(resource)
64
67
  else:
65
68
  seen.add(resource_hash)
66
- for manifest in generator.generate(resource):
67
- queue.extend(on_generated(manifest))
69
+ if any(t.matches(resource) for t in skip_resources):
70
+ queue.append(resource)
71
+ else:
72
+ for manifest in generator.generate(resource):
73
+ queue.extend(on_generated(manifest))
68
74
  loops += 1
69
75
 
70
76
  return result
@@ -46,6 +46,10 @@ def test__DispatchingGenerator__default__creates_generator_for_every_nyl_inline_
46
46
  kube_api_versions=set(),
47
47
  )
48
48
 
49
+ # We make an exception for the PostProcessor resource as it has no generator.
50
+
49
51
  for kind in resource_kinds:
52
+ if kind == "PostProcessor":
53
+ continue
50
54
  assert kind in generator.generators.keys()
51
55
  assert isinstance(generator.generators[kind], Generator)
@@ -91,7 +91,7 @@ class HelmChartGenerator(Generator[HelmChart], resource_type=HelmChart):
91
91
  else:
92
92
  logger.opt(ansi=True).debug("Using cached Helm chart '{}' from repository '{}'", chart, repository)
93
93
 
94
- chart = str(cache_dir / next(cache_dir.iterdir()))
94
+ chart = str(next(cache_dir.iterdir()))
95
95
  repository = None
96
96
 
97
97
  elif chart_ref.git:
@@ -58,11 +58,21 @@ class KubeconfigFromSsh:
58
58
  The username to connect to the remote host with.
59
59
  """
60
60
 
61
+ sudo: bool = False
62
+ """
63
+ Use `sudo cat` to retrieve the file instead of ust `cat`.
64
+ """
65
+
61
66
  host: str
62
67
  """
63
68
  The remote host to connect to.
64
69
  """
65
70
 
71
+ port: int = 22
72
+ """
73
+ SSH port to use.
74
+ """
75
+
66
76
  identity_file: str | None = None
67
77
  """
68
78
  An SSH private key file to use for authentication.
@@ -82,7 +92,10 @@ class KubeconfigFromSsh:
82
92
  """
83
93
  Replace the hostname in the apiserver configuration of the Kubeconfig. This is useful for example with K3s when
84
94
  reading reading the `/etc/rancher/k3s/k3s.yaml` file from a remote host, but the API server in that file is not
85
- reachable from the local machine (e.g. because it `0.0.0.0`).
95
+ reachable from that machine.
96
+
97
+ Note that if the host in the retrieve Kubeconfig is `localhost`, `0.0.0.0` or `127.0.0.1`, it will be automatically
98
+ replaced with the specified `host` that was also SSH-ed to, unless this option is set.
86
99
  """
87
100
 
88
101
 
@@ -67,9 +67,13 @@ class KubeconfigManager:
67
67
 
68
68
  command = [
69
69
  "ssh",
70
+ "-p",
71
+ str(source.port),
70
72
  f"{source.user}@{source.host}",
71
73
  f"cat {shlex.quote(source.path)}",
72
74
  ]
75
+ if source.sudo:
76
+ command[4] = "sudo " + command[4]
73
77
  if source.identity_file:
74
78
  command[1:1] = ["-i", str(self._cwd / source.identity_file)]
75
79
 
@@ -94,8 +98,13 @@ class KubeconfigManager:
94
98
  if api_host is None:
95
99
  raise ValueError(f"no hostname in Kubeconfig server URL: {server!r}")
96
100
 
97
- if isinstance(source, KubeconfigFromSsh) and source.replace_apiserver_hostname:
98
- api_host = source.replace_apiserver_hostname
101
+ if isinstance(source, KubeconfigFromSsh):
102
+ if source.replace_apiserver_hostname:
103
+ api_host = source.replace_apiserver_hostname
104
+ elif api_host in ("127.0.0.1", "0.0.0.0", "localhost"):
105
+ # Automatically replace the API server since we won't be able to reach the Kubernetes API server
106
+ # pointing to localhost from the current machine.
107
+ api_host = source.host
99
108
 
100
109
  raw_kubeconfig.chmod(0o600)
101
110
  return GetRawKubeconfigResult(
@@ -1,6 +1,4 @@
1
- import base64
2
1
  from dataclasses import dataclass
3
- import hashlib
4
2
  from typing import Annotated, ClassVar
5
3
  from nyl.resources import API_VERSION_K8S, NylResource, ObjectMetadata
6
4
  from databind.core import SerializeDefaults
@@ -78,33 +76,3 @@ class Placeholder(NylResource, api_version=API_VERSION_K8S):
78
76
  namespace=namespace,
79
77
  )
80
78
  )
81
-
82
-
83
- def calculate_applyset_id(*, name: str, namespace: str = "", group: str) -> str:
84
- """
85
- Calculate the ID of a Kubernetes ApplySet with the specified name.
86
- """
87
-
88
- # reference: https://kubernetes.io/docs/reference/labels-annotations-taints/#applyset-kubernetes-io-id
89
- hash = hashlib.sha256(f"{name}.{namespace}.ApplySet.{group}".encode()).digest()
90
- uid = base64.b64encode(hash).decode().rstrip("=").replace("/", "_").replace("+", "-")
91
- return f"applyset-{uid}-v1"
92
-
93
-
94
- def get_canonical_resource_kind_name(api_version: str, kind: str) -> str:
95
- """
96
- Given the apiVersion and kind of a Kubernetes resource, return the canonical name of the resource. This name can
97
- be used to identify the resource in an ApplySet's `applyset.kubernetes.io/contains-group-kinds` annotation.
98
-
99
- Note that according to the [reference][1], the resource name should use the plural form, but it appears that the
100
- resource kind name is also accepted. Deriving the plural form will be difficult without querying the Kubernetes
101
- API.
102
-
103
- [1]: https://kubernetes.io/docs/reference/labels-annotations-taints/#applyset-kubernetes-io-contains-group-kinds
104
-
105
- Args:
106
- api_version: The apiVersion of the resource.
107
- kind: The kind of the resource.
108
- """
109
-
110
- return (f"{kind}." + (api_version.split("/")[0] if "/" in api_version else "")).rstrip(".")
@@ -0,0 +1,128 @@
1
+ from dataclasses import dataclass, field
2
+ from pathlib import Path
3
+ import subprocess
4
+ import sys
5
+ from tempfile import TemporaryDirectory
6
+ from typing import Any
7
+
8
+ from loguru import logger
9
+ import yaml
10
+
11
+ from nyl.resources import API_VERSION_INLINE, NylResource
12
+ from nyl.tools.kubernetes import resource_locator
13
+ from nyl.tools.logging import lazy_str
14
+ from nyl.tools.shell import pretty_cmd
15
+ from nyl.tools.types import Manifests
16
+
17
+ KyvernoPolicyDocument = dict[str, Any]
18
+
19
+
20
+ @dataclass(kw_only=True)
21
+ class KyvernoSpec:
22
+ policyFiles: list[str] = field(default_factory=list)
23
+ """
24
+ A list of Kyverno policy filenames, either relative to the file that defined the #PostProcessor resource
25
+ or discoverable in the project search path.
26
+ """
27
+
28
+ inlinePolicies: dict[str, KyvernoPolicyDocument] = field(default_factory=dict)
29
+ """
30
+ A mapping of policy name to the Kyverno policy document. Allows specifying Kyverno policies to be applied
31
+ to the generated manifests inline.
32
+ """
33
+
34
+
35
+ @dataclass(kw_only=True)
36
+ class PostProcessorSpec:
37
+ kyverno: KyvernoSpec
38
+ """
39
+ Apply Kyverno policies.
40
+ """
41
+
42
+
43
+ @dataclass(kw_only=True)
44
+ class PostProcessor(NylResource, api_version=API_VERSION_INLINE):
45
+ """
46
+ Configuration for post-processing Kubernetes manifests in a file. Note that the post-processing is always
47
+ scoped to the file that the processor is defined in. Post processors will be applied after all inline resources
48
+ are reconciled.
49
+ """
50
+
51
+ # metadata: ObjectMetadata
52
+
53
+ spec: PostProcessorSpec
54
+
55
+ def process(self, manifests: Manifests, source_file: Path) -> Manifests:
56
+ """
57
+ Post-process the given manifests.
58
+ """
59
+
60
+ if self.spec.kyverno.policyFiles or self.spec.kyverno.inlinePolicies:
61
+ logger.info("Applying Kyverno policies to manifests from '{}': {}", source_file.name, self.spec.kyverno)
62
+
63
+ policy_paths = []
64
+
65
+ for policy in map(Path, self.spec.kyverno.policyFiles):
66
+ if (source_file.parent / policy).exists():
67
+ policy = (source_file.parent / policy).resolve()
68
+
69
+ assert policy.is_file() or policy.is_dir(), f"Path '{policy}' must be a directory or file"
70
+ # TODO: Resolve relative paths to full paths.
71
+ policy_paths.append(Path(policy))
72
+
73
+ with TemporaryDirectory() as _tmp:
74
+ tmp = Path(_tmp)
75
+
76
+ # Write inline policies to files.
77
+ inline_dir = tmp / "inline-policies"
78
+ inline_dir.mkdir()
79
+ for key, value in self.spec.kyverno.inlinePolicies.items():
80
+ # If the file name does not end with a YAML suffix, Kyverno will ignore the input file.
81
+ if not key.endswith(".yml") and not key.endswith(".yaml"):
82
+ key += ".yaml"
83
+ policy_paths.append(inline_dir.joinpath(key))
84
+ policy_paths[-1].write_text(yaml.safe_dump(value))
85
+
86
+ # Write manifests to a file.
87
+ manifests_file = tmp / "manifests.yaml"
88
+ manifests_file.write_text(yaml.safe_dump_all(manifests))
89
+ output_file = tmp / "output.yaml"
90
+
91
+ command = [
92
+ "kyverno",
93
+ "apply",
94
+ *map(str, policy_paths),
95
+ f"--resource={manifests_file}",
96
+ "-o",
97
+ str(output_file),
98
+ ]
99
+ logger.debug("$ {}", lazy_str(pretty_cmd, command))
100
+ subprocess.check_call(command, stdout=sys.stderr) # TODO: Catch output?
101
+
102
+ # Kyverno only prints the mutated manifests. We assume their name/kind doesn't change and
103
+ # merge them back into the original list.
104
+ mutated_manifests = Manifests(list(filter(None, yaml.safe_load_all(output_file.read_text()))))
105
+
106
+ logger.info(" Mutated manifests: {}", ", ".join(map(resource_locator, mutated_manifests)))
107
+ keyed_manifests = {resource_locator(m): m for m in manifests}
108
+ keyed_manifests.update({resource_locator(m): m for m in mutated_manifests})
109
+ manifests = Manifests(list(keyed_manifests.values()))
110
+
111
+ return manifests
112
+
113
+ @staticmethod
114
+ def extract_from_list(manifests: Manifests) -> tuple[Manifests, list["PostProcessor"]]:
115
+ processors = []
116
+ new_manifests = Manifests([])
117
+ for manifest in list(manifests):
118
+ if processor := PostProcessor.maybe_load(manifest):
119
+ processors.append(processor)
120
+ else:
121
+ new_manifests.append(manifest)
122
+ return new_manifests, processors
123
+
124
+ @staticmethod
125
+ def apply_all(manifests: Manifests, processors: list["PostProcessor"], source_file: Path) -> Manifests:
126
+ for processor in processors:
127
+ manifests = processor.process(manifests, source_file)
128
+ return manifests
@@ -0,0 +1,133 @@
1
+ from pathlib import Path
2
+ from nyl.resources.postprocessor import KyvernoSpec, PostProcessor, PostProcessorSpec
3
+ from nyl.tools.types import Manifest, Manifests
4
+
5
+
6
+ def test__PostProcessor__extract_from_list() -> None:
7
+ manifest = Manifests(
8
+ [
9
+ Manifest(
10
+ {
11
+ "apiVersion": "v1",
12
+ "kind": "Pod",
13
+ "metadata": {
14
+ "name": "foo",
15
+ "namespace": "bar",
16
+ },
17
+ }
18
+ ),
19
+ Manifest({"apiVersion": "inline.nyl.io/v1", "kind": "PostProcessor", "spec": {"kyverno": {}}}),
20
+ ]
21
+ )
22
+
23
+ updated_manifest, processors = PostProcessor.extract_from_list(manifest)
24
+ assert updated_manifest == [manifest[0]]
25
+ assert len(processors) == 1
26
+
27
+
28
+ def test__PostProcessor__process__inlinePolicy() -> None:
29
+ manifest = Manifests(
30
+ [
31
+ Manifest(
32
+ {
33
+ "apiVersion": "v1",
34
+ "kind": "Pod",
35
+ "metadata": {"name": "foo", "namespace": "foo"},
36
+ "spec": {
37
+ "containers": [
38
+ {
39
+ "name": "main",
40
+ "image": "nginx:latest",
41
+ }
42
+ ]
43
+ },
44
+ }
45
+ )
46
+ ]
47
+ )
48
+
49
+ processor = PostProcessor(
50
+ spec=PostProcessorSpec(
51
+ kyverno=KyvernoSpec(
52
+ inlinePolicies={
53
+ "security-profile": {
54
+ "apiVersion": "kyverno.io/v1",
55
+ "kind": "ClusterPolicy",
56
+ "metadata": {"name": "enforce-pod-security-context"},
57
+ "spec": {
58
+ "validationFailureAction": "enforce",
59
+ "rules": [
60
+ {
61
+ "name": "mutate-pod-security-context",
62
+ "match": {"resources": {"kinds": ["Pod"]}},
63
+ "mutate": {
64
+ "patchStrategicMerge": {
65
+ "spec": {
66
+ "securityContext": {
67
+ "runAsNonRoot": True,
68
+ "seccompProfile": {"type": "RuntimeDefault"},
69
+ },
70
+ "containers": [
71
+ {
72
+ "(name)": "*",
73
+ "securityContext": {
74
+ "runAsNonRoot": True,
75
+ "allowPrivilegeEscalation": False,
76
+ "capabilities": {"drop": ["ALL"]},
77
+ },
78
+ }
79
+ ],
80
+ "initContainers": [
81
+ {
82
+ "(name)": "*",
83
+ "securityContext": {
84
+ "runAsNonRoot": True,
85
+ "allowPrivilegeEscalation": False,
86
+ "capabilities": {"drop": ["ALL"]},
87
+ },
88
+ }
89
+ ],
90
+ }
91
+ }
92
+ },
93
+ },
94
+ ],
95
+ },
96
+ }
97
+ }
98
+ )
99
+ )
100
+ )
101
+
102
+ updated_manifest = processor.process(manifest, Path("/"))
103
+
104
+ assert updated_manifest == [
105
+ {
106
+ "apiVersion": "v1",
107
+ "kind": "Pod",
108
+ "metadata": {"name": "foo", "namespace": "foo"},
109
+ "spec": {
110
+ "containers": [
111
+ {
112
+ "image": "nginx:latest",
113
+ "name": "main",
114
+ "securityContext": {
115
+ "allowPrivilegeEscalation": False,
116
+ "capabilities": {
117
+ "drop": [
118
+ "ALL",
119
+ ],
120
+ },
121
+ "runAsNonRoot": True,
122
+ },
123
+ },
124
+ ],
125
+ "securityContext": {
126
+ "runAsNonRoot": True,
127
+ "seccompProfile": {
128
+ "type": "RuntimeDefault",
129
+ },
130
+ },
131
+ },
132
+ }
133
+ ]
@@ -1,6 +1,7 @@
1
1
  from loguru import logger
2
2
  from kubernetes.dynamic import DynamicClient
3
3
  from kubernetes.client.api_client import ApiClient
4
+ from nyl.tools.types import Manifest
4
5
 
5
6
 
6
7
  def discover_kubernetes_api_versions(client: ApiClient) -> set[str]:
@@ -15,3 +16,18 @@ def discover_kubernetes_api_versions(client: ApiClient) -> set[str]:
15
16
  all_versions.add(f"{resource.group_version}/{resource.kind}")
16
17
  logger.info("Discovered {} Kubernetes API version(s).", len(all_versions))
17
18
  return all_versions
19
+
20
+
21
+ def resource_locator(manifest: Manifest) -> str:
22
+ """
23
+ Create a string that contains the apiVersion, kind, namespace and name of a Kubernetes resource formatted as
24
+
25
+ apiVersion/kind/namespace/name
26
+
27
+ This can be used to uniquely identify a resource.
28
+ """
29
+
30
+ return (
31
+ f"{manifest['apiVersion']}/{manifest['kind']}/"
32
+ f"{manifest['metadata'].get('namespace', '')}/{manifest['metadata']['name']}"
33
+ )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes