Skip to content

Commit 06398bf

Browse files
WIP: DockerServerCredentialsBinding
1 parent 9a91805 commit 06398bf

File tree

7 files changed

+270
-2
lines changed

7 files changed

+270
-2
lines changed

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@
7777
<classifier>tests</classifier>
7878
<scope>test</scope>
7979
</dependency>
80+
<dependency>
81+
<groupId>org.jenkins-ci.plugins</groupId>
82+
<artifactId>docker-commons</artifactId>
83+
<version>1.0</version>
84+
<optional>true</optional>
85+
</dependency>
8086
<dependency>
8187
<groupId>org.jenkins-ci.plugins.workflow</groupId>
8288
<artifactId>workflow-support</artifactId>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package org.jenkinsci.plugins.credentialsbinding.impl;
2+
3+
import java.io.IOException;
4+
import java.util.UUID;
5+
6+
import org.jenkinsci.plugins.credentialsbinding.Binding;
7+
import org.jenkinsci.plugins.credentialsbinding.BindingDescriptor;
8+
import org.jenkinsci.plugins.docker.commons.credentials.DockerServerCredentials;
9+
import org.kohsuke.stapler.DataBoundConstructor;
10+
11+
import hudson.Extension;
12+
import hudson.FilePath;
13+
import hudson.Launcher;
14+
import hudson.model.TaskListener;
15+
import hudson.model.Run;
16+
17+
import static org.jenkinsci.plugins.credentialsbinding.impl.TempDirUtils.*;
18+
19+
public class DockerServerCredentialsBinding extends Binding<DockerServerCredentials> {
20+
21+
@DataBoundConstructor
22+
public DockerServerCredentialsBinding(String variable, String credentialsId) {
23+
super(variable, credentialsId);
24+
}
25+
26+
@Override
27+
protected Class<DockerServerCredentials> type() {
28+
return DockerServerCredentials.class;
29+
}
30+
31+
@Override
32+
public SingleEnvironment bindSingle(Run<?, ?> build, FilePath workspace, Launcher launcher, TaskListener listener)
33+
throws IOException, InterruptedException {
34+
DockerServerCredentials credentials = getCredentials(build);
35+
FilePath secrets = secretsDir(workspace);
36+
String dirName = UUID.randomUUID().toString();
37+
final FilePath dir = secrets.child(dirName);
38+
dir.mkdirs();
39+
secrets.chmod(0700);
40+
dir.chmod(0700);
41+
42+
FilePath clientKey = dir.child("key.pem");
43+
clientKey.write(credentials.getClientKey(), null);
44+
clientKey.chmod(0600);
45+
46+
FilePath clientCert = dir.child("cert.pem");
47+
clientCert.write(credentials.getClientCertificate(), null);
48+
clientCert.chmod(0600);
49+
50+
FilePath serverCACert = dir.child("ca.pem");
51+
serverCACert.write(credentials.getServerCaCertificate(), null);
52+
serverCACert.chmod(0600);
53+
54+
return new SingleEnvironment(dir.getRemote(), new UnbinderImpl(dirName));
55+
}
56+
57+
@Extension(optional = true)
58+
public static class DescriptorImpl extends BindingDescriptor<DockerServerCredentials> {
59+
60+
@Override
61+
protected Class<DockerServerCredentials> type() {
62+
return DockerServerCredentials.class;
63+
}
64+
65+
@Override
66+
public String getDisplayName() {
67+
return Messages.DockerServerCredentialsBinding_docker_client_certificate();
68+
}
69+
70+
}
71+
72+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
The MIT License
4+
5+
Copyright 2013 jglick.
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a copy
8+
of this software and associated documentation files (the "Software"), to deal
9+
in the Software without restriction, including without limitation the rights
10+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11+
copies of the Software, and to permit persons to whom the Software is
12+
furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in
15+
all copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23+
THE SOFTWARE.
24+
-->
25+
<?jelly escape-by-default='true'?>
26+
<j:jelly xmlns:j="jelly:core" xmlns:f="/lib/form" xmlns:st="jelly:stapler" xmlns:c="/lib/credentials">
27+
<f:entry title="${%Variable}" field="variable">
28+
<f:textbox default="DOCKER_CERT_PATH"/>
29+
</f:entry>
30+
</j:jelly>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<div>
2+
Name of an environment variable to be set during the build.<br>
3+
Its value will be the absolute path of the directory where the <code>{ca,cert,key}.pem</code> files will be created.<br>
4+
You probably want to call this variable <code>DOCKER_CERT_PATH</code>, which will be undestood by the docker client binary.<br>
5+
</div>

src/main/resources/org/jenkinsci/plugins/credentialsbinding/impl/Messages.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ StringBinding.secret_text=Secret text
44
UsernamePasswordBinding.username_and_password=Username and password (conjoined)
55
UsernamePasswordMultiBinding.username_and_password=Username and password (separated)
66
ZipFileBinding.secret_zip_file=Secret ZIP file
7+
DockerServerCredentialsBinding.docker_client_certificate=Docker client certificate

src/test/java/org/jenkinsci/plugins/credentialsbinding/impl/BindingStepTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,12 +310,12 @@ public void testGlobalBindingWithAuthorization() {
310310
});
311311
}
312312

313-
private static Set<String> grep(File dir, String text) throws IOException {
313+
/* package */static Set<String> grep(File dir, String text) throws IOException {
314314
Set<String> matches = new TreeSet<String>();
315315
grep(dir, text, "", matches);
316316
return matches;
317317
}
318-
private static void grep(File dir, String text, String prefix, Set<String> matches) throws IOException {
318+
/* package */static void grep(File dir, String text, String prefix, Set<String> matches) throws IOException {
319319
File[] kids = dir.listFiles();
320320
if (kids == null) {
321321
return;
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* The MIT License
3+
*
4+
* Copyright 2015 Jesse Glick.
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22+
* THE SOFTWARE.
23+
*/
24+
25+
package org.jenkinsci.plugins.credentialsbinding.impl;
26+
27+
import java.util.Collections;
28+
29+
import org.jenkinsci.plugins.credentialsbinding.MultiBinding;
30+
import org.jenkinsci.plugins.docker.commons.credentials.DockerServerCredentials;
31+
import org.jenkinsci.plugins.docker.commons.credentials.DockerServerDomainSpecification;
32+
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
33+
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
34+
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
35+
import org.jenkinsci.plugins.workflow.steps.StepConfigTester;
36+
import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep;
37+
import org.junit.Rule;
38+
import org.junit.Test;
39+
import org.junit.runners.model.Statement;
40+
import org.jvnet.hudson.test.RestartableJenkinsRule;
41+
42+
import com.cloudbees.plugins.credentials.CredentialsProvider;
43+
import com.cloudbees.plugins.credentials.CredentialsScope;
44+
import com.cloudbees.plugins.credentials.CredentialsStore;
45+
import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
46+
import com.cloudbees.plugins.credentials.domains.Domain;
47+
import com.cloudbees.plugins.credentials.domains.DomainSpecification;
48+
49+
import hudson.FilePath;
50+
51+
import static org.hamcrest.Matchers.instanceOf;
52+
import static org.jenkinsci.plugins.credentialsbinding.impl.BindingStepTest.grep;
53+
import static org.junit.Assert.*;
54+
55+
public class DockerServerCredentialsBindingTest {
56+
57+
@Rule
58+
public RestartableJenkinsRule story = new RestartableJenkinsRule();
59+
60+
@Test
61+
public void configRoundTrip() throws Exception {
62+
story.addStep(new Statement() {
63+
@SuppressWarnings("rawtypes")
64+
@Override
65+
public void evaluate() throws Throwable {
66+
CredentialsStore store = CredentialsProvider.lookupStores(story.j.getInstance()).iterator().next();
67+
assertThat(store, instanceOf(SystemCredentialsProvider.StoreImpl.class));
68+
Domain domain = new Domain("docker", "A domain for docker credentials",
69+
Collections.<DomainSpecification> singletonList(new DockerServerDomainSpecification()));
70+
DockerServerCredentials c = new DockerServerCredentials(CredentialsScope.GLOBAL,
71+
"docker-client-cert", "desc", "clientKey", "clientCertificate", "serverCaCertificate");
72+
store.addDomain(domain, c);
73+
BindingStep s = new StepConfigTester(story.j)
74+
.configRoundTrip(new BindingStep(Collections.<MultiBinding> singletonList(
75+
new DockerServerCredentialsBinding("DOCKER_CERT_PATH", "docker-client-cert"))));
76+
story.j.assertEqualDataBoundBeans(s.getBindings(), Collections.singletonList(
77+
new DockerServerCredentialsBinding("DOCKER_CERT_PATH", "docker-client-cert")));
78+
}
79+
});
80+
}
81+
82+
@Test
83+
public void basics() throws Exception {
84+
story.addStep(new Statement() {
85+
@Override
86+
public void evaluate() throws Throwable {
87+
DockerServerCredentials c = new DockerServerCredentials(CredentialsScope.GLOBAL,
88+
"docker-client-cert", "desc", "clientKey", "clientCertificate", "serverCaCertificate");
89+
CredentialsProvider.lookupStores(story.j.jenkins).iterator().next().addCredentials(Domain.global(), c);
90+
WorkflowJob p = story.j.jenkins.createProject(WorkflowJob.class, "p");
91+
p.setDefinition(new CpsFlowDefinition(""
92+
+ "node {\n"
93+
+ " withCredentials([[$class: 'DockerServerCredentialsBinding',\n"
94+
+ " variable: 'DOCKER_CERT_PATH',\n"
95+
+ " credentialsId: 'docker-client-cert']]) {\n"
96+
+ " semaphore 'basics'\n"
97+
+ "\n"
98+
+ " sh '''\n"
99+
+ " set -e -x\n"
100+
+ " # check permissions on the credentials dir and its parent\n"
101+
+ " [ $(stat -c %a \"$DOCKER_CERT_PATH\") = 700 ]\n"
102+
+ " [ $(stat -c %a \"$DOCKER_CERT_PATH\"/..) = 700 ]\n"
103+
+ "\n"
104+
+ " # check permissions and content of the certificate files\n"
105+
+ " [ $(stat -c %a \"$DOCKER_CERT_PATH/key.pem\") = 600 ]\n"
106+
+ " [ $(stat -c %a \"$DOCKER_CERT_PATH/cert.pem\") = 600 ]\n"
107+
+ " [ $(stat -c %a \"$DOCKER_CERT_PATH/ca.pem\") = 600 ]\n"
108+
+ " [ $(stat -c %s \"$DOCKER_CERT_PATH/key.pem\") = 9 ]\n"
109+
+ " [ $(stat -c %s \"$DOCKER_CERT_PATH/cert.pem\") = 17 ]\n"
110+
+ " [ $(stat -c %s \"$DOCKER_CERT_PATH/ca.pem\") = 19 ]\n"
111+
+ "\n"
112+
+ " # keep location of the certificate dir for the next step\n"
113+
+ " echo \"$DOCKER_CERT_PATH\" > cert-path"
114+
+ " '''\n"
115+
+ " }\n"
116+
+ "\n"
117+
+ " sh '''\n"
118+
+ " set -e +x\n"
119+
+ " # make sure the credentials dir have been deleted\n"
120+
+ " cert_path=$(cat cert-path)\n"
121+
+ " if [ -e \"$cert_path\" ] ; then\n"
122+
+ " echo \"$cert_path still exists!!!\" >&2\n"
123+
+ " exit 1\n"
124+
+ " fi\n"
125+
+ " '''\n"
126+
+ "}", true));
127+
WorkflowRun b = p.scheduleBuild2(0).waitForStart();
128+
SemaphoreStep.waitForStart("basics/1", b);
129+
}
130+
});
131+
story.addStep(new Statement() {
132+
@Override
133+
public void evaluate() throws Throwable {
134+
WorkflowJob p = story.j.jenkins.getItemByFullName("p", WorkflowJob.class);
135+
assertNotNull(p);
136+
WorkflowRun b = p.getBuildByNumber(1);
137+
assertNotNull(b);
138+
SemaphoreStep.success("basics/1", null);
139+
while (b.isBuilding()) { // TODO 1.607+ use waitForCompletion
140+
Thread.sleep(100);
141+
}
142+
story.j.assertBuildStatusSuccess(b);
143+
FilePath certPathFile = story.j.jenkins.getWorkspaceFor(p).child("cert-path");
144+
assertTrue(certPathFile.exists());
145+
String certPath = certPathFile.readToString().trim();
146+
// expected .../workspace/p@tmp/secretFiles/<36-chars-UUID>
147+
assertTrue(certPath.matches(".*/workspace/p@tmp/secretFiles/[-a-f0-9]{36}"));
148+
// this path is a secret, it shouldn't appear in the logs (although it doesn't really matter)
149+
assertEquals(Collections.<String> emptySet(), grep(b.getRootDir(), certPath));
150+
}
151+
});
152+
}
153+
154+
}

0 commit comments

Comments
 (0)