gitops 0.11.5__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 (80) hide show
  1. {gitops-0.11.5 → gitops-0.12.0}/PKG-INFO +1 -1
  2. {gitops-0.11.5 → gitops-0.12.0}/charts/gitops/Chart.yaml +1 -1
  3. {gitops-0.11.5 → gitops-0.12.0}/gitops/__init__.py +5 -1
  4. {gitops-0.11.5 → gitops-0.12.0}/gitops/common/app.py +29 -5
  5. {gitops-0.11.5 → gitops-0.12.0}/gitops/main.py +5 -4
  6. {gitops-0.11.5 → gitops-0.12.0}/gitops/utils/apps.py +8 -3
  7. gitops-0.12.0/gitops/utils/images.py +113 -0
  8. gitops-0.12.0/gitops_server/types.py +40 -0
  9. {gitops-0.11.5 → gitops-0.12.0}/gitops_server/workers/deployer/deploy.py +1 -2
  10. {gitops-0.11.5 → gitops-0.12.0}/pyproject.toml +1 -1
  11. gitops-0.12.0/tests/conftest.py +3 -0
  12. {gitops-0.11.5 → gitops-0.12.0}/tests/test_app.py +15 -0
  13. {gitops-0.11.5 → gitops-0.12.0}/tests/test_deploy.py +15 -1
  14. {gitops-0.11.5 → gitops-0.12.0}/tests/utils.py +8 -2
  15. gitops-0.11.5/gitops/db.py +0 -254
  16. gitops-0.11.5/gitops/utils/images.py +0 -70
  17. gitops-0.11.5/gitops_server/types.py +0 -34
  18. gitops-0.11.5/poetry.lock +0 -1808
  19. {gitops-0.11.5 → gitops-0.12.0}/.dockerignore +0 -0
  20. {gitops-0.11.5 → gitops-0.12.0}/.github/workflows/publish.yml +0 -0
  21. {gitops-0.11.5 → gitops-0.12.0}/.github/workflows/test.yml +0 -0
  22. {gitops-0.11.5 → gitops-0.12.0}/.gitignore +0 -0
  23. {gitops-0.11.5 → gitops-0.12.0}/.mise.toml +0 -0
  24. {gitops-0.11.5 → gitops-0.12.0}/.pre-commit-config.yaml +0 -0
  25. {gitops-0.11.5 → gitops-0.12.0}/Dockerfile +0 -0
  26. {gitops-0.11.5 → gitops-0.12.0}/LICENSE +0 -0
  27. {gitops-0.11.5 → gitops-0.12.0}/README.md +0 -0
  28. {gitops-0.11.5 → gitops-0.12.0}/charts/gitops/.helmignore +0 -0
  29. {gitops-0.11.5 → gitops-0.12.0}/charts/gitops/README.md +0 -0
  30. {gitops-0.11.5 → gitops-0.12.0}/charts/gitops/templates/_helpers.tpl +0 -0
  31. {gitops-0.11.5 → gitops-0.12.0}/charts/gitops/templates/certificate.yml +0 -0
  32. {gitops-0.11.5 → gitops-0.12.0}/charts/gitops/templates/deployment.yml +0 -0
  33. {gitops-0.11.5 → gitops-0.12.0}/charts/gitops/templates/environment.yml +0 -0
  34. {gitops-0.11.5 → gitops-0.12.0}/charts/gitops/templates/ingress.yml +0 -0
  35. {gitops-0.11.5 → gitops-0.12.0}/charts/gitops/templates/issuer.yml +0 -0
  36. {gitops-0.11.5 → gitops-0.12.0}/charts/gitops/templates/secrets.yml +0 -0
  37. {gitops-0.11.5 → gitops-0.12.0}/charts/gitops/templates/service.yml +0 -0
  38. {gitops-0.11.5 → gitops-0.12.0}/charts/gitops/templates/serviceaccount.yaml +0 -0
  39. {gitops-0.11.5 → gitops-0.12.0}/charts/gitops/values.yaml +0 -0
  40. {gitops-0.11.5 → gitops-0.12.0}/docker-compose.yml +0 -0
  41. {gitops-0.11.5 → gitops-0.12.0}/gitops/common/__init__.py +0 -0
  42. {gitops-0.11.5 → gitops-0.12.0}/gitops/common/utils.py +0 -0
  43. {gitops-0.11.5 → gitops-0.12.0}/gitops/core.py +0 -0
  44. {gitops-0.11.5 → gitops-0.12.0}/gitops/py.typed +0 -0
  45. {gitops-0.11.5 → gitops-0.12.0}/gitops/settings.py +0 -0
  46. {gitops-0.11.5 → gitops-0.12.0}/gitops/shorthands.py +0 -0
  47. {gitops-0.11.5 → gitops-0.12.0}/gitops/utils/__init__.py +0 -0
  48. {gitops-0.11.5 → gitops-0.12.0}/gitops/utils/async_runner.py +0 -0
  49. {gitops-0.11.5 → gitops-0.12.0}/gitops/utils/cli.py +0 -0
  50. {gitops-0.11.5 → gitops-0.12.0}/gitops/utils/exceptions.py +0 -0
  51. {gitops-0.11.5 → gitops-0.12.0}/gitops/utils/kube.py +0 -0
  52. {gitops-0.11.5 → gitops-0.12.0}/gitops/utils/tags.py +0 -0
  53. {gitops-0.11.5 → gitops-0.12.0}/gitops/utils/yaml.py +0 -0
  54. {gitops-0.11.5 → gitops-0.12.0}/gitops_server/README.md +0 -0
  55. {gitops-0.11.5 → gitops-0.12.0}/gitops_server/__init__.py +0 -0
  56. {gitops-0.11.5 → gitops-0.12.0}/gitops_server/app.py +0 -0
  57. {gitops-0.11.5 → gitops-0.12.0}/gitops_server/main.py +0 -0
  58. {gitops-0.11.5 → gitops-0.12.0}/gitops_server/settings.py +0 -0
  59. {gitops-0.11.5 → gitops-0.12.0}/gitops_server/utils/__init__.py +0 -0
  60. {gitops-0.11.5 → gitops-0.12.0}/gitops_server/utils/git.py +0 -0
  61. {gitops-0.11.5 → gitops-0.12.0}/gitops_server/utils/github.py +0 -0
  62. {gitops-0.11.5 → gitops-0.12.0}/gitops_server/utils/slack.py +0 -0
  63. {gitops-0.11.5 → gitops-0.12.0}/gitops_server/workers/__init__.py +0 -0
  64. {gitops-0.11.5 → gitops-0.12.0}/gitops_server/workers/deployer/__init__.py +0 -0
  65. {gitops-0.11.5 → gitops-0.12.0}/gitops_server/workers/deployer/hooks.py +0 -0
  66. {gitops-0.11.5 → gitops-0.12.0}/gitops_server/workers/deployer/worker.py +0 -0
  67. {gitops-0.11.5 → gitops-0.12.0}/gitops_server/workers/status_updater/__init__.py +0 -0
  68. {gitops-0.11.5 → gitops-0.12.0}/gitops_server/workers/status_updater/worker.py +0 -0
  69. {gitops-0.11.5 → gitops-0.12.0}/release.py +0 -0
  70. {gitops-0.11.5 → gitops-0.12.0}/ruff.toml +0 -0
  71. {gitops-0.11.5 → gitops-0.12.0}/secrets.example.env +0 -0
  72. {gitops-0.11.5 → gitops-0.12.0}/setup.cfg +0 -0
  73. {gitops-0.11.5 → gitops-0.12.0}/tests/__init__.py +0 -0
  74. {gitops-0.11.5 → gitops-0.12.0}/tests/sample_data.py +0 -0
  75. {gitops-0.11.5 → gitops-0.12.0}/tests/test_core.py +0 -0
  76. {gitops-0.11.5 → gitops-0.12.0}/tests/test_git.py +0 -0
  77. {gitops-0.11.5 → gitops-0.12.0}/tests/test_gitops_server.py +0 -0
  78. {gitops-0.11.5 → gitops-0.12.0}/tests/test_utils.py +0 -0
  79. {gitops-0.11.5 → gitops-0.12.0}/tests/webhook_sample_data.py +0 -0
  80. {gitops-0.11.5 → 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.5
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.5
5
+ version: 0.12.0
@@ -2,9 +2,11 @@ import os
2
2
  import sys
3
3
  from pathlib import Path
4
4
 
5
+ from gitops.utils.apps import App, get_app_details, get_apps
6
+
5
7
  from .utils.cli import success, warning
6
8
 
7
- __version__ = "0.11.5"
9
+ __version__ = "0.12.0"
8
10
 
9
11
 
10
12
  # Checking gitops version matches cluster repo version.
@@ -25,3 +27,5 @@ if versions_path.exists():
25
27
  f" requirement {success(min_gitops_version)}.",
26
28
  file=sys.stderr,
27
29
  )
30
+
31
+ __all__ = ["App", "get_app_details", "get_apps", "__version__"]
@@ -24,8 +24,19 @@ class App:
24
24
  deployments: dict | None = None,
25
25
  secrets: dict | None = None,
26
26
  load_secrets: bool = True,
27
+ encode_secrets: bool = True,
28
+ # deprecated
27
29
  account_id: str = "",
28
30
  ) -> None:
31
+ """
32
+ :params name: The name of the app
33
+ :params path: The path to the app directory
34
+ :params deployments: The deployments dictionary (injecting for testing)
35
+ :params secrets: The secrets dictionary (injecting for testing)
36
+ :params load_secrets: Whether to load the secrets file
37
+ :params encode_secrets: Whether to base64 encode the secrets
38
+ """
39
+ self.encode_secrets = encode_secrets
29
40
  self.name = name
30
41
  self.path = path
31
42
  self.account_id = account_id
@@ -47,6 +58,9 @@ class App:
47
58
  and json.dumps(self.values, sort_keys=True) == json.dumps(other.values, sort_keys=True)
48
59
  )
49
60
 
61
+ def __repr__(self) -> str:
62
+ return f"App(name={self.name}, cluster={self.cluster}, tags={self.tags})"
63
+
50
64
  def is_inactive(self) -> bool:
51
65
  return "inactive" in self.values.get("tags", [])
52
66
 
@@ -61,11 +75,11 @@ class App:
61
75
  current_dict = current_dict.setdefault(key, {})
62
76
  current_dict[keys[-1]] = value
63
77
 
64
- def _make_values(self, deployments: dict, secrets: dict) -> dict:
65
- values = {
66
- **deployments,
67
- "secrets": {**{k: b64encode(v.encode()).decode() for k, v in secrets.items()}},
68
- }
78
+ def _make_values(self, deployments: dict, secrets: dict[str, str]) -> dict:
79
+ def encode(value: str) -> str:
80
+ return b64encode(str(value).encode()).decode() if self.encode_secrets else value
81
+
82
+ values = {**deployments, "secrets": {**{k: encode(v) for k, v in secrets.items()}}}
69
83
 
70
84
  image = self._make_image(deployments)
71
85
  if image:
@@ -99,6 +113,11 @@ class App:
99
113
  """305686791668.dkr.ecr.ap-southeast-2.amazonaws.com/[uptick]:yoink-9f03ac80f3"""
100
114
  return self.image.split(":")[0].split("/")[-1]
101
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
+
102
121
  @property
103
122
  def image_tag(self) -> str:
104
123
  """305686791668.dkr.ecr.ap-southeast-2.amazonaws.com/uptick:[yoink-9f03ac80f3]"""
@@ -127,6 +146,11 @@ class App:
127
146
  def service_account_name(self) -> str:
128
147
  return self.values.get("serviceAccount", {}).get("name") or self.values.get("serviceAccountName") or "default"
129
148
 
149
+ @property
150
+ def secrets(self) -> dict[str, str]:
151
+ # TODO: This should be a first class property
152
+ return self.values.get("secrets", {})
153
+
130
154
 
131
155
  class Chart:
132
156
  """Represents a Helm chart
@@ -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)
@@ -1,5 +1,6 @@
1
1
  import sys
2
2
  from pathlib import Path
3
+ from typing import Literal
3
4
 
4
5
  from colorama import Fore
5
6
  from tabulate import tabulate
@@ -21,13 +22,16 @@ def is_valid_app_directory(directory: Path) -> bool:
21
22
  return all(file_paths)
22
23
 
23
24
 
24
- def get_app_details(app_name: str, load_secrets: bool = True, exit_if_not_found: bool = True) -> App:
25
+ def get_app_details(
26
+ app_name: str, load_secrets: bool = True, encode_secrets: bool = True, exit_if_not_found: bool = True
27
+ ) -> App:
25
28
  account_id = get_account_id() if load_secrets else "UNKNOWN"
26
29
  try:
27
30
  app = App(
28
31
  app_name,
29
32
  path=str(get_apps_directory() / app_name),
30
33
  load_secrets=load_secrets,
34
+ encode_secrets=encode_secrets,
31
35
  account_id=account_id,
32
36
  )
33
37
  except FileNotFoundError as e:
@@ -63,10 +67,11 @@ def update_app(app_name: str, **kwargs: object) -> None:
63
67
  def get_apps( # noqa: C901
64
68
  filter: set[str] | list[str] | str = "",
65
69
  exclude: set[str] | list[str] | str = "",
66
- mode: str = "PROMPT",
70
+ mode: Literal["PROMPT", "PREVIEW", "SILENT"] | None = "PROMPT",
67
71
  autoexclude_inactive: bool = True,
68
72
  message: str | None = None,
69
73
  load_secrets: bool = True,
74
+ encode_secrets: bool = True,
70
75
  ) -> list[App]:
71
76
  """Return apps that contain ALL of the tags listed in `filter` and NONE of the tags listed in
72
77
  `exclude`. The incoming filter and exclude params may come in as a list or commastring.
@@ -101,7 +106,7 @@ def get_apps( # noqa: C901
101
106
  continue
102
107
  elif not is_valid_app_directory(entry):
103
108
  continue
104
- app = get_app_details(entry.name, load_secrets=load_secrets)
109
+ app = get_app_details(entry.name, load_secrets=load_secrets, encode_secrets=encode_secrets)
105
110
 
106
111
  pseudotags = [app.name, app.cluster]
107
112
  if app.image and app.image_prefix:
@@ -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
@@ -0,0 +1,40 @@
1
+ import logging
2
+ import os
3
+ from typing import TypedDict
4
+
5
+ from gitops.common.app import App
6
+ from gitops_server import settings
7
+
8
+ logger = logging.getLogger("gitops")
9
+
10
+
11
+ class RunOutput(TypedDict):
12
+ exit_code: int
13
+ output: str
14
+
15
+
16
+ class UpdateAppResult(RunOutput):
17
+ app_name: str
18
+ slack_message: str
19
+
20
+
21
+ class AppDefinitions:
22
+ def __init__(self, name, apps: dict[str, App] | None = None, path: str | None = None):
23
+ self.name = name
24
+ self.apps = apps or {}
25
+
26
+ if path:
27
+ path = os.path.join(path, "apps")
28
+
29
+ for entry in os.listdir(path):
30
+ entry_path = os.path.join(path, entry)
31
+ if entry[0] != "." and not os.path.isfile(entry_path):
32
+ app = App(entry, entry_path, account_id=settings.ACCOUNT_ID)
33
+ self.apps[entry] = app
34
+
35
+ # Removing apps that are suspended or not part of this cluster
36
+ for app in list(self.apps.values()):
37
+ # We only care for apps pertaining to our current cluster.
38
+ if app.values["cluster"] != settings.CLUSTER_NAME or "suspended" in app.tags:
39
+ # and suspended apps are considered removed from the cluster.
40
+ self.apps.pop(app.name)
@@ -67,8 +67,7 @@ async def post_result_summary(source: str, results: list[UpdateAppResult]):
67
67
  async def load_app_definitions(url: str, sha: str) -> AppDefinitions:
68
68
  logger.info(f'Loading app definitions at "{sha}".')
69
69
  async with temp_repo(url, ref=sha) as repo:
70
- app_definitions = AppDefinitions(name=get_repo_name_from_url(url))
71
- app_definitions.from_path(repo)
70
+ app_definitions = AppDefinitions(name=get_repo_name_from_url(url), path=repo)
72
71
  return app_definitions
73
72
 
74
73
 
@@ -1,7 +1,7 @@
1
1
 
2
2
  [project]
3
3
  name = "gitops"
4
- version = "0.11.5"
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"
@@ -0,0 +1,3 @@
1
+ import gitops_server.settings
2
+
3
+ gitops_server.settings.CLUSTER_NAME = "test-cluster"
@@ -105,3 +105,18 @@ class TestChart:
105
105
 
106
106
  assert chart.type == "local"
107
107
  assert chart.path == "."
108
+
109
+ def test_app_namespace_property(self):
110
+ path = create_test_yaml()
111
+ app = App("test", path, encode_secrets=True)
112
+
113
+ assert app.namespace
114
+
115
+ def test_app_encode_secrets_works(self):
116
+ path = create_test_yaml()
117
+ app = App("test", path, encode_secrets=True)
118
+
119
+ assert app.secrets["SNAPE"] != "KILLS_DUMBLEDORE"
120
+
121
+ app_decoded = App("test", path, encode_secrets=False)
122
+ assert app_decoded.secrets["SNAPE"] == "KILLS_DUMBLEDORE"
@@ -4,10 +4,11 @@ from unittest.mock import patch
4
4
  import pytest
5
5
 
6
6
  from gitops.common.app import App
7
+ from gitops_server.types import AppDefinitions
7
8
  from gitops_server.workers.deployer import Deployer
8
9
 
9
10
  from .sample_data import SAMPLE_GITHUB_PAYLOAD, SAMPLE_GITHUB_PAYLOAD_SKIP_MIGRATIONS
10
- from .utils import mock_load_app_definitions
11
+ from .utils import create_test_yaml, mock_load_app_definitions
11
12
 
12
13
  # Patch gitops_server.git.run & check correct commands + order
13
14
  # Patch command that reads yaml from cluster repo +
@@ -120,3 +121,16 @@ class TestDeploy:
120
121
  ]
121
122
  for where, check in check_in_run_mock:
122
123
  assert check in post_mock.call_args_list[where][0][0]
124
+
125
+
126
+ class TestLoadAppDefinitions:
127
+ def test_load_app_definitions_ignores_suspended_apps(self):
128
+ app_definitions = AppDefinitions(
129
+ "mock-repo",
130
+ apps={
131
+ "in-cluster": App("in-cluster", path=create_test_yaml(tags=[])),
132
+ "suspended": App("suspended", path=create_test_yaml(tags=["suspended"])),
133
+ "not-in-cluster": App("not-in-cluster", path=create_test_yaml(tags=[], cluster="other-cluster")),
134
+ },
135
+ )
136
+ assert len(app_definitions.apps) == 1
@@ -1,3 +1,5 @@
1
+ from typing import Any
2
+
1
3
  import yaml
2
4
 
3
5
  from gitops.common.app import App
@@ -20,14 +22,14 @@ async def mock_load_app_definitions(url, sha):
20
22
  return app_definitions
21
23
 
22
24
 
23
- def create_test_yaml(fg=4, bg=2):
25
+ def create_test_yaml(fg=4, bg=2, **kwargs: Any):
24
26
  data = {
25
27
  "chart": "https://github.com/some/chart",
26
28
  "images": {"template": "template-tag-{tag}"},
27
29
  "namespace": "mynamespace",
28
30
  "tags": ["tag1", "tag2"],
29
31
  "image-tag": "myimagetag",
30
- "cluster": "UNKNOWN",
32
+ "cluster": "test-cluster",
31
33
  "containers": {"fg": {"replicas": fg}, "bg": {"replicas": bg}},
32
34
  "environment": {
33
35
  "DJANGO_SETTINGS_MODULE": "my.settings.module",
@@ -36,6 +38,10 @@ def create_test_yaml(fg=4, bg=2):
36
38
  "MEDIA_CLOUDINARY_PREFIX": "cloudinaryprefix",
37
39
  },
38
40
  }
41
+
42
+ for k, v in kwargs.items():
43
+ data[k] = v
44
+
39
45
  with open("/tmp/deployment.yml", "w+") as fh:
40
46
  fh.write(yaml.dump(data))
41
47