-
Notifications
You must be signed in to change notification settings - Fork 169
/
Tiltfile
237 lines (182 loc) · 9.83 KB
/
Tiltfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
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
# Find the directory where Tilt loaded modules.
# This is usually the XDG data directory for tilt data.
def _find_tilt_modules_load_dir():
current = os.path.abspath('./')
while True:
if os.path.exists(os.path.join(current, 'tilt_modules')):
return current
next_dir = os.path.dirname(current)
if next_dir == current:
fail('Could not find root Tiltfile')
current = next_dir
def _find_cache_dir():
from_env = os.getenv('TILT_HELM_REMOTE_CACHE_DIR', '')
if from_env != '':
return from_env
return os.path.join(_find_tilt_modules_load_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, install_crds=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
if install_crds:
_install_crds(chart, _chart_target(chart, repo_name, version))
# 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_remote_yaml(chart, repo_url, repo_name, release_name, values, set, namespace, version, username, password)
# 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 helm_remote_yaml(chart, repo_url='', repo_name='', release_name='', values=[], set=[], namespace='', version='', username='', password=''):
# ======== 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'] == repo_name 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):
args = ['helm']
args.extend(command)
if auth != None:
username, password = auth
if username != '':
args.extend(['--username', username])
if password != '':
args.extend(['--password', password])
if version != None and version != 'latest':
args.extend(['--version', version])
return args
def fetch_chart_details(chart, repo_name, auth, version):
if repo_name.startswith('oci://'):
command = build_helm_command(['show', 'chart', '%s/%s' % (repo_name, chart)], None, version)
results = decode_yaml(local(command, quiet=True))
return results
else:
command = build_helm_command(['search', 'repo', '%s/%s' % (repo_name, chart), '--output', 'yaml'], 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
if not repo_name.startswith('oci://'):
fail('Repo name is not valid')
if version != 'latest' and version != version.replace('/', '').replace('\\', ''):
fail('Version cannot contain a forward slash')
is_oci = repo_url.startswith('oci://')
# ======== Determine state of existing helm repo
# helm does not support "repo {add, update}" for OCI based charts,
# see https://helm.sh/docs/topics/registries/#other-subcommands
if repo_url != '' and not is_oci:
local_helm_repo = get_local_repo(repo_name, repo_url)
if local_helm_repo == None:
# Unaware of repo, add it
repo_command = ['repo', 'add', repo_name, 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')
# ======== Initialize
# -------- targets
if is_oci:
repo_name = repo_url
chart_target = _chart_target(chart, repo_name, version)
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' % (repo_name, chart), '--untar', '--destination', _pull_target(repo_name, version)]
# ======== Perform Installation
if cached_chart_exists:
local('rm -rf %s' % shlex.quote(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)
yaml = helm(chart_target, name=release_name, namespace=namespace, values=values, set=set)
return yaml
def _pull_target(repo_name, version):
return os.path.join(helm_remote_cache_dir, repo_name.replace(':', ''), version)
def _chart_target(chart, repo_name, version):
return os.path.join(_pull_target(repo_name, version), chart)
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