faas-gitops 0.2.1__py3-none-any.whl
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.
- faas_gitops/__init__.py +0 -0
- faas_gitops/adapters/__init__.py +0 -0
- faas_gitops/adapters/bizy_function_provider.py +31 -0
- faas_gitops/adapters/bizy_template_provider.py +31 -0
- faas_gitops/adapters/git_cli_gateway.py +107 -0
- faas_gitops/cli/__init__.py +0 -0
- faas_gitops/cli/app.py +112 -0
- faas_gitops/cli/parser.py +41 -0
- faas_gitops/cli/presenters.py +30 -0
- faas_gitops/config.py +30 -0
- faas_gitops/filtering.py +9 -0
- faas_gitops/infra/__init__.py +0 -0
- faas_gitops/infra/yaml_utils.py +17 -0
- faas_gitops/layout.py +12 -0
- faas_gitops/models.py +87 -0
- faas_gitops/protocols/__init__.py +9 -0
- faas_gitops/protocols/faas_function.py +53 -0
- faas_gitops/protocols/faas_template.py +70 -0
- faas_gitops/protocols/git_gateway.py +41 -0
- faas_gitops/usecases/__init__.py +0 -0
- faas_gitops/usecases/apply_function_changes.py +57 -0
- faas_gitops/usecases/apply_template_changes.py +49 -0
- faas_gitops/usecases/detect_drift.py +76 -0
- faas_gitops/usecases/detect_function_drift.py +45 -0
- faas_gitops/usecases/detect_template_drift.py +14 -0
- faas_gitops/usecases/render_definitions.py +96 -0
- faas_gitops/usecases/sync_functions.py +102 -0
- faas_gitops/usecases/sync_templates.py +106 -0
- faas_gitops-0.2.1.dist-info/METADATA +12 -0
- faas_gitops-0.2.1.dist-info/RECORD +33 -0
- faas_gitops-0.2.1.dist-info/WHEEL +5 -0
- faas_gitops-0.2.1.dist-info/entry_points.txt +2 -0
- faas_gitops-0.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class PushResult:
|
|
10
|
+
pushed: bool
|
|
11
|
+
url: str | None = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class GitWriter(Protocol):
|
|
16
|
+
|
|
17
|
+
async def clone_repo(self, repo_slug: str) -> Path:
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
async def prepare_branch(self, repo_path: Path, branch: str, force: bool = False) -> None:
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
async def commit_and_push(
|
|
24
|
+
self,
|
|
25
|
+
repo_path: Path,
|
|
26
|
+
prefix: str,
|
|
27
|
+
msg: str,
|
|
28
|
+
branch: str,
|
|
29
|
+
*,
|
|
30
|
+
force: bool = False,
|
|
31
|
+
create_pr: bool = False,
|
|
32
|
+
) -> PushResult:
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
async def current_branch(self, repo_path: Path) -> str:
|
|
36
|
+
"""Return the name of the current branch."""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
async def checkout(self, repo_path: Path, ref: str) -> None:
|
|
40
|
+
"""Switch to the given branch or ref."""
|
|
41
|
+
...
|
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
from faas_gitops.config import Settings
|
|
9
|
+
from faas_gitops.models import ApplyResult
|
|
10
|
+
from faas_gitops.protocols.faas_function import FaaSFunctionProvider
|
|
11
|
+
from faas_gitops.usecases.detect_function_drift import DetectFunctionDrift
|
|
12
|
+
from faas_gitops.usecases.sync_functions import read_function_from_dir
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ApplyFunctionChanges:
|
|
16
|
+
|
|
17
|
+
def __init__(self, function_provider: FaaSFunctionProvider, settings: Settings) -> None:
|
|
18
|
+
self._provider = function_provider
|
|
19
|
+
self._settings = settings
|
|
20
|
+
|
|
21
|
+
async def run(self, repo_path: Path, base: str = "main", dry_run: bool = False, sync_branch: str = "sync/faas-functions", repo_slug: str = "") -> ApplyResult:
|
|
22
|
+
result = ApplyResult()
|
|
23
|
+
drift = await DetectFunctionDrift(self._settings).run(repo_path, base, sync_branch)
|
|
24
|
+
if not drift.drifted:
|
|
25
|
+
return result
|
|
26
|
+
proc = await asyncio.create_subprocess_exec("git", "checkout", base, cwd=repo_path, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL)
|
|
27
|
+
await proc.communicate()
|
|
28
|
+
proc = await asyncio.create_subprocess_exec("git", "pull", cwd=repo_path, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL)
|
|
29
|
+
await proc.communicate()
|
|
30
|
+
proc = await asyncio.create_subprocess_exec(
|
|
31
|
+
"git", "rev-parse", "--short", f"origin/{base}",
|
|
32
|
+
cwd=repo_path,
|
|
33
|
+
stdout=asyncio.subprocess.PIPE,
|
|
34
|
+
stderr=asyncio.subprocess.PIPE,
|
|
35
|
+
)
|
|
36
|
+
stdout, _ = await proc.communicate()
|
|
37
|
+
short_hash = stdout.decode().strip()
|
|
38
|
+
comment = f"{repo_slug.rsplit('/', 1)[-1]}@{short_hash}" if short_hash else ""
|
|
39
|
+
for fid in drift.missing_in_git:
|
|
40
|
+
logger.warning(f" ! {fid}: not found on {base}, skipping")
|
|
41
|
+
for fid in drift.drifted:
|
|
42
|
+
func_dir = repo_path / "functions" / fid
|
|
43
|
+
if not func_dir.exists():
|
|
44
|
+
logger.warning(f" ! {fid}: not found on {base}, skipping")
|
|
45
|
+
result.skipped.append(fid)
|
|
46
|
+
continue
|
|
47
|
+
if dry_run:
|
|
48
|
+
result.skipped.append(fid)
|
|
49
|
+
continue
|
|
50
|
+
parts = await read_function_from_dir(func_dir)
|
|
51
|
+
if parts.definition is not None:
|
|
52
|
+
def_payload = parts.to_faas_definition(comment=comment)
|
|
53
|
+
if def_payload is not None:
|
|
54
|
+
await self._provider.push_definition(fid, def_payload)
|
|
55
|
+
await self._provider.update_function_meta(fid, parts.to_faas_meta())
|
|
56
|
+
result.applied.append(fid)
|
|
57
|
+
return result
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
from faas_gitops.models import ApplyResult
|
|
9
|
+
from faas_gitops.protocols.faas_template import FaaSTemplateProvider
|
|
10
|
+
from faas_gitops.usecases.detect_template_drift import DetectTemplateDrift
|
|
11
|
+
from faas_gitops.usecases.sync_templates import read_template_from_dir
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ApplyTemplateChanges:
|
|
15
|
+
|
|
16
|
+
def __init__(self, template_provider: FaaSTemplateProvider) -> None:
|
|
17
|
+
self._provider = template_provider
|
|
18
|
+
|
|
19
|
+
async def run(self, repo_path: Path, base: str = "main", dry_run: bool = False, sync_branch: str = "sync/faas-templates") -> ApplyResult:
|
|
20
|
+
result = ApplyResult()
|
|
21
|
+
drift = await DetectTemplateDrift().run(repo_path, base, sync_branch)
|
|
22
|
+
if not drift.drifted:
|
|
23
|
+
return result
|
|
24
|
+
proc = await asyncio.create_subprocess_exec("git", "checkout", base, cwd=repo_path, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL)
|
|
25
|
+
await proc.communicate()
|
|
26
|
+
proc = await asyncio.create_subprocess_exec("git", "pull", cwd=repo_path, stdout=asyncio.subprocess.DEVNULL, stderr=asyncio.subprocess.DEVNULL)
|
|
27
|
+
await proc.communicate()
|
|
28
|
+
for item_id in drift.missing_in_git:
|
|
29
|
+
logger.warning(f" ! {item_id}: not found on {base}, skipping")
|
|
30
|
+
for item_id in drift.drifted:
|
|
31
|
+
parts = item_id.split("/")
|
|
32
|
+
if len(parts) != 2:
|
|
33
|
+
continue
|
|
34
|
+
org_id, tpl_name = parts
|
|
35
|
+
tpl_dir = repo_path / "templates" / org_id / tpl_name
|
|
36
|
+
if not tpl_dir.exists():
|
|
37
|
+
logger.warning(f" ! {item_id}: not found on {base}, skipping")
|
|
38
|
+
result.skipped.append(item_id)
|
|
39
|
+
continue
|
|
40
|
+
git_yaml = (await read_template_from_dir(tpl_dir)).to_faas_yaml()
|
|
41
|
+
faas_yaml = await self._provider.get_template_content(org_id, tpl_name)
|
|
42
|
+
if git_yaml == faas_yaml:
|
|
43
|
+
result.unchanged.append(item_id)
|
|
44
|
+
elif dry_run:
|
|
45
|
+
result.skipped.append(item_id)
|
|
46
|
+
else:
|
|
47
|
+
await self._provider.update_template(org_id, tpl_name, git_yaml)
|
|
48
|
+
result.applied.append(item_id)
|
|
49
|
+
return result
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Shared git-diff based drift detection logic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from loguru import logger
|
|
5
|
+
import asyncio
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from faas_gitops.models import DriftResult
|
|
10
|
+
|
|
11
|
+
_DIFF_HEADER = re.compile(r"^diff --git a/.+ b/(.+)$")
|
|
12
|
+
_NEW_FILE = re.compile(r"^new file mode")
|
|
13
|
+
_DELETED_FILE = re.compile(r"^deleted file mode")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def run_git_diff(repo_path: Path, base: str, head: str, prefix: str) -> str:
|
|
17
|
+
cmd = ["git", "diff", f"origin/{head}..origin/{base}", "--", prefix]
|
|
18
|
+
logger.info("执行指令" + " ".join(cmd))
|
|
19
|
+
logger.info(f'TODo 实现打印在线预览')
|
|
20
|
+
proc = await asyncio.create_subprocess_exec(
|
|
21
|
+
*cmd,
|
|
22
|
+
cwd=repo_path,
|
|
23
|
+
stdout=asyncio.subprocess.PIPE,
|
|
24
|
+
stderr=asyncio.subprocess.PIPE,
|
|
25
|
+
)
|
|
26
|
+
stdout, _ = await proc.communicate()
|
|
27
|
+
return stdout.decode()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parse_git_diff(text: str, prefix: str, depth: int, *, exclude_suffixes: tuple[str, ...] = ()) -> DriftResult:
|
|
31
|
+
result = DriftResult(diff_text=text)
|
|
32
|
+
if not text:
|
|
33
|
+
return result
|
|
34
|
+
|
|
35
|
+
seen: dict[str, str] = {}
|
|
36
|
+
current_path: str | None = None
|
|
37
|
+
current_status: str | None = None
|
|
38
|
+
for line in text.splitlines():
|
|
39
|
+
m = _DIFF_HEADER.match(line)
|
|
40
|
+
if m:
|
|
41
|
+
if current_path and current_status:
|
|
42
|
+
seen[current_path] = current_status
|
|
43
|
+
current_path = m.group(1)
|
|
44
|
+
current_status = "modified"
|
|
45
|
+
continue
|
|
46
|
+
if current_path:
|
|
47
|
+
if _NEW_FILE.match(line):
|
|
48
|
+
current_status = "missing_in_faas"
|
|
49
|
+
elif _DELETED_FILE.match(line):
|
|
50
|
+
current_status = "missing_in_git"
|
|
51
|
+
if current_path and current_status:
|
|
52
|
+
seen[current_path] = current_status
|
|
53
|
+
|
|
54
|
+
items: dict[str, str] = {}
|
|
55
|
+
for path, status in seen.items():
|
|
56
|
+
if not path.startswith(prefix + "/"):
|
|
57
|
+
continue
|
|
58
|
+
if exclude_suffixes and any(path.endswith(s) for s in exclude_suffixes):
|
|
59
|
+
continue
|
|
60
|
+
rel = path[len(prefix) + 1:]
|
|
61
|
+
parts = rel.split("/")
|
|
62
|
+
if len(parts) <= depth:
|
|
63
|
+
continue
|
|
64
|
+
item_id = "/".join(parts[:depth])
|
|
65
|
+
if item_id in items:
|
|
66
|
+
continue
|
|
67
|
+
items[item_id] = status
|
|
68
|
+
|
|
69
|
+
for item_id, status in items.items():
|
|
70
|
+
if status == "missing_in_faas":
|
|
71
|
+
result.missing_in_faas.append(item_id)
|
|
72
|
+
elif status == "missing_in_git":
|
|
73
|
+
result.missing_in_git.append(item_id)
|
|
74
|
+
else:
|
|
75
|
+
result.drifted.append(item_id)
|
|
76
|
+
return result
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import aiofiles
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from faas_gitops.config import Settings
|
|
10
|
+
from faas_gitops.filtering import match_scope
|
|
11
|
+
from faas_gitops.layout import FUNCTION_DIR, FUNCTION_META
|
|
12
|
+
from faas_gitops.models import DriftResult
|
|
13
|
+
from faas_gitops.usecases.detect_drift import parse_git_diff, run_git_diff
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DetectFunctionDrift:
|
|
17
|
+
|
|
18
|
+
def __init__(self, settings: Settings) -> None:
|
|
19
|
+
self._settings = settings
|
|
20
|
+
|
|
21
|
+
async def run(self, repo_path: Path, base: str = "main", sync_branch: str = "sync/faas-functions") -> DriftResult:
|
|
22
|
+
text = await run_git_diff(repo_path, base, sync_branch, FUNCTION_DIR)
|
|
23
|
+
result = parse_git_diff(text, FUNCTION_DIR, depth=1, exclude_suffixes=())
|
|
24
|
+
if not any((result.drifted, result.missing_in_git, result.missing_in_faas)):
|
|
25
|
+
return result
|
|
26
|
+
await self._apply_scope(result, repo_path)
|
|
27
|
+
return result
|
|
28
|
+
|
|
29
|
+
async def _apply_scope(self, result: DriftResult, repo_path: Path) -> None:
|
|
30
|
+
scoped = self._settings.scoped_functions
|
|
31
|
+
tags = self._settings.scoped_function_tags
|
|
32
|
+
if scoped is None and tags is None:
|
|
33
|
+
return
|
|
34
|
+
result.drifted = [fid for fid in result.drifted if await self._in_scope(fid, repo_path)]
|
|
35
|
+
result.missing_in_git = [fid for fid in result.missing_in_git if await self._in_scope(fid, repo_path)]
|
|
36
|
+
result.missing_in_faas = [fid for fid in result.missing_in_faas if await self._in_scope(fid, repo_path)]
|
|
37
|
+
|
|
38
|
+
async def _in_scope(self, fid: str, repo_path: Path) -> bool:
|
|
39
|
+
meta_path = repo_path / FUNCTION_DIR / fid / FUNCTION_META
|
|
40
|
+
if not meta_path.exists():
|
|
41
|
+
return True
|
|
42
|
+
async with aiofiles.open(meta_path) as f:
|
|
43
|
+
meta = yaml.safe_load(await f.read()) or {}
|
|
44
|
+
func_tags = meta.get("tags") or []
|
|
45
|
+
return match_scope(fid, func_tags, self._settings.scoped_functions, self._settings.scoped_function_tags)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from faas_gitops.layout import TEMPLATE_DIR
|
|
6
|
+
from faas_gitops.models import DriftResult
|
|
7
|
+
from faas_gitops.usecases.detect_drift import parse_git_diff, run_git_diff
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DetectTemplateDrift:
|
|
11
|
+
|
|
12
|
+
async def run(self, repo_path: Path, base: str = "main", sync_branch: str = "sync/faas-templates") -> DriftResult:
|
|
13
|
+
text = await run_git_diff(repo_path, base, sync_branch, TEMPLATE_DIR)
|
|
14
|
+
return parse_git_diff(text, TEMPLATE_DIR, depth=2)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Render rendered.yaml for all functions in a local repo."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import aiofiles
|
|
8
|
+
import yaml
|
|
9
|
+
from loguru import logger
|
|
10
|
+
from tqdm.asyncio import tqdm_asyncio
|
|
11
|
+
|
|
12
|
+
from faas_gitops.infra.yaml_utils import dump_faas_yaml, load_yaml
|
|
13
|
+
from faas_gitops.layout import FUNCTION_BASE, FUNCTION_DEFINITION, FUNCTION_DIR, FUNCTION_RENDERED, TEMPLATE_DIR
|
|
14
|
+
from faas_gitops.protocols.faas_template import FaaSTemplateProvider
|
|
15
|
+
from faas_gitops.protocols.git_gateway import GitWriter
|
|
16
|
+
from faas_gitops.usecases.sync_functions import read_function_from_dir
|
|
17
|
+
from faas_gitops.usecases.sync_templates import _quiet_logger, read_template_from_dir
|
|
18
|
+
|
|
19
|
+
_RENDERED_HEADER = "# rendered by faas-gitops, do not modify\n"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _function_dirs(func_dir: Path) -> list[Path]:
|
|
23
|
+
if not func_dir.is_dir():
|
|
24
|
+
return []
|
|
25
|
+
return sorted(
|
|
26
|
+
d for d in func_dir.iterdir()
|
|
27
|
+
if d.is_dir() and (d / FUNCTION_DEFINITION).exists() and (d / FUNCTION_BASE).exists()
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _resolve_template_dir(repo_path: Path, base: dict) -> Path | None:
|
|
32
|
+
tpl_name: str = base.get("templateName", "")
|
|
33
|
+
if "/" not in tpl_name:
|
|
34
|
+
return None
|
|
35
|
+
org_id, tpl_id = tpl_name.split("/", 1)
|
|
36
|
+
return repo_path / TEMPLATE_DIR / org_id / tpl_id
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def _render_one(
|
|
40
|
+
func_dir: Path, repo_path: Path, provider: FaaSTemplateProvider, sem: asyncio.Semaphore,
|
|
41
|
+
) -> int:
|
|
42
|
+
async with sem:
|
|
43
|
+
try:
|
|
44
|
+
base = load_yaml((func_dir / FUNCTION_BASE).read_text())
|
|
45
|
+
tpl_dir = _resolve_template_dir(repo_path, base or {})
|
|
46
|
+
if tpl_dir is None:
|
|
47
|
+
return 0
|
|
48
|
+
if not tpl_dir.exists():
|
|
49
|
+
logger.warning(f"template not found: {tpl_dir}, skipping {func_dir.name}")
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
tpl_data = yaml.safe_load((await read_template_from_dir(tpl_dir)).to_faas_yaml())
|
|
53
|
+
parts = await read_function_from_dir(func_dir)
|
|
54
|
+
rendered = await parts.render_draft(provider, tpl_data)
|
|
55
|
+
|
|
56
|
+
content = _RENDERED_HEADER + dump_faas_yaml(rendered)
|
|
57
|
+
out = func_dir / FUNCTION_RENDERED
|
|
58
|
+
if out.exists() and out.read_text() == content:
|
|
59
|
+
return 0
|
|
60
|
+
async with aiofiles.open(out, "w") as f:
|
|
61
|
+
await f.write(content)
|
|
62
|
+
return 1
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.warning(f"render failed for {func_dir.name}: {e}")
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def render_definitions(
|
|
70
|
+
provider: FaaSTemplateProvider,
|
|
71
|
+
repo_path: Path,
|
|
72
|
+
*,
|
|
73
|
+
branch: str,
|
|
74
|
+
git: GitWriter,
|
|
75
|
+
max_workers: int = 16,
|
|
76
|
+
) -> int:
|
|
77
|
+
assert branch is not None
|
|
78
|
+
await git.prepare_branch(repo_path=repo_path, branch=branch, force=False)
|
|
79
|
+
logger.info(f"render-definitions: branch={branch}")
|
|
80
|
+
|
|
81
|
+
func_dir = repo_path / FUNCTION_DIR
|
|
82
|
+
dirs = _function_dirs(func_dir)
|
|
83
|
+
sem = asyncio.Semaphore(max_workers)
|
|
84
|
+
|
|
85
|
+
with _quiet_logger():
|
|
86
|
+
results = await tqdm_asyncio.gather(
|
|
87
|
+
*[_render_one(d, repo_path, provider, sem) for d in dirs],
|
|
88
|
+
desc=f"render-definitions [{branch}]",
|
|
89
|
+
unit="func",
|
|
90
|
+
)
|
|
91
|
+
written = sum(results)
|
|
92
|
+
await git.commit_and_push(
|
|
93
|
+
repo_path, FUNCTION_DIR, f"render-definitions[{written}]",
|
|
94
|
+
branch=branch, force=False, create_pr=False,
|
|
95
|
+
)
|
|
96
|
+
return written
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import yaml
|
|
6
|
+
from loguru import logger
|
|
7
|
+
|
|
8
|
+
import aiofiles
|
|
9
|
+
from tqdm.asyncio import tqdm_asyncio
|
|
10
|
+
|
|
11
|
+
from faas_gitops.config import Settings
|
|
12
|
+
from faas_gitops.usecases.sync_templates import _quiet_logger
|
|
13
|
+
from faas_gitops.filtering import match_scope
|
|
14
|
+
from faas_gitops.infra.yaml_utils import dump_faas_yaml, load_yaml
|
|
15
|
+
from faas_gitops.layout import FUNCTION_BASE, FUNCTION_DEFINITION, FUNCTION_DIR, FUNCTION_META, FUNCTION_RENDERED, TEMPLATE_DIR
|
|
16
|
+
from faas_gitops.models import FunctionParts
|
|
17
|
+
from faas_gitops.protocols.faas_function import FaaSFunctionProvider
|
|
18
|
+
from faas_gitops.protocols.faas_template import FaaSTemplateProvider
|
|
19
|
+
from faas_gitops.protocols.git_gateway import GitWriter
|
|
20
|
+
from faas_gitops.usecases.sync_templates import read_template_from_dir
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def _write_function(parts: FunctionParts, dest: Path) -> None:
|
|
24
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
async with aiofiles.open(dest / FUNCTION_META, "w") as f:
|
|
26
|
+
await f.write(dump_faas_yaml(parts.meta))
|
|
27
|
+
if parts.base is not None:
|
|
28
|
+
async with aiofiles.open(dest / FUNCTION_BASE, "w") as f:
|
|
29
|
+
await f.write(dump_faas_yaml(parts.base))
|
|
30
|
+
if parts.definition is not None:
|
|
31
|
+
async with aiofiles.open(dest / FUNCTION_DEFINITION, "w") as f:
|
|
32
|
+
await f.write(dump_faas_yaml(parts.definition))
|
|
33
|
+
|
|
34
|
+
async def read_function_from_dir(func_dir: Path) -> FunctionParts:
|
|
35
|
+
async with aiofiles.open(func_dir / FUNCTION_META) as f:
|
|
36
|
+
meta = load_yaml(await f.read()) or {}
|
|
37
|
+
base = None
|
|
38
|
+
base_path = func_dir / FUNCTION_BASE
|
|
39
|
+
if base_path.exists():
|
|
40
|
+
async with aiofiles.open(base_path) as f:
|
|
41
|
+
base = load_yaml(await f.read())
|
|
42
|
+
definition = None
|
|
43
|
+
def_path = func_dir / FUNCTION_DEFINITION
|
|
44
|
+
if def_path.exists():
|
|
45
|
+
async with aiofiles.open(def_path) as f:
|
|
46
|
+
definition = load_yaml(await f.read())
|
|
47
|
+
return FunctionParts(meta=meta, base=base, definition=definition)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SyncFunctions:
|
|
51
|
+
"""FaaS→Git function sync orchestration.
|
|
52
|
+
|
|
53
|
+
Fetches functions from FaaS, writes meta + runtime + definition YAML files,
|
|
54
|
+
force-pushes to a sync branch.
|
|
55
|
+
|
|
56
|
+
File layout per function:
|
|
57
|
+
functions/{func_id}/meta.yaml — function metadata (name, tags)
|
|
58
|
+
functions/{func_id}/base.yaml — runtime base (templateName)
|
|
59
|
+
functions/{func_id}/definition.yaml — spec.data
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
provider: FaaSFunctionProvider,
|
|
65
|
+
git_writer: GitWriter,
|
|
66
|
+
settings: Settings,
|
|
67
|
+
tpl_provider: FaaSTemplateProvider | None = None,
|
|
68
|
+
branch: str = "sync/faas-functions",
|
|
69
|
+
) -> None:
|
|
70
|
+
self._provider = provider
|
|
71
|
+
self._git_writer = git_writer
|
|
72
|
+
self._settings = settings
|
|
73
|
+
self._tpl_provider = tpl_provider
|
|
74
|
+
self._branch = branch
|
|
75
|
+
|
|
76
|
+
async def run(self, repo_path: Path):
|
|
77
|
+
functions = [
|
|
78
|
+
f for f in await self._provider.list_functions()
|
|
79
|
+
if match_scope(f["id"], f.get("tags", []), self._settings.scoped_functions, self._settings.scoped_function_tags)
|
|
80
|
+
]
|
|
81
|
+
await self._git_writer.prepare_branch(repo_path, self._branch, force=True)
|
|
82
|
+
out_dir = repo_path / FUNCTION_DIR
|
|
83
|
+
sem = asyncio.Semaphore(self._settings.max_workers)
|
|
84
|
+
|
|
85
|
+
async def _sync_one(func: dict) -> None:
|
|
86
|
+
async with sem:
|
|
87
|
+
fid = func["id"]
|
|
88
|
+
raw = await self._provider.get_latest_definition(fid)
|
|
89
|
+
meta = {"functionName": func.get("name", ""), "tags": func.get("tags", [])}
|
|
90
|
+
base = None
|
|
91
|
+
definition = None
|
|
92
|
+
if raw is not None:
|
|
93
|
+
base = {"templateName": raw.get("metadata", {}).get("templateName", "")}
|
|
94
|
+
definition = raw.get("spec", {}).get("data")
|
|
95
|
+
await _write_function(FunctionParts(meta=meta, base=base, definition=definition), out_dir / fid)
|
|
96
|
+
|
|
97
|
+
with _quiet_logger():
|
|
98
|
+
await tqdm_asyncio.gather(*[_sync_one(f) for f in functions], desc="sync-functions")
|
|
99
|
+
return await self._git_writer.commit_and_push(
|
|
100
|
+
repo_path, FUNCTION_DIR, f"sync: {len(functions)} functions", self._branch,
|
|
101
|
+
force=True, create_pr=True,
|
|
102
|
+
)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import aiofiles
|
|
8
|
+
import yaml
|
|
9
|
+
from loguru import logger
|
|
10
|
+
from tqdm.asyncio import tqdm_asyncio
|
|
11
|
+
|
|
12
|
+
from faas_gitops.config import Settings
|
|
13
|
+
from faas_gitops.filtering import match_scope
|
|
14
|
+
from faas_gitops.protocols.faas_template import FaaSTemplateProvider
|
|
15
|
+
from faas_gitops.infra.yaml_utils import dump_faas_yaml, load_yaml
|
|
16
|
+
from faas_gitops.layout import TEMPLATE_DIR, TEMPLATE_PRIMARY
|
|
17
|
+
from faas_gitops.models import TemplateParts, _INFO_FIELDS
|
|
18
|
+
from faas_gitops.protocols.git_gateway import GitWriter
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@contextmanager
|
|
22
|
+
def _quiet_logger():
|
|
23
|
+
logger.disable("bizy_deploy")
|
|
24
|
+
try:
|
|
25
|
+
yield
|
|
26
|
+
finally:
|
|
27
|
+
logger.enable("bizy_deploy")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _split_template(content: str) -> TemplateParts:
|
|
31
|
+
data = yaml.safe_load(content)
|
|
32
|
+
meta, spec = data.get("metadata", {}), data.get("spec", {})
|
|
33
|
+
org_id, tpl_id = meta["name"].split("/")
|
|
34
|
+
info = {"subject": org_id, "id": tpl_id} | {
|
|
35
|
+
dst: meta[src] for src, dst in _INFO_FIELDS.items() if meta.get(src)
|
|
36
|
+
}
|
|
37
|
+
if v := meta.get("parentTemplateName"):
|
|
38
|
+
info["parent"] = v
|
|
39
|
+
return TemplateParts(info=info, jinja=spec.get("jinjaTemplate"), schema=spec.get("schema"))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def read_template_from_dir(template_dir: Path) -> TemplateParts:
|
|
43
|
+
async with aiofiles.open(template_dir / TEMPLATE_PRIMARY) as f:
|
|
44
|
+
info = load_yaml(await f.read()) or {}
|
|
45
|
+
jinja = None
|
|
46
|
+
jinja_path = template_dir / "template.jinja"
|
|
47
|
+
if jinja_path.exists():
|
|
48
|
+
async with aiofiles.open(jinja_path) as f:
|
|
49
|
+
jinja = await f.read()
|
|
50
|
+
schema = None
|
|
51
|
+
schema_path = template_dir / "schema.yaml"
|
|
52
|
+
if schema_path.exists():
|
|
53
|
+
async with aiofiles.open(schema_path) as f:
|
|
54
|
+
schema = load_yaml(await f.read())
|
|
55
|
+
return TemplateParts(info=info, jinja=jinja, schema=schema)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def _write_files(parts: TemplateParts, dest: Path) -> None:
|
|
59
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
async with aiofiles.open(dest / TEMPLATE_PRIMARY, "w") as f:
|
|
61
|
+
await f.write(dump_faas_yaml(parts.info))
|
|
62
|
+
if parts.jinja:
|
|
63
|
+
async with aiofiles.open(dest / "template.jinja", "w") as f:
|
|
64
|
+
await f.write(parts.jinja)
|
|
65
|
+
if parts.schema:
|
|
66
|
+
async with aiofiles.open(dest / "schema.yaml", "w") as f:
|
|
67
|
+
await f.write(dump_faas_yaml(parts.schema))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class SyncTemplates:
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
provider: FaaSTemplateProvider,
|
|
75
|
+
git_writer: GitWriter,
|
|
76
|
+
settings: Settings,
|
|
77
|
+
branch: str = "sync/faas-templates",
|
|
78
|
+
) -> None:
|
|
79
|
+
self._provider = provider
|
|
80
|
+
self._git = git_writer
|
|
81
|
+
self._settings = settings
|
|
82
|
+
self._branch = branch
|
|
83
|
+
|
|
84
|
+
async def run(self, repo_path: Path):
|
|
85
|
+
templates = [
|
|
86
|
+
t for t in await self._provider.list_templates()
|
|
87
|
+
if match_scope(t["name"], t.get("tags", []), self._settings.scoped_templates, self._settings.scoped_template_tags)
|
|
88
|
+
]
|
|
89
|
+
await self._git.prepare_branch(repo_path, self._branch, force=True)
|
|
90
|
+
out_dir = repo_path / TEMPLATE_DIR
|
|
91
|
+
sem = asyncio.Semaphore(self._settings.max_workers)
|
|
92
|
+
|
|
93
|
+
async def _sync_one(t: dict) -> None:
|
|
94
|
+
async with sem:
|
|
95
|
+
org_id, tpl_id = t["name"].split("/")
|
|
96
|
+
content = await self._provider.get_template_content(org_id, tpl_id)
|
|
97
|
+
parts = _split_template(content)
|
|
98
|
+
dest = out_dir / parts.info["subject"] / parts.info["id"]
|
|
99
|
+
await _write_files(parts, dest)
|
|
100
|
+
|
|
101
|
+
with _quiet_logger():
|
|
102
|
+
await tqdm_asyncio.gather(*[_sync_one(t) for t in templates], desc="sync-templates")
|
|
103
|
+
return await self._git.commit_and_push(
|
|
104
|
+
repo_path, TEMPLATE_DIR, f"sync: {len(templates)} templates", self._branch,
|
|
105
|
+
force=True, create_pr=True,
|
|
106
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: faas-gitops
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Declarative GitOps for FaaS
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: aiofiles>=23.0
|
|
7
|
+
Requires-Dist: bizy-deploy>=0.5.4
|
|
8
|
+
Requires-Dist: pyyaml>=6.0
|
|
9
|
+
Requires-Dist: tqdm>=4.66.0
|
|
10
|
+
Provides-Extra: test
|
|
11
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
12
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "test"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
faas_gitops/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
faas_gitops/config.py,sha256=1cIzgALAfBQG7o9PTgLcTXICnnr6_rS1utCnOiAbQRs,948
|
|
3
|
+
faas_gitops/filtering.py,sha256=_fPG_vDVbA_mp8q-6Jx6Ovd4su350NstGrmbpwlXf4c,371
|
|
4
|
+
faas_gitops/layout.py,sha256=RRggSifRkiEVW8LXZMdQoVUxhu7N3LFVmGda4JMgxOY,383
|
|
5
|
+
faas_gitops/models.py,sha256=wp09GBqtIX3ineXgKzdXXBb5SlD7f7VGfWUGruFPZD4,3213
|
|
6
|
+
faas_gitops/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
faas_gitops/adapters/bizy_function_provider.py,sha256=C_cjPYYD-7GwnwLpo2dTwgPqLiOmph60GnKu3iH3TrA,1175
|
|
8
|
+
faas_gitops/adapters/bizy_template_provider.py,sha256=Si18FYUH08F0UQmgwFdrRQ0U6Bi0z9Mq1vx79taH6KI,1360
|
|
9
|
+
faas_gitops/adapters/git_cli_gateway.py,sha256=oIAsll39ox_EzoT_Zb8oVO37EFu4fNQrwA6KGeoOKlI,4255
|
|
10
|
+
faas_gitops/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
faas_gitops/cli/app.py,sha256=qZr7p-h1BOrFqR3mrdLMSeeA3gh8NAf-6m63sWoKmDs,4619
|
|
12
|
+
faas_gitops/cli/parser.py,sha256=jP7BB-ZdPuuPKgf8RGA7XZNwVI4l_ZdyEbhuGQNWp4c,1134
|
|
13
|
+
faas_gitops/cli/presenters.py,sha256=ky8l7fSHcR37uf74IiXTWt06fgtnpnfJ-zbUlRKWVLY,1156
|
|
14
|
+
faas_gitops/infra/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
faas_gitops/infra/yaml_utils.py,sha256=DzvjabEP2sKEUK4wv7XUzt0BowQroa1p-ZDeGsv5Iq8,442
|
|
16
|
+
faas_gitops/protocols/__init__.py,sha256=Jhmfp9QQ2K_JdKm-98hASR9vqo2SBe8ea42pj1wIhcU,219
|
|
17
|
+
faas_gitops/protocols/faas_function.py,sha256=Y8dMnML0UHDrOoX7Bv6_mSBc-GMHrxoTImjynYCYG9M,1468
|
|
18
|
+
faas_gitops/protocols/faas_template.py,sha256=OaMLRIgtYuBNMaFq0Sbas-MMbtt3p842YVHfzsSvBm8,2148
|
|
19
|
+
faas_gitops/protocols/git_gateway.py,sha256=ZJ2IbJd4KDWNNR8ayOKd8P5yRymW12CwULtj2YdnL7c,945
|
|
20
|
+
faas_gitops/usecases/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
+
faas_gitops/usecases/apply_function_changes.py,sha256=tj7nRrELwMfo9HxUhtAUGxnnS-PVqUp-gv9M_SO8rqA,2653
|
|
22
|
+
faas_gitops/usecases/apply_template_changes.py,sha256=D-TlvM0TkVs9Ua74vMOYPlNsyGBDNlJ1pnKIM3XQIDU,2234
|
|
23
|
+
faas_gitops/usecases/detect_drift.py,sha256=aeAtg2ovIa1KOI9egB3TOpf8tgXeSwF3y7R-KkXX7Kk,2513
|
|
24
|
+
faas_gitops/usecases/detect_function_drift.py,sha256=EhDxe9jMuCP1jk5_yYveECzK6sWRs9f3jVtFFaFqt7A,1969
|
|
25
|
+
faas_gitops/usecases/detect_template_drift.py,sha256=zT53j9MbU50RVFVDePbdP6UN8mI9YK1C2ewmqr_RLU8,510
|
|
26
|
+
faas_gitops/usecases/render_definitions.py,sha256=gELdUQY9KKAILhNEPNHGM303sCqT3FUOc7jc5X6HOP8,3333
|
|
27
|
+
faas_gitops/usecases/sync_functions.py,sha256=S-0Uqt2NlE0z1JyTTWMZkLvIr2RiNsFCo0BzvXNC_Mg,4268
|
|
28
|
+
faas_gitops/usecases/sync_templates.py,sha256=iF5rcPKr2sOxAk7ujL1HcIBmo_Rr-idLQoeOkhtRe7s,3857
|
|
29
|
+
faas_gitops-0.2.1.dist-info/METADATA,sha256=nTiS8m6oa2ajNQ95L8rMxIXsjLuyXnHo66ve_RZSOT4,353
|
|
30
|
+
faas_gitops-0.2.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
31
|
+
faas_gitops-0.2.1.dist-info/entry_points.txt,sha256=caS7fkXkKLHfS5QE4-_0C13hYKDusf22xpgZ9d1GAPM,57
|
|
32
|
+
faas_gitops-0.2.1.dist-info/top_level.txt,sha256=vFe6sgocS2gAPxbwBQtVR7Jm2ZaMMR-XCIDgAdVFVoU,12
|
|
33
|
+
faas_gitops-0.2.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
faas_gitops
|