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