nyl 0.0.2__tar.gz → 0.0.3__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.2 → nyl-0.0.3}/PKG-INFO +9 -6
- {nyl-0.0.2 → nyl-0.0.3}/pyproject.toml +17 -11
- nyl-0.0.3/src/nyl/commands/__init__.py +43 -0
- nyl-0.0.3/src/nyl/commands/crds.py +14 -0
- nyl-0.0.3/src/nyl/commands/new.py +91 -0
- {nyl-0.0.2 → nyl-0.0.3}/src/nyl/commands/profile.py +2 -5
- {nyl-0.0.2 → nyl-0.0.3}/src/nyl/commands/secrets.py +2 -2
- nyl-0.0.3/src/nyl/commands/template.py +311 -0
- {nyl-0.0.2 → nyl-0.0.3}/src/nyl/commands/tun.py +7 -8
- nyl-0.0.3/src/nyl/generator/__init__.py +66 -0
- nyl-0.0.3/src/nyl/generator/dispatch.py +76 -0
- nyl-0.0.3/src/nyl/generator/dispatch_test.py +45 -0
- nyl-0.0.3/src/nyl/generator/helmchart.py +154 -0
- nyl-0.0.3/src/nyl/generator/statefulsecret.py +26 -0
- {nyl-0.0.2 → nyl-0.0.3}/src/nyl/profiles/__init__.py +26 -3
- {nyl-0.0.2 → nyl-0.0.3}/src/nyl/profiles/config.py +20 -40
- {nyl-0.0.2 → nyl-0.0.3}/src/nyl/profiles/kubeconfig.py +3 -1
- nyl-0.0.3/src/nyl/project/config.py +58 -0
- nyl-0.0.3/src/nyl/resources/__init__.py +118 -0
- nyl-0.0.3/src/nyl/resources/applyset.py +225 -0
- nyl-0.0.3/src/nyl/resources/applyset_test.py +39 -0
- {nyl-0.0.2 → nyl-0.0.3}/src/nyl/resources/helmchart.py +4 -10
- {nyl-0.0.2 → nyl-0.0.3}/src/nyl/resources/statefulsecret.py +2 -2
- {nyl-0.0.2 → nyl-0.0.3}/src/nyl/secrets/config.py +17 -22
- nyl-0.0.3/src/nyl/secrets/null.py +14 -0
- nyl-0.0.3/src/nyl/tools/fs.py +29 -0
- {nyl-0.0.2 → nyl-0.0.3}/src/nyl/tools/kubectl.py +19 -10
- nyl-0.0.3/src/nyl/tools/types.py +8 -0
- nyl-0.0.2/src/nyl/appmanager/__init__.py +0 -3
- nyl-0.0.2/src/nyl/appmanager/appmanager.py +0 -150
- nyl-0.0.2/src/nyl/appmanager/crd.py +0 -107
- nyl-0.0.2/src/nyl/commands/__init__.py +0 -19
- nyl-0.0.2/src/nyl/commands/template.py +0 -274
- nyl-0.0.2/src/nyl/packages/__init__.py +0 -3
- nyl-0.0.2/src/nyl/packages/metadata.py +0 -7
- nyl-0.0.2/src/nyl/packages/reference.py +0 -91
- nyl-0.0.2/src/nyl/packages/reference_test.py +0 -12
- nyl-0.0.2/src/nyl/packages/templating.py +0 -52
- nyl-0.0.2/src/nyl/resources/__init__.py +0 -62
- nyl-0.0.2/src/nyl/resources/package.py +0 -17
- {nyl-0.0.2 → nyl-0.0.3}/README.md +0 -0
- {nyl-0.0.2 → nyl-0.0.3}/src/nyl/__init__.py +0 -0
- {nyl-0.0.2 → nyl-0.0.3}/src/nyl/profiles/tunnel.py +0 -0
- {nyl-0.0.2 → nyl-0.0.3}/src/nyl/secrets/__init__.py +0 -0
- {nyl-0.0.2 → nyl-0.0.3}/src/nyl/secrets/sops.py +0 -0
- {nyl-0.0.2 → nyl-0.0.3}/src/nyl/secrets/sops_test.py +0 -0
- {nyl-0.0.2 → nyl-0.0.3}/src/nyl/tools/kvstore.py +0 -0
- {nyl-0.0.2 → nyl-0.0.3}/src/nyl/tools/typer.py +0 -0
- {nyl-0.0.2 → nyl-0.0.3}/tests/__init__.py +0 -0
{nyl-0.0.2 → nyl-0.0.3}/PKG-INFO
RENAMED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: nyl
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: Default template for PDM package
|
|
5
5
|
Author-Email: Niklas Rosenstein <rosensteinniklas@gmail.com>
|
|
6
6
|
License: MIT
|
|
7
7
|
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: bcrypt>=4.2.0
|
|
8
9
|
Requires-Dist: databind>=4.5.2
|
|
9
|
-
Requires-Dist:
|
|
10
|
+
Requires-Dist: filelock>=3.15.4
|
|
10
11
|
Requires-Dist: jinja2>=3.1.4
|
|
11
|
-
Requires-Dist: loguru>=0.7.2
|
|
12
12
|
Requires-Dist: kubernetes>=30.1.0
|
|
13
|
-
Requires-Dist:
|
|
14
|
-
Requires-Dist: filelock>=3.15.4
|
|
15
|
-
Requires-Dist: stablehash>=0.2.0
|
|
13
|
+
Requires-Dist: loguru>=0.7.2
|
|
16
14
|
Requires-Dist: nr-stream>=1.1.5
|
|
15
|
+
Requires-Dist: pyyaml>=6.0.1
|
|
17
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
|
|
18
21
|
Description-Content-Type: text/markdown
|
|
19
22
|
|
|
20
23
|
# nyl
|
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nyl"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.3"
|
|
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
|
-
"
|
|
11
|
+
"filelock>=3.15.4",
|
|
11
12
|
"jinja2>=3.1.4",
|
|
12
|
-
"loguru>=0.7.2",
|
|
13
13
|
"kubernetes>=30.1.0",
|
|
14
|
-
"
|
|
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"
|
|
@@ -35,16 +38,19 @@ build-backend = "pdm.backend"
|
|
|
35
38
|
[tool.pdm]
|
|
36
39
|
distribution = true
|
|
37
40
|
|
|
41
|
+
[tool.pdm.scripts]
|
|
42
|
+
lint = "bash -c 'ruff format && ruff check --fix && mypy src' && pytest"
|
|
43
|
+
|
|
38
44
|
[tool.pdm.dev-dependencies]
|
|
39
45
|
dev = [
|
|
40
|
-
"
|
|
41
|
-
"mypy>=1.10.0",
|
|
42
|
-
"types-pyyaml>=6.0.12.20240311",
|
|
43
|
-
"mkdocs>=1.6.0",
|
|
46
|
+
"kubernetes-stubs>=22.6.0.post1",
|
|
44
47
|
"mkdocs-material>=9.5.28",
|
|
48
|
+
"mkdocs>=1.6.0",
|
|
45
49
|
"mkdocstrings[python]>=0.25.1",
|
|
50
|
+
"mypy>=1.10.0",
|
|
46
51
|
"pytest>=8.2.2",
|
|
47
|
-
"
|
|
52
|
+
"ruff>=0.4.8",
|
|
53
|
+
"types-pyyaml>=6.0.12.20240311",
|
|
48
54
|
"types-requests>=2.32.0.20240712",
|
|
49
55
|
]
|
|
50
56
|
|
|
@@ -0,0 +1,43 @@
|
|
|
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 enum import Enum
|
|
7
|
+
import sys
|
|
8
|
+
from loguru import logger
|
|
9
|
+
from typer import Option
|
|
10
|
+
from nyl.tools.typer import new_typer
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
app = new_typer(help=__doc__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from . import crds # noqa: F401,E402
|
|
17
|
+
from . import new # noqa: E402
|
|
18
|
+
from . import profile # noqa: E402
|
|
19
|
+
from . import secrets # noqa: E402
|
|
20
|
+
from . import template # noqa: F401,E402
|
|
21
|
+
from . import tun # noqa: E402
|
|
22
|
+
|
|
23
|
+
app.add_typer(new.app)
|
|
24
|
+
app.add_typer(profile.app)
|
|
25
|
+
app.add_typer(secrets.app)
|
|
26
|
+
app.add_typer(tun.app)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class LogLevel(str, Enum):
|
|
30
|
+
TRACE = "trace"
|
|
31
|
+
DEBUG = "debug"
|
|
32
|
+
INFO = "info"
|
|
33
|
+
WARNING = "warning"
|
|
34
|
+
ERROR = "error"
|
|
35
|
+
CRITICAL = "critical"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.callback()
|
|
39
|
+
def _callback(
|
|
40
|
+
log_level: LogLevel = Option(LogLevel.INFO, "--log-level", "-l", help="The log level to use."),
|
|
41
|
+
) -> None:
|
|
42
|
+
logger.remove()
|
|
43
|
+
logger.add(sys.stderr, level=log_level.name)
|
|
@@ -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
|
-
|
|
27
|
-
|
|
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(
|
|
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(
|
|
30
|
+
secrets = SecretsConfig.load()
|
|
31
31
|
print(json.dumps(secrets.provider.get(key), indent=4 if pretty else None))
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import 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
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ManifestsWithSource:
|
|
26
|
+
"""
|
|
27
|
+
Represents a list of manifests loaded from a particular source file.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
manifests: Manifests
|
|
31
|
+
file: Path
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@app.command()
|
|
35
|
+
def template(
|
|
36
|
+
paths: list[Path] = Argument(..., help="The YAML file(s) to render. Can be a directory."),
|
|
37
|
+
profile: str = Option("default", envvar="NYL_PROFILE", help="The Nyl profile to use."),
|
|
38
|
+
in_cluster: bool = Option(
|
|
39
|
+
False, help="Use the in-cluster Kubernetes configuration. The --profile option is ignored."
|
|
40
|
+
),
|
|
41
|
+
apply: bool = Option(
|
|
42
|
+
False,
|
|
43
|
+
help="Run `kubectl apply` on the rendered manifests, once for each source file. "
|
|
44
|
+
"Implies `--no-applyset-part-of`. When an ApplySet is defined in the source file, it will be applied "
|
|
45
|
+
"separately. Note that this option implies `kubectl --prune`.",
|
|
46
|
+
),
|
|
47
|
+
applyset_part_of: bool = Option(
|
|
48
|
+
True,
|
|
49
|
+
help="Add the 'applyset.kubernetes.io/part-of' label to all resources belonging to an ApplySet (if declared). "
|
|
50
|
+
"This option must be disabled when passing the generated manifests to `kubectl apply --applyset=...`, as it "
|
|
51
|
+
"would otherwise cause an error due to the label being present on the input data.",
|
|
52
|
+
),
|
|
53
|
+
) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Render a package template into full Kubernetes resources.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
if apply:
|
|
59
|
+
# When running with --apply, we must ensure that the --applyset-part-of option is disabled, as it would cause
|
|
60
|
+
# an error when passing the generated manifests to `kubectl apply --applyset=...`.
|
|
61
|
+
applyset_part_of = False
|
|
62
|
+
|
|
63
|
+
kubectl = Kubectl()
|
|
64
|
+
kubectl.env["KUBECTL_APPLYSET"] = "true"
|
|
65
|
+
atexit.register(kubectl.cleanup)
|
|
66
|
+
|
|
67
|
+
if in_cluster:
|
|
68
|
+
logger.info("Using in-cluster configuration.")
|
|
69
|
+
load_incluster_config()
|
|
70
|
+
else:
|
|
71
|
+
with ProfileManager.load() as profiles:
|
|
72
|
+
active = profiles.activate_profile(profile)
|
|
73
|
+
logger.info(f"Using profile '{profile}' with kubeconfig '{active.kubeconfig}'.")
|
|
74
|
+
load_kube_config(str(active.kubeconfig))
|
|
75
|
+
|
|
76
|
+
project = ProjectConfig.load()
|
|
77
|
+
if project.file:
|
|
78
|
+
state_dir = project.file.parent / ".nyl"
|
|
79
|
+
else:
|
|
80
|
+
state_dir = Path(".nyl")
|
|
81
|
+
|
|
82
|
+
secrets = SecretsConfig.load()
|
|
83
|
+
|
|
84
|
+
template_engine = TemplateEngine(
|
|
85
|
+
globals_={
|
|
86
|
+
"secrets": secrets.provider,
|
|
87
|
+
"random_password": _random_password,
|
|
88
|
+
"bcrypt": _bcrypt,
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
generator = DispatchingGenerator.default(
|
|
93
|
+
git_repo_cache_dir=state_dir / "repo-cache",
|
|
94
|
+
search_path=project.config.search_path,
|
|
95
|
+
working_dir=Path.cwd(),
|
|
96
|
+
client=ApiClient(),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
for source in load_manifests(paths):
|
|
100
|
+
logger.info("Rendering manifests from {}", source.file)
|
|
101
|
+
|
|
102
|
+
source.manifests = cast(Manifests, template_engine.evaluate(source.manifests))
|
|
103
|
+
source.manifests = reconcile_generator(
|
|
104
|
+
generator,
|
|
105
|
+
source.manifests,
|
|
106
|
+
on_generated=lambda m: cast(Manifest, template_engine.evaluate(m)),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Find the namespaces that are defined in the file. If we find any manifests without a namespace, we will
|
|
110
|
+
# inject that namespace name into them.
|
|
111
|
+
# Also find the applyset defined in the file.
|
|
112
|
+
namespaces: set[str] = set()
|
|
113
|
+
applyset: ApplySet | None = None
|
|
114
|
+
for manifest in list(source.manifests):
|
|
115
|
+
if is_namespace_resource(manifest):
|
|
116
|
+
namespaces.add(manifest["metadata"]["name"])
|
|
117
|
+
elif ApplySet.matches(manifest):
|
|
118
|
+
if applyset is not None:
|
|
119
|
+
logger.error(
|
|
120
|
+
"Multiple ApplySet resources defined in '{}', there can only be one per source.",
|
|
121
|
+
source.file,
|
|
122
|
+
)
|
|
123
|
+
exit(1)
|
|
124
|
+
applyset = ApplySet.load(manifest)
|
|
125
|
+
source.manifests.remove(manifest)
|
|
126
|
+
|
|
127
|
+
if applyset is not None:
|
|
128
|
+
applyset.set_group_kinds(source.manifests)
|
|
129
|
+
# HACK: Kubectl 1.30 can't create the custom resource, so we need to create it. But it will also reject
|
|
130
|
+
# using the custom resource unless it has the tooling label set appropriately. For more details, see
|
|
131
|
+
# https://github.com/NiklasRosenstein/nyl/issues/5.
|
|
132
|
+
applyset.tooling = "kubectl/1.30"
|
|
133
|
+
applyset.validate()
|
|
134
|
+
|
|
135
|
+
if apply:
|
|
136
|
+
# We need to ensure that ApplySet parent object exists before invoking `kubectl apply --applyset=...`.
|
|
137
|
+
logger.info("Kubectl-apply ApplySet resource '{}' from '{}'", applyset.reference, source.file)
|
|
138
|
+
kubectl.apply(Manifests([applyset.dump()]), force_conflicts=True)
|
|
139
|
+
else:
|
|
140
|
+
print("---")
|
|
141
|
+
print(yaml.safe_dump(applyset.dump()))
|
|
142
|
+
|
|
143
|
+
# Find all manifests without a namespace and inject the namespace name into them.
|
|
144
|
+
# If there is an applyset, ensure they are marked as part of the applyset.
|
|
145
|
+
for manifest in source.manifests:
|
|
146
|
+
if applyset is not None and applyset_part_of:
|
|
147
|
+
if APPLYSET_LABEL_PART_OF not in (labels := manifest["metadata"].setdefault("labels", {})):
|
|
148
|
+
labels[APPLYSET_LABEL_PART_OF] = applyset.id
|
|
149
|
+
|
|
150
|
+
if not is_cluster_scoped_resource(manifest) and "namespace" not in manifest["metadata"]:
|
|
151
|
+
if len(namespaces) > 1:
|
|
152
|
+
logger.error(
|
|
153
|
+
"Multiple namespaces defined in '{}', but manifest {}/{} does not have a namespace.",
|
|
154
|
+
source.file,
|
|
155
|
+
manifest["kind"],
|
|
156
|
+
manifest["metadata"]["name"],
|
|
157
|
+
)
|
|
158
|
+
exit(1)
|
|
159
|
+
elif len(namespaces) == 0:
|
|
160
|
+
logger.warning(
|
|
161
|
+
"No namespaces defined in '{}', injecting 'default' into manifest {}/{}.",
|
|
162
|
+
source.file,
|
|
163
|
+
manifest["kind"],
|
|
164
|
+
manifest["metadata"]["name"],
|
|
165
|
+
)
|
|
166
|
+
manifest["metadata"]["namespace"] = "default"
|
|
167
|
+
else:
|
|
168
|
+
logger.trace(
|
|
169
|
+
"Injecting namespace '{}' into manifest {}/{}.",
|
|
170
|
+
next(iter(namespaces)),
|
|
171
|
+
manifest["kind"],
|
|
172
|
+
manifest["metadata"]["name"],
|
|
173
|
+
)
|
|
174
|
+
manifest["metadata"]["namespace"] = next(iter(namespaces))
|
|
175
|
+
elif not is_cluster_scoped_resource(manifest) and manifest["metadata"]["namespace"] not in namespaces:
|
|
176
|
+
logger.warning(
|
|
177
|
+
"Resource '{}/{}' in '{}' references namespace '{}', which is not defined in the file.",
|
|
178
|
+
manifest["kind"],
|
|
179
|
+
manifest["metadata"]["name"],
|
|
180
|
+
source.file,
|
|
181
|
+
manifest["metadata"]["namespace"],
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if not apply:
|
|
185
|
+
print("---")
|
|
186
|
+
print(yaml.safe_dump(manifest))
|
|
187
|
+
|
|
188
|
+
if apply:
|
|
189
|
+
logger.info("Kubectl-apply {} manifest(s) from '{}'", len(source.manifests), source.file)
|
|
190
|
+
kubectl.apply(
|
|
191
|
+
manifests=source.manifests,
|
|
192
|
+
applyset=applyset.reference if applyset else None,
|
|
193
|
+
prune=True if applyset else False,
|
|
194
|
+
force_conflicts=True,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def load_manifests(paths: list[Path]) -> list[ManifestsWithSource]:
|
|
199
|
+
"""
|
|
200
|
+
Load all manifests from a directory.
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
logger.trace("Loading manifests from paths: {}", paths)
|
|
204
|
+
|
|
205
|
+
files = []
|
|
206
|
+
for path in paths:
|
|
207
|
+
if path.is_dir():
|
|
208
|
+
for item in path.iterdir():
|
|
209
|
+
if item.name.startswith("nyl-") or item.suffix != ".yaml" or not item.is_file():
|
|
210
|
+
continue
|
|
211
|
+
files.append(item)
|
|
212
|
+
else:
|
|
213
|
+
files.append(path)
|
|
214
|
+
|
|
215
|
+
logger.trace("Files to load: {}", files)
|
|
216
|
+
|
|
217
|
+
result = []
|
|
218
|
+
for file in files:
|
|
219
|
+
manifests = Manifests(list(map(Manifest, yaml.safe_load_all(file.read_text()))))
|
|
220
|
+
result.append(ManifestsWithSource(manifests, file))
|
|
221
|
+
|
|
222
|
+
return result
|
|
223
|
+
|
|
224
|
+
# # Check if the resource has any references and try to resolve them. If a reference cannot be resolved, the
|
|
225
|
+
# # manifest must be skipped. We emit a warning and continue with the next manifest.
|
|
226
|
+
# refs = list(Reference.collect(manifest))
|
|
227
|
+
# if refs:
|
|
228
|
+
# skip_resource = False
|
|
229
|
+
# resolves = {}
|
|
230
|
+
# for ref in refs:
|
|
231
|
+
# # TODO: Determine the namespace to fall back to.
|
|
232
|
+
# try:
|
|
233
|
+
# result = k8s.get(
|
|
234
|
+
# resource=k8s.resources.get(api_version="v1", kind=ref.kind, group=""),
|
|
235
|
+
# name=ref.name,
|
|
236
|
+
# namespace=ref.namespace, # TODO: Determine the namespace to backfill to.
|
|
237
|
+
# )
|
|
238
|
+
|
|
239
|
+
# value = result["data"][ref.key]
|
|
240
|
+
# if value is None:
|
|
241
|
+
# raise KeyError
|
|
242
|
+
# assert isinstance(value, str)
|
|
243
|
+
# resolves[str(ref)] = base64.b64decode(value.encode("ascii")).decode("utf-8")
|
|
244
|
+
# except NotFoundError:
|
|
245
|
+
# logger.warning(
|
|
246
|
+
# "Skipping resource {}/{} because its reference to {} could not be resolved.",
|
|
247
|
+
# manifest["apiVersion"],
|
|
248
|
+
# manifest["kind"],
|
|
249
|
+
# ref,
|
|
250
|
+
# )
|
|
251
|
+
# skip_resource = True
|
|
252
|
+
# break
|
|
253
|
+
# except KeyError:
|
|
254
|
+
# logger.warning(
|
|
255
|
+
# "Skipping resource {}/{} because its reference to {} could not be resolved (does not contain key {}).",
|
|
256
|
+
# manifest["apiVersion"],
|
|
257
|
+
# manifest["kind"],
|
|
258
|
+
# ref,
|
|
259
|
+
# ref.key,
|
|
260
|
+
# )
|
|
261
|
+
# skip_resource = True
|
|
262
|
+
# break
|
|
263
|
+
# if skip_resource:
|
|
264
|
+
# continue
|
|
265
|
+
# manifest = Reference.sub(manifest, lambda ref: resolves[str(ref)])
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _random_password(length: int = 32) -> str:
|
|
269
|
+
"""
|
|
270
|
+
Generate a random password.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
import secrets
|
|
274
|
+
|
|
275
|
+
return secrets.token_urlsafe(length)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _bcrypt(password: str) -> str:
|
|
279
|
+
"""
|
|
280
|
+
Hash a password using bcrypt.
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
import bcrypt
|
|
284
|
+
|
|
285
|
+
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def is_namespace_resource(manifest: Manifest) -> bool:
|
|
289
|
+
"""
|
|
290
|
+
Check if a manifest is a namespace resource.
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
return manifest.get("apiVersion") == "v1" and manifest.get("kind") == "Namespace"
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def is_cluster_scoped_resource(manifest: Manifest) -> bool:
|
|
297
|
+
"""
|
|
298
|
+
Check if a manifest is a cluster scoped resource.
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
# HACK: We should probably just list the resources via the Kubectl API?
|
|
302
|
+
fqn = manifest.get("kind", "") + "." + manifest.get("apiVersion", "").split("/")[0]
|
|
303
|
+
return fqn in {
|
|
304
|
+
"ClusterRole.rbac.authorization.k8s.io",
|
|
305
|
+
"ClusterRoleBinding.rbac.authorization.k8s.io",
|
|
306
|
+
"CustomResourceDefinition.apiextensions.k8s.io",
|
|
307
|
+
"IngressClass.networking.k8s.io",
|
|
308
|
+
"Namespace.v1",
|
|
309
|
+
"StorageClass.storage.k8s.io",
|
|
310
|
+
"ValidatingWebhookConfiguration.admissionregistration.k8s.io",
|
|
311
|
+
}
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
Access SSH tunnels globally managed by Nyl.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from typing import Optional
|
|
6
|
-
|
|
7
5
|
from loguru import logger
|
|
8
6
|
from typer import Argument
|
|
9
7
|
from rich.console import Console
|
|
@@ -24,7 +22,7 @@ def status(all: bool = False) -> None:
|
|
|
24
22
|
Show the status of all tunnels.
|
|
25
23
|
"""
|
|
26
24
|
|
|
27
|
-
config = ProfileConfig.load(
|
|
25
|
+
config = ProfileConfig.load(required=False)
|
|
28
26
|
|
|
29
27
|
table = Table()
|
|
30
28
|
table.add_column("Profile", justify="right", style="cyan")
|
|
@@ -48,6 +46,7 @@ def status(all: bool = False) -> None:
|
|
|
48
46
|
# on the system, while there may be tunnel information available for them from previous runs.
|
|
49
47
|
for profile_name, profile in config.profiles.items():
|
|
50
48
|
if profile_name not in tunnel_by_profile and profile.tunnel:
|
|
49
|
+
assert config.file is not None, "Profile configuration file must be set."
|
|
51
50
|
tunnels.append((get_tunnel_spec(config.file, profile_name, profile.tunnel), TunnelStatus.empty()))
|
|
52
51
|
|
|
53
52
|
tunnels.sort(key=lambda x: (x[0].locator.profile, x[0].locator.config_file))
|
|
@@ -76,12 +75,12 @@ def status(all: bool = False) -> None:
|
|
|
76
75
|
|
|
77
76
|
|
|
78
77
|
@app.command()
|
|
79
|
-
def start(profile_name: str) -> None:
|
|
78
|
+
def start(profile_name: str = Argument("default", envvar="NYL_PROFILE")) -> None:
|
|
80
79
|
"""
|
|
81
80
|
Open a tunnel to the cluster targeted by the profile.
|
|
82
81
|
"""
|
|
83
82
|
|
|
84
|
-
config = ProfileConfig.load(
|
|
83
|
+
config = ProfileConfig.load()
|
|
85
84
|
|
|
86
85
|
try:
|
|
87
86
|
profile = config.profiles[profile_name]
|
|
@@ -106,17 +105,17 @@ def start(profile_name: str) -> None:
|
|
|
106
105
|
|
|
107
106
|
|
|
108
107
|
@app.command()
|
|
109
|
-
def stop(profile_name:
|
|
108
|
+
def stop(profile_name: str = Argument("default", envvar="NYL_PROFILE"), all: bool = False) -> None:
|
|
110
109
|
"""
|
|
111
110
|
Close all tunnels or the tunnel for a specific profile.
|
|
112
111
|
"""
|
|
113
112
|
|
|
114
|
-
if
|
|
113
|
+
if all:
|
|
115
114
|
with TunnelManager() as manager:
|
|
116
115
|
for spec, _status in manager.get_tunnels():
|
|
117
116
|
manager.close_tunnel(spec.locator)
|
|
118
117
|
return
|
|
119
118
|
|
|
120
|
-
config = ProfileConfig.load(
|
|
119
|
+
config = ProfileConfig.load()
|
|
121
120
|
with TunnelManager() as manager:
|
|
122
121
|
manager.close_tunnel(TunnelSpec.Locator(str(config.file), profile_name))
|