Run a local process as if it were inside your cluster: prefix any command
with plug and it resolves the cluster DNS and reaches the cluster services like
any workload of the stack - no code change, no proxy configuration in your app. One
tiny agent container on the cluster side, one static CLI on yours. Docker, Swarm, Kubernetes
and OpenShift.
Continuous deployment & GitOps
If your cluster is driven by a continuous-deployment controller - Argo CD,
Flux, Rancher Fleet, Crossplane - there is
one interaction with plug worth knowing about. It affects
-s only (publishing a name into the cluster); consuming cluster services
is untouched, and everything here applies to Kubernetes and Swarm alike.
Why the two disagree
A CD controller's whole job is to keep the cluster equal to what a repository declares. It watches the live objects, and whatever differs from the declared state is drift - something to undo. That is the feature you deployed it for.
plug's -s does the opposite on purpose: for the lifetime of a session it
modifies a live object so a cluster name points at your machine instead of
the deployed workload. On Kubernetes that means patching the Service's
spec.selector and spec.ports; on Swarm, the DNS alias. The
controller sees exactly what it is built to revert, and reverts it - usually within minutes.
Nothing errors on either side. plug's patch is valid and applies cleanly; the controller's restore is valid too. The name simply stops pointing at you, quietly, some time after the session started.
How it shows up
plug proves the path end to end in the background and warns when the name never carried traffic. Which warning you get depends on whether the deployed workload is running:
| Warning | What happened |
|---|---|
is not reachable inside the cluster |
The workload is not deployed. The restore left the Service selecting a workload that does not exist - no endpoints, and connections time out. |
is answered by something else |
The workload is running. The name went back to it, so requests are served by the deployed version and your local process never sees one. |
The second is the one to watch for: nothing looks broken. The application answers, the page loads - it is simply not your code answering. If a session seems to work for a few minutes and then stops reflecting your changes, this is the first thing to check.
Confirming it
Look at the annotations and labels on the object plug tried to take over
(kubectl describe svc <name>, or d on the Service in k9s):
| Controller | What to look for |
|---|---|
| Argo CD | argocd.argoproj.io/tracking-id (annotation) or
app.kubernetes.io/instance (label) |
| Flux | kustomize.toolkit.fluxcd.io/name,
helm.toolkit.fluxcd.io/name |
| Rancher Fleet | fleet.cattle.io/bundle-name |
| Crossplane, operators | crossplane.io/composite, or an
ownerReferences entry with controller: true |
app.kubernetes.io/managed-by: Helm on its own
means nothing here - Helm acts on an upgrade and does not reconcile, so a chart-deployed
Service is perfectly takeable. And app.kubernetes.io/instance is a standard
Kubernetes label that Helm sets on everything; it only indicates Argo CD when Argo is
configured to track by label, so the annotation is the reliable signal.
Argo CD
Tell the Application not to compare the two fields plug changes. Everything else about the Service keeps being reconciled:
spec:
ignoreDifferences:
- group: ""
kind: Service
name: my-service
namespace: my-namespace
jsonPointers:
- /spec/selector
- /spec/ports
This lives in the Application object (a CRD, usually in the argocd
namespace) - not in the chart. On a cluster where plug is used routinely,
set it once for every Service in the argocd-cm ConfigMap instead of Application
by Application. The empty prefix in the key is how the core API group is spelled:
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
resource.customizations.ignoreDifferences._Service: |
jsonPointers:
- /spec/selector
- /spec/portsFor a one-off, turning off Self Heal (App Details → Sync Policy) also works and needs no manifest change - but it suspends drift correction for the whole Application, it is easy to forget to turn back on, and a Git push or a manual sync re-applies the Service anyway.
Flux
Flux is the one controller that takes the instruction on the resource itself, so here the chart is the right place:
metadata:
annotations:
kustomize.toolkit.fluxcd.io/reconcile: disabled
Note this is broader than Argo's ignoreDifferences: it stops reconciling the
whole object, not two fields. The same annotation can be applied through the
Kustomization with spec.patches if you would rather not carry it in
the chart.
Rancher Fleet
Fleet compares with patches declared in the bundle's fleet.yaml:
diff:
comparePatches:
- apiVersion: v1
kind: Service
name: my-service
namespace: my-namespace
operations:
- op: remove
path: /spec/selector
- op: remove
path: /spec/portsCrossplane and bespoke operators
When a Service is managed as a Crossplane Object, switching that resource to
managementPolicies: ["Observe"] stops it being re-applied. An operator written
in-house rarely offers an equivalent: there, the options are to suspend the operator for the
duration, or to serve a name it does not own.
After a session
Ending a session properly (Ctrl-C) is what restores the object, from a receipt
plug stored on it. That matters more than usual here, because the extra selector key plug
adds is not removed by the controller - it only manages fields it knows
about. A session killed with kill -9 can therefore leave a Service selecting
both its own workload and plug, which matches no pod at all: the name then answers nobody,
with or without plug running. If that happens, remove the leftover app: plug
key from the selector by hand.
Details of what -s does per orchestrator are on the
Kubernetes and Swarm pages; other
symptoms are covered under Troubleshooting.