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