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 := &notReadyReason{
			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

  1. Check the ProxyGroup status conditions for TailnetUnavailable and the embedded reason (client not found vs tailnet not ready)
  2. Verify spec.tailnet exactly matches a tailnet configured on the operator; fix typos or leave it blank to use the default tailnet
  3. Complete the operator-side configuration for that tailnet (register its API client/login URL) and wait for readiness
  4. 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 := &notReadyReason{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

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


AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15). Data as JSON: /api/errors/ca3439d47529c3b3. Report an issue: GitHub.