Skip to content

Commit

Permalink
Add support for namespace
Browse files Browse the repository at this point in the history
  • Loading branch information
maxlaverse committed Feb 24, 2019
1 parent 51e858a commit 8327fa6
Show file tree
Hide file tree
Showing 12 changed files with 170 additions and 36 deletions.
45 changes: 28 additions & 17 deletions README.md
Expand Up @@ -22,6 +22,32 @@ The following types of credentials are supported and can be used to authenticate

## Quick usage guide

The parameters have a slightly different effect depending if a plain KubeConfig file is provided or not.

### Parameters (without KubeConfig file)
| Name | Mandatory | Description |
| --------------- | --------- | ------------- |
| `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. |
| `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`) |

### Parameters (with KubeConfig file)

The plugin writes the plain KubeConfig file and doesn't change any other field if only `credentialsId` is provided.

| 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. |


### Pipeline usage
The `kubernetes-cli` plugin provides the function `withKubeConfig()` for Jenkins Pipeline support.
You can go to the *Snippet Generator* page under *Pipeline Syntax* section in Jenkins, select
Expand All @@ -37,22 +63,15 @@ node {
caCertificate: '<ca-certificate>',
serverUrl: '<api-server-address>',
contextName: '<context-name>',
clusterName: '<cluster-name>'
clusterName: '<cluster-name>',
namespace: '<namespace>'
]) {
sh 'kubectl get pods'
}
}
}
```

The arguments to the `withKubeConfig` step are:
* `credentialsId` - the Jenkins identifier of the credentials to authenticate against the cluster, or of a raw KubeConfig file.
* `caCertificate` (optional) - an certificate to check the Kubernetes api server's against. If you don't specify one, the CA verification will be skipped.
* `serverUrl` (optional with raw KubeConfig) - the url of the api server
* `contextName` (optional) - name of the context to create or to switch to if a raw kubeconfig was provided
* `clusterName` (optioanl) - name of the cluster to create or to switch to if a raw kubeconfig was provided


### From the web interface
1. Within the Jenkins dashboard, select a Job and then select Configure
2. Scroll down and click the "Add build step" dropdown
Expand All @@ -61,14 +80,6 @@ The arguments to the `withKubeConfig` step are:

![webui](img/webui.png)


Brief description of the named fields:
* **Credentials** - the Jenkins identifier of the credentials to use.
* **Kubernetes server endpoint** - the url of the api server
* **Context name** - name of the context to create or to switch to if a raw kubeconfig was provided
* **Cluster name** - name of the cluster to create or to switch to if a raw kubeconfig was provided
* **Certificate of certificate authority** - an optional certificate to check the Kubernetes api server's against

## Reporting an issue
Please file bug reports directly on the Jenkins [issue tracker][issue-tracker]

Expand Down
6 changes: 5 additions & 1 deletion pom.xml
Expand Up @@ -47,12 +47,16 @@
<jenkins-workflow-support.version>2.14</jenkins-workflow-support.version>
<jenkins-workflow-scm-step.version>2.4</jenkins-workflow-scm-step.version>
<jenkins-scm-api.version>2.0.7</jenkins-scm-api.version>
<jenkins-ssh-credentials.version>1.12</jenkins-ssh-credentials.version>
<jenkins-declarative.version>1.1.2</jenkins-declarative.version>
<jenkins-kubernetes-credentials.version>0.3.1</jenkins-kubernetes-credentials.version>

<!-- maven plugins versions -->
<maven-coveralls.version>4.3.0</maven-coveralls.version>
<maven-jacoco.version>0.8.1</maven-jacoco.version>

<!-- parent override -->
<surefire.rerunFailingTestsCount>0</surefire.rerunFailingTestsCount>
</properties>

<dependencies>
Expand Down Expand Up @@ -137,7 +141,7 @@
<dependency>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>ssh-credentials</artifactId>
<version>1.12</version>
<version>${jenkins-ssh-credentials.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
Expand Down
Expand Up @@ -51,7 +51,10 @@ public class KubectlBuildStep extends Step {

@DataBoundSetter
public String clusterName;


@DataBoundSetter
public String namespace;

@DataBoundConstructor
public KubectlBuildStep() {
}
Expand Down Expand Up @@ -82,6 +85,7 @@ public boolean start() throws Exception {
step.caCertificate,
step.clusterName,
step.contextName,
step.namespace,
getContext());

// Write config
Expand Down
Expand Up @@ -47,10 +47,13 @@ public class KubectlBuildWrapper extends SimpleBuildWrapper {

@DataBoundSetter
public String contextName;

@DataBoundSetter
public String clusterName;

@DataBoundSetter
public String namespace;

@DataBoundConstructor
public KubectlBuildWrapper() {
}
Expand All @@ -67,6 +70,7 @@ public void setUp(Context context, Run<?, ?> build,
this.caCertificate,
this.clusterName,
this.contextName,
this.namespace,
workspace,
launcher,
build);
Expand Down
Expand Up @@ -2,6 +2,7 @@

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

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collections;
Expand All @@ -24,7 +25,6 @@
import hudson.AbortException;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Launcher.ProcStarter;
import hudson.model.Run;
import hudson.util.QuotedStringTokenizer;
import hudson.util.Secret;
Expand All @@ -45,12 +45,13 @@ public class KubeConfigWriter {
private final String caCertificate;
private final String clusterName;
private final String contextName;
private final String namespace;
private final FilePath workspace;
private final Launcher launcher;
private final Run<?, ?> build;

public KubeConfigWriter(@Nonnull String serverUrl, @Nonnull String credentialsId,
String caCertificate, String clusterName, String contextName, FilePath workspace, Launcher launcher, Run<?, ?> build) {
String caCertificate, String clusterName, String contextName, String namespace, FilePath workspace, Launcher launcher, Run<?, ?> build) {
this.serverUrl = serverUrl;
this.credentialsId = credentialsId;
this.caCertificate = caCertificate;
Expand All @@ -59,6 +60,7 @@ public KubeConfigWriter(@Nonnull String serverUrl, @Nonnull String credentialsId
this.build = build;
this.clusterName = clusterName;
this.contextName = contextName;
this.namespace = namespace;
}

/**
Expand All @@ -81,22 +83,27 @@ public String writeKubeConfig() throws IOException, InterruptedException {
throw new AbortException("No credentials defined to setup Kubernetes CLI");
} else if (credentials instanceof FileCredentials) {
setRawKubeConfig(configFile, (FileCredentials) credentials);

if (wasContextProvided()) {
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 (this.wasClusterProvided()) {
setCluster(configFile.getRemote(), this.clusterName);
if (wasClusterProvided()) {
setCluster(configFile.getRemote(), this.clusterName);
}
} else {
setCluster(configFile.getRemote(), this.getClusterNameOrDefault());
setCluster(configFile.getRemote(), getClusterNameOrDefault());
setCredentials(configFile.getRemote(), credentials);
setContext(configFile.getRemote(), this.getContextNameOrDefault(), this.getClusterNameOrDefault());
useContext(configFile.getRemote(), this.getContextNameOrDefault());
setContext(configFile.getRemote(), getContextNameOrDefault(), getClusterNameOrDefault());
useContext(configFile.getRemote(), getContextNameOrDefault());
}

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


Expand Down Expand Up @@ -213,7 +220,6 @@ private void setCredentials(String configFile, StandardCredentials credentials)
* @throws InterruptedException on file operations
*/
private void setContext(String configFile, String contextName, String clusterName) throws IOException, InterruptedException {
// Add the context
int status = launcher.launch()
.envs(String.format("KUBECONFIG=%s", configFile))
.cmdAsSingleString(String.format("%s config set-context %s --cluster=%s --user=%s",
Expand All @@ -226,6 +232,46 @@ private void setContext(String configFile, String contextName, String clusterNam
if (status != 0) throw new IOException("Failed to add kubectl context (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 {
// 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))
.stdout(launcher.getListener())
.join();
if (status != 0) throw new IOException("Failed to set kubectl namespace (exit code " + status + ")");
}

/**
* Get the current context of the kube configuration file.
*
* @throws IOException on file operations
* @throws InterruptedException on file operations
*/
private String getCurrentContext(String configFile) throws IOException, InterruptedException {
ByteArrayOutputStream output = new ByteArrayOutputStream();
int status = launcher.launch()
.envs(String.format("KUBECONFIG=%s", configFile))
.cmdAsSingleString(String.format("%s config current-context",
KUBECTL_BINARY,
USERNAME))
.stdout(output)
.join();
if (status != 0) throw new IOException("Failed to get kubectl current-context (exit code " + status + ")");
return output.toString("UTF-8");
}

/**
* Set the current context of the kube configuration file.
*
Expand Down Expand Up @@ -304,6 +350,13 @@ private boolean wasServerUrlProvided() {
return this.serverUrl != null && !this.serverUrl.isEmpty();
}

/**
* Return whether or not a namespace was provided
*
* @return true if a namespace was provided to the plugin.
*/
private boolean wasNamespaceProvided() { return this.namespace != null && !this.namespace.isEmpty(); }

/**
* Returns a contextName
*
Expand Down
Expand Up @@ -13,15 +13,15 @@
*/
public abstract class KubeConfigWriterFactory {
public static KubeConfigWriter get(@Nonnull String serverUrl, @Nonnull String credentialsId,
String caCertificate, String clusterName, String contextName, FilePath workspace, Launcher launcher, Run<?, ?> build) {
return new KubeConfigWriter(serverUrl, credentialsId, caCertificate, clusterName, contextName, workspace, launcher, build);
String caCertificate, String clusterName, String contextName, String namespace, FilePath workspace, Launcher launcher, Run<?, ?> build) {
return new KubeConfigWriter(serverUrl, credentialsId, caCertificate, clusterName, contextName, namespace, workspace, launcher, build);
}

public static KubeConfigWriter get(@Nonnull String serverUrl, @Nonnull String credentialsId,
String caCertificate, String clusterName, String contextName, StepContext context) throws IOException, InterruptedException {
String caCertificate, String clusterName, String contextName, String namespace, StepContext context) throws IOException, InterruptedException {
Run<?, ?> run = context.get(Run.class);
FilePath workspace = context.get(FilePath.class);
Launcher launcher = context.get(Launcher.class);
return new KubeConfigWriter(serverUrl, credentialsId, caCertificate, clusterName, contextName, workspace, launcher, run);
return new KubeConfigWriter(serverUrl, credentialsId, caCertificate, clusterName, contextName,namespace, workspace, launcher, run);
}
}
Expand Up @@ -17,6 +17,10 @@
<f:textbox/>
</f:entry>

<f:entry field="namespace" title="${%Namespace}">
<f:textbox/>
</f:entry>

<f:entry title="${%Certificate of certificate authority (CA)}" field="caCertificate">
<f:textarea/>
</f:entry>
Expand Down
Expand Up @@ -17,6 +17,10 @@
<f:textbox/>
</f:entry>

<f:entry field="namespace" title="${%Namespace}">
<f:textbox/>
</f:entry>

<f:entry title="${%Certificate of certificate authority (CA)}" field="caCertificate">
<f:textarea/>
</f:entry>
Expand Down
@@ -0,0 +1,3 @@
<div>
Default namespace.
</div>
Expand Up @@ -85,6 +85,23 @@ public void testBasicWithoutCa() throws Exception {
assertThat(configDumpContent, containsString("server: " + SERVER_URL));
}

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

WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "testBasicWithNamespace");
p.setDefinition(new CpsFlowDefinition(loadResource("kubectlRawWithNamespace.groovy"), true));
WorkflowRun b = p.scheduleBuild2(0).waitForStart();
assertNotNull(b);
r.assertBuildStatusSuccess(r.waitForCompletion(b));

r.assertLogContains("kubectl configuration cleaned up", b);
FilePath configDump = r.jenkins.getWorkspaceFor(p).child("configDump");
assertTrue(configDump.exists());
String configDumpContent = configDump.readToString().trim();
assertThat(configDumpContent, containsString("namespace: test-ns"));
}

@Test
public void testUsernamePasswordCredentials() throws Exception {
CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), usernamePasswordCredentialWithSpace(CREDENTIAL_ID));
Expand Down Expand Up @@ -126,7 +143,7 @@ public void testFileCredentials() throws Exception {
CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), fileCredential(CREDENTIAL_ID));

WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "fileCredential");
p.setDefinition(new CpsFlowDefinition(loadResource("kubectlWithoutCa.groovy"), true));
p.setDefinition(new CpsFlowDefinition(loadResource("kubectlMinimal.groovy"), true));
WorkflowRun b = p.scheduleBuild2(0).waitForStart();
assertNotNull(b);
r.assertBuildStatusSuccess(r.waitForCompletion(b));
Expand Down Expand Up @@ -173,7 +190,7 @@ public void testFileCredentialsWithContext() throws Exception {
"preferences: {}\n" +
"users: []", configDumpContent);
}

@Test
public void testFileCredentialsWithCluster() throws Exception {
CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), fileCredential(CREDENTIAL_ID));
Expand All @@ -189,7 +206,23 @@ public void testFileCredentialsWithCluster() throws Exception {
String configDumpContent = configDump.readToString().trim();
assertThat(configDumpContent, containsString("name: " + CLUSTER_NAME));
}


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

WorkflowJob p = r.jenkins.createProject(WorkflowJob.class, "fileCredential");
p.setDefinition(new CpsFlowDefinition(loadResource("kubectlRawWithNamespace.groovy"), true));
WorkflowRun b = p.scheduleBuild2(0).waitForStart();
assertNotNull(b);
r.assertBuildStatusSuccess(r.waitForCompletion(b));

FilePath configDump = r.jenkins.getWorkspaceFor(p).child("configDump");
assertTrue(configDump.exists());
String configDumpContent = configDump.readToString().trim();
assertThat(configDumpContent, containsString("namespace: test-ns"));
}

@Test
public void testSecretCredentials() throws Exception {
CredentialsProvider.lookupStores(r.jenkins).iterator().next().addCredentials(Domain.global(), secretCredentialWithSpace(CREDENTIAL_ID));
Expand Down

0 comments on commit 8327fa6

Please sign in to comment.