nyl 0.4.0__tar.gz → 0.5.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 (114) hide show
  1. {nyl-0.4.0 → nyl-0.5.0}/.kraken.py +2 -0
  2. {nyl-0.4.0 → nyl-0.5.0}/PKG-INFO +1 -1
  3. nyl-0.5.0/docs/content/reference/configuration/secrets.md +152 -0
  4. {nyl-0.4.0 → nyl-0.5.0}/pyproject.toml +1 -1
  5. {nyl-0.4.0 → nyl-0.5.0}/src/kubernetes/dynamic/client.pyi +1 -0
  6. {nyl-0.4.0 → nyl-0.5.0}/src/kubernetes/dynamic/discovery.pyi +1 -0
  7. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/commands/__init__.py +55 -25
  8. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/commands/argocd.py +6 -4
  9. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/commands/crds.py +1 -0
  10. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/commands/new.py +5 -3
  11. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/commands/profile.py +6 -5
  12. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/commands/run.py +6 -4
  13. nyl-0.5.0/src/nyl/commands/secrets.py +93 -0
  14. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/commands/template.py +65 -29
  15. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/commands/tools/__init__.py +4 -3
  16. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/commands/tools/bcrypt.py +1 -0
  17. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/commands/tools/sops.py +6 -5
  18. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/commands/tun.py +7 -6
  19. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/generator/components.py +3 -1
  20. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/generator/dispatch.py +3 -2
  21. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/generator/dispatch_test.py +2 -1
  22. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/generator/helmchart.py +4 -3
  23. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/generator/statefulsecret.py +2 -1
  24. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/profiles/__init__.py +7 -8
  25. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/profiles/config.py +3 -0
  26. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/profiles/kubeconfig.py +5 -3
  27. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/profiles/tunnel.py +4 -3
  28. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/project/config.py +35 -10
  29. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/resources/__init__.py +3 -1
  30. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/resources/applyset.py +3 -2
  31. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/resources/placeholder.py +3 -1
  32. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/resources/postprocessor_test.py +1 -0
  33. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/resources/statefulsecret.py +1 -0
  34. nyl-0.5.0/src/nyl/secrets/__init__.py +77 -0
  35. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/secrets/config.py +23 -45
  36. nyl-0.5.0/src/nyl/secrets/kubernetes.py +84 -0
  37. nyl-0.5.0/src/nyl/secrets/null.py +22 -0
  38. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/secrets/sops.py +39 -6
  39. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/secrets/sops_test.py +5 -2
  40. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/templating.py +31 -25
  41. nyl-0.5.0/src/nyl/tools/di.py +84 -0
  42. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/tools/fs_test.py +1 -0
  43. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/tools/kubernetes.py +2 -1
  44. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/tools/kvstore.py +3 -3
  45. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/tools/logging.py +0 -1
  46. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/tools/typer.py +1 -0
  47. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/tools/yaml.py +2 -1
  48. {nyl-0.4.0 → nyl-0.5.0}/tests/conftest.py +1 -0
  49. {nyl-0.4.0 → nyl-0.5.0}/uv.lock +137 -92
  50. nyl-0.4.0/docs/content/reference/configuration/secrets.md +0 -87
  51. nyl-0.4.0/src/nyl/commands/secrets.py +0 -61
  52. nyl-0.4.0/src/nyl/secrets/__init__.py +0 -1
  53. nyl-0.4.0/src/nyl/secrets/null.py +0 -14
  54. {nyl-0.4.0 → nyl-0.5.0}/.envrc +0 -0
  55. {nyl-0.4.0 → nyl-0.5.0}/.github/workflows/docker.yaml +0 -0
  56. {nyl-0.4.0 → nyl-0.5.0}/.github/workflows/docs.yaml +0 -0
  57. {nyl-0.4.0 → nyl-0.5.0}/.github/workflows/python.yaml +0 -0
  58. {nyl-0.4.0 → nyl-0.5.0}/.gitignore +0 -0
  59. {nyl-0.4.0 → nyl-0.5.0}/README.md +0 -0
  60. {nyl-0.4.0 → nyl-0.5.0}/argocd-cmp/Dockerfile +0 -0
  61. {nyl-0.4.0 → nyl-0.5.0}/argocd-cmp/README.md +0 -0
  62. {nyl-0.4.0 → nyl-0.5.0}/argocd-cmp/plugin.yaml +0 -0
  63. {nyl-0.4.0 → nyl-0.5.0}/argocd-with-nyl/.envrc +0 -0
  64. {nyl-0.4.0 → nyl-0.5.0}/argocd-with-nyl/.secrets.yaml +0 -0
  65. {nyl-0.4.0 → nyl-0.5.0}/argocd-with-nyl/.sops.yaml +0 -0
  66. {nyl-0.4.0 → nyl-0.5.0}/argocd-with-nyl/README.md +0 -0
  67. {nyl-0.4.0 → nyl-0.5.0}/argocd-with-nyl/argocd.yaml +0 -0
  68. {nyl-0.4.0 → nyl-0.5.0}/argocd-with-nyl/nyl-project.yaml +0 -0
  69. {nyl-0.4.0 → nyl-0.5.0}/argocd-with-nyl/nyl-secrets.yaml +0 -0
  70. {nyl-0.4.0 → nyl-0.5.0}/docs/content/index.md +0 -0
  71. {nyl-0.4.0 → nyl-0.5.0}/docs/content/reference/applysets.md +0 -0
  72. {nyl-0.4.0 → nyl-0.5.0}/docs/content/reference/argocd-plugin.md +0 -0
  73. {nyl-0.4.0 → nyl-0.5.0}/docs/content/reference/cluster-connectivity.md +0 -0
  74. {nyl-0.4.0 → nyl-0.5.0}/docs/content/reference/configuration/environment.md +0 -0
  75. {nyl-0.4.0 → nyl-0.5.0}/docs/content/reference/configuration/profiles.md +0 -0
  76. {nyl-0.4.0 → nyl-0.5.0}/docs/content/reference/configuration/projects.md +0 -0
  77. {nyl-0.4.0 → nyl-0.5.0}/docs/content/reference/configuration-api/profiles.md +0 -0
  78. {nyl-0.4.0 → nyl-0.5.0}/docs/content/reference/configuration-api/secrets.md +0 -0
  79. {nyl-0.4.0 → nyl-0.5.0}/docs/content/reference/glossary.md +0 -0
  80. {nyl-0.4.0 → nyl-0.5.0}/docs/content/reference/templating/basics.md +0 -0
  81. {nyl-0.4.0 → nyl-0.5.0}/docs/content/reference/templating/components.md +0 -0
  82. {nyl-0.4.0 → nyl-0.5.0}/docs/content/reference/templating/inlining/helmcharts.md +0 -0
  83. {nyl-0.4.0 → nyl-0.5.0}/docs/content/reference/templating/inlining/overview.md +0 -0
  84. {nyl-0.4.0 → nyl-0.5.0}/docs/content/reference/templating/inlining/postprocessor.md +0 -0
  85. {nyl-0.4.0 → nyl-0.5.0}/docs/content/reference/templating/secrets.md +0 -0
  86. {nyl-0.4.0 → nyl-0.5.0}/docs/mkdocs.yml +0 -0
  87. {nyl-0.4.0 → nyl-0.5.0}/docs/pyproject.toml +0 -0
  88. {nyl-0.4.0 → nyl-0.5.0}/docs/uv.lock +0 -0
  89. {nyl-0.4.0 → nyl-0.5.0}/example/manifests/postgres.yaml +0 -0
  90. {nyl-0.4.0 → nyl-0.5.0}/example/nyl-profiles.yaml +0 -0
  91. {nyl-0.4.0 → nyl-0.5.0}/example/nyl-secrets.yaml +0 -0
  92. {nyl-0.4.0 → nyl-0.5.0}/example/secrets.yaml +0 -0
  93. {nyl-0.4.0 → nyl-0.5.0}/src/kubernetes/README.md +0 -0
  94. {nyl-0.4.0 → nyl-0.5.0}/src/kubernetes/__init__.pyi +0 -0
  95. {nyl-0.4.0 → nyl-0.5.0}/src/kubernetes/dynamic/__init__.pyi +0 -0
  96. {nyl-0.4.0 → nyl-0.5.0}/src/kubernetes/dynamic/exceptions.pyi +0 -0
  97. {nyl-0.4.0 → nyl-0.5.0}/src/kubernetes/dynamic/resource.pyi +0 -0
  98. {nyl-0.4.0 → nyl-0.5.0}/src/kubernetes/watch/__init__.pyi +0 -0
  99. {nyl-0.4.0 → nyl-0.5.0}/src/kubernetes/watch/watch.pyi +0 -0
  100. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/__init__.py +0 -0
  101. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/generator/__init__.py +1 -1
  102. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/profiles/config_test.py +0 -0
  103. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/resources/applyset_test.py +0 -0
  104. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/resources/helmchart.py +0 -0
  105. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/resources/postprocessor.py +3 -3
  106. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/secrets/config_test.py +0 -0
  107. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/templating_test.py +0 -0
  108. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/tools/fs.py +2 -2
  109. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/tools/kubectl.py +3 -3
  110. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/tools/loads.py +0 -0
  111. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/tools/shell.py +1 -1
  112. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/tools/testing.py +0 -0
  113. {nyl-0.4.0 → nyl-0.5.0}/src/nyl/tools/types.py +0 -0
  114. {nyl-0.4.0 → nyl-0.5.0}/tests/__init__.py +0 -0
@@ -1,5 +1,7 @@
1
1
  from kraken.common import buildscript
2
+
2
3
  buildscript(requirements=["kraken-build @ git+https://github.com/kraken-build/kraken.git@nr/python-project#egg=kraken-build&subdirectory=kraken-build"])
3
4
 
4
5
  from kraken.build.python.v1alpha1 import python_project
6
+
5
7
  python_project()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: nyl
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: Advanced templating engine for Kubernetes and GitOps.
5
5
  Author-email: Niklas Rosenstein <rosensteinniklas@gmail.com>
6
6
  License: MIT
@@ -0,0 +1,152 @@
1
+ # Secrets
2
+
3
+ You can connect Nyl with various secret providers to retrieve external or encrypted data that can be used in your
4
+ templates. This is useful for keeping sensitive data out of your configuration files and ensuring that they are not
5
+ accidentally committed to a version control system.
6
+
7
+ ## Configuration
8
+
9
+ Secret providers are configured in a `nyl-secrets.<ext>` file that is located in the current working directory or any of
10
+ its parent directories. Secret providers may also be defined in a [Project configuration file](./projects.md), though
11
+ the file closer to the working directory will take precedence.
12
+
13
+ There is no "global" way to define a secrets provider, as secrets are considered project-specific.
14
+
15
+ As with other configuration file types, the file extension can be `.toml`, `.yaml`, or `.json`.
16
+
17
+ The configuration contains any number of named secret providers. When not specified otherwise, Nyl will assume that the
18
+ provider to use is named `default`. The provider to use can be overriden by passing the corresponding CLI option to
19
+ respective Nyl commands or by setting the `NYL_SECRETS` environment variable.
20
+
21
+ ## Inspecting secret providers
22
+
23
+ You can inspect secret providers using the `nyl secrets` command.
24
+
25
+ ```
26
+ nyl secrets list List the keys for all secrets in the provider.
27
+ nyl secrets get <key> Get the value of a secret as JSON.
28
+ ```
29
+
30
+ ## Templating
31
+
32
+ Secrets are made available to templates using the `secrets.get()` function. The function takes a single argument, the
33
+ key of the secret to retrieve.
34
+
35
+ __Example__
36
+
37
+ ```yaml
38
+ apiVersion: v1
39
+ kind: Secret
40
+ metadata:
41
+ name: my-secret
42
+ type: Opaque
43
+ stringData:
44
+ password: ${{ secrets.get("my-password") }}
45
+ ```
46
+
47
+ ## CLI
48
+
49
+ The `nyl secrets` command allows you to interact with the configured secret provider(s).
50
+
51
+ ```
52
+ Usage: nyl secrets [OPTIONS] COMMAND [ARGS]...
53
+
54
+ Interact with the secrets providers configured in `nyl-secrets.yaml`.
55
+
56
+ ╭─ Options ────────────────────────────────────────────────────────────────────╮
57
+ │ --provider TEXT The name of the configured secrets provider to use. │
58
+ │ [env var: NYL_SECRETS] │
59
+ │ [default: default] │
60
+ │ --help Show this message and exit. │
61
+ ╰──────────────────────────────────────────────────────────────────────────────╯
62
+ ╭─ Commands ───────────────────────────────────────────────────────────────────╮
63
+ │ get Get the value of a secret as JSON. │
64
+ │ list List the keys for all secrets in the provider. │
65
+ │ set Set the value of a secret. │
66
+ │ unset Unset the value of a secret. │
67
+ ╰──────────────────────────────────────────────────────────────────────────────╯
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Provider: [Sops]
73
+
74
+ [Sops]: https://github.com/getsops/sops
75
+
76
+ Allows you to retrieve secrets from a [Sops] encrypted file. For a GitOps workflow, the file must be commited to the
77
+ same repository to ensure that Nyl has access to it when it is invoked as an ArgoCD Config Management plugin. You also
78
+ must have the `sops` program installed.
79
+
80
+ __Example__
81
+
82
+ === "TOML"
83
+
84
+ ```toml title="nyl-secrets.toml"
85
+ [default]
86
+ type = "sops"
87
+ path = "../secrets.yaml"
88
+ ```
89
+
90
+ === "YAML"
91
+
92
+ ```yaml title="nyl-secrets.yaml"
93
+ default:
94
+ type: sops
95
+ path: ../secrets.yaml
96
+ ```
97
+
98
+ === "JSON"
99
+
100
+ ```toml title="nyl-secrets.json"
101
+ {
102
+ "default": {
103
+ "type": "sops",
104
+ "path": "../secrets.yaml"
105
+ }
106
+ }
107
+ ```
108
+
109
+ The secrets will be decoded using the `sops` program, hence all the typical ways to configure Sops and how it decrypts
110
+ files apply. The `path` field is relative to the location of the `nyl-secrets.yaml` file.
111
+
112
+ ---
113
+
114
+ ## Provider: [KubernetesSecret](https://kubernetes.io/docs/concepts/configuration/secret/)
115
+
116
+ Allows you to point to a Kubernetes Secret as a source for secrets. Since Kubernetes secret values must be strings,
117
+ this provider does not currently support nested keys, although it could be made possible by assuming TOML/YAML/JSON
118
+ format for the Kubernetes `Secret` keys in the `data` field.
119
+
120
+ The secret provider will use the same Kubernetes context that is configured in Nyl via [Profiles](./profiles.md).
121
+
122
+ __Example__
123
+
124
+ === "TOML"
125
+
126
+ ```toml title="nyl-secrets.toml"
127
+ [default]
128
+ type = "KubernetesSecret"
129
+ name = "nyl-secrets"
130
+ namespace = "default"
131
+ ```
132
+
133
+ === "YAML"
134
+
135
+ ```yaml title="nyl-secrets.yaml"
136
+ default:
137
+ type: KubernetesSecret
138
+ name: nyl-secrets
139
+ namespace: default
140
+ ```
141
+
142
+ === "JSON"
143
+
144
+ ```toml title="nyl-secrets.json"
145
+ {
146
+ "default": {
147
+ "type": "KubernetesSecret",
148
+ "name": "nyl-secrets",
149
+ "namespace": "default"
150
+ }
151
+ }
152
+ ```
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nyl"
7
- version = "0.4.0" # sed'ed on publish in CI
7
+ version = "0.5.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"},
@@ -1,4 +1,5 @@
1
1
  from typing import Any, Iterable, Protocol
2
+
2
3
  from kubernetes.client.api_client import ApiClient
3
4
  from kubernetes.dynamic.discovery import Discoverer
4
5
  from kubernetes.dynamic.resource import Resource, ResourceInstance
@@ -1,5 +1,6 @@
1
1
  from collections.abc import Sequence
2
2
  from typing import Any
3
+
3
4
  from kubernetes.dynamic.resource import Resource
4
5
 
5
6
  class Discoverer:
@@ -6,39 +6,30 @@ applications directly or integrate as an ArgoCD ConfigManagementPlugin.
6
6
  import atexit
7
7
  import json
8
8
  import os
9
- from pathlib import Path
9
+ import sys
10
10
  import time
11
- from typing import Optional
12
- from nyl import __version__
11
+ from dataclasses import dataclass
13
12
  from enum import Enum
14
- import sys
13
+ from pathlib import Path
14
+ from typing import Optional
15
+
15
16
  from loguru import logger
16
- from typer import Option
17
+ from typer import Option, Typer
18
+
19
+ from kubernetes.client.api_client import ApiClient
20
+ from nyl import __version__
21
+ from nyl.profiles import ProfileManager
22
+ from nyl.project.config import ProjectConfig
23
+ from nyl.secrets.config import SecretsConfig
24
+ from nyl.tools.di import DependenciesProvider
17
25
  from nyl.tools.logging import lazy_str
18
26
  from nyl.tools.shell import pretty_cmd
19
27
  from nyl.tools.typer import new_typer
20
28
 
29
+ app: Typer = new_typer(help=__doc__)
21
30
 
22
- app = new_typer(help=__doc__)
23
-
24
-
25
- from . import argocd # noqa: E402
26
- from . import crds # noqa: F401,E402
27
- from . import new # noqa: E402
28
- from . import profile # noqa: E402
29
- from . import run # noqa: F401, E402
30
- from . import secrets # noqa: E402
31
- from . import template # noqa: F401,E402
32
- from . import tools # noqa: E402
33
- from . import tun # noqa: E402
34
-
35
- app.add_typer(argocd.app)
36
- app.add_typer(new.app)
37
- app.add_typer(profile.app)
38
- app.add_typer(secrets.app)
39
- app.add_typer(tools.app)
40
- app.add_typer(tun.app)
41
-
31
+ # A global instance that we use for dependency injection.
32
+ PROVIDER = DependenciesProvider.default()
42
33
 
43
34
  LOG_TIME_FORMAT = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green>"
44
35
  LOG_LEVEL_FORAMT = "<level>{level: <8}</level>"
@@ -55,6 +46,15 @@ class LogLevel(str, Enum):
55
46
  CRITICAL = "critical"
56
47
 
57
48
 
49
+ # Retrieving the Kubernetes API client depends on whether in-cluster configuration should be used or not.
50
+ @dataclass(kw_only=True)
51
+ class ApiClientConfig:
52
+ in_cluster: bool
53
+ " Load the in-cluster configuration if enabled; forego any Nyl profile configuration. "
54
+ profile: str | None
55
+ " If not loading the in-cluster configuration, use the given Nyl profile. Otherwise, use the default kubeconfig. "
56
+
57
+
58
58
  @app.callback()
59
59
  def _callback(
60
60
  quiet: bool = Option(False, "--quiet", "-q", help="Shortcut for --log-level=error."),
@@ -98,6 +98,18 @@ def _callback(
98
98
  lazy_str(pretty_cmd, sys.argv),
99
99
  )
100
100
 
101
+ PROVIDER.set_lazy(ProfileManager, lambda: ProfileManager.load(required=False))
102
+ PROVIDER.set_lazy(SecretsConfig, lambda: SecretsConfig.load(dependencies=PROVIDER))
103
+ PROVIDER.set_lazy(ProjectConfig, lambda: ProjectConfig.load(dependencies=PROVIDER))
104
+ PROVIDER.set_lazy(
105
+ ApiClient,
106
+ lambda: template.get_incluster_kubernetes_client()
107
+ if PROVIDER.get(ApiClientConfig).in_cluster
108
+ else template.get_profile_kubernetes_client(
109
+ PROVIDER.get(ProfileManager), PROVIDER.get(ApiClientConfig).profile
110
+ ),
111
+ )
112
+
101
113
 
102
114
  @app.command()
103
115
  def version() -> None:
@@ -107,3 +119,21 @@ def version() -> None:
107
119
 
108
120
  print(f"Nyl v{__version__}")
109
121
  sys.exit(0)
122
+
123
+
124
+ from . import argocd # noqa: E402
125
+ from . import crds # noqa: F401,E402
126
+ from . import new # noqa: E402
127
+ from . import profile # noqa: E402
128
+ from . import run # noqa: F401, E402
129
+ from . import secrets # noqa: E402
130
+ from . import template # noqa: F401,E402
131
+ from . import tools # noqa: E402
132
+ from . import tun # noqa: E402
133
+
134
+ app.add_typer(argocd.app)
135
+ app.add_typer(new.app)
136
+ app.add_typer(profile.app)
137
+ app.add_typer(secrets.app)
138
+ app.add_typer(tools.app)
139
+ app.add_typer(tun.app)
@@ -5,12 +5,14 @@ Helper commands for the ArgoCD integration.
5
5
  import sys
6
6
 
7
7
  from loguru import logger
8
+ from typer import Typer
9
+
10
+ from nyl.commands import PROVIDER
8
11
  from nyl.project.config import ProjectConfig
9
12
  from nyl.secrets.config import SecretsConfig
10
13
  from nyl.tools.typer import new_typer
11
14
 
12
-
13
- app = new_typer(name="argocd", help=__doc__)
15
+ app: Typer = new_typer(name="argocd", help=__doc__)
14
16
 
15
17
 
16
18
  @app.command()
@@ -21,10 +23,10 @@ def discovery() -> None:
21
23
  """
22
24
 
23
25
  found = False
24
- if (file := ProjectConfig.load().file) is not None:
26
+ if (file := PROVIDER.get(ProjectConfig).file) is not None:
25
27
  print(file.absolute())
26
28
  found = True
27
- if (file := SecretsConfig.load().file) is not None:
29
+ if (file := PROVIDER.get(SecretsConfig).file) is not None:
28
30
  print(file.absolute())
29
31
  found = True
30
32
 
@@ -2,6 +2,7 @@ import yaml
2
2
 
3
3
  from nyl.resources.applyset import ApplySet
4
4
  from nyl.resources.placeholder import Placeholder
5
+
5
6
  from . import app
6
7
 
7
8
 
@@ -7,11 +7,13 @@ from pathlib import Path
7
7
  from textwrap import dedent
8
8
 
9
9
  from loguru import logger
10
- from typer import Option
10
+ from typer import Option, Typer
11
+
12
+ from nyl.commands import PROVIDER
11
13
  from nyl.project.config import ProjectConfig
12
14
  from nyl.tools.typer import new_typer
13
15
 
14
- app = new_typer(name="new", help=__doc__)
16
+ app: Typer = new_typer(name="new", help=__doc__)
15
17
 
16
18
 
17
19
  class ComponentType(Enum):
@@ -106,5 +108,5 @@ def component(
106
108
  ) -> None:
107
109
  """Create the boilerplate for a Nyl component in the components directory."""
108
110
 
109
- components_path = ProjectConfig.load().get_components_path()
111
+ components_path = PROVIDER.get(ProjectConfig).get_components_path()
110
112
  chart(components_path / api_version / kind)
@@ -4,12 +4,13 @@ Interact with your Nyl profile configuration.
4
4
 
5
5
  import shlex
6
6
 
7
- from typer import Argument
7
+ from typer import Argument, Typer
8
+
9
+ from nyl.commands import PROVIDER
8
10
  from nyl.profiles import ProfileManager
9
11
  from nyl.tools.typer import new_typer
10
12
 
11
-
12
- app = new_typer(name="profile", help=__doc__)
13
+ app: Typer = new_typer(name="profile", help=__doc__)
13
14
 
14
15
 
15
16
  @app.command()
@@ -20,7 +21,7 @@ def activate(profile_name: str = Argument("default", envvar="NYL_PROFILE")) -> N
20
21
  Evaluate the stdout of this command to export the KUBECONFIG into your environment.
21
22
  """
22
23
 
23
- with ProfileManager.load() as manager:
24
+ with PROVIDER.get(ProfileManager) as manager:
24
25
  profile = manager.activate_profile(profile_name)
25
26
 
26
27
  for key, value in profile.env.items():
@@ -33,7 +34,7 @@ def get_kubeconfig(profile_name: str = Argument("default", envvar="NYL_PROFILE")
33
34
  Similar to `nyl profile activate`, but prints only the path to the `KUBECONFIG` file.
34
35
  """
35
36
 
36
- with ProfileManager.load() as manager:
37
+ with PROVIDER.get(ProfileManager) as manager:
37
38
  profile = manager.activate_profile(profile_name)
38
39
 
39
40
  print(profile.kubeconfig)
@@ -1,18 +1,20 @@
1
1
  import atexit
2
2
  import os
3
- from pathlib import Path
4
3
  import subprocess
5
4
  import sys
5
+ from pathlib import Path
6
6
  from tempfile import TemporaryDirectory
7
+
7
8
  from loguru import logger
8
9
  from typer import Argument, Option
9
- from nyl.tools import yaml
10
+
11
+ from nyl.commands import PROVIDER
10
12
  from nyl.profiles import ActivatedProfile, ProfileManager
11
13
  from nyl.profiles.kubeconfig import _trim_to_context
14
+ from nyl.tools import yaml
12
15
  from nyl.tools.logging import lazy_str
13
16
  from nyl.tools.shell import pretty_cmd
14
17
 
15
-
16
18
  from . import app
17
19
 
18
20
  # app = new_typer(name="run", help=__doc__)
@@ -42,7 +44,7 @@ def run(
42
44
  `nyl-profiles.yaml` configuration or from the same-named context in the global kubeconfig.
43
45
  """
44
46
 
45
- manager = ProfileManager.load(required=False)
47
+ manager = PROVIDER.get(ProfileManager)
46
48
  if manager and profile_name in manager.config.profiles:
47
49
  with manager:
48
50
  profile = manager.activate_profile(profile_name)
@@ -0,0 +1,93 @@
1
+ """
2
+ Interact with the secrets providers configured in `nyl-secrets.yaml`.
3
+ """
4
+
5
+ import json
6
+ import json as _json
7
+
8
+ from loguru import logger
9
+ from typer import Option, Typer
10
+
11
+ from nyl.commands import PROVIDER, ApiClientConfig
12
+ from nyl.secrets import SecretProvider
13
+ from nyl.secrets.config import SecretsConfig
14
+ from nyl.tools.typer import new_typer
15
+
16
+ app: Typer = new_typer(name="secrets", help=__doc__)
17
+
18
+
19
+ @app.callback()
20
+ def callback(
21
+ provider: str = Option(
22
+ "default",
23
+ "--provider",
24
+ help="The name of the configured secrets provider to use.",
25
+ envvar="NYL_SECRETS",
26
+ ),
27
+ profile: str | None = Option(
28
+ None,
29
+ "--profile",
30
+ help="The Nyl profile to assume.",
31
+ envvar="NYL_PROFILE",
32
+ ),
33
+ ) -> None:
34
+ """
35
+ Interact with the secrets providers configured in `nyl-secrets.yaml`.
36
+ """
37
+
38
+ PROVIDER.set(ApiClientConfig, ApiClientConfig(in_cluster=False, profile=profile))
39
+ PROVIDER.set_lazy(tuple[str, SecretProvider], lambda: (provider, PROVIDER.get(SecretsConfig).providers[provider]))
40
+ PROVIDER.set_lazy(SecretProvider, lambda: PROVIDER.get(tuple[str, SecretProvider])[1]) # type: ignore[type-abstract]
41
+
42
+
43
+ @app.command()
44
+ def list(
45
+ providers: bool = Option(
46
+ False, help="List the configured secrets providers instead of the current provider's available keys."
47
+ ),
48
+ ) -> None:
49
+ """
50
+ List the keys for all secrets in the provider.
51
+ """
52
+
53
+ if providers:
54
+ for alias, impl in PROVIDER.get(SecretsConfig).providers.items():
55
+ print(alias, impl)
56
+ else:
57
+ for key in PROVIDER.get(SecretProvider).keys(): # type: ignore[type-abstract]
58
+ print(key)
59
+
60
+
61
+ @app.command()
62
+ def get(key: str, pretty: bool = False, raw: bool = False) -> None:
63
+ """
64
+ Get the value of a secret as JSON.
65
+ """
66
+
67
+ value = PROVIDER.get(SecretProvider).get(key) # type: ignore[type-abstract]
68
+ if raw and isinstance(value, str):
69
+ print(value)
70
+ else:
71
+ print(json.dumps(value, indent=4 if pretty else None))
72
+
73
+
74
+ @app.command()
75
+ def set(key: str, value: str, json: bool = False) -> None:
76
+ """
77
+ Set the value of a secret.
78
+ """
79
+
80
+ provider_name, secrets = PROVIDER.get(tuple[str, SecretProvider])
81
+ logger.info("Setting key '{}' in provider '{}'", key, provider_name)
82
+ secrets.set(key, _json.loads(value) if json else value)
83
+
84
+
85
+ @app.command()
86
+ def unset(key: str) -> None:
87
+ """
88
+ Unset the value of a secret.
89
+ """
90
+
91
+ provider_name, secrets = PROVIDER.get(tuple[str, SecretProvider])
92
+ logger.info("Unsetting key '{}' in provider '{}'", key, provider_name)
93
+ secrets.unset(key)
@@ -1,33 +1,45 @@
1
1
  import atexit
2
- from dataclasses import dataclass
3
2
  import os
3
+ from dataclasses import dataclass
4
+ from enum import Enum
4
5
  from pathlib import Path
5
6
  from textwrap import indent
6
- from typing import Optional
7
+ from typing import Any, Literal, Optional, cast
8
+
7
9
  from loguru import logger
8
- from nyl.resources.postprocessor import PostProcessor
9
10
  from typer import Argument, Option
10
- from nyl.tools import yaml
11
+
12
+ from kubernetes.client.api_client import ApiClient
13
+ from kubernetes.config.incluster_config import load_incluster_config
14
+ from kubernetes.config.kube_config import load_kube_config
15
+ from nyl.commands import PROVIDER, ApiClientConfig, app
11
16
  from nyl.generator import reconcile_generator
12
17
  from nyl.generator.dispatch import DispatchingGenerator
13
18
  from nyl.profiles import ProfileManager
14
- from kubernetes.config.incluster_config import load_incluster_config
15
- from kubernetes.config.kube_config import load_kube_config
16
- from kubernetes.client.api_client import ApiClient
17
19
  from nyl.project.config import ProjectConfig
18
20
  from nyl.resources import API_VERSION_INLINE, NylResource
19
21
  from nyl.resources.applyset import APPLYSET_LABEL_PART_OF, ApplySet
22
+ from nyl.resources.postprocessor import PostProcessor
20
23
  from nyl.secrets.config import SecretsConfig
21
24
  from nyl.templating import NylTemplateEngine
25
+ from nyl.tools import yaml
22
26
  from nyl.tools.kubectl import Kubectl
23
27
  from nyl.tools.logging import lazy_str
24
28
  from nyl.tools.types import Manifest, Manifests
25
29
 
26
- from . import app
27
-
28
30
  DEFAULT_PROFILE = "default"
29
31
 
30
32
 
33
+ # Need an enum for typer
34
+ class OnLookupFailure(str, Enum):
35
+ Error = "Error"
36
+ CreatePlaceholder = "CreatePlaceholder"
37
+ SkipResource = "SkipResource"
38
+
39
+ def to_literal(self) -> Literal["Error", "CreatePlaceholder", "SkipResource"]:
40
+ return cast(Any, self.name) # type: ignore[no-any-return]
41
+
42
+
31
43
  @dataclass
32
44
  class ManifestsWithSource:
33
45
  """
@@ -38,6 +50,37 @@ class ManifestsWithSource:
38
50
  file: Path
39
51
 
40
52
 
53
+ def get_incluster_kubernetes_client() -> ApiClient:
54
+ logger.info("Using in-cluster configuration.")
55
+ load_incluster_config()
56
+ return ApiClient()
57
+
58
+
59
+ def get_profile_kubernetes_client(profiles: ProfileManager, profile: str | None) -> ApiClient:
60
+ """
61
+ Create a Kubernetes :class:`ApiClient` from the selected *profile*.
62
+
63
+ If no *profile* is specified, but the profile manager contains at least one profile, the *profile* argument will
64
+ default to the value of :data:`DEFAULT_PROFILE` (which is `"default"`). Otherwise, if no profile is selected and
65
+ none is configured, the standard Kubernetes config load takes place (i.e. try `KUBECONFIG` and then
66
+ `~/.kube/config`).
67
+ """
68
+
69
+ with profiles:
70
+ # If no profile to activate is specified, and there are no profiles defined, we're not activating a
71
+ # a profile. It should be valid to use Nyl without a `nyl-profiles.yaml` file.
72
+ if profile is not None or profiles.config.profiles:
73
+ profile = profile or DEFAULT_PROFILE
74
+ active = profiles.activate_profile(profile)
75
+ load_kube_config(str(active.kubeconfig))
76
+ else:
77
+ logger.opt(ansi=True).info(
78
+ "No <yellow>nyl-profiles.yaml</> file found, using default kubeconfig and context."
79
+ )
80
+ load_kube_config()
81
+ return ApiClient()
82
+
83
+
41
84
  @app.command()
42
85
  def template(
43
86
  paths: list[Path] = Argument(..., help="The YAML file(s) to render. Can be a directory."),
@@ -88,6 +131,10 @@ def template(
88
131
  help="The directory to store cache data in. If not set, a directory in the --state-dir is used.",
89
132
  envvar="NYL_CACHE_DIR",
90
133
  ),
134
+ on_lookup_failure: OnLookupFailure | None = Option(
135
+ None,
136
+ help="Specify what to do when a lookup() call in a Nyl templated manifest fails. This overrides the nyl-project.toml setting if specified.",
137
+ ),
91
138
  ) -> None:
92
139
  """
93
140
  Render a package template into full Kubernetes resources.
@@ -125,24 +172,10 @@ def template(
125
172
  # plugin without granting it access to the Kubernetes API. Most relevant bits of information that Nyl requires
126
173
  # about the cluster are passed via the environment variables.
127
174
  # See https://argo-cd.readthedocs.io/en/stable/user-guide/build-environment/
128
- if in_cluster:
129
- logger.info("Using in-cluster configuration.")
130
- load_incluster_config()
131
- else:
132
- with ProfileManager.load(required=False) as profiles:
133
- # If no profile to activate is specified, and there are no profiles defined, we're not activating a
134
- # a profile. It should be valid to use Nyl without a `nyl-profiles.yaml` file.
135
- if profile is not None or profiles.config.profiles:
136
- profile = profile or DEFAULT_PROFILE
137
- active = profiles.activate_profile(profile)
138
- load_kube_config(str(active.kubeconfig))
139
- else:
140
- logger.opt(ansi=True).info(
141
- "No <yellow>nyl-profiles.yaml</> file found, using default kubeconfig and context."
142
- )
143
- load_kube_config()
175
+ PROVIDER.set(ApiClientConfig, ApiClientConfig(in_cluster=in_cluster, profile=profile))
176
+ client = PROVIDER.get(ApiClient)
144
177
 
145
- project = ProjectConfig.load()
178
+ project = PROVIDER.get(ProjectConfig)
146
179
  if generate_applysets is not None:
147
180
  project.config.settings.generate_applysets = generate_applysets
148
181
 
@@ -152,11 +185,14 @@ def template(
152
185
  if cache_dir is None:
153
186
  cache_dir = state_dir / "cache"
154
187
 
155
- secrets = SecretsConfig.load()
156
- client = ApiClient()
188
+ secrets = PROVIDER.get(SecretsConfig)
157
189
 
158
190
  template_engine = NylTemplateEngine(
159
- secrets.providers[secrets_provider], client, create_placeholders=project.config.settings.generate_placeholders
191
+ secrets.providers[secrets_provider],
192
+ client,
193
+ on_lookup_failure=on_lookup_failure.to_literal()
194
+ if on_lookup_failure
195
+ else project.config.settings.on_lookup_failure,
160
196
  )
161
197
 
162
198
  generator = DispatchingGenerator.default(