2021-09-03 09:55:21 -04:00

215 lines
8.7 KiB
Plaintext

load('ext://global_vars', 'get_global', 'set_global')
def _get_skip():
return get_global(_get_skip_name())
def _set_skip(value):
set_global(_get_skip_name(), value)
def _get_skip_name():
return 'HELM_REMOTE_SKIP_UPDATES'
if _get_skip() == None:
_set_skip('False') # Gets set to true after the first update, preventing further updates within the same build/instance/up
def _find_root_tiltfile_dir():
# Find top-level Tilt path
current = os.path.abspath('./')
while current != '/':
if os.path.exists(os.path.join(current, 'tilt_modules')):
return current
current = os.path.dirname(current)
fail('Could not find root Tiltfile')
def _find_cache_dir():
from_env = os.getenv('TILT_HELM_REMOTE_CACHE_DIR', '')
if from_env != '':
return from_env
return os.path.join(_find_root_tiltfile_dir(), '.helm')
# this is the root directory into which remote helm charts will be pulled/cloned/untar'd
# use `os.putenv('TILT_HELM_REMOTE_CACHE_DIR', new_dir)` to change
helm_remote_cache_dir = _find_cache_dir()
watch_settings(ignore=helm_remote_cache_dir)
# TODO: =====================================
# if it ever becomes possible for loaded files to also load their own extensions
# this method can be replaced by `load('ext://namespace', 'namespace_create')
def namespace_create(name):
"""Returns YAML for a namespace
Args: name: The namespace name. Currently not validated.
"""
k8s_yaml(blob("""apiVersion: v1
kind: Namespace
metadata:
name: %s
""" % name))
# TODO: end TODO
# =====================================
def helm_remote(chart, repo_url='', repo_name='', release_name='', values=[], set=[], namespace='', version='', username='', password='', allow_duplicates=False, create_namespace=False):
# ======== Helper methods
def get_local_repo(repo_name, repo_url):
# if no repos are present, helm exit code is >0 and stderr output buffered
added_helm_repos = decode_yaml(local('helm repo list --output yaml 2>/dev/null || true', command_bat='helm repo list --output yaml 2> nul || ver>nul', quiet=True))
repo = [item for item in added_helm_repos if item['name'] == chart and item['url'] == repo_url] if added_helm_repos != None else []
return repo[0] if len(repo) > 0 else None
# Command string builder with common argument logic
def build_helm_command(command, auth=None, version=None):
command = 'helm ' + command
if auth != None:
username, password = auth
if username != '':
command += ' --username %s' % shlex.quote(username)
if password != '':
command += ' --password %s' % shlex.quote(password)
if version != None and version != 'latest':
command += ' --version %s' % shlex.quote(version)
return command
def fetch_chart_details(chart, repo_name, auth, version):
command = build_helm_command('search repo %s/%s --output yaml' % (shlex.quote(repo_name), shlex.quote(chart)), None, version)
results = decode_yaml(local(command, quiet=True))
return results[0] if len(results) > 0 else None
# ======== Condition Incoming Arguments
if repo_name == '':
repo_name = chart
if release_name == '':
release_name = chart
if namespace == '':
namespace = 'default'
if version == '':
version = 'latest'
# ======== Validate before we start trusting chart/repo names
# Based on helm chart conventions, and the fact we don't want anyone traversing directories
# validate is to essentially ensure there's no special characters aside from '-' being used
# str.isalnum accepts dots, which is only dangerous when slashes are allowed
# https://helm.sh/docs/chart_best_practices/conventions/#chart-names
if chart.replace('-', '').isalnum() == False or chart != chart.replace('.', ''):
# https://helm.sh/docs/chart_best_practices/conventions/#chart-names
fail('Chart name is not valid')
if repo_name != chart and repo_name.replace('-', '').isalnum() == False or repo_name != repo_name.replace('.', ''):
# https://helm.sh/docs/chart_best_practices/conventions/#chart-names
fail('Repo name is not valid')
if version != 'latest' and version != version.replace('/', '').replace('\\', ''):
fail('Version cannot contain a forward slash')
# ======== Determine state of existing helm repo
if repo_url != '':
local_helm_repo = get_local_repo(repo_name, repo_url)
if local_helm_repo == None:
# Unaware of repo, add it
repo_command = 'repo add %s %s' % (shlex.quote(repo_name), shlex.quote(repo_url))
# Add authentication for adding the repository if credentials are provided
output = str(local(build_helm_command(repo_command, (username, password)), quiet=True)).rstrip('\n')
if 'already exists' not in output: # repo was added
_set_skip('False')
else:
# Helm is already aware of the chart, update repo (unfortunately you cannot specify a single repo)
if _get_skip() != 'True':
repo_command = 'repo update'
local(build_helm_command(repo_command), quiet=True)
_set_skip('True')
# ======== Create Namespace
if create_namespace and namespace != '' and namespace != 'default':
# avoid a namespace not found error
namespace_create(namespace) # do this early so it manages to register before we attempt to install into it
# ======== Initialize
# -------- targets
pull_target = os.path.join(helm_remote_cache_dir, repo_name, version)
chart_target = os.path.join(pull_target, chart)
cached_chart_exists = os.path.exists(chart_target)
needs_pull = True
if cached_chart_exists:
# Helm chart structure is concrete, we can trust this YAML file to exist
cached_chart_details = read_yaml(os.path.join(chart_target, 'Chart.yaml'))
# check if our local cached chart matches latest remote
remote_chart_details = fetch_chart_details(chart, repo_name, (username, password), version)
# pull when version mismatch
needs_pull = cached_chart_details['version'] != remote_chart_details['version']
if needs_pull:
# -------- commands
pull_command = 'pull %s/%s --untar --destination %s' % (repo_name, chart, pull_target)
# ======== Perform Installation
if cached_chart_exists:
local('rm -rf %s' % chart_target, command_bat='if exist %s ( rd /s /q %s )' % (chart_target, chart_target), quiet=True)
local(build_helm_command(pull_command, (username, password), version), quiet=True)
install_crds(chart, chart_target)
# TODO: since neither `k8s_yaml()` nor `helm()` accept resource_deps,
# sometimes the crds haven't yet finished installing before the below tries
# to run
yaml = helm(chart_target, name=release_name, namespace=namespace, values=values, set=set)
# The allow_duplicates API is only available in 0.17.1+
if allow_duplicates and _version_tuple() >= [0, 17, 1]:
k8s_yaml(yaml, allow_duplicates=allow_duplicates)
else:
k8s_yaml(yaml)
return yaml
def _version_tuple():
ver_string = str(local('tilt version', quiet=True))
versions = ver_string.split(', ')
# pull first string and remove the `v` and `-dev`
version = versions[0].replace('-dev', '').replace('v', '')
return [int(str_num) for str_num in version.split(".")]
# install CRDs as a separate resource and wait for them to be ready
def install_crds(name, directory):
name += '-crds'
files = str(local(r"grep --include='*.yaml' --include='*.yml' -rEil '\bkind[^\w]+CustomResourceDefinition\s*$' %s || exit 0" % directory, quiet=True)).rstrip('\n')
if files == '':
files = []
else:
files = files.split("\n")
# we're applying CRDs directly and not using helm preprocessing
# this will cause errors!
# since installing CRDs in this function is a nice-to-have, just skip
# any that have preprocessing
files = [f for f in files if str(read_file(f)).find('{{') == -1]
if len(files) != 0:
local_resource(name+'-install', cmd='kubectl apply -f %s' % " -f ".join(files), deps=files) # we can wait/depend on this, but it won't cause a proper uninstall
k8s_yaml(files) # this will cause a proper uninstall, but we can't wait/depend on it
# TODO: Figure out how to avoid another named resource showing up in the tilt HUD for this waiter
local_resource(name+'-ready', resource_deps=[name+'-install'], cmd='kubectl wait --for=condition=Established crd --all') # now we can wait for those crds to finish establishing