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..4d582c1
--- /dev/null
+++ b/Tiltfile
@@ -0,0 +1,22 @@
+load('ext://helm_remote', 'helm_remote')
+helm_remote("postgresql",
+ repo_name='bitnami',
+ repo_url='https://charts.bitnami.com/bitnami',
+ set=["postgresqlPassword=password", "postgresqlDatabase=mev_inspect"],
+)
+
+load('ext://secret', 'secret_from_dict')
+k8s_yaml(secret_from_dict("mev-inspect-db-credentials", inputs = {
+ "username" : "postgres",
+ "password": "password",
+}))
+
+docker_build('mev-inspect', '.',
+ live_update=[
+ sync('.', '/app'),
+ run('cd /app && poetry install',
+ trigger='./pyproject.toml'),
+ ],
+)
+
+k8s_yaml("k8s/app.yaml")
diff --git a/alembic/env.py b/alembic/env.py
index 940ac85..cbb52c1 100644
--- a/alembic/env.py
+++ b/alembic/env.py
@@ -1,5 +1,3 @@
-import os
-
from logging.config import fileConfig
from sqlalchemy import engine_from_config
@@ -7,10 +5,12 @@ from sqlalchemy import pool
from alembic import context
+from mev_inspect.db import get_sqlalchemy_database_uri
+
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
-config.set_main_option("sqlalchemy.url", os.environ["SQLALCHEMY_DATABASE_URI"])
+config.set_main_option("sqlalchemy.url", get_sqlalchemy_database_uri())
# Interpret the config file for Python logging.
# This line sets up loggers basically.
diff --git a/k8s/app.yaml b/k8s/app.yaml
new file mode 100644
index 0000000..d88b977
--- /dev/null
+++ b/k8s/app.yaml
@@ -0,0 +1,39 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: mev-inspect-deployment
+ 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;" ]
+ env:
+ - name: POSTGRES_USER
+ valueFrom:
+ secretKeyRef:
+ name: mev-inspect-db-credentials
+ key: username
+ - name: POSTGRES_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: mev-inspect-db-credentials
+ key: password
+ livenessProbe:
+ exec:
+ command:
+ - ls
+ - /
+ initialDelaySeconds: 20
+ periodSeconds: 5
diff --git a/mev_inspect/db.py b/mev_inspect/db.py
index 862fe96..712b708 100644
--- a/mev_inspect/db.py
+++ b/mev_inspect/db.py
@@ -4,8 +4,16 @@ from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
+def get_sqlalchemy_database_uri():
+ username = os.getenv("POSTGRES_USER")
+ password = os.getenv("POSTGRES_PASSWORD")
+ server = "postgresql"
+ db_name = "mev_inspect"
+ return f"postgresql://{username}:{password}@{server}/{db_name}"
+
+
def get_engine():
- return create_engine(os.getenv("SQLALCHEMY_DATABASE_URI"))
+ return create_engine(get_sqlalchemy_database_uri())
def get_session():
diff --git a/pyproject.toml b/pyproject.toml
index 758c10f..04937a5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -42,6 +42,8 @@ attach = 'scripts.poetry.docker:attach'
exec = 'scripts.poetry.docker:exec'
inspect = 'scripts.poetry.inspect:inspect'
inspect-many = 'scripts.poetry.inspect:inspect_many'
+inspect-block = 'scripts.inspect_block:inspect_block'
+inspect-many-blocks = 'scripts.inspect_block:inspect_many_blocks'
[tool.black]
exclude = '''
diff --git a/scripts/__init__.py b/scripts/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tilt_modules/extensions.json b/tilt_modules/extensions.json
new file mode 100644
index 0000000..c59980c
--- /dev/null
+++ b/tilt_modules/extensions.json
@@ -0,0 +1,19 @@
+{
+ "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"
+ },
+ {
+ "Name": "secret",
+ "ExtensionRegistry": "https://github.com/tilt-dev/tilt-extensions",
+ "TimeFetched": "2021-09-09T08:57:26.199313-06: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"
+}
diff --git a/tilt_modules/secret/README.md b/tilt_modules/secret/README.md
new file mode 100644
index 0000000..b4e84f6
--- /dev/null
+++ b/tilt_modules/secret/README.md
@@ -0,0 +1,70 @@
+# Secret
+
+Author: [Nick Santos](https://github.com/nicks)
+
+Helper functions for creating Kubernetes secrets.
+
+## Functions
+
+### secret_yaml_generic
+
+```
+secret_yaml_generic(name: str, namespace: str = "", from_file: Union[str, List] = None, secret_type: str = None): Blob
+```
+
+Returns YAML for a generic secret.
+
+* `from_file` ( str ) – equivalent to `kubectl create secret --from-file`
+* `secret_type` ( str ) - equivalent to `kubectl create secret --type`
+
+### secret_create_generic
+
+```
+secret_create_generic(name: str, namespace: str = "", from_file: Union[str, List] = None, secret_type: str = None)
+```
+
+Deploys a secret to the cluster. Equivalent to
+
+```
+load('ext://secret', 'secret_yaml_generic')
+k8s_yaml(secret_yaml_generic('name', from_file=[...]))
+```
+
+### secret_from_dict
+
+```
+secret_from_dict(name: str, namespace: str = "", inputs = None): blob
+```
+
+Returns YAML for a secret from a dictionary.
+
+* `inputs` ( dict) - A dict of keys and values to use. Nesting is not supported
+
+## Example Usage
+
+### For a Postgres password:
+
+```
+load('ext://secret', 'secret_create_generic')
+secret_create_generic('pgpass', from_file='.pgpass=./.pgpass')
+```
+
+### For Google Cloud Platform Key:
+
+```
+load('ext://secret', 'secret_create_generic')
+secret_create_generic('gcp-key', from_file='key.json=./gcp-creds.json')
+```
+
+### From a dict:
+
+```
+load('ext://secret', 'secret_from_dict')
+k8s_yaml(secret_from_dict("secrets", inputs = {
+ 'SOME_TOKEN' : os.getenv('SOME_TOKEN')
+}))
+```
+
+## Caveats
+
+- This extension doesn't do any validation to confirm that names or namespaces are valid.
diff --git a/tilt_modules/secret/Tiltfile b/tilt_modules/secret/Tiltfile
new file mode 100644
index 0000000..bc5a75a
--- /dev/null
+++ b/tilt_modules/secret/Tiltfile
@@ -0,0 +1,105 @@
+# -*- mode: Python -*-
+
+def secret_yaml_generic(name, namespace="", from_file=None, secret_type=None, from_env_file=None):
+ """Returns YAML for a generic secret
+
+ Args:
+ name: The secret name.
+ namespace: The namespace.
+ from_file: Use the from-file secret generator. May be a string or a list of strings.
+ Example: ["ssh--privatekey=path/to/id_rsa", "ssh-publickey=path/to/id_rsa.pub"]
+ from_env_file: Specify the path to a file to read lines of key=val pairs to create a secret
+ (i.e. a Docker .env file)
+ secret_type (optional): Specify the type of the secret
+ Example: 'kubernetes.io/dockerconfigjson'
+
+ Returns:
+ The secret YAML as a blob
+ """
+
+ args = [
+ "kubectl",
+ "create",
+ "secret",
+ "generic",
+ name,
+ ]
+
+ if namespace:
+ args.extend(["-n", namespace])
+
+ generator = False
+ if from_file:
+ if type(from_file) == "string":
+ args.extend(["--from-file", from_file])
+ generator = True
+ elif type(from_file) == "list":
+ for f in from_file:
+ args.extend(["--from-file", f])
+ generator = True
+ else:
+ fail("Bad from_file argument: %s" % from_file)
+
+ if from_env_file:
+ if type(from_env_file) != "string":
+ fail("from_env_file only accepts strings")
+
+ args.extend(["--from-env-file", from_env_file])
+ generator = True
+
+ if not generator:
+ fail("No secret generator specified")
+
+ if secret_type:
+ if type(secret_type) == "string":
+ args.extend(["--type", secret_type])
+ else:
+ fail("Bad secret_type argument: %s" % secret_type)
+
+ args.extend(["-o=yaml", "--dry-run=client"])
+ return local(args)
+
+def secret_from_dict(name, namespace="", inputs={}):
+ """Returns YAML for a generic secret
+ Args:
+ name: The configmap name.
+ namespace: The namespace.
+ inputs: A dict of keys and values to use. Nesting is not supported
+ Returns:
+ The secret YAML as a blob
+ """
+
+ args = [
+ "kubectl",
+ "create",
+ "secret",
+ "generic",
+ name,
+ ]
+
+ if namespace:
+ args.extend(["-n", namespace])
+
+ if type(inputs) != "dict":
+ fail("Bad argument to secret_from_dict, inputs was not dict typed")
+
+ for k,v in inputs.items():
+ args.extend(["--from-literal", "%s=%s" % (k,v)])
+
+ args.extend(["-o=yaml", "--dry-run=client"])
+ return local(args, quiet=True)
+
+def secret_create_generic(name, namespace="", from_file=None, secret_type=None, from_env_file=None):
+ """Creates a secret in the current Kubernetes cluster.
+
+ Args:
+ name: The secret name.
+ namespace: The namespace.
+ from_file: Use the from-file secret generator. May be a string or a list of strings.
+ Example: ["ssh--privatekey=path/to/id_rsa", "ssh-publickey=path/to/id_rsa.pub"]
+ from_env_file: Specify the path to a file to read lines of key=val pairs to create a secret
+ (i.e. a Docker .env file)
+ secret_type (optional): Specify the type of the secret
+ Example: 'kubernetes.io/dockerconfigjson'
+ """
+ k8s_yaml(secret_yaml_generic(name, namespace, from_file, secret_type, from_env_file))
diff --git a/tilt_modules/secret/test/.pgpass b/tilt_modules/secret/test/.pgpass
new file mode 100644
index 0000000..f66b2c4
--- /dev/null
+++ b/tilt_modules/secret/test/.pgpass
@@ -0,0 +1 @@
+hostname:5432:database:username:password
\ No newline at end of file
diff --git a/tilt_modules/secret/test/Tiltfile b/tilt_modules/secret/test/Tiltfile
new file mode 100644
index 0000000..52dfde5
--- /dev/null
+++ b/tilt_modules/secret/test/Tiltfile
@@ -0,0 +1,7 @@
+load('../Tiltfile', 'secret_create_generic', 'secret_from_dict')
+
+k8s_yaml(secret_from_dict("secrets", inputs = {
+ 'SOME_TOKEN' : os.getenv('SOME_TOKEN')
+}))
+secret_create_generic('pgpass', namespace='default', from_file='.pgpass=./.pgpass')
+k8s_yaml('job.yaml')
diff --git a/tilt_modules/secret/test/job.yaml b/tilt_modules/secret/test/job.yaml
new file mode 100644
index 0000000..301b92f
--- /dev/null
+++ b/tilt_modules/secret/test/job.yaml
@@ -0,0 +1,33 @@
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: secret-verify
+spec:
+ backoffLimit: 1
+ template:
+ spec:
+ containers:
+ - name: secret-verify-dict
+ image: alpine
+ command: [ "/bin/echo", "$(SOME_TOKEN)" ]
+ env:
+ - name: TEST_VAR
+ valueFrom:
+ secretKeyRef:
+ name: secrets
+ key: SOME_TOKEN
+ - name: secret-verify
+ image: alpine
+ command: ["grep", "password", "/var/secrets/pgpass/.pgpass"]
+ volumeMounts:
+ - name: pgpass
+ mountPath: /var/secrets/pgpass
+ env:
+ - name: PGPASSFILE
+ value: /var/secrets/pgpass/.pgpass
+ restartPolicy: Never
+ volumes:
+ - name: pgpass
+ secret:
+ secretName: pgpass
+ defaultMode: 0600
diff --git a/tilt_modules/secret/test/test.sh b/tilt_modules/secret/test/test.sh
new file mode 100755
index 0000000..1305c33
--- /dev/null
+++ b/tilt_modules/secret/test/test.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+cd "$(dirname "$0")"
+
+export SOME_TOKEN=abc123
+
+set -ex
+tilt ci
+tilt down --delete-namespaces