Skip to content

Commit

Permalink
Update documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
ademariag committed Nov 30, 2024
1 parent 0e2ef6d commit 69c8e0b
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 64 deletions.
44 changes: 36 additions & 8 deletions kapitan/inputs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@


class InputType(object):
"""
Abstract base class for input types.
Provides methods for compiling input files. Subclasses should implement
the `compile_file` method to handle specific input formats.
"""

__metaclass__ = abc.ABCMeta

def __init__(self, compile_path, search_paths, ref_controller, target_name, args):
Expand All @@ -36,10 +44,14 @@ def __init__(self, compile_path, search_paths, ref_controller, target_name, args
self.args = args

def compile_obj(self, comp_obj: CompileInputTypeConfig):
"""
Expand globbed input paths, taking into account provided search paths
and run compile_input_path() for each resolved input_path.
kwargs are passed into compile_input_path()
"""Expand globbed input paths and compile each resolved input path.
Args:
comp_obj: CompileInputTypeConfig object containing input paths and other compilation options.
Raises:
CompileError: If an input path is not found and ignore_missing is False.
"""

# expand any globbed paths, taking into account provided search paths
Expand All @@ -61,9 +73,15 @@ def compile_obj(self, comp_obj: CompileInputTypeConfig):
self.compile_input_path(comp_obj, expanded_path)

def compile_input_path(self, comp_obj: CompileInputTypeConfig, input_path: str):
"""
Compile validated input_path in comp_obj
kwargs are passed into compile_file()
"""Compile a single input path.
Args:
comp_obj: CompileInputTypeConfig object.
input_path: Path to the input file.
Raises:
CompileError: If compilation fails.
"""
target_name = self.target_name
output_path = comp_obj.output_path
Expand All @@ -84,7 +102,17 @@ def compile_input_path(self, comp_obj: CompileInputTypeConfig, input_path: str):

@abc.abstractmethod
def compile_file(self, config: CompileInputTypeConfig, input_path: str, compile_path: str):
"""implements compilation for file_path to compile_path with ext_vars"""
"""Compile a single input file.
Args:
config: CompileInputTypeConfig object.
input_path: Path to the input file.
compile_path: Path to the output directory.
Raises:
NotImplementedError: This is an abstract method.
"""
return NotImplementedError


Expand Down
21 changes: 16 additions & 5 deletions kapitan/inputs/copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,38 @@

class Copy(InputType):
def compile_file(self, config: KapitanInputTypeCopyConfig, input_path, compile_path):
"""
Write items in path as plain rendered files to compile_path.
path can be either a file or directory.
"""Copy input_path to compile_path.
Args:
config (KapitanInputTypeCopyConfig): input configuration.
input_path (str): path to the file or directory to copy.
compile_path (str): path to the destination directory.
Raises:
OSError: if input_path does not exist and ignore_missing is False.
"""

# Whether to fail silently if the path does not exists.
ignore_missing = config.ignore_missing

try:
logger.debug("Copying %s to %s.", input_path, compile_path)
if os.path.exists(input_path):
logger.debug("Copying '%s' to '%s'.", input_path, compile_path)
if os.path.isfile(input_path):
if os.path.isfile(compile_path):
# overwrite existing file
shutil.copy2(input_path, compile_path)
else:
# create destination directory if it doesn't exist
os.makedirs(compile_path, exist_ok=True)
# copy file to destination directory
shutil.copy2(input_path, os.path.join(compile_path, os.path.basename(input_path)))
else:
# Resolve relative paths to avoid issues with copy_tree
compile_path = os.path.abspath(compile_path) # Resolve relative paths
copy_tree(input_path, compile_path)
elif not ignore_missing:
# Raise exception if input path does not exist and ignore_missing is False
raise OSError(f"Path {input_path} does not exist and `ignore_missing` is {ignore_missing}")
except OSError as e:
# Log exception and re-raise
logger.exception("Input dir not copied. Error: %s", e)
48 changes: 31 additions & 17 deletions kapitan/inputs/external.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,41 @@


class External(InputType):
"""
External input type. Executes an external command to generate Kubernetes manifests.
"""

env_vars: Dict[str, str] = {}
command_args: List[str] = []
env_vars: Dict[str, str] = {} #: Environment variables to pass to the external command.
command_args: List[str] = [] #: Command-line arguments to pass to the external command.

def set_env_vars(self, env_vars):
# Propagate HOME and PATH environment variables to external tool
# This is necessary, because calling `subprocess.run()` with `env` set doesn't propagate
# any environment variables from the current process.
# We only propagate HOME or PATH if they're present in Kapitan's environment, but aren't
# explicitly specified in the input's env_vars already. This ensures we don't run into
# issues when spawning the subprocess due to `None` values being present in the subprocess's
# environment.
"""
Sets environment variables for the external command.
Propagates HOME and PATH environment variables if they are not explicitly set.
This is necessary because `subprocess.run()` with `env` set doesn't propagate
environment variables from the current process. We only propagate HOME or PATH if they
exist in the Kapitan environment but aren't explicitly specified in `env_vars`. This
prevents issues when spawning the subprocess due to `None` values in the subprocess's
environment.
"""
if "PATH" not in env_vars and "PATH" in os.environ:
env_vars["PATH"] = os.environ["PATH"]
if "HOME" not in env_vars and "HOME" in os.environ:
env_vars["HOME"] = os.environ["HOME"]
self.env_vars = env_vars

def set_args(self, args):
def set_args(self, args: List[str]):
"""Sets command-line arguments for the external command."""
self.command_args = args

def compile_file(self, config: KapitanInputTypeExternalConfig, input_path, compile_path):
"""
Execute external with specific arguments and env vars.
If external exits with non zero error code, error is thrown
If the external command exits with a non-zero error code, an error is raised.
Args:
config: KapitanInputTypeExternalConfig object.
input_path: Path to the external command.
compile_path: Path to the compiled target directory.
"""

try:
Expand All @@ -54,23 +64,27 @@ def compile_file(self, config: KapitanInputTypeExternalConfig, input_path, compi

args = [external_path]
args.extend(self.command_args)
args = " ".join(args)
args_str = " ".join(args) # join args for logging and substitution

# compile_path (str): Path to current target compiled directory
# Substitute `${compiled_target_dir}` in the command arguments and environment variables.
compiled_target_pattern = re.compile(r"(\${compiled_target_dir})")
args = compiled_target_pattern.sub(compile_path, args)
# substitute `${compiled_target_dir}` in provided environment variables
args_str = compiled_target_pattern.sub(compile_path, args_str)
env_vars = {k: compiled_target_pattern.sub(compile_path, v) for (k, v) in config.env_vars.items()}

# Run the external command. shell=True is required for argument substitution to work correctly.
# However, this introduces a security risk if the input_path or command_args are not properly sanitized.
# Consider using the shlex module to properly quote and escape arguments to mitigate this risk.
# See https://docs.python.org/3/library/shlex.html for more information.

logger.debug("Executing external input with command '%s' and env vars '%s'.", args, env_vars)

external_result = subprocess.run(
args,
args_str,
env=env_vars,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True,
encoding="utf8",
encoding="utf-8",
)

logger.debug("External stdout: %s.", external_result.stdout)
Expand Down
49 changes: 37 additions & 12 deletions kapitan/inputs/helm.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,16 @@

class Helm(InputType):
def compile_file(self, config: KapitanInputTypeHelmConfig, input_path, compile_path):
"""
Render templates in file_path/templates and write to compile_path.
file_path must be a directory containing helm chart.
"""Render templates in input_path/templates and write to compile_path.
input_path must be a directory containing a helm chart.
kwargs:
reveal: default False, set to reveal refs on compile
target_name: default None, set to current target being compiled
Raises:
HelmTemplateError: if helm template fails
"""
helm_values_files = config.helm_values_files
helm_params = config.helm_params
Expand Down Expand Up @@ -69,13 +73,15 @@ def compile_file(self, config: KapitanInputTypeHelmConfig, input_path, compile_p
)
if error_message:
raise HelmTemplateError(error_message)
# Iterate over all files in the temporary directory

walk_root_files = os.walk(temp_dir)
for current_dir, _, files in walk_root_files:
for file in files: # go through all the template files
rel_dir = os.path.relpath(current_dir, temp_dir)
rel_file_name = os.path.join(rel_dir, file)
full_file_name = os.path.join(current_dir, file)
# Open each file and write its content to the compilation path
with open(full_file_name, "r") as f:
item_path = os.path.join(compile_path, rel_file_name)
os.makedirs(os.path.dirname(item_path), exist_ok=True)
Expand All @@ -88,6 +94,7 @@ def compile_file(self, config: KapitanInputTypeHelmConfig, input_path, compile_p
indent=indent,
) as fp:
yml_obj = list(yaml.safe_load_all(f))
# Write YAML objects to the compiled file
fp.write_yaml(yml_obj)
logger.debug("Wrote file %s to %s", full_file_name, item_path)

Expand All @@ -105,8 +112,13 @@ def render_chart(
helm_flags=None,
):
"""
Renders chart in chart_dir
Returns tuple with output and error_message
Renders helm chart located at chart_dir.
Args:
output_path: path to write rendered chart. If '-', returns rendered chart as string.
Returns:
tuple: (output, error_message)
"""
args = ["template"]

Expand All @@ -116,6 +128,7 @@ def render_chart(
if helm_flags is None:
helm_flags = HELM_DEFAULT_FLAGS

# Validate and process helm parameters
for param, value in helm_params.items():
if len(param) == 1:
raise ValueError(f"invalid helm flag: '{param}'. helm_params supports only long flag names")
Expand All @@ -138,6 +151,7 @@ def render_chart(
if param in HELM_DENIED_FLAGS:
raise ValueError(f"helm flag '{param}' is not supported.")

# Set helm flags
helm_flags[f"--{param}"] = value

# 'release_name' used to be the "helm template" [NAME] parameter.
Expand All @@ -149,6 +163,7 @@ def render_chart(
# name is used in place of release_name if both are specified
name = name or release_name

# Add flags to args list
for flag, value in helm_flags.items():
# boolean flag should be passed when present, and omitted when not specified
if isinstance(value, bool):
Expand All @@ -158,7 +173,7 @@ def render_chart(
args.append(flag)
args.append(str(value))

"""renders helm chart located at chart_dir, and stores the output to output_path"""
# Add values files to args list
if helm_values_file:
args.append("--values")
args.append(helm_values_file)
Expand All @@ -168,13 +183,15 @@ def render_chart(
args.append("--values")
args.append(file_name)

# Set output directory
if not output_file and output_path not in (None, "-"):
args.append("--output-dir")
args.append(output_path)

if "name_template" not in helm_flags:
args.append(name or "--generate-name")

# Add chart directory to args list
# uses absolute path to make sure helm interprets it as a
# local dir and not a chart_name that it should download.
args.append(chart_dir)
Expand All @@ -198,9 +215,13 @@ def render_chart(


def write_helm_values_file(helm_values: dict):
"""
Dump helm values into a yaml file whose path will
be passed over to helm binary
"""Dump helm values into a temporary YAML file.
Args:
helm_values: A dictionary containing helm values.
Returns:
str: The path to the temporary YAML file.
"""
_, helm_values_file = tempfile.mkstemp(".helm_values.yml", text=True)
with open(helm_values_file, "w") as fp:
Expand All @@ -211,17 +232,21 @@ def write_helm_values_file(helm_values: dict):

class HelmChart(BaseModel):
"""
Returns rendered helm chart in chart_dir
Each rendered file will be a key in self.root
Represents a Helm chart. Renders the chart and stores the rendered objects in self.root.
Args:
chart_dir: Path to the Helm chart directory.
Requires chart_dir to exist (it will not download it)
Raises:
HelmTemplateError: if helm template fails
"""

chart_dir: str
helm_params: dict = {}
helm_values: dict = {}
helm_path: str = None

# Load and process the Helm chart
def new(self):
for obj in self.load_chart():
self.root[f"{obj['metadata']['name'].lower()}-{obj['kind'].lower().replace(':','-')}"] = (
Expand Down
21 changes: 14 additions & 7 deletions kapitan/inputs/jinja2.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,18 @@ class Jinja2(InputType):

def compile_file(self, config: KapitanInputTypeJinja2Config, input_path, compile_path):
"""
Write items in path as jinja2 rendered files to compile_path.
path can be either a file or directory.
kwargs:
reveal: default False, set to reveal refs on compile
target_name: default None, set to current target being compiled
Compile Jinja2 templates.
Write items in ``input_path`` as Jinja2 rendered files to ``compile_path``.
``input_path`` can be either a file or directory. The rendered files will be written
to ``compile_path``.
Args:
config (KapitanInputTypeJinja2Config): Jinja2 input configuration.
input_path (str): Path to the input Jinja2 template file or directory.
compile_path (str): Path to write the compiled files.
See ``kapitan.inputs.base.InputType`` for details on ``reveal`` and ``target_name``.
"""
strip_postfix = config.suffix_remove
stripped_postfix = config.suffix_stripped
Expand All @@ -34,8 +41,8 @@ def compile_file(self, config: KapitanInputTypeJinja2Config, input_path, compile
target_name = self.target_name

# set compile_path allowing jsonnet to have context on where files
# are being compiled on the current kapitan run
# we only do this if user didn't pass its own value
# are being compiled during the current Kapitan run. This is only done if the user
# did not provide their own value.
input_params.setdefault("compile_path", compile_path)

# set ext_vars and inventory for jinja2 context
Expand Down
Loading

0 comments on commit 69c8e0b

Please sign in to comment.