Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Round versions for upgrade and schema #1038

Merged
merged 4 commits into from
Feb 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions qhub/initialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ def render_config(
qhub_domain = input("Provide domain: ")
config["domain"] = qhub_domain

# In qhub_version only use major.minor.patch version - drop any pre/post/dev suffixes
config["qhub_version"] = __version__

# Generate default password for Keycloak root user and also example-user if using password auth
Expand Down
21 changes: 18 additions & 3 deletions qhub/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import pydantic
from pydantic import validator, root_validator
from qhub.utils import namestr_regex
from .version import __version__
from .version import rounded_ver_parse, __version__


class CertificateEnum(str, enum.Enum):
Expand Down Expand Up @@ -439,20 +439,35 @@ class Main(Base):
extensions: typing.Optional[typing.List[QHubExtension]]
jupyterhub: typing.Optional[JupyterHub]

# If the qhub_version in the schema is old
# we must tell the user to first run qhub upgrade
@validator("qhub_version", pre=True, always=True)
def check_default(cls, v):
"""
Always called even if qhub_version is not supplied at all (so defaults to ''). That way we can give a more helpful error message.
"""
if v != __version__:
if not cls.is_version_accepted(v):
if v == "":
v = "not supplied"
raise ValueError(
f"qhub_version in the config file must equal {__version__} to be processed by this version of qhub (your value is {v})."
f"qhub_version in the config file must be equivalent to {__version__} to be processed by this version of qhub (your config file version is {v})."
" Install a different version of qhub or run qhub upgrade to ensure your config file is compatible."
)
return v

@classmethod
def is_version_accepted(cls, v):
return v != "" and rounded_ver_parse(v) == rounded_ver_parse(__version__)


def verify(config):
Main(**config)


def is_version_accepted(v):
"""
Given a version string, return boolean indicating whether
qhub_version in the qhub-config.yaml would be acceptable
for deployment with the current QHub package.
"""
return Main.is_version_accepted(v)
42 changes: 27 additions & 15 deletions qhub/upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@
import string
import secrets

from packaging.version import parse as ver_parse

from pydantic.error_wrappers import ValidationError

from .schema import verify
from .schema import verify, is_version_accepted
from .utils import backup_config_file, load_yaml, yaml
from .version import __version__
from .version import __version__, rounded_ver_parse

logger = logging.getLogger(__name__)

Expand All @@ -28,7 +26,7 @@ def do_upgrade(config_filename, attempt_fixes=False):
)
return
except (ValidationError, ValueError) as e:
if config.get("qhub_version", "") == __version__:
if is_version_accepted(config.get("qhub_version", "")):
# There is an unrelated validation problem
print(
f"Your config file {config_filename} appears to be already up-to-date for qhub version {__version__} but there is another validation error.\n"
Expand Down Expand Up @@ -62,7 +60,7 @@ def do_upgrade(config_filename, attempt_fixes=False):
class UpgradeStep(ABC):
_steps = {}

version = "" # Each subclass must have a version
version = "" # Each subclass must have a version - these should be full release versions (not dev/prerelease)

def __init_subclass__(cls):
assert cls.version != ""
Expand All @@ -83,15 +81,23 @@ def upgrade(
Runs through all required upgrade steps (i.e. relevant subclasses of UpgradeStep).
Calls UpgradeStep.upgrade_step for each.
"""
starting_ver = ver_parse(start_version)
finish_ver = ver_parse(finish_version)
starting_ver = rounded_ver_parse(start_version or "0.0.0")
finish_ver = rounded_ver_parse(finish_version)

if finish_ver < starting_ver:
raise ValueError(
f"Your qhub-config.yaml already belongs to a later version ({start_version}) than the installed version of QHub ({finish_version}).\n"
"You should upgrade the installed qhub package (e.g. pip install --upgrade qhub) to work with your deployment."
)

step_versions = sorted(
[
v
for v in cls._steps.keys()
if ver_parse(v) > starting_ver and ver_parse(v) <= finish_ver
if rounded_ver_parse(v) > starting_ver
and rounded_ver_parse(v) <= finish_ver
],
key=ver_parse,
key=rounded_ver_parse,
)

current_start_version = start_version
Expand All @@ -112,7 +118,7 @@ def get_version(self):
return self.version

def requires_qhub_version_field(self):
return ver_parse(self.version) > ver_parse("0.3.13")
return rounded_ver_parse(self.version) > rounded_ver_parse("0.3.13")

def upgrade_step(self, config, start_version, config_filename, *args, **kwargs):
"""
Expand All @@ -130,6 +136,9 @@ def upgrade_step(self, config, start_version, config_filename, *args, **kwargs):
"""

finish_version = self.get_version()
__rounded_finish_version__ = ".".join(
[str(c) for c in rounded_ver_parse(finish_version)]
)

print(
f"\n---> Starting upgrade from {start_version or 'old version'} to {finish_version}\n"
Expand Down Expand Up @@ -158,7 +167,7 @@ def _new_docker_image(
):
m = docker_image_regex.match(v)
if m:
return ":".join([m.groups()[0], f"v{finish_version}"])
return ":".join([m.groups()[0], f"v{__rounded_finish_version__}"])
return None

for k, v in config.get("default_images", {}).items():
Expand Down Expand Up @@ -321,8 +330,11 @@ def _version_specific_upgrade(
return config


__rounded_version__ = ".".join([str(c) for c in rounded_ver_parse(__version__)])

# Manually-added upgrade steps must go above this line
if not UpgradeStep.has_step(__version__):
# Always have a way to upgrade to the latest version number, even if no customizations
if not UpgradeStep.has_step(__rounded_version__):
# Always have a way to upgrade to the latest full version number, even if no customizations
# Don't let dev/prerelease versions cloud things
class UpgradeLatest(UpgradeStep):
version = __version__
version = __rounded_version__
23 changes: 23 additions & 0 deletions qhub/version.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,30 @@
"""a backport for the qhub version references"""

import re

try:
from importlib.metadata import distribution
except ModuleNotFoundError:
from importlib_metadata import distribution

__version__ = distribution("qhub").version


def rounded_ver_parse(versionstr):
"""
Take a package version string and return an int tuple of only (major,minor,patch),
ignoring and post/dev etc.

So:
rounded_ver_parse("0.1.2") returns (0,1,2)
rounded_ver_parse("0.1.2.dev65+g2de53174") returns (0,1,2)
rounded_ver_parse("0.1") returns (0,1,0)
"""
m = re.match(
"^(?P<major>[0-9]+)(\\.(?P<minor>[0-9]+)(\\.(?P<patch>[0-9]+))?)?", versionstr
)
assert m is not None
major = int(m.group("major") or 0)
minor = int(m.group("minor") or 0)
patch = int(m.group("patch") or 0)
return (major, minor, patch)
5 changes: 2 additions & 3 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ install_requires =
azure-identity==1.6.1
azure-mgmt-containerservice==16.2.0
boto3
packaging
python-keycloak
importlib_metadata;python_version<"3.8"

Expand Down Expand Up @@ -75,6 +74,6 @@ exclude =
home

[options.packages.find]
exclude =
exclude =
tests
tests_deployment
tests_deployment
14 changes: 9 additions & 5 deletions tests/test_upgrade.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import pytest
from pathlib import Path

from qhub.upgrade import do_upgrade, __version__, load_yaml, verify
import pytest

from qhub.upgrade import do_upgrade, load_yaml, verify
from qhub.version import __version__, rounded_ver_parse


@pytest.fixture
Expand Down Expand Up @@ -48,7 +50,7 @@ def test_upgrade(

assert not Path(tmp_path, "qhub-users-import.json").exists()

# Do the updgrade
# Do the upgrade
if not expect_upgrade_error:
do_upgrade(
tmp_qhub_config, attempt_fixes
Expand All @@ -70,14 +72,16 @@ def test_upgrade(
assert "users" not in config["security"]
assert "groups" not in config["security"]

__rounded_version__ = ".".join([str(c) for c in rounded_ver_parse(__version__)])

# Check image versions have been bumped up
assert (
config["default_images"]["jupyterhub"]
== f"quansight/qhub-jupyterhub:v{__version__}"
== f"quansight/qhub-jupyterhub:v{__rounded_version__}"
)
assert (
config["profiles"]["jupyterlab"][0]["kubespawner_override"]["image"]
== f"quansight/qhub-jupyterlab:v{__version__}"
== f"quansight/qhub-jupyterlab:v{__rounded_version__}"
)

assert (
Expand Down