Skip to content

PMM-15260 Make pmm-agent Node registration idempotent - #5890

Merged
ademidoff merged 48 commits into
mainfrom
PMM-15260-idempotent-node-registration
Sep 22, 2026
Merged

ademidoff merged 48 commits into
mainfrom
PMM-15260-idempotent-node-registration

Conversation

@ademidoff

@ademidoff ademidoff commented Sep 4, 2026 •

Copy link
Copy Markdown
Member

Ticket number: PMM-15260

Feature build: Percona-Lab/pmm-submodules#4558

pmm-agent setup registered the Node on every run. A forced registration makes PMM Server drop the
Node together with every Service on it, because RegisterNode removes the existing Node with
RemoveCascade. That is harmless for the database operators, whose sidecars keep their configuration
in an ephemeral file and re-add their own Services on each container start. It silently deletes
user-added Services from a long-lived PMM Client, which is what PMM-14989 reports and what the
pmm-ha Helm chart needs in order to pre-provision Client pods as monitoring delegates.

This PR covers points 1 and 2 of the ticket:

  • Registration is skipped when it would be a no-op. An Agent whose configuration already holds an
    ID and which is set up against the same PMM Server keeps its Node and its Services. --force still
    registers the Node again exactly as before, and pointing an Agent at a different PMM Server
    registers it there. The address is compared against the configuration file rather than the merged
    configuration, which is what keeps pmm-admin config against a new server working.
  • The Agent verifies that the server still knows it before skipping. Otherwise an Agent is
    stranded whenever PMM Server no longer has its ID, e.g. after the server was reinstalled or
    restored from a backup predating the registration, and pmm-agent loops on "No Agent with ID" while
    monitoring nothing. Setup now asks the server and registers again when the answer is no. 404, 401
    and 403 all count as an answer, since registering either succeeds or reports a credentials problem
    with an actionable message. An unreachable server is not an answer: a failed check keeps the
    existing registration, so an Agent still starts while PMM Server has no elected leader.

Both commits are the prototypes from PMM-14665-skip-non-eligible-nodes-from-add-service, reverted in
9af8db5 to keep that ticket to its own scope. They are cherry-picked onto current main here with
no conflicts, and carry their unit tests.

Not in this PR

Points 3 and 4 of the ticket are deliberately left out and stay open:

  • Node adoption, so that a client reclaims a Node whose pmm-agent is not connected instead of
    failing with AlreadyExists. It weakens a guard that matters outside Kubernetes, where two hosts can
    share a hostname, and the ticket's open question on gating it needs settling first.
  • Treating "No Agent with ID" as a terminal rejection in the connect loop. Worth doing, but it
    would turn a systemd-managed install into a restart loop, and in Kubernetes a container restart does
    not re-run init containers, so it does not by itself help the case above.

The release note required by the ticket's acceptance criteria is also not here and still needs
writing: pmm-agent setup is now idempotent, and --force remains the way to force re-registration.

No API endpoint is added, removed or altered. The registration check consumes the existing
GET /v1/inventory/agents/{agent_id}.

If this PR is related to other PRs, contributions, or ongoing work in this or other repositories, please reference them here:

  • Fixes the root cause reported in PMM-14989.
  • PMM-14665, where points 1 and 2 were first prototyped.
  • The PMM-HA-GA branch of percona/percona-helm-charts pre-provisions PMM Client pods and currently
    gates registration on the configuration file being absent. Once this ships, that gate can be
    dropped so a Client whose Node the server has forgotten recovers on a pod restart.

`pmm-agent setup` registered the Node on every run, and a forced registration
makes PMM Server drop the Node together with every Service on it. That is
harmless for the database operators, whose sidecars re-add their own services on
each container start, but it would silently delete user-added services from a
long-lived PMM Client pod, which is what the pmm-ha Helm chart is about to
pre-provision as monitoring delegates.

Registration is now skipped when the Agent already holds an ID and is being set
up against the same PMM Server it is registered with. `--force` still registers
the Node again, and pointing an Agent at a different PMM Server registers it
there as before.
…gent

Skipping registration for an Agent which already holds an ID strands it whenever
PMM Server no longer knows that ID, e.g. after the server was reinstalled or
restored from a backup taken before the Agent was registered. pmm-agent then
loops on "No Agent with ID" while the Node monitors nothing.

`pmm-agent setup` now asks the server whether it still knows the Agent and
registers the Node again when it does not. Credentials the server rejects count
as an answer too: registering reports that with an actionable message, whereas
skipping it would fail silently later on.

An unreachable server is not an answer: a failed check keeps the registration, so
an Agent still starts while PMM Server has no leader elected yet.
@ademidoff
ademidoff requested a review from a team as a code owner September 4, 2026 16:15
@ademidoff
ademidoff requested review from 4nte and JiriCtvrtka and removed request for a team September 4, 2026 16:15
@coderabbitai

coderabbitai Bot commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The setup flow loads configuration, normalizes the server address, and queries inventory with one deadline. It classifies gRPC error payloads, detects Node conflicts and type mismatches, retries with setup credentials when required, and controls registration and configuration writes. Node removal and unregistration delete Grafana service accounts with bounded cleanup. Foreign service tokens produce warnings. The inventory API and command return those warnings.

Sequence Diagram(s)

sequenceDiagram
  participant Setup
  participant InventoryClient
  participant PMMServer
  Setup->>InventoryClient: Query Agent and Node with one deadline
  InventoryClient->>PMMServer: GetAgent and GetNode
  PMMServer-->>InventoryClient: Node data or classified error
  InventoryClient-->>Setup: Registration state
  Setup-->>Setup: Register, preserve, or stop
  Setup->>PMMServer: Remove service account during node cleanup
  PMMServer-->>Setup: Warning or error
Loading

Severity of issue fixed: Medium

Merge Risk: 🟡 Moderate · up to 5e686

Setup can wait nearly twice its intended timeout and omit a warning for discarded credentials. More importantly, a Grafana outage can hold node-removal database locks and connections for the cleanup timeout, so this should be resolved before merge.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: making pmm-agent Node registration idempotent.
Description check ✅ Passed The description includes the ticket number, feature build, change scope, related work, excluded items, and confirms that no API endpoint changed. It is complete and aligned with the repository templat…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: c516639e-a555-4ae5-a838-b1650f616a79

📥 Commits

Reviewing files that changed from the base of the PR and between cd3269b and 2d76509.

⛔ Files ignored due to path filters (1)
  • agent/packages.dot is excluded by !**/*.dot
📒 Files selected for processing (7)
  • agent/commands/clients.go
  • agent/commands/clients_test.go
  • agent/commands/setup.go
  • agent/commands/setup_test.go
  • agent/config/config.go
  • agent/config/config_test.go
  • agent/config/encryption_test.go
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • percona/pmm-qa (manual)
  • percona/pmm (manual)

Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review.

Comment thread agent/commands/clients.go Resolved
Comment thread agent/commands/setup_test.go Resolved
@codecov

codecov Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.32787% with 96 lines in your changes missing coverage. Please review.
✅ Project coverage is 46.34%. Comparing base (31318c7) to head (ce3ecef).
⚠️ Report is 207 commits behind head on main.

Files with missing lines Patch % Lines
agent/commands/setup.go 76.04% 37 Missing and 3 partials ⚠️
agent/commands/clients.go 75.71% 16 Missing and 1 partial ⚠️
agent/config/config.go 91.44% 7 Missing and 6 partials ⚠️
managed/services/inventory/nodes.go 83.63% 4 Missing and 5 partials ⚠️
admin/commands/inventory/remove_node.go 0.00% 4 Missing ⚠️
managed/cmd/pmm-managed/main.go 0.00% 3 Missing ⚠️
managed/services/grafana/client.go 81.25% 2 Missing and 1 partial ⚠️
managed/services/management/node.go 57.14% 2 Missing and 1 partial ⚠️
admin/commands/config.go 50.00% 2 Missing ⚠️
managed/services/inventory/grpc/nodes_server.go 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5890      +/-   ##
==========================================
+ Coverage   43.59%   46.34%   +2.75%     
==========================================
  Files         415      418       +3     
  Lines       43134    44115     +981     
==========================================
+ Hits        18804    20447    +1643     
+ Misses      22454    21675     -779     
- Partials     1876     1993     +117     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

ademidoff and others added 3 commits September 4, 2026 19:32
The GetAgent request built from a bare GetAgentParams literal carried no
timeout, so a hung PMM Server could stall `pmm-agent setup` forever.
Build the params with the generated constructor, which applies the
runtime default, and cover the hanging server in the test.

Use the Apache-2.0 header in agent/commands/setup_test.go, as every Go
file under agent/ does.

Signed-off-by: Alex Demidoff <a@demidoff.me>
Removing a Node through the inventory API left the Grafana service
account of its pmm-agent behind, together with a valid admin token.
The next registration of a Node with the same name then failed with
"service account already exists" after the Node had already been
committed, and every retry hit "Node already exists". The UI deletes
Nodes through this endpoint, so the automatic re-registration added
in this PR ran into it on every Node deleted from the UI.

Delete the service account once the removal is committed, the same way
UnregisterNode does. A failure only logs a warning: Nodes which
pmm-agent never registered have no account.

Signed-off-by: Alex Demidoff <a@demidoff.me>

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
managed/services/inventory/nodes.go (1)

370-370: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use structured fields for the cleanup warning.

Warnf embeds node.NodeName and err in the message. Use fields on logger.Get(ctx) so log consumers can filter the Node and failure consistently.

As per coding guidelines, use structured logging with fields instead of formatted values.

Proposed change
-	logger.Get(ctx).Warnf("Failed to delete the service account of node %s: %s", node.NodeName, err)
+	logger.Get(ctx).
+		WithField("node_name", node.NodeName).
+		WithError(err).
+		Warn("Failed to delete the service account")

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: e89f182f-2843-4f4e-be87-386dbeaa4577

📥 Commits

Reviewing files that changed from the base of the PR and between 20d4d39 and 4b7d132.

📒 Files selected for processing (7)
  • .mockery.yaml
  • managed/cmd/pmm-managed/main.go
  • managed/services/inventory/deps.go
  • managed/services/inventory/mock_grafana_client_test.go
  • managed/services/inventory/nodes.go
  • managed/services/inventory/nodes_test.go
  • managed/services/inventory/services_test.go
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • percona/pmm-qa (manual)
  • percona/pmm (manual)

Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review.

Comment thread managed/services/inventory/nodes.go Resolved
ademidoff and others added 2 commits September 5, 2026 00:47
The Grafana client has no HTTP timeout, so once the Node is removed an
unresponsive Grafana could hold the RemoveNode request until the proxy
gives up. Give the cleanup its own deadline, derived from the request
context to keep the auth headers, and have the tests check for it.

Signed-off-by: Alex Demidoff <a@demidoff.me>
The cleanup context was derived from the request, so a client giving
up right after the Node was removed cancelled it, and the service
account survived. Detach it from the request cancellation while keeping
the auth headers, and log the warning Grafana returns when it keeps an
account with custom tokens.

Signed-off-by: Alex Demidoff <a@demidoff.me>
`pmm-admin config` runs `pmm-agent setup` without --config-file, so the
Agent ID was never loaded and every run registered again. Load the file
the running pmm-agent reports, and decide about the registration from it.

While keeping a registration, keep the stored service token too: the
credentials given to setup only serve to register, and used to be
replaced by the token. Compare the PMM Server address of the file in its
normalized form, and register again when PMM Server has the Agent on a
Node with another name, which is what a configuration file copied from
another host looks like.

Tell the reasons apart in the output: rejected credentials, a server
which could not be asked, and Node settings which only take effect when
the Node is registered. Treat an ID PMM Server rejects as unknown to it.

Signed-off-by: Alex Demidoff <a@demidoff.me>

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
managed/services/inventory/nodes.go (1)

314-316: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve the RemoveNotFound gRPC contract.

models.FindNodeByID returns reform.ErrNoRows for an unknown ID, and this branch returns it directly. The existing managed/services/inventory/nodes_test.go contract expects codes.NotFound with Node with ID %q not found.. Handle reform.ErrNoRows explicitly before returning other errors.

Proposed fix
 node, err := models.FindNodeByID(s.db.Querier, id)
 if err != nil {
+	if errors.Is(err, reform.ErrNoRows) {
+		return status.Errorf(codes.NotFound, `Node with ID %q not found.`, id)
+	}
 	return err
 }

As per coding guidelines, check reform.ErrNoRows explicitly for “not found”.

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 71d2c61a-c85d-4eae-99c3-02b4839e5413

📥 Commits

Reviewing files that changed from the base of the PR and between 76faf05 and 95774d1.

📒 Files selected for processing (7)
  • admin/commands/config.go
  • agent/commands/clients.go
  • agent/commands/clients_test.go
  • agent/commands/setup.go
  • agent/commands/setup_test.go
  • agent/config/config.go
  • managed/services/inventory/nodes.go
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • percona/pmm-qa (manual)
  • percona/pmm (manual)

Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review.

Comment thread agent/commands/clients.go Resolved
A rejected or missing Node in the second lookup returned as unknown and
kept the registration. Map its status the same way as the Agent lookup,
so that rejected credentials and a missing Node register the Node again.

Signed-off-by: Alex Demidoff <a@demidoff.me>
Three ways the idempotent setup could still lose the Services on a Node,
or leave the Agent unable to reach PMM Server.

A Node name which does not match the one PMM Server has registered the
Agent on made setup register again. That does not replace the registered
Node: without --region the address is not checked for uniqueness, so PMM
Server creates a second Node under the given name and leaves the first
one behind with every Service on it, monitored by nothing. The Node name
defaults to the hostname, so any Node registered under a name of its own
took this path on every plain re-run. Setup now stops instead and names
both Nodes, --node-name to keep the registered one and --force to
replace it.

The registration was checked with the credentials given to setup, and
then those were dropped in favour of the stored service token, which
nothing had verified. A token PMM Server no longer accepts left setup
reporting success and the Agent looping on a rejected connection, with
no way back short of --force. Ask the server with the credentials the
Agent runs with, so that a confirmed registration means the Agent's own
credentials still work, and a rejected token registers the Node again
with the credentials given to setup.

A configuration file which exists but cannot be read - wrong encryption
key, truncated YAML - was reported and then treated as no file at all,
which means registering the Node. Whether the Agent is registered cannot
be told from such a file, so stop, unless --force asks for exactly that.

The Node address is only set when the Node is registered and nothing
updates it afterwards, so a kept registration keeps the address it was
registered with. Report it when it no longer matches: in pull metrics
mode that is the address PMM Server scrapes.

The Node lookup now returns the Node rather than its name alone, and
checkRegistration takes the lookup as an argument, so the states it
maps the answers of PMM Server to are covered by tests.

Signed-off-by: Alex Demidoff <a@demidoff.me>
RemoveNode and UnregisterNode both delete the Grafana service account
named after the Node, so that the token its pmm-agent authenticates with
does not outlive the Node. Only the inventory copy was given a context
of its own: UnregisterNode still passed the request context, so a client
which gave up right after the Node was removed left the account behind,
and an unresponsive Grafana held the request until the proxy gave up -
the two failures this PR fixed for the other endpoint.

Move the cleanup to services.RemoveNodeServiceAccount, which detaches
the context from the request while keeping the auth headers Grafana
needs, and bounds it. Both endpoints now behave the same way, and the
contract is covered by a test of its own rather than only through the
inventory service.

Signed-off-by: Alex Demidoff <a@demidoff.me>
@ademidoff
ademidoff force-pushed the PMM-15260-idempotent-node-registration branch from 18d609e to 7342135 Compare September 7, 2026 13:43
The service account cleanup of UnregisterNode now runs on a context of
its own, so the mock no longer sees the request context it expected and
the Register/Unregister subtest failed on an unexpected call. Match a
context which carries a deadline, the way the inventory test already
does, so that the test asserts what the cleanup has to do rather than
which context object it is handed.

Signed-off-by: Alex Demidoff <a@demidoff.me>
@percona percona deleted a comment from coderabbitai Bot Sep 7, 2026
@4nte

4nte commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

I stress-tested this against a live PMM built from your Feature Build and wanted to share what I found before it merges. The core fix works: a re-run of pmm-admin config with the right Node name now keeps the Node, the Agent ID, the service token and the user-added Services, where 3.9.1 returns 409 and exits 1. The context.WithoutCancel handling in RemoveNodeServiceAccount is right and the comment explaining it earns its place, and the registrationCheck / agentLookup seams made the whole thing testable without a server.

What follows is what sits around the fix. Most of it is on the paths an operator reaches by following the tool's own on-screen instructions.

How this was tested. perconalab/pmm-server-fb:PR-4558-77cd35a and perconalab/pmm-client-fb:PR-4558-77cd35a (both FullCommit: 818955b45) in a dedicated kind cluster, with a real MySQL Service on the Node so Service loss is observable. Baseline for every regression claim is released percona/pmm-client:3.9.1, run against the same config file and the same server. The starting state is a long-lived client registered as prod-db-1, which is deliberately not the hostname, carrying one user-added Service, tuned with --proc-mounts-path, --ports-min/max, --window-connected-time and --perfschema-refresh-rate.


Critical

1. --skip-registration erases the server address and the service token

agent/commands/setup.go:247-254, effect at :281

cfg.ID = fileCfg.ID now runs before the guard that used to stop this command, so the guard never fires. SkipRegistration then skips the whole switch, so keepRegistration never runs and the credentials are never restored, and execution falls through to SaveToFile with a config assembled from flags and defaults only.

$ pmm-agent setup --skip-registration          # pmm-agent running, no --config-file
Checking local pmm-agent status...
pmm-agent is running.
Configuration file /usr/local/percona/pmm/config/pmm-agent.yaml updated.
EXIT=0

$ grep -A3 '^server:' pmm-agent.yaml
server:
    address: ""
    username: ""
    password: ""

$ pmm-agent --config-file=...
level=error msg="PMM Server address is not provided, halting." component=client

3.9.1, same state: Can't skip registration: pmm-agent ID is empty., exit 1, config byte-identical.

The service token was the only copy, so the host cannot recover locally and re-registration then fails on the existing Node name. Suggest keeping the guard on the pre-adoption value, or running keepRegistration on the skip-registration path too.

2. The conflict message describes the opposite of what --force does there

agent/commands/setup.go:91, and the inline comment at :86-89 has it right

The message offers "--force to replace it, which removes it together with every Service on it". This branch is reached only when the names differ, RegisterNode resolves the Node to cascade-delete by the requested name (managed/services/management/node.go:42-48), and CheckUniqueNodeAddressRegion returns (nil, nil) immediately when the region is empty (managed/models/node_helpers.go:70-72). So nothing is removed.

$ pmm-admin config --server-url=... --server-insecure-tls    # no Node name, as it autodetects
PMM Server at ... has pmm-agent 6eb77e33-... on Node prod-db-1, not on 9d1e6be0ce9c.
Re-run with --node-name=prod-db-1 to keep that Node, or with --force to replace it,
which removes it together with every Service on it.
EXIT=1

$ pmm-admin config --server-url=... --server-insecure-tls --force
Registered.

$ curl .../v1/inventory/nodes
  generic   prod-db-1      4e967c98      # kept, agent connected=false
  container 9d1e6be0ce9c   81d628f8      # brand new, where the Agent now lives
$ curl .../v1/inventory/services
  mysql prod-db-1-mysql on node 4e967c98 # orphaned

The old pmm-agent reports connected: false while its exporters still report AGENT_STATUS_RUNNING, because nothing updates them, so the UI looks healthy while collection stops. This is the state eeb474de ("Never add a second Node for a registered Agent") set out to prevent, now reached by following the message. Since the lookup already returned the Node, one option is to unregister that Node by ID before registering when --force is given here, and word the message to match. Otherwise the message should only offer the Node-name form.

3. --node-name is not a flag in either CLI

agent/commands/setup.go:91, agent/config/config.go:501, admin/commands/config.go:49

$ pmm-admin config --server-url=... --node-name=prod-db-1
pmm-admin: error: unknown flag --node-name
EXIT=80

$ pmm-agent setup --node-name=prod-db-1
pmm-agent: error: unknown long flag '--node-name', try --help

It is the third positional argument in both. So the guard stops the operator from doing something destructive and then hands them a command that does not parse, leaving --force as the only thing they can type, which per finding 2 does not do what the same sentence says. The positional form is pmm-admin config <address> <type> prod-db-1.

4. The address-change message recommends --force without the Service-loss warning

agent/commands/setup.go:104-112

Here the names match, so --force really does reach models.RemoveNode(..., RemoveCascade). It is the only --force suggestion in the PR that omits "removing it together with every Service on it", and the only one where the removal happens. It also fires readily, because cfg.Setup.Address is autodetected per run while the Node's address is frozen at registration.

$ pmm-admin config --server-url=... 10.20.30.40 generic prod-db-1
Node prod-db-1 is registered with address 192.168.144.3, not 10.20.30.40. The registered
address is kept; it is the address PMM Server scrapes in pull metrics mode.
Use --force to register the Node with 10.20.30.40.

$ pmm-admin config --server-url=... --force 10.20.30.40 generic prod-db-1
Registered.

services before: mysql prod-db-1-mysql, postgresql pmm-server-postgresql
services after :                        postgresql pmm-server-postgresql

5. A 401 or 403 on the new check is read as "credentials rejected" and re-registers

agent/commands/clients.go:199, setup.go:78-81

The check runs with the stored service token against /v1/inventory/*, which requires the admin role (grafana/auth_server.go:83). Any 401 or 403 becomes errCredentialsRejected, then registrationMissing, which skips the conflict guard. Two realistic triggers:

  • Someone applies least privilege to the monitoring token, a normal thing to do after a security review.
  • PMM Server returns 401 for its own internal auth failures. httpStatusForAuthError maps everything except PermissionDenied to 401 (auth_server.go:300-306), and retrieveRole returns codes.Internal whenever the Grafana call fails (auth_server.go:628-641), so a Grafana restart produces the same verdict.

Reproduced by patching the Node's service account to Viewer and then running an unrelated pmm-admin config:

$ pmm-admin config --server-url=... --metrics-mode=push
PMM Server at ... does not accept the credentials of pmm-agent 506d060a-...,
registering the Node again.
Registering pmm-agent on PMM Server...
Registered.
EXIT=0
# result: duplicate Node, prod-db-1-mysql orphaned on the old one

With the hostname already taken it instead exits 1 with ... already exists. If you want override node, use --force option, which funnels to the destructive flag. The discriminator is already in the body: auth_server.go:271-277 sets code to 16 for a genuinely rejected credential and 13 for an internal error, so 401 could be classified on that rather than on the status alone. A cheaper option is to retry the lookup with the credentials given to setup before concluding anything.


High

6. The new success path rewrites the config from flags alone

agent/commands/setup.go:262-281, agent/config/config.go:341

On the pmm-admin config path get() returns early because there is no --config-file, so cfg holds only flags and defaults and the file is never merged in. Before this PR that path always aborted before SaveToFile. Now registrationConfirmed and registrationUnverified both fall through to it, and keepRegistration restores only the username and password.

$ pmm-admin config --server-url=... --server-insecure-tls 192.168.144.3 generic prod-db-1
Node is already registered with host.docker.internal:8443, pmm-agent ID is 506d060a-...
Configuration file .../pmm-agent.yaml updated.
EXIT=0

$ diff pmm-agent.yaml.golden pmm-agent.yaml
- proc-mounts-path: /host/proc/mounts
-     min: 30000              +     min: 42000
-     max: 30100              +     max: 51999
- perfschema-refresh-rate: 10 + perfschema-refresh-rate: 5
- window-connected-time: 30m0s + window-connected-time: 1h0m0s

3.9.1 leaves the file byte-identical. None of those settings has a pmm-admin config flag, so a credential rotation silently drops the /host/proc/mounts setting that PMM-14832 shipped for containerised hosts, breaking disk metrics at the next agent restart, and reverts a firewalled port range. Either merge fileCfg before saving, or do not save when registration was skipped.

7. An unmodelled Node type becomes a conflict with an empty Node name

agent/commands/clients.go:219-221 with setup.go:85

nodeOf's default returns a zero serverNode with a nil error, and GetNodeOKBody.Validate accepts an all-nil body because every validateX short-circuits on swag.IsZero. Since cfg.Setup.NodeName is a positional with .Default(hostname) and is effectively never empty, the != "" guard never protects anything:

PMM Server at ... has pmm-agent 6a455268-... on Node , not on gw-db-1.
Re-run with --node-name= to keep that Node, or with --force to replace it, which
removes it together with every Service on it.
EXIT=1

So a server that introduces a sixth Node type breaks pmm-agent setup for every older client in the field, with a message naming an empty Node and an instruction with no value in it. registrationUnverified looks like the fail-safe answer for a response the client could not interpret. This is also the only untested branch in the new agent code.

8. A 400 or 404 from anything in the path is treated as authoritative

agent/commands/clients.go:194-197

lookupError maps 400 and 404 to errAgentNotFound without checking that pmm-managed authored the answer, and readResponse tolerates io.EOF, so a bodyless 4xx parses into a well-formed GetAgentDefault. This PR makes the client depend on /v1/inventory/* for the first time, so a proxy whose path rules predate it now answers that call itself. With nginx in front of PMM allowlisting /v1/management/, /v1/server/ and /graph/:

# gateway returns application/json 404
PMM Server at ... does not know pmm-agent 6a455268-..., registering the Node again.

# same gateway, same 404 status, text/html body
Failed to check the registration of pmm-agent 6a455268-... : response from nginx: <html>...
Keeping the registration.

Identical status, opposite decisions, decided by the Content-Type of the error body. The safe branch is safe only because PMM's own 502 and 503 pages are HTML and get caught by the errorConsumer, which yields nginxError with no Code() method.

9. inventory remove node --force deletes operator-owned Grafana tokens

managed/services/inventory/nodes.go:368 with grafana/client.go:698-706

NodesService.Remove did not touch Grafana before this PR. The force passed through is the inventory cascade flag, but in DeleteServiceAccount it means "delete the whole account even when it holds tokens that are not pmm-agent's".

$ curl .../graph/api/serviceaccounts/2/tokens
 - 4 my-reporting-script-token
 - 2 pmm-agent-st-prod-db-1

$ pmm-admin inventory remove node 425dd375-... --force
Node removed.

$ curl .../graph/api/serviceaccounts/2/tokens
[]

RemoveNodeResponse is {}, so unlike UnregisterNodeResponse.warning there is no channel to report it. At minimum the two meanings of force should not share one positional bool.

10. A failed service-account cleanup is reported as success

managed/services/inventory/nodes.go:366-374, grafana/client.go:587

The error is logged at warning level and Remove returns nil. The code cannot tell the benign case from the dangerous one, because getServiceAccountIDFromName reports "no such account" with an undifferentiated fmt.Errorf. Reproduced by warming PMM's 60s auth cache, stopping Grafana, and deleting the Node inside that window:

$ curl -X DELETE .../v1/inventory/nodes/14f7117d-...?force=true
{}                                                    HTTP 200

$ curl .../v1/inventory/nodes/14f7117d-...             HTTP 404   # Node is gone
$ curl .../graph/api/serviceaccounts/search
  6 pmm-agent-sa-old-db-host2  role=Admin  tokens=1              # account survives
$ curl .../v1/inventory/nodes -H "Authorization: Bearer glsa_..."  HTTP 200
$ curl .../v1/server/settings -H "Authorization: Bearer glsa_..." HTTP 200

The account is created with the admin role, and /v1/inventory, /v1/management, /v1/backups, /v1/accesscontrol and /prometheus all require admin, so that is a live Admin credential for a host that no longer exists and nobody is told to revoke it. The same warning is also normal noise for a remote or RDS Node that never had an account, so it cannot be alerted on:

level=warning msg="Failed to delete the service account of node old-db-host2: ... connection refused"
level=warning msg="Failed to delete the service account of node remote-rds-1: service account pmm-agent-sa-remote-rds-1 not found"

RemoveWithoutServiceAccount codifies this: it stubs a "not found" error and asserts require.NoError, so it would pass identically with "connection refused". A distinguishable sentinel for "no such account", logged at debug, with everything else at error and named in a response warning, would separate the two.

11. Encrypted pmm-agent.yaml makes the idempotent path unreachable

agent/commands/setup.go:151-162, 235-241, agent/config/config.go:561, admin/commands/config.go:108-152

pmm-admin config forwards neither --config-file nor --config-file-key-file, and the documented way to enable encryption puts the key in the systemd unit or container environment, not in the admin's shell. LoadFromFile decrypts only when KeyFile is set, so registeredConfig parses ciphertext and fails.

$ pmm-admin config --server-url=... 192.168.144.3 generic prod-db-1
Failed to read the configuration file .../pmm-agent.yaml: yaml: control characters are not allowed.
Whether this pmm-agent is registered cannot be told from it. Fix the file, or re-run
with --force to register the Node again, removing it together with every Service on it.
EXIT=1

3.9.1 also exits 1 here, so the exit code is not the regression. The regression is that this PR's idempotent path is unreachable for these users, and that its failure is destructive where 3.9.1's was inert. After the 3.9.1 attempt the file is still ciphertext and the agent still starts. After following this message:

$ pmm-admin config --server-url=... --force 192.168.144.3 generic prod-db-1
Registered.
# 1. prod-db-1-mysql cascade deleted
# 2. od -c pmm-agent.yaml  ->  "# Updated by `pm"     plaintext, encryption gone
# 3. PMM_AGENT_CONFIG_FILE_KEY_FILE=... pmm-agent --config-file=...
#    level=fatal msg="Failed to load configuration: unable to RSA-unwrap AES key"

The file is not corrupt, the caller simply has no key, so the message should say that and point at --config-file-key-file rather than --force.


Medium

Six smaller items (click to expand)

A Node type change is accepted and ignored. setup.go:180-193. unappliedSetupFlags omits the positional node-type, --container-id and --container-name, which are all sent in RegisterNodeBody and all registration-only. pmm-admin config <addr> container prod-db-1 exits 0 having reported --region and --agent-password as unapplied and said nothing about the type, so the operator reasonably concludes the unreported change landed. 3.9.1 returned 409.

Wrong credentials produce a clean success. setup.go:167-171. --server-username and --server-password are not in the unapplied list either, so --server-url=https://admin:WRONG-PASSWORD@... exits 0 with "Node is already registered" and writes the stored token straight back. Skipping the supplied credentials is correct, it just needs saying out loud the way --region is.

An unreachable server now exits 0. setup.go:269-271. Keeping the registration is the right default and I would not change it. The issue is that it is indistinguishable from success to anything reading exit codes, so an Ansible run during a PMM outage goes green having verified nothing and, per finding 6, also rewritten the config. A distinct exit code, or a warning on stderr, keeps the intent.

The check uses the flag-supplied --id, never the file's. setup.go:119-129. registrationOf compares addresses but not IDs, so a stale PMM_AGENT_ID from a provisioning template is looked up using the file's token, comes back unknown, and turns a healthy client into a 409 plus a --force suggestion.

NormalizedAddress is not idempotent for bracketed IPv6. agent/config/config.go:56-66. [2001:db8::1] becomes [[2001:db8::1]]:443, and again [[[2001:db8::1]]:443]:443. The double-bracketing predates this PR, but the method is now the sole gate on the "same PMM Server?" comparison, so a mismatch there means re-registration. It also has no direct unit test in its own package.

The check can add 60s before registration starts. clients.go:156-180. Two sequential requests, each bounded by go-openapi's 30s DefaultTimeout. Measured 31s for one stalled request against a black-holed address. The comment says a hung server "cannot stall the setup"; naming the real value, or bounding the whole check once, would match.


The pattern

Most of the above traces to two decisions, so they may be cheaper to fix at the root than one by one.

Anything that is not a clear "yes" collapses into registrationMissing, which is also the enum's zero value and the terminus for six unrelated conditions, three of which are not statements about the registration. Making registrationUnverified the zero value and routing anything the client could not interpret there would retire finding 7, soften 5 and 8, and cost nothing when the server answers clearly.

Only registrationConflict ever compares Node names. Every other route to registrationMissing reaches register() with no idea which Node the Agent is on, and serverRegister sets Reregister only from --force, so that path can never replace a Node, only duplicate it or 409. Carrying the looked-up Node into register() would let a re-registration either replace the right Node or refuse.

On the release note the PR body already flags as outstanding: it is worth covering the new exit-1 conditions and the new pmm-admin config semantics, and documentation/docs/use/commands/pmm-agent.md:48 still carries the pre-PR --force wording. The two updated help strings in agent/config/config.go and admin/commands/config.go also disagree with each other about whether a removal always happens.

Happy to pair on any of these, or to hand over the environment and scripts if that is easier than rebuilding them.

The message for an Agent registered on another Node offered --force to
replace that Node together with its Services. RegisterNode resolves the
Node to cascade-delete by the requested name, and the uniqueness check on
the address is skipped without a region, so nothing is removed there: a
second Node appears and the registered one is left behind, monitored by
nothing. It also named --node-name, which is a positional argument in both
CLIs, not a flag.

The address-change message is the one place where --force does remove the
Node with every Service on it, and the only one which did not say so.

The two --force help strings disagreed with each other; align them and the
documentation with what the flag does.

Signed-off-by: Alex Demidoff <a@demidoff.me>
Reading the ID from the configuration file before the guard let
`pmm-agent setup --skip-registration` through where it used to stop. That
command skips the whole registration switch, so nothing restores the
credentials the Agent runs with, and it stored a configuration assembled
from the flags and the defaults alone: an empty server address and no
service token, which was the only copy of it.

Signed-off-by: Alex Demidoff <a@demidoff.me>
ademidoff and others added 6 commits September 20, 2026 09:13
`pmm-agent setup` now reads the configuration file to tell whether this
Node is already registered, and an encrypted file reads exactly like a
damaged one without the key: LoadFromFile skips Decrypt when no key file
is set and hands ciphertext to the YAML parser, so setup exits 1.

pmm-admin forwarded neither key flag, so there was no supported way to
run `pmm-admin config` on an encrypted installation - the env variable
the message suggests is the systemd unit's, not the operator's shell.
Forward both, defaulting to the same variables pmm-agent reads.

Signed-off-by: Alex Demidoff <a@demidoff.me>
The check compared the registered Node against cfg.Setup.NodeName, which
both `pmm-agent setup` and `pmm-admin config` fall back to the hostname
and always pass on. A Node registered under any name which is not the
hostname therefore failed every routine re-run: `pmm-admin config
--metrics-mode=pull <addr> generic` exited 1 on a Node named db-prod-1,
and a pmm-client container with PMM_AGENT_SETUP=1, a persisted config
volume and no PMM_AGENT_SETUP_NODE_NAME refused to start, because the
entrypoint exits on a non-zero setup.

Classify the name while the flags are parsed: a name is the operator's
when it is not the one it falls back to. kingpin fires no action for a
value which came from the environment - which is how a container names
its Node - so the classification happens after parsing rather than
during it.

A name which is only this host's is now reported the way a registered
address or type already is, and the registration stands.

Signed-off-by: Alex Demidoff <a@demidoff.me>
Keeping a registration refused to write the configuration file at all,
because what setup assembled holds nothing the file alone carries and
storing it would drop the ports range, the /proc/mounts path and every
other file-only setting. `pmm-admin config` runs setup without
--config-file, so every one of its invocations took that branch: it
printed that the file was left unchanged and exited 0 having applied
neither --paths-base nor --listen-port, --log-level, --log-lines-count
or --server-insecure-tls, none of which it even named. The same branch
swallowed `pmm-agent setup --skip-registration`, which exists to do
nothing but write.

Merge the flags onto the file the Agent runs with instead, which is the
merge Get already performs for --config-file: the flags given win, and
everything the file alone holds survives. The credentials stay the ones
the Agent runs with, since those given to setup only serve to register
and nothing was registered.

Get's exit hook becomes applyDefaults so that the merged configuration
reaches the same derived paths and defaults as a loaded one.

Signed-off-by: Alex Demidoff <a@demidoff.me>
setServerTransport maps HTMLMime, TextMime and DefaultMime to a consumer
which returns nginxError, and go-swagger drops the response it was
reading into whenever a consumer fails. The typed answer went with it,
and with the answer the HTTP status - the only thing which says a proxy
in front of PMM Server demanded credentials of its own. serverStatus
therefore returned 0 for every real proxy 401, so serverRefused was
false and the registration was never checked again with the credentials
given to setup.

Take the body as the message for the two registration lookups and keep
the typed answer, whose gRPC code stays zero because nothing PMM Server
sent set one. The management client still gets nginxError, which is what
register reports as "check pmm-managed logs".

The test asserted the proxy case while serving application/json on every
response, which no proxy does for its own error page, so it passed on a
path the code never took. Serve the content type each case describes.

Signed-off-by: Alex Demidoff <a@demidoff.me>
Registering returned the assembled configuration, so `--force` still
wrote a file built from flags and defaults alone and dropped the ports
range, the paths and the listening port with it. Those describe this
host's installation rather than the Node on PMM Server: --force asks for
the Node to be registered again, not for the Agent to be reconfigured
from scratch. Merge onto the file there as well, and take the ID and the
credentials from the assembled configuration, which after a registration
holds the ones it just returned.

Keep a log line count the file holds. --log-lines-count is the only
stored setting whose flag carries a default, so it was the only one
kingpin put that default over when the flags were re-parsed - a file
saying 512 came back as 1024. Neither an action nor a default tells a
value which was given from one which stood in, so the flag records that
it fired and the variable is read directly.

Signed-off-by: Alex Demidoff <a@demidoff.me>
serverNodeOfAgent built its own deadline, so the second lookup - the one
made when PMM Server refuses the credentials the Agent runs with - got a
fresh 30 seconds of its own. An operator promised one wait could face
two: measured at 560ms against a 300ms budget.

The caller owns the deadline now, and checkRegistrationOnServer sets it
once for the whole check. The comment on registrationCheckTimeout said
it bounded the check as a whole, which stopped being true when the
retry landed; it is true again.

Also report a username which was not used. keepRegistration restores
both credentials from the file, but unappliedCredentials compared only
passwords, so a username given beside the password the Agent already
runs with was discarded without being named - the one thing that stops
it passing for an accepted one.

Signed-off-by: Alex Demidoff <a@demidoff.me>
@JiriCtvrtka
JiriCtvrtka self-requested a review September 21, 2026 06:06
Comment thread agent/commands/setup.go Resolved
Comment thread agent/commands/setup.go Resolved
Comment thread agent/commands/setup.go Resolved
Comment thread agent/commands/setup.go Resolved
Comment thread agent/commands/setup.go Resolved
Comment thread agent/commands/setup.go Resolved
ademidoff and others added 6 commits September 21, 2026 10:27
A configuration file holds every path, derived or not, so parsing
--paths-base over it moved the base and nothing else: the exporters, the
tools, the temporary directory and the Nomad data directory kept pointing
at the base the file came from. Before the flags are merged onto the file
this was flags and defaults alone, where every path derived from the base
given.

Empty the paths the old base derived before the defaults are applied
again, so that they follow the new base. A path an operator set does not
match the old derivation and stays where it is.

Signed-off-by: Alex Demidoff <a@demidoff.me>
Storing the whole Server struct of the assembled configuration put back
exactly what merging the flags onto the file exists to avoid. The
assembled configuration carries the flags and the defaults, so a file
which alone holds `insecure-tls: true` was rewritten with it off, and
`without-tls`, which no flag carries, could never survive a re-run. On a
self-signed installation that leaves an Agent which can no longer reach
PMM Server.

Store only what setup settles, the ID and the credentials. The address
and the TLS settings are the ones the merge already applied.

Signed-off-by: Alex Demidoff <a@demidoff.me>
The lookup took the credentials from the file and the TLS settings from
the flags. PMM Server ships a self-signed certificate, so a re-run which
did not repeat --server-insecure-tls failed the lookup on certificate
verification and left the registration unverified, saying nothing about a
registration which is there.

Carry the setting the Agent runs with into the lookup. A flag which asks
to skip the check still stands over a file which does not.

Signed-off-by: Alex Demidoff <a@demidoff.me>
`pmm-admin config` runs `pmm-agent setup` with c.Output(), which keeps
stdout and drops stderr for a command that exits zero. The warning that
nothing about the registration was verified went to stderr, so the run
read as a confirmed registration.

Write it where the rest of what setup says already goes.

Signed-off-by: Alex Demidoff <a@demidoff.me>
Forwarding --config-file-key-password to `pmm-agent setup` put it in an
argv every local user can read with ps, and in the line which logs the
command under --debug.

Hand it to the child in the environment, which pmm-agent reads it from
anyway. The key file stays on the argv; it is a path, not a secret.

Signed-off-by: Alex Demidoff <a@demidoff.me>
Comment thread managed/services/management/node.go Resolved
@ademidoff
ademidoff merged commit 426f286 into main Sep 22, 2026
34 checks passed
@ademidoff
ademidoff deleted the PMM-15260-idempotent-node-registration branch September 22, 2026 10:25
JiriCtvrtka added a commit that referenced this pull request Oct 9, 2026
PMM Server answers a token Grafana failed to look up exactly like one it
rejects, so a rejected token says nothing about the registration - the
rule #5890 already applies to a refused token. The previous commit
handled every errCredentialsRejected that way instead, which:

- registered the Node again when setup had only the Agent's own token,
  so a brief Grafana failure could exit 1 or add a second Node;
- hid a mistyped password: a rejected password was asked about again
  and ended unverified, where #5890 reports it and exits 1;
- relied on errors.New(err.Error()) not matching the sentinel, and
  printed only "credentials rejected".

withGivenCredentials now turns errCredentialsRejected into
errTokenRejected only when the Agent runs with a token, and treats it
like a refusal through a single predicate. A rejected password takes
the #5890 path again, so the lookupError comment is back to its
original text.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Jiří Čtvrtka <j.ctvrtka@gmail.com>
JiriCtvrtka added a commit that referenced this pull request Oct 14, 2026
* PMM-15692 Report a rejected service-account token as Unauthenticated.

Grafana's /api/auth/serviceaccount has no sign-in middleware, so a token
it rejects (wrong, expired, revoked, or of a disabled service account)
gets 400 "Auth method is not service account token" instead of 401.
pmm-managed mapped that 400 to codes.Internal. Since PMM-15186 the CLIs
pick their hint from the code, so pmm-admin told users to check PMM
Server logs instead of their credentials.

Before PMM-13463 that 400 fell through to the API-key check, which
answered 401; the live "Token auth - fail" test was switched to
Internal when that fallback went away. Map 400 to Unauthenticated
alongside 401 and 403, and restore the test's original expectation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* PMM-15692 Keep the Node when a token is rejected

pmm-managed now answers a service token Grafana rejects with
codes.Unauthenticated instead of codes.Internal. pmm-agent setup read
Unauthenticated as errCredentialsRejected and registered the Node again
without asking with the credentials given to setup. With the Node still
registered that failed on its name ("already exists", exit 1, which
stops a container running setup on start) or added a second Node under
the hostname. A brief Grafana DB failure ends in the same 400, so it
could trigger this too.

withGivenCredentials now treats a rejection like a refusal: it asks
again with the credentials given to setup and only registers when PMM
Server does not know the Agent; otherwise the registration is left
unverified and kept, as before. The agent tests modelled a refused token
as 401 with codes.Internal; they now cover 401 with
codes.Unauthenticated, end to end through checkRegistrationOnServer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Jiří Čtvrtka <j.ctvrtka@gmail.com>

* PMM-15692 Shorten the rejected-token comment

AGENTS.md allows a comment one line, two at most; the explanation of
Grafana's 400 lives in the ticket and the first commit's message.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Jiří Čtvrtka <j.ctvrtka@gmail.com>

* PMM-15692 Test the rejected-token answer end to end

The test called retrieveRole directly, so it checked the authError but
not what a client receives. It now goes through ServeHTTP and asserts
the HTTP status and the JSON body, the "code" the CLIs pick their hint
from included.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Jiří Čtvrtka <j.ctvrtka@gmail.com>

* PMM-15692 Map only the token lookup's 400

The 400 Grafana gives a token it did not authenticate is specific to
/api/auth/serviceaccount, which has no sign-in middleware. Mapping every
400 in retrieveRole also covered /api/user and /api/user/orgs, so a
future 400 on the basic-auth or cookie path would have read as bad
credentials. getRoleForServiceToken now marks its 400 with
errServiceTokenRejected, and retrieveRole maps only that, alongside 401
and 403 as before. The log keeps Grafana's real status.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Jiří Čtvrtka <j.ctvrtka@gmail.com>

* PMM-15692 Name the Grafana error test for what it covers

The table holds a server error and a basic-auth 400 next to the
rejected credentials, so "RejectedCredentials" and its doc comment
described only part of it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Jiří Čtvrtka <j.ctvrtka@gmail.com>

* PMM-15692 Treat a rejected token as a refusal

PMM Server answers a token Grafana failed to look up exactly like one it
rejects, so a rejected token says nothing about the registration - the
rule #5890 already applies to a refused token. The previous commit
handled every errCredentialsRejected that way instead, which:

- registered the Node again when setup had only the Agent's own token,
  so a brief Grafana failure could exit 1 or add a second Node;
- hid a mistyped password: a rejected password was asked about again
  and ended unverified, where #5890 reports it and exits 1;
- relied on errors.New(err.Error()) not matching the sentinel, and
  printed only "credentials rejected".

withGivenCredentials now turns errCredentialsRejected into
errTokenRejected only when the Agent runs with a token, and treats it
like a refusal through a single predicate. A rejected password takes
the #5890 path again, so the lookupError comment is back to its
original text.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Jiří Čtvrtka <j.ctvrtka@gmail.com>

* PMM-15692 Say which Node a rejected token belongs to

With the service account disabled and the Node still there, the
credentials given to setup find the Node, but that answer was thrown
away: setup said "did not confirm that pmm-agent is registered", told
the operator to re-run once PMM Server answers, and offered --force,
which removes the Node with every Service on it.

For a rejected token, the Node the second lookup finds is now the
answer. Setup says that PMM Server has the Agent on that Node but does
not accept its token, points to the Node's service account, and spells
out what --force would remove; the registration is then confirmed and
kept. A refused (401 without Unauthenticated) answer keeps the first
answer as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Jiří Čtvrtka <j.ctvrtka@gmail.com>

* PMM-15692 Ask again the way the Agent reaches PMM Server

The second lookup set up its transport with the --server-insecure-tls
given to setup only, ignoring insecure-tls from the configuration file
that runningServer merges in for the first lookup. With the flag left
off, it failed PMM Server's self-signed certificate instead of finding
the Node, or finding it gone, and setup ended unverified. It now uses
the merged setting, as the first lookup does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Jiří Čtvrtka <j.ctvrtka@gmail.com>

* PMM-15692 Bring the registration doc comments up to date

serverNodeOfAgent and runningServer still said that a rejected token
registers the Node again, and serverRefused named a 401 without
Unauthenticated as the only grounds for asking again. A caller trusting
them would re-register on errCredentialsRejected, which is what
withGivenCredentials now guards against for a token.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Jiří Čtvrtka <j.ctvrtka@gmail.com>

* PMM-15692 Cover the rejected-token branches setup had no test for

A rejected token with no PMM Server address to ask again, and a second
lookup that fails for something other than the credentials - a deadline
or credentials that cannot read the inventory - all have to keep the
registration rather than register the Node again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Jiří Čtvrtka <j.ctvrtka@gmail.com>

---------

Signed-off-by: Jiří Čtvrtka <j.ctvrtka@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Documentation changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants