gitops 0.11.5__tar.gz → 0.11.6__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.5 → gitops-0.11.6}/PKG-INFO +1 -1
  2. {gitops-0.11.5 → gitops-0.11.6}/charts/gitops/Chart.yaml +1 -1
  3. {gitops-0.11.5 → gitops-0.11.6}/gitops/__init__.py +5 -1
  4. {gitops-0.11.5 → gitops-0.11.6}/gitops/common/app.py +24 -5
  5. {gitops-0.11.5 → gitops-0.11.6}/gitops/utils/apps.py +8 -3
  6. gitops-0.11.6/gitops_server/types.py +40 -0
  7. {gitops-0.11.5 → gitops-0.11.6}/gitops_server/workers/deployer/deploy.py +1 -2
  8. {gitops-0.11.5 → gitops-0.11.6}/pyproject.toml +1 -1
  9. gitops-0.11.6/tests/conftest.py +3 -0
  10. {gitops-0.11.5 → gitops-0.11.6}/tests/test_app.py +15 -0
  11. {gitops-0.11.5 → gitops-0.11.6}/tests/test_deploy.py +15 -1
  12. {gitops-0.11.5 → gitops-0.11.6}/tests/utils.py +8 -2
  13. gitops-0.11.5/gitops_server/types.py +0 -34
  14. {gitops-0.11.5 → gitops-0.11.6}/.dockerignore +0 -0
  15. {gitops-0.11.5 → gitops-0.11.6}/.github/workflows/publish.yml +0 -0
  16. {gitops-0.11.5 → gitops-0.11.6}/.github/workflows/test.yml +0 -0
  17. {gitops-0.11.5 → gitops-0.11.6}/.gitignore +0 -0
  18. {gitops-0.11.5 → gitops-0.11.6}/.mise.toml +0 -0
  19. {gitops-0.11.5 → gitops-0.11.6}/.pre-commit-config.yaml +0 -0
  20. {gitops-0.11.5 → gitops-0.11.6}/Dockerfile +0 -0
  21. {gitops-0.11.5 → gitops-0.11.6}/LICENSE +0 -0
  22. {gitops-0.11.5 → gitops-0.11.6}/README.md +0 -0
  23. {gitops-0.11.5 → gitops-0.11.6}/charts/gitops/.helmignore +0 -0
  24. {gitops-0.11.5 → gitops-0.11.6}/charts/gitops/README.md +0 -0
  25. {gitops-0.11.5 → gitops-0.11.6}/charts/gitops/templates/_helpers.tpl +0 -0
  26. {gitops-0.11.5 → gitops-0.11.6}/charts/gitops/templates/certificate.yml +0 -0
  27. {gitops-0.11.5 → gitops-0.11.6}/charts/gitops/templates/deployment.yml +0 -0
  28. {gitops-0.11.5 → gitops-0.11.6}/charts/gitops/templates/environment.yml +0 -0
  29. {gitops-0.11.5 → gitops-0.11.6}/charts/gitops/templates/ingress.yml +0 -0
  30. {gitops-0.11.5 → gitops-0.11.6}/charts/gitops/templates/issuer.yml +0 -0
  31. {gitops-0.11.5 → gitops-0.11.6}/charts/gitops/templates/secrets.yml +0 -0
  32. {gitops-0.11.5 → gitops-0.11.6}/charts/gitops/templates/service.yml +0 -0
  33. {gitops-0.11.5 → gitops-0.11.6}/charts/gitops/templates/serviceaccount.yaml +0 -0
  34. {gitops-0.11.5 → gitops-0.11.6}/charts/gitops/values.yaml +0 -0
  35. {gitops-0.11.5 → gitops-0.11.6}/docker-compose.yml +0 -0
  36. {gitops-0.11.5 → gitops-0.11.6}/gitops/common/__init__.py +0 -0
  37. {gitops-0.11.5 → gitops-0.11.6}/gitops/common/utils.py +0 -0
  38. {gitops-0.11.5 → gitops-0.11.6}/gitops/core.py +0 -0
  39. {gitops-0.11.5 → gitops-0.11.6}/gitops/db.py +0 -0
  40. {gitops-0.11.5 → gitops-0.11.6}/gitops/main.py +0 -0
  41. {gitops-0.11.5 → gitops-0.11.6}/gitops/py.typed +0 -0
  42. {gitops-0.11.5 → gitops-0.11.6}/gitops/settings.py +0 -0
  43. {gitops-0.11.5 → gitops-0.11.6}/gitops/shorthands.py +0 -0
  44. {gitops-0.11.5 → gitops-0.11.6}/gitops/utils/__init__.py +0 -0
  45. {gitops-0.11.5 → gitops-0.11.6}/gitops/utils/async_runner.py +0 -0
  46. {gitops-0.11.5 → gitops-0.11.6}/gitops/utils/cli.py +0 -0
  47. {gitops-0.11.5 → gitops-0.11.6}/gitops/utils/exceptions.py +0 -0
  48. {gitops-0.11.5 → gitops-0.11.6}/gitops/utils/images.py +0 -0
  49. {gitops-0.11.5 → gitops-0.11.6}/gitops/utils/kube.py +0 -0
  50. {gitops-0.11.5 → gitops-0.11.6}/gitops/utils/tags.py +0 -0
  51. {gitops-0.11.5 → gitops-0.11.6}/gitops/utils/yaml.py +0 -0
  52. {gitops-0.11.5 → gitops-0.11.6}/gitops_server/README.md +0 -0
  53. {gitops-0.11.5 → gitops-0.11.6}/gitops_server/__init__.py +0 -0
  54. {gitops-0.11.5 → gitops-0.11.6}/gitops_server/app.py +0 -0
  55. {gitops-0.11.5 → gitops-0.11.6}/gitops_server/main.py +0 -0
  56. {gitops-0.11.5 → gitops-0.11.6}/gitops_server/settings.py +0 -0
  57. {gitops-0.11.5 → gitops-0.11.6}/gitops_server/utils/__init__.py +0 -0
  58. {gitops-0.11.5 → gitops-0.11.6}/gitops_server/utils/git.py +0 -0
  59. {gitops-0.11.5 → gitops-0.11.6}/gitops_server/utils/github.py +0 -0
  60. {gitops-0.11.5 → gitops-0.11.6}/gitops_server/utils/slack.py +0 -0
  61. {gitops-0.11.5 → gitops-0.11.6}/gitops_server/workers/__init__.py +0 -0
  62. {gitops-0.11.5 → gitops-0.11.6}/gitops_server/workers/deployer/__init__.py +0 -0
  63. {gitops-0.11.5 → gitops-0.11.6}/gitops_server/workers/deployer/hooks.py +0 -0
  64. {gitops-0.11.5 → gitops-0.11.6}/gitops_server/workers/deployer/worker.py +0 -0
  65. {gitops-0.11.5 → gitops-0.11.6}/gitops_server/workers/status_updater/__init__.py +0 -0
  66. {gitops-0.11.5 → gitops-0.11.6}/gitops_server/workers/status_updater/worker.py +0 -0
  67. {gitops-0.11.5 → gitops-0.11.6}/poetry.lock +0 -0
  68. {gitops-0.11.5 → gitops-0.11.6}/release.py +0 -0
  69. {gitops-0.11.5 → gitops-0.11.6}/ruff.toml +0 -0
  70. {gitops-0.11.5 → gitops-0.11.6}/secrets.example.env +0 -0
  71. {gitops-0.11.5 → gitops-0.11.6}/setup.cfg +0 -0
  72. {gitops-0.11.5 → gitops-0.11.6}/tests/__init__.py +0 -0
  73. {gitops-0.11.5 → gitops-0.11.6}/tests/sample_data.py +0 -0
  74. {gitops-0.11.5 → gitops-0.11.6}/tests/test_core.py +0 -0
  75. {gitops-0.11.5 → gitops-0.11.6}/tests/test_git.py +0 -0
  76. {gitops-0.11.5 → gitops-0.11.6}/tests/test_gitops_server.py +0 -0
  77. {gitops-0.11.5 → gitops-0.11.6}/tests/test_utils.py +0 -0
  78. {gitops-0.11.5 → gitops-0.11.6}/tests/webhook_sample_data.py +0 -0
  79. {gitops-0.11.5 → gitops-0.11.6}/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.11.6
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.11.6
@@ -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.11.6"
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:
@@ -127,6 +141,11 @@ class App:
127
141
  def service_account_name(self) -> str:
128
142
  return self.values.get("serviceAccount", {}).get("name") or self.values.get("serviceAccountName") or "default"
129
143
 
144
+ @property
145
+ def secrets(self) -> dict[str, str]:
146
+ # TODO: This should be a first class property
147
+ return self.values.get("secrets", {})
148
+
130
149
 
131
150
  class Chart:
132
151
  """Represents a Helm chart
@@ -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,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.11.6"
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
 
@@ -1,34 +0,0 @@
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 | None = None):
23
- self.name = name
24
- self.apps = apps or {}
25
-
26
- def from_path(self, path: str):
27
- path = os.path.join(path, "apps")
28
- for entry in os.listdir(path):
29
- entry_path = os.path.join(path, entry)
30
- if entry[0] != "." and not os.path.isfile(entry_path):
31
- app = App(entry, entry_path, account_id=settings.ACCOUNT_ID)
32
- # We only care for apps pertaining to our current cluster.
33
- if app.values["cluster"] == settings.CLUSTER_NAME:
34
- self.apps[entry] = app
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes