I am working on a page for htmx-patterns and I ran into a situation with lots of duplication. Especially when i am using tailwind I run into situations where the duplication can get tedious to maintiain. The solution I found is macros.
Now I can use the same code for all of my links, and call the macro to use it.
{% macro link(id, text, boosted=false) -%}
<a
class="
{% if id is none %}
pointer-events-none bg-terminal-950 text-terminal-900 ring-terminal-900
{% else %}
bg-terminal-950 hover:bg-terminal-900 hover:text-terminal-400 text-terminal-500 shadow-lg shadow-terminal-300/20 hover:shadow-terminal-300/30 ring-terminal-300
{% endif %}
cursor-pointer block text-center font-bold py-2 px-4 rounded w-full ring-1
"
{% if id is not none %}
href="{{ url_for('boosted', id=id) }}"
{% endif %}
{% if boosted %}
hx-boost="true"
{% endif %}>
{{ text }}
</a>
{%- endmacro %}
<h2 class='text-3xl font-light mt-0 max-w-xl text-center prose-xl mt-8 text-terminal-500'>
Boosted Links
</h2>
<div class='flex flex-row gap-4'>
{{ link(prev_id, 'Previous', boosted=True) }}
{{ link(next_id, 'Next', boosted=True) }}
</div>
<h2 class='text-3xl font-light mt-0 max-w-xl text-center prose-xl mt-8 text-terminal-500'>
Normal Links
</h2>
<div class='flex flex-row gap-4'>
{{ link(prev_id, 'Previous', boosted=False) }}
{{ link(next_id, 'Next', boosted=False) }}
</div>
jinja has a loop variable that is very handy to use with htmx. Whether you want to implement a click to load more or an infinite scroll this loop variable is very handy.
{% for person in persons %}
<li
{% if loop.last %}
hx-get="{{ url_for('infinite', page=next_page) }}"
hx-trigger="intersect once"
hx-target="#persons"
hx-swap='beforeend'
hx-indicator="#persons-loading"
{% endif %}
{{ person.name.upper() }} -
{{ person.phone_number }}
</li>
{% endfor %}
Now for every chunk of contacts that we load we will trigger the infinite scroll by loading more once the last one has intersected the screen.
Out of the box FastAPI.">Starlette does not support url_for with query params. When trying to use url_for with query params it throws the following error.
starlette.routing.NoMatchFound: No route exists for name "infinite" and params "page"
In my searching for this I found starlette issue #560 quite helpful, but not complete, as it did not work for me.
import jinja2
if hasattr(jinja2, "pass_context"):
pass_context = jinja2.pass_context
else:
pass_context = jinja2.contextfunction
@pass_context
def url_for_query(context: dict, name: str, **params: dict) -> str:
request = context["request"]
url = str(request.url_for(name))
if params == {}:
return url
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
# Parse the URL
parsed_url = urlparse(url)
# Parse the query parameters
query_params = parse_qs(parsed_url.query)
# Update the query parameters with the new ones
query_params.update(params)
# Rebuild the query string
updated_query_string = urlencode(query_params, doseq=True)
# Rebuild the URL with the updated query string
updated_url = urlunparse(
(
parsed_url.scheme,
parsed_url.netloc,
parsed_url.path,
parsed_url.params,
updated_query_string,
parsed_url.fragment,
)
)
if os.environ.get("ENV") in ["dev", "qa", "prod"]:
updated_url = updated_url.replace("http", "https", 1)
return updated_url
def get_templates():
templates = Jinja2Templates(directory="templates")
templates.env.globals["url_for"] = url_for_query
return templates
https
If you want url_for to work in production you need some way to convert http
to https. Here is how I make it work, for local development I export ENV=local then for each environment that I am running on a server I include
it in the list and update ENV appropriately.
if os.environ.get("ENV") in ["dev", "qa", "prod"]:
updated_url = updated_url.replace("http", "https", 1)
The route might look something like this.
@infinite_router.get("/")
async def home(request: Request, page: int = 1, n: int = 10):
...
To access the home route using url_for in a jinja template you can use the
following, once you have applied the url_for_query function as your default
url_for
<a href="{{ url_for('home', page=1) }}">Home</a>
Kind (Kubernetes in Docker) is a tool that makes it easy to create and tear down local clusters quickly. I like to use it to test out new workflows.
Argocd is a continuous delivery tool that makes it easy to setup gitops workflows in kubernetes.
Here is how you can setup a new kind cluster and install argocd into it using helm, the kubernetes package manager.
kind create cluster --name argocd
# your first time through you need to add the argocd repo
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update
# install argocd into the cluster
helm install argo argo/argo-cd --namespace argocd --create-namespace
# deploy the app of apps
kubectl apply -f apps/apps.yaml
If you want to add repos and apps to your cluster you can use the argo cli to do that, but first you will need forward the argocd port and login.
# Wait until Argo CD API server is available
echo "Waiting for Argo CD API server to be available..."
while ! kubectl wait --for=condition=available --timeout=60s deployment/argo-argocd-server -n argocd; do
echo "Waiting for Argo CD API server to be ready..."
sleep 10
done
kubectl port-forward svc/argo-argocd-server -n argocd 8080:443 &
argocd_admin_password=$(kubectl get secret argocd-initial-admin-secret -n argocd -o jsonpath="{.data.password}" | base64 -d)
argocd login localhost:8080 --insecure --username admin --password $argocd_admin_password
argocd repo add https://github.com/fokais-com/app.fokais.git --username waylonwalker --password ${GH_ARGO_PAT}
argocd app create app-fokais-local --repo https://github.com/fokais-com/app.fokais.git --path k8s/overlays/local --dest-server https://kubernetes.default.svc --sync-policy automated --sync-option Prune=true
feeds
Thoughts
In my homelab kubernetes cluster I am using kubeseal to encrypt secrets. I have been using it successfully for a few months now wtih great success. It allows me to commit all of my secrets manifests to git with out risk of leaking secrets.
You see kubeseal encrypts your secrets with a private key only stored in your cluster, so only the cluster itself can decrypt them using the kubeseal controller.
KubeSeal #
https://sealed-secrets.netlify.app/
installation #
Installation happens in two steps. You need the kubernetes controller and the client side cli to create a sealed secret.
For a more complete instruction see the [docs#installation](https://github.com/bitnami-labs/sealed-secrets?tab=readme-ov-file#installation]
installation - controller #
Warning
**context**
Make sure that you are in the right context before running any kubectl commands.
kubectl config current-context
sealed-secrets is installed using the helm package manager. To install sealed-secrets run the following command.
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm install sealed-secrets -n kube-system --set-string fullnameOverride=sealed-secrets-controller sealed-secrets/sealed-secrets
installation - client #
For the client you can check your OS package manager, brew, or the github-releases. For me I found it in the main arch repos.
paru -S kubeseal
# or
sudo pacman -S kubeseal
# or
brew install kubeseal
Note
You will need to install kubeseal on every device that you will want to
create sealed secrets on.
Example #
Most of these commands come straight from the docs. From my experience I have always specified the namespace, my projects go per namespace and I don’t have any reason that other namepsaces should see the secret, and if they do I deploy another secret in that namespace.
# Create a json/yaml-encoded Secret somehow:
# (note use of `--dry-run` - this is just a local file!)
echo -n bar | kubectl create secret generic mysecret --dry-run=client --from-file=foo=/dev/stdin -o yaml -n thenamespace > mysecret.yaml
note that the key of the secret is
fooand the value isbar
results
apiVersion: v1
data:
foo: YmFy
kind: Secret
metadata:
creationTimestamp: null
name: mysecret
namespace: thenamespace
Note
The data is base64 encoded.
echo -n bar | base64
# YmFy
# This is the important bit:
kubeseal -f mysecret.yaml -w mysealedsecret.yaml
# At this point mysealedsecret.json is safe to upload to Github,
# post on Twitter, etc.
# Eventually:
kubectl create -f mysealedsecret.yaml -n thenamespace
# sealedsecret.bitnami.com/mysecret created
# Profit!
kubectl get secret mysecret
kubectl get secret mysecret -n thenamespace
# NAME TYPE DATA AGE
# mysecret Opaque 1 27s
cat mysealedsecret.yaml | kubeseal --validate
echo -n bar | kubectl create secret generic mysecret --dry-run=client --from-file=foo=/dev/stdin -o yaml \
| kubeseal -o yaml -n thenamespace > mysealedsecret.yaml
echo -n baz | kubectl create secret generic mysecret --dry-run=client --from-file=bar=/dev/stdin -o yaml \
| kubeseal -o yaml -n thenamespace --merge-into mysealedsecret.yaml
Results
---
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
creationTimestamp: null
name: mysecret
namespace: thenamespace
spec:
encryptedData:
bar: AgBLkamltcLfH1dC1JxQ3qd8lJ8aBZF2ARoq3uo055hnzXOy8g2T5liTx5UPvyPV8yyWqABU8eOnwjNhDtzSATvYeBB3fGkucdOZziWEoiNsWTR9ZtFEkod7Ya6uGkzZOJwi3IkrHFVIT9oWZQUxxJZ6vFhPiFcx9Dorr8TNSzG4KOug25+PhWPPiHDgSup5N3CkWCZaYOF7dbZRVSA4nGP1fZxjFByHP4AsdjLCHptyVbkpLRKeiXTkLxfLX4K+JLZGM41S1On5bSP56mCfv1daTJx619kDXkRLw9l21Ot283/L0NMNAiw781AefYMVoO3aHmYgcT6wAtsQAKje9fyL7DQRHt8a5NZOWukp/P6XjdXRz/nfQasQlbSTrRkDpplKIM5/WdPcBoKi+yyoOL0rZ8x1X7YzUI3BggZmzWyEPD01BK1YAHGZnYIZbbCy1JSm8JCBvP+xWMg+i0Z/DCD8nclAhH1GX2Q7/NrNHF//589AJfuriymd2+mk7uaLA4RRsY0l5QeZD6HVAqSv5jWsVQQtSftWmI9vn9oL/Pno7sEUjSDpXPfF4nnsULhxsPEe2DFAMm1kZAjdF06ueF4/x2Fdy80ZQNyycaDx2CWm4z3b14A75WGyOXl2wJZQqxrFCz8el4hD2nH3zQFEzd6AIh49myqVAGuu2qGlYP4p94LJghVa+mQjztLD/2ZUZjY+anQ=
foo: AgAducXW6iUCY900cPDdmRfuj7tKnh2hY4C1+2hFoAtjyvjepsKNWsiPJ81t8anaMfFPat4ta060l5VtTrceFE8oS/rViz1tvNWGPBjL+GwL/QjkGl6H8ju87vKERKQn5qw7B/V5j24VM8AxOd+/vJNt/IeRIHLubvFft4hyMq4b0xmIxaemBSTxchQX/5364T3VJH2kHaqpqd+JJgQnTbiTQe/XnyZokDX8GSxw4rAbJUJSRUtY9DB9ZDu2zC5VngX+GJjbwHGbv9EKs8LShJIPrD8xHqrDmlSXGkkP01D4A6268Qoi3x5S0H5aqDgtrgBiWsNkzdKwjfrTNx7pKecOi41lyFdffHOGUew4aPPMqjzWR2TEms9WNNQXwnBdDHKMkFsisocF52BolEkcjF8g/u5h2Af92abMu6k16VybJrB7TV1set5A9W7rqG1iXI4+1W6XQfFnpja8xL/zJBvZcyHgeYMNaxa3C3s6PANhPzAUVaXV9eedAkptGJLN13IZj4LujpoAxRKo6bEdydv/5P23R3fx5PgTOpVI7riECAOIg2PThFsEoVCUwStmKCvIx1I2+YixIlv/OiaUWo4lrI/3ve5WGp4ZnuiJPk34JoYAlRbR6+sX14d8Ek6viq/pJUUIfVpNIkNMboUL4u+KpT47eyQ/mWih/KFduQyX9II/vQ+/IJGzEEHIipxAhdmV+K4=
template:
metadata:
creationTimestamp: null
name: mysecret
namespace: thenamespace
backing up your sealing key #
kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml >main.key
converting .env files to a secret #
Working with web applications .env is a common way to store credentials. Let’s look at how we can convert these to secrets.
kubectl create secret generic mysecret --from-env-file=.env -n thenamespace --dry-run=client -o yaml > mysecret.yaml
Now you have a secret that looks like this.
apiVersion: v1
data:
foo: YmFy
kind: Secret
metadata:
creationTimestamp: null
name: mysecret
namespace: thenamespace
Seal it up just like before.
kubeseal -f mysecret.yaml -w mysealedsecret.yaml
Using the secrets #
I typically use the secrets in the container spec.
containers:
- name: myapp
envFrom:
- secretRef:
name: mysecret
# You can still have other env vars
env:
- name: foo
value: bar
Sometimes I want to mount the secret as a volume.
containers:
- name: myapp
volumeMounts:
- name: mysecret
mountPath: /mysecret
volumes:
- name: mysecret
secret:
secretName: mysecret
Image Pull Secrets #
I also need to use imagePullSecrets. Let’s walk through the whole process. Starting with the secret.
kubectl create secret docker-registry regcred --docker-server=myprivateregistry.example.com --docker-username=foo --docker-password=bar --dry-run=client -o yaml
Generates the following secret.
apiVersion: v1
data:
.dockerconfigjson: eyJhdXRocyI6eyJteXByaXZhdGVyZWdpc3RyeS5leGFtcGxlLmNvbSI6eyJ1c2VybmFtZSI6ImZvbyIsInBhc3N3b3JkIjoiYmFyIiwiYXV0aCI6IlptOXZPbUpoY2c9PSJ9fX0=
kind: Secret
metadata:
creationTimestamp: null
name: regcred
type: kubernetes.io/dockerconfigjson
---
the secret
Now we we can seal that secret.
kubeseal -f regcred.yaml -w regcred-sealed.yaml
And that gives us the following sealed secret that we can deploy into our cluster.
---
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
creationTimestamp: null
name: regcred
namespace: default
spec:
encryptedData:
.dockerconfigjson: AgATYVEywkyYEaoErQbJo6xEZfOnRn1ydNTLkO3Jt/NF/UH+0o9lHpDecRpN0XnVu8xJUcdjkgD9q2XkwP8e6qQDS2mMPTiTNIN+8gbJx97WrD1YQDT0lXBuoyi9I/iwlXxx6MgH/6GY6CeGTz5SRlvoU0Xhlt4d11s7/xapdE8QMLsAReqPEv8oZHEyAxDRrjXX0V+tO8dV+G+GXjUDMBBceLael9rvGzSKIwDVXACVqQhLkB6FoP98M+yyBE46RBNnSnS0ShQM5PprL24HKpRZ43x4RM53KBrQ7R/MxeshafY+B6vUvrolmVox4sud8xngMOcjTO28LLOrck5V8ZiDabhN7ajHEf03IESr1o/ADGf5k9988Vv1txJtsZW0K2mpRu0D7/BLVL9KzbZ5ywULqIoD/Ur2GIGnZqMAKOq4laGp/GJtMKLrhmEvekT397wC/Gf/xdDKVhHf2p4ocsPu7LKFuS5H/Auel/Q5grdn8L5wwrO4VWRv3eJroKh/Hux7Qd7f64O7qdi0XthDocf+gmtjys+Gy72M7tyf8f/O+3oKbS4CWQVTj4ZThMc9znrFnHqt2q/7pAyytTQCpk51wlzOsNvOhCueJM/jmeahaL0LuBrqngqISpnd65sgVzBcZpwK9i2Fckyt0DrZLH+NoIuvaqNhzlF+OMbAft/ylWWKCH4WUP+FKG+1LXM7ud7AA3MMbGSBxHL0/WK/INa7MB56xZKMqqyvvLLQHFTQUROJjkgkzsumdOgwZTRgIFnAZ4+vOX3/1Rtt3mAs3vdoJhL4GuKUYCnEHt908eKkWEVEs7eMk5SdSRtIsbaXO2s0dtADwg==
template:
metadata:
creationTimestamp: null
name: regcred
namespace: default
type: kubernetes.io/dockerconfigjson
Now that we have our sealed registry secret, we can deploy it into our cluster.
kubectl apply -f regcred-sealed.yaml
Now we can use it to pull images from our private registry.
containers:
imagePullSecrets:
- name: regcred
Full example #
Here is a full deployment example using all the secrets we have created.
- regcred
- mounting a secret
- envFrom secret
---
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
service: myservice
name: myservice
namespace: mynamespace
spec:
replicas: 1
selector:
matchLabels:
service: myservice
strategy:
type: Recreate
template:
metadata:
creationTimestamp: null
labels:
service: myservice
spec:
containers:
- envFrom:
- secretRef:
name: mysecret
env:
- name: foo
value: bar
image: private-registry.io/myimage:1.0.0
name: myimage
ports:
- containerPort: 5000
protocol: TCP
resources: {}
volumeMounts:
- mountPath: /mysecret
name: mysecret
restartPolicy: Always
volumes:
- name: mysecret
secret:
secretName: mysecret
imagePullSecrets:
- name: regcred
Downside #
Now the main downside I see with kubeseal is that it does not provide a way to store your secrets in a way that you can access outside of your cluster. So you need to make sure that you have another solution in place to store your secrets so that you still have them if you ever were to take the cluster down or move from k8s to something else.
Overall the likelyhood of you loosing a production cluster is pretty low, so maybe it’s ok to just trust it depending on what the secrets are. Especially for things you control and can rotate anyways its fine.