Compare commits
2 commits
main
...
localize-t
| Author | SHA1 | Date | |
|---|---|---|---|
| af0fce2a05 | |||
| ac40a18f3f |
8 changed files with 307 additions and 12 deletions
|
|
@ -9,12 +9,19 @@ resources:
|
|||
- proxygroup-ingress.yaml
|
||||
- external-secret.yaml
|
||||
|
||||
# Rewrite the proxyclass image to our local nix-built mirror.
|
||||
# Scoped to ringtail only; indri's tailscale-operator/kustomization.yaml still
|
||||
# pulls from upstream docker.io. A strategic merge patch is used instead of
|
||||
# kustomize's `images:` directive because that directive only rewrites images
|
||||
# in standard k8s container fields, not custom-resource fields like
|
||||
# ProxyClass.spec.statefulSet.pod.tailscaleContainer.image.
|
||||
# Rewrite the operator image to the locally nix-built (amd64) mirror.
|
||||
# The name must match the post-base-render image (base already rewrites
|
||||
# tailscale/k8s-operator -> docker.io/tailscale/k8s-operator).
|
||||
images:
|
||||
- name: docker.io/tailscale/k8s-operator
|
||||
newName: registry.ops.eblu.me/blumeops/tailscale-operator
|
||||
newTag: v1.94.2-ac40a18-nix
|
||||
|
||||
# Rewrite the proxyclass image to our local nix-built mirror (indri's overlay
|
||||
# carries the equivalent dagger/arm64 patch). A strategic merge patch is used
|
||||
# instead of kustomize's `images:` directive because that directive only
|
||||
# rewrites images in standard k8s container fields, not custom-resource fields
|
||||
# like ProxyClass.spec.statefulSet.pod.tailscaleContainer.image.
|
||||
patches:
|
||||
- path: proxyclass-image.yaml
|
||||
target:
|
||||
|
|
|
|||
|
|
@ -14,3 +14,23 @@ resources:
|
|||
# Endpoints). Apply manually:
|
||||
# kubectl --context=minikube-indri apply -f endpoints-forge.yaml
|
||||
- ingress-forge.yaml
|
||||
|
||||
# Rewrite the operator image to the locally dagger-built (arm64) mirror.
|
||||
# The name must match the post-base-render image (base already rewrites
|
||||
# tailscale/k8s-operator -> docker.io/tailscale/k8s-operator).
|
||||
images:
|
||||
- name: docker.io/tailscale/k8s-operator
|
||||
newName: registry.ops.eblu.me/blumeops/tailscale-operator
|
||||
newTag: v1.94.2-ac40a18
|
||||
|
||||
# Rewrite the proxyclass image to the local mirror. A strategic merge patch
|
||||
# is used instead of kustomize's `images:` directive because that directive
|
||||
# only rewrites standard k8s container fields, not custom-resource fields
|
||||
# like ProxyClass.spec.statefulSet.pod.tailscaleContainer.image.
|
||||
patches:
|
||||
- path: proxyclass-image.yaml
|
||||
target:
|
||||
group: tailscale.com
|
||||
version: v1alpha1
|
||||
kind: ProxyClass
|
||||
name: default
|
||||
|
|
|
|||
11
argocd/manifests/tailscale-operator/proxyclass-image.yaml
Normal file
11
argocd/manifests/tailscale-operator/proxyclass-image.yaml
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
apiVersion: tailscale.com/v1alpha1
|
||||
kind: ProxyClass
|
||||
metadata:
|
||||
name: default
|
||||
spec:
|
||||
statefulSet:
|
||||
pod:
|
||||
tailscaleContainer:
|
||||
image: registry.ops.eblu.me/blumeops/tailscale:v1.94.2-ac40a18
|
||||
tailscaleInitContainer:
|
||||
image: registry.ops.eblu.me/blumeops/tailscale:v1.94.2-ac40a18
|
||||
53
containers/tailscale-operator/container.py
Normal file
53
containers/tailscale-operator/container.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"""Tailscale Kubernetes operator — native Dagger build.
|
||||
|
||||
Single Go binary (cmd/k8s-operator) from the forge mirror, mirroring
|
||||
upstream's build_docker.sh mkctr recipe: binary at /usr/local/bin/operator,
|
||||
go tags ts_kube + ts_package_container, version stamps in ldflags.
|
||||
|
||||
Consumed by the tailscale-operator app on indri's minikube (arm64); the
|
||||
ringtail app uses the -nix tag from default.nix instead.
|
||||
"""
|
||||
|
||||
import dagger
|
||||
|
||||
from blumeops.containers import (
|
||||
alpine_runtime,
|
||||
clone_from_forge,
|
||||
go_build,
|
||||
oci_labels,
|
||||
)
|
||||
|
||||
VERSION = "v1.94.2"
|
||||
|
||||
|
||||
async def build(src: dagger.Directory) -> dagger.Container:
|
||||
source = clone_from_forge("tailscale", VERSION)
|
||||
semver = VERSION.removeprefix("v")
|
||||
|
||||
builder = go_build(
|
||||
source,
|
||||
"/out/operator",
|
||||
cmd_path="./cmd/k8s-operator",
|
||||
tags="ts_kube,ts_package_container",
|
||||
ldflags=(
|
||||
"-w -s"
|
||||
f" -X tailscale.com/version.longStamp={semver}"
|
||||
f" -X tailscale.com/version.shortStamp={semver}"
|
||||
),
|
||||
)
|
||||
|
||||
# Upstream runs the operator as root on a minimal base; only CA certs
|
||||
# are needed at runtime (operator talks to the k8s API and Tailscale
|
||||
# control plane over HTTPS).
|
||||
runtime = alpine_runtime(extra_apk=["ca-certificates"], create_user=False)
|
||||
runtime = oci_labels(
|
||||
runtime,
|
||||
title="Tailscale Kubernetes Operator",
|
||||
description="Tailscale operator for Kubernetes Ingress/egress proxies",
|
||||
version=VERSION,
|
||||
)
|
||||
return runtime.with_file(
|
||||
"/usr/local/bin/operator",
|
||||
builder.file("/out/operator"),
|
||||
permissions=0o555,
|
||||
).with_entrypoint(["/usr/local/bin/operator"])
|
||||
67
containers/tailscale-operator/default.nix
Normal file
67
containers/tailscale-operator/default.nix
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Nix-built tailscale k8s-operator for ringtail's tailscale-operator app.
|
||||
# Builds cmd/k8s-operator v1.94.2 from the forge mirror, mirroring upstream's
|
||||
# build_docker.sh mkctr recipe (binary at /usr/local/bin/operator, ts_kube +
|
||||
# ts_package_container go tags). Built on the ringtail nix-container-builder.
|
||||
{ pkgs ? import <nixpkgs> { } }:
|
||||
|
||||
let
|
||||
version = "1.94.2";
|
||||
|
||||
src = pkgs.fetchgit {
|
||||
url = "https://forge.ops.eblu.me/mirrors/tailscale.git";
|
||||
rev = "v${version}";
|
||||
hash = "sha256-qjWVB8xWVgIVUgrf27F6hwiFIE+4ERXWeHv26ugg/x4=";
|
||||
};
|
||||
|
||||
operator = pkgs.buildGoModule {
|
||||
inherit src version;
|
||||
pname = "tailscale-operator";
|
||||
vendorHash = "sha256-WeMTOkERj4hvdg4yPaZ1gRgKnhRIBXX55kUVbX/k/xM=";
|
||||
|
||||
subPackages = [ "cmd/k8s-operator" ];
|
||||
|
||||
tags = [
|
||||
"ts_kube"
|
||||
"ts_package_container"
|
||||
];
|
||||
|
||||
ldflags = [
|
||||
"-s"
|
||||
"-w"
|
||||
"-X tailscale.com/version.longStamp=${version}"
|
||||
"-X tailscale.com/version.shortStamp=${version}"
|
||||
];
|
||||
|
||||
doCheck = false;
|
||||
|
||||
meta = with pkgs.lib; {
|
||||
description = "Tailscale operator for Kubernetes";
|
||||
homepage = "https://tailscale.com";
|
||||
license = licenses.bsd3;
|
||||
};
|
||||
};
|
||||
in
|
||||
|
||||
pkgs.dockerTools.buildLayeredImage {
|
||||
name = "blumeops/tailscale-operator";
|
||||
tag = "v${version}";
|
||||
|
||||
contents = [
|
||||
operator
|
||||
pkgs.cacert
|
||||
];
|
||||
|
||||
# buildGoModule names the binary after the package dir (k8s-operator);
|
||||
# upstream's image expects /usr/local/bin/operator.
|
||||
extraCommands = ''
|
||||
mkdir -p usr/local/bin
|
||||
ln -s /bin/k8s-operator usr/local/bin/operator
|
||||
'';
|
||||
|
||||
config = {
|
||||
Entrypoint = [ "/usr/local/bin/operator" ];
|
||||
Env = [
|
||||
"SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"
|
||||
];
|
||||
};
|
||||
}
|
||||
104
containers/tailscale/container.py
Normal file
104
containers/tailscale/container.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"""Tailscale proxy image (containerboot) — native Dagger build.
|
||||
|
||||
Builds cmd/tailscale, cmd/tailscaled, and cmd/containerboot from the forge
|
||||
mirror, mirroring the upstream Dockerfile: Alpine runtime with iptables
|
||||
(legacy symlinked over the default, per upstream issue #17854), iproute2,
|
||||
and the /tailscale/run.sh compat symlink.
|
||||
|
||||
Consumed by the tailscale-operator ProxyClass on indri's minikube (arm64);
|
||||
ringtail's ProxyClass uses the -nix tag from default.nix instead.
|
||||
"""
|
||||
|
||||
import dagger
|
||||
|
||||
from blumeops.containers import (
|
||||
alpine_runtime,
|
||||
clone_from_forge,
|
||||
go_build,
|
||||
oci_labels,
|
||||
)
|
||||
|
||||
VERSION = "v1.94.2"
|
||||
|
||||
|
||||
async def build(src: dagger.Directory) -> dagger.Container:
|
||||
source = clone_from_forge("tailscale", VERSION)
|
||||
semver = VERSION.removeprefix("v")
|
||||
|
||||
ldflags = (
|
||||
"-w -s"
|
||||
f" -X tailscale.com/version.longStamp={semver}"
|
||||
f" -X tailscale.com/version.shortStamp={semver}"
|
||||
)
|
||||
builder = go_build(
|
||||
source,
|
||||
"/out/tailscale",
|
||||
cmd_path="./cmd/tailscale",
|
||||
ldflags=ldflags,
|
||||
)
|
||||
builder = builder.with_exec(
|
||||
[
|
||||
"go",
|
||||
"build",
|
||||
f"-ldflags={ldflags}",
|
||||
"-o",
|
||||
"/out/tailscaled",
|
||||
"./cmd/tailscaled",
|
||||
]
|
||||
).with_exec(
|
||||
[
|
||||
"go",
|
||||
"build",
|
||||
f"-ldflags={ldflags}",
|
||||
"-o",
|
||||
"/out/containerboot",
|
||||
"./cmd/containerboot",
|
||||
]
|
||||
)
|
||||
|
||||
runtime = alpine_runtime(
|
||||
extra_apk=["ca-certificates", "iptables", "iproute2", "ip6tables"],
|
||||
create_user=False,
|
||||
)
|
||||
runtime = oci_labels(
|
||||
runtime,
|
||||
title="Tailscale",
|
||||
description="Tailscale containerboot proxy image for the k8s operator",
|
||||
version=VERSION,
|
||||
)
|
||||
return (
|
||||
runtime
|
||||
# Match upstream Dockerfile: nftables-backed iptables misbehaves in
|
||||
# some environments, force the legacy backend (tailscale/tailscale#17854).
|
||||
.with_exec(
|
||||
[
|
||||
"sh",
|
||||
"-c",
|
||||
"rm /usr/sbin/iptables && ln -s /usr/sbin/iptables-legacy /usr/sbin/iptables"
|
||||
" && rm /usr/sbin/ip6tables && ln -s /usr/sbin/ip6tables-legacy /usr/sbin/ip6tables",
|
||||
]
|
||||
)
|
||||
.with_file(
|
||||
"/usr/local/bin/tailscale",
|
||||
builder.file("/out/tailscale"),
|
||||
permissions=0o555,
|
||||
)
|
||||
.with_file(
|
||||
"/usr/local/bin/tailscaled",
|
||||
builder.file("/out/tailscaled"),
|
||||
permissions=0o555,
|
||||
)
|
||||
.with_file(
|
||||
"/usr/local/bin/containerboot",
|
||||
builder.file("/out/containerboot"),
|
||||
permissions=0o555,
|
||||
)
|
||||
.with_exec(
|
||||
[
|
||||
"sh",
|
||||
"-c",
|
||||
"mkdir /tailscale && ln -s /usr/local/bin/containerboot /tailscale/run.sh",
|
||||
]
|
||||
)
|
||||
.with_entrypoint(["/usr/local/bin/containerboot"])
|
||||
)
|
||||
1
docs/changelog.d/localize-tailscale-operator.infra.md
Normal file
1
docs/changelog.d/localize-tailscale-operator.infra.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
Localized the Tailscale operator stack: the k8s-operator image (both clusters) and the ProxyClass proxy image (indri, completing parity with ringtail) are now built from the forge mirror instead of pulled from Docker Hub.
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Tailscale Operator
|
||||
modified: 2026-06-08
|
||||
last-reviewed: 2026-06-08
|
||||
modified: 2026-06-09
|
||||
last-reviewed: 2026-06-09
|
||||
tags:
|
||||
- kubernetes
|
||||
- tailscale
|
||||
|
|
@ -22,10 +22,42 @@ The Tailscale operator enables Kubernetes services to be exposed directly on the
|
|||
The operator runs on **both** clusters — indri's minikube and ringtail's k3s.
|
||||
Both apps layer on the shared `tailscale-operator-base` kustomize directory
|
||||
(operator manifest, `ProxyClass`, `dnsconfig`); each cluster supplies its own
|
||||
`ProxyGroup` (indri: 2 replicas, ringtail: 1) and OAuth `ExternalSecret`. The
|
||||
ringtail overlay additionally rewrites the proxy image to a locally nix-built
|
||||
mirror. See [[ringtail]] and [[migrate-wave1-ringtail]] for the ongoing
|
||||
migration of k8s workloads onto ringtail.
|
||||
`ProxyGroup` (indri: 2 replicas, ringtail: 1) and OAuth `ExternalSecret`. See
|
||||
[[ringtail]] and [[migrate-wave1-ringtail]] for the ongoing migration of k8s
|
||||
workloads onto ringtail.
|
||||
|
||||
## Local Images
|
||||
|
||||
Both the operator and the proxy run locally-built images from the forge
|
||||
mirror (`mirrors/tailscale`), not Docker Hub:
|
||||
|
||||
| Image | Build | Used by |
|
||||
|-------|-------|---------|
|
||||
| `blumeops/tailscale-operator` | `containers/tailscale-operator/` (`container.py` for indri/arm64, `default.nix` `-nix` tag for ringtail/amd64) | operator Deployment, via each overlay's `images:` override |
|
||||
| `blumeops/tailscale` | `containers/tailscale/` (same dual build) | `ProxyClass` proxy pods, via a strategic-merge patch in each overlay |
|
||||
|
||||
The ProxyClass image must be set with a **patch**, not kustomize's `images:`
|
||||
directive — that directive only rewrites standard container fields, not
|
||||
custom-resource fields like `ProxyClass.spec.statefulSet.pod.tailscaleContainer.image`.
|
||||
|
||||
The `dnsconfig` nameserver image (`tailscale/k8s-nameserver:stable`) is still
|
||||
upstream — a known follow-up.
|
||||
|
||||
## Rollout Safety (device identity)
|
||||
|
||||
Proxy and operator tailnet identity lives in Kubernetes state Secrets in the
|
||||
`tailscale` namespace, not in pods or images. An image swap rolls the
|
||||
Deployment/StatefulSets but pods re-authenticate with their existing node
|
||||
keys — devices keep their names. Shadow devices (`foo-1` suffixes) appear only
|
||||
when a pod registers *fresh* while a stale device record still holds the name
|
||||
(deleted state Secrets, cluster rebuilds). When rolling out image changes:
|
||||
|
||||
1. Never delete the `tailscale` namespace state Secrets.
|
||||
2. Verify after sync: pods healthy, device names unchanged in the admin
|
||||
console, `mise run services-check` green.
|
||||
3. If a collision does occur: delete the stale device in the admin console
|
||||
AND the affected state Secret, then restart the pod (see
|
||||
[[rebuild-minikube-cluster]]).
|
||||
|
||||
## How It Works
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue