gitlab-cicd-python-wrapper 0.1.0__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.
- gitlab_cicd_python_wrapper/__init__.py +99 -0
- gitlab_cicd_python_wrapper/_async.py +50 -0
- gitlab_cicd_python_wrapper/artifacts.py +40 -0
- gitlab_cicd_python_wrapper/cache.py +25 -0
- gitlab_cicd_python_wrapper/cli.py +69 -0
- gitlab_cicd_python_wrapper/common.py +87 -0
- gitlab_cicd_python_wrapper/component.py +148 -0
- gitlab_cicd_python_wrapper/environment.py +22 -0
- gitlab_cicd_python_wrapper/globals.py +44 -0
- gitlab_cicd_python_wrapper/image.py +22 -0
- gitlab_cicd_python_wrapper/include.py +75 -0
- gitlab_cicd_python_wrapper/job.py +96 -0
- gitlab_cicd_python_wrapper/needs.py +19 -0
- gitlab_cicd_python_wrapper/pages.py +10 -0
- gitlab_cicd_python_wrapper/pipeline.py +102 -0
- gitlab_cicd_python_wrapper/py.typed +0 -0
- gitlab_cicd_python_wrapper/release.py +14 -0
- gitlab_cicd_python_wrapper/retry.py +12 -0
- gitlab_cicd_python_wrapper/rules.py +28 -0
- gitlab_cicd_python_wrapper/secrets.py +24 -0
- gitlab_cicd_python_wrapper/serialization.py +64 -0
- gitlab_cicd_python_wrapper/spec.py +55 -0
- gitlab_cicd_python_wrapper/trigger.py +23 -0
- gitlab_cicd_python_wrapper/variables.py +12 -0
- gitlab_cicd_python_wrapper-0.1.0.dist-info/METADATA +262 -0
- gitlab_cicd_python_wrapper-0.1.0.dist-info/RECORD +28 -0
- gitlab_cicd_python_wrapper-0.1.0.dist-info/WHEEL +4 -0
- gitlab_cicd_python_wrapper-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Gitlab CICD Python Wrapper - Pydantic models for GitLab CI/CD YAML."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from gitlab_cicd_python_wrapper._async import AsyncComponent, AsyncPipeline
|
|
6
|
+
from gitlab_cicd_python_wrapper.artifacts import ArtifactReports, Artifacts
|
|
7
|
+
from gitlab_cicd_python_wrapper.cache import Cache, CacheKey
|
|
8
|
+
from gitlab_cicd_python_wrapper.common import (
|
|
9
|
+
ArtifactAccess,
|
|
10
|
+
ArtifactWhen,
|
|
11
|
+
AutoCancelOnJobFailure,
|
|
12
|
+
AutoCancelOnNewCommit,
|
|
13
|
+
CachePolicy,
|
|
14
|
+
CacheWhen,
|
|
15
|
+
DeploymentTier,
|
|
16
|
+
EnvironmentAction,
|
|
17
|
+
InputType,
|
|
18
|
+
RetryWhen,
|
|
19
|
+
WhenCondition,
|
|
20
|
+
)
|
|
21
|
+
from gitlab_cicd_python_wrapper.component import Component
|
|
22
|
+
from gitlab_cicd_python_wrapper.environment import Environment
|
|
23
|
+
from gitlab_cicd_python_wrapper.globals import AutoCancel, Default, Workflow
|
|
24
|
+
from gitlab_cicd_python_wrapper.image import Image, Service
|
|
25
|
+
from gitlab_cicd_python_wrapper.include import (
|
|
26
|
+
ComponentReference,
|
|
27
|
+
IncludeComponent,
|
|
28
|
+
IncludeItem,
|
|
29
|
+
IncludeLocal,
|
|
30
|
+
IncludeProject,
|
|
31
|
+
IncludeRemote,
|
|
32
|
+
IncludeTemplate,
|
|
33
|
+
)
|
|
34
|
+
from gitlab_cicd_python_wrapper.job import AllowFailure, Inherit, Job, Parallel
|
|
35
|
+
from gitlab_cicd_python_wrapper.needs import Need, NeedsPipeline
|
|
36
|
+
from gitlab_cicd_python_wrapper.pages import Pages
|
|
37
|
+
from gitlab_cicd_python_wrapper.pipeline import Pipeline
|
|
38
|
+
from gitlab_cicd_python_wrapper.release import Release
|
|
39
|
+
from gitlab_cicd_python_wrapper.retry import Retry
|
|
40
|
+
from gitlab_cicd_python_wrapper.rules import Rule, WorkflowRule
|
|
41
|
+
from gitlab_cicd_python_wrapper.secrets import Secret, VaultConfig, VaultEngine
|
|
42
|
+
from gitlab_cicd_python_wrapper.spec import ComponentInput, ComponentSpec, InputRule
|
|
43
|
+
from gitlab_cicd_python_wrapper.trigger import Trigger, TriggerForward
|
|
44
|
+
from gitlab_cicd_python_wrapper.variables import Variable
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"AllowFailure",
|
|
48
|
+
"ArtifactAccess",
|
|
49
|
+
"ArtifactReports",
|
|
50
|
+
"ArtifactWhen",
|
|
51
|
+
"Artifacts",
|
|
52
|
+
"AsyncComponent",
|
|
53
|
+
"AsyncPipeline",
|
|
54
|
+
"AutoCancel",
|
|
55
|
+
"AutoCancelOnJobFailure",
|
|
56
|
+
"AutoCancelOnNewCommit",
|
|
57
|
+
"Cache",
|
|
58
|
+
"CacheKey",
|
|
59
|
+
"CachePolicy",
|
|
60
|
+
"CacheWhen",
|
|
61
|
+
"Component",
|
|
62
|
+
"ComponentInput",
|
|
63
|
+
"ComponentReference",
|
|
64
|
+
"ComponentSpec",
|
|
65
|
+
"Default",
|
|
66
|
+
"DeploymentTier",
|
|
67
|
+
"Environment",
|
|
68
|
+
"EnvironmentAction",
|
|
69
|
+
"Image",
|
|
70
|
+
"IncludeComponent",
|
|
71
|
+
"IncludeItem",
|
|
72
|
+
"IncludeLocal",
|
|
73
|
+
"IncludeProject",
|
|
74
|
+
"IncludeRemote",
|
|
75
|
+
"IncludeTemplate",
|
|
76
|
+
"Inherit",
|
|
77
|
+
"InputRule",
|
|
78
|
+
"InputType",
|
|
79
|
+
"Job",
|
|
80
|
+
"Need",
|
|
81
|
+
"NeedsPipeline",
|
|
82
|
+
"Pages",
|
|
83
|
+
"Parallel",
|
|
84
|
+
"Pipeline",
|
|
85
|
+
"Release",
|
|
86
|
+
"Retry",
|
|
87
|
+
"RetryWhen",
|
|
88
|
+
"Rule",
|
|
89
|
+
"Secret",
|
|
90
|
+
"Service",
|
|
91
|
+
"Trigger",
|
|
92
|
+
"TriggerForward",
|
|
93
|
+
"Variable",
|
|
94
|
+
"VaultConfig",
|
|
95
|
+
"VaultEngine",
|
|
96
|
+
"WhenCondition",
|
|
97
|
+
"Workflow",
|
|
98
|
+
"WorkflowRule",
|
|
99
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import aiofiles
|
|
6
|
+
|
|
7
|
+
from gitlab_cicd_python_wrapper.component import Component
|
|
8
|
+
from gitlab_cicd_python_wrapper.pipeline import Pipeline
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AsyncPipeline:
|
|
12
|
+
@classmethod
|
|
13
|
+
async def from_yaml(cls, source: str | Path) -> Pipeline:
|
|
14
|
+
if isinstance(source, Path):
|
|
15
|
+
async with aiofiles.open(source) as f:
|
|
16
|
+
content = await f.read()
|
|
17
|
+
return Pipeline.from_yaml(content)
|
|
18
|
+
return Pipeline.from_yaml(source)
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
async def to_yaml(cls, pipeline: Pipeline, target: str | Path | None = None) -> str:
|
|
22
|
+
result = pipeline.to_yaml()
|
|
23
|
+
if target is not None:
|
|
24
|
+
async with aiofiles.open(target, "w") as f:
|
|
25
|
+
await f.write(result)
|
|
26
|
+
return result
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
async def validate_file(cls, path: Path) -> list[str]:
|
|
30
|
+
async with aiofiles.open(path) as f:
|
|
31
|
+
content = await f.read()
|
|
32
|
+
return Pipeline.validate_file_from_string(content)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AsyncComponent:
|
|
36
|
+
@classmethod
|
|
37
|
+
async def from_yaml(cls, source: str | Path) -> Component:
|
|
38
|
+
if isinstance(source, Path):
|
|
39
|
+
async with aiofiles.open(source) as f:
|
|
40
|
+
content = await f.read()
|
|
41
|
+
return Component.from_yaml(content)
|
|
42
|
+
return Component.from_yaml(source)
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
async def to_yaml(cls, component: Component, target: str | Path | None = None) -> str:
|
|
46
|
+
result = component.to_yaml()
|
|
47
|
+
if target is not None:
|
|
48
|
+
async with aiofiles.open(target, "w") as f:
|
|
49
|
+
await f.write(result)
|
|
50
|
+
return result
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
|
|
5
|
+
from gitlab_cicd_python_wrapper.common import ArtifactAccess, ArtifactWhen
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ArtifactReports(BaseModel):
|
|
9
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
10
|
+
|
|
11
|
+
junit: list[str] | str | None = None
|
|
12
|
+
coverage_report: dict[str, str] | None = None
|
|
13
|
+
codequality: list[str] | str | None = None
|
|
14
|
+
sast: list[str] | str | None = None
|
|
15
|
+
dependency_scanning: list[str] | str | None = None
|
|
16
|
+
container_scanning: list[str] | str | None = None
|
|
17
|
+
dast: list[str] | str | None = None
|
|
18
|
+
license_scanning: list[str] | str | None = None
|
|
19
|
+
performance: list[str] | str | None = None
|
|
20
|
+
dotenv: list[str] | str | None = None
|
|
21
|
+
terraform: list[str] | str | None = None
|
|
22
|
+
metrics: list[str] | str | None = None
|
|
23
|
+
requirements: list[str] | str | None = None
|
|
24
|
+
secret_detection: list[str] | str | None = None
|
|
25
|
+
cyclonedx: list[str] | str | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Artifacts(BaseModel):
|
|
29
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
30
|
+
|
|
31
|
+
paths: list[str] | None = None
|
|
32
|
+
exclude: list[str] | None = None
|
|
33
|
+
expire_in: str | None = None
|
|
34
|
+
expose_as: str | None = None
|
|
35
|
+
name: str | None = None
|
|
36
|
+
public: bool | None = None
|
|
37
|
+
access: ArtifactAccess | None = None
|
|
38
|
+
reports: ArtifactReports | None = None
|
|
39
|
+
untracked: bool | None = None
|
|
40
|
+
when: ArtifactWhen | None = None
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
|
|
5
|
+
from gitlab_cicd_python_wrapper.common import CachePolicy, CacheWhen
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CacheKey(BaseModel):
|
|
9
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
10
|
+
|
|
11
|
+
files: list[str] | None = None
|
|
12
|
+
files_commits: list[str] | None = None
|
|
13
|
+
prefix: str | None = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Cache(BaseModel):
|
|
17
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
18
|
+
|
|
19
|
+
paths: list[str] | None = None
|
|
20
|
+
key: str | CacheKey | None = None
|
|
21
|
+
untracked: bool | None = None
|
|
22
|
+
unprotect: bool | None = None
|
|
23
|
+
when: CacheWhen | None = None
|
|
24
|
+
policy: CachePolicy | None = None
|
|
25
|
+
fallback_keys: list[str] | None = None
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from gitlab_cicd_python_wrapper import __version__
|
|
8
|
+
from gitlab_cicd_python_wrapper.component import Component
|
|
9
|
+
from gitlab_cicd_python_wrapper.pipeline import Pipeline
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _validate_pipeline(path: Path) -> list[str]:
|
|
13
|
+
return Pipeline.validate_file(path)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _validate_component(path: Path) -> list[str]:
|
|
17
|
+
return Component.validate_file(path)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> int:
|
|
21
|
+
parser = argparse.ArgumentParser(
|
|
22
|
+
prog="gitlab-cicd-validate",
|
|
23
|
+
description="Validate GitLab CI/CD YAML files",
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument("files", nargs="+", type=Path, help="YAML files to validate")
|
|
26
|
+
parser.add_argument("--strict", action="store_true", help="Enable strict validation")
|
|
27
|
+
parser.add_argument("--component", action="store_true", help="Validate as component")
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"--format", choices=["text", "json"], default="text", dest="output_format", help="Output format"
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument("--quiet", action="store_true", help="Suppress output on success")
|
|
32
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
33
|
+
|
|
34
|
+
args = parser.parse_args()
|
|
35
|
+
|
|
36
|
+
all_valid = True
|
|
37
|
+
results: list[dict] = []
|
|
38
|
+
|
|
39
|
+
for file_path in args.files:
|
|
40
|
+
if args.component:
|
|
41
|
+
errors = _validate_component(file_path)
|
|
42
|
+
else:
|
|
43
|
+
errors = _validate_pipeline(file_path)
|
|
44
|
+
|
|
45
|
+
valid = len(errors) == 0
|
|
46
|
+
if not valid:
|
|
47
|
+
all_valid = False
|
|
48
|
+
|
|
49
|
+
result = {"file": str(file_path), "valid": valid, "errors": errors}
|
|
50
|
+
results.append(result)
|
|
51
|
+
|
|
52
|
+
if args.output_format == "json":
|
|
53
|
+
print(json.dumps(results, indent=2))
|
|
54
|
+
elif not args.quiet:
|
|
55
|
+
for result in results:
|
|
56
|
+
if result["valid"]:
|
|
57
|
+
print(f"{result['file']}: valid")
|
|
58
|
+
else:
|
|
59
|
+
print(f"{result['file']}: invalid")
|
|
60
|
+
for error in result["errors"]:
|
|
61
|
+
print(f" - {error}")
|
|
62
|
+
elif not all_valid:
|
|
63
|
+
for result in results:
|
|
64
|
+
if not result["valid"]:
|
|
65
|
+
print(f"{result['file']}: invalid")
|
|
66
|
+
for error in result["errors"]:
|
|
67
|
+
print(f" - {error}")
|
|
68
|
+
|
|
69
|
+
return 0 if all_valid else 1
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class WhenCondition(str, Enum):
|
|
7
|
+
on_success = "on_success"
|
|
8
|
+
on_failure = "on_failure"
|
|
9
|
+
always = "always"
|
|
10
|
+
never = "never"
|
|
11
|
+
manual = "manual"
|
|
12
|
+
delayed = "delayed"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CachePolicy(str, Enum):
|
|
16
|
+
pull = "pull"
|
|
17
|
+
push = "push"
|
|
18
|
+
pull_push = "pull-push"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CacheWhen(str, Enum):
|
|
22
|
+
on_success = "on_success"
|
|
23
|
+
on_failure = "on_failure"
|
|
24
|
+
always = "always"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ArtifactWhen(str, Enum):
|
|
28
|
+
on_success = "on_success"
|
|
29
|
+
on_failure = "on_failure"
|
|
30
|
+
always = "always"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ArtifactAccess(str, Enum):
|
|
34
|
+
all = "all"
|
|
35
|
+
developer = "developer"
|
|
36
|
+
maintainer = "maintainer"
|
|
37
|
+
none = "none"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RetryWhen(str, Enum):
|
|
41
|
+
always = "always"
|
|
42
|
+
unknown_failure = "unknown_failure"
|
|
43
|
+
script_failure = "script_failure"
|
|
44
|
+
api_failure = "api_failure"
|
|
45
|
+
stuck_or_timeout_failure = "stuck_or_timeout_failure"
|
|
46
|
+
runner_system_failure = "runner_system_failure"
|
|
47
|
+
missing_dependency_failure = "missing_dependency_failure"
|
|
48
|
+
runner_unsupported = "runner_unsupported"
|
|
49
|
+
stale_schedule = "stale_schedule"
|
|
50
|
+
job_execution_timeout = "job_execution_timeout"
|
|
51
|
+
archived_failure = "archived_failure"
|
|
52
|
+
unmet_prerequisites = "unmet_prerequisites"
|
|
53
|
+
scheduler_failure = "scheduler_failure"
|
|
54
|
+
data_integrity_failure = "data_integrity_failure"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class DeploymentTier(str, Enum):
|
|
58
|
+
production = "production"
|
|
59
|
+
staging = "staging"
|
|
60
|
+
testing = "testing"
|
|
61
|
+
development = "development"
|
|
62
|
+
other = "other"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class EnvironmentAction(str, Enum):
|
|
66
|
+
start = "start"
|
|
67
|
+
stop = "stop"
|
|
68
|
+
prepare = "prepare"
|
|
69
|
+
rollback = "rollback"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class AutoCancelOnNewCommit(str, Enum):
|
|
73
|
+
conservative = "conservative"
|
|
74
|
+
interruptible = "interruptible"
|
|
75
|
+
none = "none"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class AutoCancelOnJobFailure(str, Enum):
|
|
79
|
+
all = "all"
|
|
80
|
+
none = "none"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class InputType(str, Enum):
|
|
84
|
+
string = "string"
|
|
85
|
+
number = "number"
|
|
86
|
+
boolean = "boolean"
|
|
87
|
+
array = "array"
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from gitlab_cicd_python_wrapper.pipeline import Pipeline
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict
|
|
11
|
+
|
|
12
|
+
from gitlab_cicd_python_wrapper.common import InputType
|
|
13
|
+
from gitlab_cicd_python_wrapper.job import Job
|
|
14
|
+
from gitlab_cicd_python_wrapper.serialization import dump_yaml_multi, load_yaml_multi
|
|
15
|
+
from gitlab_cicd_python_wrapper.spec import ComponentSpec
|
|
16
|
+
|
|
17
|
+
INPUT_INTERPOLATION_RE = re.compile(r"\$\[\[\s*inputs\.(\w+)\s*\]\]")
|
|
18
|
+
COMPONENT_INTERPOLATION_RE = re.compile(r"\$\[\[\s*component\.(\w+)\s*\]\]")
|
|
19
|
+
_INTERPOLATION_RE = re.compile(r"\$\[\[.+?\]\]")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _strip_interpolation_fields(raw_job: dict) -> dict:
|
|
23
|
+
stripped = {}
|
|
24
|
+
for k, v in raw_job.items():
|
|
25
|
+
if isinstance(v, str) and _INTERPOLATION_RE.fullmatch(v.strip()):
|
|
26
|
+
continue
|
|
27
|
+
stripped[k] = v
|
|
28
|
+
return stripped
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _check_type(name: str, value: Any, expected: InputType) -> None:
|
|
32
|
+
type_map = {
|
|
33
|
+
InputType.string: str,
|
|
34
|
+
InputType.number: (int, float),
|
|
35
|
+
InputType.boolean: bool,
|
|
36
|
+
InputType.array: list,
|
|
37
|
+
}
|
|
38
|
+
expected_type = type_map[expected]
|
|
39
|
+
if not isinstance(value, expected_type):
|
|
40
|
+
raise ValueError(f"Input '{name}' must be of type {expected.value}, got {type(value).__name__}")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Component(BaseModel):
|
|
44
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
45
|
+
|
|
46
|
+
spec: ComponentSpec
|
|
47
|
+
jobs: dict[str, Job]
|
|
48
|
+
|
|
49
|
+
_raws: list = []
|
|
50
|
+
_raw_jobs: dict[str, dict] = {}
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_yaml(cls, source: str | Path) -> Component:
|
|
54
|
+
dicts, raws = load_yaml_multi(source)
|
|
55
|
+
if len(dicts) < 2:
|
|
56
|
+
raise ValueError("Component must have spec: section and job definitions separated by ---")
|
|
57
|
+
spec_data = dicts[0].get("spec", dicts[0])
|
|
58
|
+
spec = ComponentSpec.model_validate(spec_data)
|
|
59
|
+
job_data = dicts[1]
|
|
60
|
+
jobs = {}
|
|
61
|
+
raw_jobs = {}
|
|
62
|
+
for name, raw_job in job_data.items():
|
|
63
|
+
raw_jobs[name] = dict(raw_job) if hasattr(raw_job, "items") else raw_job
|
|
64
|
+
try:
|
|
65
|
+
jobs[name] = Job.model_validate(raw_job)
|
|
66
|
+
except Exception:
|
|
67
|
+
jobs[name] = Job.model_validate(_strip_interpolation_fields(raw_job))
|
|
68
|
+
component = cls(spec=spec, jobs=jobs)
|
|
69
|
+
component._raws = raws
|
|
70
|
+
component._raw_jobs = raw_jobs
|
|
71
|
+
return component
|
|
72
|
+
|
|
73
|
+
def validate_inputs(self, provided: dict[str, Any]) -> dict[str, Any]:
|
|
74
|
+
if self.spec.inputs is None:
|
|
75
|
+
if provided:
|
|
76
|
+
raise ValueError(f"Component accepts no inputs, but got: {list(provided.keys())}")
|
|
77
|
+
return {}
|
|
78
|
+
resolved = {}
|
|
79
|
+
for name, input_def in self.spec.inputs.items():
|
|
80
|
+
if name in provided:
|
|
81
|
+
value = provided[name]
|
|
82
|
+
elif input_def.default is not None:
|
|
83
|
+
value = input_def.default
|
|
84
|
+
else:
|
|
85
|
+
raise ValueError(f"Input '{name}' is required (no default provided)")
|
|
86
|
+
_check_type(name, value, input_def.type)
|
|
87
|
+
if input_def.options is not None and value not in input_def.options:
|
|
88
|
+
raise ValueError(f"Input '{name}' value '{value}' must be one of: {input_def.options}")
|
|
89
|
+
if input_def.regex is not None and isinstance(value, str):
|
|
90
|
+
if not re.match(input_def.regex, value):
|
|
91
|
+
raise ValueError(f"Input '{name}' value '{value}' does not match regex: {input_def.regex}")
|
|
92
|
+
resolved[name] = value
|
|
93
|
+
extra = set(provided) - set(self.spec.inputs)
|
|
94
|
+
if extra:
|
|
95
|
+
raise ValueError(f"Unknown inputs: {extra}")
|
|
96
|
+
return resolved
|
|
97
|
+
|
|
98
|
+
def render(self, inputs: dict[str, Any]) -> "Pipeline":
|
|
99
|
+
from gitlab_cicd_python_wrapper.pipeline import Pipeline
|
|
100
|
+
|
|
101
|
+
resolved = self.validate_inputs(inputs)
|
|
102
|
+
|
|
103
|
+
def interpolate(value: Any) -> Any:
|
|
104
|
+
if isinstance(value, str):
|
|
105
|
+
|
|
106
|
+
def replace_input(m: re.Match) -> str:
|
|
107
|
+
key = m.group(1)
|
|
108
|
+
v = resolved.get(key, m.group(0))
|
|
109
|
+
return str(v) if not isinstance(v, str) else v
|
|
110
|
+
|
|
111
|
+
return INPUT_INTERPOLATION_RE.sub(replace_input, value)
|
|
112
|
+
if isinstance(value, list):
|
|
113
|
+
return [interpolate(v) for v in value]
|
|
114
|
+
if isinstance(value, dict):
|
|
115
|
+
return {k: interpolate(v) for k, v in value.items()}
|
|
116
|
+
return value
|
|
117
|
+
|
|
118
|
+
raw_source = (
|
|
119
|
+
self._raw_jobs
|
|
120
|
+
if self._raw_jobs
|
|
121
|
+
else {n: j.model_dump(exclude_none=True, by_alias=True) for n, j in self.jobs.items()}
|
|
122
|
+
)
|
|
123
|
+
jobs = {}
|
|
124
|
+
for name, raw_job in raw_source.items():
|
|
125
|
+
job_data = interpolate(raw_job)
|
|
126
|
+
jobs[name] = Job.model_validate(job_data)
|
|
127
|
+
return Pipeline(jobs=jobs)
|
|
128
|
+
|
|
129
|
+
def to_yaml(self, target: str | Path | None = None) -> str:
|
|
130
|
+
if self._raws:
|
|
131
|
+
return dump_yaml_multi(self._raws, target)
|
|
132
|
+
raise NotImplementedError("to_yaml from constructed Component not yet supported")
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def validate_file(cls, path: Path) -> list[str]:
|
|
136
|
+
try:
|
|
137
|
+
cls.from_yaml(path)
|
|
138
|
+
return []
|
|
139
|
+
except Exception as e:
|
|
140
|
+
return [str(e)]
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def validate_file_from_string(cls, content: str) -> list[str]:
|
|
144
|
+
try:
|
|
145
|
+
cls.from_yaml(content)
|
|
146
|
+
return []
|
|
147
|
+
except Exception as e:
|
|
148
|
+
return [str(e)]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
|
|
5
|
+
from gitlab_cicd_python_wrapper.common import DeploymentTier, EnvironmentAction
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EnvironmentKubernetes(BaseModel):
|
|
9
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
10
|
+
|
|
11
|
+
namespace: str | None = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Environment(BaseModel):
|
|
15
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
16
|
+
|
|
17
|
+
name: str
|
|
18
|
+
url: str | None = None
|
|
19
|
+
deployment_tier: DeploymentTier | None = None
|
|
20
|
+
auto_stop_in: str | None = None
|
|
21
|
+
action: EnvironmentAction | None = None
|
|
22
|
+
kubernetes: EnvironmentKubernetes | None = None
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
|
|
5
|
+
from gitlab_cicd_python_wrapper.artifacts import Artifacts
|
|
6
|
+
from gitlab_cicd_python_wrapper.cache import Cache
|
|
7
|
+
from gitlab_cicd_python_wrapper.common import (
|
|
8
|
+
AutoCancelOnJobFailure,
|
|
9
|
+
AutoCancelOnNewCommit,
|
|
10
|
+
)
|
|
11
|
+
from gitlab_cicd_python_wrapper.image import Image, Service
|
|
12
|
+
from gitlab_cicd_python_wrapper.retry import Retry
|
|
13
|
+
from gitlab_cicd_python_wrapper.rules import WorkflowRule
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Default(BaseModel):
|
|
17
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
18
|
+
|
|
19
|
+
after_script: list[str] | None = None
|
|
20
|
+
artifacts: Artifacts | None = None
|
|
21
|
+
before_script: list[str] | None = None
|
|
22
|
+
cache: Cache | list[Cache] | None = None
|
|
23
|
+
hooks: dict[str, list[str]] | None = None
|
|
24
|
+
id_tokens: dict[str, dict[str, str]] | None = None
|
|
25
|
+
image: str | Image | None = None
|
|
26
|
+
interruptible: bool | None = None
|
|
27
|
+
retry: int | Retry | None = None
|
|
28
|
+
services: list[str | Service] | None = None
|
|
29
|
+
tags: list[str] | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AutoCancel(BaseModel):
|
|
33
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
34
|
+
|
|
35
|
+
on_new_commit: AutoCancelOnNewCommit | None = None
|
|
36
|
+
on_job_failure: AutoCancelOnJobFailure | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Workflow(BaseModel):
|
|
40
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
41
|
+
|
|
42
|
+
name: str | None = None
|
|
43
|
+
rules: list[WorkflowRule] | None = None
|
|
44
|
+
auto_cancel: AutoCancel | None = None
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Image(BaseModel):
|
|
7
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
8
|
+
|
|
9
|
+
name: str
|
|
10
|
+
entrypoint: list[str] | str | None = None
|
|
11
|
+
pull_policy: str | None = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Service(BaseModel):
|
|
15
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
16
|
+
|
|
17
|
+
name: str
|
|
18
|
+
alias: str | None = None
|
|
19
|
+
entrypoint: list[str] | str | None = None
|
|
20
|
+
command: list[str] | str | None = None
|
|
21
|
+
pull_policy: str | None = None
|
|
22
|
+
variables: dict[str, str] | None = None
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Union
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ComponentReference(BaseModel):
|
|
9
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
10
|
+
|
|
11
|
+
fqdn: str
|
|
12
|
+
project_path: str
|
|
13
|
+
component_name: str
|
|
14
|
+
version: str
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_string(cls, ref: str) -> ComponentReference:
|
|
18
|
+
base, version = ref.split("@", 1)
|
|
19
|
+
parts = base.split("/")
|
|
20
|
+
return cls(
|
|
21
|
+
fqdn=parts[0],
|
|
22
|
+
component_name=parts[-1],
|
|
23
|
+
project_path="/".join(parts[1:-1]),
|
|
24
|
+
version=version,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class IncludeLocal(BaseModel):
|
|
29
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
30
|
+
|
|
31
|
+
local: str
|
|
32
|
+
inputs: dict[str, Any] | None = None
|
|
33
|
+
rules: list[dict[str, Any]] | None = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class IncludeRemote(BaseModel):
|
|
37
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
38
|
+
|
|
39
|
+
remote: str
|
|
40
|
+
inputs: dict | None = None
|
|
41
|
+
rules: list[dict] | None = None
|
|
42
|
+
integrity: str | None = None
|
|
43
|
+
cache: bool | str | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class IncludeTemplate(BaseModel):
|
|
47
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
48
|
+
|
|
49
|
+
template: str
|
|
50
|
+
inputs: dict | None = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class IncludeProject(BaseModel):
|
|
54
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
55
|
+
|
|
56
|
+
project: str
|
|
57
|
+
file: str | list[str] | None = None
|
|
58
|
+
ref: str | None = None
|
|
59
|
+
inputs: dict | None = None
|
|
60
|
+
rules: list[dict] | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class IncludeComponent(BaseModel):
|
|
64
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
65
|
+
|
|
66
|
+
component: str
|
|
67
|
+
inputs: dict | None = None
|
|
68
|
+
rules: list[dict] | None = None
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def parsed_ref(self) -> ComponentReference:
|
|
72
|
+
return ComponentReference.from_string(self.component)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
IncludeItem = Union[IncludeLocal, IncludeRemote, IncludeTemplate, IncludeProject, IncludeComponent]
|