diff --git a/.pylint-dict b/.pylint-dict index cc95b1f0e6..ef166ecfd2 100644 --- a/.pylint-dict +++ b/.pylint-dict @@ -29,9 +29,10 @@ nginx prebuilt py readonly -rpmspec +renderer repo rpmlint +rpmspec scality skopeo sls diff --git a/.pylintrc b/.pylintrc index 8aeb79b43d..ce7a529903 100644 --- a/.pylintrc +++ b/.pylintrc @@ -42,11 +42,12 @@ confidence= enable=all # Disable the message, report, category or checker with the given ID(s). -disable=abstract-method, # Too many false positive :/ - bad-whitespace, # Alignment is a good thing. - bad-continuation, # I kindly disagree :) - locally-disabled, # Sometime we need to disable a warning locally. - suppressed-message # Don't pollute the output with useless info. +disable=abstract-method, # Too many false positive :/ + bad-whitespace, # Alignment is a good thing. + bad-continuation, # I kindly disagree :) + locally-disabled, # Sometime we need to disable a warning locally. + suppressed-message, # Don't pollute the output with useless info. + useless-return # Disagreement with mypy, see https://github.com/python/mypy/issues/3974 # }}} # Basic {{{ diff --git a/buildchain/buildchain/constants.py b/buildchain/buildchain/constants.py index 6c8f39aada..56c818b119 100644 --- a/buildchain/buildchain/constants.py +++ b/buildchain/buildchain/constants.py @@ -17,11 +17,13 @@ CMD_WIDTH : int = 12 # URLs of the main container repositories. -GOOGLE_REGISTRY : str = 'k8s.gcr.io' -DOCKER_REGISTRY : str = 'docker.io/library' -COREOS_REGISTRY : str = 'quay.io/coreos' -PROMETHEUS_REGISTRY : str = 'quay.io/prometheus' -GRAFANA_REGISTRY : str = 'docker.io/grafana' +CALICO_REPOSITORY : str = 'quay.io/calico' +COREOS_REPOSITORY : str = 'quay.io/coreos' +DOCKER_REPOSITORY : str = 'docker.io/library' +GOOGLE_REPOSITORY : str = 'k8s.gcr.io' +GRAFANA_REPOSITORY : str = 'docker.io/grafana' +INGRESS_REPOSITORY : str = 'quay.io/kubernetes-ingress-controller' +PROMETHEUS_REPOSITORY : str = 'quay.io/prometheus' # Paths {{{ @@ -46,44 +48,7 @@ VAGRANT_SSH_KEY_PAIR : Path = VAGRANT_ROOT/'preshared_key_for_k8s_nodes' # }}} -# Versions {{{ - -K8S_VERSION : str = '1.11.10' -SALT_VERSION : str = '2018.3.4' -KEEPALIVED_VERSION : str = '1.3.5-8.el7_6' -KEEPALIVED_BUILD_ID : int = 1 - -CENTOS_BASE_IMAGE : str = 'docker.io/centos' -CENTOS_BASE_IMAGE_SHA256 : str = \ - '6ae4cddb2b37f889afd576a17a5286b311dcbf10a904409670827f6f9b50065e' - -def load_version_information() -> None: - """Load version information from `VERSION`.""" - to_update = { - 'VERSION_MAJOR', 'VERSION_MINOR', 'VERSION_PATCH', 'VERSION_SUFFIX' - } - with VERSION_FILE.open('r', encoding='utf-8') as fp: - for line in fp: - name, _, value = line.strip().partition('=') - # Don't overwrite random variables by trusting an external file. - var = name.strip() - if var in to_update: - globals()[var] = value.strip() - - -VERSION_FILE : Path= ROOT/'VERSION' - -# Metalk8s version. -# (Those declarations are not mandatory, but they help pylint and mypy). -VERSION_MAJOR : str -VERSION_MINOR : str -VERSION_PATCH : str -VERSION_SUFFIX : str - -load_version_information() - -SHORT_VERSION : str = '{}.{}'.format(VERSION_MAJOR, VERSION_MINOR) -VERSION : str = '{}.{}{}'.format(SHORT_VERSION, VERSION_PATCH, VERSION_SUFFIX) +# Git project information {{{ def git_ref() -> Optional[str]: diff --git a/buildchain/buildchain/docker_command.py b/buildchain/buildchain/docker_command.py index de74b2b581..56e53c71c0 100644 --- a/buildchain/buildchain/docker_command.py +++ b/buildchain/buildchain/docker_command.py @@ -141,13 +141,14 @@ def __init__( @task_error(docker.errors.BuildError, handler=build_error_handler) @task_error(docker.errors.APIError) def __call__(self) -> Optional[TaskError]: - return DOCKER_CLIENT.images.build( + DOCKER_CLIENT.images.build( tag=self.tag, path=self.path, dockerfile=self.dockerfile, buildargs=self.buildargs, forcerm=True, ) + return None class DockerRun: @@ -267,11 +268,12 @@ def expand_config(self) -> Dict[str, Any]: @task_error(docker.errors.APIError) def __call__(self) -> Optional[TaskError]: run_config = self.expand_config() - return DOCKER_CLIENT.containers.run( + DOCKER_CLIENT.containers.run( image=self.builder.tag, command=self.command, **run_config ) + return None class DockerTag: @@ -294,7 +296,8 @@ def __init__(self, repository: str, full_name: str, version: str): @task_error(docker.errors.APIError) def __call__(self) -> Optional[TaskError]: to_tag = DOCKER_CLIENT.images.get(self.full_name) - return to_tag.tag(self.repository, tag=self.version) + to_tag.tag(self.repository, tag=self.version) + return None class DockerPull: @@ -302,19 +305,41 @@ class DockerPull: # pylint: disable=too-few-public-methods """A class to expose the `docker pull` command through the API client.""" - def __init__(self, repository: str, digest: str): + def __init__( + self, repository: str, name: str, version: str, digest: str + ): """Initialize a `docker pull` callable object. + Arguments: repository: the repository to pull from - digest: the digest to pull from the repository + name: the image name to pull from the repository + version: the version of the image to pull + digest: the expected digest of the image to pull """ self.repository = repository + self.name = name + self.version = version self.digest = digest @task_error(docker.errors.BuildError, handler=build_error_handler) @task_error(docker.errors.APIError) def __call__(self) -> Optional[TaskError]: - return DOCKER_CLIENT.images.pull(self.repository, tag=self.digest) + pulled = DOCKER_CLIENT.images.pull( + # For some reason, the repository must include the image name + '{}/{}'.format(self.repository, self.name), + tag=self.version, + ) + + if pulled.id != self.digest: + return TaskError( + "Image {s.name}:{s.version} pulled from {s.repository} " + "doesn't match expected digest: " + "expected {s.digest}, got {observed_digest}".format( + s=self, observed_digest=pulled.id + ) + ) + + return None class DockerSave: @@ -336,7 +361,9 @@ def __init__(self, tag: str, save_path: Path): def __call__(self) -> Optional[TaskError]: to_save = DOCKER_CLIENT.images.get(self.tag) image_stream = to_save.save(named=True) + with self.save_path.open('wb') as image_file: for chunk in image_stream: image_file.write(chunk) - return True + + return None diff --git a/buildchain/buildchain/image.py b/buildchain/buildchain/image.py index 8b18c4fac6..f145bbdbe8 100644 --- a/buildchain/buildchain/image.py +++ b/buildchain/buildchain/image.py @@ -30,7 +30,7 @@ import datetime -from typing import Iterator, Tuple +from typing import Any, Dict, FrozenSet, Iterator, List, Tuple from buildchain import config from buildchain import constants @@ -38,6 +38,7 @@ from buildchain import targets from buildchain import types from buildchain import utils +from buildchain import versions def task_images() -> types.TaskDict: @@ -89,244 +90,141 @@ def show() -> str: ] return task +# Helpers {{{ +def _get_image_info(name: str) -> versions.Image: + """Retrieve an `Image` information by name from the versions listing.""" + try: + return versions.CONTAINER_IMAGES_MAP[name] + except KeyError: + raise ValueError( + 'Missing version for container image "{}"'.format(name) + ) -NGINX_IMAGE_VERSION : str = '1.15.8' -NODE_IMAGE_VERSION : str = '10.16.0' - -# List of container images to pull. -# -# Digests are quite a mouthful, so: -# pylint:disable=line-too-long -TO_PULL : Tuple[targets.RemoteImage, ...] = ( - targets.RemoteImage( - registry=constants.GOOGLE_REGISTRY, - name='addon-resizer', - version='1.8.3', - digest='sha256:07353f7b26327f0d933515a22b1de587b040d3d85c464ea299c1b9f242529326', - destination=constants.ISO_IMAGE_ROOT, - task_dep=['_image_mkdir_root'], - ), - targets.RemoteImage( - registry=constants.PROMETHEUS_REGISTRY, - name='alertmanager', - version='v0.15.2', - digest='sha256:c16294ecb0b6dd77b8a0834c9d98fd9d1090c7ea904786bc37b58ebdb428851f', - destination=constants.ISO_IMAGE_ROOT, - task_dep=['_image_mkdir_root'], - ), - targets.RemoteImage( - registry='calico', - name='calico-node', - remote_name='node', - version='3.8.0', - digest='sha256:6679ccc9f19dba3eb084db991c788dc9661ad3b5d5bafaa3379644229dca6b05', - destination=constants.ISO_IMAGE_ROOT, - task_dep=['_image_mkdir_root'], - ), - targets.RemoteImage( - registry='calico', - name='calico-kube-controllers', - remote_name='kube-controllers', - version='3.8.0', - digest='sha256:cf461efd25ee74d1855e1ee26db98fe87de00293f7d039212adb03c91fececcd', - destination=constants.ISO_IMAGE_ROOT, - task_dep=['_image_mkdir_root'], - ), - targets.RemoteImage( - registry=constants.COREOS_REGISTRY, - name='configmap-reload', - version='v0.0.1', - digest='sha256:e2fd60ff0ae4500a75b80ebaa30e0e7deba9ad107833e8ca53f0047c42c5a057', - destination=constants.ISO_IMAGE_ROOT, - task_dep=['_image_mkdir_root'], - ), - targets.RemoteImage( - registry=constants.GOOGLE_REGISTRY, - name='coredns', - version='1.3.1', - digest='sha256:02382353821b12c21b062c59184e227e001079bb13ebd01f9d3270ba0fcbf1e4', - destination=constants.ISO_IMAGE_ROOT, - task_dep=['_image_mkdir_root'], - ), - targets.RemoteImage( - registry=constants.GOOGLE_REGISTRY, - name='etcd', - version='3.2.18', - digest='sha256:b960569ade5f37205a033dcdc3191fe99dc95b15c6795a6282859070ec2c6124', - destination=constants.ISO_IMAGE_ROOT, - task_dep=['_image_mkdir_root'], - ), - targets.RemoteImage( - registry=constants.GRAFANA_REGISTRY, - name='grafana', - version='5.2.4', - digest='sha256:aaf50da5faf2596bfb0caed81f08b5569110e7b5468b291fedad25d8cbc51f2b', - destination=constants.ISO_IMAGE_ROOT, - task_dep=['_image_mkdir_root'], - ), - targets.RemoteImage( - registry=constants.GOOGLE_REGISTRY, - name='kube-apiserver', - version=constants.K8S_VERSION, - digest='sha256:a6733a3ec08e4a84d5d1492c0fa2833b6d067ea78e37c87fcffc47bd1ab4ed9c', - destination=constants.ISO_IMAGE_ROOT, - task_dep=['_image_mkdir_root'], - ), - targets.RemoteImage( - registry=constants.GOOGLE_REGISTRY, - name='kube-controller-manager', - version=constants.K8S_VERSION, - digest='sha256:f5ddb81466e7467dacc8b6498bdd117ab77fb2fdb0b333c4ebe3e95e5493a661', - destination=constants.ISO_IMAGE_ROOT, - task_dep=['_image_mkdir_root'], - ), - targets.RemoteImage( - registry=constants.GOOGLE_REGISTRY, - name='kube-proxy', - version=constants.K8S_VERSION, - digest='sha256:fd6c29a779b3e30ad4072a1c77aef49f20bd3ea6cbd290c6f47be28ef333bb69', - destination=constants.ISO_IMAGE_ROOT, - task_dep=['_image_mkdir_root'], - ), - targets.RemoteImage( - registry=constants.COREOS_REGISTRY, - name='kube-rbac-proxy', - version='v0.3.1', - digest='sha256:a578315f24e6fd01a65e187e4d1979678598a7d800d039ee5cfe4e11b0b1788d', - destination=constants.ISO_IMAGE_ROOT, - task_dep=['_image_mkdir_root'], - ), - targets.RemoteImage( - registry=constants.GOOGLE_REGISTRY, - name='kube-scheduler', - version=constants.K8S_VERSION, - digest='sha256:119fcd453469b7a3cc644ea7cda992371c5b746ef705dd88d7a8bfefae48b3be', - destination=constants.ISO_IMAGE_ROOT, - task_dep=['_image_mkdir_root'], - ), - targets.RemoteImage( - registry=constants.COREOS_REGISTRY, - name='kube-state-metrics', - version='v1.3.1', - digest='sha256:fa2e6d33183755f924f05744c282386f38e962160f66ad0b6a8a24a36884fb9a', - destination=constants.ISO_IMAGE_ROOT, - task_dep=['_image_mkdir_root'], - ), - targets.RemoteImage( - registry=constants.DOCKER_REGISTRY, - name='nginx', - version=NGINX_IMAGE_VERSION, - digest='sha256:dd2d0ac3fff2f007d99e033b64854be0941e19a2ad51f174d9240dda20d9f534', - destination=constants.ISO_IMAGE_ROOT, - save_as_tar=True, - task_dep=['_image_mkdir_root'], - ), - targets.RemoteImage( - registry=constants.PROMETHEUS_REGISTRY, - name='node-exporter', - version='v0.17.0', - digest='sha256:1b129a3801a0440f9c5b2afb20082dfdb31bf6092b561f5f249531130000cb83', - destination=constants.ISO_IMAGE_ROOT, - task_dep=['_image_mkdir_root'], - ), - targets.RemoteImage( - registry=constants.PROMETHEUS_REGISTRY, - name='prometheus', - version='v2.4.3', - digest='sha256:8e0e85af45fc2bcc18bd7221b8c92fe4bb180f6bd5e30aa2b226f988029c2085', - destination=constants.ISO_IMAGE_ROOT, - task_dep=['_image_mkdir_root'], - ), - targets.RemoteImage( - registry=constants.COREOS_REGISTRY, - name='prometheus-config-reloader', - version='v0.23.2', - digest='sha256:df1453c7c69e4f2ab8a86fc18fe3b890ce2f80fed6d6519dc9d33927451b214d', - destination=constants.ISO_IMAGE_ROOT, - task_dep=['_image_mkdir_root'], - ), - targets.RemoteImage( - registry=constants.COREOS_REGISTRY, - name='prometheus-operator', - version='v0.23.2', - digest='sha256:8211b3eb30cb8591ddf536f1cf62100f5c97659c14d18dd45001acf94dafd713', - destination=constants.ISO_IMAGE_ROOT, - task_dep=['_image_mkdir_root'], - ), - targets.RemoteImage( - registry='quay.io/kubernetes-ingress-controller', - name='nginx-ingress-controller', - version='0.25.0', - digest='sha256:464db4880861bd9d1e74e67a4a9c975a6e74c1e9968776d8d4cc73492a56dfa5', - destination=constants.ISO_IMAGE_ROOT, - task_dep=['_image_mkdir_root'], - ), - targets.RemoteImage( - registry=constants.GOOGLE_REGISTRY, - remote_name='defaultbackend-amd64', - name='nginx-ingress-defaultbackend-amd64', - version='1.5', - digest='sha256:4dc5e07c8ca4e23bddb3153737d7b8c556e5fb2f29c4558b7cd6e6df99c512c7', - destination=constants.ISO_IMAGE_ROOT, - task_dep=['_image_mkdir_root'], - ), -) - - -# The build ID is to be augmented whenever we rebuild the `salt-master` image, -# e.g. because the `Dockerfile` is changed, or one of the dependencies installed -# in the image needs to be updated. -# This should be reset to 1 when SALT_VERSION is changed. -SALT_MASTER_BUILD_ID : int = 1 - -KEEPALIVED_IMAGE_TAG = '{}-{}'.format( - constants.KEEPALIVED_VERSION, - constants.KEEPALIVED_BUILD_ID, -) - -# List of container images to build. +def _remote_image( + name: str, repository: str, **overrides: Any +) -> targets.RemoteImage: + """Build a `RemoteImage` from a name and a repository. + + Provides sane defaults, relies on the `REMOTE_NAMES` and `SAVE_AS_TAR` + constants to add some arguments. + """ + overrides.setdefault('destination', constants.ISO_IMAGE_ROOT) + overrides.setdefault('task_dep', ['_image_mkdir_root']) + + image_info = _get_image_info(name) + kwargs = dict(image_info._asdict(), repository=repository, **overrides) + + if name in REMOTE_NAMES: + kwargs['remote_name'] = REMOTE_NAMES[name] + + if name in SAVE_AS_TAR: + kwargs['save_as_tar'] = True + + return targets.RemoteImage(**kwargs) + +def _local_image(name: str, **kwargs: Any) -> targets.LocalImage: + """Build a `LocalImage` from its name, with sane defaults. + + Build-specific information is left to the caller, as each image will + require its own set of particularities. + """ + image_info = _get_image_info(name) + + kwargs.setdefault('destination', constants.ISO_IMAGE_ROOT) + kwargs.setdefault('dockerfile', constants.ROOT/'images'/name/'Dockerfile') + kwargs.setdefault('save_on_disk', True) + kwargs.setdefault('task_dep', ['_image_mkdir_root']) + + return targets.LocalImage( + name=name, + version=image_info.version, + **kwargs, + ) + +# }}} +# Container images to pull {{{ +TO_PULL : List[targets.RemoteImage] = [] + +IMGS_PER_REPOSITORY : Dict[str, List[str]] = { + constants.CALICO_REPOSITORY: [ + 'calico-node', + 'calico-kube-controllers', + ], + constants.COREOS_REPOSITORY: [ + 'configmap-reload', + 'kube-rbac-proxy', + 'kube-state-metrics', + 'prometheus-config-reloader', + 'prometheus-operator', + ], + constants.DOCKER_REPOSITORY: [ + 'nginx', + ], + constants.GOOGLE_REPOSITORY: [ + 'addon-resizer', + 'coredns', + 'etcd', + 'kube-apiserver', + 'kube-controller-manager', + 'kube-proxy', + 'kube-scheduler', + 'nginx-ingress-defaultbackend-amd64', + ], + constants.GRAFANA_REPOSITORY: [ + 'grafana', + ], + constants.INGRESS_REPOSITORY: [ + 'nginx-ingress-controller', + ], + constants.PROMETHEUS_REPOSITORY: [ + 'alertmanager', + 'node-exporter', + 'prometheus', + ], +} + +REMOTE_NAMES : Dict[str, str] = { + 'calico-node': 'node', + 'calico-kube-controllers': 'kube-controllers', + 'nginx-ingress-defaultbackend-amd64': 'defaultbackend-amd64', +} + +SAVE_AS_TAR : FrozenSet[str] = frozenset(('nginx', 'pause')) + +for repo, images in IMGS_PER_REPOSITORY.items(): + for image_name in images: + TO_PULL.append(_remote_image(image_name, repo)) + +# }}} +# Container images to build {{{ TO_BUILD : Tuple[targets.LocalImage, ...] = ( - targets.LocalImage( + _local_image( name='salt-master', - version='{}-{}'.format(constants.SALT_VERSION, SALT_MASTER_BUILD_ID), - dockerfile=constants.ROOT/'images'/'salt-master'/'Dockerfile', - destination=constants.ISO_IMAGE_ROOT, - save_on_disk=True, - build_args={'SALT_VERSION': constants.SALT_VERSION}, - task_dep=['_image_mkdir_root'], + build_args={'SALT_VERSION': versions.SALT_VERSION}, file_dep=[constants.ROOT/'images'/'salt-master'/'source.key'], ), - targets.LocalImage( + _local_image( name='keepalived', - version=KEEPALIVED_IMAGE_TAG, - dockerfile=constants.ROOT/'images'/'keepalived'/'Dockerfile', - destination=constants.ISO_IMAGE_ROOT, - save_on_disk=True, build_args={ - 'KEEPALIVED_IMAGE_SHA256': constants.CENTOS_BASE_IMAGE_SHA256, - 'KEEPALIVED_IMAGE': constants.CENTOS_BASE_IMAGE, - 'KEEPALIVED_VERSION': constants.KEEPALIVED_VERSION, + 'KEEPALIVED_IMAGE': versions.CENTOS_BASE_IMAGE, + 'KEEPALIVED_IMAGE_SHA256': versions.CENTOS_BASE_IMAGE_SHA256, + 'KEEPALIVED_VERSION': versions.KEEPALIVED_VERSION, 'BUILD_DATE': datetime.datetime.now(datetime.timezone.utc) .astimezone() .isoformat(), 'VCS_REF': constants.GIT_REF or '', - 'VERSION': KEEPALIVED_IMAGE_TAG, - 'METALK8S_VERSION': constants.VERSION, + 'VERSION': versions.CONTAINER_IMAGES_MAP['keepalived'].version, + 'METALK8S_VERSION': versions.VERSION, }, - task_dep=['_image_mkdir_root'], file_dep=[constants.ROOT/'images'/'keepalived'/'entrypoint.sh'], ), - targets.LocalImage( + _local_image( name='metalk8s-ui', - version=constants.VERSION, dockerfile=constants.ROOT/'ui'/'Dockerfile', - destination=constants.ISO_IMAGE_ROOT, - save_on_disk=True, build_args={ - 'NGINX_IMAGE_VERSION': NGINX_IMAGE_VERSION, - 'NODE_IMAGE_VERSION': NODE_IMAGE_VERSION + 'NGINX_IMAGE_VERSION': versions.NGINX_IMAGE_VERSION, + 'NODE_IMAGE_VERSION': versions.NODEJS_IMAGE_VERSION, }, - task_dep=['_image_mkdir_root'], file_dep=( list(coreutils.ls_files_rec(constants.ROOT/'ui'/'public')) + list(coreutils.ls_files_rec(constants.ROOT/'ui'/'src')) + @@ -337,24 +235,20 @@ def show() -> str: ] ) ), - targets.LocalImage( + _local_image( name='metalk8s-utils', - version=constants.VERSION, - dockerfile=constants.ROOT/'images'/'metalk8s-utils'/'Dockerfile', - destination=constants.ISO_IMAGE_ROOT, - save_on_disk=True, build_args={ - 'BASE_IMAGE': constants.CENTOS_BASE_IMAGE, - 'BASE_IMAGE_SHA256': constants.CENTOS_BASE_IMAGE_SHA256, + 'BASE_IMAGE': versions.CENTOS_BASE_IMAGE, + 'BASE_IMAGE_SHA256': versions.CENTOS_BASE_IMAGE_SHA256, 'BUILD_DATE': datetime.datetime.now(datetime.timezone.utc) .astimezone() .isoformat(), 'VCS_REF': constants.GIT_REF or '', - 'METALK8S_VERSION': constants.VERSION, + 'METALK8S_VERSION': versions.VERSION, }, - task_dep=['_image_mkdir_root'], ), ) +# }}} __all__ = utils.export_only_tasks(__name__) diff --git a/buildchain/buildchain/iso.py b/buildchain/buildchain/iso.py index 4c13b1d79e..7aadfa6321 100644 --- a/buildchain/buildchain/iso.py +++ b/buildchain/buildchain/iso.py @@ -40,6 +40,7 @@ from buildchain import targets as helper from buildchain import types from buildchain import utils +from buildchain import versions ISO_FILE : Path = config.BUILD_ROOT/'{}.iso'.format(config.PROJECT_NAME.lower()) @@ -148,8 +149,8 @@ def task__iso_render_bootstrap() -> types.TaskDict: return helper.TemplateFile( source=constants.ROOT/'scripts'/'bootstrap.sh.in', destination=constants.ISO_ROOT/'bootstrap.sh', - context={'VERSION': constants.VERSION}, - file_dep=[constants.VERSION_FILE], + context={'VERSION': versions.VERSION}, + file_dep=[versions.VERSION_FILE], task_dep=['_iso_mkdir_root'], ).task @@ -158,11 +159,11 @@ def task__iso_generate_product_txt() -> types.TaskDict: """Generate the product.txt file.""" def action(targets: Sequence[str]) -> None: datefmt = "%Y-%m-%dT%H:%M:%SZ" - dev_release = '1' if constants.VERSION_SUFFIX == '-dev' else '0' + dev_release = '1' if versions.VERSION_SUFFIX == '-dev' else '0' info = ( ('NAME', config.PROJECT_NAME), - ('VERSION', constants.VERSION), - ('SHORT_VERSION', constants.SHORT_VERSION), + ('VERSION', versions.VERSION), + ('SHORT_VERSION', versions.SHORT_VERSION), ('GIT', constants.GIT_REF or ''), ('DEVELOPMENT_RELEASE', dev_release), ('BUILD_TIMESTAMP', dt.datetime.utcnow().strftime(datefmt)), @@ -177,7 +178,7 @@ def action(targets: Sequence[str]) -> None: 'title': lambda task: utils.title_with_target1('GENERATE', task), 'actions': [action], 'targets': [constants.ISO_ROOT/'product.txt'], - 'file_dep': [constants.VERSION_FILE], + 'file_dep': [versions.VERSION_FILE], 'task_dep': ['_iso_mkdir_root'], 'uptodate': [False], # False because we include the build timestamp. 'clean': True, @@ -194,7 +195,7 @@ def task__iso_build() -> types.TaskDict: '-joliet', '-joliet-long', '-full-iso9660-filenames', - '-volid', '{} {}'.format(config.PROJECT_NAME, constants.VERSION), + '-volid', '{} {}'.format(config.PROJECT_NAME, versions.VERSION), '--iso-level', '3', '-gid', '0', '-uid', '0', @@ -207,7 +208,7 @@ def task__iso_build() -> types.TaskDict: ) # Every file used for the ISO is a dependency. depends = list(coreutils.ls_files_rec(constants.ISO_ROOT)) - depends.append(constants.VERSION_FILE) + depends.append(versions.VERSION_FILE) return { 'title': lambda task: utils.title_with_target1('MKISOFS', task), 'doc': doc, diff --git a/buildchain/buildchain/packaging.py b/buildchain/buildchain/packaging.py index 25d9afcd8f..27f2dc7288 100644 --- a/buildchain/buildchain/packaging.py +++ b/buildchain/buildchain/packaging.py @@ -27,15 +27,18 @@ from pathlib import Path -from typing import Dict, Iterator, List, Tuple +from typing import Dict, FrozenSet, Iterator, List, Tuple + +from doit.tools import config_changed # type: ignore from buildchain import config from buildchain import constants from buildchain import coreutils +from buildchain import docker_command from buildchain import targets from buildchain import types from buildchain import utils -from buildchain import docker_command +from buildchain import versions def task_packaging() -> types.TaskDict: @@ -87,9 +90,6 @@ def clean() -> None: continue coreutils.rm_rf(repository.rootdir) - pkg_list = constants.ROOT/'packages/packages.list' - packages = _load_package_list(pkg_list) - mounts = [ docker_command.bind_mount( source=constants.PKG_ROOT, target=Path('/install_root') @@ -99,7 +99,7 @@ def clean() -> None: ), ] dl_packages_callable = docker_command.DockerRun( - command=['/entrypoint.sh', 'download_packages', *packages], + command=['/entrypoint.sh', 'download_packages', *TO_DOWNLOAD], builder=BUILDER, mounts=mounts, environment={'RELEASEVER': 7} @@ -108,12 +108,11 @@ def clean() -> None: 'title': lambda task: utils.title_with_target1('GET PKGS', task), 'actions': [dl_packages_callable], 'targets': [constants.PKG_ROOT/'var'], - 'file_dep': [pkg_list], 'task_dep': [ '_package_mkdir_root', '_package_mkdir_iso_root', '_build_container' ], 'clean': [clean], - 'uptodate': [True], + 'uptodate': [config_changed(_TO_DOWNLOAD_CONFIG)], # Prevent Docker from polluting our output. 'verbosity': 0, } @@ -121,7 +120,7 @@ def clean() -> None: def task__build_packages() -> Iterator[types.TaskDict]: """Build a package.""" - for repo_pkgs in PACKAGES.values(): + for repo_pkgs in TO_BUILD.values(): for package in repo_pkgs: yield from package.execution_plan @@ -142,52 +141,86 @@ def task__build_repositories() -> Iterator[types.TaskDict]: task_dep=['_build_root'], file_dep=[ constants.ROOT/'packages/yum_repositories/kubernetes.repo', - constants.ROOT/'packages/yum_repositories/saltstack.repo' + constants.ROOT/'packages/yum_repositories/saltstack.repo', ], + build_args={ + # Used to template the SaltStack repository definition + 'SALT_VERSION': versions.SALT_VERSION, + }, ) +# Packages to build, per repository. +def _package(name: str, sources: List[Path]) -> targets.Package: + try: + pkg_info = versions.PACKAGES_MAP[name] + except KeyError: + raise ValueError( + 'Missing version for package "{}"'.format(name) + ) + + # In case the `release` is of form "{build_id}.{os}", which is standard + build_id_str, _, _ = pkg_info.release.partition('.') + + return targets.Package( + basename='_build_packages', + name=name, + version=pkg_info.version, + build_id=int(build_id_str), + sources=sources, + builder=BUILDER, + task_dep=['_package_mkdir_root', '_build_container'], + ) -CALICO_CNI_PLUGIN_VERSION : str = '3.8.0' -# Packages per repository. -PACKAGES : Dict[str, Tuple[targets.Package, ...]] = { +TO_BUILD : Dict[str, Tuple[targets.Package, ...]] = { 'scality': ( # SOS report custom plugins. - targets.Package( - basename='_build_packages', + _package( name='metalk8s-sosreport', - version=constants.SHORT_VERSION, - build_id=1, sources=[ Path('metalk8s.py'), Path('containerd.py'), ], - builder=BUILDER, - task_dep=['_package_mkdir_root', '_build_container'], ), # Calico Container Network Interface Plugin. - targets.Package( - basename='_build_packages', + _package( name='calico-cni-plugin', - version=CALICO_CNI_PLUGIN_VERSION, - build_id=1, sources=[ Path('calico-amd64'), Path('calico-ipam-amd64'), - Path('v{}.tar.gz'.format(CALICO_CNI_PLUGIN_VERSION)), + Path('v{}.tar.gz'.format(versions.CALICO_VERSION)), ], - builder=BUILDER, - task_dep=['_package_mkdir_root', '_build_container'], ), ), } +_TO_BUILD_PKG_NAMES : List[str] = [] + +for pkgs in TO_BUILD.values(): + for pkg in pkgs: + _TO_BUILD_PKG_NAMES.append(pkg.name) + +# All packages not referenced in `TO_BUILD` but listed in `versions.PACKAGES` +# are supposed to be downloaded. +TO_DOWNLOAD : FrozenSet[str] = frozenset( + "{p.name}-{p.version}-{p.release}".format(p=package) + for package in versions.PACKAGES + if package.name not in _TO_BUILD_PKG_NAMES +) + +# Store these versions in a dict to use with doit.tools.config_changed +_TO_DOWNLOAD_CONFIG : Dict[str, str] = { + pkg.name: "{p.version}-{p.release}".format(p=pkg) + for pkg in versions.PACKAGES + if pkg.name not in _TO_BUILD_PKG_NAMES +} + REPOSITORIES : Tuple[targets.Repository, ...] = ( targets.Repository( basename='_build_repositories', name='scality', builder=BUILDER, - packages=PACKAGES['scality'], + packages=TO_BUILD['scality'], task_dep=['_package_mkdir_iso_root'], ), targets.Repository( @@ -229,20 +262,4 @@ def task__build_repositories() -> Iterator[types.TaskDict]: ) -def _load_package_list(pkg_list: Path) -> List[str]: - """Load the list of packages to download. - - Arguments: - pkg_list: path to the file that contains the package list - - Returns: - A list of package names. - """ - packages : List[str] = [] - with pkg_list.open('r', encoding='utf-8') as fp: - for line in fp: - packages.append(line.strip()) - return packages - - __all__ = utils.export_only_tasks(__name__) diff --git a/buildchain/buildchain/salt_tree.py b/buildchain/buildchain/salt_tree.py index b115536b03..b7637c3e8f 100644 --- a/buildchain/buildchain/salt_tree.py +++ b/buildchain/buildchain/salt_tree.py @@ -29,15 +29,17 @@ import importlib -import sys from pathlib import Path +import sys from typing import Any, Iterator, Tuple, Union from buildchain import config from buildchain import constants from buildchain import targets -from buildchain import utils from buildchain import types +from buildchain import utils +from buildchain import versions +from buildchain.targets.serialize import Renderer sys.path.append(str(constants.STATIC_CONTAINER_REGISTRY)) container_registry : Any = importlib.import_module('static-container-registry') @@ -163,8 +165,8 @@ def _run(self) -> None: task_name='top.sls', source=constants.ROOT/'pillar'/'top.sls.in', destination=constants.ISO_ROOT/'pillar'/'top.sls', - context={'VERSION': constants.VERSION}, - file_dep=[constants.VERSION_FILE], + context={'VERSION': versions.VERSION}, + file_dep=[versions.VERSION_FILE], ), ) @@ -175,8 +177,26 @@ def _run(self) -> None: task_name='top.sls', source=constants.ROOT/'salt'/'top.sls.in', destination=constants.ISO_ROOT/'salt'/'top.sls', - context={'VERSION': constants.VERSION}, - file_dep=[constants.VERSION_FILE], + context={'VERSION': versions.VERSION}, + file_dep=[versions.VERSION_FILE], + ), + + targets.SerializedData( + task_name='versions.json', + destination=constants.ISO_ROOT/'salt'/'metalk8s'/'versions.json', + data={ + 'kubernetes': {'version': versions.K8S_VERSION}, + 'packages': { + pkg.name: {'version': "{}-{}".format(pkg.version, pkg.release)} + for pkg in versions.PACKAGES + }, + 'images': { + img.name: {'version': img.version} + for img in versions.CONTAINER_IMAGES + }, + 'metalk8s': {'version': versions.VERSION}, + }, + renderer=Renderer.JSON, ), Path('salt/metalk8s/addons/monitoring/alertmanager/deployed.sls'), @@ -197,19 +217,7 @@ def _run(self) -> None: Path('salt/metalk8s/addons/monitoring/prometheus-operator/upstream.sls'), Path('salt/metalk8s/addons/ui/deployed.sls'), - targets.TemplateFile( - task_name='metalk8s-ui-deployment', - source=constants.ROOT.joinpath( - 'salt', 'metalk8s', 'addons', 'ui', 'files', - 'metalk8s-ui-deployment.yaml.in' - ), - destination=constants.ISO_ROOT.joinpath( - 'salt', 'metalk8s', 'addons', 'ui', 'files', - 'metalk8s-ui-deployment.yaml' - ), - context={'VERSION': constants.VERSION}, - file_dep=[constants.VERSION_FILE], - ), + Path('salt/metalk8s/addons/ui/files/metalk8s-ui-deployment.yaml'), Path('salt/metalk8s/addons/ui/precheck.sls'), @@ -312,8 +320,8 @@ def _run(self) -> None: 'salt', 'metalk8s', 'kubernetes', 'mark-control-plane', 'deployed.sls' ), - context={'VERSION': constants.VERSION}, - file_dep=[constants.VERSION_FILE], + context={'VERSION': versions.VERSION}, + file_dep=[versions.VERSION_FILE], ), Path('salt/metalk8s/kubernetes/sa/advertised.sls'), @@ -419,14 +427,16 @@ def _run(self) -> None: Path('salt/_utils/pillar_utils.py'), + # This image is defined here and not in the `image` module since it is + # saved into the `salt/` tree. targets.RemoteImage( - registry=constants.GOOGLE_REGISTRY, name='pause', - version='3.1', + version=versions.CONTAINER_IMAGES_MAP['pause'].version, + digest=versions.CONTAINER_IMAGES_MAP['pause'].digest, + repository=constants.GOOGLE_REPOSITORY, + save_as_tar=True, # pylint:disable=line-too-long - digest='sha256:f78411e19d84a252e53bff71a4407a5686c46983a2c2eeed83929b888179acea', destination=constants.ISO_ROOT/'salt/metalk8s/container-engine/containerd/files', - save_as_tar=True, ), CommonStaticContainerRegistry( @@ -440,10 +450,10 @@ def _run(self) -> None: root=constants.ISO_IMAGE_ROOT, server_root='${}_{}_images'.format( config.PROJECT_NAME.lower(), - constants.VERSION.replace('.', '_').replace('-', '_') + versions.VERSION.replace('.', '_').replace('-', '_') ), name_prefix='{}-{}/'.format( - config.PROJECT_NAME.lower(), constants.VERSION + config.PROJECT_NAME.lower(), versions.VERSION ), destination=Path( constants.ISO_ROOT, diff --git a/buildchain/buildchain/targets/__init__.py b/buildchain/buildchain/targets/__init__.py index 0e9dc0cbba..14f7026f93 100644 --- a/buildchain/buildchain/targets/__init__.py +++ b/buildchain/buildchain/targets/__init__.py @@ -12,4 +12,5 @@ from buildchain.targets.package import Package from buildchain.targets.remote_image import RemoteImage from buildchain.targets.repository import Repository +from buildchain.targets.serialize import SerializedData from buildchain.targets.template import TemplateFile diff --git a/buildchain/buildchain/targets/remote_image.py b/buildchain/buildchain/targets/remote_image.py index 919624d9f5..68956f9893 100644 --- a/buildchain/buildchain/targets/remote_image.py +++ b/buildchain/buildchain/targets/remote_image.py @@ -3,7 +3,7 @@ """Provides container image retrieval. -The images are downloaded from a registry. +The images are downloaded from a repository. Then, they are tagged, saved on the disk and optionally compressed. All of these actions are done by a single task. @@ -11,7 +11,6 @@ import operator -import os from pathlib import Path from typing import Any, Optional, List @@ -27,7 +26,7 @@ class RemoteImage(image.ContainerImage): def __init__( self, - registry: str, + repository: str, name: str, version: str, digest: str, @@ -39,7 +38,7 @@ def __init__( """Initialize a remote container image. Arguments: - registry: registry where the image is + repository: repository where the image is stored name: image name version: image version digest: image digest @@ -50,7 +49,7 @@ def __init__( Keyword Arguments: They are passed to `Target` init method. """ - self._registry = registry + self._repository = repository self._digest = digest self._remote_name = remote_name or name self._use_tar = save_as_tar @@ -62,47 +61,28 @@ def __init__( ) self._targets = [self.filepath] - registry = property(operator.attrgetter('_registry')) + repository = property(operator.attrgetter('_repository')) digest = property(operator.attrgetter('_digest')) @property - def fullname(self) -> str: - """Complete image name. - - Usable by `docker` commands. - """ - return '{obj.registry}/{obj._remote_name}@{obj.digest}'.format( - obj=self - ) - - @property - def basicname(self) -> str: - """Base image name (no digest). - - Usable by `docker` commands. - """ - return '{obj.registry}/{obj._remote_name}:{obj.version}'.format( - obj=self - ) - + def remote_fullname(self) -> str: + """Complete image name retrieved from the remote repository.""" + return ( + "{img.repository}/{img._remote_name}:{img.version}" + ).format(img=self) @property - def repository(self) -> str: - """Base image name (no digest). - - Usable by `docker` commands. - """ - return '{obj.registry}/{obj._remote_name}'.format( - obj=self - ) + def fullname(self) -> str: + """Complete image name to use as a tag before saving with Docker.""" + return "{img.repository}/{img.tag}".format(img=self) @property def filepath(self) -> Path: """Path to the file tracked on disk.""" if self._use_tar: - return self.dest_dir/'{obj.name}-{obj.version}{ext}'.format( - obj=self, ext='.tar' + return self.dest_dir/'{img.name}-{img.version}{ext}'.format( + img=self, ext='.tar' ) # Just to keep track of something on disk. return self.dirname/'manifest.json' @@ -115,31 +95,34 @@ def task(self) -> types.TaskDict: 'doc': 'Download {} container image.'.format(self.name), 'uptodate': [True], }) - docker_pull = docker_command.DockerPull( - self.repository, - self.digest - ) - docker_tag = docker_command.DockerTag( - self.repository, - self.fullname, - self.version - ) - docker_save = docker_command.DockerSave( - self.basicname, - Path(os.path.join( - self.dest_dir, - '{}-{}.tar'.format(self.name, self.version) - )) - ) + if self._use_tar: - task.update({ - 'actions': [docker_pull, docker_tag, docker_save], - }) + # Use Docker to pull, tag, then save the image + task['actions'] = [ + docker_command.DockerPull( + self.repository, + self._remote_name, + self.version, + self.digest, + ), + docker_command.DockerTag( + '{img.repository}/{img.name}'.format(img=self), + self.remote_fullname, + self.version, + ), + docker_command.DockerSave( + self.fullname, + self.filepath, + ) + ] else: + # Use Skopeo to directly copy the remote image into a directory + # of image layers task.update({ 'actions': [self.mkdirs, self._skopeo_copy()], 'clean': [self.clean], }) + return task def _skopeo_copy(self) -> List[str]: @@ -148,14 +131,6 @@ def _skopeo_copy(self) -> List[str]: config.ExtCommand.SKOPEO.value, '--override-os', 'linux', '--insecure-policy', 'copy', '--format', 'v2s2' ] - if not self._use_tar: - cmd.append('--dest-compress') - cmd.append('docker://{}'.format(self.fullname)) - cmd.append(self._skopeo_dest()) + cmd.append('docker://{}'.format(self.remote_fullname)) + cmd.append('dir:{}'.format(self.dirname)) return cmd - - def _skopeo_dest(self) -> str: - """Return the destination, formatted for skopeo copy.""" - if self._use_tar: - return 'docker-archive:{}'.format(self.filepath) - return 'dir:{}'.format(self.dirname) diff --git a/buildchain/buildchain/targets/serialize.py b/buildchain/buildchain/targets/serialize.py new file mode 100644 index 0000000000..d013b21f9f --- /dev/null +++ b/buildchain/buildchain/targets/serialize.py @@ -0,0 +1,89 @@ +# coding: utf-8 + +"""Targets to write files from Python objects.""" + +import enum +import json +from pathlib import Path +from typing import Any, Callable + +from buildchain import types +from buildchain import utils + +from . import base + + +def render_json(obj: Any, filepath: Path) -> None: + """Serialize an object as JSON to a given file path.""" + with filepath.open('w', encoding='utf-8') as file_obj: + json.dump(obj, file_obj, sort_keys=True, indent=2) + + +class Renderer(enum.Enum): + """Supported rendering methods for `SerializedData` targets.""" + JSON = 'JSON' + + +class SerializedData(base.AtomicTarget): + """Serialize an object into a file with a specific renderer.""" + + RENDERERS = { + Renderer.JSON: render_json, + } + + def __init__( + self, + data: Any, + destination: Path, + renderer: Renderer = Renderer.JSON, + **kwargs: Any + ): + """Configure a file rendering task. + + Arguments: + data: object to render into a file + destination: path to the rendered file + + Keyword Arguments: + They are passed to `Target` init method + """ + kwargs['targets'] = [destination] + super().__init__(**kwargs) + + self._data = data + self._dest = destination + + if not isinstance(renderer, Renderer): + raise ValueError( + 'Invalid `renderer`: {!r}. Must be one of: {}'.format( + renderer, ', '.join(map(repr, Renderer)) + ) + ) + + self._renderer = renderer + + @property + def task(self) -> types.TaskDict: + def title(task: types.TaskDict) -> str: + return utils.title_with_target1( + 'RENDER {}'.format(self._renderer), + task + ) + + task = self.basic_task + task.update({ + 'title': title, + 'doc': 'Render file "{}" with "{}"'.format( + self._dest, self._renderer + ), + 'actions': [self._run], + }) + return task + + @property + def _render(self) -> Callable[[Any, Path], None]: + return self.RENDERERS[self._renderer] + + def _run(self) -> None: + """Render the file.""" + self._render(self._data, self._dest) diff --git a/buildchain/buildchain/versions.py b/buildchain/buildchain/versions.py new file mode 100644 index 0000000000..fa4af4c702 --- /dev/null +++ b/buildchain/buildchain/versions.py @@ -0,0 +1,345 @@ +# coding: utf-8 + + +"""Authoritative listing of image and package versions used in the project. + +This module MUST be kept valid in a standalone context, since it is intended +for use in tests and documentation as well. +""" + +from collections import namedtuple +from pathlib import Path +from typing import Tuple + + +Image = namedtuple('Image', ('name', 'version', 'digest')) +Package = namedtuple('Package', ('name', 'version', 'release')) + +# Project-wide versions {{{ + +CALICO_VERSION : str = '3.8.0' +K8S_VERSION : str = '1.11.10' +KEEPALIVED_VERSION : str = '1.3.5-8.el7_6' +SALT_VERSION : str = '2018.3.4' + +def load_version_information() -> None: + """Load version information from `VERSION`.""" + to_update = { + 'VERSION_MAJOR', 'VERSION_MINOR', 'VERSION_PATCH', 'VERSION_SUFFIX' + } + with VERSION_FILE.open('r', encoding='utf-8') as fp: + for line in fp: + name, _, value = line.strip().partition('=') + # Don't overwrite random variables by trusting an external file. + var = name.strip() + if var in to_update: + globals()[var] = value.strip() + + +VERSION_FILE = (Path(__file__)/'../../../VERSION').resolve() + +# Metalk8s version. +# (Those declarations are not mandatory, but they help pylint and mypy). +VERSION_MAJOR : str +VERSION_MINOR : str +VERSION_PATCH : str +VERSION_SUFFIX : str + +load_version_information() + +SHORT_VERSION : str = '{}.{}'.format(VERSION_MAJOR, VERSION_MINOR) +VERSION : str = '{}.{}{}'.format(SHORT_VERSION, VERSION_PATCH, VERSION_SUFFIX) + + +# }}} +# Container images {{{ + +CENTOS_BASE_IMAGE : str = 'docker.io/centos' +CENTOS_BASE_IMAGE_SHA256 : str = \ + '6ae4cddb2b37f889afd576a17a5286b311dcbf10a904409670827f6f9b50065e' + +NGINX_IMAGE_VERSION : str = '1.15.8' +NODEJS_IMAGE_VERSION : str = '10.16.0' + +# Current build IDs, to be augmented whenever we rebuild the corresponding +# image, e.g. because the `Dockerfile` is changed, or one of the dependencies +# installed in the image needs to be updated. +# This should be reset to 1 when the service exposed by the container changes +# version. +SALT_MASTER_BUILD_ID = 1 +KEEPALIVED_BUILD_ID = 1 + + +def _version_prefix(version: str, prefix: str = 'v') -> str: + return "{}{}".format(prefix, version) + + +# Digests are quite a mouthful, so: +# pylint:disable=line-too-long +CONTAINER_IMAGES : Tuple[Image, ...] = ( + # Remote images + Image( + name='addon-resizer', + version='1.8.3', + digest='sha256:07353f7b26327f0d933515a22b1de587b040d3d85c464ea299c1b9f242529326', + ), + Image( + name='alertmanager', + version='v0.15.2', + digest='sha256:c16294ecb0b6dd77b8a0834c9d98fd9d1090c7ea904786bc37b58ebdb428851f', + ), + Image( + name='calico-node', + version=_version_prefix(CALICO_VERSION), + digest='sha256:6679ccc9f19dba3eb084db991c788dc9661ad3b5d5bafaa3379644229dca6b05', + ), + Image( + name='calico-kube-controllers', + version=_version_prefix(CALICO_VERSION), + digest='sha256:cf461efd25ee74d1855e1ee26db98fe87de00293f7d039212adb03c91fececcd', + ), + Image( + name='configmap-reload', + version='v0.0.1', + digest='sha256:e2fd60ff0ae4500a75b80ebaa30e0e7deba9ad107833e8ca53f0047c42c5a057', + ), + Image( + name='coredns', + version='1.3.1', + digest='sha256:02382353821b12c21b062c59184e227e001079bb13ebd01f9d3270ba0fcbf1e4', + ), + Image( + name='etcd', + version='3.2.18', + digest='sha256:b960569ade5f37205a033dcdc3191fe99dc95b15c6795a6282859070ec2c6124', + ), + Image( + name='grafana', + version='5.2.4', + digest='sha256:aaf50da5faf2596bfb0caed81f08b5569110e7b5468b291fedad25d8cbc51f2b', + ), + Image( + name='kube-apiserver', + version=_version_prefix(K8S_VERSION), + digest='sha256:a6733a3ec08e4a84d5d1492c0fa2833b6d067ea78e37c87fcffc47bd1ab4ed9c', + ), + Image( + name='kube-controller-manager', + version=_version_prefix(K8S_VERSION), + digest='sha256:f5ddb81466e7467dacc8b6498bdd117ab77fb2fdb0b333c4ebe3e95e5493a661', + ), + Image( + name='kube-proxy', + version=_version_prefix(K8S_VERSION), + digest='sha256:fd6c29a779b3e30ad4072a1c77aef49f20bd3ea6cbd290c6f47be28ef333bb69', + ), + Image( + name='kube-rbac-proxy', + version='v0.3.1', + digest='sha256:a578315f24e6fd01a65e187e4d1979678598a7d800d039ee5cfe4e11b0b1788d', + ), + Image( + name='kube-scheduler', + version=_version_prefix(K8S_VERSION), + digest='sha256:119fcd453469b7a3cc644ea7cda992371c5b746ef705dd88d7a8bfefae48b3be', + ), + Image( + name='kube-state-metrics', + version='v1.3.1', + digest='sha256:fa2e6d33183755f924f05744c282386f38e962160f66ad0b6a8a24a36884fb9a', + ), + Image( + name='nginx', + version=NGINX_IMAGE_VERSION, + digest='sha256:dd2d0ac3fff2f007d99e033b64854be0941e19a2ad51f174d9240dda20d9f534', + ), + Image( + name='nginx-ingress-controller', + version='0.25.0', + digest='sha256:464db4880861bd9d1e74e67a4a9c975a6e74c1e9968776d8d4cc73492a56dfa5', + ), + Image( + name='nginx-ingress-defaultbackend-amd64', + version='1.5', + digest='sha256:4dc5e07c8ca4e23bddb3153737d7b8c556e5fb2f29c4558b7cd6e6df99c512c7', + ), + Image( + name='node-exporter', + version='v0.17.0', + digest='sha256:1b129a3801a0440f9c5b2afb20082dfdb31bf6092b561f5f249531130000cb83', + ), + Image( + name='pause', + version='3.1', + digest='sha256:f78411e19d84a252e53bff71a4407a5686c46983a2c2eeed83929b888179acea', + ), + Image( + name='prometheus', + version='v2.4.3', + digest='sha256:8e0e85af45fc2bcc18bd7221b8c92fe4bb180f6bd5e30aa2b226f988029c2085', + ), + Image( + name='prometheus-config-reloader', + version='v0.23.2', + digest='sha256:df1453c7c69e4f2ab8a86fc18fe3b890ce2f80fed6d6519dc9d33927451b214d', + ), + Image( + name='prometheus-operator', + version='v0.23.2', + digest='sha256:8211b3eb30cb8591ddf536f1cf62100f5c97659c14d18dd45001acf94dafd713', + ), + # Local images + Image( + name='keepalived', + version='{version}-{build_id}'.format( + version=KEEPALIVED_VERSION, build_id=KEEPALIVED_BUILD_ID + ), + digest=None, + ), + Image( + name='metalk8s-ui', + version=VERSION, + digest=None, + ), + Image( + name='metalk8s-utils', + version=VERSION, + digest=None, + ), + Image( + name='salt-master', + version='{version}-{build_id}'.format( + version=SALT_VERSION, build_id=SALT_MASTER_BUILD_ID + ), + digest=None, + ), +) + +CONTAINER_IMAGES_MAP = {image.name: image for image in CONTAINER_IMAGES} + +# }}} +# Packages {{{ + +PACKAGES = ( + # Remote packages + Package( + name='containerd', + version='1.2.4', + release='1.el7', + ), + Package( + name='cri-tools', + version='1.13.0', + release='0', + ), + Package( + name='container-selinux', + version='2.99', + release='1.el7_6', + ), + Package( + name='coreutils', + version='8.22', + release='23.el7', + ), + Package( + name='ebtables', + version='2.0.10', + release='16.el7', + ), + Package( + name='ethtool', + version='4.8', + release='9.el7', + ), + Package( + name='genisoimage', + version='1.1.11', + release='25.el7', + ), + Package( + name='iproute', + version='4.11.0', + release='14.el7_6.2', + ), + Package( + name='iptables', + version='1.4.21', + release='28.el7', + ), + Package( + name='kubectl', + version=K8S_VERSION, + release='0', + ), + Package( + name='kubelet', + version=K8S_VERSION, + release='0', + ), + Package( + name='kubernetes-cni', + version='0.7.5', + release='0', + ), + Package( + name='m2crypto', + version='0.31.0', + release='3.el7', + ), + Package( + name='python2-kubernetes', + version='8.0.1', + release='1.el7', + ), + Package( + name='runc', + version='1.0.0', + release='59.dev.git2abd837.el7.centos', + ), + Package( + name='salt-minion', + version=SALT_VERSION, + release='1.el7', + ), + Package( + name='skopeo', + version='0.1.35', + release='2.git404c5bd.el7.centos', + ), + Package( + name='socat', + version='1.7.3.2', + release='2.el7', + ), + Package( + name='sos', + version='3.6', + release='17.el7.centos', + ), + Package( + name='util-linux', + version='2.23.2', + release='59.el7_6.1', + ), + Package( + name='yum-plugin-versionlock', + version='1.1.31', + release='50.el7', + ), + # Local packages + Package( + name='metalk8s-sosreport', + version=SHORT_VERSION, + release='1.el7', + ), + Package( + name='calico-cni-plugin', + version=CALICO_VERSION, + release='1.el7', + ), +) + +PACKAGES_MAP = {pkg.name: pkg for pkg in PACKAGES} + +# }}} diff --git a/eve/main.yml b/eve/main.yml index acfb1a27d1..e121d02925 100644 --- a/eve/main.yml +++ b/eve/main.yml @@ -383,9 +383,8 @@ stages: SSH_CONFIG: >- eve/workers/openstack-multiple-nodes/terraform/ssh_config command: > - ssh -F $SSH_CONFIG bastion mkdir metalk8s && - scp -F $SSH_CONFIG tox.ini bastion:metalk8s/ && - scp -F $SSH_CONFIG -r tests bastion:metalk8s/ + tar cfp - tox.ini VERSION tests/ buildchain/buildchain/versions.py + | ssh -F $SSH_CONFIG bastion '(mkdir metalk8s; cd "$_"; tar xf -)' - ShellCommand: name: Run tests on the bastion # yamllint disable rule:line-length diff --git a/packages/Dockerfile b/packages/Dockerfile index b80693a7bd..a48a7aff0b 100644 --- a/packages/Dockerfile +++ b/packages/Dockerfile @@ -4,7 +4,9 @@ ARG BUILD_IMAGE_SHA256=5d4f4e6051c7cc10f2e712f9dc3f86a2bd67e457bced7ca52a71c2430 ARG BUILD_IMAGE=docker.io/centos FROM ${BUILD_IMAGE}@sha256:${BUILD_IMAGE_SHA256} as build +ARG SALT_VERSION ADD yum_repositories/*.repo /etc/yum.repos.d/ +RUN sed -i s/@SALT_VERSION@/$SALT_VERSION/ /etc/yum.repos.d/saltstack.repo RUN yum install -y \ createrepo \ diff --git a/packages/packages.list b/packages/packages.list deleted file mode 100644 index d723217832..0000000000 --- a/packages/packages.list +++ /dev/null @@ -1,13 +0,0 @@ -containerd -cri-tools -container-selinux -genisoimage -kubectl-1.11.10 -kubelet-1.11.10 -m2crypto -python2-kubernetes -runc -salt-minion-2018.3.4-1.el7 -skopeo -yum-plugin-versionlock -sos diff --git a/packages/yum_repositories/saltstack.repo b/packages/yum_repositories/saltstack.repo index cae6707f5c..ef478a314a 100644 --- a/packages/yum_repositories/saltstack.repo +++ b/packages/yum_repositories/saltstack.repo @@ -1,6 +1,6 @@ [saltstack] name=SaltStack repo for RHEL/CentOS $releasever -baseurl=https://repo.saltstack.com/yum/redhat/$releasever/$basearch/archive/2018.3.4 +baseurl=https://repo.saltstack.com/yum/redhat/$releasever/$basearch/archive/@SALT_VERSION@ enabled=1 gpgcheck=1 -gpgkey=https://repo.saltstack.com/yum/redhat/$releasever/$basearch/archive/2018.3.4/SALTSTACK-GPG-KEY.pub +gpgkey=https://repo.saltstack.com/yum/redhat/$releasever/$basearch/archive/@SALT_VERSION@/SALTSTACK-GPG-KEY.pub diff --git a/salt/_modules/metalk8s_package_manager.py b/salt/_modules/metalk8s_package_manager.py index 41df0a1626..fdf8d12774 100644 --- a/salt/_modules/metalk8s_package_manager.py +++ b/salt/_modules/metalk8s_package_manager.py @@ -2,7 +2,6 @@ Describes our custom way to deal with yum packages so that we can support downgrade in metalk8s ''' - import logging log = logging.getLogger(__name__) @@ -15,64 +14,131 @@ def __virtual__(): return __virtualname__ -def list_pkg_deps(pkg_name, version=None, fromrepo=None): - ''' - Check dependencies related to the packages installed so that we can pass - this information to pkg.installed - - name - Name of the package installed - - version - Version number of the package +def _list_dependents( + name, version, fromrepo=None, allowed_versions=None +): + '''List and filter all packages requiring package `{name}-{version}`. - Use : salt '*' metalk8s_package_manager.list_pkg_deps kubelet 1.11.9 + Filter based on the `allowed_versions` provided, within the provided + `fromrepo` repositories. ''' log.info( - 'Listing deps for "%s" with version "%s"', - str(pkg_name), + 'Listing packages depending on "%s" with version "%s"', + str(name), str(version) ) - pkgs_dict = {pkg_name: version} - if not version: - return pkgs_dict + allowed_versions = allowed_versions or {} - command_all = [ - 'repoquery', '--whatrequires', '--recursive', '--qf', - '%{NAME} %{VERSION}-%{RELEASE}', - '{}-{}'.format(str(pkg_name), str(version)) + command = [ + 'repoquery', '--whatrequires', '--recursive', + '--qf', '%{NAME} %{VERSION}-%{RELEASE}', + '{}-{}'.format(name, version) ] if fromrepo: - command_all.extend(['--disablerepo', '*', '--enablerepo', fromrepo]) + command.extend(['--disablerepo', '*', '--enablerepo', fromrepo]) - deps_list = __salt__['cmd.run_all'](command_all) + ret = __salt__['cmd.run_all'](command) - if deps_list['retcode'] != 0: + if ret['retcode'] != 0: log.error( - 'Failed to list package dependencies: %s', - deps_list['stderr'] or deps_list['stdout'] + 'Failed to list packages requiring "%s": %s', + '{}-{}'.format(name, version), + ret['stderr'] or ret['stdout'] ) return None - out = deps_list['stdout'].splitlines() - for line in out: - name, version = line.strip().split() - pkgs_dict[name] = version + dependents = {} + for line in ret['stdout'].splitlines(): + req_name, req_version = line.strip().split() + + # NOTE: The following test filters out unknown packages and versions + # not referenced in `allowed_versions` (there can be only one) + if req_version == allowed_versions.get(req_name): + dependents[req_name] = req_version - for key in pkgs_dict.keys(): - package_query = __salt__['cmd.run_all']( - ['rpm', '-qa', key] + return dependents + + +def list_pkg_dependents( + name, version=None, fromrepo=None, pkgs_info=None +): + ''' + Check dependents of the package `name`-`version` to install, to add in a + later `pkg.installed` state along with the original package. + + Ensure all selected versions are compliant with those listed in `pkgs_info` + if provided. + + name + Name of the package installed + + version + Version number of the package + + pkgs_info + Value of pillar key `repo:packages` to consider for the requiring + packages to update (format {"": {"version": ""}, ...}) + + Usage : + salt '*' metalk8s_package_manager.list_pkg_dependents kubelet 1.11.10 + ''' + if pkgs_info: + versions_dict = { + p_name: p_info['version'] + for p_name, p_info in pkgs_info.items() + } + else: + versions_dict = {} + + if pkgs_info and name not in versions_dict: + log.error( + 'Trying to list dependents for "%s", which is not referenced in ' + 'the packages information provided', + name + ) + return None + + all_pkgs = {name: version} + + if not version: + return all_pkgs + + if pkgs_info and versions_dict[name] != version: + log.error( + 'Trying to list dependents for "%s" with version "%s", ' + 'while version configured is "%s"', + name, + version, + versions_dict[name] ) + return None - if package_query['retcode'] == 1: - pkgs_dict.pop(key) - elif package_query['retcode'] != 0: + dependents = _list_dependents( + name, + version, + fromrepo=fromrepo, + allowed_versions=versions_dict, + ) + + all_pkgs.update(dependents) + + for pkg_name, desired_version in all_pkgs.items(): + ret = __salt__['cmd.run_all'](['rpm', '-qa', pkg_name]) + + if ret['retcode'] != 0: log.error( - 'Failed to check if package is installed: %s', - deps_list['stderr'] or deps_list['stdout'] + 'Failed to check if package "%s" is installed: %s', + pkg_name, + ret['stderr'] or ret['stdout'] ) return None - return pkgs_dict + is_installed = bool(ret['stdout'].strip()) + if not is_installed and pkg_name != name: + # Any package requiring the target `name` that is not yet installed + # should not be installed + del all_pkgs[pkg_name] + + return all_pkgs diff --git a/salt/_states/metalk8s_package_manager.py b/salt/_states/metalk8s_package_manager.py index 100ce26832..1735566a4d 100644 --- a/salt/_states/metalk8s_package_manager.py +++ b/salt/_states/metalk8s_package_manager.py @@ -17,16 +17,31 @@ def __virtualname__(): return (False, "metalk8s_package_manager: no RPM-based system detected") -def installed(name, version=None, fromrepo=None, **kwargs): - """Simple helper to manage package downgrade including dependencies.""" +def installed(name, version=None, fromrepo=None, pkgs_info=None, **kwargs): + """Custom implementation of `pkg.installed`. + + Manage packages requiring the target package when upgrading/downgrading, + ensuring all versions are uniform with respect to what is declared + in `pkgs_info`. + """ if version is None or kwargs.get('pkgs'): return __states__["pkg.installed"]( name=name, version=version, fromrepo=fromrepo, **kwargs ) - dep_list = __salt__['metalk8s_package_manager.list_pkg_deps']( - name, version, fromrepo + dep_list = __salt__['metalk8s_package_manager.list_pkg_dependents']( + name, version, fromrepo=fromrepo, pkgs_info=pkgs_info, ) + if dep_list is None: + return { + 'name': name, + 'result': False, + 'changes': {}, + 'comment': ( + 'Failed to update package "{}" and its dependents'.format(name) + ), + } + pkgs = [{k: v} for k, v in dep_list.items()] return __states__["pkg.installed"]( name=name, pkgs=pkgs, fromrepo=fromrepo, **kwargs diff --git a/salt/metalk8s/addons/monitoring/alertmanager/upstream.sls b/salt/metalk8s/addons/monitoring/alertmanager/upstream.sls index 96382eca41..e0e785228c 100644 --- a/salt/metalk8s/addons/monitoring/alertmanager/upstream.sls +++ b/salt/metalk8s/addons/monitoring/alertmanager/upstream.sls @@ -1,6 +1,12 @@ #!jinja | kubernetes kubeconfig=/etc/kubernetes/admin.conf&context=kubernetes-admin@kubernetes -{%- from "metalk8s/repo/macro.sls" import build_image_name with context %} +{%- from "metalk8s/repo/macro.sls" import metalk8s_repository with context %} +{%- from "metalk8s/map.jinja" import repo with context %} + +{%- set alertmanager_version = repo.images.get('alertmanager', {}).get('version') %} +{%- if not alertmanager_version %} + {{ raise('Missing version information for "alertmanager"') }} +{%- endif %} # The content below has been generated from # https://github.com/coreos/prometheus-operator, v0.24.0 tag, @@ -27,13 +33,13 @@ metadata: name: main namespace: monitoring spec: - baseImage: {{ build_image_name('alertmanager') }} + baseImage: {{ metalk8s_repository }}/alertmanager nodeSelector: beta.kubernetes.io/os: linux node-role.kubernetes.io/infra: '' replicas: 3 serviceAccountName: alertmanager-main - version: v0.15.2 + version: {{ alertmanager_version }} tolerations: - key: "node-role.kubernetes.io/bootstrap" operator: "Exists" diff --git a/salt/metalk8s/addons/monitoring/grafana/upstream.sls b/salt/metalk8s/addons/monitoring/grafana/upstream.sls index b29e9ebc01..aa6d6237fe 100644 --- a/salt/metalk8s/addons/monitoring/grafana/upstream.sls +++ b/salt/metalk8s/addons/monitoring/grafana/upstream.sls @@ -7393,7 +7393,7 @@ spec: app: grafana spec: containers: - - image: {{ build_image_name('grafana', '5.2.4') }} + - image: {{ build_image_name('grafana') }} name: grafana ports: - containerPort: 3000 diff --git a/salt/metalk8s/addons/monitoring/kube-state-metrics/upstream.sls b/salt/metalk8s/addons/monitoring/kube-state-metrics/upstream.sls index cfa26a065b..3dde330222 100644 --- a/salt/metalk8s/addons/monitoring/kube-state-metrics/upstream.sls +++ b/salt/metalk8s/addons/monitoring/kube-state-metrics/upstream.sls @@ -183,7 +183,7 @@ spec: - args: - --secure-listen-address=:8443 - --upstream=http://127.0.0.1:8081/ - image: {{ build_image_name('kube-rbac-proxy', 'v0.3.1') }} + image: {{ build_image_name('kube-rbac-proxy') }} name: kube-rbac-proxy-main ports: - containerPort: 8443 @@ -198,7 +198,7 @@ spec: - args: - --secure-listen-address=:9443 - --upstream=http://127.0.0.1:8082/ - image: {{ build_image_name('kube-rbac-proxy', 'v0.3.1') }} + image: {{ build_image_name('kube-rbac-proxy') }} name: kube-rbac-proxy-self ports: - containerPort: 9443 @@ -215,7 +215,7 @@ spec: - --port=8081 - --telemetry-host=127.0.0.1 - --telemetry-port=8082 - image: {{ build_image_name('kube-state-metrics', 'v1.3.1') }} + image: {{ build_image_name('kube-state-metrics') }} name: kube-state-metrics resources: limits: @@ -244,7 +244,7 @@ spec: fieldRef: apiVersion: v1 fieldPath: metadata.namespace - image: {{ build_image_name('addon-resizer', '1.8.3') }} + image: {{ build_image_name('addon-resizer') }} name: addon-resizer resources: limits: diff --git a/salt/metalk8s/addons/monitoring/node-exporter/upstream.sls b/salt/metalk8s/addons/monitoring/node-exporter/upstream.sls index cadb459d44..22bf5295a8 100644 --- a/salt/metalk8s/addons/monitoring/node-exporter/upstream.sls +++ b/salt/metalk8s/addons/monitoring/node-exporter/upstream.sls @@ -66,7 +66,7 @@ spec: - --path.sysfs=/host/sys - --collector.filesystem.ignored-mount-points=^/(dev|proc|sys|var/lib/docker/.+)($|/) - --collector.filesystem.ignored-fs-types=^(autofs|binfmt_misc|cgroup|configfs|debugfs|devpts|devtmpfs|fusectl|hugetlbfs|mqueue|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|sysfs|tracefs)$ - image: {{ build_image_name('node-exporter', 'v0.17.0') }} + image: {{ build_image_name('node-exporter') }} name: node-exporter resources: limits: @@ -89,7 +89,7 @@ spec: - args: - --secure-listen-address=:9100 - --upstream=http://127.0.0.1:9101/ - image: {{ build_image_name('kube-rbac-proxy', 'v0.3.1') }} + image: {{ build_image_name('kube-rbac-proxy') }} name: kube-rbac-proxy ports: - containerPort: 9100 diff --git a/salt/metalk8s/addons/monitoring/prometheus-operator/upstream.sls b/salt/metalk8s/addons/monitoring/prometheus-operator/upstream.sls index c568caa4ca..e7c8f95240 100644 --- a/salt/metalk8s/addons/monitoring/prometheus-operator/upstream.sls +++ b/salt/metalk8s/addons/monitoring/prometheus-operator/upstream.sls @@ -6208,9 +6208,9 @@ spec: - args: - --kubelet-service=kube-system/kubelet - --logtostderr=true - - --config-reloader-image={{ build_image_name('configmap-reload', 'v0.0.1') }} - - --prometheus-config-reloader={{ build_image_name('prometheus-config-reloader', 'v0.23.2') }} - image: {{ build_image_name('prometheus-operator', 'v0.23.2') }} + - --config-reloader-image={{ build_image_name('configmap-reload') }} + - --prometheus-config-reloader={{ build_image_name('prometheus-config-reloader') }} + image: {{ build_image_name('prometheus-operator') }} name: prometheus-operator ports: - containerPort: 8080 diff --git a/salt/metalk8s/addons/monitoring/prometheus/upstream.sls b/salt/metalk8s/addons/monitoring/prometheus/upstream.sls index 46a64eeeac..cb825703a2 100644 --- a/salt/metalk8s/addons/monitoring/prometheus/upstream.sls +++ b/salt/metalk8s/addons/monitoring/prometheus/upstream.sls @@ -1,6 +1,12 @@ #!jinja | kubernetes kubeconfig=/etc/kubernetes/admin.conf&context=kubernetes-admin@kubernetes -{%- from "metalk8s/repo/macro.sls" import build_image_name with context %} +{%- from "metalk8s/repo/macro.sls" import metalk8s_repository with context %} +{%- from "metalk8s/map.jinja" import repo with context %} + +{%- set prometheus_version = repo.images.get('prometheus', {}).get('version') %} +{%- if not prometheus_version %} + {{ raise('Missing version information for "prometheus"') }} +{%- endif %} # The content below has been generated from # https://github.com/coreos/prometheus-operator, v0.24.0 tag, @@ -149,7 +155,7 @@ spec: - name: alertmanager-main namespace: monitoring port: web - baseImage: {{ build_image_name('prometheus') }} + baseImage: {{ metalk8s_repository }}/prometheus nodeSelector: beta.kubernetes.io/os: linux node-role.kubernetes.io/infra: '' @@ -164,7 +170,7 @@ spec: serviceAccountName: prometheus-k8s serviceMonitorNamespaceSelector: {} serviceMonitorSelector: {} - version: v2.4.3 + version: {{ prometheus_version }} tolerations: - key: "node-role.kubernetes.io/bootstrap" operator: "Exists" diff --git a/salt/metalk8s/addons/nginx-ingress/deployed/chart.sls b/salt/metalk8s/addons/nginx-ingress/deployed/chart.sls index 79d9f52e53..caf52a62a1 100644 --- a/salt/metalk8s/addons/nginx-ingress/deployed/chart.sls +++ b/salt/metalk8s/addons/nginx-ingress/deployed/chart.sls @@ -311,7 +311,7 @@ spec: valueFrom: fieldRef: fieldPath: metadata.namespace - image: '{{ build_image_name("nginx-ingress-controller") }}:0.25.0' + image: '{{ build_image_name("nginx-ingress-controller") }}' imagePullPolicy: IfNotPresent livenessProbe: failureThreshold: 3 @@ -391,7 +391,7 @@ spec: spec: containers: - args: null - image: '{{ build_image_name("nginx-ingress-defaultbackend-amd64") }}:1.5' + image: '{{ build_image_name("nginx-ingress-defaultbackend-amd64") }}' imagePullPolicy: IfNotPresent livenessProbe: failureThreshold: 3 diff --git a/salt/metalk8s/addons/ui/files/metalk8s-ui-deployment.yaml.in b/salt/metalk8s/addons/ui/files/metalk8s-ui-deployment.yaml similarity index 95% rename from salt/metalk8s/addons/ui/files/metalk8s-ui-deployment.yaml.in rename to salt/metalk8s/addons/ui/files/metalk8s-ui-deployment.yaml index 6251f6a491..0071bd66f7 100644 --- a/salt/metalk8s/addons/ui/files/metalk8s-ui-deployment.yaml.in +++ b/salt/metalk8s/addons/ui/files/metalk8s-ui-deployment.yaml @@ -26,7 +26,7 @@ spec: node-role.kubernetes.io/infra: '' containers: - name: metalk8s-ui - image: {{ build_image_name('metalk8s-ui', '@@VERSION') }} + image: {{ build_image_name('metalk8s-ui') }} imagePullPolicy: IfNotPresent resources: limits: diff --git a/salt/metalk8s/defaults.yaml b/salt/metalk8s/defaults.yaml index dbccd0d296..1628846753 100644 --- a/salt/metalk8s/defaults.yaml +++ b/salt/metalk8s/defaults.yaml @@ -3,7 +3,6 @@ metalk8s: {} kubernetes: cluster: kubernetes - version: 1.11.10 # NOTE: remember to update kubelet version as well kubeadm_preflight: mandatory: @@ -95,38 +94,6 @@ repo: repo_gpg_check: 0 enabled: 0 - packages: - calico-cni-plugin: - version: latest - repository: metalk8s-scality - container-selinux: - version: latest - repository: metalk8s-extras - containerd: - version: latest - repository: metalk8s-epel - cri-tools: - version: latest - repository: metalk8s-kubernetes - kubelet: - version: 1.11.10 - repository: metalk8s-kubernetes - m2crypto: - version: latest - repository: metalk8s-saltstack - python2-kubernetes: - version: latest - repository: metalk8s-epel - runc: - version: latest - repository: metalk8s-extras - salt-minion: - version: 2018.3.4-1.el7 - repository: metalk8s-saltstack - skopeo: - version: latest - repository: metalk8s-extras - networks: {} kubelet: diff --git a/salt/metalk8s/internal/preflight/mandatory.sls b/salt/metalk8s/internal/preflight/mandatory.sls index b2b610314b..fb73dafef4 100644 --- a/salt/metalk8s/internal/preflight/mandatory.sls +++ b/salt/metalk8s/internal/preflight/mandatory.sls @@ -6,11 +6,12 @@ include: - metalk8s.repo -Install mandatory packages: - {{ pkg_installed() }} - - pkgs: {{ kubeadm_preflight.mandatory.packages }} +{%- for pkg_name in kubeadm_preflight.mandatory.packages %} +Install mandatory package "{{ pkg_name }}": + {{ pkg_installed(pkg_name) }} - require: - test: Repositories configured +{%- endfor %} {%- if kubelet.container_engine %} Enable {{ kubelet.container_engine }} service: diff --git a/salt/metalk8s/internal/preflight/recommended.sls b/salt/metalk8s/internal/preflight/recommended.sls index 974b9aa1ab..d7e36d74f5 100644 --- a/salt/metalk8s/internal/preflight/recommended.sls +++ b/salt/metalk8s/internal/preflight/recommended.sls @@ -4,8 +4,9 @@ include: - metalk8s.repo -Install recommended packages: - {{ pkg_installed() }} - - pkgs: {{ kubeadm_preflight.recommended.packages }} +{%- for pkg_name in kubeadm_preflight.mandatory.packages %} +Install recommended package "{{ pkg_name }}": + {{ pkg_installed(pkg_name) }} - require: - test: Repositories configured +{%- endfor %} diff --git a/salt/metalk8s/kubernetes/apiserver/installed.sls b/salt/metalk8s/kubernetes/apiserver/installed.sls index c17dd70202..a8d33a3b8b 100644 --- a/salt/metalk8s/kubernetes/apiserver/installed.sls +++ b/salt/metalk8s/kubernetes/apiserver/installed.sls @@ -1,11 +1,8 @@ -{%- from "metalk8s/repo/macro.sls" import kubernetes_image, build_image_name with context %} +{%- from "metalk8s/repo/macro.sls" import build_image_name with context %} {%- from "metalk8s/map.jinja" import networks with context %} {%- set htpasswd_path = "/etc/kubernetes/htpasswd" %} -{%- set keepalived_image = "keepalived" %} -{%- set keepalived_version = "1.3.5-8.el7_6-1" %} - include: - metalk8s.kubernetes.ca.advertised - metalk8s.kubernetes.sa.advertised @@ -104,7 +101,7 @@ Create kube-apiserver Pod manifest: - context: name: kube-apiserver host: {{ host }} - image_name: {{ kubernetes_image("kube-apiserver") }} + image_name: {{ build_image_name("kube-apiserver") }} port: 6443 scheme: HTTPS command: @@ -155,7 +152,7 @@ Create kube-apiserver Pod manifest: sidecars: {%- if pillar.metalk8s.api_server.keepalived.enabled %} - name: keepalived - image: {{ build_image_name(keepalived_image, keepalived_version) }} + image: {{ build_image_name("keepalived") }} args: - --dont-fork - --dump-conf diff --git a/salt/metalk8s/kubernetes/cni/calico/deployed.sls b/salt/metalk8s/kubernetes/cni/calico/deployed.sls index fee6a0b4fa..4773108e65 100644 --- a/salt/metalk8s/kubernetes/cni/calico/deployed.sls +++ b/salt/metalk8s/kubernetes/cni/calico/deployed.sls @@ -596,7 +596,7 @@ spec: # container programs network policy and routes on each # host. - name: calico-node - image: {{ build_image_name('calico-node', '3.8.0') }} + image: {{ build_image_name('calico-node') }} env: # Use Kubernetes API as the backing datastore. - name: DATASTORE_TYPE @@ -782,7 +782,7 @@ spec: priorityClassName: system-cluster-critical containers: - name: calico-kube-controllers - image: {{ build_image_name('calico-kube-controllers', '3.8.0') }} + image: {{ build_image_name('calico-kube-controllers') }} env: # Choose which controllers to run. - name: ENABLED_CONTROLLERS diff --git a/salt/metalk8s/kubernetes/controller-manager/installed.sls b/salt/metalk8s/kubernetes/controller-manager/installed.sls index ea1ed98ef6..b2dc6e7b57 100644 --- a/salt/metalk8s/kubernetes/controller-manager/installed.sls +++ b/salt/metalk8s/kubernetes/controller-manager/installed.sls @@ -1,4 +1,4 @@ -{% from "metalk8s/repo/macro.sls" import kubernetes_image with context %} +{% from "metalk8s/repo/macro.sls" import build_image_name with context %} {% from "metalk8s/map.jinja" import networks with context %} include: @@ -16,7 +16,7 @@ Create kube-controller-manager Pod manifest: - /etc/kubernetes/pki/sa.key - context: name: kube-controller-manager - image_name: {{ kubernetes_image("kube-controller-manager") }} + image_name: {{ build_image_name("kube-controller-manager") }} host: {{ grains['metalk8s']['control_plane_ip'] }} port: 10252 scheme: HTTP diff --git a/salt/metalk8s/kubernetes/coredns/files/coredns-deployment.yaml.j2 b/salt/metalk8s/kubernetes/coredns/files/coredns-deployment.yaml.j2 index 537a695445..dd2470806e 100644 --- a/salt/metalk8s/kubernetes/coredns/files/coredns-deployment.yaml.j2 +++ b/salt/metalk8s/kubernetes/coredns/files/coredns-deployment.yaml.j2 @@ -34,7 +34,7 @@ spec: node-role.kubernetes.io/infra: '' containers: - name: coredns - image: {{ build_image_name('coredns', '1.3.1') }} + image: {{ build_image_name('coredns') }} imagePullPolicy: IfNotPresent resources: limits: diff --git a/salt/metalk8s/kubernetes/etcd/installed.sls b/salt/metalk8s/kubernetes/etcd/installed.sls index ba5f5727ed..199caae7f3 100644 --- a/salt/metalk8s/kubernetes/etcd/installed.sls +++ b/salt/metalk8s/kubernetes/etcd/installed.sls @@ -7,8 +7,6 @@ include: {%- set host_name = grains['id'] %} {%- set host = grains['metalk8s']['control_plane_ip'] %} -{%- set image_name = build_image_name('etcd', '3.2.18') %} - {%- set endpoint = host_name ~ '=https://' ~ host ~ ':2380' %} {#- Get the list of existing etcd node. #} @@ -40,7 +38,7 @@ Create local etcd Pod manifest: - /etc/kubernetes/pki/etcd/server.key - context: name: etcd - image_name: {{ image_name }} + image_name: {{ build_image_name('etcd') }} command: - etcd - --advertise-client-urls=https://{{ host }}:2379 diff --git a/salt/metalk8s/kubernetes/kube-proxy/deployed.sls b/salt/metalk8s/kubernetes/kube-proxy/deployed.sls index 69e0de3a58..b7e04460ef 100644 --- a/salt/metalk8s/kubernetes/kube-proxy/deployed.sls +++ b/salt/metalk8s/kubernetes/kube-proxy/deployed.sls @@ -1,7 +1,7 @@ -{%- from "metalk8s/repo/macro.sls" import kubernetes_image with context %} +{%- from "metalk8s/repo/macro.sls" import build_image_name with context %} {%- from "metalk8s/map.jinja" import networks with context %} -{%- set image = kubernetes_image("kube-proxy") -%} +{%- set image = build_image_name("kube-proxy") -%} {%- set kubeconfig = "/etc/kubernetes/admin.conf" %} {%- set context = "kubernetes-admin@kubernetes" %} diff --git a/salt/metalk8s/kubernetes/scheduler/installed.sls b/salt/metalk8s/kubernetes/scheduler/installed.sls index 17819bd6ae..c19f4b2e1a 100644 --- a/salt/metalk8s/kubernetes/scheduler/installed.sls +++ b/salt/metalk8s/kubernetes/scheduler/installed.sls @@ -1,4 +1,4 @@ -{% from "metalk8s/repo/macro.sls" import kubernetes_image with context %} +{% from "metalk8s/repo/macro.sls" import build_image_name with context %} include: - .kubeconfig @@ -11,7 +11,7 @@ Create kube-scheduler Pod manifest: - /etc/kubernetes/scheduler.conf - context: name: kube-scheduler - image_name: {{ kubernetes_image("kube-scheduler") }} + image_name: {{ build_image_name("kube-scheduler") }} host: {{ grains['metalk8s']['control_plane_ip'] }} port: 10251 scheme: HTTP diff --git a/salt/metalk8s/macro.sls b/salt/metalk8s/macro.sls index 64fcd6bf7c..bcaa42b003 100644 --- a/salt/metalk8s/macro.sls +++ b/salt/metalk8s/macro.sls @@ -1,14 +1,20 @@ {%- from "metalk8s/map.jinja" import repo with context %} {%- macro pkg_installed(name='') -%} - {%- set package = repo.packages[name] | default({}) %} + {%- set package = repo.packages.get(name, {}) %} + {%- if package %} metalk8s_package_manager.installed: - name: {{ name }} - fromrepo: {{ repo.repositories.keys() | join(',') }} + - pkgs_info: {{ repo.packages }} {%- if package.version | default(None) %} - version: {{ package.version }} - hold: True - update_holds: True + - ignore_epoch: True {%- endif %} - reload_modules: True + {%- else %} + {{ raise('Missing information for package "' ~ name ~ '"') }} + {%- endif %} {%- endmacro -%} diff --git a/salt/metalk8s/map.jinja b/salt/metalk8s/map.jinja index 2620a3f3d7..04b60099e9 100644 --- a/salt/metalk8s/map.jinja +++ b/salt/metalk8s/map.jinja @@ -1,5 +1,20 @@ {% import_yaml 'metalk8s/defaults.yaml' as defaults with context %} +{% import_json 'metalk8s/versions.json' as versions with context %} +{# First merge the basic defaults with versions information #} +{% set version_defaults = { + 'kubernetes': versions.kubernetes, + 'repo': { + 'packages': versions.packages, + 'images': versions.images, + }, +} %} + +{% set defaults = salt['grains.filter_by']({ + 'default': defaults, +}, merge=version_defaults) %} + +{# Then merge with pillar overrides #} {% set defaults = salt['grains.filter_by']({ 'default': defaults }, merge=pillar) %} @@ -24,7 +39,7 @@ 'kubernetes': { 'name': 'kubernetes' } - } + }, }, merge=defaults.get('repo')) %} {% set networks = salt['grains.filter_by']({ diff --git a/salt/metalk8s/repo/installed.sls b/salt/metalk8s/repo/installed.sls index 467aa531f6..d4f526be0b 100644 --- a/salt/metalk8s/repo/installed.sls +++ b/salt/metalk8s/repo/installed.sls @@ -1,4 +1,3 @@ -{%- from "metalk8s/repo/macro.sls" import build_image_name with context %} {%- from "metalk8s/map.jinja" import repo with context %} {%- set repositories_name = 'repositories' %} @@ -6,13 +5,23 @@ {%- set products = salt.metalk8s.get_products() %} +{%- set docker_repository = 'docker.io/library' %} +{%- set image_name = 'nginx' %} + +{%- set image_version = repo.images.get(image_name, {}).get('version') %} +{%- if not image_version %} + {{ raise('Missing version information for "nginx"') }} +{%- endif %} + +{%- set image_fullname = docker_repository ~ '/' ~ image_name ~ ':' ~ image_version %} + include: - .configured Inject nginx image: containerd.image_managed: - - name: docker.io/library/nginx:1.15.8 - - archive_path: {{ products[saltenv].path }}/images/nginx-1.15.8.tar + - name: {{ image_fullname }} + - archive_path: {{ products[saltenv].path }}/images/{{ image_name }}-{{ image_version }}.tar Install repositories manifest: metalk8s.static_pod_managed: @@ -31,7 +40,7 @@ Install repositories manifest: {%- endfor %} - context: container_port: {{ repo.port }} - image: docker.io/library/nginx:1.15.8 + image: {{ image_fullname }} name: {{ repositories_name }} version: {{ repositories_version }} products: {{ products }} diff --git a/salt/metalk8s/repo/macro.sls b/salt/metalk8s/repo/macro.sls index 3f1b1f189f..debb8d24c5 100644 --- a/salt/metalk8s/repo/macro.sls +++ b/salt/metalk8s/repo/macro.sls @@ -1,10 +1,16 @@ -{%- from "metalk8s/map.jinja" import kubernetes with context %} {%- from "metalk8s/map.jinja" import metalk8s with context %} +{%- from "metalk8s/map.jinja" import repo with context %} -{%- macro build_image_name(name='', tag='') -%} -{{ metalk8s.endpoints['repositories'].ip }}:{{ metalk8s.endpoints['repositories'].ports.http }}/{{ saltenv }}/{{ name }}{{ ':' ~ tag if tag else '' }} -{%- endmacro -%} +{%- set repo_endpoint = metalk8s.endpoints.repositories %} +{%- set repo_prefix = repo_endpoint.ip ~ ':' ~ repo_endpoint.ports.http %} +{%- set metalk8s_repository = repo_prefix ~ '/' ~ saltenv %} -{%- macro kubernetes_image(component) -%} -{{ build_image_name(component, kubernetes.version) }} +{%- macro build_image_name(name='') -%} + {%- set image_info = repo.images.get(name, {}) -%} + {%- if image_info -%} + {%- set image_tag = name ~ ':' ~ image_info.version -%} +{{ metalk8s_repository }}/{{ image_tag }} + {%- else -%} +{{ raise('Missing version information about image "' ~ name ~ '"') }} + {%- endif -%} {%- endmacro -%} diff --git a/salt/metalk8s/salt/master/files/salt-master-manifest.yaml.j2 b/salt/metalk8s/salt/master/files/salt-master-manifest.yaml.j2 index b6b53d64dd..09c8d5d5fe 100644 --- a/salt/metalk8s/salt/master/files/salt-master-manifest.yaml.j2 +++ b/salt/metalk8s/salt/master/files/salt-master-manifest.yaml.j2 @@ -1,5 +1,3 @@ -{%- from "metalk8s/repo/macro.sls" import build_image_name with context -%} - apiVersion: v1 kind: Pod metadata: @@ -8,7 +6,7 @@ metadata: labels: app: salt-master app.kubernetes.io/name: salt-master - app.kubernetes.io/version: '{{ salt_master_version }}' + app.kubernetes.io/version: '{{ version }}' app.kubernetes.io/component: salt heritage: metalk8s app.kubernetes.io/part-of: metalk8s @@ -28,7 +26,7 @@ spec: - ALL containers: - name: salt-master - image: {{ build_image_name(salt_master_image, salt_master_version) }} + image: {{ image }} args: ['--log-level=info'] ports: - name: publisher @@ -75,7 +73,7 @@ spec: mountPath: '/etc/kubernetes' readOnly: true - name: salt-api - image: {{ build_image_name(salt_master_image, salt_master_version) }} + image: {{ image }} command: ['/tini'] args: ['salt-api', '--', '--log-level=info'] ports: diff --git a/salt/metalk8s/salt/master/installed.sls b/salt/metalk8s/salt/master/installed.sls index b55d8448de..afc4bc07ef 100644 --- a/salt/metalk8s/salt/master/installed.sls +++ b/salt/metalk8s/salt/master/installed.sls @@ -1,7 +1,9 @@ {% from "metalk8s/map.jinja" import metalk8s with context %} +{% from "metalk8s/map.jinja" import repo with context %} +{%- from "metalk8s/repo/macro.sls" import build_image_name with context -%} -{% set salt_master_image = 'salt-master' %} -{% set salt_master_version = '2018.3.4-1' %} +{% set image_name = build_image_name('salt-master') %} +{% set image_version = repo.images['salt-master'].version %} {%- set salt_ip = grains['metalk8s']['control_plane_ip'] -%} @@ -27,8 +29,8 @@ Install and start salt master manifest: - /etc/salt/master.d/99-metalk8s.conf - /etc/salt/master.d/99-metalk8s-roots.conf - context: - salt_master_image: {{ salt_master_image }} - salt_master_version: {{ salt_master_version }} + image: {{ image_name }} + version: {{ image_version }} products: {{ salt.metalk8s.get_products() }} salt_ip: "{{ salt_ip }}" - require: diff --git a/tests/conftest.py b/tests/conftest.py index 8d95942fd0..f9310759ca 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -84,6 +84,10 @@ def k8s_client(request, k8s_apiclient): def test_something(k8s_client): assert k8s_client.list_namespaced_deployment(namespace="default") ``` + + FIXME: this is not working as of right now, since `pytest-bdd` manipulates + fixtures in its own way through the various scenario/when/then/given + decorators. """ api_name = getattr(request, "param", "CoreV1Api") api_cls = getattr(kubernetes.client, api_name, None) diff --git a/tests/post/features/versions.feature b/tests/post/features/versions.feature new file mode 100644 index 0000000000..aa1bff2ecf --- /dev/null +++ b/tests/post/features/versions.feature @@ -0,0 +1,5 @@ +@post @ci @local +Feature: Check versions in the running cluster + Scenario: Check the cluster's Kubernetes version + Given the Kubernetes API is available + Then the Kubernetes version deployed is the same as the configured one diff --git a/tests/post/steps/test_versions.py b/tests/post/steps/test_versions.py new file mode 100644 index 0000000000..3d83b9bcb6 --- /dev/null +++ b/tests/post/steps/test_versions.py @@ -0,0 +1,26 @@ +from kubernetes.client import VersionApi +from pytest_bdd import scenario, then + +from tests import versions + + +# Scenarios +@scenario('../features/versions.feature', + "Check the cluster's Kubernetes version") +def test_cluster_version(host): + pass + + +# Then +@then('the Kubernetes version deployed is the same as the configured one') +def check_kubernetes_version(k8s_apiclient): + # NOTE: the `vX.Y.Z` format is used by Kubernetes, not our buildchain + configured_version = 'v{}'.format(versions.K8S_VERSION) + + k8s_client = VersionApi(api_client=k8s_apiclient) + observed_version = k8s_client.get_code().git_version + + assert configured_version == observed_version, ( + "The running version of Kubernetes is '{}', while the expected version" + "is '{}'.".format(observed_version, configured_version) + ) diff --git a/tests/versions.py b/tests/versions.py new file mode 120000 index 0000000000..f810f52932 --- /dev/null +++ b/tests/versions.py @@ -0,0 +1 @@ +../buildchain/buildchain/versions.py \ No newline at end of file