Skip to content

Commit

Permalink
[JENKINS-50726] GUI to configure the plugin (#37)
Browse files Browse the repository at this point in the history
[JENKINS-50726] GUI to configure the plugin
  • Loading branch information
kuisathaverat committed Jun 8, 2018
1 parent 5d3bac0 commit ab3412d
Show file tree
Hide file tree
Showing 20 changed files with 324 additions and 54 deletions.
24 changes: 12 additions & 12 deletions README.md
Expand Up @@ -41,12 +41,19 @@ This is an example policy
}
```

Then, run the Jenkins master with the environment variables. Note the `/` at the end of `S3_DIR`
In order to configure the plugin on Jenkins, you have to go to Manage Jenkins/Configure System to
the `Artifact Managment for Builds` section, there you have to select the Cloud Provider `Amazon S3`.

```
S3_BUCKET=my-bucket-name
S3_DIR=some/path/
```
![](images/cloud-provider-no-configured.png)

Then you can configure the S3 Bucket settings on the section `Amazon S3 Bucket Access settings` in
the same configuration page.

* S3 Bucket Name: Name of the S3 Bucket to use to store artifacts.
* S3 Bucket Region: Region to use to generate the URLs to get/put artifacts, by default it is autodetected.
* Base Prefix: Prefix to use for files and folders inside the S3 Bucket, if the prefix is a folder should be end with `/`.

![](images/bucket-settings.png)

# Testing

Expand Down Expand Up @@ -81,8 +88,6 @@ For interactive testing, you may instead add to `~/.mavenrc` (cf. comment in MNG
```sh
export AWS_PROFILE=…
export AWS_REGION=…
export S3_BUCKET=…
export S3_DIR=…/
```

then:
Expand Down Expand Up @@ -118,8 +123,3 @@ Or to just see HTTP traffic:
```bash
java -jar jenkins-cli.jar -s http://localhost:8080/jenkins/ tail-log org.jclouds.rest.internal.InvokeHttpMethod -l FINE
```

# Force the Region

If you have problems detecting the region on your environment you can force the region setting by adding this property
`-Dio.jenkins.plugins.artifact_manager_s3.S3BlobStore.region=REGION_NAME` to the Jenkins JVM options.
Binary file added images/bucket-settings.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/cloud-provider-configured.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/cloud-provider-no-configured.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Expand Up @@ -59,13 +59,13 @@ public enum HttpMethod {
@NonNull
public abstract String getContainer();

/** A constant to define whether we should delete blobs or leave them to be managed on the blob service side. */
public abstract boolean isDeleteBlobs();
/** A constant to define whether we should delete artifacts or leave them to be managed on the blob service side. */
public abstract boolean isDeleteArtifacts();

/** A constant to define whether we should delete stashes or leave them to be managed on the blob service side. */
public abstract boolean isDeleteStashes();

/** Creates the jclouds handle for working with blobs. */
/** Creates the jclouds handle for working with blob. */
@NonNull
public abstract BlobStoreContext getContext() throws IOException;

Expand Down
Expand Up @@ -153,7 +153,7 @@ public Void invoke(File f, VirtualChannel channel) throws IOException, Interrupt
@Override
public boolean delete() throws IOException, InterruptedException {
String blobPath = getBlobPath("");
if (!provider.isDeleteBlobs()) {
if (!provider.isDeleteArtifacts()) {
LOGGER.log(Level.FINE, "Ignoring blob deletion: {0}", blobPath);
return false;
}
Expand Down
Expand Up @@ -38,9 +38,11 @@
import java.util.logging.Logger;

import javax.annotation.Nonnull;
import javax.ws.rs.HEAD;

import io.jenkins.plugins.artifact_manager_jclouds.JCloudsArtifactManager;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSSessionCredentials;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;

import org.apache.commons.lang.StringUtils;
import org.jclouds.ContextBuilder;
import org.jclouds.aws.domain.SessionCredentials;
Expand All @@ -54,10 +56,6 @@
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSSessionCredentials;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;

import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import io.jenkins.plugins.artifact_manager_jclouds.BlobStoreProvider;
Expand All @@ -75,39 +73,36 @@ public class S3BlobStore extends BlobStoreProvider {

private static final long serialVersionUID = -8864075675579867370L;

// For now, these are taken from the environment, rather than being configured.
@SuppressWarnings("FieldMayBeFinal")
private static String BLOB_CONTAINER = System.getenv("S3_BUCKET");
@SuppressWarnings("FieldMayBeFinal")
private static String PREFIX = System.getenv("S3_DIR");
@SuppressWarnings("FieldMayBeFinal")
private static String REGION = System.getProperty(S3BlobStore.class.getName() + ".region");
@SuppressWarnings("FieldMayBeFinal")
private static boolean DELETE_BLOBS = Boolean.getBoolean(S3BlobStore.class.getName() + ".deleteBlobs");
@SuppressWarnings("FieldMayBeFinal")
private static boolean DELETE_STASHES = Boolean.getBoolean(S3BlobStore.class.getName() + ".deleteStashes");

@DataBoundConstructor
public S3BlobStore() {}
public S3BlobStore() {
}

@Override
public String getPrefix() {
return PREFIX;
return getConfiguration().getPrefix();
}

@Override
public String getContainer() {
return BLOB_CONTAINER;
return getConfiguration().getContainer();
}

public String getRegion() {
return getConfiguration().getRegion();
}

public S3BlobStoreConfig getConfiguration(){
return S3BlobStoreConfig.get();
}

@Override
public boolean isDeleteBlobs() {
return DELETE_BLOBS;
public boolean isDeleteArtifacts() {
return getConfiguration().isDeleteArtifacts();
}

@Override
public boolean isDeleteStashes() {
return DELETE_STASHES;
return getConfiguration().isDeleteStashes();
}

@Override
Expand All @@ -117,11 +112,12 @@ public BlobStoreContext getContext() throws IOException {
try {
Properties props = new Properties();

if(StringUtils.isNotBlank(REGION)) {
props.setProperty(LocationConstants.PROPERTY_REGIONS, REGION);
if(StringUtils.isNotBlank(getRegion())) {
props.setProperty(LocationConstants.PROPERTY_REGIONS, getRegion());
}

return ContextBuilder.newBuilder("aws-s3").credentialsSupplier(getCredentialsSupplier())
.overrides(props)
.buildView(BlobStoreContext.class);
} catch (NoSuchElementException x) {
throw new IOException(x);
Expand Down Expand Up @@ -199,14 +195,28 @@ public URL toExternalURL(@NonNull Blob blob, @NonNull HttpMethod httpMethod) thr
return builder.build().generatePresignedUrl(container, name, expiration, awsMethod);
}

public boolean isConfigured(){
return StringUtils.isNotBlank(getContainer());
}

@Extension
public static final class DescriptorImpl extends BlobStoreProviderDescriptor {

@Override
public String getDisplayName() {
return "Amazon S3";
}

}

@Override
public String toString() {
final StringBuilder sb = new StringBuilder("S3BlobStore{");
sb.append("container='").append(getContainer()).append('\'');
sb.append(", prefix='").append(getPrefix()).append('\'');
sb.append(", region='").append(getRegion()).append('\'');
sb.append(", deleteArtifacts='").append(isDeleteArtifacts()).append('\'');
sb.append(", deleteStashes='").append(isDeleteStashes()).append('\'');
sb.append('}');
return sb.toString();
}
}
@@ -0,0 +1,163 @@
/*
* The MIT License
*
* Copyright 2018 CloudBees, Inc.
*
* 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 io.jenkins.plugins.artifact_manager_s3;

import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import org.apache.commons.lang.StringUtils;
import org.jclouds.aws.domain.Region;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.util.FormValidation;
import hudson.util.ListBoxModel;
import jenkins.model.GlobalConfiguration;

/**
* Store the S3BlobStore configuration to save it on a separate file. This make that
* the change of container does not affected to the Artifact functionality, you could change the container
* and it would still work if both container contains the same data.
*/
@Extension
public class S3BlobStoreConfig extends GlobalConfiguration {

private static final String BUCKET_REGEXP = "^([a-z]|(\\d(?!\\d{0,2}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})))([a-z\\d]|(\\.(?!(\\.|-)))|(-(?!\\.))){1,61}[a-z\\d\\.]$";
private static final Pattern bucketPattern = Pattern.compile(BUCKET_REGEXP);

private static final Logger LOGGER = Logger.getLogger(S3BlobStoreConfig.class.getName());

@SuppressWarnings("FieldMayBeFinal")
private static boolean DELETE_ARTIFACTS = Boolean.getBoolean(S3BlobStoreConfig.class.getName() + ".deleteArtifacts");
@SuppressWarnings("FieldMayBeFinal")
private static boolean DELETE_STASHES = Boolean.getBoolean(S3BlobStoreConfig.class.getName() + ".deleteStashes");

/**
* Name of the S3 Bucket.
*/
private String container;
/**
* Prefix to use for files, use to be a folder.
*/
private String prefix;
/**
* force the region to use for the URLs generated.
*/
private String region;

@DataBoundConstructor
public S3BlobStoreConfig() {
load();
}

public String getContainer() {
return container;
}

@DataBoundSetter
public void setContainer(String container) {
this.container = container;
save();
}

public String getPrefix() {
return prefix;
}

@DataBoundSetter
public void setPrefix(String prefix) {
this.prefix = prefix;
save();
}

public String getRegion() {
return region;
}
@DataBoundSetter
public void setRegion(String region) {
this.region = region;
save();
}

public boolean isDeleteArtifacts() {
return DELETE_ARTIFACTS;
}

public boolean isDeleteStashes() {
return DELETE_STASHES;
}

@Nonnull
@Override
public String getDisplayName() {
return "Amazon S3 Bucket Access settings";
}

@Nonnull
public static S3BlobStoreConfig get() {
return ExtensionList.lookupSingleton(S3BlobStoreConfig.class);
}

public ListBoxModel doFillRegionItems() {
ListBoxModel regions = new ListBoxModel();
regions.add("Auto", "");
for (String s : Region.DEFAULT_S3) {
regions.add(s);
}
return regions;
}

public FormValidation doCheckContainer(@QueryParameter String container){
FormValidation ret = FormValidation.ok();
if (StringUtils.isBlank(container)){
ret = FormValidation.warning("The container name cannot be empty");
} else if (!bucketPattern.matcher(container).matches()){
ret = FormValidation.error("The S3 Bucket name does not match with S3 bucket rules");
}
return ret;
}

public FormValidation doCheckPrefix(@QueryParameter String prefix){
FormValidation ret;
if (StringUtils.isBlank(prefix)) {
ret = FormValidation.ok("Artifacts will be stored in the root folder of the S3 Bucket.");
} else if (prefix.endsWith("/")) {
ret = FormValidation.ok();
} else {
ret = FormValidation.error("A prefix must end with a slash.");
}
return ret;
}

public FormValidation doCheckRegion(@QueryParameter String region){
FormValidation ret = FormValidation.ok();
if (StringUtils.isNotBlank(region) && !Region.DEFAULT_REGIONS.contains(region)){
ret = FormValidation.error("Region is not valid");
}
return ret;
}
}
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<j:if test="${!instance.isConfigured()}">
<div class="alert alert-warning">
<strong>${%configure_first}</strong>
</div>
</j:if>
</j:jelly>
@@ -0,0 +1 @@
configure_first=You have to configure your Amazon S3 setting in the section "Amazon S3 Bucket Access settings"
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form">
<f:section title="${%Amazon S3 Bucket Access settings}">
<f:entry title="${%S3 Bucket Name}" field="container">
<f:textbox/>
</f:entry>
<f:entry title="${%S3 Bucket Region}" field="region">
<f:select/>
</f:entry>
<f:entry title="${%Base Prefix}" field="prefix">
<f:textbox/>
</f:entry>
<f:entry title="${%Delete Artifacts}" field="deleteArtifacts">
<f:checkbox readonly="true"/>
</f:entry>
<f:entry title="${%Delete Stashes}" field="deleteStashes">
<f:checkbox readonly="true"/>
</f:entry>
</f:section>
</j:jelly>
@@ -0,0 +1 @@
<div>Name of the S3 Bucket, the S3 Bucket should exists and the AWS account/profile/role used to access should have access to it</div>

0 comments on commit ab3412d

Please sign in to comment.