faas-gitops 0.2.1__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.
- faas_gitops-0.2.1/MANIFEST.in +1 -0
- faas_gitops-0.2.1/PKG-INFO +12 -0
- faas_gitops-0.2.1/pyproject.toml +30 -0
- faas_gitops-0.2.1/setup.cfg +4 -0
- faas_gitops-0.2.1/src/faas_gitops/__init__.py +0 -0
- faas_gitops-0.2.1/src/faas_gitops/adapters/__init__.py +0 -0
- faas_gitops-0.2.1/src/faas_gitops/adapters/bizy_function_provider.py +31 -0
- faas_gitops-0.2.1/src/faas_gitops/adapters/bizy_template_provider.py +31 -0
- faas_gitops-0.2.1/src/faas_gitops/adapters/git_cli_gateway.py +107 -0
- faas_gitops-0.2.1/src/faas_gitops/cli/__init__.py +0 -0
- faas_gitops-0.2.1/src/faas_gitops/cli/app.py +112 -0
- faas_gitops-0.2.1/src/faas_gitops/cli/parser.py +41 -0
- faas_gitops-0.2.1/src/faas_gitops/cli/presenters.py +30 -0
- faas_gitops-0.2.1/src/faas_gitops/config.py +30 -0
- faas_gitops-0.2.1/src/faas_gitops/filtering.py +9 -0
- faas_gitops-0.2.1/src/faas_gitops/infra/__init__.py +0 -0
- faas_gitops-0.2.1/src/faas_gitops/infra/yaml_utils.py +17 -0
- faas_gitops-0.2.1/src/faas_gitops/layout.py +12 -0
- faas_gitops-0.2.1/src/faas_gitops/models.py +87 -0
- faas_gitops-0.2.1/src/faas_gitops/protocols/__init__.py +9 -0
- faas_gitops-0.2.1/src/faas_gitops/protocols/faas_function.py +53 -0
- faas_gitops-0.2.1/src/faas_gitops/protocols/faas_template.py +70 -0
- faas_gitops-0.2.1/src/faas_gitops/protocols/git_gateway.py +41 -0
- faas_gitops-0.2.1/src/faas_gitops/usecases/__init__.py +0 -0
- faas_gitops-0.2.1/src/faas_gitops/usecases/apply_function_changes.py +57 -0
- faas_gitops-0.2.1/src/faas_gitops/usecases/apply_template_changes.py +49 -0
- faas_gitops-0.2.1/src/faas_gitops/usecases/detect_drift.py +76 -0
- faas_gitops-0.2.1/src/faas_gitops/usecases/detect_function_drift.py +45 -0
- faas_gitops-0.2.1/src/faas_gitops/usecases/detect_template_drift.py +14 -0
- faas_gitops-0.2.1/src/faas_gitops/usecases/render_definitions.py +96 -0
- faas_gitops-0.2.1/src/faas_gitops/usecases/sync_functions.py +102 -0
- faas_gitops-0.2.1/src/faas_gitops/usecases/sync_templates.py +106 -0
- faas_gitops-0.2.1/src/faas_gitops.egg-info/PKG-INFO +12 -0
- faas_gitops-0.2.1/src/faas_gitops.egg-info/SOURCES.txt +44 -0
- faas_gitops-0.2.1/src/faas_gitops.egg-info/dependency_links.txt +1 -0
- faas_gitops-0.2.1/src/faas_gitops.egg-info/entry_points.txt +2 -0
- faas_gitops-0.2.1/src/faas_gitops.egg-info/requires.txt +8 -0
- faas_gitops-0.2.1/src/faas_gitops.egg-info/top_level.txt +1 -0
- faas_gitops-0.2.1/tests/test_apply_function_changes.py +171 -0
- faas_gitops-0.2.1/tests/test_apply_template_changes.py +158 -0
- faas_gitops-0.2.1/tests/test_detect_drift.py +154 -0
- faas_gitops-0.2.1/tests/test_detect_function_drift.py +53 -0
- faas_gitops-0.2.1/tests/test_git_cli_gateway.py +69 -0
- faas_gitops-0.2.1/tests/test_render_definitions.py +115 -0
- faas_gitops-0.2.1/tests/test_sync_functions.py +131 -0
- faas_gitops-0.2.1/tests/test_sync_templates.py +109 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
exclude README.md
|
|
@@ -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,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "faas-gitops"
|
|
7
|
+
version = "0.2.1"
|
|
8
|
+
description = "Declarative GitOps for FaaS"
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"aiofiles>=23.0",
|
|
12
|
+
"bizy-deploy>=0.5.4",
|
|
13
|
+
"pyyaml>=6.0",
|
|
14
|
+
"tqdm>=4.66.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
test = [
|
|
19
|
+
"pytest>=7.0",
|
|
20
|
+
"pytest-asyncio>=0.23",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
faas-gitops = "faas_gitops.cli.app:main"
|
|
25
|
+
|
|
26
|
+
[tool.pytest.ini_options]
|
|
27
|
+
asyncio_mode = "auto"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from bizy_deploy import AsyncFaaSActionProvider
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BizyFunctionProvider:
|
|
6
|
+
"""FaaS function operations via bizy-deploy SDK.
|
|
7
|
+
|
|
8
|
+
Implements FaaSFunctionProvider protocol.
|
|
9
|
+
Delegates to AsyncFaaSActionProvider from bizy_deploy.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, provider: AsyncFaaSActionProvider) -> None:
|
|
13
|
+
self._p = provider
|
|
14
|
+
|
|
15
|
+
async def list_functions(self) -> list[dict]:
|
|
16
|
+
result = await self._p.list_functions()
|
|
17
|
+
return [i.to_dict() for i in result.data.data if i is not None]
|
|
18
|
+
|
|
19
|
+
async def get_latest_definition(self, func_id: str) -> dict | None:
|
|
20
|
+
result = await self._p.get_latest_function_definition(function_id=func_id)
|
|
21
|
+
return result.to_dict() if result else None
|
|
22
|
+
|
|
23
|
+
async def push_definition(self, func_id: str, data: dict, comment: str = "") -> str:
|
|
24
|
+
return await self._p.create_function_definition(merged_def=data, function_id=func_id)
|
|
25
|
+
|
|
26
|
+
async def update_function_meta(self, func_id: str, meta: dict) -> None:
|
|
27
|
+
await self._p.update_function(
|
|
28
|
+
function_id=func_id,
|
|
29
|
+
name=meta.get("functionName"),
|
|
30
|
+
tags=meta.get("tags"),
|
|
31
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from bizy_deploy import AsyncFaaSActionProvider
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BizyTemplateProvider:
|
|
9
|
+
|
|
10
|
+
def __init__(self, provider: AsyncFaaSActionProvider) -> None:
|
|
11
|
+
self._p = provider
|
|
12
|
+
|
|
13
|
+
async def list_templates(self) -> list[dict]:
|
|
14
|
+
priv = await self._p.list_private_templates()
|
|
15
|
+
pub = await self._p.list_published_templates()
|
|
16
|
+
items = [i.to_dict() for i in (priv.data.data + pub.data.data) if i is not None]
|
|
17
|
+
return items
|
|
18
|
+
|
|
19
|
+
async def get_template(self, org_id: str, template_id: str, dest: Path) -> None:
|
|
20
|
+
await self._p.get_template(org_id=org_id, template_id=template_id, output=str(dest))
|
|
21
|
+
|
|
22
|
+
async def get_template_content(self, org_id: str, template_name: str) -> str:
|
|
23
|
+
path = await self._p.get_template(org_id=org_id, template_id=template_name)
|
|
24
|
+
return Path(path).read_text()
|
|
25
|
+
|
|
26
|
+
async def update_template(self, org_id: str, template_id: str, yaml_content: str) -> dict:
|
|
27
|
+
files = {"file": ("template.yaml", yaml_content, "application/x-yaml")}
|
|
28
|
+
return await self._p.update_template(name=template_id, files=files)
|
|
29
|
+
|
|
30
|
+
async def render_template_draft(self, *, file: str, use_cache: bool = False, output: str | None = None) -> dict:
|
|
31
|
+
return await self._p.render_template_draft_v2(file=file, use_cache=use_cache, output=output)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from loguru import logger
|
|
6
|
+
from faas_gitops.protocols.git_gateway import PushResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def _git(*args: str, cwd: Path | None = None, check: bool = True) -> str:
|
|
10
|
+
cmd = ["git", *args]
|
|
11
|
+
proc = await asyncio.create_subprocess_exec(
|
|
12
|
+
*cmd, cwd=cwd,
|
|
13
|
+
stdout=asyncio.subprocess.PIPE,
|
|
14
|
+
stderr=asyncio.subprocess.PIPE,
|
|
15
|
+
)
|
|
16
|
+
stdout, stderr = await proc.communicate()
|
|
17
|
+
if check and proc.returncode != 0:
|
|
18
|
+
raise RuntimeError(f"git {' '.join(args)} failed: {stderr.decode()}")
|
|
19
|
+
return stdout.decode().strip()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def _gh(*args: str, cwd: Path | None = None, check: bool = True) -> str:
|
|
23
|
+
cmd = ["gh", *args]
|
|
24
|
+
proc = await asyncio.create_subprocess_exec(
|
|
25
|
+
*cmd, cwd=cwd,
|
|
26
|
+
stdout=asyncio.subprocess.PIPE,
|
|
27
|
+
stderr=asyncio.subprocess.PIPE,
|
|
28
|
+
)
|
|
29
|
+
stdout, stderr = await proc.communicate()
|
|
30
|
+
if check and proc.returncode != 0:
|
|
31
|
+
raise RuntimeError(f"gh {' '.join(args)} failed: {stderr.decode()}")
|
|
32
|
+
return stdout.decode().strip()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class GitCliGateway:
|
|
36
|
+
|
|
37
|
+
def __init__(self, clone_root: Path) -> None:
|
|
38
|
+
self._root = clone_root
|
|
39
|
+
self._root.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
logger.info(f"GitCliGateway init: clone_root={self._root}")
|
|
41
|
+
|
|
42
|
+
async def clone_repo(self, repo_slug: str) -> Path:
|
|
43
|
+
dest = self._root / repo_slug.split("/")[-1]
|
|
44
|
+
if dest.exists():
|
|
45
|
+
logger.info(f"pull existing repo: {dest.name}")
|
|
46
|
+
await _git("pull", cwd=dest, check=False)
|
|
47
|
+
else:
|
|
48
|
+
logger.info(f"gh repo clone {repo_slug}")
|
|
49
|
+
await _gh("repo", "clone", repo_slug, str(dest))
|
|
50
|
+
return dest
|
|
51
|
+
|
|
52
|
+
async def prepare_branch(self, repo_path: Path, branch: str, force: bool = False) -> None:
|
|
53
|
+
await _git("fetch", "origin", cwd=repo_path)
|
|
54
|
+
remote_exists = await _git("rev-parse", "--verify", f"origin/{branch}", cwd=repo_path, check=False)
|
|
55
|
+
if force or not remote_exists:
|
|
56
|
+
await _git("checkout", "main", cwd=repo_path)
|
|
57
|
+
await _git("pull", cwd=repo_path, check=False)
|
|
58
|
+
if force:
|
|
59
|
+
await _git("branch", "-D", branch, cwd=repo_path, check=False)
|
|
60
|
+
await _git("checkout", "-B", branch, cwd=repo_path)
|
|
61
|
+
else:
|
|
62
|
+
await _git("checkout", branch, cwd=repo_path, check=False) or await _git("checkout", "-B", branch, cwd=repo_path)
|
|
63
|
+
await _git("reset", "--hard", f"origin/{branch}", cwd=repo_path)
|
|
64
|
+
|
|
65
|
+
async def current_branch(self, repo_path: Path) -> str:
|
|
66
|
+
return await _git("rev-parse", "--abbrev-ref", "HEAD", cwd=repo_path)
|
|
67
|
+
|
|
68
|
+
async def checkout(self, repo_path: Path, ref: str) -> None:
|
|
69
|
+
await _git("checkout", ref, cwd=repo_path)
|
|
70
|
+
|
|
71
|
+
async def commit_and_push(
|
|
72
|
+
self,
|
|
73
|
+
repo_path: Path,
|
|
74
|
+
prefix: str,
|
|
75
|
+
msg: str,
|
|
76
|
+
branch: str,
|
|
77
|
+
*,
|
|
78
|
+
force: bool = False,
|
|
79
|
+
create_pr: bool = False,
|
|
80
|
+
) -> PushResult:
|
|
81
|
+
await _git("add", prefix, cwd=repo_path)
|
|
82
|
+
proc = await asyncio.create_subprocess_exec(
|
|
83
|
+
"git", "diff", "--cached", "--quiet",
|
|
84
|
+
cwd=repo_path,
|
|
85
|
+
stdout=asyncio.subprocess.PIPE,
|
|
86
|
+
stderr=asyncio.subprocess.PIPE,
|
|
87
|
+
)
|
|
88
|
+
await proc.communicate()
|
|
89
|
+
if proc.returncode == 0:
|
|
90
|
+
return PushResult(pushed=False)
|
|
91
|
+
await _git("commit", "-m", msg, cwd=repo_path)
|
|
92
|
+
if not force:
|
|
93
|
+
await _git("pull", "--rebase", "origin", branch, cwd=repo_path, check=False)
|
|
94
|
+
await _git("push", "origin", branch, *(["--force"] if force else []), cwd=repo_path)
|
|
95
|
+
if create_pr:
|
|
96
|
+
proc = await asyncio.create_subprocess_exec(
|
|
97
|
+
"gh", "pr", "create", "--fill", "--base", "main", "--head", branch,
|
|
98
|
+
cwd=repo_path,
|
|
99
|
+
stdout=asyncio.subprocess.PIPE,
|
|
100
|
+
stderr=asyncio.subprocess.PIPE,
|
|
101
|
+
)
|
|
102
|
+
stdout, stderr = await proc.communicate()
|
|
103
|
+
out = stdout.decode().strip()
|
|
104
|
+
err = stderr.decode().strip()
|
|
105
|
+
url = out or next((l for l in err.splitlines() if l.startswith("http")), None)
|
|
106
|
+
return PushResult(pushed=True, url=url)
|
|
107
|
+
return PushResult(pushed=True)
|
|
File without changes
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import tempfile
|
|
5
|
+
from contextlib import AsyncExitStack
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from bizy_deploy import AsyncFaaSActionProvider
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
from faas_gitops.adapters.bizy_function_provider import BizyFunctionProvider
|
|
12
|
+
from faas_gitops.adapters.bizy_template_provider import BizyTemplateProvider
|
|
13
|
+
from faas_gitops.adapters.git_cli_gateway import GitCliGateway
|
|
14
|
+
from faas_gitops.cli.parser import build_parser
|
|
15
|
+
from faas_gitops.cli.presenters import print_apply_report, print_drift_report
|
|
16
|
+
from faas_gitops.config import load_settings
|
|
17
|
+
from faas_gitops.models import ApplyResult
|
|
18
|
+
from faas_gitops.usecases.apply_function_changes import ApplyFunctionChanges
|
|
19
|
+
from faas_gitops.usecases.apply_template_changes import ApplyTemplateChanges
|
|
20
|
+
from faas_gitops.usecases.detect_function_drift import DetectFunctionDrift
|
|
21
|
+
from faas_gitops.usecases.detect_template_drift import DetectTemplateDrift
|
|
22
|
+
from faas_gitops.usecases.render_definitions import render_definitions
|
|
23
|
+
from faas_gitops.usecases.sync_functions import SyncFunctions
|
|
24
|
+
from faas_gitops.usecases.sync_templates import SyncTemplates
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _make_git() -> GitCliGateway:
|
|
28
|
+
return GitCliGateway(Path(tempfile.mkdtemp(prefix="faas-gitops-")))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def _make_faas(stack: AsyncExitStack) -> AsyncFaaSActionProvider:
|
|
32
|
+
faas = AsyncFaaSActionProvider()
|
|
33
|
+
stack.push_async_callback(faas.client.close)
|
|
34
|
+
return faas
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def async_main() -> int:
|
|
38
|
+
settings = load_settings()
|
|
39
|
+
parser = build_parser(settings)
|
|
40
|
+
args = parser.parse_args()
|
|
41
|
+
if not args.command:
|
|
42
|
+
parser.print_help()
|
|
43
|
+
return 1
|
|
44
|
+
|
|
45
|
+
repo_slug: str = args.repo
|
|
46
|
+
|
|
47
|
+
async with AsyncExitStack() as stack:
|
|
48
|
+
if args.command == "sync-templates":
|
|
49
|
+
faas = await _make_faas(stack)
|
|
50
|
+
provider = BizyTemplateProvider(faas)
|
|
51
|
+
git = _make_git()
|
|
52
|
+
push = await SyncTemplates(provider, git, settings).run(await git.clone_repo(repo_slug))
|
|
53
|
+
logger.info(push.url if push.pushed and push.url else ("branch pushed" if push.pushed else "no changes"))
|
|
54
|
+
return 0
|
|
55
|
+
|
|
56
|
+
if args.command == "sync-functions":
|
|
57
|
+
faas = await _make_faas(stack)
|
|
58
|
+
provider = BizyFunctionProvider(faas)
|
|
59
|
+
tpl_provider = BizyTemplateProvider(faas)
|
|
60
|
+
git = _make_git()
|
|
61
|
+
repo_path = await git.clone_repo(repo_slug)
|
|
62
|
+
push = await SyncFunctions(provider, git, settings, tpl_provider=tpl_provider, branch=args.branch).run(repo_path)
|
|
63
|
+
logger.info(push.url if push.pushed and push.url else ("branch pushed" if push.pushed else "no changes"))
|
|
64
|
+
return 0
|
|
65
|
+
|
|
66
|
+
if args.command == "render-definitions":
|
|
67
|
+
faas = await _make_faas(stack)
|
|
68
|
+
tpl_provider = BizyTemplateProvider(faas)
|
|
69
|
+
git = _make_git()
|
|
70
|
+
repo_path = await git.clone_repo(repo_slug)
|
|
71
|
+
n = await render_definitions(tpl_provider, repo_path, branch=args.branch, git=git, max_workers=settings.max_workers)
|
|
72
|
+
logger.info(f"render-definitions: {n} file(s) written")
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
if args.command == "detect-template-changes":
|
|
76
|
+
git = _make_git()
|
|
77
|
+
repo_path = await git.clone_repo(repo_slug)
|
|
78
|
+
return print_drift_report(await DetectTemplateDrift().run(repo_path))
|
|
79
|
+
|
|
80
|
+
if args.command == "detect-function-changes":
|
|
81
|
+
git = _make_git()
|
|
82
|
+
repo_path = await git.clone_repo(repo_slug)
|
|
83
|
+
return print_drift_report(await DetectFunctionDrift(settings).run(repo_path))
|
|
84
|
+
|
|
85
|
+
if args.command == "apply-template-changes":
|
|
86
|
+
faas = await _make_faas(stack)
|
|
87
|
+
git = _make_git()
|
|
88
|
+
repo_path = await git.clone_repo(repo_slug)
|
|
89
|
+
result = await ApplyTemplateChanges(BizyTemplateProvider(faas)).run(
|
|
90
|
+
repo_path, dry_run=getattr(args, "dry_run", False),
|
|
91
|
+
)
|
|
92
|
+
return print_apply_report(result, getattr(args, "dry_run", False))
|
|
93
|
+
|
|
94
|
+
if args.command == "apply-function-changes":
|
|
95
|
+
faas = await _make_faas(stack)
|
|
96
|
+
git = _make_git()
|
|
97
|
+
repo_path = await git.clone_repo(repo_slug)
|
|
98
|
+
result = await ApplyFunctionChanges(BizyFunctionProvider(faas), settings).run(
|
|
99
|
+
repo_path, dry_run=getattr(args, "dry_run", False), repo_slug=repo_slug,
|
|
100
|
+
)
|
|
101
|
+
return print_apply_report(result, getattr(args, "dry_run", False))
|
|
102
|
+
|
|
103
|
+
logger.warning(f"unknown command: {args.command}")
|
|
104
|
+
return 1
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def main() -> int:
|
|
108
|
+
return asyncio.run(async_main())
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from faas_gitops.config import Settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_parser(settings: Settings) -> argparse.ArgumentParser:
|
|
9
|
+
parser = argparse.ArgumentParser(prog="faas-gitops")
|
|
10
|
+
sub = parser.add_subparsers(dest="command")
|
|
11
|
+
|
|
12
|
+
def _add_repo(p: argparse.ArgumentParser) -> None:
|
|
13
|
+
p.add_argument("--repo", default=settings.default_repo)
|
|
14
|
+
|
|
15
|
+
p = sub.add_parser("sync-templates")
|
|
16
|
+
p.add_argument("--branch", default="sync/faas-templates")
|
|
17
|
+
_add_repo(p)
|
|
18
|
+
|
|
19
|
+
p = sub.add_parser("sync-functions")
|
|
20
|
+
p.add_argument("--branch", default="sync/faas-functions")
|
|
21
|
+
_add_repo(p)
|
|
22
|
+
|
|
23
|
+
p = sub.add_parser("render-definitions")
|
|
24
|
+
p.add_argument("--branch", default=None)
|
|
25
|
+
_add_repo(p)
|
|
26
|
+
|
|
27
|
+
p = sub.add_parser("detect-template-changes")
|
|
28
|
+
_add_repo(p)
|
|
29
|
+
|
|
30
|
+
p = sub.add_parser("detect-function-changes")
|
|
31
|
+
_add_repo(p)
|
|
32
|
+
|
|
33
|
+
p = sub.add_parser("apply-template-changes")
|
|
34
|
+
p.add_argument("--dry-run", action="store_true")
|
|
35
|
+
_add_repo(p)
|
|
36
|
+
|
|
37
|
+
p = sub.add_parser("apply-function-changes")
|
|
38
|
+
p.add_argument("--dry-run", action="store_true")
|
|
39
|
+
_add_repo(p)
|
|
40
|
+
|
|
41
|
+
return parser
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from loguru import logger
|
|
4
|
+
|
|
5
|
+
from faas_gitops.models import ApplyResult, DriftResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def print_drift_report(result: DriftResult) -> int:
|
|
9
|
+
has_drift = bool(result.drifted or result.missing_in_git or result.missing_in_faas)
|
|
10
|
+
logger.info(f"drifted={len(result.drifted)} missing_git={len(result.missing_in_git)} missing_faas={len(result.missing_in_faas)}")
|
|
11
|
+
for n in result.drifted:
|
|
12
|
+
logger.info(f" Δ {n}")
|
|
13
|
+
for n in result.missing_in_git:
|
|
14
|
+
logger.info(f" + {n}")
|
|
15
|
+
for n in result.missing_in_faas:
|
|
16
|
+
logger.info(f" - {n}")
|
|
17
|
+
if result.diff_text:
|
|
18
|
+
logger.info(result.diff_text)
|
|
19
|
+
return 1 if has_drift else 0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def print_apply_report(result: ApplyResult, dry_run: bool = False) -> int:
|
|
23
|
+
logger.info(f"applied={len(result.applied)} unchanged={len(result.unchanged)} skipped={len(result.skipped)}{' (dry-run)' if dry_run else ''}")
|
|
24
|
+
for n in result.applied:
|
|
25
|
+
logger.info(f" ✓ {n}")
|
|
26
|
+
for n in result.skipped:
|
|
27
|
+
logger.info(f" - {n}")
|
|
28
|
+
for name, err in result.failed:
|
|
29
|
+
logger.warning(f" ! {name}: {err}")
|
|
30
|
+
return 1 if result.failed else 0
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class Settings:
|
|
9
|
+
default_repo: str = "siliconflow/faas-gitops-test"
|
|
10
|
+
scoped_templates: set[str] | None = None
|
|
11
|
+
scoped_template_tags: set[str] | None = None
|
|
12
|
+
scoped_functions: set[str] | None = None
|
|
13
|
+
scoped_function_tags: set[str] | None = None
|
|
14
|
+
max_workers: int = 8
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _csv_set(key: str) -> set[str] | None:
|
|
18
|
+
val = os.getenv(key)
|
|
19
|
+
return {s for s in val.split(",") if s} if val else None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_settings() -> Settings:
|
|
23
|
+
return Settings(
|
|
24
|
+
default_repo=os.getenv("DEFAULT_REPO", Settings.default_repo),
|
|
25
|
+
scoped_templates=_csv_set("SCOPED_TEMPLATES"),
|
|
26
|
+
scoped_template_tags=_csv_set("SCOPED_TEMPLATE_TAGS"),
|
|
27
|
+
scoped_functions=_csv_set("SCOPED_FUNCTIONS"),
|
|
28
|
+
scoped_function_tags=_csv_set("SCOPED_FUNCTION_TAGS"),
|
|
29
|
+
max_workers=int(os.getenv("MAX_WORKERS", "128")),
|
|
30
|
+
)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def match_scope(item_id: str, tags: list[str], id_set: set[str] | None, tag_set: set[str] | None) -> bool:
|
|
5
|
+
if id_set is None and tag_set is None:
|
|
6
|
+
return True
|
|
7
|
+
by_id = id_set is not None and item_id in id_set
|
|
8
|
+
by_tag = tag_set is not None and tags is not None and bool(set(tags) & tag_set)
|
|
9
|
+
return by_id or by_tag
|
|
File without changes
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import yaml
|
|
4
|
+
from bizy_deploy.core.utils import standardize_faas_yaml
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def load_yaml(text: str | None) -> dict | None:
|
|
8
|
+
if not text:
|
|
9
|
+
return None
|
|
10
|
+
return yaml.safe_load(text)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def dump_faas_yaml(data: dict | str) -> str:
|
|
14
|
+
if isinstance(data, dict):
|
|
15
|
+
data = yaml.dump(data, sort_keys=False, allow_unicode=True)
|
|
16
|
+
assert isinstance(data, str)
|
|
17
|
+
return standardize_faas_yaml(data)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""File layout conventions."""
|
|
2
|
+
|
|
3
|
+
TEMPLATE_DIR = "templates"
|
|
4
|
+
TEMPLATE_FILES = ("info.yaml", "template.jinja", "schema.yaml")
|
|
5
|
+
TEMPLATE_PRIMARY = "info.yaml"
|
|
6
|
+
|
|
7
|
+
FUNCTION_DIR = "functions"
|
|
8
|
+
FUNCTION_META = "meta.yaml"
|
|
9
|
+
FUNCTION_BASE = "base.yaml"
|
|
10
|
+
FUNCTION_DEFINITION = "definition.yaml"
|
|
11
|
+
FUNCTION_FILES = (FUNCTION_META, FUNCTION_BASE, FUNCTION_DEFINITION)
|
|
12
|
+
FUNCTION_RENDERED = "rendered.yaml"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Dict
|
|
5
|
+
import tempfile
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from faas_gitops.infra.yaml_utils import dump_faas_yaml
|
|
10
|
+
from faas_gitops.protocols.faas_template import FaaSTemplateProvider
|
|
11
|
+
|
|
12
|
+
_INFO_FIELDS = {"displayName": "name", "description": "description", "icon": "icon", "tags": "tags"}
|
|
13
|
+
_INFO_FIELDS_INV = {dst: src for src, dst in _INFO_FIELDS.items()}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class DriftResult:
|
|
18
|
+
"""Container for drift detection results."""
|
|
19
|
+
|
|
20
|
+
drifted: list[str] = field(default_factory=list)
|
|
21
|
+
missing_in_git: list[str] = field(default_factory=list)
|
|
22
|
+
missing_in_faas: list[str] = field(default_factory=list)
|
|
23
|
+
diff_text: str = ""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True)
|
|
27
|
+
class TemplateParts:
|
|
28
|
+
"""Decomposed template: info dict + optional jinja/schema."""
|
|
29
|
+
|
|
30
|
+
info: dict
|
|
31
|
+
jinja: str | None = None
|
|
32
|
+
schema: dict | None = None
|
|
33
|
+
|
|
34
|
+
def to_faas_yaml(self) -> str:
|
|
35
|
+
"""Reassemble into SiliconFunctionTemplate YAML for FaaS API."""
|
|
36
|
+
metadata: dict = {"name": f"{self.info['subject']}/{self.info['id']}"}
|
|
37
|
+
for dst, src in _INFO_FIELDS_INV.items():
|
|
38
|
+
if dst in self.info:
|
|
39
|
+
metadata[src] = self.info[dst]
|
|
40
|
+
if "parent" in self.info:
|
|
41
|
+
metadata["parentTemplateName"] = self.info["parent"]
|
|
42
|
+
spec: dict = {}
|
|
43
|
+
if self.jinja is not None:
|
|
44
|
+
spec["jinjaTemplate"] = self.jinja
|
|
45
|
+
if self.schema is not None:
|
|
46
|
+
spec["schema"] = self.schema
|
|
47
|
+
return dump_faas_yaml({
|
|
48
|
+
"apiVersion": "function-template.6scloud.com/v1alpha1",
|
|
49
|
+
"kind": "SiliconFunctionTemplate",
|
|
50
|
+
"metadata": metadata,
|
|
51
|
+
"spec": spec,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True, slots=True)
|
|
56
|
+
class FunctionParts:
|
|
57
|
+
"""Decomposed function: meta + optional base/definition."""
|
|
58
|
+
|
|
59
|
+
meta: dict
|
|
60
|
+
base: dict | None = None
|
|
61
|
+
definition: dict | None = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def to_faas_meta(self) -> dict:
|
|
65
|
+
return {"functionName": self.meta.get("functionName", ""), "tags": self.meta.get("tags", [])}
|
|
66
|
+
|
|
67
|
+
def to_faas_definition(self, comment: str = None) -> dict | None:
|
|
68
|
+
if self.definition is None:
|
|
69
|
+
return
|
|
70
|
+
assert comment is not None
|
|
71
|
+
return {"metadata": {"templateName": self.base["templateName"]}, "spec": {"data": self.definition, "comment": comment}, "kind": "SiliconFunctionDefinition", "apiVersion": "function-form-value.6scloud.com/v1alpha1"}
|
|
72
|
+
|
|
73
|
+
async def render_draft(self, provider: FaaSTemplateProvider, tpl_data: Dict) -> dict:
|
|
74
|
+
tpl_data.setdefault("spec", {})["examples"] = [self.definition]
|
|
75
|
+
with tempfile.NamedTemporaryFile(mode='w+', suffix='.yaml', delete=False) as tmp_file:
|
|
76
|
+
yaml.dump(tpl_data, tmp_file, default_flow_style=False, allow_unicode=True)
|
|
77
|
+
out = await provider.render_template_draft(file=tmp_file.name)
|
|
78
|
+
return out.to_dict()
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class ApplyResult:
|
|
82
|
+
"""Container for apply operation results."""
|
|
83
|
+
|
|
84
|
+
applied: list[str] = field(default_factory=list)
|
|
85
|
+
unchanged: list[str] = field(default_factory=list)
|
|
86
|
+
skipped: list[str] = field(default_factory=list)
|
|
87
|
+
failed: list[tuple[str, str]] = field(default_factory=list)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Protocol, runtime_checkable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@runtime_checkable
|
|
7
|
+
class FaaSFunctionProvider(Protocol):
|
|
8
|
+
"""Interface for FaaS function CRUD operations.
|
|
9
|
+
|
|
10
|
+
Implementations wrap the bizy-deploy SDK or any other
|
|
11
|
+
FaaS-compatible backend. Used by sync (read) and apply
|
|
12
|
+
(read + write) use cases.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
async def list_functions(self) -> list[dict]:
|
|
16
|
+
"""List all accessible functions.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
List of function dicts with at least 'id', 'name', and 'tags' keys.
|
|
20
|
+
"""
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
async def get_latest_definition(self, func_id: str) -> dict | None:
|
|
24
|
+
"""Get the latest function definition dict.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
func_id: Function identifier.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Definition dict, or None if no definitions exist.
|
|
31
|
+
"""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
async def push_definition(self, func_id: str, data: dict, comment: str = "") -> str:
|
|
35
|
+
"""Create a new function definition version.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
func_id: Function identifier.
|
|
39
|
+
data: Definition payload dict.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Version string of the newly created definition.
|
|
43
|
+
"""
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
async def update_function_meta(self, func_id: str, meta: dict) -> None:
|
|
47
|
+
"""Update function metadata (name, tags).
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
func_id: Function identifier.
|
|
51
|
+
meta: Dict with 'functionName' and/or 'tags' keys.
|
|
52
|
+
"""
|
|
53
|
+
...
|