package org.csanchez.jenkins.plugins.kubernetes;
import com.cloudbees.plugins.credentials.CredentialsMatchers;
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.StandardListBoxModel;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import hudson.AbortException;
import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.AbstractProject;
import hudson.model.Item;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.security.ACL;
import hudson.tasks.BuildWrapperDescriptor;
import hudson.util.ListBoxModel;
import hudson.util.Secret;
import jenkins.model.Jenkins;
import jenkins.tasks.SimpleBuildWrapper;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.Set;
import static com.google.common.collect.Sets.newHashSet;
/**
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
*/
public class KubectlBuildWrapper extends SimpleBuildWrapper {
private static final String BEGIN_CERTIFICATE = "-----BEGIN CERTIFICATE-----";
private static final String END_CERTIFICATE = "-----END CERTIFICATE-----";
private static final String BEGIN_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----";
private static final String END_PRIVATE_KEY = "-----END PRIVATE KEY-----";
private final String serverUrl;
private final String credentialsId;
private final String caCertificate;
@DataBoundConstructor
public KubectlBuildWrapper(@Nonnull String serverUrl, @Nonnull String credentialsId,
@Nonnull String caCertificate) {
this.serverUrl = serverUrl;
this.credentialsId = credentialsId;
this.caCertificate = caCertificate;
}
public String getServerUrl() {
return serverUrl;
}
public String getCredentialsId() {
return credentialsId;
}
public String getCaCertificate() {
return caCertificate;
}
@Override
public void setUp(Context context, Run<?, ?> build, FilePath workspace, Launcher launcher, TaskListener listener, EnvVars initialEnvironment) throws IOException, InterruptedException {
FilePath configFile = workspace.createTempFile(".kube", "config");
Set<String> tempFiles = newHashSet(configFile.getRemote());
String tlsConfig;
if (caCertificate != null && !caCertificate.isEmpty()) {
FilePath caCrtFile = workspace.createTempFile("cert-auth", "crt");
String ca = caCertificate;
if (!ca.startsWith(BEGIN_CERTIFICATE)) {
ca = wrapWithMarker(BEGIN_CERTIFICATE, END_CERTIFICATE, ca);
}
caCrtFile.write(ca, null);
tempFiles.add(caCrtFile.getRemote());
tlsConfig = " --certificate-authority=" + caCrtFile.getRemote();
} else {
tlsConfig = " --insecure-skip-tls-verify=true";
}
int status = launcher.launch()
.cmdAsSingleString("kubectl config --kubeconfig=" + configFile.getRemote()
+ " set-cluster k8s --server=" + serverUrl + tlsConfig)
.join();
if (status != 0) throw new IOException("Failed to run kubectl config "+status);
final StandardCredentials c = getCredentials();
String login;
if (c == null) {
throw new AbortException("No credentials defined to setup Kubernetes CLI");
} else if (c instanceof TokenProducer) {
login = "--token=" + ((TokenProducer) c).getToken(serverUrl, null, true);
} else if (c instanceof UsernamePasswordCredentials) {
UsernamePasswordCredentials upc = (UsernamePasswordCredentials) c;
login = "--username=" + upc.getUsername() + " --password=" + Secret.toString(upc.getPassword());
} else if (c instanceof StandardCertificateCredentials) {
StandardCertificateCredentials scc = (StandardCertificateCredentials) c;
KeyStore keyStore = scc.getKeyStore();
String alias;
try {
alias = keyStore.aliases().nextElement();
X509Certificate certificate = (X509Certificate) keyStore.getCertificate(alias);
Key key = keyStore.getKey(alias, Secret.toString(scc.getPassword()).toCharArray());
FilePath clientCrtFile = workspace.createTempFile("client", "crt");
FilePath clientKeyFile = workspace.createTempFile("client", "key");
String encodedClientCrt = wrapWithMarker(BEGIN_CERTIFICATE, END_CERTIFICATE,
Base64.encodeBase64String(certificate.getEncoded()));
String encodedClientKey = wrapWithMarker(BEGIN_PRIVATE_KEY, END_PRIVATE_KEY,
Base64.encodeBase64String(key.getEncoded()));
clientCrtFile.write(encodedClientCrt, null);
clientKeyFile.write(encodedClientKey, null);
tempFiles.add(clientCrtFile.getRemote());
tempFiles.add(clientKeyFile.getRemote());
login = "--client-certificate=" + clientCrtFile.getRemote() + " --client-key="
+ clientKeyFile.getRemote();
} catch (KeyStoreException e) {
throw new AbortException(e.getMessage());
} catch (UnrecoverableKeyException e) {
throw new AbortException(e.getMessage());
} catch (NoSuchAlgorithmException e) {
throw new AbortException(e.getMessage());
} catch (CertificateEncodingException e) {
throw new AbortException(e.getMessage());
}
} else {
throw new AbortException("Unsupported Credentials type " + c.getClass().getName());
}
status = launcher.launch()
.cmdAsSingleString("kubectl config --kubeconfig=" + configFile.getRemote() + " set-credentials cluster-admin " + login)
.masks(false, false, false, false, false, false, true)
.join();
if (status != 0) throw new IOException("Failed to run kubectl config "+status);
status = launcher.launch()
.cmdAsSingleString("kubectl config --kubeconfig=" + configFile.getRemote() + " set-context k8s --cluster=k8s --user=cluster-admin")
.join();
if (status != 0) throw new IOException("Failed to run kubectl config "+status);
status = launcher.launch()
.cmdAsSingleString("kubectl config --kubeconfig=" + configFile.getRemote() + " use-context k8s")
.join();
if (status != 0) throw new IOException("Failed to run kubectl config "+status);
context.setDisposer(new CleanupDisposer(tempFiles));
context.env("KUBECONFIG", configFile.getRemote());
}
/**
* Get the {@link StandardCredentials}.
*
* @return the credentials matching the {@link #credentialsId} or {@code null} is {@code #credentialsId} is blank
* @throws AbortException if no {@link StandardCredentials} matching {@link #credentialsId} is found
*/
@CheckForNull
private StandardCredentials getCredentials() throws AbortException {
if (StringUtils.isBlank(credentialsId)) {
return null;
}
StandardCredentials result = CredentialsMatchers.firstOrNull(
CredentialsProvider.lookupCredentials(StandardCredentials.class,
Jenkins.getInstance(), ACL.SYSTEM, Collections.<DomainRequirement>emptyList()),
CredentialsMatchers.withId(credentialsId)
);
if (result == null) {
throw new AbortException("No credentials found for id \"" + credentialsId + "\"");
}
return result;
}
private static String wrapWithMarker(String begin, String end, String encodedBody) {
return new StringBuilder(begin).append("\n")
.append(encodedBody).append("\n")
.append(end)
.toString();
}
@Extension
public static class DescriptorImpl extends BuildWrapperDescriptor {
@Override
public boolean isApplicable(AbstractProject<?, ?> item) {
return true;
}
@Override
public String getDisplayName() {
return "Setup Kubernetes CLI (kubectl)";
}
public ListBoxModel doFillCredentialsIdItems(@AncestorInPath Item item, @QueryParameter String serverUrl) {
return new StandardListBoxModel()
.withEmptySelection()
.withMatching(
CredentialsMatchers.anyOf(
CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class),
CredentialsMatchers.instanceOf(TokenProducer.class),
CredentialsMatchers.instanceOf(StandardCertificateCredentials.class)
),
CredentialsProvider.lookupCredentials(
StandardCredentials.class,
item,
null,
URIRequirementBuilder.fromUri(serverUrl).build()
)
);
}
}
private static class CleanupDisposer extends Disposer {
private static final long serialVersionUID = 3006113419319201358L;
private Set<String> configFiles;
public CleanupDisposer(Set<String> tempFiles) {
this.configFiles = tempFiles;
}
@Override
public void tearDown(Run<?, ?> build, FilePath workspace, Launcher launcher, TaskListener listener) throws IOException, InterruptedException {
for (String configFile : configFiles) {
workspace.child(configFile).delete();
}
}
}
}