Skip to content

Conversation

@Raghav1428
Copy link

Fixes #26187

Cloud configuration URLs break when clouds are deleted or reordered. If you have three clouds and delete the first one, the URLs for the remaining two shift, causing 404 errors. This is especially problematic when multiple clouds share the same name since there's no way to distinguish them by URL alone.
The fix assigns each cloud a persistent unique ID (UUID) and routes URLs through that ID instead of the cloud name or index. This way, deleting or reordering clouds doesn't affect the URLs of other clouds.
Testing done
Wrote 14 unit tests in CloudUniqueIdTest covering:

UUID generation and uniqueness across 200 clouds
Thread safety of UUID generation (50 concurrent threads)
Duplicate name clouds can be individually accessed by their unique IDs
Deleting a cloud doesn't affect the IDs of remaining clouds
Copied clouds automatically get a new UUID via the overridden add() in CloudList
Input validation on getCloudById (null, empty, blank, missing)
Backward compatibility with cloudByIndex

Also manually tested on localhost:

  • Created three clouds with the same name
  • Verified each got a unique URL (cloud/cloudById/{uuid}/)
  • Deleted the first cloud — the other two remained accessible
  • Copied a cloud via the UI — the copy got a different UUID
  • Edited and saved a cloud — URL stayed the same

Screenshots (UI changes only)

N/A

Proposed changelog entries

  • Stabilize cloud configuration URLs by using persistent unique IDs instead of name or index-based routing

Proposed changelog category

/label bug

Proposed upgrade guidelines

N/A

Submitter checklist

  • The issue, if it exists, is well-described.
  • The changelog entries and upgrade guidelines are appropriate for the audience affected by the change (users or developers, depending on the change) and are in the imperative mood (see examples). Fill in the Proposed upgrade guidelines section only if there are breaking changes or changes that may require extra steps from users during upgrade.
  • There is automated testing or an explanation as to why this change has no tests.
  • New public classes, fields, and methods are annotated with @Restricted or have @since TODO Javadocs, as appropriate.
  • New deprecations are annotated with @Deprecated(since = "TODO") or @Deprecated(forRemoval = true, since = "TODO"), if applicable.
  • UI changes do not introduce regressions when enforcing the current default rules of Content Security Policy Plugin. In particular, new or substantially changed JavaScript is not defined inline and does not call eval to ease future introduction of Content Security Policy (CSP) directives (see documentation).
  • For dependency updates, there are links to external changelogs and, if possible, full differentials.
  • For new APIs and extension points, there is a link to at least one consumer.

Desired reviewers

@timja

Before the changes are marked as ready-for-merge:

Maintainer checklist

  • There are at least two (2) approvals for the pull request and no outstanding requests for change.
  • Conversations in the pull request are over, or it is explicit that a reviewer is not blocking the change.
  • Changelog entries in the pull request title and/or Proposed changelog entries are accurate, human-readable, and in the imperative mood.
  • Proper changelog labels are set so that the changelog can be generated automatically.
  • If the change needs additional upgrade steps from users, the upgrade-guide-needed label is set and there is a Proposed upgrade guidelines section in the pull request title (see example).
  • If it would make sense to backport the change to LTS, be a Bug or Improvement, and either the issue or pull request must be labeled as lts-candidate to be considered.

- Add unique ID field to Cloud class for stable identification
- Implement cloud/byId URL routing to prevent URL breakage
- Add CloudByIdDispatcher in CloudSet and Jenkins.CloudList
- Preserve cloud identity across reconfigurations
- Add comprehensive tests for unique ID behavior
@comment-ops-bot comment-ops-bot bot added the bug For changelog: Minor bug. Will be listed after features label Jan 31, 2026

/**
* Stapler dispatcher that routes cloud requests by unique ID.
* Handles URL patterns like /cloud/cloudById/{uuid}/
Copy link
Member

Choose a reason for hiding this comment

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

can't we either do:

/cloud/id/{uuid}

or just:
/cloud/{uuid} and we can route it by checking if the name is a UUID / it matches a cloud that we have a uuid for?

Copy link
Author

Choose a reason for hiding this comment

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

Ok i will change it to cloud/{uuud}

-Update getDynamic methods in CloudSet to consistently use getById() instead of mixing name-based and UUID-based lookups
Copy link
Contributor

@mawinter69 mawinter69 left a comment

Choose a reason for hiding this comment

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

Maybe we should include the uuid in the configure.jelly as a hidden field.
In the doConfigsubmit method we check if the uuids of this and the new cloud are identical. If not we throw an error. Don't know if that works though. Would require a DataBoundSetter setUniqueId

Might avoid all the logic we have in the doConfigSubmit at the beginning

I think we need a test for following scenario:

  1. Jenkins starts with a cloud that has no uuid
  2. the cloud should now have an id
  3. restart Jenkins
  4. the uuid you get in step 2 shouldn't change.

The problem right now might be that after the call to readResolve during startup there is no save of the Jenkins configuration happening which could lead to a loss of the uuid.

There is work in one of my plugins jenkinsci/agent-maintenance-plugin#320 that would greatly benefit from that uuid

}

@Test
void testReadResolveMigrationAssignsId() throws Exception {
Copy link
Contributor

@mawinter69 mawinter69 Feb 1, 2026

Choose a reason for hiding this comment

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

This is not testing the readResolve method. You test here that adding a cloud without id to Jenkins assigns a new id.

private volatile String uniqueId;

/**
* Uniquely identifies this {@link Cloud} instance among other instances in {@link jenkins.model.Jenkins#clouds}.
Copy link
Contributor

Choose a reason for hiding this comment

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

That comment is not true. Should be adjusted accordingly.

// Use identity comparison to find the correct cloud to replace
// This avoids issues where equals() (often based on name) matches multiple
// clouds
List<Cloud> newClouds = new ArrayList<>(j.clouds);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is that threadsafe? Assume one user changes a cloud and another user deletes a cloud at the same time. Unlikely but possible. One of the changes might then be lost when you replace the complete cloud list below

}
}

Cloud reconfigured = cloud.reconfigure(req, req.getSubmittedForm());
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we just use this.reconfigure here?

throw new Failure(String.format("No cloud type ‘%s’ is known", cloudDescriptorName));
throw new Failure(String.format("No cloud type '%s' is known", cloudDescriptorName));
}
Cloud cloud = cloudDescriptor.newInstance(req, req.getSubmittedForm());
Copy link
Contributor

Choose a reason for hiding this comment

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

Already here the new uuid should be set I think

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug For changelog: Minor bug. Will be listed after features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cloudbyIndex approach to handle clouds with same name is broken

3 participants