Skip to content

Commit

Permalink
feat: allow specifying config file with cli option (#170)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamblake committed Mar 26, 2024
1 parent e08b24b commit 243010f
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 24 deletions.
79 changes: 55 additions & 24 deletions chartpress.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
This is used as part of the JupyterHub and Binder projects.
"""

import argparse
import os
import pipes
Expand Down Expand Up @@ -277,13 +278,13 @@ def _get_current_branchname(**kwargs):

def _get_image_build_args(image_options, ns):
"""
Render buildArgs from chartpress.yaml that could be templates, using
Render buildArgs from the config file that could be templates, using
provided namespace that contains keys with dynamic values such as
LAST_COMMIT or TAG.
Args:
image_options (dict):
The dictionary for a given image from chartpress.yaml.
The dictionary for a given image from the config file.
Fields in `image_options['buildArgs']` will be rendered
and returned, if defined.
ns (dict): the namespace used when rendering templated arguments
Expand All @@ -296,13 +297,13 @@ def _get_image_build_args(image_options, ns):

def _get_image_extra_build_command_options(image_options, ns):
"""
Render extraBuildCommandOptions from chartpress.yaml that could be
Render extraBuildCommandOptions from the config file that could be
templates, using the provided namespace that contains keys with dynamic
values such as LAST_COMMIT or TAG.
Args:
image_options (dict):
The dictionary for a given image from chartpress.yaml.
The dictionary for a given image from the config file.
Strings in `image_options['extraBuildCommandOptions']` will be rendered
and returned.
ns (dict): the namespace used when rendering templated arguments
Expand Down Expand Up @@ -333,7 +334,7 @@ def _get_image_dockerfile_path(name, options):
return os.path.join(_get_image_build_context_path(name, options), "Dockerfile")


def _get_all_image_paths(name, options):
def _get_all_image_paths(name, options, config_path):
"""
Returns the unique paths that when changed should trigger a rebuild of a
chart's image. This includes the Dockerfile itself and the context of the
Expand All @@ -343,26 +344,26 @@ def _get_all_image_paths(name, options):
Dockerfile path, and the optional others for extra paths.
"""
paths = []
paths.append("chartpress.yaml")
paths.append(config_path)
if options.get("rebuildOnContextPathChanges", True):
paths.append(_get_image_build_context_path(name, options))
paths.append(_get_image_dockerfile_path(name, options))
paths.extend(options.get("paths", []))
return list(set(paths))


def _get_all_chart_paths(options):
def _get_all_chart_paths(options, config_path):
"""
Returns the unique paths that when changed should trigger a version update
of the chart. These paths includes all the chart's images' paths as well.
"""
paths = []
paths.append("chartpress.yaml")
paths.append(config_path)
paths.append(options["chartPath"])
paths.extend(options.get("paths", []))
if "images" in options:
for image_name, image_config in options["images"].items():
paths.extend(_get_all_image_paths(image_name, image_config))
paths.extend(_get_all_image_paths(image_name, image_config, config_path))
return list(set(paths))


Expand Down Expand Up @@ -393,7 +394,7 @@ def build_image(
directory during the build process of the Dockerfile. This is typically
the same folder as the Dockerfile resides in.
dockerfile_path (str, optional):
Path to Dockerfile relative to chartpress.yaml's directory if not
Path to Dockerfile relative to the config file's directory if not
"<context_path>/Dockerfile".
build_args (dict, optional):
Dictionary of docker build arguments.
Expand Down Expand Up @@ -603,12 +604,13 @@ def build_images(
builder=Builder.DOCKER_BUILD,
platforms=None,
base_version=None,
config_path="chartpress.yaml",
):
"""Build a collection of docker images
Args:
prefix (str): the prefix to add to image names
images (dict): dict of image-specs from chartpress.yaml
images (dict): dict of image-specs from config file.
tag (str):
Specific tag to use instead of the last modified commit.
If unspecified the tag for each image will be the hash of the last commit
Expand Down Expand Up @@ -643,16 +645,18 @@ def build_images(
base_version (str):
The base version string (before '.git'), used when useChartVersion is True
instead of the tag found via `git describe`.
config_path (str):
Path to the chartpress config file (default: "chartpress.yaml").
"""
if platforms:
# for later use of set operations like .difference()
platforms = frozenset(platforms)

values_file_modifications = {}
for name, options in images.items():
# include chartpress.yaml in the image paths to inspect as
# chartpress.yaml can contain build args influencing the image
all_image_paths = _get_all_image_paths(name, options)
# include config file in the image paths to inspect as
# it can contain build args influencing the image
all_image_paths = _get_all_image_paths(name, options, config_path)

if tag is None:
image_tag = _get_identifier_from_paths(
Expand Down Expand Up @@ -1056,7 +1060,7 @@ def _version_number(groups):
# check ordering with latest tag
# do not check on a tagged commit
if tag and count:
sort_error = f"baseVersion {base_version} is not greater than latest tag {tag}. Please update baseVersion config in chartpress.yaml."
sort_error = f"baseVersion {base_version} is not greater than latest tag {tag}. Please update baseVersion in config."
if tag_match:
base_version_number = _version_number(base_version_groups)
if base_version_number < tag_version_number:
Expand Down Expand Up @@ -1143,7 +1147,7 @@ def main(argv=None):
argparser.add_argument(
"--reset",
action="store_true",
help="Skip image build step and reset Chart.yaml's version field and values.yaml's image tags. What it resets to can be configured in chartpress.yaml with the resetTag and resetVersion configurations.",
help="Skip image build step and reset Chart.yaml's version field and values.yaml's image tags. What it resets to can be configured in your config file with the resetTag and resetVersion configurations.",
)
skip_or_force_build_group = argparser.add_mutually_exclusive_group()
skip_or_force_build_group.add_argument(
Expand Down Expand Up @@ -1182,6 +1186,14 @@ def main(argv=None):
action="store_true",
help="print list of images to stdout. Images will not be built.",
)

argparser.add_argument(
"--config",
type=str,
default="chartpress.yaml",
help="Path to the configuration file",
)

argparser.add_argument(
"--version",
action="version",
Expand All @@ -1192,16 +1204,21 @@ def main(argv=None):
if args.builder == Builder.DOCKER_BUILD and args.platform:
argparser.error(f"--platform is not supported with {Builder.DOCKER_BUILD}")

if args.config:
# check that config exists and is readable
with open(args.config):
pass

if args.reset:
# reset conflicts with everything
# reset conflicts with everything except the configuration file
# this could probably be clearer by using subparsers
argv = list(argv or sys.argv[1:])
argv.remove("--reset")
remove_config_arg(argv)
if len(argv) > 1:
argv = list(argv)
argv.remove("--reset")
extra_args = " ".join(shlex.quote(arg) for arg in argv if arg != "--reset")
extra_args = " ".join(shlex.quote(arg) for arg in argv)
argparser.error(
f"`chartpress --reset` takes no additional arguments: {extra_args}"
f"`chartpress --reset` can only be used with `--config` and no additional arguments: {extra_args}"
)

# allow simple checks for whether publish will happen
Expand All @@ -1212,11 +1229,11 @@ def main(argv=None):
args.no_build = True
args.publish_chart = False

with open("chartpress.yaml") as f:
with open(args.config) as f:
config = yaml.load(f)

# main logic
# - loop through each chart listed in chartpress.yaml
# - loop through each chart listed in the config file
# - build chart.yaml (--reset)
# - build images (--skip-build | --reset)
# - push images (--push)
Expand Down Expand Up @@ -1248,7 +1265,7 @@ def main(argv=None):
# update Chart.yaml with a version
chart_version = build_chart(
chart["chartPath"],
paths=_get_all_chart_paths(chart),
paths=_get_all_chart_paths(chart, args.config),
version=forced_version,
base_version=base_version,
long=args.long,
Expand Down Expand Up @@ -1307,5 +1324,19 @@ def main(argv=None):
)


def remove_config_arg(argv):
# get the index for --config, --config=something, or None
config_idx = next(
(i for i, arg in enumerate(argv) if arg.startswith("--config")),
None,
)
if config_idx is not None:
# remove the --config argument (and its value if passed with =)
argv.pop(config_idx)
if not argv[config_idx].startswith("--") and config_idx < len(argv):
# remove the value of the --config argument if it was passed separately
argv.pop(config_idx)


if __name__ == "__main__":
main()
40 changes: 40 additions & 0 deletions tests/test_repo_interactions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import contextlib
import os
import subprocess
import sys
import tempfile

import pytest
from conftest import cache_clear
Expand All @@ -9,6 +11,15 @@
from chartpress import PRERELEASE_PREFIX, yaml


@contextlib.contextmanager
def temporary_copy(file_path):
with tempfile.NamedTemporaryFile(dir=".") as temp:
with open(file_path) as src:
temp.write(src.read().encode())
temp.flush()
yield temp.name


def check_version(tag):
chartpress._fix_chart_version(tag, strict=True)

Expand Down Expand Up @@ -80,6 +91,23 @@ def test_chartpress_run(git_repo, capfd, base_version):
f"Updating testchart/values.yaml: image: testchart/testimage:{reset_tag}" in out
)

# verify usage of --config
with temporary_copy("chartpress.yaml") as temp:
temp_reset_version = "set-with-temp-config"

with open(temp) as f:
temp_config = yaml.load(f)
temp_chart = temp_config["charts"][0]
temp_chart["resetVersion"] = temp_reset_version
with open(temp, "w") as f:
yaml.dump(temp_config, f)

with open(temp) as f:
temp_config = yaml.load(f)
temp_chart = temp_config["charts"][0]
out = _capture_output(["--reset", "--config", temp], capfd)
assert f"Updating testchart/Chart.yaml: version: {temp_reset_version}" in out

# verify that we don't need to rebuild the image
out = _capture_output([], capfd)
assert "Skipping build" in out
Expand Down Expand Up @@ -549,3 +577,15 @@ def test_reset_exclusive(git_repo, capfd):
chartpress.main(["--reset", "--tag", "1.2.3"])
out, err = capfd.readouterr()
assert "no additional arguments" in err

with pytest.raises(SystemExit):
chartpress.main(["--reset", "--config=chartpress.yaml", "--tag", "1.2.3"])
out, err = capfd.readouterr()
assert "no additional arguments" in err
assert "chartpress.yaml" not in err

with pytest.raises(SystemExit):
chartpress.main(["--reset", "--config", "chartpress.yaml", "--tag", "1.2.3"])
out, err = capfd.readouterr()
assert "no additional arguments" in err
assert "chartpress.yaml" not in err

0 comments on commit 243010f

Please sign in to comment.