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.
- {gitops-0.11.6 → gitops-0.12.0}/PKG-INFO +1 -1
- {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/Chart.yaml +1 -1
- {gitops-0.11.6 → gitops-0.12.0}/gitops/__init__.py +1 -1
- {gitops-0.11.6 → gitops-0.12.0}/gitops/common/app.py +5 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops/main.py +5 -4
- gitops-0.12.0/gitops/utils/images.py +113 -0
- {gitops-0.11.6 → gitops-0.12.0}/pyproject.toml +1 -1
- gitops-0.11.6/gitops/db.py +0 -254
- gitops-0.11.6/gitops/utils/images.py +0 -70
- gitops-0.11.6/poetry.lock +0 -1808
- {gitops-0.11.6 → gitops-0.12.0}/.dockerignore +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/.github/workflows/publish.yml +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/.github/workflows/test.yml +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/.gitignore +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/.mise.toml +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/.pre-commit-config.yaml +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/Dockerfile +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/LICENSE +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/README.md +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/.helmignore +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/README.md +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/templates/_helpers.tpl +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/templates/certificate.yml +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/templates/deployment.yml +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/templates/environment.yml +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/templates/ingress.yml +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/templates/issuer.yml +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/templates/secrets.yml +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/templates/service.yml +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/templates/serviceaccount.yaml +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/charts/gitops/values.yaml +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/docker-compose.yml +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops/common/__init__.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops/common/utils.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops/core.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops/py.typed +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops/settings.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops/shorthands.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops/utils/__init__.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops/utils/apps.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops/utils/async_runner.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops/utils/cli.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops/utils/exceptions.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops/utils/kube.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops/utils/tags.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops/utils/yaml.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops_server/README.md +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops_server/__init__.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops_server/app.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops_server/main.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops_server/settings.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops_server/types.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops_server/utils/__init__.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops_server/utils/git.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops_server/utils/github.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops_server/utils/slack.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops_server/workers/__init__.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops_server/workers/deployer/__init__.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops_server/workers/deployer/deploy.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops_server/workers/deployer/hooks.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops_server/workers/deployer/worker.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops_server/workers/status_updater/__init__.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/gitops_server/workers/status_updater/worker.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/release.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/ruff.toml +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/secrets.example.env +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/setup.cfg +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/tests/__init__.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/tests/conftest.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/tests/sample_data.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/tests/test_app.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/tests/test_core.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/tests/test_deploy.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/tests/test_git.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/tests/test_gitops_server.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/tests/test_utils.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/tests/utils.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/tests/webhook_sample_data.py +0 -0
- {gitops-0.11.6 → gitops-0.12.0}/uv.lock +0 -0
|
@@ -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,
|
|
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
|
gitops-0.11.6/gitops/db.py
DELETED
|
@@ -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
|