gitops 0.9.20__tar.gz → 0.10.0__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.
- {gitops-0.9.20 → gitops-0.10.0}/PKG-INFO +5 -5
- {gitops-0.9.20 → gitops-0.10.0}/gitops/__init__.py +2 -4
- {gitops-0.9.20 → gitops-0.10.0}/gitops/common/app.py +10 -16
- {gitops-0.9.20 → gitops-0.10.0}/gitops/common/utils.py +4 -5
- {gitops-0.9.20 → gitops-0.10.0}/gitops/core.py +6 -17
- {gitops-0.9.20 → gitops-0.10.0}/gitops/db.py +8 -21
- {gitops-0.9.20 → gitops-0.10.0}/gitops/shorthands.py +1 -3
- {gitops-0.9.20 → gitops-0.10.0}/gitops/utils/__init__.py +1 -3
- {gitops-0.9.20 → gitops-0.10.0}/gitops/utils/apps.py +31 -19
- {gitops-0.9.20 → gitops-0.10.0}/gitops/utils/async_runner.py +2 -9
- gitops-0.10.0/gitops/utils/exceptions.py +14 -0
- {gitops-0.9.20 → gitops-0.10.0}/gitops/utils/images.py +2 -4
- {gitops-0.9.20 → gitops-0.10.0}/gitops/utils/kube.py +14 -24
- {gitops-0.9.20 → gitops-0.10.0}/pyproject.toml +14 -15
- gitops-0.9.20/gitops/utils/exceptions.py +0 -22
- {gitops-0.9.20 → gitops-0.10.0}/LICENSE +0 -0
- {gitops-0.9.20 → gitops-0.10.0}/gitops/common/__init__.py +0 -0
- {gitops-0.9.20 → gitops-0.10.0}/gitops/main.py +0 -0
- {gitops-0.9.20 → gitops-0.10.0}/gitops/monkeypatches.py +0 -0
- {gitops-0.9.20 → gitops-0.10.0}/gitops/py.typed +0 -0
- {gitops-0.9.20 → gitops-0.10.0}/gitops/settings.py +0 -0
- {gitops-0.9.20 → gitops-0.10.0}/gitops/utils/cli.py +0 -0
- {gitops-0.9.20 → gitops-0.10.0}/gitops/utils/tags.py +0 -0
- {gitops-0.9.20 → gitops-0.10.0}/gitops/utils/yaml.py +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: gitops
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Manage multiple apps across one or more k8s clusters.
|
|
5
5
|
License: BSD
|
|
6
6
|
Author: Jarek Głowacki
|
|
7
7
|
Author-email: jarekwg@gmail.com
|
|
8
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.11,<4.0
|
|
9
9
|
Classifier: License :: Other/Proprietary License
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
13
|
Provides-Extra: server
|
|
14
14
|
Requires-Dist: boto3
|
|
15
15
|
Requires-Dist: colorama (>=0.4.4,<0.5.0)
|
|
@@ -18,8 +18,8 @@ Requires-Dist: fastapi ; extra == "server"
|
|
|
18
18
|
Requires-Dist: httpx (>=0.18.1,<0.24.0) ; extra == "server"
|
|
19
19
|
Requires-Dist: humanize (>=3.5.0,<4.0.0)
|
|
20
20
|
Requires-Dist: invoke
|
|
21
|
-
Requires-Dist: kubernetes_asyncio (>=
|
|
22
|
-
Requires-Dist: pyyaml (>=
|
|
21
|
+
Requires-Dist: kubernetes_asyncio (>=25.1.2,<26.0.0) ; extra == "server"
|
|
22
|
+
Requires-Dist: pyyaml (>=6.0.1)
|
|
23
23
|
Requires-Dist: sentry-sdk (>=1.3.0,<2.0.0) ; extra == "server"
|
|
24
24
|
Requires-Dist: tabulate (>=0.8.9,<0.9.0)
|
|
25
25
|
Requires-Dist: uvicorn (>=0.17.0,<0.18.0) ; extra == "server" or extra == "server"
|
|
@@ -5,13 +5,11 @@ from pathlib import Path
|
|
|
5
5
|
from . import monkeypatches # NOQA
|
|
6
6
|
from .utils.cli import success, warning
|
|
7
7
|
|
|
8
|
-
__version__ = "0.
|
|
8
|
+
__version__ = "0.10.0"
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
# Checking gitops version matches cluster repo version.
|
|
12
|
-
versions_path = (
|
|
13
|
-
Path(os.environ.get("GITOPS_APPS_DIRECTORY", "apps")).parent.absolute() / "setup.cfg"
|
|
14
|
-
)
|
|
12
|
+
versions_path = Path(os.environ.get("GITOPS_APPS_DIRECTORY", "apps")).parent.absolute() / "setup.cfg"
|
|
15
13
|
if versions_path.exists():
|
|
16
14
|
import configparser
|
|
17
15
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
3
|
from base64 import b64encode
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any
|
|
5
5
|
|
|
6
6
|
from .utils import load_yaml
|
|
7
7
|
|
|
@@ -20,9 +20,9 @@ class App:
|
|
|
20
20
|
def __init__(
|
|
21
21
|
self,
|
|
22
22
|
name: str,
|
|
23
|
-
path:
|
|
24
|
-
deployments:
|
|
25
|
-
secrets:
|
|
23
|
+
path: str | None = None,
|
|
24
|
+
deployments: dict | None = None,
|
|
25
|
+
secrets: dict | None = None,
|
|
26
26
|
load_secrets: bool = True,
|
|
27
27
|
account_id: str = "",
|
|
28
28
|
):
|
|
@@ -60,7 +60,7 @@ class App:
|
|
|
60
60
|
current_dict = current_dict.setdefault(key, {})
|
|
61
61
|
current_dict[keys[-1]] = value
|
|
62
62
|
|
|
63
|
-
def _make_values(self, deployments:
|
|
63
|
+
def _make_values(self, deployments: dict, secrets: dict) -> dict:
|
|
64
64
|
values = {
|
|
65
65
|
**deployments,
|
|
66
66
|
"secrets": {**{k: b64encode(v.encode()).decode() for k, v in secrets.items()}},
|
|
@@ -75,7 +75,7 @@ class App:
|
|
|
75
75
|
values.pop("images", None)
|
|
76
76
|
return values
|
|
77
77
|
|
|
78
|
-
def _make_image(self, deployment_config:
|
|
78
|
+
def _make_image(self, deployment_config: dict):
|
|
79
79
|
if "image-tag" in deployment_config:
|
|
80
80
|
return deployment_config["images"]["template"].format(
|
|
81
81
|
account_id=self.account_id,
|
|
@@ -119,16 +119,12 @@ class App:
|
|
|
119
119
|
return self.values.get("cluster", "")
|
|
120
120
|
|
|
121
121
|
@property
|
|
122
|
-
def tags(self) ->
|
|
122
|
+
def tags(self) -> list[str]:
|
|
123
123
|
return self.values.get("tags", [])
|
|
124
124
|
|
|
125
125
|
@property
|
|
126
126
|
def service_account_name(self) -> str:
|
|
127
|
-
return (
|
|
128
|
-
self.values.get("serviceAccount", {}).get("name")
|
|
129
|
-
or self.values.get("serviceAccountName")
|
|
130
|
-
or "default"
|
|
131
|
-
)
|
|
127
|
+
return self.values.get("serviceAccount", {}).get("name") or self.values.get("serviceAccountName") or "default"
|
|
132
128
|
|
|
133
129
|
|
|
134
130
|
class Chart:
|
|
@@ -153,7 +149,7 @@ class Chart:
|
|
|
153
149
|
chart: https://github.com/uptick/workforce
|
|
154
150
|
"""
|
|
155
151
|
|
|
156
|
-
def __init__(self, definition:
|
|
152
|
+
def __init__(self, definition: dict | str):
|
|
157
153
|
if isinstance(definition, str):
|
|
158
154
|
# for backwards compat, any chart definition which is a string, is a git repo
|
|
159
155
|
self.type = "git"
|
|
@@ -169,9 +165,7 @@ class Chart:
|
|
|
169
165
|
self.version = definition.get("version")
|
|
170
166
|
self.path = definition.get("path")
|
|
171
167
|
else:
|
|
172
|
-
raise Exception(
|
|
173
|
-
"Chart definition must be either a dict or string. Instead it is: {definition}"
|
|
174
|
-
)
|
|
168
|
+
raise Exception("Chart definition must be either a dict or string. Instead it is: {definition}")
|
|
175
169
|
|
|
176
170
|
if self.git_repo_url and "@" in self.git_repo_url:
|
|
177
171
|
self.git_repo_url, self.git_sha = self.git_repo_url.split("@")
|
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
import os
|
|
2
|
-
from typing import Dict
|
|
3
2
|
|
|
4
3
|
import yaml
|
|
5
4
|
|
|
6
5
|
|
|
7
|
-
def load_yaml(path) ->
|
|
8
|
-
with open(path
|
|
6
|
+
def load_yaml(path) -> dict:
|
|
7
|
+
with open(path) as file:
|
|
9
8
|
return resolve_values(yaml.safe_load(file), path)
|
|
10
9
|
|
|
11
10
|
|
|
12
|
-
def resolve_values(values, path) ->
|
|
11
|
+
def resolve_values(values, path) -> dict:
|
|
13
12
|
if "extends" not in values:
|
|
14
13
|
return values
|
|
15
14
|
parent_values = load_yaml(os.path.join(os.path.dirname(path), values["extends"]))
|
|
16
15
|
return deep_merge(parent_values, values)
|
|
17
16
|
|
|
18
17
|
|
|
19
|
-
def deep_merge(parent:
|
|
18
|
+
def deep_merge(parent: dict, child: dict) -> dict:
|
|
20
19
|
"""Deeply merge two dictionaries.
|
|
21
20
|
|
|
22
21
|
Dictionary entries will be followed and merged, anything else will be
|
|
@@ -31,7 +31,7 @@ def summary(ctx, filter="", exclude=""):
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
@task
|
|
34
|
-
def bump(
|
|
34
|
+
def bump( # noqa: C901
|
|
35
35
|
ctx,
|
|
36
36
|
filter,
|
|
37
37
|
exclude="",
|
|
@@ -105,10 +105,7 @@ def bump(
|
|
|
105
105
|
print(f"Redeploying {colourise(app_name, Fore.LIGHTGREEN_EX)}")
|
|
106
106
|
update_app(app_name, **{"bump": str(uuid.uuid4())})
|
|
107
107
|
else:
|
|
108
|
-
print(
|
|
109
|
-
f"Skipping {colourise(app_name, Fore.LIGHTGREEN_EX)}: already on"
|
|
110
|
-
f" {colour_image(new_image_tag)}"
|
|
111
|
-
)
|
|
108
|
+
print(f"Skipping {colourise(app_name, Fore.LIGHTGREEN_EX)}: already on" f" {colour_image(new_image_tag)}")
|
|
112
109
|
if redeploy:
|
|
113
110
|
commit_message = f"Redeploying {filter}"
|
|
114
111
|
else:
|
|
@@ -166,18 +163,12 @@ def command(
|
|
|
166
163
|
if sequential or (not interactive) or len(apps) == 1:
|
|
167
164
|
for app in apps:
|
|
168
165
|
# For each app, just run the coroutine and print the output
|
|
169
|
-
print(
|
|
170
|
-
asyncio.run(
|
|
171
|
-
run_job(app, command, cleanup=cleanup, sequential=True, cpu=cpu, memory=memory)
|
|
172
|
-
)
|
|
173
|
-
)
|
|
166
|
+
print(asyncio.run(run_job(app, command, cleanup=cleanup, sequential=True, cpu=cpu, memory=memory)))
|
|
174
167
|
else:
|
|
175
168
|
# Build list of coroutines, and execute them all at once
|
|
176
169
|
jobs = [
|
|
177
170
|
(
|
|
178
|
-
run_job(
|
|
179
|
-
app, command, cleanup=cleanup, sequential=sequential, cpu=cpu, memory=memory
|
|
180
|
-
),
|
|
171
|
+
run_job(app, command, cleanup=cleanup, sequential=sequential, cpu=cpu, memory=memory),
|
|
181
172
|
app.name,
|
|
182
173
|
)
|
|
183
174
|
for app in apps
|
|
@@ -257,10 +248,8 @@ def _getenv(env_or_secrets, filter, exclude, filter_values):
|
|
|
257
248
|
for app in apps:
|
|
258
249
|
print("-" * 20, progress(app.name), sep="\n")
|
|
259
250
|
values = app.values.get(env_or_secrets)
|
|
260
|
-
if
|
|
261
|
-
filtered_values = (
|
|
262
|
-
{k: v for k, v in values.items() if k in filter_values} if filter_values else values
|
|
263
|
-
)
|
|
251
|
+
if isinstance(values, dict):
|
|
252
|
+
filtered_values = {k: v for k, v in values.items() if k in filter_values} if filter_values else values
|
|
264
253
|
for k, v in filtered_values.items():
|
|
265
254
|
print(f"{k}={v}")
|
|
266
255
|
else:
|
|
@@ -129,13 +129,11 @@ def proxy(
|
|
|
129
129
|
except ValueError:
|
|
130
130
|
# app_name was not a valid dsn
|
|
131
131
|
app = get_app_details(app_name, load_secrets=True)
|
|
132
|
-
database_url = base64.b64decode(
|
|
133
|
-
app.values["secrets"]["DATABASE_URL"].encode("ascii")
|
|
134
|
-
).decode("ascii")
|
|
132
|
+
database_url = base64.b64decode(app.values["secrets"]["DATABASE_URL"].encode("ascii")).decode("ascii")
|
|
135
133
|
database_dsn = dsnparse.parse(database_url)
|
|
136
134
|
|
|
137
135
|
if not local_port:
|
|
138
|
-
local_port = random.randint(1000, 9999)
|
|
136
|
+
local_port = random.randint(1000, 9999) # noqa
|
|
139
137
|
|
|
140
138
|
# Maybe we need to connect via RDS IAM instead!
|
|
141
139
|
modified_dsn = dsnparse.parse(database_url)
|
|
@@ -154,15 +152,11 @@ def proxy(
|
|
|
154
152
|
|
|
155
153
|
bastion_instance_id = bastion_instance_id or os.environ.get("GITOPS_BASTION_INSTANCE_ID")
|
|
156
154
|
if not bastion_instance_id:
|
|
157
|
-
raise Exception(
|
|
158
|
-
"Please set GITOPS_BASTION_INSTANCE_ID environment variable for db proxy to work."
|
|
159
|
-
)
|
|
155
|
+
raise Exception("Please set GITOPS_BASTION_INSTANCE_ID environment variable for db proxy to work.")
|
|
160
156
|
|
|
161
157
|
aws_availability_zone = aws_availability_zone or os.environ.get("GITOPS_AWS_AVAILABILITY_ZONE")
|
|
162
158
|
if not aws_availability_zone:
|
|
163
|
-
raise Exception(
|
|
164
|
-
"Please set GITOPS_AWS_AVAILABILITY_ZONE environment variable for db proxy to work."
|
|
165
|
-
)
|
|
159
|
+
raise Exception("Please set GITOPS_AWS_AVAILABILITY_ZONE environment variable for db proxy to work.")
|
|
166
160
|
|
|
167
161
|
print(progress(f"Creating a proxy to the RDS instance of: {app_name} "))
|
|
168
162
|
|
|
@@ -222,17 +216,10 @@ def logs(ctx, app_name, last=24):
|
|
|
222
216
|
)
|
|
223
217
|
for line in log_file["LogFileData"].split("\n"):
|
|
224
218
|
if line:
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
.replace(tzinfo=datetime.timezone.utc)
|
|
230
|
-
.astimezone()
|
|
231
|
-
)
|
|
232
|
-
+ line[23:]
|
|
233
|
-
)
|
|
234
|
-
except Exception:
|
|
235
|
-
print(line)
|
|
219
|
+
print(
|
|
220
|
+
str(datetime.datetime.fromisoformat(line[:19]).replace(tzinfo=datetime.timezone.utc).astimezone())
|
|
221
|
+
+ line[23:]
|
|
222
|
+
)
|
|
236
223
|
|
|
237
224
|
|
|
238
225
|
@task
|
|
@@ -50,9 +50,7 @@ def shell_plus(ctx, app, cleanup=True, cpu=0, memory=0):
|
|
|
50
50
|
app = get_app_details(app)
|
|
51
51
|
if "production" in app.tags:
|
|
52
52
|
confirm_dangerous_command()
|
|
53
|
-
asyncio.run(
|
|
54
|
-
run_job(app, f"{UNSAFE_MANAGE_PY} shell_plus", cleanup=cleanup, cpu=cpu, memory=memory)
|
|
55
|
-
)
|
|
53
|
+
asyncio.run(run_job(app, f"{UNSAFE_MANAGE_PY} shell_plus", cleanup=cleanup, cpu=cpu, memory=memory))
|
|
56
54
|
print(success("Done!"))
|
|
57
55
|
|
|
58
56
|
|
|
@@ -9,9 +9,7 @@ CACHE = {}
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def gen_secret(length=64):
|
|
12
|
-
return "".join(
|
|
13
|
-
random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(length)
|
|
14
|
-
)
|
|
12
|
+
return "".join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(length))
|
|
15
13
|
|
|
16
14
|
|
|
17
15
|
def get_account_id():
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
from
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import PosixPath
|
|
3
3
|
|
|
4
4
|
from colorama import Fore
|
|
5
5
|
from tabulate import tabulate
|
|
@@ -10,12 +10,18 @@ from gitops.utils import yaml as yaml
|
|
|
10
10
|
|
|
11
11
|
from . import get_account_id
|
|
12
12
|
from .cli import colourise, confirm, warning
|
|
13
|
-
from .exceptions import AppDoesNotExist, AppOperationAborted
|
|
13
|
+
from .exceptions import AppDirectoryDoesNotExist, AppDoesNotExist, AppOperationAborted
|
|
14
14
|
from .images import colour_image
|
|
15
15
|
from .tags import colour_tags, validate_tags
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def
|
|
18
|
+
def is_valid_app_directory(directory: PosixPath) -> bool:
|
|
19
|
+
files = ["deployment.yml", "secrets.yml"]
|
|
20
|
+
file_paths = [(directory / file).is_file() for file in files]
|
|
21
|
+
return all(file_paths)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_app_details(app_name: str, load_secrets: bool = True, exit_if_not_found: bool = True) -> App:
|
|
19
25
|
account_id = get_account_id() if load_secrets else "UNKNOWN"
|
|
20
26
|
try:
|
|
21
27
|
app = App(
|
|
@@ -24,19 +30,23 @@ def get_app_details(app_name: str, load_secrets: bool = True) -> App:
|
|
|
24
30
|
load_secrets=load_secrets,
|
|
25
31
|
account_id=account_id,
|
|
26
32
|
)
|
|
27
|
-
except FileNotFoundError:
|
|
28
|
-
|
|
29
|
-
if
|
|
30
|
-
|
|
33
|
+
except FileNotFoundError as e:
|
|
34
|
+
msg, exc = "", Exception
|
|
35
|
+
if get_apps_directory().exists():
|
|
36
|
+
msg, exc = f"There's no app with the name '{app_name}', silly.", AppDoesNotExist
|
|
31
37
|
else:
|
|
32
|
-
|
|
38
|
+
msg, exc = "Could not find an 'apps' directory. Are you in a cluster repo?", AppDirectoryDoesNotExist
|
|
39
|
+
if exit_if_not_found:
|
|
40
|
+
sys.exit(warning(msg))
|
|
41
|
+
else:
|
|
42
|
+
raise exc(msg) from e
|
|
33
43
|
|
|
34
44
|
return app
|
|
35
45
|
|
|
36
46
|
|
|
37
47
|
def update_app(app_name: str, **kwargs):
|
|
38
48
|
filename = get_apps_directory() / app_name / "deployment.yml"
|
|
39
|
-
with open(filename
|
|
49
|
+
with open(filename) as f:
|
|
40
50
|
data = yaml.safe_load(f)
|
|
41
51
|
for k, v in kwargs.items():
|
|
42
52
|
if k not in DEPLOYMENT_ATTRIBUTES:
|
|
@@ -50,14 +60,14 @@ def update_app(app_name: str, **kwargs):
|
|
|
50
60
|
yaml.dump(data, f, default_flow_style=False)
|
|
51
61
|
|
|
52
62
|
|
|
53
|
-
def get_apps(
|
|
54
|
-
filter:
|
|
55
|
-
exclude:
|
|
63
|
+
def get_apps( # noqa: C901
|
|
64
|
+
filter: list[str] | str = "",
|
|
65
|
+
exclude: list[str] | str = "",
|
|
56
66
|
mode="PROMPT",
|
|
57
67
|
autoexclude_inactive=True,
|
|
58
68
|
message=None,
|
|
59
69
|
load_secrets=True,
|
|
60
|
-
) ->
|
|
70
|
+
) -> list[App]:
|
|
61
71
|
"""Return apps that contain ALL of the tags listed in `filter` and NONE of the tags listed in
|
|
62
72
|
`exclude`. The incoming filter and exclude params may come in as a list or commastring.
|
|
63
73
|
For the purpose of this filtering, app names and image tag prefixes are also considered as
|
|
@@ -80,15 +90,17 @@ def get_apps(
|
|
|
80
90
|
exclude.add("inactive")
|
|
81
91
|
|
|
82
92
|
apps = []
|
|
83
|
-
existing_tags =
|
|
93
|
+
existing_tags = {"suspended", "inactive", "release", "qa", "no_shutdown"}
|
|
84
94
|
|
|
85
95
|
try:
|
|
86
96
|
directory = sorted(get_apps_directory().iterdir())
|
|
87
|
-
except FileNotFoundError:
|
|
88
|
-
raise AppDoesNotExist()
|
|
97
|
+
except FileNotFoundError as e:
|
|
98
|
+
raise AppDoesNotExist() from e
|
|
89
99
|
for entry in directory:
|
|
90
100
|
if not entry.is_dir():
|
|
91
101
|
continue
|
|
102
|
+
elif not is_valid_app_directory(entry):
|
|
103
|
+
continue
|
|
92
104
|
app = get_app_details(entry.name, load_secrets=load_secrets)
|
|
93
105
|
|
|
94
106
|
pseudotags = [app.name, app.cluster]
|
|
@@ -116,7 +128,7 @@ def get_apps(
|
|
|
116
128
|
return apps
|
|
117
129
|
|
|
118
130
|
|
|
119
|
-
def preview_apps(apps:
|
|
131
|
+
def preview_apps(apps: list[App]):
|
|
120
132
|
"""Produce a summary of apps, their tags, and their expected images & replicas.
|
|
121
133
|
May not necessarily reflect actual app statuses if recent changes haven't yet been pushed to
|
|
122
134
|
the remote, or the deployment has failed.
|
|
@@ -125,7 +137,7 @@ def preview_apps(apps: List[App]):
|
|
|
125
137
|
for app in apps:
|
|
126
138
|
table.append(
|
|
127
139
|
[
|
|
128
|
-
colourise(app.name, Fore.RED, lambda _: "inactive" in app.tags),
|
|
140
|
+
colourise(app.name, Fore.RED, lambda _: "inactive" in app.tags), # noqa
|
|
129
141
|
colour_image(app.image_tag),
|
|
130
142
|
app.cluster,
|
|
131
143
|
colourise(
|
|
@@ -76,18 +76,11 @@ async def print_async_complete(task, x, y, status_y_offset, win_info, sem):
|
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
async def async_run(cmd):
|
|
79
|
-
proc = await asyncio.create_subprocess_shell(
|
|
80
|
-
cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
|
81
|
-
)
|
|
79
|
+
proc = await asyncio.create_subprocess_shell(cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
|
|
82
80
|
|
|
83
81
|
stdout, stderr = await proc.communicate()
|
|
84
82
|
return (
|
|
85
83
|
stdout,
|
|
86
84
|
stderr,
|
|
87
|
-
(
|
|
88
|
-
success("[stdout]\n")
|
|
89
|
-
+ f"{stdout.decode()}\n"
|
|
90
|
-
+ warning("[stderr]\n")
|
|
91
|
-
+ f"{stderr.decode()}\n"
|
|
92
|
-
),
|
|
85
|
+
(success("[stdout]\n") + f"{stdout.decode()}\n" + warning("[stderr]\n") + f"{stderr.decode()}\n"),
|
|
93
86
|
)
|
|
@@ -29,9 +29,7 @@ def get_latest_image(repository_name: str, prefix: str) -> str:
|
|
|
29
29
|
):
|
|
30
30
|
for image in ecr_response["imageDetails"]:
|
|
31
31
|
if prefix != "":
|
|
32
|
-
if prefix_tags := [
|
|
33
|
-
tag for tag in image["imageTags"] if tag.startswith(prefix + "-")
|
|
34
|
-
]:
|
|
32
|
+
if prefix_tags := [tag for tag in image["imageTags"] if tag.startswith(prefix + "-")]:
|
|
35
33
|
results.append((prefix_tags[0], image["imagePushedAt"]))
|
|
36
34
|
else:
|
|
37
35
|
if prefix_tags := [tag for tag in image["imageTags"] if "-" not in tag]:
|
|
@@ -69,4 +67,4 @@ def color_hash(bit):
|
|
|
69
67
|
Fore.MAGENTA,
|
|
70
68
|
Fore.CYAN,
|
|
71
69
|
Fore.WHITE,
|
|
72
|
-
][int.from_bytes(md5(bit.encode()).digest(), "big") % 7]
|
|
70
|
+
][int.from_bytes(md5(bit.encode()).digest(), "big") % 7] # noqa: S324
|
|
@@ -12,7 +12,7 @@ from base64 import b64encode
|
|
|
12
12
|
from contextlib import contextmanager
|
|
13
13
|
from datetime import datetime
|
|
14
14
|
from functools import wraps
|
|
15
|
-
from typing import
|
|
15
|
+
from typing import TypedDict
|
|
16
16
|
|
|
17
17
|
import boto3
|
|
18
18
|
import humanize
|
|
@@ -105,7 +105,7 @@ def download_backup(product, prefix, index, path=None, datestamp=False):
|
|
|
105
105
|
def copy_db(ctx, source, destination, context=""):
|
|
106
106
|
"""Copy database between two applications."""
|
|
107
107
|
with tempfile.NamedTemporaryFile("wt", suffix=".yml") as tmp:
|
|
108
|
-
job = open("copy-db-job.yml"
|
|
108
|
+
job = open("copy-db-job.yml").read()
|
|
109
109
|
job = job.replace("{{ source }}", source)
|
|
110
110
|
job = job.replace("{{ destination }}", destination)
|
|
111
111
|
tmp.write(job)
|
|
@@ -125,9 +125,7 @@ def copy_db(ctx, source, destination, context=""):
|
|
|
125
125
|
)
|
|
126
126
|
print("done")
|
|
127
127
|
run(
|
|
128
|
-
("kubectl delete -n workforce job copy-db-{source}-{destination}")
|
|
129
|
-
source=source, destination=destination
|
|
130
|
-
),
|
|
128
|
+
(f"kubectl delete -n workforce job copy-db-{source}-{destination}"),
|
|
131
129
|
hide=True,
|
|
132
130
|
)
|
|
133
131
|
|
|
@@ -140,7 +138,7 @@ def get_backups(product, prefix):
|
|
|
140
138
|
name = obj.key.split("/")[-1].split(".")[0]
|
|
141
139
|
if not name:
|
|
142
140
|
continue
|
|
143
|
-
dt = datetime.strptime(name, "%Y-%m-%d_%H-%M-%S")
|
|
141
|
+
dt = datetime.strptime(name, "%Y-%m-%d_%H-%M-%S").astimezone(None)
|
|
144
142
|
all_backups.append((name, dt, obj.size, obj.key))
|
|
145
143
|
return sorted(all_backups, key=lambda x: x[1])
|
|
146
144
|
|
|
@@ -154,9 +152,9 @@ def get_backup_datestamp(url):
|
|
|
154
152
|
def get_secret(name, base64=False):
|
|
155
153
|
try:
|
|
156
154
|
value = os.environ[name]
|
|
157
|
-
except KeyError:
|
|
155
|
+
except KeyError as e:
|
|
158
156
|
msg = f"Variable {Fore.RED}{name}{Fore.RESET} missing from environment."
|
|
159
|
-
raise CommandError(msg)
|
|
157
|
+
raise CommandError(msg) from e
|
|
160
158
|
if base64:
|
|
161
159
|
value = b64encode(value.encode()).decode()
|
|
162
160
|
else:
|
|
@@ -183,15 +181,13 @@ def run_wrapper(intro):
|
|
|
183
181
|
|
|
184
182
|
def make_key(length=64):
|
|
185
183
|
"""Generate a sequence of random characters."""
|
|
186
|
-
return "".join(
|
|
187
|
-
random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(length)
|
|
188
|
-
)
|
|
184
|
+
return "".join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(length))
|
|
189
185
|
|
|
190
186
|
|
|
191
187
|
def render_template(
|
|
192
188
|
template: str,
|
|
193
|
-
values:
|
|
194
|
-
extra_labels:
|
|
189
|
+
values: dict = None,
|
|
190
|
+
extra_labels: dict = None,
|
|
195
191
|
container_resources: ContainerResources = None,
|
|
196
192
|
) -> str:
|
|
197
193
|
"""Given a yaml of a K8s Job, replace template values and add extra labels to the pod spec"""
|
|
@@ -214,13 +210,13 @@ def render_template(
|
|
|
214
210
|
|
|
215
211
|
async def _run_job(
|
|
216
212
|
path,
|
|
217
|
-
values:
|
|
213
|
+
values: dict = None,
|
|
218
214
|
context="",
|
|
219
215
|
namespace="default",
|
|
220
216
|
attach=False,
|
|
221
217
|
cleanup=True,
|
|
222
218
|
sequential=True,
|
|
223
|
-
extra_labels:
|
|
219
|
+
extra_labels: dict = None,
|
|
224
220
|
container_resources: ContainerResources = None,
|
|
225
221
|
):
|
|
226
222
|
name = values["name"]
|
|
@@ -228,7 +224,7 @@ async def _run_job(
|
|
|
228
224
|
with tempfile.NamedTemporaryFile("wt", suffix=".yml") as tmp:
|
|
229
225
|
# Generate yaml template to render
|
|
230
226
|
rendered_template = render_template(
|
|
231
|
-
open(get_apps_directory() / ".." / path
|
|
227
|
+
open(get_apps_directory() / ".." / path).read(),
|
|
232
228
|
values,
|
|
233
229
|
extra_labels,
|
|
234
230
|
container_resources=container_resources,
|
|
@@ -293,20 +289,14 @@ async def _run_job(
|
|
|
293
289
|
|
|
294
290
|
async def wait_for_pod(context, namespace, pod):
|
|
295
291
|
while True:
|
|
296
|
-
cmd =
|
|
297
|
-
"kubectl get pod"
|
|
298
|
-
f" --context {context}"
|
|
299
|
-
f" -n {namespace}"
|
|
300
|
-
' -o jsonpath="{.status.phase}"'
|
|
301
|
-
f" {pod}"
|
|
302
|
-
)
|
|
292
|
+
cmd = "kubectl get pod" f" --context {context}" f" -n {namespace}" ' -o jsonpath="{.status.phase}"' f" {pod}"
|
|
303
293
|
stdout, _, _ = await async_run(cmd)
|
|
304
294
|
stdout = stdout.decode().lower()
|
|
305
295
|
if stdout != "pending":
|
|
306
296
|
return stdout
|
|
307
297
|
|
|
308
298
|
|
|
309
|
-
def retry(*args, max_attempts=3, delay=1):
|
|
299
|
+
def retry(*args, max_attempts=3, delay=1): # noqa: C901
|
|
310
300
|
"""A decorator for retrying a function.
|
|
311
301
|
|
|
312
302
|
After `max_attempts` failed attempts the last thrown exception
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "gitops"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.10.0"
|
|
4
4
|
description = "Manage multiple apps across one or more k8s clusters."
|
|
5
5
|
authors = ["Jarek Głowacki <jarekwg@gmail.com>"]
|
|
6
6
|
license = "BSD"
|
|
7
7
|
|
|
8
8
|
[tool.poetry.dependencies]
|
|
9
|
-
python = "^3.
|
|
9
|
+
python = "^3.11"
|
|
10
10
|
# CLI requirements
|
|
11
11
|
invoke = "*"
|
|
12
|
-
pyyaml = ">=
|
|
12
|
+
pyyaml = ">=6.0.1"
|
|
13
13
|
dsnparse = "*"
|
|
14
14
|
colorama = "^0.4.4"
|
|
15
15
|
boto3 = "*"
|
|
@@ -20,7 +20,7 @@ fastapi = { version = "*", optional = true }
|
|
|
20
20
|
httpx = { version = ">=0.18.1,<0.24.0", optional = true }
|
|
21
21
|
uvicorn = { version = "^0.17.0", optional = true }
|
|
22
22
|
sentry-sdk = { version = "^1.3.0", optional = true }
|
|
23
|
-
kubernetes_asyncio = { version = "^
|
|
23
|
+
kubernetes_asyncio = { version = "^25.1.2", optional = true }
|
|
24
24
|
|
|
25
25
|
[tool.poetry.extras]
|
|
26
26
|
server = [
|
|
@@ -33,14 +33,14 @@ server = [
|
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
[tool.poetry.dev-dependencies]
|
|
36
|
-
|
|
36
|
+
ruff = "^0.2.0"
|
|
37
37
|
isort = "*"
|
|
38
38
|
ipdb = "*"
|
|
39
39
|
python-dotenv = "*"
|
|
40
40
|
pytest = "*"
|
|
41
41
|
requests = "^2.25.1"
|
|
42
42
|
pre-commit = "^2.13.0"
|
|
43
|
-
mypy = "^0
|
|
43
|
+
mypy = "^1.8.0"
|
|
44
44
|
black = "*"
|
|
45
45
|
pytest-asyncio = "^0.20.3"
|
|
46
46
|
|
|
@@ -56,12 +56,11 @@ gitops = 'gitops.main:program.run'
|
|
|
56
56
|
requires = ["poetry-core>=1.0.0"]
|
|
57
57
|
build-backend = "poetry.core.masonry.api"
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
multi_line_output = 3
|
|
59
|
+
[tool.nitpick]
|
|
60
|
+
# Generated by the 'nitpick init' command
|
|
61
|
+
# More info at https://nitpick.rtfd.io/en/latest/configuration.html
|
|
62
|
+
style = [
|
|
63
|
+
'https://raw.githubusercontent.com/uptick/.github/main/nitpick/python.toml',
|
|
64
|
+
'py://nitpick/resources/any/commitizen',
|
|
65
|
+
]
|
|
66
|
+
cache = "never"
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
|
|
3
|
-
from .cli import warning
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class AppOperationAborted(Exception):
|
|
7
|
-
pass
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class AppDoesNotExist(Exception):
|
|
11
|
-
def __init__(self, app_name=None):
|
|
12
|
-
# Funky hack to stomp on traceback.
|
|
13
|
-
if app_name:
|
|
14
|
-
message = f"There's no app with the name '{app_name}', silly."
|
|
15
|
-
else:
|
|
16
|
-
message = "Could not find an 'apps' directory. Are you in a cluster repo?"
|
|
17
|
-
self.args = (warning(message),)
|
|
18
|
-
sys.exit(self)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class CommandError(Exception):
|
|
22
|
-
pass
|
|
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
|