gitops 0.11.6__tar.gz → 0.12.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.
Files changed (79) hide show
  1. {gitops-0.11.6 → gitops-0.12.0}/PKG-INFO +1 -1
  2. {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/Chart.yaml +1 -1
  3. {gitops-0.11.6 → gitops-0.12.0}/gitops/__init__.py +1 -1
  4. {gitops-0.11.6 → gitops-0.12.0}/gitops/common/app.py +5 -0
  5. {gitops-0.11.6 → gitops-0.12.0}/gitops/main.py +5 -4
  6. gitops-0.12.0/gitops/utils/images.py +113 -0
  7. {gitops-0.11.6 → gitops-0.12.0}/pyproject.toml +1 -1
  8. gitops-0.11.6/gitops/db.py +0 -254
  9. gitops-0.11.6/gitops/utils/images.py +0 -70
  10. gitops-0.11.6/poetry.lock +0 -1808
  11. {gitops-0.11.6 → gitops-0.12.0}/.dockerignore +0 -0
  12. {gitops-0.11.6 → gitops-0.12.0}/.github/workflows/publish.yml +0 -0
  13. {gitops-0.11.6 → gitops-0.12.0}/.github/workflows/test.yml +0 -0
  14. {gitops-0.11.6 → gitops-0.12.0}/.gitignore +0 -0
  15. {gitops-0.11.6 → gitops-0.12.0}/.mise.toml +0 -0
  16. {gitops-0.11.6 → gitops-0.12.0}/.pre-commit-config.yaml +0 -0
  17. {gitops-0.11.6 → gitops-0.12.0}/Dockerfile +0 -0
  18. {gitops-0.11.6 → gitops-0.12.0}/LICENSE +0 -0
  19. {gitops-0.11.6 → gitops-0.12.0}/README.md +0 -0
  20. {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/.helmignore +0 -0
  21. {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/README.md +0 -0
  22. {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/templates/_helpers.tpl +0 -0
  23. {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/templates/certificate.yml +0 -0
  24. {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/templates/deployment.yml +0 -0
  25. {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/templates/environment.yml +0 -0
  26. {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/templates/ingress.yml +0 -0
  27. {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/templates/issuer.yml +0 -0
  28. {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/templates/secrets.yml +0 -0
  29. {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/templates/service.yml +0 -0
  30. {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/templates/serviceaccount.yaml +0 -0
  31. {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/values.yaml +0 -0
  32. {gitops-0.11.6 → gitops-0.12.0}/docker-compose.yml +0 -0
  33. {gitops-0.11.6 → gitops-0.12.0}/gitops/common/__init__.py +0 -0
  34. {gitops-0.11.6 → gitops-0.12.0}/gitops/common/utils.py +0 -0
  35. {gitops-0.11.6 → gitops-0.12.0}/gitops/core.py +0 -0
  36. {gitops-0.11.6 → gitops-0.12.0}/gitops/py.typed +0 -0
  37. {gitops-0.11.6 → gitops-0.12.0}/gitops/settings.py +0 -0
  38. {gitops-0.11.6 → gitops-0.12.0}/gitops/shorthands.py +0 -0
  39. {gitops-0.11.6 → gitops-0.12.0}/gitops/utils/__init__.py +0 -0
  40. {gitops-0.11.6 → gitops-0.12.0}/gitops/utils/apps.py +0 -0
  41. {gitops-0.11.6 → gitops-0.12.0}/gitops/utils/async_runner.py +0 -0
  42. {gitops-0.11.6 → gitops-0.12.0}/gitops/utils/cli.py +0 -0
  43. {gitops-0.11.6 → gitops-0.12.0}/gitops/utils/exceptions.py +0 -0
  44. {gitops-0.11.6 → gitops-0.12.0}/gitops/utils/kube.py +0 -0
  45. {gitops-0.11.6 → gitops-0.12.0}/gitops/utils/tags.py +0 -0
  46. {gitops-0.11.6 → gitops-0.12.0}/gitops/utils/yaml.py +0 -0
  47. {gitops-0.11.6 → gitops-0.12.0}/gitops_server/README.md +0 -0
  48. {gitops-0.11.6 → gitops-0.12.0}/gitops_server/__init__.py +0 -0
  49. {gitops-0.11.6 → gitops-0.12.0}/gitops_server/app.py +0 -0
  50. {gitops-0.11.6 → gitops-0.12.0}/gitops_server/main.py +0 -0
  51. {gitops-0.11.6 → gitops-0.12.0}/gitops_server/settings.py +0 -0
  52. {gitops-0.11.6 → gitops-0.12.0}/gitops_server/types.py +0 -0
  53. {gitops-0.11.6 → gitops-0.12.0}/gitops_server/utils/__init__.py +0 -0
  54. {gitops-0.11.6 → gitops-0.12.0}/gitops_server/utils/git.py +0 -0
  55. {gitops-0.11.6 → gitops-0.12.0}/gitops_server/utils/github.py +0 -0
  56. {gitops-0.11.6 → gitops-0.12.0}/gitops_server/utils/slack.py +0 -0
  57. {gitops-0.11.6 → gitops-0.12.0}/gitops_server/workers/__init__.py +0 -0
  58. {gitops-0.11.6 → gitops-0.12.0}/gitops_server/workers/deployer/__init__.py +0 -0
  59. {gitops-0.11.6 → gitops-0.12.0}/gitops_server/workers/deployer/deploy.py +0 -0
  60. {gitops-0.11.6 → gitops-0.12.0}/gitops_server/workers/deployer/hooks.py +0 -0
  61. {gitops-0.11.6 → gitops-0.12.0}/gitops_server/workers/deployer/worker.py +0 -0
  62. {gitops-0.11.6 → gitops-0.12.0}/gitops_server/workers/status_updater/__init__.py +0 -0
  63. {gitops-0.11.6 → gitops-0.12.0}/gitops_server/workers/status_updater/worker.py +0 -0
  64. {gitops-0.11.6 → gitops-0.12.0}/release.py +0 -0
  65. {gitops-0.11.6 → gitops-0.12.0}/ruff.toml +0 -0
  66. {gitops-0.11.6 → gitops-0.12.0}/secrets.example.env +0 -0
  67. {gitops-0.11.6 → gitops-0.12.0}/setup.cfg +0 -0
  68. {gitops-0.11.6 → gitops-0.12.0}/tests/__init__.py +0 -0
  69. {gitops-0.11.6 → gitops-0.12.0}/tests/conftest.py +0 -0
  70. {gitops-0.11.6 → gitops-0.12.0}/tests/sample_data.py +0 -0
  71. {gitops-0.11.6 → gitops-0.12.0}/tests/test_app.py +0 -0
  72. {gitops-0.11.6 → gitops-0.12.0}/tests/test_core.py +0 -0
  73. {gitops-0.11.6 → gitops-0.12.0}/tests/test_deploy.py +0 -0
  74. {gitops-0.11.6 → gitops-0.12.0}/tests/test_git.py +0 -0
  75. {gitops-0.11.6 → gitops-0.12.0}/tests/test_gitops_server.py +0 -0
  76. {gitops-0.11.6 → gitops-0.12.0}/tests/test_utils.py +0 -0
  77. {gitops-0.11.6 → gitops-0.12.0}/tests/utils.py +0 -0
  78. {gitops-0.11.6 → gitops-0.12.0}/tests/webhook_sample_data.py +0 -0
  79. {gitops-0.11.6 → gitops-0.12.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: gitops
3
- Version: 0.11.6
3
+ Version: 0.12.0
4
4
  Summary: Manage multiple apps across one or more k8s clusters.
5
5
  Author-email: Jarek Głowacki <jarekwg@gmail.com>
6
6
  License-File: LICENSE
@@ -2,4 +2,4 @@ apiVersion: v1
2
2
  appVersion: "1.0"
3
3
  description: GitOps Server Helm chart.
4
4
  name: gitops
5
- version: 0.11.6
5
+ version: 0.12.0
@@ -6,7 +6,7 @@ from gitops.utils.apps import App, get_app_details, get_apps
6
6
 
7
7
  from .utils.cli import success, warning
8
8
 
9
- __version__ = "0.11.6"
9
+ __version__ = "0.12.0"
10
10
 
11
11
 
12
12
  # Checking gitops version matches cluster repo version.
@@ -113,6 +113,11 @@ class App:
113
113
  """305686791668.dkr.ecr.ap-southeast-2.amazonaws.com/[uptick]:yoink-9f03ac80f3"""
114
114
  return self.image.split(":")[0].split("/")[-1]
115
115
 
116
+ @property
117
+ def image_repository(self) -> str:
118
+ """[305686791668.dkr.ecr.ap-southeast-2.amazonaws.com]/uptick:yoink-9f03ac80f3"""
119
+ return self.image.split("/")[0]
120
+
116
121
  @property
117
122
  def image_tag(self) -> str:
118
123
  """305686791668.dkr.ecr.ap-southeast-2.amazonaws.com/uptick:[yoink-9f03ac80f3]"""
@@ -1,6 +1,10 @@
1
+ import logging
2
+
1
3
  from invoke import Collection, Program, Task
2
4
 
3
- from . import __version__, core, db, shorthands
5
+ from . import __version__, core, shorthands
6
+
7
+ logging.basicConfig(level=logging.INFO, format="%(message)s")
4
8
 
5
9
  version = __version__
6
10
 
@@ -12,7 +16,4 @@ for core_ns in [core, shorthands]:
12
16
  for task in tasks:
13
17
  namespace.add_task(task)
14
18
 
15
- # Namespace the rarer ones.
16
- namespace.add_collection(db) # type: ignore
17
-
18
19
  program = Program(namespace=namespace, version=version)
@@ -0,0 +1,113 @@
1
+ import datetime
2
+ import logging
3
+ from functools import lru_cache
4
+ from hashlib import md5
5
+ from typing import TYPE_CHECKING
6
+
7
+ import boto3
8
+ import botocore
9
+ import botocore.exceptions
10
+ from colorama import Fore
11
+
12
+ from .cli import colourise
13
+
14
+ if TYPE_CHECKING:
15
+ from mypy_boto3_ecr.type_defs import ImageDetailTypeDef # type: ignore
16
+ BATCH_SIZE = 100
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def get_image(tag: str) -> str:
22
+ """Finds a specific image in ECR."""
23
+ # TODO
24
+ raise NotImplementedError
25
+
26
+
27
+ @lru_cache
28
+ def get_latest_image(repository_name: str, prefix: str, ecr_repository: str | None = None) -> str | None: # noqa:C901
29
+ """Finds latest image in ECR with the given prefix and returns the image tag
30
+
31
+ param ecr_repository is expected in this format: 305686791668.dkr.ecr.ap-southeast-2.amazonaws.com
32
+ """
33
+ describe_image_args = {}
34
+ region_name = None
35
+ if ecr_repository:
36
+ account_id = ecr_repository.split(".")[0]
37
+ describe_image_args["registryId"] = account_id
38
+ region_name = ecr_repository.split(".")[3]
39
+
40
+ ecr_client = boto3.client("ecr", region_name=region_name)
41
+ client_paginator = ecr_client.get_paginator("describe_images")
42
+
43
+ results = []
44
+
45
+ # First we try to find the image with `*latest`
46
+ image_tag = f"{prefix}-latest" if prefix else "latest"
47
+
48
+ def add_image_to_results(image: "ImageDetailTypeDef") -> None:
49
+ if prefix != "":
50
+ if prefix_tags := [
51
+ tag for tag in image["imageTags"] if tag.startswith(prefix + "-") and "latest" not in tag
52
+ ]:
53
+ results.append((prefix_tags[0], image["imagePushedAt"]))
54
+ else:
55
+ if prefix_tags := [tag for tag in image["imageTags"] if "-" not in tag and "latest" not in tag]:
56
+ results.append((prefix_tags[0], image["imagePushedAt"]))
57
+
58
+ try:
59
+ image = ecr_client.describe_images(
60
+ repositoryName=repository_name,
61
+ imageIds=[{"imageTag": image_tag}],
62
+ **describe_image_args, # type: ignore
63
+ )["imageDetails"][0]
64
+ # TODO: Remove this check after we've fully migrated to the new image tagging scheme
65
+ if image["imagePushedAt"] < datetime.datetime(2024, 9, 1, 0, 0, 0, tzinfo=datetime.timezone.utc):
66
+ logger.warning(f"Image {image_tag} is too old to be considered latest.")
67
+ raise ValueError("Image is too old")
68
+
69
+ add_image_to_results(image)
70
+ except (botocore.exceptions.ClientError, ValueError):
71
+ # Ok we couldn't find the -latest image; lets scan images manually
72
+ for ecr_response in client_paginator.paginate(
73
+ repositoryName=repository_name,
74
+ filter={
75
+ "tagStatus": "TAGGED",
76
+ },
77
+ **describe_image_args, # type: ignore
78
+ ):
79
+ for image in ecr_response["imageDetails"]:
80
+ add_image_to_results(image)
81
+ if not results:
82
+ if prefix:
83
+ logger.info(f'No images found in repository: {repository_name} with tag "{prefix}-*".')
84
+ else:
85
+ logger.info(f"No images found in repository: {repository_name}")
86
+ return None
87
+
88
+ latest_image_tag = sorted(results, key=lambda image: image[1], reverse=True)[0][0]
89
+ return latest_image_tag
90
+
91
+
92
+ def colour_image(image_tag: str) -> str:
93
+ if not image_tag:
94
+ return image_tag
95
+
96
+ bits = image_tag.split("-")
97
+ if len(bits) > 1:
98
+ bits[0] = colourise(bits[0], color_hash(bits[1]))
99
+ return "-".join(bits)
100
+ else:
101
+ return colourise(bits[0], color_hash(bits[0]))
102
+
103
+
104
+ def color_hash(bit: str) -> str:
105
+ return [
106
+ Fore.RED,
107
+ Fore.GREEN,
108
+ Fore.YELLOW,
109
+ Fore.BLUE,
110
+ Fore.MAGENTA,
111
+ Fore.CYAN,
112
+ Fore.WHITE,
113
+ ][int.from_bytes(md5(bit.encode()).digest(), "big") % 7] # noqa: S324
@@ -1,7 +1,7 @@
1
1
 
2
2
  [project]
3
3
  name = "gitops"
4
- version = "0.11.6"
4
+ version = "0.12.0"
5
5
  description = "Manage multiple apps across one or more k8s clusters."
6
6
  requires-python = ">=3.12"
7
7
  readme = "README.md"
@@ -1,254 +0,0 @@
1
- import asyncio
2
- import base64
3
- import datetime
4
- import os
5
- import random
6
- import time
7
-
8
- import boto3
9
- import dsnparse # type: ignore[import]
10
- from invoke import run, task
11
-
12
- from .utils import kube
13
- from .utils.apps import get_app_details
14
- from .utils.cli import progress, success, warning
15
-
16
- # TODO: Abstract this code out of gitops.
17
-
18
-
19
- #####################
20
- # DATABASE COMMANDS #
21
- #####################
22
- @task
23
- def backup(ctx, app_name):
24
- """Backup production or staging database."""
25
- values = {"name": f"{app_name}-backup", "app": app_name}
26
- app = get_app_details(app_name, load_secrets=False)
27
- asyncio.run(
28
- kube._run_job(
29
- "jobs/backup-job.yml",
30
- values,
31
- context=app.cluster,
32
- namespace=app.namespace,
33
- sequential=True,
34
- )
35
- )
36
-
37
-
38
- @task
39
- def list_backups(ctx, app):
40
- kube.list_backups("workforce", app)
41
-
42
-
43
- @task
44
- def restore_backup(ctx, app_name, index, cleanup=True):
45
- """Restore backed up database."""
46
- kube.confirm_database(app_name)
47
- backups = kube.get_backups("workforce", app_name)
48
- backup = backups[int(index) - 1]
49
- values = {
50
- "name": f"{app_name}-restore",
51
- "timestamp": backup[0],
52
- "app": app_name,
53
- }
54
- app = get_app_details(app_name, load_secrets=False)
55
- asyncio.run(
56
- kube._run_job(
57
- "jobs/restore-job.yml",
58
- values,
59
- context=app.cluster,
60
- namespace=app.namespace,
61
- cleanup=cleanup,
62
- )
63
- )
64
-
65
-
66
- @task
67
- def copy_db(ctx, source, destination, skip_backup=False, cleanup=True):
68
- """Copy database between apps."""
69
- source_app = get_app_details(source, load_secrets=True)
70
- destination_app = get_app_details(destination, load_secrets=True)
71
- kube.confirm_database(destination)
72
- values = {
73
- "name": f"copy-db-{source}-{destination}",
74
- "source": source,
75
- "destination": destination,
76
- "SOURCE_DATABASE_URL_ENCODED": source_app.values["secrets"]["DATABASE_URL"],
77
- "DESTINATION_DATABASE_URL_ENCODED": destination_app.values["secrets"]["DATABASE_URL"],
78
- "skip_backup": "skip" if skip_backup else "",
79
- }
80
- asyncio.run(
81
- kube._run_job(
82
- "jobs/copy-db-job.yml",
83
- values,
84
- context=source_app.cluster,
85
- namespace=source_app.namespace,
86
- cleanup=cleanup,
87
- )
88
- )
89
- print(progress("You may want to clear the redis cache now!"))
90
- print(progress(f"\t- gitops mcommand {destination} clear_cache"))
91
-
92
- print(progress(":: Running migrations"))
93
- run(f"gitops migrate {destination}", echo=True)
94
-
95
-
96
- @task
97
- def download_backup(ctx, app, index=None, path=None, datestamp=False):
98
- """Download production or staging backup."""
99
- if not index:
100
- backups = kube.get_backups("workforce", app)
101
- if not backups:
102
- print(warning(f"No backups found for {app}"))
103
- return
104
- index = len(backups)
105
- print(progress("No index specified. Downloading latest backup: " + backups[-1][3]))
106
-
107
- kube.download_backup("workforce", app, index, path, datestamp=datestamp)
108
-
109
-
110
- @task
111
- def proxy(
112
- ctx,
113
- app_name,
114
- local_port=None,
115
- bastion_instance_id=None,
116
- aws_availability_zone=None,
117
- background=False,
118
- ):
119
- """Creates a proxy to RDS. Can supply either the app name or a DSN
120
-
121
- Usage: gitops db.proxy app_name
122
- or gitops db.proxy postgres://...:...@5432/db
123
- """
124
-
125
- try:
126
- database_url = app_name
127
- database_dsn = dsnparse.parse(database_url)
128
- app_name = database_dsn.user
129
- except ValueError:
130
- # app_name was not a valid dsn
131
- app = get_app_details(app_name, load_secrets=True)
132
- database_url = base64.b64decode(app.values["secrets"]["DATABASE_URL"].encode("ascii")).decode("ascii")
133
- database_dsn = dsnparse.parse(database_url)
134
-
135
- if not local_port:
136
- local_port = random.randint(1000, 9999) # noqa
137
-
138
- # Maybe we need to connect via RDS IAM instead!
139
- modified_dsn = dsnparse.parse(database_url)
140
- modified_dsn.hostname = "localhost"
141
- modified_dsn.port = local_port
142
- rds_iam = False
143
-
144
- if not database_dsn.password:
145
- result = run(
146
- f"aws rds generate-db-auth-token --hostname {database_dsn.host} --port"
147
- f" {database_dsn.port} --username {database_dsn.user}",
148
- hide=True,
149
- )
150
- modified_dsn.password = result.stdout.strip()
151
- rds_iam = True
152
-
153
- bastion_instance_id = bastion_instance_id or os.environ.get("GITOPS_BASTION_INSTANCE_ID")
154
- if not bastion_instance_id:
155
- raise Exception("Please set GITOPS_BASTION_INSTANCE_ID environment variable for db proxy to work.")
156
-
157
- aws_availability_zone = aws_availability_zone or os.environ.get("GITOPS_AWS_AVAILABILITY_ZONE")
158
- if not aws_availability_zone:
159
- raise Exception("Please set GITOPS_AWS_AVAILABILITY_ZONE environment variable for db proxy to work.")
160
-
161
- print(progress(f"Creating a proxy to the RDS instance of: {app_name} "))
162
-
163
- if rds_iam:
164
- proxy_dsn = (
165
- f"'user={modified_dsn.user} password={modified_dsn.password} host={modified_dsn.hostname} dbname={modified_dsn.dbname}"
166
- f" port={modified_dsn.port}'"
167
- )
168
- else:
169
- proxy_dsn = modified_dsn.geturl()
170
- print(
171
- progress(
172
- "Proxy established to :"
173
- f"postgres://{modified_dsn.user}@{modified_dsn.hostname}:{modified_dsn.port}/{modified_dsn.dbname}\n"
174
- )
175
- )
176
- print(progress(f"Connect to the db using: {proxy_dsn}\n"))
177
- cmd = f"""aws ssm start-session \
178
- --target {bastion_instance_id} \
179
- --document-name AWS-StartPortForwardingSessionToRemoteHost \
180
- --region {aws_availability_zone} \
181
- --parameters='{{"host": ["{database_dsn.host}"], "portNumber":["{database_dsn.port}"],"localPortNumber":["{local_port}"]}}'
182
- """
183
- return proxy_dsn, run(cmd, hide=True, asynchronous=background)
184
-
185
-
186
- @task
187
- def pgcli(
188
- ctx,
189
- app_name,
190
- ):
191
- """Opens pgcli to a remote DB"""
192
- print("making proxy")
193
- proxy_dsn, ctx = proxy(ctx, app_name, background=True)
194
- with ctx:
195
- print("Waiting for proxy to open")
196
- time.sleep(4)
197
- run(f"pgcli {proxy_dsn}", pty=True)
198
-
199
-
200
- @task
201
- def logs(ctx, app_name, last=24):
202
- """
203
- Fetches RDS logs since the last N hours.
204
-
205
- Usage:
206
- gitops db.logs APP --last 5
207
- """
208
- rds = boto3.client("rds")
209
- app = get_app_details(app_name, load_secrets=True)
210
- db_name = app.name.replace("_", "")
211
- logs = rds.describe_db_log_files(DBInstanceIdentifier=db_name)
212
- for log in logs["DescribeDBLogFiles"][-last:]:
213
- log_file = rds.download_db_log_file_portion(
214
- DBInstanceIdentifier=db_name,
215
- LogFileName=log["LogFileName"],
216
- )
217
- for line in log_file["LogFileData"].split("\n"):
218
- if line:
219
- print(
220
- str(datetime.datetime.fromisoformat(line[:19]).replace(tzinfo=datetime.timezone.utc).astimezone())
221
- + line[23:]
222
- )
223
-
224
-
225
- @task
226
- def wipe_db(ctx, destination, skip_backup=False, cleanup=True):
227
- """Wipes a customers database."""
228
- source_app = get_app_details(destination, load_secrets=True)
229
- kube.confirm_database(destination)
230
- values = {
231
- "name": f"wipe-db-{destination}",
232
- "destination": destination,
233
- "SOURCE_DATABASE_URL_ENCODED": source_app.values["secrets"]["DATABASE_URL"],
234
- "DESTINATION_DATABASE_URL_ENCODED": source_app.values["secrets"]["DATABASE_URL"],
235
- "skip_backup": "skip" if skip_backup else "",
236
- }
237
- print(progress(f":: Wiping database {destination}"))
238
- asyncio.run(
239
- kube._run_job(
240
- "jobs/wipe-db-job.yml",
241
- values,
242
- context=source_app.cluster,
243
- namespace=source_app.namespace,
244
- cleanup=cleanup,
245
- )
246
- )
247
- print(success("Deleted!"))
248
- print(progress(":: Running migrations"))
249
- run(f"gitops migrate {destination}", echo=True)
250
-
251
- print(progress(":: Clearing cache"))
252
- run(f"gitops mcommand {destination} clear_cache", echo=True)
253
-
254
- print(progress("Success!"))
@@ -1,70 +0,0 @@
1
- from functools import lru_cache
2
- from hashlib import md5
3
-
4
- import boto3
5
- from colorama import Fore
6
-
7
- from .cli import colourise
8
-
9
- BATCH_SIZE = 100
10
-
11
-
12
- def get_image(tag: str) -> str:
13
- """Finds a specific image in ECR."""
14
- # TODO
15
- raise NotImplementedError
16
-
17
-
18
- @lru_cache
19
- def get_latest_image(repository_name: str, prefix: str) -> str | None:
20
- """Finds latest image in ECR with the given prefix and returns the image tag"""
21
- ecr_client = boto3.client("ecr")
22
- client_paginator = ecr_client.get_paginator("describe_images")
23
-
24
- results = []
25
- for ecr_response in client_paginator.paginate(
26
- repositoryName=repository_name,
27
- filter={"tagStatus": "TAGGED"},
28
- maxResults=BATCH_SIZE,
29
- ):
30
- for image in ecr_response["imageDetails"]:
31
- if prefix != "":
32
- if prefix_tags := [tag for tag in image["imageTags"] if tag.startswith(prefix + "-")]:
33
- results.append((prefix_tags[0], image["imagePushedAt"]))
34
- else:
35
- if prefix_tags := [tag for tag in image["imageTags"] if "-" not in tag]:
36
- results.append((prefix_tags[0], image["imagePushedAt"]))
37
-
38
- if not results:
39
- if prefix:
40
- print(f'No images found in repository: {repository_name} with tag "{prefix}-*".')
41
- else:
42
- print(f"No images found in repository: {repository_name}")
43
- return None
44
-
45
- latest_image_tag = sorted(results, key=lambda image: image[1], reverse=True)[0][0]
46
- return latest_image_tag
47
-
48
-
49
- def colour_image(image_tag: str) -> str:
50
- if not image_tag:
51
- return image_tag
52
-
53
- bits = image_tag.split("-")
54
- if len(bits) > 1:
55
- bits[0] = colourise(bits[0], color_hash(bits[1]))
56
- return "-".join(bits)
57
- else:
58
- return colourise(bits[0], color_hash(bits[0]))
59
-
60
-
61
- def color_hash(bit: str) -> str:
62
- return [
63
- Fore.RED,
64
- Fore.GREEN,
65
- Fore.YELLOW,
66
- Fore.BLUE,
67
- Fore.MAGENTA,
68
- Fore.CYAN,
69
- Fore.WHITE,
70
- ][int.from_bytes(md5(bit.encode()).digest(), "big") % 7] # noqa: S324