- Go 1.24+
- Docker (for building container images)
- A kubeconfig pointing at a cluster (for manual testing)
- frp binaries (for integration tests)
# Build the operator binary
make build
# Build the Docker image
make docker-build IMG=fly-tunnel-operator:devThe binary is output to bin/manager.
The operator can run outside the cluster using your local kubeconfig:
export FLY_API_TOKEN=<your-token>
export FLY_ORG=<your-org-slug>
export FLY_REGION=ord
go run . --namespace <target-namespace>Or equivalently:
make runThe --namespace flag (or OPERATOR_NAMESPACE env var) controls where the operator creates frpc Deployments, ConfigMaps, and the leader election Lease. When running locally you should always set this explicitly:
# Create the namespace first
kubectl create namespace my-dev-ns
# Run with an explicit namespace
go run . --namespace my-dev-nsIf omitted, it defaults to fly-tunnel-operator-system. The Helm chart handles this automatically by setting --namespace={{ .Release.Namespace }} in the Deployment spec, so the operator always targets the Helm release namespace.
By default the operator watches Services with loadBalancerClass: fly-tunnel-operator.dev/lb. Override with --load-balancer-class.
Unit tests use a fake Fly.io API server (internal/fakefly/server.go) and controller-runtime's fake client. No network or cluster access required.
make testOr run specific packages:
go test ./internal/frp/ -v # frp config generation
go test ./internal/flyio/ -v # Fly.io API client
go test ./internal/tunnel/ -v # tunnel lifecycle managerController tests use envtest which runs a real kube-apiserver and etcd locally. The envtest binaries are auto-discovered from ~/.local/share/kubebuilder-envtest/.
go test ./internal/controller/ -vIf you don't have envtest binaries installed, use setup-envtest:
go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
setup-envtest use 1.31.xThen set the KUBEBUILDER_ASSETS environment variable to the path printed by setup-envtest, or let the test suite auto-discover them.
Integration tests verify that generated frpc/frps configs work with real frp binaries. They are gated behind a build tag and skipped if the binaries are not found.
# Download frp binaries
curl -L https://github.com/fatedier/frp/releases/download/v0.61.1/frp_0.61.1_linux_amd64.tar.gz | tar xz -C /tmp
# Run integration tests
go test -tags integration ./internal/frp/ -vThe tests look for frps/frpc in these locations (in order):
FRP_BIN_DIRenvironment variablePATH/tmp/frp_0.61.1_linux_amd64//usr/local/bin//usr/bin/
The integration test suite includes:
| Test | What it verifies |
|---|---|
TestIntegration_SinglePortTunnel |
Full TCP tunnel with echo server, data flows end-to-end |
TestIntegration_MultiPortTunnel |
Multi-port tunnel (HTTP + HTTPS), independent backends |
TestIntegration_ConfigParseValid |
frpc verify accepts a 3-port generated config |
TestIntegration_ServerConfigParseValid |
frps verify accepts the server config |
TestIntegration_LargePortRange |
20-port config generates correctly and parses |
TestIntegration_UDPProxy |
UDP protocol type is emitted and parseable |
# Unit + controller integration tests
make test
# Including frp integration tests
go test -tags integration ./... -vinternal/
├── controller/
│ ├── service_controller.go # Reconciler: watches Services, drives provisioning
│ ├── service_controller_test.go # envtest integration tests (6 tests)
│ └── suite_test.go # envtest setup (shared manager, fake fly server)
├── tunnel/
│ ├── manager.go # Provision / Update / Teardown orchestration
│ └── manager_test.go # Unit tests with fakes (5 tests)
├── flyio/
│ ├── client.go # Fly.io Machines REST API + GraphQL client
│ └── client_test.go # Unit tests with httptest server (15 tests)
├── frp/
│ ├── config.go # TOML config generation for frpc/frps
│ ├── config_test.go # Unit tests (3 tests)
│ └── config_integration_test.go # Integration tests with real frp binaries (6 tests)
└── fakefly/
└── server.go # Fake Fly.io API (REST + GraphQL) for testing
The operator works entirely with core Service objects. It watches Service type: LoadBalancer with a specific loadBalancerClass and stores all tunnel state in annotations on the Service itself. This avoids CRD installation and version management.
A finalizer (fly-tunnel-operator.dev/finalizer) is added to every managed Service. On deletion, the operator tears down the Fly.io Machine, releases the IPv4, and deletes the in-cluster frpc Deployment + ConfigMap before removing the finalizer and allowing the Service to be garbage collected.
Each LoadBalancer Service gets its own Fly.io Machine running frps and its own dedicated IPv4. This provides isolation and makes per-service region/size overrides straightforward.
The frpc client runs as a Deployment inside the cluster. Its config is mounted from a ConfigMap that the operator regenerates on port changes. The frpc connects outbound to the Fly.io Machine's public IP, so no inbound firewall rules are needed on the cluster.
All tunnel state is stored directly on the Service as annotations — no external database or CRD state:
| Annotation | Description |
|---|---|
fly-tunnel-operator.dev/fly-app |
Fly.io App name created for this Service |
fly-tunnel-operator.dev/machine-id |
Fly.io Machine ID |
fly-tunnel-operator.dev/frpc-deployment |
Name of the in-cluster frpc Deployment |
fly-tunnel-operator.dev/ip-id |
Fly.io IP address allocation ID |
fly-tunnel-operator.dev/public-ip |
Allocated public IPv4 address |
fly-tunnel-operator.dev/fly-region |
(user-set) Override Fly.io region |
fly-tunnel-operator.dev/fly-machine-size |
(user-set) Override machine size |
The Helm chart is in charts/fly-tunnel-operator/. To render templates without installing:
make helm-templateTo install:
make helm-installTo customize values:
helm install fly-tunnel-operator charts/fly-tunnel-operator \
--namespace fly-tunnel-operator-system \
--create-namespace \
-f my-values.yaml| Target | Description |
|---|---|
make build |
Build the operator binary to bin/manager |
make run |
Run the operator locally via go run |
make test |
Run all unit and envtest integration tests |
make lint |
Run golangci-lint |
make fmt |
Format Go source files |
make vet |
Run go vet |
make docker-build |
Build Docker image |
make docker-push |
Push Docker image |
make helm-install |
Install Helm chart |
make helm-uninstall |
Uninstall Helm chart |
make helm-template |
Render Helm templates to stdout |
make clean |
Remove build artifacts |