tailscale/tailscale · error
failed to get tailscale client and loginUrl: %w
Error message
failed to get tailscale client and loginUrl: %w
What it means
The operator supports multiple tailnets: ClientProvider.For(tailnet) returns a per-tailnet API client, failing with ErrClientNotFound for an unregistered tailnet or ErrNotReady for one whose login/API credentials are not yet usable. This error (also recorded on the ProxyGroup status with reason TailnetUnavailable) means the value in the ProxyGroup's spec.tailnet cannot be resolved to a usable client. Provisioning halts until the tailnet becomes available; the reconciler also attempts to update status, joining both errors.
Source
Thrown at cmd/k8s-operator/proxygroup.go:137
logger := r.logger(req.Name)
logger.Debugf("starting reconcile")
defer logger.Debugf("reconcile finished")
pg := new(tsapi.ProxyGroup)
err = r.Get(ctx, req.NamespacedName, pg)
if apierrors.IsNotFound(err) {
logger.Debugf("ProxyGroup not found, assuming it was deleted")
return reconcile.Result{}, nil
} else if err != nil {
return reconcile.Result{}, fmt.Errorf("failed to get tailscale.com ProxyGroup: %w", err)
}
tsClient, err := r.clients.For(pg.Spec.Tailnet)
if err != nil {
oldPGStatus := pg.Status.DeepCopy()
nrr := ¬ReadyReason{
reason: reasonProxyGroupTailnetUnavailable,
message: fmt.Errorf("failed to get tailscale client and loginUrl: %w", err).Error(),
}
return reconcile.Result{}, errors.Join(err, r.maybeUpdateStatus(ctx, logger, pg, oldPGStatus, nrr, make(map[string][]netip.AddrPort)))
}
if markedForDeletion(pg) {
logger.Debugf("ProxyGroup is being deleted, cleaning up resources")
ix := xslices.Index(pg.Finalizers, FinalizerName)
if ix < 0 {
logger.Debugf("no finalizer, nothing to do")
return reconcile.Result{}, nil
}
if done, err := r.maybeCleanup(ctx, tsClient, pg); err != nil {
if strings.Contains(err.Error(), optimisticLockErrorMsg) {
logger.Infof("optimistic lock error, retrying: %s", err)
return reconcile.Result{}, nil
}View on GitHub (pinned to cfe32b8be6)
Solutions
- Check the ProxyGroup status conditions for TailnetUnavailable and the embedded reason (client not found vs tailnet not ready)
- Verify spec.tailnet exactly matches a tailnet configured on the operator; fix typos or leave it blank to use the default tailnet
- Complete the operator-side configuration for that tailnet (register its API client/login URL) and wait for readiness
- If the tailnet is intentionally gone, update or delete the ProxyGroups referencing it
Example fix
# before apiVersion: tailscale.com/v1alpha1 kind: ProxyGroup metadata: name: pg spec: tailnet: prod-tailent # typo, unregistered # after apiVersion: tailscale.com/v1alpha1 kind: ProxyGroup metadata: name: pg spec: tailnet: prod-tailnet
Defensive patterns
Strategy: validation
Validate before calling
// Before applying a ProxyGroup, verify the tailnet is resolvable
if _, err := clients.For(pg.Spec.Tailnet); err != nil {
if errors.Is(err, tsclient.ErrClientNotFound) {
return fmt.Errorf("tailnet %q not configured on operator", pg.Spec.Tailnet)
}
if errors.Is(err, tsclient.ErrNotReady) {
return fmt.Errorf("tailnet %q not ready yet, retry", pg.Spec.Tailnet)
}
} Type guard
func tailnetUnavailable(err error) (notFound, notReady bool) {
notFound = errors.Is(err, tsclient.ErrClientNotFound)
notReady = errors.Is(err, tsclient.ErrNotReady)
return
} Try / catch
tsClient, err := r.clients.For(pg.Spec.Tailnet)
if err != nil {
if errors.Is(err, tsclient.ErrClientNotFound) {
// deterministic misconfig: surface on status, stop hot-retrying
nrr := ¬ReadyReason{reason: reasonProxyGroupTailnetUnavailable, message: err.Error()}
return reconcile.Result{}, r.maybeUpdateStatus(ctx, logger, pg, oldPGStatus, nrr, nil)
}
// ErrNotReady is transient: requeue
return reconcile.Result{RequeueAfter: time.Minute}, nil
} Prevention
- Configure and validate a tailnet on the operator before applying ProxyGroups that reference it
- Leave spec.tailnet blank unless multi-tailnet is intentionally enabled
- Watch the ProxyGroup TailnetUnavailable condition rather than logs to detect drift
- Keep tailnet names in a single source of truth (values file / GitOps repo) to avoid typos
When it happens
Trigger: r.clients.For(pg.Spec.Tailnet) where (1) spec.tailnet names a tailnet never registered with the operator via its multi-tailnet configuration (ErrClientNotFound: <name>), (2) the tailnet is registered but not yet marked ready — e.g., its login server credentials/API key not yet validated (ErrNotReady: <name>), (3) spec.tailnet non-empty while the operator runs without multi-tailnet support configured, so only the default (blank) tailnet exists.
Common situations: Setting spec.tailnet on a ProxyGroup without first configuring that tailnet on the operator; typos in the tailnet name; the tailnet's credential Secret still being bootstrapped; feature-flag/multi-tailnet alpha config missing after operator restart.
Related errors
- error validating cluster resources: %w
- cleaning up resources for previous ProxyGroup failed: %w
- unable to allocate additional ports on ProxyGroup %s, %d por
- failed to update tailscaled config: %w
- ProxyGroup %q is of type %q but must be of type %q
AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15).
Data as JSON: /api/errors/ca3439d47529c3b3.
Report an issue: GitHub.