diff --git a/.gitignore b/.gitignore
index 12081c9..40ea1a2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,6 @@ htmlcov
# don't commit cache
cache
+
+# k8s
+.helm
diff --git a/Dockerfile b/Dockerfile
index 5041e59..bc4fba2 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,14 +6,16 @@ RUN pip install -U pip \
ENV PATH="${PATH}:/root/.poetry/bin"
-COPY . /app
+COPY ./pyproject.toml /app/pyproject.toml
+COPY ./poetry.lock /app/poetry.lock
WORKDIR /app/
-# poetry uses virtual env by default, turn this off inside container
RUN poetry config virtualenvs.create false && \
poetry install
+COPY . /app
+
# easter eggs 😝
RUN echo "PS1='🕵️:\[\033[1;36m\]\h \[\033[1;34m\]\W\[\033[0;35m\]\[\033[1;36m\]$ \[\033[0m\]'" >> ~/.bashrc
-CMD /bin/bash
+CMD ["/bin/bash"]
diff --git a/Tiltfile b/Tiltfile
new file mode 100644
index 0000000..078fdfb
--- /dev/null
+++ b/Tiltfile
@@ -0,0 +1,16 @@
+load('ext://helm_remote', 'helm_remote')
+helm_remote("postgresql",
+ repo_name='bitnami',
+ repo_url='https://charts.bitnami.com/bitnami',
+ values=["k8s/postgresql/values_dev.yaml"]
+)
+
+docker_build('mev-inspect', '.',
+ live_update=[
+ sync('.', '/app'),
+ run('cd /app && poetry install',
+ trigger='./pyproject.toml'),
+ ],
+)
+
+k8s_yaml("k8s/app.yaml")
diff --git a/k8s/app.yaml b/k8s/app.yaml
new file mode 100644
index 0000000..3097ee2
--- /dev/null
+++ b/k8s/app.yaml
@@ -0,0 +1,28 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: mev-inspect
+ labels:
+ app: mev-inspect
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: mev-inspect
+ template:
+ metadata:
+ labels:
+ app: mev-inspect
+ spec:
+ containers:
+ - name: mev-inspect
+ image: mev-inspect:latest
+ command: [ "/bin/bash", "-c", "--" ]
+ args: [ "while true; do sleep 30; done;" ]
+ livenessProbe:
+ exec:
+ command:
+ - ls
+ - /
+ initialDelaySeconds: 20
+ periodSeconds: 5
diff --git a/k8s/postgresql/values_dev.yaml b/k8s/postgresql/values_dev.yaml
new file mode 100644
index 0000000..46f8c77
--- /dev/null
+++ b/k8s/postgresql/values_dev.yaml
@@ -0,0 +1,5 @@
+global:
+ postgresql:
+ postgresqlDatabase: "mev_inspect"
+ postgresqlUsername: "postgres"
+ postgresqlPassword: "password"
diff --git a/tilt_modules/extensions.json b/tilt_modules/extensions.json
new file mode 100644
index 0000000..28c435a
--- /dev/null
+++ b/tilt_modules/extensions.json
@@ -0,0 +1,14 @@
+{
+ "Extensions": [
+ {
+ "Name": "helm_remote",
+ "ExtensionRegistry": "https://github.com/tilt-dev/tilt-extensions",
+ "TimeFetched": "2021-09-03T08:56:46.938205-04:00"
+ },
+ {
+ "Name": "global_vars",
+ "ExtensionRegistry": "https://github.com/tilt-dev/tilt-extensions",
+ "TimeFetched": "2021-09-03T08:56:48.751933-04:00"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tilt_modules/global_vars/README.md b/tilt_modules/global_vars/README.md
new file mode 100644
index 0000000..49ce8f8
--- /dev/null
+++ b/tilt_modules/global_vars/README.md
@@ -0,0 +1,13 @@
+# Git Resource
+
+Author: [Bob Jackman](https://github.com/kogi)
+
+An extension for reading/writing global variable values
+
+## Usage
+
+```python
+set_global('foo', some_value)
+print(get_global('foo'))
+unset_global('foo')
+```
diff --git a/tilt_modules/global_vars/Tiltfile b/tilt_modules/global_vars/Tiltfile
new file mode 100644
index 0000000..1bdaf99
--- /dev/null
+++ b/tilt_modules/global_vars/Tiltfile
@@ -0,0 +1,11 @@
+def get_global(name):
+ return os.getenv(_get_env_name(name))
+
+def set_global(name, value):
+ os.putenv(_get_env_name(name), value)
+
+def unset_global(name):
+ os.unsetenv(_get_env_name(name))
+
+def _get_env_name(name):
+ return 'TILT_GLOBAL_VAR_%s' % name.upper()
diff --git a/tilt_modules/helm_remote/README.md b/tilt_modules/helm_remote/README.md
new file mode 100644
index 0000000..ec62bc4
--- /dev/null
+++ b/tilt_modules/helm_remote/README.md
@@ -0,0 +1,41 @@
+# Helm Remote
+
+Author: [Bob Jackman](https://github.com/kogi)
+
+Install a remotely hosted Helm chart in a way that it will be properly uninstalled when running `tilt down`
+
+## Usage
+
+#### Install a Remote Chart
+
+```py
+load('ext://helm_remote', 'helm_remote')
+helm_remote('myChartName')
+```
+
+##### Additional Parameters
+
+```
+helm_remote(chart, repo_url='', repo_name='', release_name='', namespace='', version='', username='', password='', values=[], set=[])
+```
+
+* `chart` ( str ) – the name of the chart to install
+* `repo_name` ( str ) – the name of the repo within which to find the chart (assuming the repo is already added locally)
+
if omitted, defaults to the same value as `chart_name`
+* `repo_url` ( str ) – the URL of the repo within which to find the chart (equivalent to `helm repo add `)
+* `release_name` (str) - the name of the helm release
+
if omitted, defaults to the same value as `chart_name`
+* `namespace` ( str ) – the namespace to deploy the chart to (equivalent to helm's `--namespace ` flags)
+* `version` ( str ) – the version of the chart to install. If omitted, defaults to latest version (equivalent to helm's `--version` flag)
+* `username` ( str ) – repository authentication username, if needed (equivalent to helm's `--username` flag)
+* `password` ( str ) – repository authentication password, if needed (equivalent to helm's `--password` flag)
+* `values` ( Union [ str , List [ str ]]) – Specify one or more values files (in addition to the values.yaml file in the chart). Equivalent to the helm's `--values` or `-f` flags
+* `set` ( Union [ str , List [ str ]]) – Directly specify one or more values (equivalent to helm's `--set` flag)
+* `allow_duplicates` ( bool ) - Allow duplicate resources. Usually duplicate resources indicate a programmer error.
+ But some charts specify resources twice.
+* `create_namespace` ( bool ) - Create the namespace specified in `namespace` ( equivalent to helm's `--create_namespace` flag)
+
+#### Change the Cache Location
+
+By default `helm_remote` will store retrieved helm charts in the `.helm` directory at your workspace's root.
+This location can be customized by calling `os.putenv('TILT_HELM_REMOTE_CACHE_DIR', new_directory)` before loading the module.
diff --git a/tilt_modules/helm_remote/Tiltfile b/tilt_modules/helm_remote/Tiltfile
new file mode 100644
index 0000000..3e59792
--- /dev/null
+++ b/tilt_modules/helm_remote/Tiltfile
@@ -0,0 +1,214 @@
+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
diff --git a/tilt_modules/helm_remote/test/Dockerfile b/tilt_modules/helm_remote/test/Dockerfile
new file mode 100644
index 0000000..695df82
--- /dev/null
+++ b/tilt_modules/helm_remote/test/Dockerfile
@@ -0,0 +1,7 @@
+FROM alpine
+
+RUN apk update && apk add expect busybox-extras
+
+ADD ./verify.exp ./verify.exp
+
+ENTRYPOINT expect < verify.exp
diff --git a/tilt_modules/helm_remote/test/Tiltfile b/tilt_modules/helm_remote/test/Tiltfile
new file mode 100644
index 0000000..3c1e482
--- /dev/null
+++ b/tilt_modules/helm_remote/test/Tiltfile
@@ -0,0 +1,17 @@
+os.putenv('TILT_HELM_REMOTE_CACHE_DIR', os.path.abspath('./.helm'))
+load('../Tiltfile', 'helm_remote')
+
+# Note that .helm is in the .tiltignore!
+helm_remote('memcached', repo_url='https://charts.bitnami.com/bitnami')
+if not os.path.exists('./.helm/memcached'):
+ fail('memcached failed to load in the right directory')
+
+# This chart has a bunch of CRDs (including templated CRDs), so we can test the CRD init logic.
+helm_remote('gloo', repo_url='https://storage.googleapis.com/solo-public-helm',
+ # The gloo chart has duplicate resources, see discussion here:
+ # https://github.com/tilt-dev/tilt/issues/3656
+ allow_duplicates=True)
+
+docker_build('helm-remote-test-verify', '.')
+k8s_yaml('job.yaml')
+k8s_resource('helm-remote-test-verify', resource_deps=['memcached'])
diff --git a/tilt_modules/helm_remote/test/job.yaml b/tilt_modules/helm_remote/test/job.yaml
new file mode 100644
index 0000000..2674d5d
--- /dev/null
+++ b/tilt_modules/helm_remote/test/job.yaml
@@ -0,0 +1,12 @@
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: helm-remote-test-verify
+spec:
+ backoffLimit: 1
+ template:
+ spec:
+ containers:
+ - name: helm-remote-test-verify
+ image: helm-remote-test-verify
+ restartPolicy: Never
\ No newline at end of file
diff --git a/tilt_modules/helm_remote/test/test.sh b/tilt_modules/helm_remote/test/test.sh
new file mode 100755
index 0000000..9aaf15c
--- /dev/null
+++ b/tilt_modules/helm_remote/test/test.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+cd "$(dirname "$0")"
+
+set -ex
+tilt ci
+tilt down --delete-namespaces
diff --git a/tilt_modules/helm_remote/test/verify.exp b/tilt_modules/helm_remote/test/verify.exp
new file mode 100644
index 0000000..37e0636
--- /dev/null
+++ b/tilt_modules/helm_remote/test/verify.exp
@@ -0,0 +1,12 @@
+#!/usr/bin/expect
+
+spawn telnet memcached 11211
+expect {
+ timeout {exit 1}
+ "Connected"
+}
+send "stats\r\n"
+expect {
+ timeout {exit 1}
+ "STAT pid 1"
+}