-
-
Notifications
You must be signed in to change notification settings - Fork 8.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[JENKINS-70729] Rework clouds management into multiple pages (#7658)
Co-authored-by: Jesse Glick <[email protected]> Co-authored-by: Daniel Beck <[email protected]> Co-authored-by: Tim Jacomb <[email protected]> Co-authored-by: Tim Jacomb <[email protected]>
- Loading branch information
1 parent
31f6dde
commit 4963094
Showing
28 changed files
with
771 additions
and
81 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,272 @@ | ||
/* | ||
* The MIT License | ||
* | ||
* Copyright (c) 2023, CloudBees Inc, and other contributors | ||
* | ||
* Permission is hereby granted, free of charge, to any person obtaining a copy | ||
* of this software and associated documentation files (the "Software"), to deal | ||
* in the Software without restriction, including without limitation the rights | ||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
* copies of the Software, and to permit persons to whom the Software is | ||
* furnished to do so, subject to the following conditions: | ||
* | ||
* The above copyright notice and this permission notice shall be included in | ||
* all copies or substantial portions of the Software. | ||
* | ||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
* THE SOFTWARE. | ||
*/ | ||
|
||
package jenkins.agents; | ||
|
||
import hudson.Extension; | ||
import hudson.Functions; | ||
import hudson.Util; | ||
import hudson.model.AbstractModelObject; | ||
import hudson.model.AutoCompletionCandidates; | ||
import hudson.model.Describable; | ||
import hudson.model.Descriptor; | ||
import hudson.model.Failure; | ||
import hudson.model.RootAction; | ||
import hudson.model.UpdateCenter; | ||
import hudson.slaves.Cloud; | ||
import hudson.util.FormValidation; | ||
import java.io.IOException; | ||
import java.net.URLEncoder; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.logging.Level; | ||
import java.util.logging.Logger; | ||
import javax.servlet.ServletException; | ||
import jenkins.model.Jenkins; | ||
import jenkins.model.ModelObjectWithChildren; | ||
import jenkins.model.ModelObjectWithContextMenu; | ||
import net.sf.json.JSONObject; | ||
import org.kohsuke.accmod.Restricted; | ||
import org.kohsuke.accmod.restrictions.DoNotUse; | ||
import org.kohsuke.accmod.restrictions.NoExternalUse; | ||
import org.kohsuke.stapler.QueryParameter; | ||
import org.kohsuke.stapler.StaplerProxy; | ||
import org.kohsuke.stapler.StaplerRequest; | ||
import org.kohsuke.stapler.StaplerResponse; | ||
import org.kohsuke.stapler.interceptor.RequirePOST; | ||
import org.kohsuke.stapler.verb.POST; | ||
|
||
@Restricted(NoExternalUse.class) | ||
public class CloudSet extends AbstractModelObject implements Describable<CloudSet>, ModelObjectWithChildren, RootAction, StaplerProxy { | ||
private static final Logger LOGGER = Logger.getLogger(CloudSet.class.getName()); | ||
|
||
@Override | ||
public Descriptor<CloudSet> getDescriptor() { | ||
return Jenkins.get().getDescriptorOrDie(CloudSet.class); | ||
} | ||
|
||
public Cloud getDynamic(String token) { | ||
return Jenkins.get().getCloud(token); | ||
} | ||
|
||
@Override | ||
@Restricted(NoExternalUse.class) | ||
public Object getTarget() { | ||
Jenkins.get().checkPermission(Jenkins.SYSTEM_READ); | ||
return this; | ||
} | ||
|
||
@Override | ||
public String getIconFileName() { | ||
return null; | ||
} | ||
|
||
@Override | ||
public String getDisplayName() { | ||
return Messages.CloudSet_DisplayName(); | ||
} | ||
|
||
@Override | ||
public String getUrlName() { | ||
return "cloud"; | ||
} | ||
|
||
@Override | ||
public String getSearchUrl() { | ||
return "/cloud/"; | ||
} | ||
|
||
@SuppressWarnings("unused") // stapler | ||
@Restricted(DoNotUse.class) // stapler | ||
public String getCloudUrl(StaplerRequest request, Jenkins jenkins, Cloud cloud) { | ||
String context = Functions.getNearestAncestorUrl(request, jenkins); | ||
if (Jenkins.get().getCloud(cloud.name) != cloud) { // this cloud is not the first occurrence with this name | ||
return context + "/cloud/cloudByIndex/" + getClouds().indexOf(cloud) + "/"; | ||
} else { | ||
return context + "/" + cloud.getUrl(); | ||
} | ||
} | ||
|
||
@SuppressWarnings("unused") // stapler | ||
@Restricted(DoNotUse.class) // stapler | ||
public Cloud getCloudByIndex(int index) { | ||
return Jenkins.get().clouds.get(index); | ||
} | ||
|
||
@SuppressWarnings("unused") // stapler | ||
public boolean isCloudAvailable() { | ||
return !Cloud.all().isEmpty(); | ||
} | ||
|
||
@SuppressWarnings("unused") // stapler | ||
public String getCloudUpdateCenterCategoryLabel() { | ||
return URLEncoder.encode(UpdateCenter.getCategoryDisplayName("cloud"), StandardCharsets.UTF_8); | ||
} | ||
|
||
@Override | ||
public ModelObjectWithContextMenu.ContextMenu doChildrenContextMenu(StaplerRequest request, StaplerResponse response) throws Exception { | ||
ModelObjectWithContextMenu.ContextMenu m = new ModelObjectWithContextMenu.ContextMenu(); | ||
Jenkins.get().clouds.stream().forEach(c -> m.add(c)); | ||
return m; | ||
} | ||
|
||
public Cloud getDynamic(String name, StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { | ||
return Jenkins.get().clouds.getByName(name); | ||
} | ||
|
||
@SuppressWarnings("unused") // stapler | ||
@Restricted(DoNotUse.class) // stapler | ||
public Jenkins.CloudList getClouds() { | ||
return Jenkins.get().clouds; | ||
} | ||
|
||
@SuppressWarnings("unused") // stapler | ||
@Restricted(DoNotUse.class) // stapler | ||
public boolean hasClouds() { | ||
return !Jenkins.get().clouds.isEmpty(); | ||
} | ||
|
||
/** | ||
* Makes sure that the given name is good as an agent name. | ||
* @return trimmed name if valid; throws ParseException if not | ||
*/ | ||
public String checkName(String name) throws Failure { | ||
if (name == null) | ||
throw new Failure("Query parameter 'name' is required"); | ||
|
||
name = name.trim(); | ||
Jenkins.checkGoodName(name); | ||
|
||
if (Jenkins.get().getCloud(name) != null) | ||
throw new Failure(Messages.CloudSet_CloudAlreadyExists(name)); | ||
|
||
// looks good | ||
return name; | ||
} | ||
|
||
@SuppressWarnings("unused") // stapler | ||
public FormValidation doCheckName(@QueryParameter String value) { | ||
Jenkins.get().checkPermission(Jenkins.ADMINISTER); | ||
if (Util.fixEmpty(value) == null) { | ||
return FormValidation.ok(); | ||
} | ||
try { | ||
checkName(value); | ||
return FormValidation.ok(); | ||
} catch (Failure e) { | ||
return FormValidation.error(e.getMessage()); | ||
} | ||
} | ||
|
||
/** | ||
* First check point in creating a new cloud. | ||
*/ | ||
@RequirePOST | ||
public synchronized void doCreate(StaplerRequest req, StaplerResponse rsp, | ||
@QueryParameter String name, @QueryParameter String mode, | ||
@QueryParameter String from) throws IOException, ServletException, Descriptor.FormException { | ||
final Jenkins jenkins = Jenkins.get(); | ||
jenkins.checkPermission(Jenkins.ADMINISTER); | ||
|
||
if (mode != null && mode.equals("copy")) { | ||
name = checkName(name); | ||
|
||
Cloud src = jenkins.getCloud(from); | ||
if (src == null) { | ||
if (Util.fixEmpty(from) == null) { | ||
throw new Failure(Messages.CloudSet_SpecifyCloudToCopy()); | ||
} else { | ||
throw new Failure(Messages.CloudSet_NoSuchCloud(from)); | ||
} | ||
} | ||
|
||
// copy through XStream | ||
String xml = Jenkins.XSTREAM.toXML(src); | ||
// Not great, but cloud name is final | ||
xml = xml.replace("<name>" + src.name + "</name>", "<name>" + name + "</name>"); | ||
Cloud result = (Cloud) Jenkins.XSTREAM.fromXML(xml); | ||
jenkins.clouds.add(result); | ||
// send the browser to the config page | ||
rsp.sendRedirect2(Functions.getNearestAncestorUrl(req, jenkins) + "/" + result.getUrl() + "configure"); | ||
} else { | ||
// proceed to step 2 | ||
if (mode == null) { | ||
throw new Failure("No mode given"); | ||
} | ||
|
||
Descriptor<Cloud> d = Cloud.all().findByName(mode); | ||
if (d == null) { | ||
throw new Failure("No node type ‘" + mode + "’ is known"); | ||
} | ||
handleNewCloudPage(d, name, req, rsp); | ||
} | ||
} | ||
|
||
private void handleNewCloudPage(Descriptor<Cloud> descriptor, String name, StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, Descriptor.FormException { | ||
checkName(name); | ||
JSONObject formData = req.getSubmittedForm(); | ||
formData.put("name", name); | ||
formData.put("cloudName", name); // ec2 uses that field name | ||
formData.remove("mode"); // Cloud descriptors won't have this field. | ||
req.setAttribute("instance", formData); | ||
req.setAttribute("descriptor", descriptor); | ||
req.getView(this, "_new.jelly").forward(req, rsp); | ||
} | ||
|
||
/** | ||
* Really creates a new agent. | ||
*/ | ||
@POST | ||
public synchronized void doDoCreate(StaplerRequest req, StaplerResponse rsp, | ||
@QueryParameter String type) throws IOException, ServletException, Descriptor.FormException { | ||
Jenkins.get().checkPermission(Jenkins.ADMINISTER); | ||
Cloud cloud = Cloud.all().find(type).newInstance(req, req.getSubmittedForm()); | ||
if (!Jenkins.get().clouds.add(cloud)) { | ||
LOGGER.log(Level.WARNING, () -> "Creating duplicate cloud name " + cloud.name + ". Plugin " + Jenkins.get().getPluginManager().whichPlugin(cloud.getClass()) + " should be updated to support user provided name."); | ||
} | ||
// take the user back to the cloud list top page | ||
rsp.sendRedirect2("."); | ||
} | ||
|
||
@Extension | ||
public static class DescriptorImpl extends Descriptor<CloudSet> implements StaplerProxy { | ||
|
||
/** | ||
* Auto-completion for the "copy from" field in the new cloud page. | ||
*/ | ||
@SuppressWarnings("unused") // stapler | ||
public AutoCompletionCandidates doAutoCompleteCopyNewItemFrom(@QueryParameter final String value) { | ||
final AutoCompletionCandidates r = new AutoCompletionCandidates(); | ||
Jenkins.get().clouds.stream() | ||
.filter(c -> c.name.startsWith(value)) | ||
.forEach(c -> r.add(c.name)); | ||
return r; | ||
} | ||
|
||
@Override | ||
public Object getTarget() { | ||
Jenkins.get().checkPermission(Jenkins.ADMINISTER); | ||
return this; | ||
} | ||
} | ||
} |
Oops, something went wrong.