derailed/k9s · error

invalid annotation %q

Error message

invalid annotation %q

What it means

ParsePlainPF parses the k9s port-forward annotation shorthand (plain form: 'port' or 'local:remote' / 'port:name', full form 'container::local:remote'). It rejects an annotation value that is empty before any regex matching, because an empty string cannot carry a port pair.

Source

Thrown at internal/port/pf.go:39

)

var (
	pfRX      = regexp.MustCompile(`\A([\w-]+)::(\d*):?(\d*|[\w-]*)/?(\d+)?\z`)
	pfPlainRX = regexp.MustCompile(`\A(\d*):?(\d*|[\w-]*)\z`)
)

// PFAnn represents a portforward annotation value.
// Shape: container/portname|portNum:localPort
type PFAnn struct {
	Container        string
	ContainerPort    intstr.IntOrString
	LocalPort        string
	containerPortNum string
}

func ParsePlainPF(ann string) (*PFAnn, error) {
	if ann == "" {
		return nil, fmt.Errorf("invalid annotation %q", ann)
	}
	var pf PFAnn
	mm := pfPlainRX.FindStringSubmatch(strings.TrimSpace(ann))
	if len(mm) < 3 {
		return nil, fmt.Errorf("invalid plain port-forward %s", ann)
	}
	if mm[2] == "" {
		pf.ContainerPort = intstr.Parse(mm[1])
		pf.LocalPort = mm[1]
		return &pf, nil
	}
	pf.LocalPort, pf.ContainerPort = mm[1], intstr.Parse(mm[2])

	return &pf, nil
}

// ParsePF hydrate a portforward annotation from string.
func ParsePF(ann string) (*PFAnn, error) {

View on GitHub (pinned to 2d3ccc6ba2)

Solutions

  1. Remove the empty annotation: kubectl annotate pod <name> k9scli.io/port-forwards-
  2. Or set a valid value: plain '8080' (same local/remote), '9090:8080' (local:remote), or 'container::9090:8080'
  3. In Helm templates, guard with {{- if .Values.pf }} so the annotation is only emitted when non-empty

Example fix

# before: empty annotation
metadata:
  annotations:
    k9scli.io/port-forwards: ""
# after: either omit the key or set a valid value
metadata:
  annotations:
    k9scli.io/port-forwards: "9090:8080"
Defensive patterns

Strategy: validation

Validate before calling

// Skip empty annotation values before parsing.
for _, key := range []string{port.K9sPortForwardsKey, port.K9sAutoPortForwardsKey} {
    v := strings.TrimSpace(anns[key])
    if anns[key] != "" && v == "" {
        return fmt.Errorf("annotation %s is whitespace-only; remove it or set e.g. '9090:8080'", key)
    }
    if v == "" { continue }
    pf, err := port.ParsePlainPF(v)
    ...
}

Type guard

func validPFShape(v string) bool {
    v = strings.TrimSpace(v)
    if v == "" { return false }
    mm := regexp.MustCompile(`\A(\d*):?(\d*|[\w-]*)\z`).FindStringSubmatch(v)
    return len(mm) >= 3 && mm[0] == v
}

Try / catch

pf, err := port.ParsePlainPF(ann)
if err != nil {
    if strings.Contains(err.Error(), "invalid annotation") {
        // tolerate and skip: an empty annotation must not break port-forward discovery
        slog.Warn("ignoring empty k9s port-forward annotation", slogs.GVR, gvr)
        continue
    }
    return err
}

Prevention

When it happens

Trigger: A pod/service carries the k9scli.io/port-forwards or k9scli.io/auto-port-forwards annotation but its value is '' (or whitespace-only after trimming in the regex path): e.g. `k9scli.io/port-forwards: ""` set by a template variable that rendered empty.

Common situations: Helm templates defaulting the annotation to an empty string instead of omitting it; CI pipelines injecting the annotation key unconditionally; manual copy-paste leaving the value blank.

Related errors


AI-assisted analysis of derailed/k9s@2d3ccc6ba2 (2026-08-15). Data as JSON: /api/errors/8ee579bb4208e473. Report an issue: GitHub.