Skip to content

Commit 98884a5

Browse files
authored
refactor: change of a sync_apps.py logic (#190)
Extend sync-apps with custom config file in tenant application directories / Refactor sync-apps
1 parent f351bc4 commit 98884a5

File tree

9 files changed

+343
-169
lines changed

9 files changed

+343
-169
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ testrepo/
99
*.iml
1010
.eggs/
1111
site/
12+
build/
1213
.mypy_cache

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ FROM base AS dev
1010
WORKDIR /workdir
1111
RUN apk add --no-cache gcc linux-headers musl-dev make
1212
RUN python -m venv /opt/venv
13-
RUN pip install --upgrade pip
13+
RUN python -m pip install --upgrade pip
1414

1515
# =========
1616
FROM dev AS deps

docs/commands/sync-apps.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,22 @@ root-config-repo/
2828
└── bootstrap
2929
└── values.yaml
3030
```
31+
### app specific values
32+
app specific values may be set using a .config.yaml file directly in the app directory. gitopscli will process these values and add them under customAppConfig parameter of application
33+
**tenantrepo.git/app1/app_value_file.yaml**
34+
```yaml
35+
customvalue: test
36+
```
37+
**rootrepo.git/apps/tenantrepo.yaml**
38+
```yaml
39+
config:
40+
repository: https://tenantrepo.git
41+
applications:
42+
app1:
43+
customAppConfig:
44+
customvalue: test
45+
app2: {}
46+
```
3147
3248
**bootstrap/values.yaml**
3349
```yaml

gitopscli/appconfig_api/__init__.py

Whitespace-only changes.
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import logging
2+
from dataclasses import dataclass, field
3+
import os
4+
from typing import Any
5+
6+
from gitopscli.git_api import GitRepo
7+
from gitopscli.io_api.yaml_util import yaml_load, yaml_file_load
8+
9+
from gitopscli.gitops_exception import GitOpsException
10+
11+
12+
@dataclass
13+
class AppTenantConfig:
14+
yaml: dict[str, dict[str, Any]]
15+
tenant_config: dict[str, dict[str, Any]] = field(default_factory=dict)
16+
repo_url: str = ""
17+
file_path: str = ""
18+
dirty: bool = False
19+
20+
def __post_init__(self) -> None:
21+
if "config" in self.yaml:
22+
self.tenant_config = self.yaml["config"]
23+
else:
24+
self.tenant_config = self.yaml
25+
if "repository" not in self.tenant_config:
26+
raise GitOpsException("Cannot find key 'repository' in " + self.file_path)
27+
self.repo_url = str(self.tenant_config["repository"])
28+
29+
def list_apps(self) -> dict[str, dict[str, Any]]:
30+
return dict(self.tenant_config["applications"])
31+
32+
def merge_applications(self, desired_tenant_config: "AppTenantConfig") -> None:
33+
desired_apps = desired_tenant_config.list_apps()
34+
self.__delete_removed_applications(desired_apps)
35+
self.__add_new_applications(desired_apps)
36+
self.__update_custom_app_config(desired_apps)
37+
38+
def __update_custom_app_config(self, desired_apps: dict[str, dict[str, Any]]) -> None:
39+
for desired_app_name, desired_app_value in desired_apps.items():
40+
if desired_app_name in self.list_apps():
41+
existing_application_value = self.list_apps()[desired_app_name]
42+
if "customAppConfig" not in desired_app_value:
43+
if existing_application_value and "customAppConfig" in existing_application_value:
44+
logging.info(
45+
"Removing customAppConfig in for %s in %s applications",
46+
existing_application_value,
47+
self.file_path,
48+
)
49+
del existing_application_value["customAppConfig"]
50+
self.__set_dirty()
51+
else:
52+
if (
53+
"customAppConfig" not in existing_application_value
54+
or existing_application_value["customAppConfig"] != desired_app_value["customAppConfig"]
55+
):
56+
logging.info(
57+
"Updating customAppConfig in for %s in %s applications",
58+
existing_application_value,
59+
self.file_path,
60+
)
61+
existing_application_value["customAppConfig"] = desired_app_value["customAppConfig"]
62+
self.__set_dirty()
63+
64+
def __add_new_applications(self, desired_apps: dict[str, Any]) -> None:
65+
for desired_app_name, desired_app_value in desired_apps.items():
66+
if desired_app_name not in self.list_apps().keys():
67+
logging.info("Adding % in %s applications", desired_app_name, self.file_path)
68+
self.tenant_config["applications"][desired_app_name] = desired_app_value
69+
self.__set_dirty()
70+
71+
def __delete_removed_applications(self, desired_apps: dict[str, Any]) -> None:
72+
for current_app in self.list_apps().keys():
73+
if current_app not in desired_apps.keys():
74+
logging.info("Removing %s from %s applications", current_app, self.file_path)
75+
del self.tenant_config["applications"][current_app]
76+
self.__set_dirty()
77+
78+
def __set_dirty(self) -> None:
79+
self.dirty = True
80+
81+
82+
def __generate_config_from_tenant_repo(
83+
tenant_repo: GitRepo,
84+
) -> Any: # TODO: supposed to be ruamel object than Any pylint: disable=fixme
85+
tenant_app_dirs = __get_all_tenant_applications_dirs(tenant_repo)
86+
tenant_config_template = """
87+
config:
88+
repository: {}
89+
applications: {{}}
90+
""".format(
91+
tenant_repo.get_clone_url()
92+
)
93+
yaml = yaml_load(tenant_config_template)
94+
for app_dir in tenant_app_dirs:
95+
tenant_application_template = """
96+
{}: {{}}
97+
""".format(
98+
app_dir
99+
)
100+
tenant_applications_yaml = yaml_load(tenant_application_template)
101+
# dict path hardcoded as object generated will always be in v2 or later
102+
yaml["config"]["applications"].update(tenant_applications_yaml)
103+
custom_app_config = __get_custom_config(app_dir, tenant_repo)
104+
if custom_app_config:
105+
yaml["config"]["applications"][app_dir]["customAppConfig"] = custom_app_config
106+
return yaml
107+
108+
109+
def __get_all_tenant_applications_dirs(tenant_repo: GitRepo) -> set[str]:
110+
repo_dir = tenant_repo.get_full_file_path(".")
111+
applist = {
112+
name
113+
for name in os.listdir(repo_dir)
114+
if os.path.isdir(os.path.join(repo_dir, name)) and not name.startswith(".")
115+
}
116+
return applist
117+
118+
119+
def __get_custom_config(appname: str, tenant_config_git_repo: GitRepo) -> Any:
120+
custom_config_path = tenant_config_git_repo.get_full_file_path(f"{appname}/.config.yaml")
121+
if os.path.exists(custom_config_path):
122+
custom_config_content = yaml_file_load(custom_config_path)
123+
return custom_config_content
124+
return dict()
125+
126+
127+
def create_app_tenant_config_from_repo(
128+
tenant_repo: GitRepo,
129+
) -> "AppTenantConfig":
130+
tenant_repo.clone()
131+
tenant_config_yaml = __generate_config_from_tenant_repo(tenant_repo)
132+
return AppTenantConfig(yaml=tenant_config_yaml)

gitopscli/appconfig_api/root_repo.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from dataclasses import dataclass
2+
from typing import List, Any, Optional
3+
4+
from gitopscli.git_api import GitRepo
5+
from gitopscli.io_api.yaml_util import yaml_file_load
6+
7+
from gitopscli.appconfig_api.app_tenant_config import AppTenantConfig
8+
from gitopscli.gitops_exception import GitOpsException
9+
10+
11+
@dataclass
12+
class RootRepo:
13+
tenants: dict[str, AppTenantConfig]
14+
15+
def list_tenants(self) -> list[str]:
16+
return list(self.tenants.keys())
17+
18+
def get_tenant_by_repo_url(self, repo_url: str) -> Optional[AppTenantConfig]:
19+
for tenant in self.tenants.values():
20+
if tenant.repo_url == repo_url:
21+
return tenant
22+
return None
23+
24+
def get_all_applications(self) -> list[str]:
25+
apps: list[str] = list()
26+
for tenant in self.tenants.values():
27+
apps.extend(tenant.list_apps().keys())
28+
return apps
29+
30+
def validate_tenant(self, tenant_config: AppTenantConfig) -> None:
31+
apps_from_other_tenants: list[str] = list()
32+
for tenant in self.tenants.values():
33+
if tenant.repo_url != tenant_config.repo_url:
34+
apps_from_other_tenants.extend(tenant.list_apps().keys())
35+
for app_name in tenant_config.list_apps().keys():
36+
if app_name in apps_from_other_tenants:
37+
raise GitOpsException(f"Application '{app_name}' already exists in a different repository")
38+
39+
40+
def __load_tenants_from_bootstrap_values(root_repo: GitRepo) -> dict[str, AppTenantConfig]:
41+
boostrap_tenant_list = __get_bootstrap_tenant_list(root_repo)
42+
tenants = dict()
43+
for bootstrap_tenant in boostrap_tenant_list:
44+
try:
45+
tenant_name = bootstrap_tenant["name"]
46+
absolute_tenant_file_path = root_repo.get_full_file_path("apps/" + tenant_name + ".yaml")
47+
yaml = yaml_file_load(absolute_tenant_file_path)
48+
tenants[tenant_name] = AppTenantConfig(
49+
yaml=yaml,
50+
file_path=absolute_tenant_file_path,
51+
)
52+
except FileNotFoundError as ex:
53+
raise GitOpsException(f"File '{absolute_tenant_file_path}' not found in root repository.") from ex
54+
return tenants
55+
56+
57+
def __get_bootstrap_tenant_list(root_repo: GitRepo) -> List[Any]:
58+
root_repo.clone()
59+
try:
60+
boostrap_values_path = root_repo.get_full_file_path("bootstrap/values.yaml")
61+
bootstrap_yaml = yaml_file_load(boostrap_values_path)
62+
except FileNotFoundError as ex:
63+
raise GitOpsException("File 'bootstrap/values.yaml' not found in root repository.") from ex
64+
bootstrap_tenants = []
65+
if "bootstrap" in bootstrap_yaml:
66+
bootstrap_tenants = list(bootstrap_yaml["bootstrap"])
67+
if "config" in bootstrap_yaml and "bootstrap" in bootstrap_yaml["config"]:
68+
bootstrap_tenants = list(bootstrap_yaml["config"]["bootstrap"])
69+
__validate_bootstrap_tenants(bootstrap_tenants)
70+
return bootstrap_tenants
71+
72+
73+
def __validate_bootstrap_tenants(bootstrap_entries: Optional[List[Any]]) -> None:
74+
if not bootstrap_entries:
75+
raise GitOpsException("Cannot find key 'bootstrap' or 'config.bootstrap' in 'bootstrap/values.yaml'")
76+
for bootstrap_entry in bootstrap_entries:
77+
if "name" not in bootstrap_entry:
78+
raise GitOpsException("Every bootstrap entry must have a 'name' property.")
79+
80+
81+
def create_root_repo(root_repo: GitRepo) -> "RootRepo":
82+
root_repo_tenants = __load_tenants_from_bootstrap_values(root_repo)
83+
return RootRepo(root_repo_tenants)

0 commit comments

Comments
 (0)