nyl 0.0.2__tar.gz → 0.0.4__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 (53) hide show
  1. nyl-0.0.4/PKG-INFO +31 -0
  2. nyl-0.0.4/README.md +9 -0
  3. {nyl-0.0.2 → nyl-0.0.4}/pyproject.toml +21 -11
  4. nyl-0.0.4/src/nyl/__init__.py +3 -0
  5. nyl-0.0.4/src/nyl/commands/__init__.py +68 -0
  6. nyl-0.0.4/src/nyl/commands/crds.py +14 -0
  7. nyl-0.0.4/src/nyl/commands/new.py +91 -0
  8. {nyl-0.0.2 → nyl-0.0.4}/src/nyl/commands/profile.py +2 -5
  9. {nyl-0.0.2 → nyl-0.0.4}/src/nyl/commands/secrets.py +2 -2
  10. nyl-0.0.4/src/nyl/commands/template.py +349 -0
  11. {nyl-0.0.2 → nyl-0.0.4}/src/nyl/commands/tun.py +7 -8
  12. nyl-0.0.4/src/nyl/generator/__init__.py +66 -0
  13. nyl-0.0.4/src/nyl/generator/dispatch.py +76 -0
  14. nyl-0.0.4/src/nyl/generator/dispatch_test.py +45 -0
  15. nyl-0.0.4/src/nyl/generator/helmchart.py +154 -0
  16. nyl-0.0.4/src/nyl/generator/statefulsecret.py +26 -0
  17. {nyl-0.0.2 → nyl-0.0.4}/src/nyl/profiles/__init__.py +54 -11
  18. {nyl-0.0.2 → nyl-0.0.4}/src/nyl/profiles/config.py +20 -40
  19. {nyl-0.0.2 → nyl-0.0.4}/src/nyl/profiles/kubeconfig.py +27 -23
  20. {nyl-0.0.2 → nyl-0.0.4}/src/nyl/profiles/tunnel.py +3 -3
  21. nyl-0.0.4/src/nyl/project/config.py +67 -0
  22. nyl-0.0.4/src/nyl/resources/__init__.py +118 -0
  23. nyl-0.0.4/src/nyl/resources/applyset.py +238 -0
  24. nyl-0.0.4/src/nyl/resources/applyset_test.py +39 -0
  25. {nyl-0.0.2 → nyl-0.0.4}/src/nyl/resources/helmchart.py +4 -10
  26. {nyl-0.0.2 → nyl-0.0.4}/src/nyl/resources/statefulsecret.py +2 -2
  27. {nyl-0.0.2 → nyl-0.0.4}/src/nyl/secrets/config.py +17 -22
  28. nyl-0.0.4/src/nyl/secrets/null.py +14 -0
  29. nyl-0.0.4/src/nyl/tools/fs.py +29 -0
  30. {nyl-0.0.2 → nyl-0.0.4}/src/nyl/tools/kubectl.py +19 -10
  31. nyl-0.0.4/src/nyl/tools/shell.py +6 -0
  32. nyl-0.0.4/src/nyl/tools/types.py +8 -0
  33. nyl-0.0.2/PKG-INFO +0 -155
  34. nyl-0.0.2/README.md +0 -136
  35. nyl-0.0.2/src/nyl/appmanager/__init__.py +0 -3
  36. nyl-0.0.2/src/nyl/appmanager/appmanager.py +0 -150
  37. nyl-0.0.2/src/nyl/appmanager/crd.py +0 -107
  38. nyl-0.0.2/src/nyl/commands/__init__.py +0 -19
  39. nyl-0.0.2/src/nyl/commands/template.py +0 -274
  40. nyl-0.0.2/src/nyl/packages/__init__.py +0 -3
  41. nyl-0.0.2/src/nyl/packages/metadata.py +0 -7
  42. nyl-0.0.2/src/nyl/packages/reference.py +0 -91
  43. nyl-0.0.2/src/nyl/packages/reference_test.py +0 -12
  44. nyl-0.0.2/src/nyl/packages/templating.py +0 -52
  45. nyl-0.0.2/src/nyl/resources/__init__.py +0 -62
  46. nyl-0.0.2/src/nyl/resources/package.py +0 -17
  47. nyl-0.0.2/tests/__init__.py +0 -0
  48. {nyl-0.0.2 → nyl-0.0.4}/src/nyl/secrets/__init__.py +0 -0
  49. {nyl-0.0.2 → nyl-0.0.4}/src/nyl/secrets/sops.py +0 -0
  50. {nyl-0.0.2 → nyl-0.0.4}/src/nyl/secrets/sops_test.py +0 -0
  51. {nyl-0.0.2 → nyl-0.0.4}/src/nyl/tools/kvstore.py +0 -0
  52. {nyl-0.0.2 → nyl-0.0.4}/src/nyl/tools/typer.py +0 -0
  53. {nyl-0.0.2/src/nyl → nyl-0.0.4/tests}/__init__.py +0 -0
nyl-0.0.4/PKG-INFO ADDED
@@ -0,0 +1,31 @@
1
+ Metadata-Version: 2.1
2
+ Name: nyl
3
+ Version: 0.0.4
4
+ Summary: Default template for PDM package
5
+ Author-Email: Niklas Rosenstein <rosensteinniklas@gmail.com>
6
+ License: MIT
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: bcrypt>=4.2.0
9
+ Requires-Dist: databind>=4.5.2
10
+ Requires-Dist: filelock>=3.15.4
11
+ Requires-Dist: jinja2>=3.1.4
12
+ Requires-Dist: kubernetes>=30.1.0
13
+ Requires-Dist: loguru>=0.7.2
14
+ Requires-Dist: nr-stream>=1.1.5
15
+ Requires-Dist: pyyaml>=6.0.1
16
+ Requires-Dist: requests>=2.32.3
17
+ Requires-Dist: stablehash>=0.2.0
18
+ Requires-Dist: structured-templates<0.1.0,>=0.0.5
19
+ Requires-Dist: typer>=0.12.3
20
+ Requires-Dist: typing-extensions>=4.12.2
21
+ Description-Content-Type: text/markdown
22
+
23
+ # nyl
24
+
25
+ Nyl is a versatile tool for generating Kubernetes manifests from a simple YAML configuration, encouraging
26
+ consistent and reusable deployment configurations, project layouts and operational workflows.
27
+
28
+ ## Local development
29
+
30
+ This project uses the [PDM](https://pdm-project.org/latest/) package manager for Python. Start with
31
+ `$ pipx install pdm` followed by `$ pdm install --frozen-lockfile`.
nyl-0.0.4/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # nyl
2
+
3
+ Nyl is a versatile tool for generating Kubernetes manifests from a simple YAML configuration, encouraging
4
+ consistent and reusable deployment configurations, project layouts and operational workflows.
5
+
6
+ ## Local development
7
+
8
+ This project uses the [PDM](https://pdm-project.org/latest/) package manager for Python. Start with
9
+ `$ pipx install pdm` followed by `$ pdm install --frozen-lockfile`.
@@ -1,21 +1,24 @@
1
1
  [project]
2
2
  name = "nyl"
3
- version = "0.0.2"
3
+ version = "0.0.4"
4
4
  description = "Default template for PDM package"
5
5
  authors = [
6
6
  { name = "Niklas Rosenstein", email = "rosensteinniklas@gmail.com" },
7
7
  ]
8
8
  dependencies = [
9
+ "bcrypt>=4.2.0",
9
10
  "databind>=4.5.2",
10
- "pyyaml>=6.0.1",
11
+ "filelock>=3.15.4",
11
12
  "jinja2>=3.1.4",
12
- "loguru>=0.7.2",
13
13
  "kubernetes>=30.1.0",
14
- "typer>=0.12.3",
15
- "filelock>=3.15.4",
16
- "stablehash>=0.2.0",
14
+ "loguru>=0.7.2",
17
15
  "nr-stream>=1.1.5",
16
+ "pyyaml>=6.0.1",
18
17
  "requests>=2.32.3",
18
+ "stablehash>=0.2.0",
19
+ "structured-templates>=0.0.5,<0.1.0",
20
+ "typer>=0.12.3",
21
+ "typing-extensions>=4.12.2",
19
22
  ]
20
23
  requires-python = ">=3.10"
21
24
  readme = "README.md"
@@ -34,17 +37,24 @@ build-backend = "pdm.backend"
34
37
 
35
38
  [tool.pdm]
36
39
  distribution = true
40
+ source = [
41
+ { name = "pypi", url = "https://pypi.org/simple" },
42
+ ]
43
+
44
+ [tool.pdm.scripts]
45
+ lint = "bash -c 'ruff format && ruff check --fix && mypy src' && pytest"
46
+ docs = "bash -c 'cd docs && mkdocs serve'"
37
47
 
38
48
  [tool.pdm.dev-dependencies]
39
49
  dev = [
40
- "ruff>=0.4.8",
41
- "mypy>=1.10.0",
42
- "types-pyyaml>=6.0.12.20240311",
43
- "mkdocs>=1.6.0",
50
+ "kubernetes-stubs>=22.6.0.post1",
44
51
  "mkdocs-material>=9.5.28",
52
+ "mkdocs>=1.6.0",
45
53
  "mkdocstrings[python]>=0.25.1",
54
+ "mypy>=1.10.0",
46
55
  "pytest>=8.2.2",
47
- "kubernetes-stubs>=22.6.0.post1",
56
+ "ruff>=0.4.8",
57
+ "types-pyyaml>=6.0.12.20240311",
48
58
  "types-requests>=2.32.0.20240712",
49
59
  ]
50
60
 
@@ -0,0 +1,3 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version("nyl")
@@ -0,0 +1,68 @@
1
+ """
2
+ Nyl is a flexible configuration management tool for Kubernetes resources that can be used to generate and deploy
3
+ applications directly or integrate as an ArgoCD ConfigManagementPlugin.
4
+ """
5
+
6
+ from pathlib import Path
7
+ from nyl import __version__
8
+ from enum import Enum
9
+ import sys
10
+ from loguru import logger
11
+ from typer import Option
12
+ from nyl.tools.typer import new_typer
13
+
14
+
15
+ app = new_typer(help=__doc__)
16
+
17
+
18
+ from . import crds # noqa: F401,E402
19
+ from . import new # noqa: E402
20
+ from . import profile # noqa: E402
21
+ from . import secrets # noqa: E402
22
+ from . import template # noqa: F401,E402
23
+ from . import tun # noqa: E402
24
+
25
+ app.add_typer(new.app)
26
+ app.add_typer(profile.app)
27
+ app.add_typer(secrets.app)
28
+ app.add_typer(tun.app)
29
+
30
+
31
+ LOG_TIME_FORMAT = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green>"
32
+ LOG_LEVEL_FORAMT = "<level>{level: <8}</level>"
33
+ LOG_DETAILS_FORMAT = "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan>"
34
+ LOG_MESSAGE_FORMAT = "<level>{message}</level>"
35
+
36
+
37
+ class LogLevel(str, Enum):
38
+ TRACE = "trace"
39
+ DEBUG = "debug"
40
+ INFO = "info"
41
+ WARNING = "warning"
42
+ ERROR = "error"
43
+ CRITICAL = "critical"
44
+
45
+
46
+ @app.callback()
47
+ def _callback(
48
+ log_level: LogLevel = Option(LogLevel.INFO, "--log-level", "-l", help="The log level to use."),
49
+ log_details: bool = Option(False, help="Include logger- and function names in the log message format."),
50
+ ) -> None:
51
+ if log_details:
52
+ fmt = f"{LOG_TIME_FORMAT} | {LOG_LEVEL_FORAMT} | {LOG_DETAILS_FORMAT} | {LOG_MESSAGE_FORMAT}"
53
+ else:
54
+ fmt = f"{LOG_TIME_FORMAT} | {LOG_LEVEL_FORAMT} | {LOG_MESSAGE_FORMAT}"
55
+
56
+ logger.remove()
57
+ logger.add(sys.stderr, level=log_level.name, format=fmt)
58
+ logger.opt(ansi=True).debug("Nyl v{} run from <yellow>{}</>.", __version__, Path.cwd())
59
+
60
+
61
+ @app.command()
62
+ def version() -> None:
63
+ """
64
+ Print the version of Nyl.
65
+ """
66
+
67
+ print(f"Nyl v{__version__}")
68
+ sys.exit(0)
@@ -0,0 +1,14 @@
1
+ import yaml
2
+
3
+ from nyl.resources.applyset import ApplySet
4
+ from . import app
5
+
6
+
7
+ @app.command()
8
+ def crds() -> None:
9
+ """
10
+ Print out the CRDs that need to be installed on a Kubernetes cluster before you can use all of Nyl's features.
11
+ """
12
+
13
+ print("---")
14
+ print(yaml.safe_dump(ApplySet.CRD))
@@ -0,0 +1,91 @@
1
+ """
2
+ Bootstrap files for a new Nyl project or Helm chart.
3
+ """
4
+
5
+ from pathlib import Path
6
+ from textwrap import dedent
7
+
8
+ from loguru import logger
9
+ from nyl.tools.typer import new_typer
10
+
11
+ app = new_typer(name="new", help=__doc__)
12
+
13
+
14
+ def _write_file_dedent(dir: Path, name: str, content: str) -> None:
15
+ path = dir.joinpath(name)
16
+ if path.exists():
17
+ logger.warning("File already exists: {}", path)
18
+ return
19
+ logger.info("Writing {}", path)
20
+ path.parent.mkdir(parents=True, exist_ok=True)
21
+ path.write_text(dedent(content).lstrip())
22
+
23
+
24
+ @app.command()
25
+ def chart(dir: Path) -> None:
26
+ """
27
+ Similar to `helm create`, but generates a much simpler template.
28
+ """
29
+
30
+ dir.mkdir(parents=True, exist_ok=True)
31
+
32
+ _write_file_dedent(
33
+ dir,
34
+ "Chart.yaml",
35
+ f"""
36
+ apiVersion: v2
37
+ name: {dir.name}
38
+ version: '0.0.0'
39
+ """,
40
+ )
41
+
42
+ _write_file_dedent(
43
+ dir,
44
+ "values.yaml",
45
+ """
46
+ image: my/image:tag
47
+ """,
48
+ )
49
+
50
+ _write_file_dedent(
51
+ dir,
52
+ "values.schema.json",
53
+ """
54
+ {
55
+ "$schema": "https://json-schema.org/draft/2020-12/output/schema",
56
+ "type": "object",
57
+ "required": [
58
+ "image"
59
+ ],
60
+ "properties": {
61
+ "image": {
62
+ "type": "string"
63
+ }
64
+ }
65
+ }
66
+ """,
67
+ )
68
+
69
+ _write_file_dedent(
70
+ dir,
71
+ "templates/deployment.yaml",
72
+ """
73
+ apiVersion: apps/v1
74
+ kind: Deployment
75
+ metadata:
76
+ name: {{ .Release.Name }}
77
+ spec:
78
+ replicas: 1
79
+ selector:
80
+ matchLabels:
81
+ app: {{ .Release.Name }}
82
+ template:
83
+ metadata:
84
+ labels:
85
+ app: {{ .Release.Name }}
86
+ spec:
87
+ containers:
88
+ - name: {{ .Release.Name }}
89
+ image: {{ .Values.image }}
90
+ """,
91
+ )
@@ -23,8 +23,5 @@ def activate(profile_name: str = Argument("default", envvar="NYL_PROFILE")) -> N
23
23
  with ProfileManager.load() as manager:
24
24
  profile = manager.activate_profile(profile_name)
25
25
 
26
- # For standard tooling, like kubectl.
27
- print(f"export KUBECONFIG={shlex.quote(str(profile.kubeconfig.absolute()))}")
28
-
29
- # Used by Terraform providers.
30
- print(f"export KUBE_CONFIG_PATH={shlex.quote(str(profile.kubeconfig.absolute()))}")
26
+ for key, value in profile.env.items():
27
+ print(f"export {key}={shlex.quote(value)}")
@@ -16,7 +16,7 @@ def list() -> None:
16
16
  List the keys for all secrets in the provider.
17
17
  """
18
18
 
19
- secrets = SecretsConfig.load(SecretsConfig.find_config_file())
19
+ secrets = SecretsConfig.load()
20
20
  for key in secrets.provider.keys():
21
21
  print(key)
22
22
 
@@ -27,5 +27,5 @@ def get(key: str, pretty: bool = False) -> None:
27
27
  Get the value of a secret as JSON.
28
28
  """
29
29
 
30
- secrets = SecretsConfig.load(SecretsConfig.find_config_file())
30
+ secrets = SecretsConfig.load()
31
31
  print(json.dumps(secrets.provider.get(key), indent=4 if pretty else None))
@@ -0,0 +1,349 @@
1
+ import atexit
2
+ from dataclasses import dataclass
3
+ from pathlib import Path
4
+ from typing import Optional, cast
5
+ from loguru import logger
6
+ from structured_templates import TemplateEngine
7
+ from typer import Argument, Option
8
+ import yaml
9
+ from nyl.generator import reconcile_generator
10
+ from nyl.generator.dispatch import DispatchingGenerator
11
+ from nyl.profiles import ProfileManager
12
+ from kubernetes.config.incluster_config import load_incluster_config
13
+ from kubernetes.config.kube_config import load_kube_config
14
+ from kubernetes.client.api_client import ApiClient
15
+ from nyl.project.config import ProjectConfig
16
+ from nyl.resources.applyset import APPLYSET_LABEL_PART_OF, ApplySet
17
+ from nyl.secrets.config import SecretsConfig
18
+ from nyl.tools.kubectl import Kubectl
19
+ from nyl.tools.types import Manifest, Manifests
20
+
21
+ from . import app
22
+
23
+ DEFAULT_PROFILE = "default"
24
+
25
+
26
+ @dataclass
27
+ class ManifestsWithSource:
28
+ """
29
+ Represents a list of manifests loaded from a particular source file.
30
+ """
31
+
32
+ manifests: Manifests
33
+ file: Path
34
+
35
+
36
+ @app.command()
37
+ def template(
38
+ paths: list[Path] = Argument(..., help="The YAML file(s) to render. Can be a directory."),
39
+ profile: Optional[str] = Option(None, envvar="NYL_PROFILE", help="The Nyl profile to use."),
40
+ in_cluster: bool = Option(
41
+ False, help="Use the in-cluster Kubernetes configuration. The --profile option is ignored."
42
+ ),
43
+ apply: bool = Option(
44
+ False,
45
+ help="Run `kubectl apply` on the rendered manifests, once for each source file. "
46
+ "Implies `--no-applyset-part-of`. When an ApplySet is defined in the source file, it will be applied "
47
+ "separately. Note that this option implies `kubectl --prune`.",
48
+ ),
49
+ applyset_part_of: bool = Option(
50
+ True,
51
+ help="Add the 'applyset.kubernetes.io/part-of' label to all resources belonging to an ApplySet (if declared). "
52
+ "This option must be disabled when passing the generated manifests to `kubectl apply --applyset=...`, as it "
53
+ "would otherwise cause an error due to the label being present on the input data.",
54
+ ),
55
+ ) -> None:
56
+ """
57
+ Render a package template into full Kubernetes resources.
58
+ """
59
+
60
+ if apply:
61
+ # When running with --apply, we must ensure that the --applyset-part-of option is disabled, as it would cause
62
+ # an error when passing the generated manifests to `kubectl apply --applyset=...`.
63
+ applyset_part_of = False
64
+
65
+ kubectl = Kubectl()
66
+ kubectl.env["KUBECTL_APPLYSET"] = "true"
67
+ atexit.register(kubectl.cleanup)
68
+
69
+ if in_cluster:
70
+ logger.info("Using in-cluster configuration.")
71
+ load_incluster_config()
72
+ else:
73
+ with ProfileManager.load(required=False) as profiles:
74
+ # If no profile to activate is specified, and there are no profiles defined, we're not activating a
75
+ # a profile. It should be valid to use Nyl without a `nyl-profiles.yaml` file.
76
+ if profile is not None or profiles.config.profiles:
77
+ profile = profile or DEFAULT_PROFILE
78
+ active = profiles.activate_profile(profile)
79
+ load_kube_config(str(active.kubeconfig))
80
+ else:
81
+ logger.opt(ansi=True).info(
82
+ "No <yellow>nyl-profiles.yaml</> file found, using default kubeconfig and context."
83
+ )
84
+ load_kube_config()
85
+
86
+ project = ProjectConfig.load()
87
+ if project.file:
88
+ state_dir = project.file.parent / ".nyl"
89
+ else:
90
+ state_dir = Path(".nyl")
91
+
92
+ secrets = SecretsConfig.load()
93
+
94
+ template_engine = TemplateEngine(
95
+ globals_={
96
+ "secrets": secrets.provider,
97
+ "random_password": _random_password,
98
+ "bcrypt": _bcrypt,
99
+ }
100
+ )
101
+
102
+ generator = DispatchingGenerator.default(
103
+ git_repo_cache_dir=state_dir / "repo-cache",
104
+ search_path=project.config.search_path,
105
+ working_dir=Path.cwd(),
106
+ client=ApiClient(),
107
+ )
108
+
109
+ for source in load_manifests(paths):
110
+ logger.opt(ansi=True).info("Rendering manifests from <blue>{}</>.", source.file)
111
+
112
+ source.manifests = cast(Manifests, template_engine.evaluate(source.manifests))
113
+ source.manifests = reconcile_generator(
114
+ generator,
115
+ source.manifests,
116
+ on_generated=lambda m: cast(Manifest, template_engine.evaluate(m)),
117
+ )
118
+
119
+ # Find the namespaces that are defined in the file. If we find any manifests without a namespace, we will
120
+ # inject that namespace name into them.
121
+ # Also find the applyset defined in the file.
122
+ namespaces: set[str] = set()
123
+ applyset: ApplySet | None = None
124
+ for manifest in list(source.manifests):
125
+ if is_namespace_resource(manifest):
126
+ namespaces.add(manifest["metadata"]["name"])
127
+ elif ApplySet.matches(manifest):
128
+ if applyset is not None:
129
+ logger.opt(ansi=True).error(
130
+ "Multiple ApplySet resources defined in <yellow>{}</>, there can only be one per source.",
131
+ source.file,
132
+ )
133
+ exit(1)
134
+ applyset = ApplySet.load(manifest)
135
+ source.manifests.remove(manifest)
136
+
137
+ if not applyset and project.config.automatic_applyset:
138
+ if len(namespaces) > 1:
139
+ logger.opt(ansi=True).error(
140
+ "Multiple namespaces defined in <yellow>{}</>, but automatic ApplySet generation is enabled. "
141
+ "There can only be one namespace per source in this case.",
142
+ source.file,
143
+ )
144
+ exit(1)
145
+ elif len(namespaces) == 1:
146
+ applyset_name = next(iter(namespaces))
147
+ else:
148
+ applyset_name = source.file.stem
149
+
150
+ logger.opt(ansi=True).info(
151
+ "Automatically creating ApplySet for <blue>{}</> (name: <magenta>{}</>).", source.file, applyset_name
152
+ )
153
+ applyset = ApplySet.new(applyset_name)
154
+
155
+ if applyset is not None:
156
+ applyset.set_group_kinds(source.manifests)
157
+ # HACK: Kubectl 1.30 can't create the custom resource, so we need to create it. But it will also reject
158
+ # using the custom resource unless it has the tooling label set appropriately. For more details, see
159
+ # https://github.com/NiklasRosenstein/nyl/issues/5.
160
+ applyset.tooling = "kubectl/1.30"
161
+ applyset.validate()
162
+
163
+ if apply:
164
+ # We need to ensure that ApplySet parent object exists before invoking `kubectl apply --applyset=...`.
165
+ logger.opt(ansi=True).info(
166
+ "Kubectl-apply ApplySet resource <yellow>{}</> from <cyan>{}</>.",
167
+ applyset.reference,
168
+ source.file,
169
+ )
170
+ kubectl.apply(Manifests([applyset.dump()]), force_conflicts=True)
171
+ else:
172
+ print("---")
173
+ print(yaml.safe_dump(applyset.dump()))
174
+
175
+ # Find all manifests without a namespace and inject the namespace name into them.
176
+ # If there is an applyset, ensure they are marked as part of the applyset.
177
+ for manifest in source.manifests:
178
+ if applyset is not None and applyset_part_of:
179
+ if APPLYSET_LABEL_PART_OF not in (labels := manifest["metadata"].setdefault("labels", {})):
180
+ labels[APPLYSET_LABEL_PART_OF] = applyset.id
181
+
182
+ if not is_cluster_scoped_resource(manifest) and "namespace" not in manifest["metadata"]:
183
+ if len(namespaces) > 1:
184
+ logger.error(
185
+ "Multiple namespaces defined in '{}', but manifest {}/{} does not have a namespace.",
186
+ source.file,
187
+ manifest["kind"],
188
+ manifest["metadata"]["name"],
189
+ )
190
+ exit(1)
191
+ elif len(namespaces) == 0:
192
+ logger.warning(
193
+ "No namespaces defined in '{}', injecting 'default' into manifest {}/{}.",
194
+ source.file,
195
+ manifest["kind"],
196
+ manifest["metadata"]["name"],
197
+ )
198
+ manifest["metadata"]["namespace"] = "default"
199
+ else:
200
+ logger.trace(
201
+ "Injecting namespace '{}' into manifest {}/{}.",
202
+ next(iter(namespaces)),
203
+ manifest["kind"],
204
+ manifest["metadata"]["name"],
205
+ )
206
+ manifest["metadata"]["namespace"] = next(iter(namespaces))
207
+ elif not is_cluster_scoped_resource(manifest) and manifest["metadata"]["namespace"] not in namespaces:
208
+ logger.warning(
209
+ "Resource '{}/{}' in '{}' references namespace '{}', which is not defined in the file.",
210
+ manifest["kind"],
211
+ manifest["metadata"]["name"],
212
+ source.file,
213
+ manifest["metadata"]["namespace"],
214
+ )
215
+
216
+ if not apply:
217
+ print("---")
218
+ print(yaml.safe_dump(manifest))
219
+
220
+ if apply:
221
+ logger.info("Kubectl-apply {} manifest(s) from '{}'", len(source.manifests), source.file)
222
+ kubectl.apply(
223
+ manifests=source.manifests,
224
+ applyset=applyset.reference if applyset else None,
225
+ prune=True if applyset else False,
226
+ force_conflicts=True,
227
+ )
228
+
229
+
230
+ def load_manifests(paths: list[Path]) -> list[ManifestsWithSource]:
231
+ """
232
+ Load all manifests from a directory.
233
+ """
234
+
235
+ logger.trace("Loading manifests from paths: {}", paths)
236
+
237
+ files = []
238
+ for path in paths:
239
+ if path.is_dir():
240
+ for item in path.iterdir():
241
+ if (
242
+ item.name.startswith("nyl-")
243
+ or item.name.startswith(".")
244
+ or item.name.startswith("_")
245
+ or item.suffix != ".yaml"
246
+ or not item.is_file()
247
+ ):
248
+ continue
249
+ files.append(item)
250
+ else:
251
+ files.append(path)
252
+
253
+ logger.trace("Files to load: {}", files)
254
+
255
+ result = []
256
+ for file in files:
257
+ manifests = Manifests(list(map(Manifest, yaml.safe_load_all(file.read_text()))))
258
+ result.append(ManifestsWithSource(manifests, file))
259
+
260
+ return result
261
+
262
+ # # Check if the resource has any references and try to resolve them. If a reference cannot be resolved, the
263
+ # # manifest must be skipped. We emit a warning and continue with the next manifest.
264
+ # refs = list(Reference.collect(manifest))
265
+ # if refs:
266
+ # skip_resource = False
267
+ # resolves = {}
268
+ # for ref in refs:
269
+ # # TODO: Determine the namespace to fall back to.
270
+ # try:
271
+ # result = k8s.get(
272
+ # resource=k8s.resources.get(api_version="v1", kind=ref.kind, group=""),
273
+ # name=ref.name,
274
+ # namespace=ref.namespace, # TODO: Determine the namespace to backfill to.
275
+ # )
276
+
277
+ # value = result["data"][ref.key]
278
+ # if value is None:
279
+ # raise KeyError
280
+ # assert isinstance(value, str)
281
+ # resolves[str(ref)] = base64.b64decode(value.encode("ascii")).decode("utf-8")
282
+ # except NotFoundError:
283
+ # logger.warning(
284
+ # "Skipping resource {}/{} because its reference to {} could not be resolved.",
285
+ # manifest["apiVersion"],
286
+ # manifest["kind"],
287
+ # ref,
288
+ # )
289
+ # skip_resource = True
290
+ # break
291
+ # except KeyError:
292
+ # logger.warning(
293
+ # "Skipping resource {}/{} because its reference to {} could not be resolved (does not contain key {}).",
294
+ # manifest["apiVersion"],
295
+ # manifest["kind"],
296
+ # ref,
297
+ # ref.key,
298
+ # )
299
+ # skip_resource = True
300
+ # break
301
+ # if skip_resource:
302
+ # continue
303
+ # manifest = Reference.sub(manifest, lambda ref: resolves[str(ref)])
304
+
305
+
306
+ def _random_password(length: int = 32) -> str:
307
+ """
308
+ Generate a random password.
309
+ """
310
+
311
+ import secrets
312
+
313
+ return secrets.token_urlsafe(length)
314
+
315
+
316
+ def _bcrypt(password: str) -> str:
317
+ """
318
+ Hash a password using bcrypt.
319
+ """
320
+
321
+ import bcrypt
322
+
323
+ return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
324
+
325
+
326
+ def is_namespace_resource(manifest: Manifest) -> bool:
327
+ """
328
+ Check if a manifest is a namespace resource.
329
+ """
330
+
331
+ return manifest.get("apiVersion") == "v1" and manifest.get("kind") == "Namespace"
332
+
333
+
334
+ def is_cluster_scoped_resource(manifest: Manifest) -> bool:
335
+ """
336
+ Check if a manifest is a cluster scoped resource.
337
+ """
338
+
339
+ # HACK: We should probably just list the resources via the Kubectl API?
340
+ fqn = manifest.get("kind", "") + "." + manifest.get("apiVersion", "").split("/")[0]
341
+ return fqn in {
342
+ "ClusterRole.rbac.authorization.k8s.io",
343
+ "ClusterRoleBinding.rbac.authorization.k8s.io",
344
+ "CustomResourceDefinition.apiextensions.k8s.io",
345
+ "IngressClass.networking.k8s.io",
346
+ "Namespace.v1",
347
+ "StorageClass.storage.k8s.io",
348
+ "ValidatingWebhookConfiguration.admissionregistration.k8s.io",
349
+ }