Skip to content

Commit 00a1963

Browse files
feat(core): do not update template if dockerfile is modified (#3396)
1 parent 4f10d5f commit 00a1963

File tree

13 files changed

+276
-57
lines changed

13 files changed

+276
-57
lines changed

docs/spelling_wordlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ versioned
287287
versioning
288288
Versioning
289289
vertices
290+
viewmodel
290291
vm
291292
wasDerivedFrom
292293
webhook

renku/command/migrate.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,9 @@ def _dockerfile_migration_check():
131131
Dictionary of Dockerfile migration status.
132132
"""
133133
from renku import __version__
134-
from renku.core.migration.migrate import is_docker_update_possible
134+
from renku.core.migration.migrate import update_dockerfile
135135

136-
automated_dockerfile_update, newer_renku_available, dockerfile_renku_version = is_docker_update_possible()
136+
automated_dockerfile_update, newer_renku_available, dockerfile_renku_version = update_dockerfile(check_only=True)
137137

138138
return {
139139
"automated_dockerfile_update": automated_dockerfile_update,
@@ -194,14 +194,13 @@ def _check_project():
194194
# NOTE: v10 migration not done
195195
return MIGRATION_REQUIRED
196196

197-
# NOTE: ``project.automated_update`` is deprecated and we always allow template update for a project
197+
# NOTE: ``project.automated_update`` is deprecated. We always allow template update for a project
198198
status = AUTOMATED_TEMPLATE_UPDATE_SUPPORTED
199199

200200
if check_for_template_update(project_context.project)[0]:
201201
status |= TEMPLATE_UPDATE_POSSIBLE
202-
if is_docker_update_possible()[0]:
202+
if is_docker_update_possible():
203203
status |= DOCKERFILE_UPDATE_POSSIBLE
204-
205204
if is_migration_required():
206205
return status | MIGRATION_REQUIRED
207206

renku/command/view_model/template.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ def from_template(
156156
FileAction.KEEP: "Keep",
157157
FileAction.OVERWRITE: "Overwrite",
158158
FileAction.RECREATE: "Recreate deleted file",
159+
FileAction.UPDATE_DOCKERFILE: "Update",
159160
}
160161

161162
file_changes = [

renku/core/login.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def login(endpoint: Optional[str], git_login: bool, yes: bool):
136136
raise errors.AuthenticationError(f"Invalid status code from server: {status_code} - {response.content}")
137137

138138
access_token = response.json().get("access_token")
139-
_store_token(parsed_endpoint.netloc, access_token)
139+
store_token(parsed_endpoint.netloc, access_token)
140140

141141
if git_login and repository:
142142
set_git_credential_helper(repository=cast("Repository", repository), hostname=parsed_endpoint.netloc)
@@ -170,8 +170,9 @@ def _get_url(parsed_endpoint, path, **query_args) -> str:
170170
return parsed_endpoint._replace(path=path, query=query).geturl()
171171

172172

173-
def _store_token(netloc, access_token):
174-
set_value(section=CONFIG_SECTION, key=netloc, value=access_token, global_only=True)
173+
def store_token(hostname: str, access_token: str):
174+
"""Store access token for ``hostname``."""
175+
set_value(section=CONFIG_SECTION, key=hostname, value=access_token, global_only=True)
175176
os.chmod(project_context.global_config_path, 0o600)
176177

177178

@@ -269,6 +270,11 @@ def _restore_git_remote(repository):
269270
communication.error(f"Cannot delete backup remote '{backup_remote}'")
270271

271272

273+
def has_credentials_for_hostname(hostname: str) -> bool:
274+
"""Check if credentials are set for the given hostname."""
275+
return bool(_read_renku_token_for_hostname(hostname))
276+
277+
272278
@validate_arguments(config=dict(arbitrary_types_allowed=True))
273279
def credentials(command: str, hostname: Optional[str]):
274280
"""Credentials helper for git."""

renku/core/migration/migrate.py

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@
2323
public ``migrate`` function that accepts a ``MigrationContext`` as its argument.
2424
2525
When executing a migration, the migration file is imported as a module and the
26-
``migrate`` function is executed. Migration version is checked against the Renku
27-
project version and any migration which has a higher version is applied to the
28-
project.
26+
``migrate`` function is executed. Renku checks project's metadata version and
27+
applies any migration that has a higher version to the project.
2928
"""
3029

3130
import importlib
3231
import re
3332
import shutil
3433
from pathlib import Path
34+
from typing import Optional, Tuple
3535

3636
from packaging.version import Version
3737

@@ -47,14 +47,13 @@
4747
from renku.core.interface.project_gateway import IProjectGateway
4848
from renku.core.migration.models.migration import MigrationContext, MigrationType
4949
from renku.core.migration.utils import is_using_temporary_datasets_path, read_project_version
50-
from renku.core.template.usecase import update_dockerfile_checksum
50+
from renku.core.template.usecase import calculate_dockerfile_checksum, update_dockerfile_checksum
5151
from renku.core.util import communication
5252
from renku.core.util.metadata import (
5353
is_renku_project,
5454
read_renku_version_from_dockerfile,
5555
replace_renku_version_in_dockerfile,
5656
)
57-
from renku.core.util.os import hash_string
5857
from renku.domain_model.project import ProjectTemplateMetadata
5958
from renku.domain_model.project_context import project_context
6059

@@ -84,9 +83,9 @@ def is_project_unsupported():
8483
return is_renku_project() and get_project_version() > SUPPORTED_PROJECT_VERSION
8584

8685

87-
def is_docker_update_possible():
86+
def is_docker_update_possible() -> bool:
8887
"""Check if the Dockerfile can be updated to a new version of renku-python."""
89-
return _update_dockerfile(check_only=True)
88+
return update_dockerfile(check_only=True)[0]
9089

9190

9291
@inject.autoparams("project_gateway")
@@ -141,15 +140,15 @@ def migrate_project(
141140
template_updated = _update_template()
142141
except TemplateUpdateError:
143142
raise
144-
except (Exception, BaseException) as e:
143+
except Exception as e:
145144
raise TemplateUpdateError("Couldn't update from template.") from e
146145

147146
if not skip_docker_update:
148147
try:
149-
docker_updated, _, _ = _update_dockerfile()
148+
docker_updated, _, _ = update_dockerfile()
150149
except DockerfileUpdateError:
151150
raise
152-
except (Exception, BaseException) as e:
151+
except Exception as e:
153152
raise DockerfileUpdateError("Couldn't update renku version in Dockerfile.") from e
154153

155154
if skip_migrations:
@@ -194,13 +193,12 @@ def _remove_untracked_renku_files(metadata_path):
194193
shutil.rmtree(path, ignore_errors=True)
195194

196195

197-
@inject.autoparams()
198-
def _update_template(project_gateway: IProjectGateway) -> bool:
196+
def _update_template() -> bool:
199197
"""Update local files from the remote template."""
200198
from renku.core.template.usecase import update_template
201199

202200
try:
203-
project = project_gateway.get_project()
201+
project = project_context.project
204202
except ValueError:
205203
# NOTE: Old project, we don't know the status until it is migrated
206204
return False
@@ -211,15 +209,13 @@ def _update_template(project_gateway: IProjectGateway) -> bool:
211209
return bool(update_template(interactive=False, force=False, dry_run=False))
212210

213211

214-
def _update_dockerfile(check_only=False):
212+
def update_dockerfile(*, check_only=False) -> Tuple[bool, Optional[bool], Optional[str]]:
215213
"""Update the dockerfile to the newest version of renku."""
216214
from renku import __version__
217215

218216
if not project_context.dockerfile_path.exists():
219217
return False, None, None
220218

221-
communication.echo("Updating dockerfile...")
222-
223219
with open(project_context.dockerfile_path) as f:
224220
dockerfile_content = f.read()
225221

@@ -238,8 +234,10 @@ def _update_dockerfile(check_only=False):
238234
if check_only:
239235
return True, True, str(docker_version)
240236

237+
communication.echo("Updating dockerfile...")
238+
241239
new_content = replace_renku_version_in_dockerfile(dockerfile_content=dockerfile_content, version=__version__)
242-
new_checksum = hash_string(new_content)
240+
new_checksum = calculate_dockerfile_checksum(dockerfile_content=new_content)
243241

244242
try:
245243
update_dockerfile_checksum(new_checksum=new_checksum)

renku/core/template/template.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
from renku.core import errors
3030
from renku.core.util import communication
3131
from renku.core.util.git import clone_repository
32-
from renku.core.util.os import hash_file
3332
from renku.core.util.util import to_semantic_version, to_string
3433
from renku.domain_model.project_context import project_context
3534
from renku.domain_model.template import (
@@ -40,6 +39,8 @@
4039
TemplateParameter,
4140
TemplatesManifest,
4241
TemplatesSource,
42+
hash_template_file,
43+
update_dockerfile_content,
4344
)
4445
from renku.infrastructure.repository import Repository
4546

@@ -73,6 +74,7 @@ class FileAction(IntEnum):
7374
KEEP = 6
7475
OVERWRITE = 7
7576
RECREATE = 8
77+
UPDATE_DOCKERFILE = 9
7678

7779

7880
def fetch_templates_source(source: Optional[str], reference: Optional[str]) -> TemplatesSource:
@@ -142,6 +144,7 @@ def copy_template_metadata_to_project():
142144
FileAction.KEEP: ("", "Keeping"),
143145
FileAction.OVERWRITE: ("copy", "Overwriting"),
144146
FileAction.RECREATE: ("copy", "Recreating deleted file"),
147+
FileAction.UPDATE_DOCKERFILE: ("dockerfile", "Updating"),
145148
}
146149

147150
for relative_path, action in get_sorted_actions(actions=actions).items():
@@ -161,6 +164,10 @@ def copy_template_metadata_to_project():
161164
shutil.copy(source, destination, follow_symlinks=False)
162165
elif operation == "append":
163166
destination.write_text(destination.read_text() + "\n" + source.read_text())
167+
elif operation == "dockerfile":
168+
update_dockerfile_content(source=source, destination=destination)
169+
else:
170+
raise errors.TemplateUpdateError(f"Unknown operation '{operation}'")
164171
except OSError as e:
165172
# TODO: Use a general cleanup strategy: https://github.com/SwissDataScienceCenter/renku-python/issues/736
166173
if cleanup:
@@ -211,7 +218,7 @@ def get_action_for_initialize(relative_path: str, destination: Path) -> FileActi
211218

212219
def get_action_for_set(relative_path: str, destination: Path, new_checksum: Optional[str]) -> FileAction:
213220
"""Decide what to do with a template file."""
214-
current_checksum = hash_file(destination)
221+
current_checksum = hash_template_file(relative_path=relative_path, absolute_path=destination)
215222

216223
if not destination.exists():
217224
return FileAction.CREATE
@@ -220,8 +227,6 @@ def get_action_for_set(relative_path: str, destination: Path, new_checksum: Opti
220227
elif interactive:
221228
overwrite = communication.confirm(f"Overwrite {relative_path}?", default=True)
222229
return FileAction.OVERWRITE if overwrite else FileAction.KEEP
223-
elif relative_path == "Dockerfile" and not is_dockerfile_updated_by_user():
224-
return FileAction.OVERWRITE
225230
elif should_keep(relative_path):
226231
return FileAction.KEEP
227232
else:
@@ -231,7 +236,7 @@ def get_action_for_update(
231236
relative_path: str, destination: Path, old_checksum: Optional[str], new_checksum: Optional[str]
232237
) -> FileAction:
233238
"""Decide what to do with a template file."""
234-
current_checksum = hash_file(destination)
239+
current_checksum = hash_template_file(relative_path=relative_path, absolute_path=destination)
235240
local_changes = current_checksum != old_checksum
236241
remote_changes = new_checksum != old_checksum
237242
file_exists = destination.exists()
@@ -250,8 +255,11 @@ def get_action_for_update(
250255
return FileAction.OVERWRITE if overwrite else FileAction.KEEP
251256
elif not remote_changes:
252257
return FileAction.IGNORE_UNCHANGED_REMOTE
253-
elif relative_path == "Dockerfile" and not is_dockerfile_updated_by_user():
254-
return FileAction.OVERWRITE
258+
elif relative_path == "Dockerfile":
259+
if is_dockerfile_updated_by_user():
260+
raise errors.TemplateUpdateError("Can't update template as Dockerfile was locally changed.")
261+
else:
262+
return FileAction.UPDATE_DOCKERFILE
255263
elif file_deleted or local_changes:
256264
if relative_path in immutable_files:
257265
# NOTE: There are local changes in a file that should not be changed by users, and the file was

renku/core/template/usecase.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,16 @@
4141
)
4242
from renku.core.util import communication
4343
from renku.core.util.metadata import is_renku_project, replace_renku_version_in_dockerfile
44-
from renku.core.util.os import hash_file, hash_string
4544
from renku.core.util.tabulate import tabulate
4645
from renku.domain_model.project import Project
4746
from renku.domain_model.project_context import project_context
48-
from renku.domain_model.template import RenderedTemplate, Template, TemplateMetadata, TemplatesSource
47+
from renku.domain_model.template import (
48+
RenderedTemplate,
49+
Template,
50+
TemplateMetadata,
51+
TemplatesSource,
52+
calculate_dockerfile_checksum,
53+
)
4954
from renku.infrastructure.repository import DiffLineChangeType, Repository
5055

5156

@@ -89,7 +94,7 @@ def is_dockerfile_updated_by_user() -> bool:
8994
return False
9095

9196
original_checksum = read_template_checksum().get("Dockerfile")
92-
current_checksum = hash_file(dockerfile)
97+
current_checksum = calculate_dockerfile_checksum(dockerfile=dockerfile)
9398

9499
if original_checksum == current_checksum: # Dockerfile was never updated
95100
return False
@@ -99,7 +104,7 @@ def is_dockerfile_updated_by_user() -> bool:
99104
original_renku_version = metadata.get("__renku_version__")
100105

101106
original_dockerfile_content = replace_renku_version_in_dockerfile(dockerfile.read_text(), original_renku_version)
102-
original_calculated_checksum = hash_string(original_dockerfile_content)
107+
original_calculated_checksum = calculate_dockerfile_checksum(dockerfile_content=original_dockerfile_content)
103108

104109
if original_checksum == original_calculated_checksum:
105110
return False
@@ -186,7 +191,7 @@ def set_template(
186191

187192
@validate_arguments(config=dict(arbitrary_types_allowed=True))
188193
def update_template(force: bool, interactive: bool, dry_run: bool) -> Optional[TemplateChangeViewModel]:
189-
"""Update project's template if possible. Return True if updated."""
194+
"""Update project's template if possible. Return corresponding viewmodel if updated."""
190195
template_metadata = TemplateMetadata.from_project(project=project_context.project)
191196

192197
if not template_metadata.source:

renku/core/util/git.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def is_valid_git_repository(repository: Optional["Repository"]) -> bool:
9494
repository(Optional[Repository]): The repository to check.
9595
9696
Returns:
97-
bool: Whether or not this is a valid Git repository.
97+
bool: Whether this is a valid Git repository.
9898
9999
"""
100100
return repository is not None and repository.head.is_valid()
@@ -648,13 +648,15 @@ def clone_renku_repository(
648648
progress: The GitProgress object (Default value = None).
649649
config(Optional[dict], optional): Set configuration for the project (Default value = None).
650650
raise_git_except: Whether to raise git exceptions (Default value = False).
651-
checkout_revision: The revision to checkout after clone (Default value = None).
651+
checkout_revision: The revision to check out after clone (Default value = None).
652652
use_renku_credentials(bool, optional): Whether to use Renku provided credentials (Default value = False).
653653
reuse_existing_repository(bool, optional): Whether to clone over an existing repository (Default value = False).
654654
655655
Returns:
656656
The cloned repository.
657657
"""
658+
from renku.core.login import has_credentials_for_hostname
659+
658660
parsed_url = parse_git_url(url)
659661

660662
clone_options = None
@@ -665,7 +667,11 @@ def clone_renku_repository(
665667
git_url = str(absolute_path)
666668
elif parsed_url.scheme in ["http", "https"] and gitlab_token:
667669
git_url = get_oauth_url(url, gitlab_token)
668-
elif parsed_url.scheme in ["http", "https"] and use_renku_credentials:
670+
elif (
671+
parsed_url.scheme in ["http", "https"]
672+
and use_renku_credentials
673+
and has_credentials_for_hostname(parsed_url.hostname) # NOTE: Don't change remote URL if no credentials exist
674+
):
669675
clone_options = [f"--config credential.helper='!renku credentials --hostname {parsed_url.hostname}'"]
670676
deployment_hostname = deployment_hostname or parsed_url.hostname
671677
git_url = get_renku_repo_url(url, deployment_hostname=deployment_hostname, access_token=None)
@@ -727,7 +733,7 @@ def clone_repository(
727733
progress: The GitProgress object (Default value = None).
728734
config(Optional[dict], optional): Set configuration for the project (Default value = None).
729735
raise_git_except: Whether to raise git exceptions (Default value = False).
730-
checkout_revision: The revision to checkout after clone (Default value = None).
736+
checkout_revision: The revision to check out after clone (Default value = None).
731737
no_checkout(bool, optional): Whether to perform a checkout (Default value = False).
732738
clean(bool, optional): Whether to require the target folder to be clean (Default value = False).
733739
clone_options(List[str], optional): Additional clone options (Default value = None).
@@ -775,7 +781,7 @@ def check_and_reuse_existing_repository() -> Optional["Repository"]:
775781
if remote and have_same_remote(remote.url, url):
776782
repository.reset(hard=True)
777783
repository.fetch(all=True, tags=True)
778-
# NOTE: By default we checkout remote repository's HEAD since the local HEAD might not point to
784+
# NOTE: By default we check out remote repository's HEAD since the local HEAD might not point to
779785
# the default branch.
780786
default_checkout_revision = checkout_revision or "origin/HEAD"
781787
repository.checkout(default_checkout_revision)

0 commit comments

Comments
 (0)