Skip to content

feat(node): declare labels in the node config file instead of --labels - #382

Open
kaisoz wants to merge 1 commit into
google:mainfrom
kaisoz:kaisoz/move-labels-config
Open

feat(node): declare labels in the node config file instead of --labels#382
kaisoz wants to merge 1 commit into
google:mainfrom
kaisoz:kaisoz/move-labels-config

Conversation

@kaisoz

@kaisoz kaisoz commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

What

Operator-declared node labels move from sam-node run --labels region=eu,team=platform to a top-level labels: map in the node config file. The --labels flag is removed.

version: "v1alpha1"
labels:
  region: us-east-1
  team: platform

Why

Labels are declared at enrollment and attested by the control plane. But --labels was registered only on runCmd, while joinCmd read the same package-level variable - so sam-node join could never set it and always enrolled with no labels, silently. That is the path the unauthenticated sidecar tells operators to run.

--config is a persistent flag, so moving the declaration into sam-node.yaml makes both enrollment paths carry it.

Breaking change

sam-node run --labels region=eu now fails with unknown flag: --labels. No deprecation shim: the config schema is v1alpha1 and every caller lived in this repo.

The schema version is not bumped. LoadNodeConfig uses yaml.UnmarshalStrict, so an older binary already refuses a config carrying an unknown labels: key - which is what that check is for.

Notable details

  • Validation moves to load time. api.ValidateLabels runs in LoadNodeConfig and the error names the offending file, matching how a bad service or policy is reported. The check in node.Options.Validate is dropped: nothing covered it, the control plane re-validates enrollment labels fail-closed, and gossiped labels only rank candidates - the gate reads control-plane-signed label() facts.
  • The label value charset is unchanged (no ,, =, or control characters). YAML would allow them, but the comma-separated key=value wire format did not move: it is still the X-Sam-Required-Labels header and the label() facts.
  • SamNode.LocalPolicy is renamed nodeConfig, since the struct now carries identity claims alongside Services and the attenuation rules.

Tests

  • internal/node/config_test.go - labels parsed; malformed key rejected.
  • tests/integration/{a2a,openai_facade}_test.go - labels declared through the config file; writeServicesConfig becomes writeNodeConfig(t, dir, labels, services...).
  • tests/e2e/a2a_mesh.bats - labels move into tests/e2e/docker/a2a-echo/sam-node-config.yaml.
  • tests/e2e/auth_flows.bats - new: sam-node join with a labels config must come back attested, read from /sam/identity over the Unix socket. This is the regression the change fixes.

@kaisoz
kaisoz requested a review from aojea September 9, 2026 21:30
@google-cla

google-cla Bot commented Sep 9, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request transitions the declaration of operator labels from command-line flags to the node configuration file (sam-node.yaml), removing the --labels flag and updating configuration schemas, tests, and documentation. The review feedback identifies multiple potential nil pointer dereference risks when accessing nodeConfig.Labels directly and suggests adding defensive nil checks to prevent panics.

Comment thread internal/node/discovery_source.go Outdated
// inference services announce model IDs, MCP services announce tool names.
func (n *SamNode) discoverySource() []discovery.Announcement {
labels := n.config.Labels // validated at startup
labels := n.nodeConfig.Labels // validated at load

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Accessing n.nodeConfig.Labels directly without a nil check on n.nodeConfig can cause a nil pointer dereference panic if the node is initialized with a nil configuration (which is common in testing environments or custom setups). Adding a defensive nil check ensures the node remains robust and panic-free.

	var labels map[string]string
	if n.nodeConfig != nil {
		labels = n.nodeConfig.Labels
	}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nodeConfig cannot be nil on any node this path reaches. NewSamNode is the only non-test constructor and calls cfg.Default() first, which fills it. It is nil in tests that build SamNode literals, though, so the four accesses now go through a labels() accessor holding a single guard, rather than repeating the check at each call site.

Comment thread internal/node/enroll.go Outdated
PublicKey: pubBytes,
RequestedRole: n.config.RequiredRole,
Labels: n.config.Labels, // validated at startup
Labels: n.nodeConfig.Labels, // validated at load

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Accessing n.nodeConfig.Labels directly without a nil check on n.nodeConfig can cause a nil pointer dereference panic if the node is initialized with a nil configuration. Adding a defensive nil check ensures the node remains robust and panic-free.

Suggested change
Labels: n.nodeConfig.Labels, // validated at load
Labels: func() map[string]string { if n.nodeConfig != nil { return n.nodeConfig.Labels }; return nil }(),

@kaisoz kaisoz Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as #382 (comment)

Comment thread internal/node/enroll.go Outdated
PublicKey: pubBytes,
RequestedRole: n.config.RequiredRole,
Labels: n.config.Labels, // validated at startup
Labels: n.nodeConfig.Labels, // validated at load

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Accessing n.nodeConfig.Labels directly without a nil check on n.nodeConfig can cause a nil pointer dereference panic if the node is initialized with a nil configuration. Adding a defensive nil check ensures the node remains robust and panic-free.

Suggested change
Labels: n.nodeConfig.Labels, // validated at load
Labels: func() map[string]string { if n.nodeConfig != nil { return n.nodeConfig.Labels }; return nil }(),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as #382 (comment)

Comment thread internal/node/openai_facade.go Outdated
return node.revokedPeers != nil && node.revokedPeers.Contains(peerID)
},
localLabels: func() map[string]string { return node.config.Labels },
localLabels: func() map[string]string { return node.nodeConfig.Labels },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Accessing node.nodeConfig.Labels directly without a nil check on node.nodeConfig can cause a nil pointer dereference panic if the node is initialized with a nil configuration. Adding a defensive nil check ensures the node remains robust and panic-free.

Suggested change
localLabels: func() map[string]string { return node.nodeConfig.Labels },
localLabels: func() map[string]string { if node.nodeConfig != nil { return node.nodeConfig.Labels }; return nil },

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as #382 (comment)

@kaisoz
kaisoz force-pushed the kaisoz/move-labels-config branch from e08d546 to 3853aa7 Compare September 9, 2026 21:35
Labels are declared at enrollment, but --labels was registered only on
runCmd while joinCmd read the same variable, so `sam-node join` always
enrolled with no labels at all. --config is a persistent flag, so moving
the declaration into sam-node.yaml makes both enrollment paths carry it.

The flag goes with no shim: the config schema is v1alpha1 and every caller
lived in this repo. Validation moves to LoadNodeConfig, which names the
offending file, and SamNode.LocalPolicy becomes nodeConfig now that the
struct carries identity claims alongside policy.
@kaisoz
kaisoz force-pushed the kaisoz/move-labels-config branch from 3853aa7 to fe189d9 Compare September 9, 2026 21:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant