Skip to content

Commit d4088d9

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

File tree

4 files changed

+85
-57
lines changed

4 files changed

+85
-57
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 & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747

4848

4949
DEFAULT_SOURCE = "https://github.com/snowflakedb/snowflake-cli-templates"
50-
DEFAULT_CI_SOURCE = DEFAULT_SOURCE + "/cicd"
5150

5251
log = logging.getLogger(__name__)
5352

@@ -220,51 +219,39 @@ def init(
220219
variables_from_flags = {
221220
v.key: v.value for v in parse_key_value_variables(variables)
222221
}
223-
is_remote = any(
224-
template_source.startswith(prefix) for prefix in ["git@", "http://", "https://"] # type: ignore
225-
)
226222
args_error_msg = f"Check whether {TemplateOption.param_decls[0]} and {SourceOption.param_decls[0]} arguments are correct."
227223

228224
# copy/download template into tmpdir, so it is going to be removed in case command ends with an error
229225
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-
)
226+
assert isinstance(template_source, str)
227+
template_root = _fetch_template(template_source, template, tmpdir)
228+
229+
template_metadata = _read_template_metadata(
230+
template_root, args_error_msg=args_error_msg
231+
)
232+
if template_metadata.minimum_cli_version:
233+
_validate_cli_version(template_metadata.minimum_cli_version)
240234

241235
if ci_provider:
242236
ci_provider_instance = CIProvider.from_choice(ci_provider)
243237
if ci_template_source is not None:
244238
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
239+
cicd_template_root = _fetch_template(
240+
ci_template_source, None, cicd_tmpdir
241+
)
242+
ci_provider_instance.copy(cicd_template_root, template_root)
243+
ci_template_metadata = _read_template_metadata(
244+
cicd_template_root,
245+
args_error_msg="template.yml is required for --ci-template-source.",
250246
)
251-
cicd_template_root.copy(template_root.path)
247+
template_metadata += ci_template_metadata
252248

253249
elif ci_provider_instance.has_template(template_root):
254250
pass # template has ci files
255251
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)
252+
raise ClickException(
253+
f"Template for {ci_provider_instance.name} not provided and not configured on selected template."
254+
)
268255

269256
variable_values = _determine_variable_values(
270257
variables_metadata=template_metadata.variables,
@@ -274,13 +261,12 @@ def init(
274261
"project_dir_name": SecurePath(path).name,
275262
"snowflake_cli_version": VERSION,
276263
}
277-
files_to_render = template_metadata.files_to_render + list(
278-
ci_provider_instance.get_files_to_render(template_root)
264+
log.debug(
265+
"Rendering template files: %s", ", ".join(template_metadata.files_to_render)
279266
)
280-
log.debug("Rendering template files: %s", ", ".join(files_to_render))
281267
render_template_files(
282268
template_root=template_root,
283-
files_to_render=files_to_render,
269+
files_to_render=template_metadata.files_to_render,
284270
data=variable_values,
285271
)
286272
_remove_template_metadata_file(template_root)
@@ -291,5 +277,27 @@ def init(
291277
return MessageResult(f"Initialized the new project in {path}")
292278

293279

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

src/snowflake/cli/api/secure_path.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -116,14 +116,6 @@ def glob(self, pattern: str):
116116
"""
117117
return self._path.glob(pattern)
118118

119-
def rglob(self, pattern: str) -> Generator[Path, None, None]:
120-
"""
121-
Recursively yield all existing files (of any kind, including
122-
directories) matching the given relative pattern, anywhere in
123-
this subtree.
124-
"""
125-
return self._path.rglob(pattern)
126-
127119
def as_posix(self) -> str:
128120
"""
129121
Return the string representation of the path with forward slashes (/) as the path separator.

0 commit comments

Comments
 (0)