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" +}