Skip to content

Commit

Permalink
Merge pull request #25 from jenkinsci/fix_plain_kubeconfig_setup
Browse files Browse the repository at this point in the history
Fix plain kubeconfig setup
  • Loading branch information
maxlaverse committed Feb 24, 2019
2 parents 389027d + 881a14f commit 596c04f
Show file tree
Hide file tree
Showing 18 changed files with 299 additions and 161 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ The parameters have a slightly different effect depending if a plain KubeConfig
| --------------- | --------- | ------------- |
| `credentialsId` | yes | The Jenkins ID of the credentials. |
| `serverUrl` | yes | URL of the API server's. |
| `caCertificate` | no | Cluster Certificate Authority used to validate the API server's certificate. Validation skipped if the parameter is not provided. |
| `caCertificate` | no | Cluster Certificate Authority used to validate the API server's certificate. The validation is skipped if the parameter is not provided. |
| `clusterName` | no | Name of the generated Cluster configuration. (default: `k8s`) |
| `namespace` | no | Namespace for the Context. |
| `contextName` | no | Name of the generated Context configuration. (default: `k8s`) |
Expand All @@ -41,11 +41,11 @@ The plugin writes the plain KubeConfig file and doesn't change any other field i
| Name | Mandatory | Description |
| --------------- | --------- | ------------- |
| `credentialsId` | yes | The Jenkins ID of the plain KubeConfig file. |
| `serverUrl` | no | URL of the API server's. |
| `caCertificate` | no | Cluster Certificate Authority used to validate the API server's certificate. Validation skipped if the parameter is not provided. |
| `clusterName` | no | Name of the generated Cluster configuration if a `clusterName` was provided. |
| `namespace` | no | Namespace for the Context. |
| `contextName` | no | Name of the Context to use. |
| `serverUrl` | no | URL of the API server's. This will create a new `cluster` block and modify the current Context to use it. |
| `caCertificate` | no | Cluster Certificate Authority used to validate the API server's certificate if a `serverUrl` was provided. The validation is skipped if the parameter is not provided. |
| `clusterName` | no | Modifies the Cluster of the current Context. Also used for the generated `cluster` block if a `serverUrl` was provided. |
| `namespace` | no | Modifies the Namespace of the current Context. |
| `contextName` | no | Switch the current Context to this name. The Context must already exist in the KubeConfig file. |


### Pipeline usage
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,30 @@
package org.jenkinsci.plugins.kubernetes.cli.kubeconfig;

import static com.google.common.collect.Sets.newHashSet;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collections;
import java.util.Set;

import javax.annotation.Nonnull;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.plugins.kubernetes.credentials.TokenProducer;
import org.jenkinsci.plugins.plaincredentials.FileCredentials;
import org.jenkinsci.plugins.plaincredentials.StringCredentials;

import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;

import hudson.AbortException;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.Run;
import hudson.util.QuotedStringTokenizer;
import hudson.util.Secret;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.plugins.kubernetes.credentials.TokenProducer;
import org.jenkinsci.plugins.plaincredentials.FileCredentials;
import org.jenkinsci.plugins.plaincredentials.StringCredentials;

import javax.annotation.Nonnull;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collections;
import java.util.Set;

import static com.google.common.collect.Sets.newHashSet;

/**
* @author Max Laverse
Expand Down Expand Up @@ -88,25 +85,30 @@ public String writeKubeConfig() throws IOException, InterruptedException {
useContext(configFile.getRemote(), this.contextName);
}

if (this.wasServerUrlProvided()) {
launcher.getListener().getLogger().println("the serverUrl will be ignored as a raw kubeconfig file was provided");
if (wasServerUrlProvided()) {
setCluster(configFile.getRemote());
}

if (wasClusterProvided()) {
setCluster(configFile.getRemote(), this.clusterName);
setContextCluster(configFile.getRemote(), this.clusterName);
} else if (wasServerUrlProvided()) {
setContextCluster(configFile.getRemote(), getClusterNameOrDefault());
}

if (wasNamespaceProvided()) {
setContextNamespace(configFile.getRemote(), namespace);
}
} else {
setCluster(configFile.getRemote(), getClusterNameOrDefault());
setCluster(configFile.getRemote());
setCredentials(configFile.getRemote(), credentials);
setContext(configFile.getRemote(), getContextNameOrDefault(), getClusterNameOrDefault());
if (wasNamespaceProvided()) {
setFullContext(configFile.getRemote(), namespace);
} else {
setFullContext(configFile.getRemote());
}
useContext(configFile.getRemote(), getContextNameOrDefault());
}

if (wasNamespaceProvided()){
setNamespace(configFile.getRemote(),namespace);
}


return configFile.getRemote();
}

Expand All @@ -130,7 +132,7 @@ private void setRawKubeConfig(FilePath configFile, FileCredentials credentials)
* @throws IOException on file operations
* @throws InterruptedException on file operations
*/
private void setCluster(String configFile, String clusterName) throws IOException, InterruptedException {
private void setCluster(String configFile) throws IOException, InterruptedException {
String tlsConfigArgs;
Set<String> filesToBeRemoved = newHashSet();

Expand All @@ -150,7 +152,7 @@ private void setCluster(String configFile, String clusterName) throws IOExceptio
.envs(String.format("KUBECONFIG=%s", configFile))
.cmdAsSingleString(String.format("%s config set-cluster %s --server=%s %s",
KUBECTL_BINARY,
clusterName,
getClusterNameOrDefault(),
serverUrl,
tlsConfigArgs))
.stdout(launcher.getListener())
Expand Down Expand Up @@ -219,38 +221,72 @@ private void setCredentials(String configFile, StandardCredentials credentials)
* @throws IOException on file operations
* @throws InterruptedException on file operations
*/
private void setContext(String configFile, String contextName, String clusterName) throws IOException, InterruptedException {
private void setFullContext(String configFile) throws IOException, InterruptedException {
int status = launcher.launch()
.envs(String.format("KUBECONFIG=%s", configFile))
.cmdAsSingleString(String.format("%s config set-context %s --cluster=%s --user=%s",
KUBECTL_BINARY,
contextName,
clusterName,
getContextNameOrDefault(),
getClusterNameOrDefault(),
USERNAME))
.stdout(launcher.getListener())
.join();
if (status != 0) throw new IOException("Failed to add kubectl context (exit code " + status + ")");
}

private void setFullContext(String configFile, String namespace) throws IOException, InterruptedException {
int status = launcher.launch()
.envs(String.format("KUBECONFIG=%s", configFile))
.cmdAsSingleString(String.format("%s config set-context %s --cluster=%s --user=%s --namespace=%s",
KUBECTL_BINARY,
getContextNameOrDefault(),
getClusterNameOrDefault(),
USERNAME,
namespace))
.stdout(launcher.getListener())
.join();
if (status != 0)
throw new IOException("Failed to add kubectl context with namespace (exit code " + status + ")");
}

/**
* Set the namespace of the context section in the kube configuration file.
*
* @throws IOException on file operations
* @throws InterruptedException on file operations
*/
private void setNamespace(String configFile, String namespace) throws IOException, InterruptedException {
private void setContextNamespace(String configFile, String namespace) throws IOException, InterruptedException {
// Starting kubectl 1.12, we can use --current instead of having to determine the context we are in.
// To be done once we drop support for <1.12
int status = launcher.launch()
.envs(String.format("KUBECONFIG=%s", configFile))
.cmdAsSingleString(String.format("%s config set-context %s --namespace=%s",
KUBECTL_BINARY,
getCurrentContext(configFile),
namespace,
USERNAME))
namespace))
.stdout(launcher.getListener())
.join();
if (status != 0) throw new IOException("Failed to set kubectl namespace (exit code " + status + ")");
if (status != 0) throw new IOException("Failed to set kubectl context namespace (exit code " + status + ")");
}

/**
* Set the cluster of the context section in the kube configuration file.
*
* @throws IOException on file operations
* @throws InterruptedException on file operations
*/
private void setContextCluster(String configFile, String clusterName) throws IOException, InterruptedException {
// Starting kubectl 1.12, we can use --current instead of having to determine the context we are in.
// To be done once we drop support for <1.12
int status = launcher.launch()
.envs(String.format("KUBECONFIG=%s", configFile))
.cmdAsSingleString(String.format("%s config set-context %s --cluster=%s",
KUBECTL_BINARY,
getCurrentContext(configFile),
clusterName))
.stdout(launcher.getListener())
.join();
if (status != 0) throw new IOException("Failed to set kubectl context cluster (exit code " + status + ")");
}

/**
Expand Down Expand Up @@ -331,7 +367,7 @@ private StandardCredentials getCredentials(Run<?, ?> build) throws AbortExceptio
private boolean wasContextProvided() {
return this.contextName != null && !this.contextName.isEmpty();
}

/**
* Return whether or not a clusterName was provided
*
Expand All @@ -355,7 +391,9 @@ private boolean wasServerUrlProvided() {
*
* @return true if a namespace was provided to the plugin.
*/
private boolean wasNamespaceProvided() { return this.namespace != null && !this.namespace.isEmpty(); }
private boolean wasNamespaceProvided() {
return this.namespace != null && !this.namespace.isEmpty();
}

/**
* Returns a contextName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public void testScopedCredentials() throws Exception {
CredentialsProvider.lookupStores(folder).iterator().next().addCredentials(Domain.global(), usernamePasswordCredential(CREDENTIAL_ID));

WorkflowJob p = folder.createProject(WorkflowJob.class, "testScopedCredentials");
p.setDefinition(new CpsFlowDefinition(loadResource("mockedKubectl.groovy"), true));
p.setDefinition(new CpsFlowDefinition(loadResource("kubectlMocked.groovy"), true));
WorkflowRun b = p.scheduleBuild2(0).waitForStart();
assertNotNull(b);
waitForResult(b, Result.SUCCESS);
Expand All @@ -49,7 +49,7 @@ public void testMissingScopedCredentials() throws Exception {
CredentialsProvider.lookupStores(folder).iterator().next().addCredentials(Domain.global(), usernamePasswordCredential(CREDENTIAL_ID));

WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "testMissingScopedCredentials");
p.setDefinition(new CpsFlowDefinition(loadResource("mockedKubectl.groovy"), true));
p.setDefinition(new CpsFlowDefinition(loadResource("kubectlMocked.groovy"), true));
WorkflowRun b = p.scheduleBuild2(0).waitForStart();
assertNotNull(b);
waitForResult(b, Result.FAILURE);
Expand All @@ -61,7 +61,7 @@ public void testSecretWithSpace() throws Exception {
CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), secretCredentialWithSpace(CREDENTIAL_ID));

WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "testSecretWithSpace");
p.setDefinition(new CpsFlowDefinition(loadResource("mockedKubectl.groovy"), true));
p.setDefinition(new CpsFlowDefinition(loadResource("kubectlMocked.groovy"), true));
WorkflowRun b = p.scheduleBuild2(0).waitForStart();
assertNotNull(b);
waitForResult(b, Result.SUCCESS);
Expand All @@ -74,7 +74,7 @@ public void testUsernamePasswordWithSpace() throws Exception {
CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), usernamePasswordCredentialWithSpace(CREDENTIAL_ID));

WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "testUsernamePasswordWithSpace");
p.setDefinition(new CpsFlowDefinition(loadResource("mockedKubectl.groovy"), true));
p.setDefinition(new CpsFlowDefinition(loadResource("kubectlMocked.groovy"), true));
WorkflowRun b = p.scheduleBuild2(0).waitForStart();
assertNotNull(b);
waitForResult(b, Result.SUCCESS);
Expand All @@ -87,7 +87,7 @@ public void testKubeConfigDisposed() throws Exception {
CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), usernamePasswordCredentialWithSpace(CREDENTIAL_ID));

WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "testCleanupOnFailure");
p.setDefinition(new CpsFlowDefinition(loadResource("mockedKubectlFailure.groovy"), true));
p.setDefinition(new CpsFlowDefinition(loadResource("kubectlMockedFailure.groovy"), true));
WorkflowRun b = p.scheduleBuild2(0).waitForStart();
assertNotNull(b);
waitForResult(b, Result.FAILURE);
Expand All @@ -97,7 +97,7 @@ public void testKubeConfigDisposed() throws Exception {
@Test
public void testCredentialNotProvided() throws Exception {
WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "testWithEmptyCredentials");
p.setDefinition(new CpsFlowDefinition(loadResource("mockedKubectlWithEmptyCredential.groovy"), true));
p.setDefinition(new CpsFlowDefinition(loadResource("kubectlMockedWithEmptyCredential.groovy"), true));
WorkflowRun b = p.scheduleBuild2(0).waitForStart();
assertNotNull(b);
waitForResult(b, Result.FAILURE);
Expand All @@ -109,7 +109,7 @@ public void testUnsupportedCredential() throws Exception {
CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), unsupportedCredential(CREDENTIAL_ID));

WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "testWithUnsupportedCredentials");
p.setDefinition(new CpsFlowDefinition(loadResource("mockedKubectl.groovy"), true));
p.setDefinition(new CpsFlowDefinition(loadResource("kubectlMocked.groovy"), true));
WorkflowRun b = p.scheduleBuild2(0).waitForStart();
assertNotNull(b);
waitForResult(b, Result.FAILURE);
Expand Down Expand Up @@ -137,23 +137,35 @@ public void testInvalidCertificate() throws Exception {
CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), brokenCertificateCredential(CREDENTIAL_ID));

WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "testWithBrokenCertificate");
p.setDefinition(new CpsFlowDefinition(loadResource("mockedKubectl.groovy"), true));
p.setDefinition(new CpsFlowDefinition(loadResource("kubectlMocked.groovy"), true));
WorkflowRun b = p.scheduleBuild2(0).waitForStart();
assertNotNull(b);
waitForResult(b, Result.FAILURE);
r.assertLogContains("ERROR: Uninitialized keystore", b);
}

@Test
public void testServerProvidedWithFileCredential() throws Exception {
public void testPlainKubeConfig() throws Exception {
CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), fileCredential(CREDENTIAL_ID));

WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "testWithFileCertificateAndClusterName");
p.setDefinition(new CpsFlowDefinition(loadResource("kubectlMockedWithCluster.groovy"), true));
WorkflowRun b = p.scheduleBuild2(0).waitForStart();
assertNotNull(b);
waitForResult(b, Result.SUCCESS);
r.assertLogNotContains("kubectl, config, set-cluster", b);
}

@Test
public void testPlainKubeConfigWithServerUrl() throws Exception {
CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), fileCredential(CREDENTIAL_ID));

WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "testWithFileCertificateAndServer");
p.setDefinition(new CpsFlowDefinition(loadResource("mockedKubectl.groovy"), true));
p.setDefinition(new CpsFlowDefinition(loadResource("kubectlMocked.groovy"), true));
WorkflowRun b = p.scheduleBuild2(0).waitForStart();
assertNotNull(b);
waitForResult(b, Result.SUCCESS);
r.assertLogContains("the serverUrl will be ignored as a raw kubeconfig file was provided", b);
r.assertLogContains("kubectl, config, set-cluster", b);
}

private void waitForResult(WorkflowRun b, Result result) throws Exception {
Expand Down
Loading

0 comments on commit 596c04f

Please sign in to comment.