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.
- nyl-0.0.4/PKG-INFO +31 -0
- nyl-0.0.4/README.md +9 -0
- {nyl-0.0.2 → nyl-0.0.4}/pyproject.toml +21 -11
- nyl-0.0.4/src/nyl/__init__.py +3 -0
- nyl-0.0.4/src/nyl/commands/__init__.py +68 -0
- nyl-0.0.4/src/nyl/commands/crds.py +14 -0
- nyl-0.0.4/src/nyl/commands/new.py +91 -0
- {nyl-0.0.2 → nyl-0.0.4}/src/nyl/commands/profile.py +2 -5
- {nyl-0.0.2 → nyl-0.0.4}/src/nyl/commands/secrets.py +2 -2
- nyl-0.0.4/src/nyl/commands/template.py +349 -0
- {nyl-0.0.2 → nyl-0.0.4}/src/nyl/commands/tun.py +7 -8
- nyl-0.0.4/src/nyl/generator/__init__.py +66 -0
- nyl-0.0.4/src/nyl/generator/dispatch.py +76 -0
- nyl-0.0.4/src/nyl/generator/dispatch_test.py +45 -0
- nyl-0.0.4/src/nyl/generator/helmchart.py +154 -0
- nyl-0.0.4/src/nyl/generator/statefulsecret.py +26 -0
- {nyl-0.0.2 → nyl-0.0.4}/src/nyl/profiles/__init__.py +54 -11
- {nyl-0.0.2 → nyl-0.0.4}/src/nyl/profiles/config.py +20 -40
- {nyl-0.0.2 → nyl-0.0.4}/src/nyl/profiles/kubeconfig.py +27 -23
- {nyl-0.0.2 → nyl-0.0.4}/src/nyl/profiles/tunnel.py +3 -3
- nyl-0.0.4/src/nyl/project/config.py +67 -0
- nyl-0.0.4/src/nyl/resources/__init__.py +118 -0
- nyl-0.0.4/src/nyl/resources/applyset.py +238 -0
- nyl-0.0.4/src/nyl/resources/applyset_test.py +39 -0
- {nyl-0.0.2 → nyl-0.0.4}/src/nyl/resources/helmchart.py +4 -10
- {nyl-0.0.2 → nyl-0.0.4}/src/nyl/resources/statefulsecret.py +2 -2
- {nyl-0.0.2 → nyl-0.0.4}/src/nyl/secrets/config.py +17 -22
- nyl-0.0.4/src/nyl/secrets/null.py +14 -0
- nyl-0.0.4/src/nyl/tools/fs.py +29 -0
- {nyl-0.0.2 → nyl-0.0.4}/src/nyl/tools/kubectl.py +19 -10
- nyl-0.0.4/src/nyl/tools/shell.py +6 -0
- nyl-0.0.4/src/nyl/tools/types.py +8 -0
- nyl-0.0.2/PKG-INFO +0 -155
- nyl-0.0.2/README.md +0 -136
- 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/tests/__init__.py +0 -0
- {nyl-0.0.2 → nyl-0.0.4}/src/nyl/secrets/__init__.py +0 -0
- {nyl-0.0.2 → nyl-0.0.4}/src/nyl/secrets/sops.py +0 -0
- {nyl-0.0.2 → nyl-0.0.4}/src/nyl/secrets/sops_test.py +0 -0
- {nyl-0.0.2 → nyl-0.0.4}/src/nyl/tools/kvstore.py +0 -0
- {nyl-0.0.2 → nyl-0.0.4}/src/nyl/tools/typer.py +0 -0
- {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.
|
|
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
|
-
"
|
|
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"
|
|
@@ -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
|
-
"
|
|
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
|
-
"
|
|
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,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
|
-
|
|
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,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
|
+
}
|