Skip to content

Commit 9df5bfd

Browse files
feat: [SNOW-1942875] initial version of cicd templates in snow init
1 parent f0e4b6c commit 9df5bfd

File tree

3 files changed

+85
-48
lines changed

3 files changed

+85
-48
lines changed

src/snowflake/cli/_plugins/cicd/manager.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import enum
2-
from typing import Generator, List, Optional, Type
2+
from typing import List, Optional, Type
33

44
from snowflake.cli.api.secure_path import SecurePath
55

@@ -11,7 +11,6 @@ class CIProviderChoices(str, enum.Enum):
1111

1212
class CIProvider:
1313
name: str
14-
files_to_render_directories: list[str] = []
1514

1615
@classmethod
1716
def cleanup(cls, root: SecurePath) -> None:
@@ -31,18 +30,15 @@ def from_choice(cls, choice: CIProviderChoices) -> "CIProvider":
3130
def all(cls) -> List[Type["CIProvider"]]: # noqa: A003
3231
return [GithubProvider, GitLabProvider]
3332

34-
def get_files_to_render(self, root_dir: SecurePath) -> Generator[str, None, None]:
35-
for directory in self.files_to_render_directories:
36-
for path in (root_dir / directory).rglob("*"):
37-
yield str(path.relative_to(root_dir.path))
38-
3933
def has_template(self, root_dir: SecurePath) -> bool:
4034
raise NotImplementedError()
4135

36+
def copy(self, source: SecurePath, destination: SecurePath):
37+
raise NotImplementedError()
38+
4239

4340
class GithubProvider(CIProvider):
4441
name = CIProviderChoices.GITLAB.name
45-
files_to_render_directories = [".github/workflows/"]
4642

4743
@classmethod
4844
def cleanup(cls, root_dir: SecurePath):
@@ -51,6 +47,9 @@ def cleanup(cls, root_dir: SecurePath):
5147
def has_template(self, root_dir: SecurePath) -> bool:
5248
return (root_dir / ".github/workflows").exists()
5349

50+
def copy(self, source: SecurePath, destination: SecurePath) -> None:
51+
(source / ".github").copy(destination.path, dirs_exist_ok=True)
52+
5453

5554
class GitLabProvider(CIProvider):
5655
name = CIProviderChoices.GITLAB.name
@@ -59,13 +58,14 @@ class GitLabProvider(CIProvider):
5958
def cleanup(cls, root_dir: SecurePath):
6059
(root_dir / ".gitlab-ci.yml").unlink(missing_ok=True)
6160

62-
def get_files_to_render(self, root_dir: SecurePath):
63-
if (root_dir / ".gitlab-ci.yml").exists():
64-
return [".gitlab-ci.yml"]
65-
6661
def has_template(self, root_dir: SecurePath) -> bool:
6762
return (root_dir / ".gitlab-ci.yml").exists()
6863

64+
def copy(self, source: SecurePath, destination: SecurePath) -> None:
65+
if (destination / ".gitlab-ci.yml").exists():
66+
(destination / ".gitlab-ci.yml").unlink()
67+
(source / ".gitlab-ci.yml").move(destination.path)
68+
6969

7070
class CIProviderManager:
7171
@staticmethod

src/snowflake/cli/_plugins/init/commands.py

Lines changed: 44 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -220,51 +220,39 @@ def init(
220220
variables_from_flags = {
221221
v.key: v.value for v in parse_key_value_variables(variables)
222222
}
223-
is_remote = any(
224-
template_source.startswith(prefix) for prefix in ["git@", "http://", "https://"] # type: ignore
225-
)
226223
args_error_msg = f"Check whether {TemplateOption.param_decls[0]} and {SourceOption.param_decls[0]} arguments are correct."
227224

228225
# copy/download template into tmpdir, so it is going to be removed in case command ends with an error
229226
with SecurePath.temporary_directory() as tmpdir:
230-
if is_remote:
231-
template_root = _fetch_remote_template(
232-
url=template_source, path=template, destination=tmpdir # type: ignore
233-
)
234-
else:
235-
template_root = _fetch_local_template(
236-
template_source=SecurePath(template_source),
237-
path=template,
238-
destination=tmpdir,
239-
)
227+
assert isinstance(template_source, str)
228+
template_root = _fetch_template(template_source, template, tmpdir)
229+
230+
template_metadata = _read_template_metadata(
231+
template_root, args_error_msg=args_error_msg
232+
)
233+
if template_metadata.minimum_cli_version:
234+
_validate_cli_version(template_metadata.minimum_cli_version)
240235

241236
if ci_provider:
242237
ci_provider_instance = CIProvider.from_choice(ci_provider)
243238
if ci_template_source is not None:
244239
with SecurePath.temporary_directory() as cicd_tmpdir:
245-
cicd_template_root = _fetch_remote_template(
246-
url=ci_template_source,
247-
path=None,
248-
destination=cicd_tmpdir
249-
# type: ignore
240+
cicd_template_root = _fetch_template(
241+
ci_template_source, None, cicd_tmpdir
242+
)
243+
ci_provider_instance.copy(cicd_template_root, template_root)
244+
ci_template_metadata = _read_template_metadata(
245+
cicd_template_root,
246+
args_error_msg="template.yml is required for --ci-template-source.",
250247
)
251-
cicd_template_root.copy(template_root.path)
248+
template_metadata += ci_template_metadata
252249

253250
elif ci_provider_instance.has_template(template_root):
254251
pass # template has ci files
255252
else:
256-
# use generic ci/cd template
257-
with SecurePath.temporary_directory() as cicd_tmpdir:
258-
cicd_template_root = _fetch_remote_template(
259-
url=DEFAULT_SOURCE, path=f"cicd/{ci_provider_instance.name.lower()}", destination=cicd_tmpdir # type: ignore
260-
)
261-
cicd_template_root.copy(template_root.path)
262-
263-
template_metadata = _read_template_metadata(
264-
template_root, args_error_msg=args_error_msg
265-
)
266-
if template_metadata.minimum_cli_version:
267-
_validate_cli_version(template_metadata.minimum_cli_version)
253+
raise ClickException(
254+
f"Template for {ci_provider_instance.name} CICD not provided and not configured on selected template."
255+
)
268256

269257
variable_values = _determine_variable_values(
270258
variables_metadata=template_metadata.variables,
@@ -274,13 +262,12 @@ def init(
274262
"project_dir_name": SecurePath(path).name,
275263
"snowflake_cli_version": VERSION,
276264
}
277-
files_to_render = template_metadata.files_to_render + list(
278-
ci_provider_instance.get_files_to_render(template_root)
265+
log.debug(
266+
"Rendering template files: %s", ", ".join(template_metadata.files_to_render)
279267
)
280-
log.debug("Rendering template files: %s", ", ".join(files_to_render))
281268
render_template_files(
282269
template_root=template_root,
283-
files_to_render=files_to_render,
270+
files_to_render=template_metadata.files_to_render,
284271
data=variable_values,
285272
)
286273
_remove_template_metadata_file(template_root)
@@ -291,5 +278,27 @@ def init(
291278
return MessageResult(f"Initialized the new project in {path}")
292279

293280

281+
def _fetch_template(
282+
template_source: str, template: Optional[str], tmpdir: SecurePath
283+
) -> SecurePath:
284+
if _is_remote_source(template_source):
285+
template_root = _fetch_remote_template(
286+
url=template_source, path=template, destination=tmpdir # type: ignore
287+
)
288+
else:
289+
template_root = _fetch_local_template(
290+
template_source=SecurePath(template_source),
291+
path=template,
292+
destination=tmpdir,
293+
)
294+
return template_root
295+
296+
297+
def _is_remote_source(template_source: str) -> bool:
298+
return any(
299+
template_source.startswith(prefix) for prefix in ["git@", "http://", "https://"] # type: ignore
300+
)
301+
302+
294303
def post_generate(template_root: SecurePath, ci_provider: Optional[CIProvider]):
295304
CIProviderManager.project_post_gen_cleanup(ci_provider, template_root)

src/snowflake/cli/api/project/schemas/template.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@
1818

1919
import typer
2020
from click import ClickException
21-
from pydantic import BaseModel, Field
21+
from pydantic import BaseModel, ConfigDict, Field
2222
from snowflake.cli.api.exceptions import InvalidTemplate
2323
from snowflake.cli.api.secure_path import SecurePath
2424

2525

2626
class TemplateVariable(BaseModel):
27+
model_config = ConfigDict(frozen=True)
28+
2729
name: str = Field(..., title="Variable identifier")
2830
type: Optional[Literal["string", "float", "int"]] = Field( # noqa: A003
2931
title="Type of the variable", default=None
@@ -64,6 +66,32 @@ def __init__(self, template_root: SecurePath, **kwargs):
6466
super().__init__(**kwargs)
6567
self._validate_files_exist(template_root)
6668

69+
def __add__(self, other: Template):
70+
errors = []
71+
if not isinstance(other, Template):
72+
raise ClickException(f"Can not merge template with {type(other)}")
73+
74+
if self.minimum_cli_version != other.minimum_cli_version:
75+
errors.append(
76+
f"minimum_cli_versions do not match: {self.minimum_cli_version} != {other.minimum_cli_version}"
77+
)
78+
variable_map = {variable.name: variable for variable in self.variables}
79+
for other_variable in other.variables:
80+
if self_variable := variable_map.get(other_variable.name):
81+
for attr in ["type", "prompt", "default"]:
82+
if getattr(self_variable, attr) != getattr(other_variable, attr):
83+
errors.append(
84+
f"Conflicting variable definitions: '{self_variable.name}' has different values for attribute '{attr}': '{getattr(self_variable, attr)}' != '{getattr(other_variable, attr)}'"
85+
)
86+
if errors:
87+
error_str = "\n\t" + "\n\t".join(error for error in errors)
88+
raise ClickException(
89+
f"Could not merge templates. Following errors found:{error_str}"
90+
)
91+
self.files_to_render = list(set(self.files_to_render + other.files_to_render))
92+
self.variables = list(set(self.variables + other.variables))
93+
return self
94+
6795
def _validate_files_exist(self, template_root: SecurePath) -> None:
6896
for path_in_template in self.files_to_render:
6997
full_path = template_root / path_in_template

0 commit comments

Comments
 (0)