Skip to content

Commit

Permalink
[JENKINS-40364] Implement the config-files extension point to enhance…
Browse files Browse the repository at this point in the history
… the npmrc user config file to handle NPM registries, scope and credentials. Builder generate the configuration file and delete it when finish.
  • Loading branch information
nfalco79 committed Jan 13, 2017
1 parent 148305e commit fe65981
Show file tree
Hide file tree
Showing 33 changed files with 1,179 additions and 17 deletions.
5 changes: 5 additions & 0 deletions pom.xml
Expand Up @@ -45,6 +45,11 @@
</properties>

<dependencies>
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>config-file-provider</artifactId>
<version>2.13</version>

This comment has been minimized.

Copy link
@imod

imod Jan 13, 2017

Member

this version of the config-file-provider is not a good base to start a new integration, you must use the newest one 2.15.4. A lot of the API got changed and no backward compatibility is provided...

This comment has been minimized.

Copy link
@nfalco79

nfalco79 Jan 14, 2017

Author Member

I start the develop on 2.15.1 (than 2.15.3) and as you suggest in the JIRA issue I copy/paste a simple the implementation (custom file) as start point.
When I run Jenkins config page explode for unknown reason in edit jelly page, so I decide to try on older 2.13. Copy/paste the example of custom file worked like a sharm.

Newest version have a lot of deprecated methods and no javadoc that explain which is the new API to use.
I think that when a lot of API is going to be changed and no method are provided to be back compatible maybe a more appropiate release could be 3.0 instead of 2.15, this typically protect users of this kind of changes. For example semantic version typically could update automatically till next major release excluded.

I have complete the work, but will do a tentative to switch on the new 2.15

This comment has been minimized.

Copy link
@imod

imod Jan 14, 2017

Member

I agree with the version number and that more javadoc is required

</dependency>
</dependencies>

<repositories>
Expand Down
32 changes: 30 additions & 2 deletions src/main/java/jenkins/plugins/nodejs/NodeJSBuildWrapper.java
@@ -1,9 +1,13 @@
package jenkins.plugins.nodejs;

import java.io.IOException;
import java.util.Collection;
import java.util.Collections;

import javax.annotation.Nonnull;

import org.jenkinsci.lib.configprovider.ConfigProvider;
import org.jenkinsci.lib.configprovider.model.Config;
import org.kohsuke.stapler.DataBoundConstructor;

import hudson.AbortException;
Expand All @@ -12,11 +16,14 @@
import hudson.FilePath;
import hudson.Launcher;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.Computer;
import hudson.model.Node;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.tasks.BuildWrapperDescriptor;
import jenkins.plugins.nodejs.configfiles.NPMConfig;
import jenkins.plugins.nodejs.tools.NodeJSInstallation;
import jenkins.tasks.SimpleBuildWrapper;

Expand Down Expand Up @@ -46,10 +53,12 @@ public String put(String key, String value) {
}

private final String nodeJSInstallationName;
private final String configId;

@DataBoundConstructor
public NodeJSBuildWrapper(String nodeJSInstallationName){
public NodeJSBuildWrapper(String nodeJSInstallationName, String configId) {
this.nodeJSInstallationName = Util.fixEmpty(nodeJSInstallationName);
this.configId = Util.fixEmpty(configId);
}

/**
Expand All @@ -65,6 +74,10 @@ public String getNodeJSInstallationName() {
return nodeJSInstallationName;
}

public String getConfigId() {
return configId;
}

/*
* (non-Javadoc)
* @see jenkins.tasks.SimpleBuildWrapper#setUp(jenkins.tasks.SimpleBuildWrapper.Context, hudson.model.Run, hudson.FilePath, hudson.Launcher, hudson.model.TaskListener, hudson.EnvVars)
Expand All @@ -76,13 +89,19 @@ public void setUp(final Context context, Run<?, ?> build, FilePath workspace, La
if (ni == null) {
throw new IOException(Messages.NodeJSCommandInterpreter_noInstallationFound(nodeJSInstallationName));
}
Node node = workspace.toComputer().getNode();
Computer computer = workspace.toComputer();
if (computer == null) {
throw new AbortException(Messages.NodeJSCommandInterpreter_nodeOffline());
}
Node node = computer.getNode();
if (node == null) {
throw new AbortException(Messages.NodeJSCommandInterpreter_nodeOffline());
}
ni = ni.forNode(node, listener);
ni = ni.forEnvironment(initialEnvironment);
ni.buildEnvVars(new EnvVarsAdapter(context));

NodeJSUtils.supplyConfig(configId, (AbstractBuild<?, ?>) build, listener);
}


Expand All @@ -103,6 +122,15 @@ public NodeJSInstallation[] getInstallations() {
return NodeJSUtils.getInstallations();
}

public Collection<Config> getConfigs() {
ConfigProvider provider = ConfigProvider.getByIdOrNull(NPMConfig.class.getName());
if (provider != null) {
return provider.getAllConfigs();
}

return Collections.emptyList();
}

}

}
37 changes: 33 additions & 4 deletions src/main/java/jenkins/plugins/nodejs/NodeJSCommandInterpreter.java
@@ -1,7 +1,11 @@
package jenkins.plugins.nodejs;

import java.io.IOException;
import java.util.Collection;
import java.util.Collections;

import org.jenkinsci.lib.configprovider.ConfigProvider;
import org.jenkinsci.lib.configprovider.model.Config;
import org.kohsuke.stapler.DataBoundConstructor;

import hudson.AbortException;
Expand All @@ -17,6 +21,7 @@
import hudson.tasks.Builder;
import hudson.tasks.CommandInterpreter;
import hudson.util.ArgumentListBuilder;
import jenkins.plugins.nodejs.configfiles.NPMConfig;
import jenkins.plugins.nodejs.tools.NodeJSInstallation;
import jenkins.plugins.nodejs.tools.Platform;

Expand All @@ -28,9 +33,8 @@
* @author Nikolas Falco
*/
public class NodeJSCommandInterpreter extends CommandInterpreter {
private static final String JAVASCRIPT_EXT = ".js";

private final String nodeJSInstallationName;
private final String configId;
private transient String nodeExec; // NOSONAR

/**
Expand All @@ -40,11 +44,14 @@ public class NodeJSCommandInterpreter extends CommandInterpreter {
* the NodeJS script
* @param nodeJSInstallationName
* the NodeJS label configured in Jenkins
* @param configId
* the provided Config id
*/
@DataBoundConstructor
public NodeJSCommandInterpreter(final String command, final String nodeJSInstallationName) {
public NodeJSCommandInterpreter(final String command, final String nodeJSInstallationName, final String configId) {
super(command);
this.nodeJSInstallationName = Util.fixEmpty(nodeJSInstallationName);
this.configId = Util.fixEmpty(configId);
}

/**
Expand Down Expand Up @@ -82,6 +89,9 @@ public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListen
throw new AbortException(Messages.NodeJSCommandInterpreter_noExecutableFound(ni.getHome()));
}
}

// add npmrc config
NodeJSUtils.supplyConfig(configId, build, listener);
} catch (AbortException e) {
listener.fatalError(e.getMessage()); // NOSONAR
return false;
Expand Down Expand Up @@ -110,13 +120,17 @@ protected String getContents() {

@Override
protected String getFileExtension() {
return JAVASCRIPT_EXT;
return NodeJSConstants.JAVASRIPT_EXT;
}

public String getNodeJSInstallationName() {
return nodeJSInstallationName;
}

public String getConfigId() {
return configId;
}

/**
* Provides builder details for the job configuration page.
*
Expand Down Expand Up @@ -149,6 +163,21 @@ public NodeJSInstallation[] getInstallations() {
return NodeJSUtils.getInstallations();
}

/**
* Gather all defined npmrc config files.
*
* @return a collection of user npmrc files or {@empty} if no one
* defined.
*/
public Collection<Config> getConfigs() {
ConfigProvider provider = ConfigProvider.getByIdOrNull(NPMConfig.class.getName());
if (provider != null) {
return provider.getAllConfigs();
}

return Collections.emptyList();
}

}

}
18 changes: 18 additions & 0 deletions src/main/java/jenkins/plugins/nodejs/NodeJSConstants.java
@@ -0,0 +1,18 @@
package jenkins.plugins.nodejs;

public final class NodeJSConstants {

/**
* Default extension for javascript file.
*/
public static final String JAVASRIPT_EXT = ".js";

/**
* The location of user-level configuration settings.
*/
public static final String NPM_USERCONFIG = "npm_config_userconfig";

private NodeJSConstants() {
// constructor
}
}
85 changes: 84 additions & 1 deletion src/main/java/jenkins/plugins/nodejs/NodeJSUtils.java
@@ -1,12 +1,32 @@
package jenkins.plugins.nodejs;

import hudson.AbortException;
import hudson.FilePath;
import hudson.Util;
import hudson.model.Environment;
import hudson.model.TaskListener;
import hudson.model.AbstractBuild;

import java.util.List;
import java.util.Map;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import jenkins.model.Jenkins;
import jenkins.plugins.nodejs.configfiles.NPMConfig;
import jenkins.plugins.nodejs.configfiles.NPMRegistry;
import jenkins.plugins.nodejs.configfiles.RegistryHelper;
import jenkins.plugins.nodejs.tools.NodeJSInstallation;
import jenkins.plugins.nodejs.tools.NodeJSInstallation.DescriptorImpl;

import org.apache.commons.lang.StringUtils;
import org.jenkinsci.lib.configprovider.model.Config;
import org.jenkinsci.plugins.configfiles.buildwrapper.ManagedFileUtil;
import org.jenkinsci.plugins.configfiles.common.CleanTempFilesAction;

import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;

/*package*/final class NodeJSUtils {

private NodeJSUtils() {
Expand All @@ -25,7 +45,7 @@ private NodeJSUtils() {
public static NodeJSInstallation getNodeJS(@Nullable String name) {
if (name != null) {
for (NodeJSInstallation installation : getInstallations()) {
if (name != null && name.equals(installation.getName()))
if (name.equals(installation.getName()))
return installation;
}
}
Expand All @@ -46,4 +66,67 @@ public static NodeJSInstallation[] getInstallations() {
return descriptor.getInstallations();
}

/**
* Create a copy of the given configuration in a no accessible folder for
* the user.
* <p>
* This file will be deleted at the end of job also in case of user
* interruption.
* </p>
*
* @param configId the configuration identification
* @param build a build being run
* @param listener a way to report progress
* @throws AbortException in case the provided configId is not valid
*/
public static FilePath supplyConfig(String configId, AbstractBuild<?, ?> build, TaskListener listener) throws AbortException {
if (StringUtils.isNotBlank(configId)) {
Config c = Config.getByIdOrNull(configId);

if (c == null) {
throw new AbortException("this NodeJS build is setup to use a config with id " + configId + " but can not be find");
} else {
NPMConfig config;
if (c instanceof NPMConfig) {
config = (NPMConfig) c;
} else {
config = new NPMConfig(c.id, c.name, c.comment, c.content, c.getProviderId(), null);
}

listener.getLogger().println("using user config with name " + config.name);
String fileContent = config.content;

listener.getLogger().println("Adding all registry entries");
List<NPMRegistry> registries = config.getRegistries();
RegistryHelper helper = new RegistryHelper(registries);
if (!registries.isEmpty()) {
Map<String, StandardUsernameCredentials> registry2Credentials = helper.resolveCredentials(build);
fileContent = helper.fillRegistry(fileContent, registry2Credentials);
}

try {
if (StringUtils.isNotBlank(fileContent)) { // NOSONAR
FilePath workDir = ManagedFileUtil.tempDir(build.getWorkspace());
final FilePath f = workDir.createTextTempFile(".npmrc", "", Util.replaceMacro(fileContent, build.getEnvironment(listener)), true);
listener.getLogger().printf("Create %s", f);

build.getEnvironments().add(new Environment() {
@Override
public void buildEnvVars(Map<String, String> env) {
env.put(NodeJSConstants.NPM_USERCONFIG, f.getRemote());
}
});

build.addAction(new CleanTempFilesAction(f.getRemote()));
return f;
}
} catch (Exception e) {
throw new IllegalStateException("the npmrc user config could not be supplied for the current build: " + e.getMessage(), e);
}
}
}

return null;
}

}

0 comments on commit fe65981

Please sign in to comment.