Skip to content

Commit

Permalink
Merge pull request #646 from projectsyn/feat/improve-kustomize-support
Browse files Browse the repository at this point in the history
Improve support for `kustomize` in Commodore
  • Loading branch information
simu authored Oct 11, 2022
2 parents dd32bc1 + 75cdebe commit 65f3c42
Show file tree
Hide file tree
Showing 10 changed files with 283 additions and 5 deletions.
3 changes: 3 additions & 0 deletions commodore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@

# provide Commodore installation dir as variable that can be imported
__install_dir__ = P(__file__).parent

# Location of Kustomize wrapper script
__kustomize_wrapper__ = __install_dir__ / "scripts" / "run-kustomize"
2 changes: 2 additions & 0 deletions commodore/cluster.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import click

from . import __kustomize_wrapper__
from .helpers import (
lieutenant_query,
yaml_dump,
Expand Down Expand Up @@ -160,6 +161,7 @@ def render_target(
}
if not bootstrap:
parameters["_base_directory"] = str(components[component].target_directory)
parameters["_kustomize_wrapper"] = str(__kustomize_wrapper__)

for c in components:
if inv.defaults_file(c).is_file():
Expand Down
2 changes: 2 additions & 0 deletions commodore/component/compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import git
from kapitan.resources import inventory_reclass

from commodore import __kustomize_wrapper__
from commodore.config import Config
from commodore.component import Component
from commodore.dependency_mgmt.component_library import (
Expand Down Expand Up @@ -250,6 +251,7 @@ def _prepare_kapitan_inventory(
"parameters": {
"_instance": instance_name,
"_base_directory": str(component.target_directory),
"_kustomize_wrapper": str(__kustomize_wrapper__),
},
},
inv.target_file(instance_name),
Expand Down
38 changes: 38 additions & 0 deletions commodore/lib/commodore.libjsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,43 @@ local generateResources(resources, resourceFn) =
]
);

/**
*
* \brief Generate a kustomization overlay
*
* \arg base_url The URL of the base kustomization
* \arg base_version The version of the base kustomization
* \arg images An object with keys referring to container image URIs
* used in the base and values providing `newTag` and
* `newName` to apply.
* \arg kustomize_input User-provided content to merge into the overlay
*
* \returns an object suitable as a Jsonnet output to generate a
* `kustomization.yaml` to be passed to `kustomize build`
*/
local kustomization(base_url, base_version='', images={}, kustomize_input={}) = {
// Generate `kustomization.yaml` as output
kustomization: {
// Configure the provided kustomization as a base for our overlay
resources: [
if base_version != '' then
'%s?ref=%s' % [ base_url, base_version ]
else
base_url,
],
// Render `images` from the provided parameter
images: [
{
name: img,
newTag: images[img].newTag,
newName: images[img].newName,
}
for img in std.objectFields(images)
],
// Inject the kustomize input provided in the component parameters
} + makeMergeable(kustomize_input),
};

{
inventory: inventory,
list_dir: list_dir,
Expand All @@ -424,4 +461,5 @@ local generateResources(resources, resourceFn) =
fixupDir: fixupDir,
renderArray: renderArray,
generateResources: generateResources,
Kustomization: kustomization,
}
38 changes: 38 additions & 0 deletions commodore/scripts/run-kustomize
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/bin/bash
#
# The wrapper always calls `kustomize build`. To use the wrapper provide the
# directory in which the output should be written as the first argument. We
# need to pass the output directory as an argument, because otherwise Kapitan
# won't substitute `${compiled_target_dir}` with the path of the compilation
# target directory. To avoid having to reimplement kustomize argument parsing,
# we require that the output directory is the first argument.
# Further arguments are passed to kustomize as provided. The input directory
# is expected to be provided in environment variable ${INPUT_DIR}.
#
# export INPUT_DIR=/path/to/kustomization
# run-kustomize <OUTPUT_DIR> [kustomize args...]
#
# Wrapper around kustomize which provides some convenience features
# 1) The wrapper searches for the kustomize binary in ${PATH}
# 2) The wrapper ensures that the user provides the expected arguments
# 3) The wrapper ensures that the provided output directory exists
#
set -e

# Kapitan provides a fairly standard PATH variable, we add /opt/homebrew/bin for macOS
export PATH="${PATH}:/opt/homebrew/bin"

kustomize=$(which kustomize) || (>&2 echo "kustomize not found in ${PATH}"; exit 7)

if [ -z "${INPUT_DIR}" ]; then
(>&2 echo "INPUT_DIR environment variable not provided"; exit 2)
fi

# Assumption: output dir provided as first arg
readonly output_dir="$1"
if [ -z "${output_dir}" ]; then
(>&2 echo "First argument is empty, expected output directory as first argument"; exit 2)
fi
mkdir -p "${output_dir}"

exec "$kustomize" build "${INPUT_DIR}" -o "${@}"
79 changes: 79 additions & 0 deletions docs/modules/ROOT/pages/reference/commodore-libjsonnet.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,82 @@ stringData:
secret: another
type: Opaque
----

== `Kustomization(base_url, base_version='', images={}, kustomize_input={})`

This function generates a Kustomize overlay which uses the parameter `base_url` as a base resource.

The parameters `base_url` and `base_version` are used to format an entry in the kustomization's https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/resource/[resources] field.

=== Arguments

`base_url`:: The URL of the base kustomization
`base_version`:: The version of the base kustomization.
The version can be any Git reference understood by `kustomize`.
If version is the empty string, the base kustomization is added to `resources` without the `?ref=<version>` suffix
`images`:: An object with keys referring to container image URIs used in the base and values providing `newTag` and `newName` to apply.
The contents of parameter `images` are transformed into entries in the kustomization's https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/images/[`images`] field.
`kustomize_input`:: User-provided content to merge into the overlay
This variable is merged into the kustomization without further modifications.

=== Return value

An object suitable as a Jsonnet output to generate a `kustomization.yaml` to be passed to `kustomize build`.

=== Example

Let's look at what results from the following configuration, where `defaults.yml` is assumed to be a reclass class and `kustomization.jsonnet` a Commodore component Jsonnet script.

.defaults.yml
[source,yaml]
----
parameters:
<component-name>:
namespace: syn-example
images:
example:
registry: quay-mirror.syn.tools
repository: example
tag: v1.0.0
kustomize_input:
namespace: ${<component_name>:namespace}
----

.kustomization.jsonnet
[source,jsonnet]
----
// Omitted `local params = ...` which reads the configuration
// In the example the configuration has field `images` which specifies the
// container images, and field `kustomize_input` which provides additional
// kustomization configs.
//local params = ...;
// Render `kustomization.yaml`
com.Kustomization(
'https://syn.example.com/example//config/default',
'v1.0.0',
{
// Assumes that `params.images.example` is formatted according to Commodore
// component best practices
'quay.io/syn/example': {
newTag: params.images.example.tag,
newName: '%(registry)s/%(repository)s' % params.images.example
}
},
params.kustomize_input,
)
----

The rendered output will then be a single `kustomization.yaml` as shown below.

.kustomization.yaml
[source,yaml]
----
images:
- name: quay.io/syn/example
newName: quay-mirror.syn.tools/example
newTag: v1.0.0
namespace: syn-example
resources:
- https://syn.example.com/example//config/default?ref=v1.0.0
----
20 changes: 19 additions & 1 deletion docs/modules/ROOT/pages/reference/parameters.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This class is created by Commodore in file `inventory/classes/params/cluster.yml

The class is included in each Kapitan target with the lowest precedence of all classes.

== Parameters
== Global parameters

=== `cluster`

Expand Down Expand Up @@ -68,3 +68,21 @@ parameters:
----
<1> The parameter is overwritten using dynamic facts in the Project Syn installation's global configuration repository.
====

== Component-specific parameters

Commodore adds some "meta-parameters" to each component's Kapitan target.
These are provided to simplify component configurations.

Commodore provides the following component-specific top-level parameters

=== `_base_directory`

This parameter provides the absolute path to the component's base directory.
This parameter is intended for component authors to use in `kapitan.compile` and `kapitan.dependencies` entries when referencing files in the component directory.

=== `_kustomize_wrapper`

This parameter provides the absolute path to the Kustomize wrapper script bundled with Commodore.
This parameter is intended for component authors to use to call Kustomize in components.
See the xref:syn:ROOT:explanations/commodore-components/kustomizations.adoc[Kustomization best practices] for more details.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ packages = [
include = [
"commodore/lib/commodore.libjsonnet",
"commodore/filters/helm_namespace.jsonnet",
"commodore/scripts/run-kustomize",
]

[tool.poetry.dependencies]
Expand Down
99 changes: 98 additions & 1 deletion tests/test_component_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def _prepare_component(
cli_runner: RunnerFunc,
component_name: str = "test-component",
subpath: str = "",
):
) -> P:
if not subpath:
call_component_new(tmp_path, cli_runner, lib="--lib")
component_root = tmp_path / "dependencies" / component_name
Expand Down Expand Up @@ -317,3 +317,100 @@ def test_component_compile_no_repo(tmp_path: P, cli_runner: RunnerFunc):
target = yaml.safe_load(file)
assert target["kind"] == "ServiceAccount"
assert target["metadata"]["namespace"] == "syn-test-component"


def test_component_compile_kustomize(tmp_path: P, cli_runner: RunnerFunc):
component_name = "test-component"
component_path = _prepare_component(tmp_path, cli_runner, component_name)

with open(
component_path / "component" / "kustomization.jsonnet", "w", encoding="utf-8"
) as f:
f.write(
"""
local com = import 'lib/commodore.libjsonnet';
com.Kustomization(
'https://github.com/appuio/appuio-cloud-agent//config/default',
'v0.5.0',
{
'ghcr.io/appuio/appuio-cloud-agent': {
newTag: 'v0.5.0',
newName: 'ghcr.io/appuio/appuio-cloud-agent',
},
},
{
namespace: 'foo',
}
)"""
)
with open(
component_path / "class" / f"{component_name}.yml", "r", encoding="utf-8"
) as cyaml:
component_yaml = yaml.safe_load(cyaml)

component_yaml["parameters"]["kapitan"]["compile"].extend(
[
{
"input_type": "jsonnet",
"input_paths": ["${_base_directory}/component/kustomization.jsonnet"],
"output_path": "${_base_directory}/kust/",
},
{
"input_type": "external",
"input_paths": ["${_kustomize_wrapper}"],
"output_path": ".",
"env_vars": {
"INPUT_DIR": "${_base_directory}/kust",
},
"args": [
"\\${compiled_target_dir}/${_instance}/",
],
},
]
)

with open(
component_path / "class" / f"{component_name}.yml", "w", encoding="utf-8"
) as cyaml:
yaml.safe_dump(component_yaml, cyaml)

exit_status = call(
_cli_command_string(tmp_path, component_name),
shell=True,
)

assert exit_status == 0

kustomization = component_path / "kust" / "kustomization.yaml"
assert kustomization.is_file()
with open(kustomization, "r", encoding="utf-8") as f:
kustomization_yaml = yaml.safe_load(f)
assert set(kustomization_yaml.keys()) == {"images", "namespace", "resources"}
assert kustomization_yaml["namespace"] == "foo"
assert len(kustomization_yaml["resources"]) == 1
assert (
kustomization_yaml["resources"][0]
== "https://github.com/appuio/appuio-cloud-agent//config/default?ref=v0.5.0"
)
assert len(kustomization_yaml["images"]) == 1
assert kustomization_yaml["images"][0] == {
"name": "ghcr.io/appuio/appuio-cloud-agent",
"newName": "ghcr.io/appuio/appuio-cloud-agent",
"newTag": "v0.5.0",
}

rendered_manifests = (
tmp_path / "testdir" / "compiled" / component_name / component_name
)
assert rendered_manifests.is_dir()
with open(
rendered_manifests / "apps_v1_deployment_appuio-cloud-agent.yaml",
"r",
encoding="utf-8",
) as f:
deploy = yaml.safe_load(f)
assert deploy["kind"] == "Deployment"
assert deploy["metadata"]["namespace"] == "foo"
container = deploy["spec"]["template"]["spec"]["containers"][0]
assert container["image"] == "ghcr.io/appuio/appuio-cloud-agent:v0.5.0"
Loading

0 comments on commit 65f3c42

Please sign in to comment.