Skip to content

Commit

Permalink
Merge pull request #32 from ArieShout/jep-200
Browse files Browse the repository at this point in the history
Transfrom slave exceptions to whitelist exceptions (JENKINS-50760)
  • Loading branch information
allxiao committed Apr 18, 2018
2 parents bd475dc + 6507bb3 commit 7bafafa
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 63 deletions.
Expand Up @@ -7,11 +7,11 @@
package com.microsoft.jenkins.kubernetes.command;

import com.google.common.annotations.VisibleForTesting;
import com.microsoft.jenkins.azurecommons.EnvironmentInjector;
import com.microsoft.jenkins.azurecommons.JobContext;
import com.microsoft.jenkins.azurecommons.command.CommandState;
import com.microsoft.jenkins.azurecommons.command.IBaseCommandData;
import com.microsoft.jenkins.azurecommons.command.ICommand;
import com.microsoft.jenkins.azurecommons.core.EnvironmentInjector;
import com.microsoft.jenkins.azurecommons.telemetry.AppInsightsUtils;
import com.microsoft.jenkins.kubernetes.KubernetesCDPlugin;
import com.microsoft.jenkins.kubernetes.KubernetesClientWrapper;
Expand All @@ -23,6 +23,7 @@
import hudson.FilePath;
import hudson.model.Item;
import hudson.model.TaskListener;
import hudson.remoting.ProxyException;
import hudson.util.VariableResolver;
import io.fabric8.kubernetes.client.KubernetesClient;
import jenkins.security.MasterToSlaveCallable;
Expand All @@ -46,66 +47,25 @@ public class DeploymentCommand implements ICommand<DeploymentCommand.IDeployment
@Override
public void execute(IDeploymentCommand context) {
JobContext jobContext = context.getJobContext();

// all the final variables below are serializable, which will be captured in the below MasterToSlaveCallable
// and execute on the slave if the job is scheduled on slave.
final TaskListener taskListener = jobContext.getTaskListener();
final String secretNameCfg = context.getSecretName();
final String secretNamespace = context.getSecretNamespace();
final String configPaths = context.getConfigs();
final FilePath workspace = jobContext.getWorkspace();
final String defaultSecretName = jobContext.getRun().getDisplayName();
final EnvVars envVars = context.getEnvVars();
final boolean enableSubstitution = context.isEnableConfigSubstitution();
final ClientWrapperFactory clientFactory = context.clientFactory(context.getJobContext().getRun().getParent());
FilePath workspace = jobContext.getWorkspace();
EnvVars envVars = context.getEnvVars();

TaskResult taskResult = null;
try {
final List<ResolvedDockerRegistryEndpoint> dockerRegistryEndpoints =
context.resolveEndpoints(jobContext.getRun().getParent());
taskResult = workspace.act(new MasterToSlaveCallable<TaskResult, Exception>() {
@Override
public TaskResult call() throws Exception {
TaskResult result = new TaskResult();

checkState(StringUtils.isNotBlank(secretNamespace), Messages.DeploymentCommand_blankNamespace());
checkState(StringUtils.isNotBlank(configPaths), Messages.DeploymentCommand_blankConfigFiles());

KubernetesClientWrapper wrapper =
clientFactory.buildClient(workspace).withLogger(taskListener.getLogger());
result.masterHost = getMasterHost(wrapper);

FilePath[] configFiles = workspace.list(configPaths);
if (configFiles.length == 0) {
String message = Messages.DeploymentCommand_noMatchingConfigFiles(configPaths);
taskListener.error(message);
result.commandState = CommandState.HasError;
throw new IllegalStateException(message);
}

if (!dockerRegistryEndpoints.isEmpty()) {
String secretName =
KubernetesClientWrapper.prepareSecretName(secretNameCfg, defaultSecretName, envVars);

wrapper.createOrReplaceSecrets(secretNamespace, secretName, dockerRegistryEndpoints);

taskListener.getLogger().println(Messages.DeploymentCommand_injectSecretName(
Constants.KUBERNETES_SECRET_NAME_PROP, secretName));
envVars.put(Constants.KUBERNETES_SECRET_NAME_PROP, secretName);
result.extraEnvVars.put(Constants.KUBERNETES_SECRET_NAME_PROP, secretName);
}
DeploymentTask task = new DeploymentTask();
task.setWorkspace(workspace);
task.setTaskListener(jobContext.getTaskListener());
task.setClientFactory(context.clientFactory(context.getJobContext().getRun().getParent()));
task.setEnvVars(envVars);
task.setConfigPaths(context.getConfigs());
task.setSecretNamespace(context.getSecretNamespace());
task.setSecretNameCfg(context.getSecretName());
task.setDefaultSecretNameSeed(jobContext.getRun().getDisplayName());
task.setEnableSubstitution(context.isEnableConfigSubstitution());
task.setDockerRegistryEndpoints(context.resolveEndpoints(jobContext.getRun().getParent()));

taskResult = workspace.act(task);

if (enableSubstitution) {
wrapper.withVariableResolver(new VariableResolver.ByMap<>(envVars));
}

wrapper.apply(configFiles);

result.commandState = CommandState.Success;

return result;
}
});
for (Map.Entry<String, String> entry : taskResult.extraEnvVars.entrySet()) {
EnvironmentInjector.inject(jobContext.getRun(), envVars, entry.getKey(), entry.getValue());
}
Expand All @@ -130,7 +90,7 @@ public TaskResult call() throws Exception {
}

@VisibleForTesting
String getMasterHost(KubernetesClientWrapper wrapper) {
static String getMasterHost(KubernetesClientWrapper wrapper) {
if (wrapper != null) {
KubernetesClient client = wrapper.getClient();
if (client != null) {
Expand All @@ -143,6 +103,117 @@ String getMasterHost(KubernetesClientWrapper wrapper) {
return "Unknown";
}

static class DeploymentTask extends MasterToSlaveCallable<TaskResult, ProxyException> {
private FilePath workspace;
private TaskListener taskListener;
private ClientWrapperFactory clientFactory;
private EnvVars envVars;

private String configPaths;
private String secretNamespace;
private String secretNameCfg;
private String defaultSecretNameSeed;
private boolean enableSubstitution;

private List<ResolvedDockerRegistryEndpoint> dockerRegistryEndpoints;

@Override
public TaskResult call() throws ProxyException {
try {
return doCall();
} catch (Exception ex) {
// JENKINS-50760
// JEP-200 restricts the classes allowed to be serialized with XStream to a whitelist.
// The task being executed in doCall may throw some exceptions from the third party libraries,
// which will cause SecurityException when it's transferred from the slave back to the master.
// We catch the exception and wrap the stack trace in a ProxyException which can
// be serialized properly.
throw new ProxyException(ex);
}
}

private TaskResult doCall() throws Exception {
TaskResult result = new TaskResult();

checkState(StringUtils.isNotBlank(secretNamespace), Messages.DeploymentCommand_blankNamespace());
checkState(StringUtils.isNotBlank(configPaths), Messages.DeploymentCommand_blankConfigFiles());

KubernetesClientWrapper wrapper =
clientFactory.buildClient(workspace).withLogger(taskListener.getLogger());
result.masterHost = getMasterHost(wrapper);

FilePath[] configFiles = workspace.list(configPaths);
if (configFiles.length == 0) {
String message = Messages.DeploymentCommand_noMatchingConfigFiles(configPaths);
taskListener.error(message);
result.commandState = CommandState.HasError;
throw new IllegalStateException(message);
}

if (!dockerRegistryEndpoints.isEmpty()) {
String secretName =
KubernetesClientWrapper.prepareSecretName(secretNameCfg, defaultSecretNameSeed, envVars);

wrapper.createOrReplaceSecrets(secretNamespace, secretName, dockerRegistryEndpoints);

taskListener.getLogger().println(Messages.DeploymentCommand_injectSecretName(
Constants.KUBERNETES_SECRET_NAME_PROP, secretName));
envVars.put(Constants.KUBERNETES_SECRET_NAME_PROP, secretName);
result.extraEnvVars.put(Constants.KUBERNETES_SECRET_NAME_PROP, secretName);
}

if (enableSubstitution) {
wrapper.withVariableResolver(new VariableResolver.ByMap<>(envVars));
}

wrapper.apply(configFiles);

result.commandState = CommandState.Success;

return result;
}

public void setWorkspace(FilePath workspace) {
this.workspace = workspace;
}

public void setTaskListener(TaskListener taskListener) {
this.taskListener = taskListener;
}

public void setClientFactory(ClientWrapperFactory clientFactory) {
this.clientFactory = clientFactory;
}

public void setEnvVars(EnvVars envVars) {
this.envVars = envVars;
}

public void setConfigPaths(String configPaths) {
this.configPaths = configPaths;
}

public void setSecretNamespace(String secretNamespace) {
this.secretNamespace = secretNamespace;
}

public void setSecretNameCfg(String secretNameCfg) {
this.secretNameCfg = secretNameCfg;
}

public void setDefaultSecretNameSeed(String defaultSecretNameSeed) {
this.defaultSecretNameSeed = defaultSecretNameSeed;
}

public void setEnableSubstitution(boolean enableSubstitution) {
this.enableSubstitution = enableSubstitution;
}

public void setDockerRegistryEndpoints(List<ResolvedDockerRegistryEndpoint> dockerRegistryEndpoints) {
this.dockerRegistryEndpoints = dockerRegistryEndpoints;
}
}

public static class TaskResult implements Serializable {
private static final long serialVersionUID = 1L;

Expand Down
Expand Up @@ -18,20 +18,19 @@ public class DeploymentCommandTest {

@Test
public void testGetMasterHost() {
DeploymentCommand command = new DeploymentCommand();
assertEquals(FALLBACK_MASTER, command.getMasterHost(null));
assertEquals(FALLBACK_MASTER, DeploymentCommand.getMasterHost(null));

KubernetesClientWrapper wrapper = mock(KubernetesClientWrapper.class);
assertEquals(FALLBACK_MASTER, command.getMasterHost(wrapper));
assertEquals(FALLBACK_MASTER, DeploymentCommand.getMasterHost(wrapper));

KubernetesClient client = mock(KubernetesClient.class);
when(wrapper.getClient()).thenReturn(client);
assertEquals(FALLBACK_MASTER, command.getMasterHost(wrapper));
assertEquals(FALLBACK_MASTER, DeploymentCommand.getMasterHost(wrapper));

URL url = mock(URL.class);
final String host = "some.host";
when(url.getHost()).thenReturn(host);
when(client.getMasterUrl()).thenReturn(url);
assertEquals(host, command.getMasterHost(wrapper));
assertEquals(host, DeploymentCommand.getMasterHost(wrapper));
}
}

0 comments on commit 7bafafa

Please sign in to comment.