nyl 0.1.1__tar.gz → 0.2.2__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.1.1 → nyl-0.2.2}/PKG-INFO +2 -2
- {nyl-0.1.1 → nyl-0.2.2}/pyproject.toml +2 -2
- {nyl-0.1.1 → nyl-0.2.2}/src/kubernetes/dynamic/resource.pyi +1 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/commands/__init__.py +2 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/commands/run.py +3 -3
- nyl-0.2.2/src/nyl/commands/secrets.py +61 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/commands/template.py +58 -36
- nyl-0.2.2/src/nyl/commands/tools/__init__.py +13 -0
- nyl-0.2.2/src/nyl/commands/tools/bcrypt.py +17 -0
- nyl-0.1.1/src/nyl/commands/secrets.py → nyl-0.2.2/src/nyl/commands/tools/sops.py +9 -53
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/generator/helmchart.py +3 -3
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/profiles/kubeconfig.py +4 -4
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/resources/placeholder.py +10 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/templating.py +65 -2
- nyl-0.2.2/src/nyl/tools/yaml.py +27 -0
- {nyl-0.1.1 → nyl-0.2.2}/uv.lock +4 -4
- {nyl-0.1.1 → nyl-0.2.2}/.envrc +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/.github/workflows/docker.yaml +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/.github/workflows/docs.yaml +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/.github/workflows/python.yaml +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/.gitignore +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/.kraken.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/README.md +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/argocd-cmp/Dockerfile +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/argocd-cmp/README.md +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/argocd-cmp/plugin.yaml +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/argocd-with-nyl/.envrc +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/argocd-with-nyl/.secrets.yaml +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/argocd-with-nyl/.sops.yaml +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/argocd-with-nyl/README.md +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/argocd-with-nyl/argocd.yaml +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/argocd-with-nyl/nyl-project.yaml +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/argocd-with-nyl/nyl-secrets.yaml +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/docs/content/index.md +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/docs/content/reference/applysets.md +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/docs/content/reference/argocd-plugin.md +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/docs/content/reference/cluster-connectivity.md +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/docs/content/reference/configuration/environment.md +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/docs/content/reference/configuration/profiles.md +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/docs/content/reference/configuration/projects.md +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/docs/content/reference/configuration/secrets.md +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/docs/content/reference/configuration-api/profiles.md +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/docs/content/reference/configuration-api/secrets.md +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/docs/content/reference/templating/basics.md +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/docs/content/reference/templating/components.md +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/docs/content/reference/templating/inlining.md +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/docs/content/reference/templating/secrets.md +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/docs/mkdocs.yml +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/docs/pyproject.toml +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/docs/uv.lock +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/example/manifests/postgres.yaml +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/example/nyl-profiles.yaml +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/example/nyl-secrets.yaml +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/example/secrets.yaml +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/kubernetes/README.md +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/kubernetes/__init__.pyi +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/kubernetes/dynamic/__init__.pyi +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/kubernetes/dynamic/client.pyi +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/kubernetes/dynamic/discovery.pyi +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/kubernetes/dynamic/exceptions.pyi +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/kubernetes/watch/__init__.pyi +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/kubernetes/watch/watch.pyi +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/__init__.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/commands/argocd.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/commands/crds.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/commands/new.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/commands/profile.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/commands/tun.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/generator/__init__.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/generator/components.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/generator/dispatch.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/generator/dispatch_test.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/generator/statefulsecret.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/profiles/__init__.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/profiles/config.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/profiles/config_test.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/profiles/tunnel.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/project/config.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/resources/__init__.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/resources/applyset.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/resources/applyset_test.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/resources/helmchart.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/resources/statefulsecret.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/secrets/__init__.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/secrets/config.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/secrets/config_test.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/secrets/null.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/secrets/sops.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/secrets/sops_test.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/templating_test.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/tools/fs.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/tools/fs_test.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/tools/kubectl.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/tools/kubernetes.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/tools/kvstore.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/tools/loads.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/tools/logging.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/tools/shell.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/tools/testing.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/tools/typer.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/src/nyl/tools/types.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/tests/__init__.py +0 -0
- {nyl-0.1.1 → nyl-0.2.2}/tests/conftest.py +0 -0
{nyl-0.1.1 → nyl-0.2.2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: nyl
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Advanced templating engine for Kubernetes and GitOps.
|
|
5
5
|
Author-email: Niklas Rosenstein <rosensteinniklas@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -15,7 +15,7 @@ Requires-Dist: nr-stream>=1.1.5
|
|
|
15
15
|
Requires-Dist: pyyaml>=6.0.1
|
|
16
16
|
Requires-Dist: requests>=2.32.3
|
|
17
17
|
Requires-Dist: stablehash<0.3.0,>=0.2.1
|
|
18
|
-
Requires-Dist: structured-templates
|
|
18
|
+
Requires-Dist: structured-templates>=0.1.1
|
|
19
19
|
Requires-Dist: typer>=0.12.3
|
|
20
20
|
Requires-Dist: typing-extensions>=4.12.2
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nyl"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.2" # sed'ed on publish in CI
|
|
8
8
|
description = "Advanced templating engine for Kubernetes and GitOps."
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Niklas Rosenstein", email = "rosensteinniklas@gmail.com"},
|
|
@@ -20,7 +20,7 @@ dependencies = [
|
|
|
20
20
|
"pyyaml>=6.0.1",
|
|
21
21
|
"requests>=2.32.3",
|
|
22
22
|
"stablehash>=0.2.1,<0.3.0",
|
|
23
|
-
"structured-templates>=0.
|
|
23
|
+
"structured-templates>=0.1.1",
|
|
24
24
|
"typer>=0.12.3",
|
|
25
25
|
"typing-extensions>=4.12.2",
|
|
26
26
|
]
|
|
@@ -29,12 +29,14 @@ from . import profile # noqa: E402
|
|
|
29
29
|
from . import run # noqa: F401, E402
|
|
30
30
|
from . import secrets # noqa: E402
|
|
31
31
|
from . import template # noqa: F401,E402
|
|
32
|
+
from . import tools # noqa: E402
|
|
32
33
|
from . import tun # noqa: E402
|
|
33
34
|
|
|
34
35
|
app.add_typer(argocd.app)
|
|
35
36
|
app.add_typer(new.app)
|
|
36
37
|
app.add_typer(profile.app)
|
|
37
38
|
app.add_typer(secrets.app)
|
|
39
|
+
app.add_typer(tools.app)
|
|
38
40
|
app.add_typer(tun.app)
|
|
39
41
|
|
|
40
42
|
|
|
@@ -6,7 +6,7 @@ import sys
|
|
|
6
6
|
from tempfile import TemporaryDirectory
|
|
7
7
|
from loguru import logger
|
|
8
8
|
from typer import Argument, Option
|
|
9
|
-
import yaml
|
|
9
|
+
from nyl.tools import yaml
|
|
10
10
|
from nyl.profiles import ActivatedProfile, ProfileManager
|
|
11
11
|
from nyl.profiles.kubeconfig import _trim_to_context
|
|
12
12
|
from nyl.tools.logging import lazy_str
|
|
@@ -56,7 +56,7 @@ def run(
|
|
|
56
56
|
sys.exit(1)
|
|
57
57
|
|
|
58
58
|
try:
|
|
59
|
-
kubeconfig_data = yaml.
|
|
59
|
+
kubeconfig_data = yaml.loads(kubeconfig.read_text())
|
|
60
60
|
kubeconfig_data = _trim_to_context(kubeconfig_data, profile_name)
|
|
61
61
|
except ValueError:
|
|
62
62
|
logger.debug("Failed to parse the kubeconfig file/find context '{}'.", profile_name)
|
|
@@ -83,7 +83,7 @@ def run(
|
|
|
83
83
|
tmpdir = TemporaryDirectory()
|
|
84
84
|
atexit.register(tmpdir.cleanup)
|
|
85
85
|
kubeconfig = Path(tmpdir.name) / "kubeconfig"
|
|
86
|
-
kubeconfig.write_text(yaml.
|
|
86
|
+
kubeconfig.write_text(yaml.dumps(kubeconfig_data))
|
|
87
87
|
kubeconfig.chmod(0o600)
|
|
88
88
|
|
|
89
89
|
profile = ActivatedProfile(kubeconfig)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interact with the secrets providers configured in `nyl-secrets.yaml`.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from typer import Option
|
|
8
|
+
from nyl.secrets.config import SecretsConfig
|
|
9
|
+
from nyl.tools.typer import new_typer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
app = new_typer(name="secrets", help=__doc__)
|
|
13
|
+
|
|
14
|
+
# Initialized from callback for access by subcommands.
|
|
15
|
+
provider: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.callback()
|
|
19
|
+
def callback(
|
|
20
|
+
_provider: str = Option(
|
|
21
|
+
"default",
|
|
22
|
+
"--provider",
|
|
23
|
+
help="The name of the configured secrets provider to use.",
|
|
24
|
+
envvar="NYL_SECRETS",
|
|
25
|
+
),
|
|
26
|
+
) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Interact with the secrets providers configured in `nyl-secrets.yaml`.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
global provider
|
|
32
|
+
provider = _provider
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.command()
|
|
36
|
+
def list(
|
|
37
|
+
providers: bool = Option(
|
|
38
|
+
False, help="List the configured secrets providers instead of the current provider's available keys."
|
|
39
|
+
),
|
|
40
|
+
) -> None:
|
|
41
|
+
"""
|
|
42
|
+
List the keys for all secrets in the provider.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
secrets = SecretsConfig.load()
|
|
46
|
+
if providers:
|
|
47
|
+
for alias, impl in secrets.providers.items():
|
|
48
|
+
print(alias, impl)
|
|
49
|
+
else:
|
|
50
|
+
for key in secrets.providers[provider].keys():
|
|
51
|
+
print(key)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@app.command()
|
|
55
|
+
def get(key: str, pretty: bool = False) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Get the value of a secret as JSON.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
secrets = SecretsConfig.load()
|
|
61
|
+
print(json.dumps(secrets.providers[provider].get(key), indent=4 if pretty else None))
|
|
@@ -6,7 +6,7 @@ from textwrap import indent
|
|
|
6
6
|
from typing import Optional
|
|
7
7
|
from loguru import logger
|
|
8
8
|
from typer import Argument, Option
|
|
9
|
-
import yaml
|
|
9
|
+
from nyl.tools import yaml
|
|
10
10
|
from nyl.generator import reconcile_generator
|
|
11
11
|
from nyl.generator.dispatch import DispatchingGenerator
|
|
12
12
|
from nyl.profiles import ProfileManager
|
|
@@ -66,6 +66,18 @@ def template(
|
|
|
66
66
|
"This option must be disabled when passing the generated manifests to `kubectl apply --applyset=...`, as it "
|
|
67
67
|
"would otherwise cause an error due to the label being present on the input data.",
|
|
68
68
|
),
|
|
69
|
+
default_namespace: Optional[str] = Option(
|
|
70
|
+
None,
|
|
71
|
+
help="The name of the Kubernetes namespace to fill in to Kubernetes resource that don't have a namespace set. "
|
|
72
|
+
"If this is not specified as an argument or via environment variables, it will default to the stem of the "
|
|
73
|
+
"manifest source filename. Note that if a manifest defines a Namespace resource, that namespace is used "
|
|
74
|
+
"instead, regardless of the value of this option. If a manifest source file defines multiple namespaces and a "
|
|
75
|
+
"resource without namespace is encountered, this option is considered if it matches one of the namespaces "
|
|
76
|
+
"defined in the file. Note that this option is usually problematic when rendering multiple files, as often "
|
|
77
|
+
"a single file is intended to deploy into a single namespace.\n\n"
|
|
78
|
+
"Note that Nyl's detection for cluster-scoped vs. namespace-scoped resources is not very good, yet.",
|
|
79
|
+
envvar="ARGOCD_APP_NAMESPACE",
|
|
80
|
+
),
|
|
69
81
|
inline: bool = Option(True, help="Evaluate Nyl inlined resources."),
|
|
70
82
|
state_dir: Optional[Path] = Option(
|
|
71
83
|
None, help="The directory to store state in (such as kubeconfig files).", envvar="NYL_STATE_DIR"
|
|
@@ -168,10 +180,10 @@ def template(
|
|
|
168
180
|
)
|
|
169
181
|
|
|
170
182
|
# Find the namespaces that are defined in the file. If we find any manifests without a namespace, we will
|
|
171
|
-
# inject that namespace name into them.
|
|
172
|
-
# Also find the applyset defined in the file.
|
|
183
|
+
# inject that namespace name into them. Also find the applyset defined in the file.
|
|
173
184
|
namespaces: set[str] = set()
|
|
174
185
|
applyset: ApplySet | None = None
|
|
186
|
+
|
|
175
187
|
for manifest in list(source.manifests):
|
|
176
188
|
if is_namespace_resource(manifest):
|
|
177
189
|
namespaces.add(manifest["metadata"]["name"])
|
|
@@ -185,23 +197,40 @@ def template(
|
|
|
185
197
|
applyset = ApplySet.load(manifest)
|
|
186
198
|
source.manifests.remove(manifest)
|
|
187
199
|
|
|
200
|
+
# Determine what namespace to fill in if we encounter resources without it.
|
|
201
|
+
current_default_namespace = default_namespace
|
|
202
|
+
if current_default_namespace is None:
|
|
203
|
+
if len(namespaces) == 1:
|
|
204
|
+
current_default_namespace = next(iter(namespaces))
|
|
205
|
+
elif len(namespaces) == 0:
|
|
206
|
+
current_default_namespace = source.file.stem
|
|
207
|
+
elif len(namespaces) > 2 and current_default_namespace not in namespaces:
|
|
208
|
+
logger.error(
|
|
209
|
+
"The specified --default-namespace='{}' is not defined in manifest source file '{}', despite it "
|
|
210
|
+
"defining {} namespaces. If your manifest source file defines multiple namespaces, the specified "
|
|
211
|
+
"--default-namespace must be one of the namespaces defined in it (the file current defines "
|
|
212
|
+
"the following namespaces: {})",
|
|
213
|
+
default_namespace,
|
|
214
|
+
source.file,
|
|
215
|
+
len(namespaces),
|
|
216
|
+
", ".join(namespaces),
|
|
217
|
+
)
|
|
218
|
+
exit(1)
|
|
219
|
+
|
|
188
220
|
if not applyset and project.config.settings.generate_applysets:
|
|
189
|
-
if
|
|
221
|
+
if not current_default_namespace:
|
|
190
222
|
logger.opt(ansi=True).error(
|
|
191
|
-
"
|
|
192
|
-
"
|
|
223
|
+
"No default namespace defined for <yellow>{}</>, but it is required for the automatically "
|
|
224
|
+
"generated nyl.io/v1/ApplySet resource (the ApplySet is named after the default namespace).",
|
|
193
225
|
source.file,
|
|
194
226
|
)
|
|
195
227
|
exit(1)
|
|
196
|
-
elif len(namespaces) == 1:
|
|
197
|
-
applyset_name = next(iter(namespaces))
|
|
198
|
-
else:
|
|
199
|
-
applyset_name = source.file.stem
|
|
200
228
|
|
|
229
|
+
applyset_name = current_default_namespace
|
|
230
|
+
applyset = ApplySet.new(applyset_name)
|
|
201
231
|
logger.opt(ansi=True).info(
|
|
202
232
|
"Automatically creating ApplySet for <blue>{}</> (name: <magenta>{}</>).", source.file, applyset_name
|
|
203
233
|
)
|
|
204
|
-
applyset = ApplySet.new(applyset_name)
|
|
205
234
|
|
|
206
235
|
if applyset is not None:
|
|
207
236
|
applyset.set_group_kinds(source.manifests)
|
|
@@ -223,7 +252,7 @@ def template(
|
|
|
223
252
|
kubectl.diff(Manifests([applyset.dump()]))
|
|
224
253
|
else:
|
|
225
254
|
print("---")
|
|
226
|
-
print(yaml.
|
|
255
|
+
print(yaml.dumps(applyset.dump()))
|
|
227
256
|
|
|
228
257
|
# Find all manifests without a namespace and inject the namespace name into them.
|
|
229
258
|
# If there is an applyset, ensure they are marked as part of the applyset.
|
|
@@ -236,9 +265,9 @@ def template(
|
|
|
236
265
|
|
|
237
266
|
if "metadata" not in manifest:
|
|
238
267
|
logger.opt(ansi=True).error(
|
|
239
|
-
"
|
|
268
|
+
"A manifest in <yellow>'{}'</> has no <cyan>metadata</> key:\n\n{}",
|
|
240
269
|
source.file,
|
|
241
|
-
indent(yaml.
|
|
270
|
+
indent(yaml.dumps(manifest), " "),
|
|
242
271
|
)
|
|
243
272
|
exit(1)
|
|
244
273
|
|
|
@@ -247,30 +276,23 @@ def template(
|
|
|
247
276
|
labels[APPLYSET_LABEL_PART_OF] = applyset.id
|
|
248
277
|
|
|
249
278
|
if not is_cluster_scoped_resource(manifest) and "namespace" not in manifest["metadata"]:
|
|
250
|
-
if
|
|
279
|
+
if not current_default_namespace:
|
|
251
280
|
logger.error(
|
|
252
|
-
"
|
|
281
|
+
"A namespace-scoped manifest in '{}' has no namespace, and no default namespace is defined. "
|
|
282
|
+
"Try defining a single namespace in the manifest source file, naming the manifest source file "
|
|
283
|
+
"the same as that default namespace or passing the --default-namespace option.",
|
|
253
284
|
source.file,
|
|
254
|
-
manifest["kind"],
|
|
255
|
-
manifest["metadata"]["name"],
|
|
256
285
|
)
|
|
257
286
|
exit(1)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
logger.trace(
|
|
268
|
-
"Injecting namespace '{}' into manifest {}/{}.",
|
|
269
|
-
next(iter(namespaces)),
|
|
270
|
-
manifest["kind"],
|
|
271
|
-
manifest["metadata"]["name"],
|
|
272
|
-
)
|
|
273
|
-
manifest["metadata"]["namespace"] = next(iter(namespaces))
|
|
287
|
+
|
|
288
|
+
logger.trace(
|
|
289
|
+
"Injecting namespace '{}' into manifest {}/{}.",
|
|
290
|
+
current_default_namespace,
|
|
291
|
+
manifest["kind"],
|
|
292
|
+
manifest["metadata"]["name"],
|
|
293
|
+
)
|
|
294
|
+
manifest["metadata"]["namespace"] = current_default_namespace
|
|
295
|
+
|
|
274
296
|
elif not is_cluster_scoped_resource(manifest) and manifest["metadata"]["namespace"] not in namespaces:
|
|
275
297
|
logger.warning(
|
|
276
298
|
"Resource '{}/{}' in '{}' references namespace '{}', which is not defined in the file.",
|
|
@@ -295,7 +317,7 @@ def template(
|
|
|
295
317
|
# If we're not going to be applying the manifests immediately via `kubectl`, we print them to stdout.
|
|
296
318
|
for manifest in source.manifests:
|
|
297
319
|
print("---")
|
|
298
|
-
print(yaml.
|
|
320
|
+
print(yaml.dumps(manifest))
|
|
299
321
|
|
|
300
322
|
|
|
301
323
|
def load_manifests(paths: list[Path]) -> list[ManifestsWithSource]:
|
|
@@ -325,7 +347,7 @@ def load_manifests(paths: list[Path]) -> list[ManifestsWithSource]:
|
|
|
325
347
|
|
|
326
348
|
result = []
|
|
327
349
|
for file in files:
|
|
328
|
-
manifests = Manifests(list(map(Manifest, filter(None, yaml.
|
|
350
|
+
manifests = Manifests(list(map(Manifest, filter(None, yaml.loads_all(file.read_text())))))
|
|
329
351
|
result.append(ManifestsWithSource(manifests, file))
|
|
330
352
|
|
|
331
353
|
return result
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Swiss-army-knife for various common operations in conjunction with Nyl (Kubernetes, secrets management, etc).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from nyl.tools.typer import new_typer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
app = new_typer(name="tools", help=__doc__)
|
|
9
|
+
|
|
10
|
+
from . import sops # noqa: E402
|
|
11
|
+
from . import bcrypt # noqa: F401,E402
|
|
12
|
+
|
|
13
|
+
app.add_typer(sops.app)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from bcrypt import gensalt, hashpw
|
|
5
|
+
from . import app
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@app.command()
|
|
9
|
+
def bcrypt(text: Optional[str] = None) -> None:
|
|
10
|
+
"""
|
|
11
|
+
Bcrypt-hash the specified text. If no argument is specified, read from stdin.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
if text is None:
|
|
15
|
+
text = sys.stdin.read()
|
|
16
|
+
|
|
17
|
+
print(hashpw(text.encode("utf-8"), gensalt()).decode("utf-8"))
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Utilities for SOPS files.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
import json
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
from shlex import quote
|
|
8
7
|
import sys
|
|
@@ -17,60 +16,17 @@ from nyl.tools.logging import lazy_str
|
|
|
17
16
|
from nyl.tools.typer import new_typer
|
|
18
17
|
|
|
19
18
|
|
|
20
|
-
app = new_typer(name="
|
|
21
|
-
|
|
22
|
-
sops = new_typer(name="sops", help=__doc__)
|
|
23
|
-
app.add_typer(sops)
|
|
24
|
-
|
|
25
|
-
# Initialized from callback for access by subcommands.
|
|
26
|
-
provider: str
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@app.callback()
|
|
30
|
-
def callback(
|
|
31
|
-
_provider: str = Option(
|
|
32
|
-
"default", "--provider", help="The name of the configured secrets provider to use.", envvar="NYL_SECRETS"
|
|
33
|
-
),
|
|
34
|
-
) -> None:
|
|
35
|
-
"""
|
|
36
|
-
Interact with the secrets providers configured in `nyl-secrets.yaml`.
|
|
37
|
-
"""
|
|
38
|
-
|
|
39
|
-
global provider
|
|
40
|
-
provider = _provider
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@app.command()
|
|
44
|
-
def list(
|
|
45
|
-
providers: bool = Option(
|
|
46
|
-
False, help="List the configured secrets providers instead of the current provider's available keys."
|
|
47
|
-
),
|
|
48
|
-
) -> None:
|
|
49
|
-
"""
|
|
50
|
-
List the keys for all secrets in the provider.
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
|
-
secrets = SecretsConfig.load()
|
|
54
|
-
if providers:
|
|
55
|
-
for alias, impl in secrets.providers.items():
|
|
56
|
-
print(alias, impl)
|
|
57
|
-
else:
|
|
58
|
-
for key in secrets.providers[provider].keys():
|
|
59
|
-
print(key)
|
|
19
|
+
app = new_typer(name="sops", help=__doc__)
|
|
60
20
|
|
|
61
21
|
|
|
62
22
|
@app.command()
|
|
63
|
-
def get(key: str, pretty: bool = False) -> None:
|
|
64
|
-
"""
|
|
65
|
-
Get the value of a secret as JSON.
|
|
66
|
-
"""
|
|
67
|
-
|
|
68
|
-
secrets = SecretsConfig.load()
|
|
69
|
-
print(json.dumps(secrets.providers[provider].get(key), indent=4 if pretty else None))
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
@sops.command()
|
|
73
23
|
def re_encrypt(
|
|
24
|
+
provider: str = Option(
|
|
25
|
+
"default",
|
|
26
|
+
"--provider",
|
|
27
|
+
help="The name of the configured secrets provider to use.",
|
|
28
|
+
envvar="NYL_SECRETS",
|
|
29
|
+
),
|
|
74
30
|
file: Optional[Path] = Option(
|
|
75
31
|
None,
|
|
76
32
|
help="The SOPS-file to re-encrypt. Defaults to the SOPS file mentioned in `nyl-secrets.yaml` "
|
|
@@ -110,7 +66,7 @@ def re_encrypt(
|
|
|
110
66
|
sops.save(file_type)
|
|
111
67
|
|
|
112
68
|
|
|
113
|
-
@
|
|
69
|
+
@app.command()
|
|
114
70
|
def export_dotenv(
|
|
115
71
|
file: Path,
|
|
116
72
|
prefix: str = Option("", help="Only export keys with the given prefix, and strip the prefix."),
|
|
@@ -9,7 +9,7 @@ from textwrap import indent
|
|
|
9
9
|
from urllib.parse import parse_qs, urlparse
|
|
10
10
|
|
|
11
11
|
from loguru import logger
|
|
12
|
-
import yaml
|
|
12
|
+
from nyl.tools import yaml
|
|
13
13
|
from nyl.generator import Generator
|
|
14
14
|
from nyl.resources.helmchart import ChartRef, HelmChart, ReleaseMetadata
|
|
15
15
|
from nyl.tools.shell import pretty_cmd
|
|
@@ -152,7 +152,7 @@ class HelmChartGenerator(Generator[HelmChart], resource_type=HelmChart):
|
|
|
152
152
|
|
|
153
153
|
with TemporaryDirectory() as tmp:
|
|
154
154
|
values_file = Path(tmp) / "values.yaml"
|
|
155
|
-
values_file.write_text(yaml.
|
|
155
|
+
values_file.write_text(yaml.dumps(res.spec.values))
|
|
156
156
|
|
|
157
157
|
command = [
|
|
158
158
|
"helm",
|
|
@@ -207,6 +207,6 @@ class HelmChartGenerator(Generator[HelmChart], resource_type=HelmChart):
|
|
|
207
207
|
f"stderr:\n{indent(e.stderr.decode(), prefix)}"
|
|
208
208
|
)
|
|
209
209
|
|
|
210
|
-
manifests = Manifests(list(filter(None, yaml.
|
|
210
|
+
manifests = Manifests(list(filter(None, yaml.loads_all(result.stdout.decode()))))
|
|
211
211
|
|
|
212
212
|
return manifests
|
|
@@ -5,8 +5,8 @@ import shlex
|
|
|
5
5
|
import subprocess
|
|
6
6
|
from typing import Any
|
|
7
7
|
from urllib.parse import urlparse
|
|
8
|
-
import yaml
|
|
9
8
|
|
|
9
|
+
from nyl.tools import yaml
|
|
10
10
|
from nyl.tools.shell import pretty_cmd
|
|
11
11
|
from .config import KubeconfigFromSsh, LocalKubeconfig
|
|
12
12
|
from loguru import logger
|
|
@@ -87,7 +87,7 @@ class KubeconfigManager:
|
|
|
87
87
|
raise ValueError(f"Unsupported Kubeconfig type: {source.__class__.__name__}")
|
|
88
88
|
|
|
89
89
|
# Find the Kubernetes API host and port.
|
|
90
|
-
kubeconfig_data = yaml.
|
|
90
|
+
kubeconfig_data = yaml.loads(raw_kubeconfig.read_text())
|
|
91
91
|
kubeconfig_data = _trim_to_context(kubeconfig_data, source.context)
|
|
92
92
|
server: str = kubeconfig_data["clusters"][0]["cluster"]["server"]
|
|
93
93
|
api_host, api_port = (lambda parsed: (parsed.hostname, parsed.port or 443))(urlparse(server))
|
|
@@ -108,7 +108,7 @@ class KubeconfigManager:
|
|
|
108
108
|
def get_updated_kubeconfig(
|
|
109
109
|
self, *, profile_name: str, path: Path, context: str, api_host: str, api_port: int
|
|
110
110
|
) -> Path:
|
|
111
|
-
kubeconfig_data = yaml.
|
|
111
|
+
kubeconfig_data = yaml.loads(path.read_text())
|
|
112
112
|
kubeconfig_data = _trim_to_context(kubeconfig_data, context, rename_context=profile_name)
|
|
113
113
|
|
|
114
114
|
# TODO: Do we need to support the Kubernetes API hosted on a subpath?
|
|
@@ -116,7 +116,7 @@ class KubeconfigManager:
|
|
|
116
116
|
|
|
117
117
|
final_kubeconfig = self._state_dir / profile_name / "kubeconfig.local"
|
|
118
118
|
final_kubeconfig.parent.mkdir(parents=True, exist_ok=True)
|
|
119
|
-
final_kubeconfig.write_text(yaml.
|
|
119
|
+
final_kubeconfig.write_text(yaml.dumps(kubeconfig_data))
|
|
120
120
|
final_kubeconfig.chmod(0o600)
|
|
121
121
|
|
|
122
122
|
return final_kubeconfig
|
|
@@ -51,6 +51,16 @@ class Placeholder(NylResource, api_version=API_VERSION_K8S):
|
|
|
51
51
|
"properties": {
|
|
52
52
|
"spec": {
|
|
53
53
|
"type": "object",
|
|
54
|
+
"properties": {
|
|
55
|
+
"message": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"description": "A message that describes the missing resource.",
|
|
58
|
+
},
|
|
59
|
+
"reason": {
|
|
60
|
+
"type": "string",
|
|
61
|
+
"description": "The reason why the resource is missing (e.g. 'NotFound').",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
54
64
|
},
|
|
55
65
|
},
|
|
56
66
|
}
|
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
Implements Nyl's variant of structured templating.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from collections.abc import Mapping
|
|
5
6
|
from contextlib import contextmanager
|
|
6
7
|
from dataclasses import dataclass
|
|
7
|
-
from typing import Any, Callable, ClassVar, Iterator, TypeVar, cast
|
|
8
|
+
from typing import Any, Callable, ClassVar, Iterator, Sequence, TypeVar, cast
|
|
8
9
|
from structured_templates import TemplateEngine as _TemplateEngine
|
|
9
10
|
from structured_templates.exceptions import TemplateError
|
|
10
11
|
from kubernetes.client.api_client import ApiClient
|
|
11
12
|
|
|
12
13
|
from kubernetes.dynamic.client import DynamicClient
|
|
13
14
|
from kubernetes.client.exceptions import ApiException
|
|
15
|
+
from kubernetes.dynamic.resource import ResourceField, ResourceInstance
|
|
14
16
|
from nyl.secrets.config import SecretProvider
|
|
15
17
|
from nyl.tools.types import Manifest, Manifests
|
|
16
18
|
|
|
@@ -90,7 +92,67 @@ def lookup(api_version: str, kind: str, name: str, namespace: str) -> Any:
|
|
|
90
92
|
raise LookupError(f"Resource '{kind}/{name}' not found in namespace '{namespace}'.")
|
|
91
93
|
else:
|
|
92
94
|
raise
|
|
93
|
-
return obj
|
|
95
|
+
return LookupResourceWrapper(obj)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class LookupResourceWrapper(Mapping[str, Any]):
|
|
99
|
+
"""
|
|
100
|
+
A wrapper for a Kubernetes resources returned by `lookup()` that permits looking up fields by `__getitem__()`
|
|
101
|
+
and `__getattr__()`. This wraps a `ResourceInstance` or `ResourceField`, which can later be treated by the
|
|
102
|
+
`NylTemplateEngine` to serialize into a dictionary when embedded into a manifest.
|
|
103
|
+
|
|
104
|
+
This class is needed because the YAML serializer will not be able to serialize the `ResourceInstance` or
|
|
105
|
+
`ResourceField` objects returned by `lookup()`.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def maybe_wrap(obj: Any) -> Any:
|
|
110
|
+
if isinstance(obj, (ResourceInstance, ResourceField)):
|
|
111
|
+
return LookupResourceWrapper(obj)
|
|
112
|
+
if isinstance(obj, Sequence) and not isinstance(obj, str):
|
|
113
|
+
obj = [LookupResourceWrapper.maybe_wrap(x) for x in obj]
|
|
114
|
+
return obj
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def materialize(obj: Any) -> Any:
|
|
118
|
+
match obj:
|
|
119
|
+
case LookupResourceWrapper():
|
|
120
|
+
return obj.__obj.to_dict()
|
|
121
|
+
case str():
|
|
122
|
+
return obj
|
|
123
|
+
if isinstance(obj, Sequence):
|
|
124
|
+
return [LookupResourceWrapper.materialize(x) for x in obj]
|
|
125
|
+
if isinstance(obj, Mapping):
|
|
126
|
+
return {k: LookupResourceWrapper.materialize(v) for k, v in obj.items()}
|
|
127
|
+
return obj
|
|
128
|
+
|
|
129
|
+
def __init__(self, obj: ResourceInstance | ResourceField) -> None:
|
|
130
|
+
assert isinstance(obj, (ResourceInstance, ResourceField)), f"got {type(obj).__name__}"
|
|
131
|
+
self.__obj = obj
|
|
132
|
+
|
|
133
|
+
def __iter__(self) -> Iterator[str]:
|
|
134
|
+
if isinstance(self.__obj, ResourceInstance):
|
|
135
|
+
target = self.__obj.attributes
|
|
136
|
+
else:
|
|
137
|
+
target = self.__obj
|
|
138
|
+
for key, _ in target:
|
|
139
|
+
yield key
|
|
140
|
+
|
|
141
|
+
def __len__(self) -> int:
|
|
142
|
+
# TODO: Find a better (faster) way to get the length of the resource instance/field attributes.
|
|
143
|
+
return len(self.__obj.to_dict())
|
|
144
|
+
|
|
145
|
+
def __getattr__(self, key: str) -> Any:
|
|
146
|
+
return LookupResourceWrapper.maybe_wrap(getattr(self.__obj, key))
|
|
147
|
+
|
|
148
|
+
def __getitem__(self, key: str) -> Any:
|
|
149
|
+
return LookupResourceWrapper.maybe_wrap(self.__obj[key])
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class LookupError(Exception):
|
|
153
|
+
"""
|
|
154
|
+
Raised when a `lookup()` call fails because the resource was not found.
|
|
155
|
+
"""
|
|
94
156
|
|
|
95
157
|
|
|
96
158
|
@dataclass
|
|
@@ -159,6 +221,7 @@ class NylTemplateEngine:
|
|
|
159
221
|
)
|
|
160
222
|
)
|
|
161
223
|
|
|
224
|
+
result = LookupResourceWrapper.materialize(result)
|
|
162
225
|
return Manifests(result)
|
|
163
226
|
|
|
164
227
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from collections.abc import Iterator
|
|
2
|
+
from typing import Any, TextIO
|
|
3
|
+
from yaml import SafeDumper, SafeLoader
|
|
4
|
+
import yaml
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PatchedSafeLoader(SafeLoader):
|
|
8
|
+
yaml_implicit_resolvers = SafeLoader.yaml_implicit_resolvers.copy()
|
|
9
|
+
|
|
10
|
+
# See https://github.com/yaml/pyyaml/issues/89
|
|
11
|
+
yaml_implicit_resolvers.pop("=")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PatchedSafeDumper(SafeDumper):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def dumps(data: Any) -> str:
|
|
19
|
+
return yaml.dump(data, Dumper=PatchedSafeDumper)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def loads(text: str) -> Any:
|
|
23
|
+
return yaml.load(text, Loader=PatchedSafeLoader)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def loads_all(stream: str | TextIO) -> Iterator[Any]:
|
|
27
|
+
return yaml.load_all(stream, Loader=PatchedSafeLoader)
|
{nyl-0.1.1 → nyl-0.2.2}/uv.lock
RENAMED
|
@@ -340,7 +340,7 @@ requires-dist = [
|
|
|
340
340
|
{ name = "pyyaml", specifier = ">=6.0.1" },
|
|
341
341
|
{ name = "requests", specifier = ">=2.32.3" },
|
|
342
342
|
{ name = "stablehash", specifier = ">=0.2.1,<0.3.0" },
|
|
343
|
-
{ name = "structured-templates", specifier = ">=0.
|
|
343
|
+
{ name = "structured-templates", specifier = ">=0.1.1" },
|
|
344
344
|
{ name = "typer", specifier = ">=0.12.3" },
|
|
345
345
|
{ name = "typing-extensions", specifier = ">=4.12.2" },
|
|
346
346
|
]
|
|
@@ -554,11 +554,11 @@ wheels = [
|
|
|
554
554
|
|
|
555
555
|
[[package]]
|
|
556
556
|
name = "structured-templates"
|
|
557
|
-
version = "0.
|
|
557
|
+
version = "0.1.1"
|
|
558
558
|
source = { registry = "https://pypi.org/simple" }
|
|
559
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
559
|
+
sdist = { url = "https://files.pythonhosted.org/packages/04/97/89afe2eecb86ecb2673ebec1d18cc80bd252559ad27dc8940ff5d8a9b45f/structured_templates-0.1.1.tar.gz", hash = "sha256:b94d6c6c0b8a193e869dfa2511cc3b8b73a88f7edd69ecaa9ff4fd5539b6c5dc", size = 6044 }
|
|
560
560
|
wheels = [
|
|
561
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
561
|
+
{ url = "https://files.pythonhosted.org/packages/18/02/d9b2946086264afae6fe757af1a64b8f1629197106b2c93399f5814174d4/structured_templates-0.1.1-py3-none-any.whl", hash = "sha256:72b9e1b8ead52f33e91028838ccdf6ea69e1373da6d26b7b3d20e628472d3eaa", size = 7261 },
|
|
562
562
|
]
|
|
563
563
|
|
|
564
564
|
[[package]]
|
{nyl-0.1.1 → nyl-0.2.2}/.envrc
RENAMED
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|