nyl 0.0.3__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.
- nyl-0.0.4/PKG-INFO +31 -0
- nyl-0.0.4/README.md +9 -0
- {nyl-0.0.3 → nyl-0.0.4}/pyproject.toml +5 -1
- nyl-0.0.4/src/nyl/__init__.py +3 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/commands/__init__.py +26 -1
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/commands/template.py +49 -11
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/profiles/__init__.py +30 -10
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/profiles/kubeconfig.py +24 -22
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/profiles/tunnel.py +3 -3
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/project/config.py +9 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/resources/applyset.py +13 -0
- nyl-0.0.4/src/nyl/tools/shell.py +6 -0
- nyl-0.0.3/PKG-INFO +0 -158
- nyl-0.0.3/README.md +0 -136
- nyl-0.0.3/tests/__init__.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/commands/crds.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/commands/new.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/commands/profile.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/commands/secrets.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/commands/tun.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/generator/__init__.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/generator/dispatch.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/generator/dispatch_test.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/generator/helmchart.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/generator/statefulsecret.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/profiles/config.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/resources/__init__.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/resources/applyset_test.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/resources/helmchart.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/resources/statefulsecret.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/secrets/__init__.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/secrets/config.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/secrets/null.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/secrets/sops.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/secrets/sops_test.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/tools/fs.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/tools/kubectl.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/tools/kvstore.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/tools/typer.py +0 -0
- {nyl-0.0.3 → nyl-0.0.4}/src/nyl/tools/types.py +0 -0
- {nyl-0.0.3/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,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nyl"
|
|
3
|
-
version = "0.0.
|
|
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" },
|
|
@@ -37,9 +37,13 @@ build-backend = "pdm.backend"
|
|
|
37
37
|
|
|
38
38
|
[tool.pdm]
|
|
39
39
|
distribution = true
|
|
40
|
+
source = [
|
|
41
|
+
{ name = "pypi", url = "https://pypi.org/simple" },
|
|
42
|
+
]
|
|
40
43
|
|
|
41
44
|
[tool.pdm.scripts]
|
|
42
45
|
lint = "bash -c 'ruff format && ruff check --fix && mypy src' && pytest"
|
|
46
|
+
docs = "bash -c 'cd docs && mkdocs serve'"
|
|
43
47
|
|
|
44
48
|
[tool.pdm.dev-dependencies]
|
|
45
49
|
dev = [
|
|
@@ -3,6 +3,8 @@ Nyl is a flexible configuration management tool for Kubernetes resources that ca
|
|
|
3
3
|
applications directly or integrate as an ArgoCD ConfigManagementPlugin.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from nyl import __version__
|
|
6
8
|
from enum import Enum
|
|
7
9
|
import sys
|
|
8
10
|
from loguru import logger
|
|
@@ -26,6 +28,12 @@ app.add_typer(secrets.app)
|
|
|
26
28
|
app.add_typer(tun.app)
|
|
27
29
|
|
|
28
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
|
+
|
|
29
37
|
class LogLevel(str, Enum):
|
|
30
38
|
TRACE = "trace"
|
|
31
39
|
DEBUG = "debug"
|
|
@@ -38,6 +46,23 @@ class LogLevel(str, Enum):
|
|
|
38
46
|
@app.callback()
|
|
39
47
|
def _callback(
|
|
40
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."),
|
|
41
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
|
+
|
|
42
56
|
logger.remove()
|
|
43
|
-
logger.add(sys.stderr, level=log_level.name)
|
|
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)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import atexit
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import cast
|
|
4
|
+
from typing import Optional, cast
|
|
5
5
|
from loguru import logger
|
|
6
6
|
from structured_templates import TemplateEngine
|
|
7
7
|
from typer import Argument, Option
|
|
@@ -20,6 +20,8 @@ from nyl.tools.types import Manifest, Manifests
|
|
|
20
20
|
|
|
21
21
|
from . import app
|
|
22
22
|
|
|
23
|
+
DEFAULT_PROFILE = "default"
|
|
24
|
+
|
|
23
25
|
|
|
24
26
|
@dataclass
|
|
25
27
|
class ManifestsWithSource:
|
|
@@ -34,7 +36,7 @@ class ManifestsWithSource:
|
|
|
34
36
|
@app.command()
|
|
35
37
|
def template(
|
|
36
38
|
paths: list[Path] = Argument(..., help="The YAML file(s) to render. Can be a directory."),
|
|
37
|
-
profile: str = Option(
|
|
39
|
+
profile: Optional[str] = Option(None, envvar="NYL_PROFILE", help="The Nyl profile to use."),
|
|
38
40
|
in_cluster: bool = Option(
|
|
39
41
|
False, help="Use the in-cluster Kubernetes configuration. The --profile option is ignored."
|
|
40
42
|
),
|
|
@@ -68,10 +70,18 @@ def template(
|
|
|
68
70
|
logger.info("Using in-cluster configuration.")
|
|
69
71
|
load_incluster_config()
|
|
70
72
|
else:
|
|
71
|
-
with ProfileManager.load() as profiles:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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()
|
|
75
85
|
|
|
76
86
|
project = ProjectConfig.load()
|
|
77
87
|
if project.file:
|
|
@@ -97,7 +107,7 @@ def template(
|
|
|
97
107
|
)
|
|
98
108
|
|
|
99
109
|
for source in load_manifests(paths):
|
|
100
|
-
logger.info("Rendering manifests from {}", source.file)
|
|
110
|
+
logger.opt(ansi=True).info("Rendering manifests from <blue>{}</>.", source.file)
|
|
101
111
|
|
|
102
112
|
source.manifests = cast(Manifests, template_engine.evaluate(source.manifests))
|
|
103
113
|
source.manifests = reconcile_generator(
|
|
@@ -116,14 +126,32 @@ def template(
|
|
|
116
126
|
namespaces.add(manifest["metadata"]["name"])
|
|
117
127
|
elif ApplySet.matches(manifest):
|
|
118
128
|
if applyset is not None:
|
|
119
|
-
logger.error(
|
|
120
|
-
"Multiple ApplySet resources defined in
|
|
129
|
+
logger.opt(ansi=True).error(
|
|
130
|
+
"Multiple ApplySet resources defined in <yellow>{}</>, there can only be one per source.",
|
|
121
131
|
source.file,
|
|
122
132
|
)
|
|
123
133
|
exit(1)
|
|
124
134
|
applyset = ApplySet.load(manifest)
|
|
125
135
|
source.manifests.remove(manifest)
|
|
126
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
|
+
|
|
127
155
|
if applyset is not None:
|
|
128
156
|
applyset.set_group_kinds(source.manifests)
|
|
129
157
|
# HACK: Kubectl 1.30 can't create the custom resource, so we need to create it. But it will also reject
|
|
@@ -134,7 +162,11 @@ def template(
|
|
|
134
162
|
|
|
135
163
|
if apply:
|
|
136
164
|
# We need to ensure that ApplySet parent object exists before invoking `kubectl apply --applyset=...`.
|
|
137
|
-
logger.info(
|
|
165
|
+
logger.opt(ansi=True).info(
|
|
166
|
+
"Kubectl-apply ApplySet resource <yellow>{}</> from <cyan>{}</>.",
|
|
167
|
+
applyset.reference,
|
|
168
|
+
source.file,
|
|
169
|
+
)
|
|
138
170
|
kubectl.apply(Manifests([applyset.dump()]), force_conflicts=True)
|
|
139
171
|
else:
|
|
140
172
|
print("---")
|
|
@@ -206,7 +238,13 @@ def load_manifests(paths: list[Path]) -> list[ManifestsWithSource]:
|
|
|
206
238
|
for path in paths:
|
|
207
239
|
if path.is_dir():
|
|
208
240
|
for item in path.iterdir():
|
|
209
|
-
if
|
|
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
|
+
):
|
|
210
248
|
continue
|
|
211
249
|
files.append(item)
|
|
212
250
|
else:
|
|
@@ -42,12 +42,16 @@ class ProfileManager:
|
|
|
42
42
|
tunnels: TunnelManager
|
|
43
43
|
kubeconfig: KubeconfigManager
|
|
44
44
|
|
|
45
|
+
def __post_init__(self) -> None:
|
|
46
|
+
# We don't need to enter the TunnelsManager context manager until we actually need to open a tunnel.
|
|
47
|
+
self._tunnels_entered: bool = False
|
|
48
|
+
|
|
45
49
|
def __enter__(self) -> "ProfileManager":
|
|
46
|
-
self.tunnels.__enter__()
|
|
47
50
|
return self
|
|
48
51
|
|
|
49
52
|
def __exit__(self, *args: Any) -> None:
|
|
50
|
-
self.
|
|
53
|
+
if self._tunnels_entered:
|
|
54
|
+
self.tunnels.__exit__(*args)
|
|
51
55
|
|
|
52
56
|
def activate_profile(self, profile_name: str, update_process_env: bool = True) -> ActivatedProfile:
|
|
53
57
|
"""
|
|
@@ -59,10 +63,18 @@ class ProfileManager:
|
|
|
59
63
|
point to the activated profile's Kubeconfig file.
|
|
60
64
|
"""
|
|
61
65
|
|
|
66
|
+
logger.opt(ansi=True).info("Activating profile <magenta>{}</>...", profile_name)
|
|
67
|
+
|
|
62
68
|
profile = self.config.profiles[profile_name]
|
|
69
|
+
|
|
63
70
|
raw_kubeconfig = self.kubeconfig.get_raw_kubeconfig(profile_name, profile.kubeconfig)
|
|
64
71
|
|
|
65
72
|
if profile.tunnel:
|
|
73
|
+
# If the tunnel manager has not been entered yet, do so now.
|
|
74
|
+
if not self._tunnels_entered:
|
|
75
|
+
self.tunnels.__enter__()
|
|
76
|
+
self._tunnels_entered = True
|
|
77
|
+
|
|
66
78
|
forwardings = {"kubernetes": f"{raw_kubeconfig.api_host}:{raw_kubeconfig.api_port}"}
|
|
67
79
|
assert self.config.file is not None, "Profile configuration file must be set."
|
|
68
80
|
tun_spec = get_tunnel_spec(self.config.file, profile_name, profile.tunnel)
|
|
@@ -80,10 +92,6 @@ class ProfileManager:
|
|
|
80
92
|
tun_description = ""
|
|
81
93
|
timeout = 2
|
|
82
94
|
|
|
83
|
-
api_server = f"https://{raw_kubeconfig.api_host}:{raw_kubeconfig.api_port}"
|
|
84
|
-
logger.debug("Checking for API server connectivity ({}{})", api_server, tun_description)
|
|
85
|
-
_wait_for_api_server(api_server, timeout)
|
|
86
|
-
|
|
87
95
|
activated_profile = ActivatedProfile(
|
|
88
96
|
kubeconfig=self.kubeconfig.get_updated_kubeconfig(
|
|
89
97
|
profile_name=profile_name,
|
|
@@ -94,6 +102,10 @@ class ProfileManager:
|
|
|
94
102
|
)
|
|
95
103
|
)
|
|
96
104
|
|
|
105
|
+
api_server = f"https://{raw_kubeconfig.api_host}:{raw_kubeconfig.api_port}"
|
|
106
|
+
logger.opt(ansi=True).info("Waiting for API server connectivity (<blue>{}{}</>)", api_server, tun_description)
|
|
107
|
+
_wait_for_api_server(api_server, timeout)
|
|
108
|
+
|
|
97
109
|
if update_process_env:
|
|
98
110
|
logger.trace("Updating process environment with activated profile: {}", activated_profile.env)
|
|
99
111
|
os.environ.update(activated_profile.env)
|
|
@@ -101,15 +113,23 @@ class ProfileManager:
|
|
|
101
113
|
return activated_profile
|
|
102
114
|
|
|
103
115
|
@staticmethod
|
|
104
|
-
def load() -> "ProfileManager":
|
|
116
|
+
def load(required: bool = True) -> "ProfileManager":
|
|
105
117
|
"""
|
|
106
118
|
Load the profile manager from the default configuration file.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
required: Passed on to [`ProfileConfig.load()`]. If it is set to `False`, the Nyl state directory
|
|
122
|
+
is assumed relative to the current working directory (`./.nyl`).
|
|
107
123
|
"""
|
|
108
124
|
|
|
109
|
-
config = ProfileConfig.load()
|
|
110
|
-
|
|
125
|
+
config = ProfileConfig.load(required=required)
|
|
126
|
+
context_dir = config.file.parent if config.file else Path.cwd()
|
|
127
|
+
|
|
111
128
|
tunnels = TunnelManager()
|
|
112
|
-
kubeconfig = KubeconfigManager(
|
|
129
|
+
kubeconfig = KubeconfigManager(
|
|
130
|
+
cwd=context_dir,
|
|
131
|
+
state_dir=context_dir / ".nyl" / "profiles",
|
|
132
|
+
)
|
|
113
133
|
return ProfileManager(config, tunnels, kubeconfig)
|
|
114
134
|
|
|
115
135
|
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
import os
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
import shlex
|
|
4
5
|
import subprocess
|
|
5
6
|
from typing import Any
|
|
6
7
|
from urllib.parse import urlparse
|
|
7
8
|
import yaml
|
|
9
|
+
|
|
10
|
+
from nyl.tools.shell import pretty_cmd
|
|
8
11
|
from .config import KubeconfigFromSsh, LocalKubeconfig
|
|
9
12
|
from loguru import logger
|
|
10
13
|
|
|
@@ -55,42 +58,41 @@ class KubeconfigManager:
|
|
|
55
58
|
match source:
|
|
56
59
|
case LocalKubeconfig():
|
|
57
60
|
raw_kubeconfig = Path(os.getenv("KUBECONFIG", os.path.expanduser("~/.kube/config")))
|
|
58
|
-
if
|
|
61
|
+
if not raw_kubeconfig.exists():
|
|
59
62
|
raise FileNotFoundError(f"Kubeconfig file '{raw_kubeconfig}' does not exist.")
|
|
60
|
-
logger.info("Using
|
|
63
|
+
logger.opt(ansi=True).info("Using kubeconfig <yellow>{}</>.", raw_kubeconfig)
|
|
61
64
|
|
|
62
65
|
case KubeconfigFromSsh():
|
|
63
66
|
raw_kubeconfig = self._state_dir / profile_name / "kubeconfig.orig"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
|
|
68
|
+
command = [
|
|
69
|
+
"ssh",
|
|
70
|
+
f"{source.user}@{source.host}",
|
|
71
|
+
f"cat {shlex.quote(source.path)}",
|
|
72
|
+
]
|
|
73
|
+
if source.identity_file:
|
|
74
|
+
command[1:1] = ["-i", str(self._cwd / source.identity_file)]
|
|
75
|
+
|
|
76
|
+
# Fetch the Kubeconfig file.
|
|
77
|
+
if not raw_kubeconfig.exists() or force_refresh:
|
|
78
|
+
logger.opt(ansi=True).info("Fetching kubeconfig with <yellow>$ {}</>.", pretty_cmd(command))
|
|
72
79
|
raw_kubeconfig.parent.mkdir(parents=True, exist_ok=True)
|
|
73
|
-
command = [
|
|
74
|
-
"ssh",
|
|
75
|
-
f"{source.user}@{source.host}",
|
|
76
|
-
"cat",
|
|
77
|
-
source.path,
|
|
78
|
-
]
|
|
79
|
-
if source.identity_file:
|
|
80
|
-
command[1:1] = ["-i", str(self._cwd / source.identity_file)]
|
|
81
80
|
kubeconfig_content = subprocess.check_output(command, text=True)
|
|
82
81
|
Path(raw_kubeconfig).write_text(kubeconfig_content)
|
|
83
82
|
else:
|
|
84
|
-
logger.
|
|
83
|
+
logger.opt(ansi=True).info(
|
|
84
|
+
"Using <u>cached</> kubeconfig from <yellow>$ {}</>.", pretty_cmd(command)
|
|
85
|
+
)
|
|
85
86
|
case _:
|
|
86
87
|
raise ValueError(f"Unsupported Kubeconfig type: {source.__class__.__name__}")
|
|
87
88
|
|
|
88
89
|
# Find the Kubernetes API host and port.
|
|
89
90
|
kubeconfig_data = yaml.safe_load(raw_kubeconfig.read_text())
|
|
90
91
|
kubeconfig_data = _trim_to_context(kubeconfig_data, source.context)
|
|
91
|
-
server = kubeconfig_data["clusters"][0]["cluster"]["server"]
|
|
92
|
-
api_host = urlparse(server)
|
|
93
|
-
|
|
92
|
+
server: str = kubeconfig_data["clusters"][0]["cluster"]["server"]
|
|
93
|
+
api_host, api_port = (lambda parsed: (parsed.hostname, parsed.port or 443))(urlparse(server))
|
|
94
|
+
if api_host is None:
|
|
95
|
+
raise ValueError(f"no hostname in Kubeconfig server URL: {server!r}")
|
|
94
96
|
|
|
95
97
|
raw_kubeconfig.chmod(0o600)
|
|
96
98
|
return GetRawKubeconfigResult(
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import shlex
|
|
2
1
|
from dataclasses import dataclass
|
|
3
2
|
import os
|
|
4
3
|
import random
|
|
@@ -10,6 +9,7 @@ from loguru import logger
|
|
|
10
9
|
|
|
11
10
|
from stablehash import stablehash
|
|
12
11
|
from nyl.tools.kvstore import JsonFileKvStore, SerializingStore
|
|
12
|
+
from nyl.tools.shell import pretty_cmd
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@dataclass
|
|
@@ -169,7 +169,7 @@ class TunnelManager:
|
|
|
169
169
|
# Check if the tunnel is open and up-to-date.
|
|
170
170
|
spec_hash = stablehash(spec).hexdigest()
|
|
171
171
|
if status is not None and status.status == "open" and status.spec_hash == spec_hash:
|
|
172
|
-
logger.
|
|
172
|
+
logger.opt(ansi=True).info("Reusing existing SSH tunnel.", spec.locator)
|
|
173
173
|
return status
|
|
174
174
|
|
|
175
175
|
# Close the tunnel if it is open.
|
|
@@ -206,7 +206,7 @@ class TunnelManager:
|
|
|
206
206
|
if spec.identity_file is not None:
|
|
207
207
|
ssh_args.extend(["-i", spec.identity_file])
|
|
208
208
|
|
|
209
|
-
logger.
|
|
209
|
+
logger.opt(ansi=True).info("Opening SSH tunnel with <yellow>$ {}</>.", pretty_cmd(ssh_args))
|
|
210
210
|
proc = subprocess.Popen(ssh_args)
|
|
211
211
|
|
|
212
212
|
# Update the status.
|
|
@@ -12,6 +12,15 @@ class Project:
|
|
|
12
12
|
Configuration for a Nyl project that is stored in a `nyl-project.toml` file.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
+
automatic_applyset: bool = True
|
|
16
|
+
"""
|
|
17
|
+
Automatically generate an ApplySet for every template file. The applyset will be named after the template file,
|
|
18
|
+
unless there is namespace defined in the template file, in which case the applyset will be named after the
|
|
19
|
+
namespace.
|
|
20
|
+
|
|
21
|
+
This option is turned on by default. Using Nyl without a `nyl-project.toml` file will have this option enabled.
|
|
22
|
+
"""
|
|
23
|
+
|
|
15
24
|
search_path: list[Path] = field(default_factory=list)
|
|
16
25
|
"""
|
|
17
26
|
Search path for additional resources used by the project. Used for example when using the `chart.path` option on a
|
|
@@ -194,6 +194,19 @@ class ApplySet(NylResource, api_version=API_VERSION_K8S):
|
|
|
194
194
|
if self.contains_group_kinds is None:
|
|
195
195
|
raise ValueError(f"ApplySet resource must have a {APPLYSET_ANNOTATION_CONTAINS_GROUP_KINDS!r} annotation")
|
|
196
196
|
|
|
197
|
+
@staticmethod
|
|
198
|
+
def new(name: str) -> "ApplySet":
|
|
199
|
+
"""
|
|
200
|
+
Create a new ApplySet resource with the specified name.
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
return ApplySet(
|
|
204
|
+
metadata=ObjectMetadata(
|
|
205
|
+
name=name,
|
|
206
|
+
namespace=None,
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
|
|
197
210
|
|
|
198
211
|
def calculate_applyset_id(*, name: str, namespace: str = "", group: str) -> str:
|
|
199
212
|
"""
|
nyl-0.0.3/PKG-INFO
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: nyl
|
|
3
|
-
Version: 0.0.3
|
|
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 Kubernetes resource management tool that fulfills various functions to enable reusable
|
|
26
|
-
deployment configurations.
|
|
27
|
-
|
|
28
|
-
__Features__
|
|
29
|
-
|
|
30
|
-
* Generate Kubernetes manifests from a simple YAML configuration with templating support.
|
|
31
|
-
* Inject secrets into Kubernetes manifests as time of generation.
|
|
32
|
-
* Bootstrap a cluster and transfer control to ArgoCD or use it to manage a cluster from CI/CD or locally.
|
|
33
|
-
* Connect to a Kubernetes cluster via an SSH tunnel.
|
|
34
|
-
|
|
35
|
-
__Roadmap__
|
|
36
|
-
|
|
37
|
-
* Integrate as an ArgoCD ConfigManagementPlugin.
|
|
38
|
-
* Diffing
|
|
39
|
-
* How to work around fields owned by a different field manager (e.g. MetalLB CRD `conversion..caBundle`) or `Job` resources?
|
|
40
|
-
|
|
41
|
-
## Concepts
|
|
42
|
-
|
|
43
|
-
### Packages
|
|
44
|
-
|
|
45
|
-
A package is similar to a Helm chart in that it is a source for Kubernetes resources that can make use of
|
|
46
|
-
templating to conditionally render resources and inject values into them. Resources generated by packages
|
|
47
|
-
may make use of Nyl-specific resource kinds that are only available time of resource generation (see the
|
|
48
|
-
[Templating > Resource Kinds](#resource-kinds) section below).
|
|
49
|
-
|
|
50
|
-
Nyl packages can be checked into a source repository in a DRY form, but can be compiled to a bundle (e.g.
|
|
51
|
-
including other referenced packages or Helm charts) for deployment when needed (e.g. for shipping to an
|
|
52
|
-
air-gapped environment).
|
|
53
|
-
|
|
54
|
-
Packages may have a `nyl-package.yaml` file that defines a schema for the package parameters and additional
|
|
55
|
-
metadata. A package without this file may still accept and use parameters in its templates.
|
|
56
|
-
|
|
57
|
-
### Applications
|
|
58
|
-
|
|
59
|
-
An application is an instance of one package that is deployed to a Kubernetes cluster and are in turn defined
|
|
60
|
-
also in a Nyl package. Packages instantiated as applications may not produce other applications. Applications
|
|
61
|
-
are usually accompanied by a `nyl-deployment.yaml` file that defines the top-level templating context for the
|
|
62
|
-
package(s), such as the secret store.
|
|
63
|
-
|
|
64
|
-
When deploying a package as an application, the package must not generated resources other than applications,
|
|
65
|
-
as all deployed resources must be owned by an application.
|
|
66
|
-
|
|
67
|
-
### Deployment
|
|
68
|
-
|
|
69
|
-
A deployment is where things get together: This is the place where you define the applications that should be
|
|
70
|
-
deployed to a Kubernetes cluster, which cluster they are being deployed to, as well as sourcing secrets from
|
|
71
|
-
a secret store. The configuration for a deployment is defined in a `nyl-deployment.yaml` file.
|
|
72
|
-
|
|
73
|
-
```yaml
|
|
74
|
-
apiVersion: nyl/v1
|
|
75
|
-
kind: Deployment
|
|
76
|
-
spec:
|
|
77
|
-
secretStores:
|
|
78
|
-
default:
|
|
79
|
-
type: Sops
|
|
80
|
-
path: secrets.yaml
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
__Spec__
|
|
84
|
-
|
|
85
|
-
* `secrets` (array): The secret stores to make available in the templating context of the deployment. Any secrets
|
|
86
|
-
need to be injected from these stores into the application values at templating time.
|
|
87
|
-
|
|
88
|
-
## Templating
|
|
89
|
-
|
|
90
|
-
[Jinja2]: https://jinja.palletsprojects.com/en/3.0.x/
|
|
91
|
-
|
|
92
|
-
Nyl uses [Jinja2] as a templating engine, and as such is slightly different from Helm.
|
|
93
|
-
|
|
94
|
-
### Injecting secrets
|
|
95
|
-
|
|
96
|
-
Secrets are only available at the deployment level and need to be propagated further down.
|
|
97
|
-
|
|
98
|
-
```yaml
|
|
99
|
-
apiVersion: nyl/v1
|
|
100
|
-
kind: Application
|
|
101
|
-
metadata:
|
|
102
|
-
name: my-app
|
|
103
|
-
spec:
|
|
104
|
-
package: ./path/to/package
|
|
105
|
-
values:
|
|
106
|
-
theSecret: {{ Secrets.default.get("my-secret") }}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
### Resource kinds
|
|
110
|
-
|
|
111
|
-
At templating time, Nyl supports special resource kinds that will be expanded to more Kubernetes resources.
|
|
112
|
-
|
|
113
|
-
#### `templating.nyl/v1/HelmChart`
|
|
114
|
-
|
|
115
|
-
This resource can be used to instantiate Helm charts as part of a package.
|
|
116
|
-
|
|
117
|
-
```yaml
|
|
118
|
-
apiVersion: templating.nyl/v1
|
|
119
|
-
kind: HelmChart
|
|
120
|
-
spec:
|
|
121
|
-
repository: https://kubernetes.github.io/ingress-nginx
|
|
122
|
-
chart: ingress-nginx
|
|
123
|
-
version: 4.10.1
|
|
124
|
-
releaseName: ingress-nginx
|
|
125
|
-
releaseNamespace: ingress-nginx
|
|
126
|
-
values: {}
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
__Spec__
|
|
130
|
-
|
|
131
|
-
* `repository` (string): The Helm repository URL, OCI registry URL (sans the chart name), a local path or a
|
|
132
|
-
Git repository clone URL.
|
|
133
|
-
* `chart` (string): The name of the chart in the repository, the chart name to append to the OCI registry URL,
|
|
134
|
-
the name of the chart in the local path or the path to the chart in the Git repository.
|
|
135
|
-
* `version` (string): The version of the chart to use. Only relevant for Helm/OCI repositories.
|
|
136
|
-
* `releaseName` (string): The name of the Helm release.
|
|
137
|
-
* `releaseNamespace` (string): The namespace to install the Helm release into. If any resources generated by the
|
|
138
|
-
chart have no `metadata.namespace` field, it will be set to this value.
|
|
139
|
-
* `values` (object): The values to pass to the Helm chart.
|
|
140
|
-
|
|
141
|
-
#### `templating.nyl/v1/Package`
|
|
142
|
-
|
|
143
|
-
Use this resource to instantiate another package.
|
|
144
|
-
|
|
145
|
-
```yaml
|
|
146
|
-
apiVersion: templating.nyl/v1
|
|
147
|
-
kind: Package
|
|
148
|
-
spec:
|
|
149
|
-
package: ./path/to/package
|
|
150
|
-
values: {}
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
__Spec__
|
|
154
|
-
|
|
155
|
-
* `package` (string): The path to the package to instantiate. Must be prefixed with `./` to be resolved relative
|
|
156
|
-
to the package that references it. Otherwise, it will be resolved in a search path that is defined in the
|
|
157
|
-
`nyl-deployment.yaml` file.
|
|
158
|
-
* `values` (object): The values to pass to the package.
|
nyl-0.0.3/README.md
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
# nyl
|
|
2
|
-
|
|
3
|
-
Nyl is a versatile Kubernetes resource management tool that fulfills various functions to enable reusable
|
|
4
|
-
deployment configurations.
|
|
5
|
-
|
|
6
|
-
__Features__
|
|
7
|
-
|
|
8
|
-
* Generate Kubernetes manifests from a simple YAML configuration with templating support.
|
|
9
|
-
* Inject secrets into Kubernetes manifests as time of generation.
|
|
10
|
-
* Bootstrap a cluster and transfer control to ArgoCD or use it to manage a cluster from CI/CD or locally.
|
|
11
|
-
* Connect to a Kubernetes cluster via an SSH tunnel.
|
|
12
|
-
|
|
13
|
-
__Roadmap__
|
|
14
|
-
|
|
15
|
-
* Integrate as an ArgoCD ConfigManagementPlugin.
|
|
16
|
-
* Diffing
|
|
17
|
-
* How to work around fields owned by a different field manager (e.g. MetalLB CRD `conversion..caBundle`) or `Job` resources?
|
|
18
|
-
|
|
19
|
-
## Concepts
|
|
20
|
-
|
|
21
|
-
### Packages
|
|
22
|
-
|
|
23
|
-
A package is similar to a Helm chart in that it is a source for Kubernetes resources that can make use of
|
|
24
|
-
templating to conditionally render resources and inject values into them. Resources generated by packages
|
|
25
|
-
may make use of Nyl-specific resource kinds that are only available time of resource generation (see the
|
|
26
|
-
[Templating > Resource Kinds](#resource-kinds) section below).
|
|
27
|
-
|
|
28
|
-
Nyl packages can be checked into a source repository in a DRY form, but can be compiled to a bundle (e.g.
|
|
29
|
-
including other referenced packages or Helm charts) for deployment when needed (e.g. for shipping to an
|
|
30
|
-
air-gapped environment).
|
|
31
|
-
|
|
32
|
-
Packages may have a `nyl-package.yaml` file that defines a schema for the package parameters and additional
|
|
33
|
-
metadata. A package without this file may still accept and use parameters in its templates.
|
|
34
|
-
|
|
35
|
-
### Applications
|
|
36
|
-
|
|
37
|
-
An application is an instance of one package that is deployed to a Kubernetes cluster and are in turn defined
|
|
38
|
-
also in a Nyl package. Packages instantiated as applications may not produce other applications. Applications
|
|
39
|
-
are usually accompanied by a `nyl-deployment.yaml` file that defines the top-level templating context for the
|
|
40
|
-
package(s), such as the secret store.
|
|
41
|
-
|
|
42
|
-
When deploying a package as an application, the package must not generated resources other than applications,
|
|
43
|
-
as all deployed resources must be owned by an application.
|
|
44
|
-
|
|
45
|
-
### Deployment
|
|
46
|
-
|
|
47
|
-
A deployment is where things get together: This is the place where you define the applications that should be
|
|
48
|
-
deployed to a Kubernetes cluster, which cluster they are being deployed to, as well as sourcing secrets from
|
|
49
|
-
a secret store. The configuration for a deployment is defined in a `nyl-deployment.yaml` file.
|
|
50
|
-
|
|
51
|
-
```yaml
|
|
52
|
-
apiVersion: nyl/v1
|
|
53
|
-
kind: Deployment
|
|
54
|
-
spec:
|
|
55
|
-
secretStores:
|
|
56
|
-
default:
|
|
57
|
-
type: Sops
|
|
58
|
-
path: secrets.yaml
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
__Spec__
|
|
62
|
-
|
|
63
|
-
* `secrets` (array): The secret stores to make available in the templating context of the deployment. Any secrets
|
|
64
|
-
need to be injected from these stores into the application values at templating time.
|
|
65
|
-
|
|
66
|
-
## Templating
|
|
67
|
-
|
|
68
|
-
[Jinja2]: https://jinja.palletsprojects.com/en/3.0.x/
|
|
69
|
-
|
|
70
|
-
Nyl uses [Jinja2] as a templating engine, and as such is slightly different from Helm.
|
|
71
|
-
|
|
72
|
-
### Injecting secrets
|
|
73
|
-
|
|
74
|
-
Secrets are only available at the deployment level and need to be propagated further down.
|
|
75
|
-
|
|
76
|
-
```yaml
|
|
77
|
-
apiVersion: nyl/v1
|
|
78
|
-
kind: Application
|
|
79
|
-
metadata:
|
|
80
|
-
name: my-app
|
|
81
|
-
spec:
|
|
82
|
-
package: ./path/to/package
|
|
83
|
-
values:
|
|
84
|
-
theSecret: {{ Secrets.default.get("my-secret") }}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### Resource kinds
|
|
88
|
-
|
|
89
|
-
At templating time, Nyl supports special resource kinds that will be expanded to more Kubernetes resources.
|
|
90
|
-
|
|
91
|
-
#### `templating.nyl/v1/HelmChart`
|
|
92
|
-
|
|
93
|
-
This resource can be used to instantiate Helm charts as part of a package.
|
|
94
|
-
|
|
95
|
-
```yaml
|
|
96
|
-
apiVersion: templating.nyl/v1
|
|
97
|
-
kind: HelmChart
|
|
98
|
-
spec:
|
|
99
|
-
repository: https://kubernetes.github.io/ingress-nginx
|
|
100
|
-
chart: ingress-nginx
|
|
101
|
-
version: 4.10.1
|
|
102
|
-
releaseName: ingress-nginx
|
|
103
|
-
releaseNamespace: ingress-nginx
|
|
104
|
-
values: {}
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
__Spec__
|
|
108
|
-
|
|
109
|
-
* `repository` (string): The Helm repository URL, OCI registry URL (sans the chart name), a local path or a
|
|
110
|
-
Git repository clone URL.
|
|
111
|
-
* `chart` (string): The name of the chart in the repository, the chart name to append to the OCI registry URL,
|
|
112
|
-
the name of the chart in the local path or the path to the chart in the Git repository.
|
|
113
|
-
* `version` (string): The version of the chart to use. Only relevant for Helm/OCI repositories.
|
|
114
|
-
* `releaseName` (string): The name of the Helm release.
|
|
115
|
-
* `releaseNamespace` (string): The namespace to install the Helm release into. If any resources generated by the
|
|
116
|
-
chart have no `metadata.namespace` field, it will be set to this value.
|
|
117
|
-
* `values` (object): The values to pass to the Helm chart.
|
|
118
|
-
|
|
119
|
-
#### `templating.nyl/v1/Package`
|
|
120
|
-
|
|
121
|
-
Use this resource to instantiate another package.
|
|
122
|
-
|
|
123
|
-
```yaml
|
|
124
|
-
apiVersion: templating.nyl/v1
|
|
125
|
-
kind: Package
|
|
126
|
-
spec:
|
|
127
|
-
package: ./path/to/package
|
|
128
|
-
values: {}
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
__Spec__
|
|
132
|
-
|
|
133
|
-
* `package` (string): The path to the package to instantiate. Must be prefixed with `./` to be resolved relative
|
|
134
|
-
to the package that references it. Otherwise, it will be resolved in a search path that is defined in the
|
|
135
|
-
`nyl-deployment.yaml` file.
|
|
136
|
-
* `values` (object): The values to pass to the package.
|
nyl-0.0.3/tests/__init__.py
DELETED
|
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
|