package io.jenkins.plugins.remotingkafka; 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.domains.SchemeRequirement; import com.cloudbees.plugins.credentials.matchers.IdMatcher; import hudson.Extension; import hudson.Util; import hudson.model.Item; import hudson.security.ACL; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import io.fabric8.kubernetes.api.model.Container; import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.apps.StatefulSet; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import io.jenkins.plugins.remotingkafka.exception.RemotingKafkaConfigurationException; import io.jenkins.plugins.remotingkafka.exception.RemotingKafkaException; import jenkins.model.GlobalConfiguration; import jenkins.model.Jenkins; import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; import org.apache.http.client.utils.URIBuilder; import org.jenkinsci.Symbol; import org.jenkinsci.plugins.kubernetes.credentials.TokenProducer; import org.jenkinsci.plugins.plaincredentials.FileCredentials; import org.jenkinsci.plugins.plaincredentials.StringCredentials; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.interceptor.RequirePOST; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.servlet.ServletException; import java.io.IOException; import java.io.InputStream; import java.net.Socket; import java.nio.file.Paths; import java.security.GeneralSecurityException; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; @Extension @Symbol("kafka") public class GlobalKafkaConfiguration extends GlobalConfiguration { private static final Logger LOGGER = Logger.getLogger(GlobalKafkaConfiguration.class.getName()); public static final SchemeRequirement KAFKA_SCHEME = new SchemeRequirement("kafka"); private String brokerURL; private String zookeeperURL; private boolean enableSSL; private String kafkaCredentialsId; private String sslTruststoreCredentialsId; private String sslKeystoreCredentialsId; private String sslKeyCredentialsId; private boolean useKubernetes; private String kubernetesIp; private String kubernetesApiPort; private String kubernetesCertificate; private String kubernetesCredentialsId; private boolean kubernetesSkipTlsVerify; private String kubernetesNamespace; public GlobalKafkaConfiguration() { load(); } public static GlobalKafkaConfiguration get() { return GlobalConfiguration.all().getInstance(GlobalKafkaConfiguration.class); } public String getBrokerURL() { return brokerURL; } public void setBrokerURL(String brokerURL) { this.brokerURL = brokerURL; } public String getZookeeperURL() { return zookeeperURL; } public void setZookeeperURL(String zookeeperURL) { this.zookeeperURL = zookeeperURL; } public boolean getEnableSSL() { return enableSSL; } public void setEnableSSL(boolean enableSSL) { this.enableSSL = enableSSL; } public String getKafkaCredentialsId() { return kafkaCredentialsId; } public void setKafkaCredentialsId(String kafkaCredentialsId) { this.kafkaCredentialsId = kafkaCredentialsId; } public String getSslTruststoreCredentialsId() { return sslTruststoreCredentialsId; } public void setSslTruststoreCredentialsId(String sslTruststoreCredentialsId) { this.sslTruststoreCredentialsId = sslTruststoreCredentialsId; } public String getSslKeystoreCredentialsId() { return sslKeystoreCredentialsId; } public void setSslKeystoreCredentialsId(String sslKeystoreCredentialsId) { this.sslKeystoreCredentialsId = sslKeystoreCredentialsId; } public String getSslKeyCredentialsId() { return sslKeyCredentialsId; } public void setSslKeyCredentialsId(String sslKeyCredentialsId) { this.sslKeyCredentialsId = sslKeyCredentialsId; } public boolean getUseKubernetes() { return useKubernetes; } public void setUseKubernetes(boolean useKubernetes) { this.useKubernetes = useKubernetes; } public String getKubernetesIp() { return kubernetesIp; } public void setKubernetesIp(String kubernetesIp) { this.kubernetesIp = kubernetesIp; } public String getKubernetesApiPort() { return kubernetesApiPort; } public void setKubernetesApiPort(String kubernetesApiPort) { this.kubernetesApiPort = kubernetesApiPort; } public String getKubernetesCertificate() { return kubernetesCertificate; } public void setKubernetesCertificate(String kubernetesCertificate) { this.kubernetesCertificate = kubernetesCertificate; } public String getKubernetesCredentialsId() { return kubernetesCredentialsId; } public void setKubernetesCredentialsId(String kubernetesCredentialsId) { this.kubernetesCredentialsId = kubernetesCredentialsId; } public boolean getKubernetesSkipTlsVerify() { return kubernetesSkipTlsVerify; } public void setKubernetesSkipTlsVerify(boolean kubernetesSkipTlsVerify) { this.kubernetesSkipTlsVerify = kubernetesSkipTlsVerify; } public String getKubernetesNamespace() { return kubernetesNamespace; } public void setKubernetesNamespace(String kubernetesNamespace) { this.kubernetesNamespace = kubernetesNamespace; } public String getKafkaUsername() throws RemotingKafkaConfigurationException { StandardUsernamePasswordCredentials credential = getCredential(kafkaCredentialsId); if (credential == null) { throw new RemotingKafkaConfigurationException("No Kafka credential provided"); } return credential.getUsername(); } public String getKafkaPassword() throws RemotingKafkaConfigurationException { StandardUsernamePasswordCredentials credential = getCredential(kafkaCredentialsId); if (credential == null) { throw new RemotingKafkaConfigurationException("No Kafka credential provided"); } return credential.getPassword().getPlainText(); } public String getSSLTruststoreLocation() throws RemotingKafkaConfigurationException { StandardUsernamePasswordCredentials credential = getCredential(sslTruststoreCredentialsId); if (credential == null) { throw new RemotingKafkaConfigurationException("No SSL truststore credential provided"); } return credential.getUsername(); } public String getSSLTruststorePassword() throws RemotingKafkaConfigurationException { StandardUsernamePasswordCredentials credential = getCredential(sslTruststoreCredentialsId); if (credential == null) { throw new RemotingKafkaConfigurationException("No SSL truststore credential provided"); } return credential.getPassword().getPlainText(); } public String getSSLKeystoreLocation() throws RemotingKafkaConfigurationException { StandardUsernamePasswordCredentials credential = getCredential(sslKeystoreCredentialsId); if (credential == null) { throw new RemotingKafkaConfigurationException("No SSL keystore credential provided"); } return credential.getUsername(); } public String getSSLKeystorePassword() throws RemotingKafkaConfigurationException { StandardUsernamePasswordCredentials credential = getCredential(sslKeystoreCredentialsId); if (credential == null) { throw new RemotingKafkaConfigurationException("No SSL keystore credential provided"); } return credential.getPassword().getPlainText(); } public String getSSLKeyPassword() throws RemotingKafkaConfigurationException { StandardUsernamePasswordCredentials credential = getCredential(sslKeyCredentialsId); if (credential == null) { throw new RemotingKafkaConfigurationException("No SSL key credential provided"); } return credential.getPassword().getPlainText(); } public FormValidation doCheckBrokerURL(@QueryParameter("brokerURL") final String brokerURL) { if (StringUtils.isBlank(brokerURL)) { return FormValidation.error(Messages.GlobalKafkaConfiguration_KafkaConnectionURLWarning()); } return FormValidation.ok(); } public FormValidation doCheckZookeeperURL(@QueryParameter("zookeeperURL") String zookeeperURL) { if (StringUtils.isBlank(zookeeperURL)) { return FormValidation.error(Messages.GlobalKafkaConfiguration_ZookeeperURLWarning()); } return FormValidation.ok(); } @RequirePOST public FormValidation doTestZookeeperConnection(@QueryParameter("zookeeperURL") final String zookeeperURL) throws IOException, ServletException { if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { return FormValidation.error("Need admin permission to perform this action"); } try { String[] hostport = zookeeperURL.split(":"); String host = hostport[0]; int port = Integer.parseInt(hostport[1]); testConnection(host, port); return FormValidation.ok("Success"); } catch (Exception e) { return FormValidation.error("Connection error : " + e.getMessage()); } } @RequirePOST public FormValidation doTestBrokerConnection(@QueryParameter("brokerURL") final String brokerURL) throws IOException, ServletException { if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { return FormValidation.error("Need admin permission to perform this action"); } try { String[] hostport = brokerURL.split(":"); String host = hostport[0]; int port = Integer.parseInt(hostport[1]); testConnection(host, port); return FormValidation.ok("Success"); } catch (Exception e) { return FormValidation.error("Connection error : " + e.getMessage()); } } @RequirePOST public FormValidation doTestKubernetesConnection( @QueryParameter("kubernetesIp") String serverIp, @QueryParameter("kubernetesApiPort") String serverPort, @QueryParameter("kubernetesCredentialsId") String credentialsId, @QueryParameter("kubernetesCertificate") String serverCertificate, @QueryParameter("kubernetesSkipTlsVerify") boolean skipTlsVerify, @QueryParameter("kubernetesNamespace") String namespace ) { Jenkins.get().checkPermission(Jenkins.ADMINISTER); try { String serverUrl = getURL(serverIp, Integer.parseInt(serverPort)); KubernetesClient client = new KubernetesFactoryAdapter(serverUrl, namespace, Util.fixEmpty(serverCertificate), Util.fixEmpty(credentialsId), skipTlsVerify ).createClient(); // Call Pod list API to ensure functionality client.pods().list(); return FormValidation.ok("Success"); } catch (KubernetesClientException e) { LOGGER.log(Level.FINE, "Error testing Kubernetes connection", e); return FormValidation.error("Error: %s", e.getCause() == null ? e.getMessage() : String.format("%s: %s", e.getCause().getClass().getName(), e.getCause().getMessage())); } catch (Exception e) { LOGGER.log(Level.FINE, "Error testing Kubernetes connection", e); return FormValidation.error("Error: %s", e.getMessage()); } } @RequirePOST public FormValidation doStartKafkaOnKubernetes( @QueryParameter("kubernetesIp") String serverIp, @QueryParameter("kubernetesApiPort") String serverPort, @QueryParameter("kubernetesCredentialsId") String credentialsId, @QueryParameter("kubernetesCertificate") String serverCertificate, @QueryParameter("kubernetesSkipTlsVerify") boolean skipTlsVerify, @QueryParameter("kubernetesNamespace") String namespace ) { Jenkins.get().checkPermission(Jenkins.ADMINISTER); Class clazz = GlobalKafkaConfiguration.class; try (InputStream zookeeperFile = clazz.getResourceAsStream(Paths.get("kubernetes", "zookeeper.yaml").toString()); InputStream kafkaServiceFile = clazz.getResourceAsStream(Paths.get("kubernetes", "kafka-service.yaml").toString()); InputStream kafkaStatefulSetFile = clazz.getResourceAsStream(Paths.get("kubernetes", "kafka-statefulset.yaml").toString())) { String serverUrl = getURL(serverIp, Integer.parseInt(serverPort)); KubernetesClient client = new KubernetesFactoryAdapter(serverUrl, namespace, Util.fixEmpty(serverCertificate), Util.fixEmpty(credentialsId), skipTlsVerify ).createClient(); KubernetesQuery kubernetesQuery = new KubernetesQuery(client); client.load(zookeeperFile).createOrReplace(); LOGGER.info("Starting Zookeeper"); client.load(kafkaServiceFile).createOrReplace(); LOGGER.info("Starting Kafka Services"); Integer zookeeperPort = kubernetesQuery.getFirstNodePortByServiceName("zookeeper-svc"); Integer kafkaPort = kubernetesQuery.getFirstNodePortByServiceName("kafka-svc"); if (zookeeperPort == null || kafkaPort == null) throw new RemotingKafkaConfigurationException("Zookeeper NodePort or Kafka NodePort not found"); // Set Kafka advertised.listeners property List<HasMetadata> kafkaStatefulSetResources = client.load(kafkaStatefulSetFile).get(); StatefulSet kafkaStatefulSet = (StatefulSet) kafkaStatefulSetResources .stream() .filter(res -> res.getKind().equals("StatefulSet") && res.getMetadata().getName().equals("kafka")) .findFirst() .orElseThrow(() -> new RemotingKafkaConfigurationException("Couldn't find StatefulSet named kafka in YAML configuration")); Container kafkaContainer = kafkaStatefulSet .getSpec() .getTemplate() .getSpec() .getContainers() .stream() .filter(con -> con.getName().equals("kafka")) .findFirst() .orElseThrow(() -> new RemotingKafkaConfigurationException("Couldn't find Container named kafka in template specification")); kafkaContainer.getEnv().add(new EnvVar( "KAFKA_ADVERTISED_LISTENERS", String.format("EXTERNAL://%s:%s", serverIp, kafkaPort), null )); client.resourceList(kafkaStatefulSetResources).createOrReplace(); LOGGER.info("Starting Kafka StatefulSet"); // Probe for Kafka readiness while (true) { try { testConnection(serverIp, kafkaPort); break; } catch (IOException e) { LOGGER.fine(String.format("Waiting for Kafka connection at %s:%s", serverIp, kafkaPort)); } TimeUnit.SECONDS.sleep(1); } LOGGER.info("Zookeeper and Kafka started"); // Set Zookeeper and Broker URL GlobalKafkaConfiguration.get().setZookeeperURL(getURL(serverIp, zookeeperPort)); GlobalKafkaConfiguration.get().setBrokerURL(getURL(serverIp, kafkaPort)); return FormValidation.ok(String.format("Success. Zookeeper: %s:%s and Kafka: %s:%s", serverIp, zookeeperPort, serverIp, kafkaPort)); } catch (RuntimeException | InterruptedException | RemotingKafkaException | IOException | GeneralSecurityException e) { LOGGER.log(Level.SEVERE, "Error", e); return FormValidation.error("Error: %s", e.getCause() == null ? e.getMessage() : String.format("%s: %s", e.getCause().getClass().getName(), e.getCause().getMessage())); } } @Override public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { this.brokerURL = formData.getString("brokerURL"); this.zookeeperURL = formData.getString("zookeeperURL"); this.enableSSL = Boolean.valueOf(formData.getString("enableSSL")); this.useKubernetes = Boolean.valueOf(formData.getString("useKubernetes")); this.kubernetesIp = formData.getString("kubernetesIp"); this.kubernetesApiPort = formData.getString("kubernetesApiPort"); this.kubernetesCertificate = formData.getString("kubernetesCertificate"); this.kubernetesSkipTlsVerify = Boolean.valueOf(formData.getString("kubernetesSkipTlsVerify")); this.kubernetesNamespace = formData.getString("kubernetesNamespace"); save(); return true; } private void testConnection(String host, int port) throws IOException { new Socket(host, port); } @CheckForNull private StandardUsernamePasswordCredentials getCredential(@Nonnull String id) { StandardUsernamePasswordCredentials credential = null; List<StandardUsernamePasswordCredentials> credentials = CredentialsProvider.lookupCredentials( StandardUsernamePasswordCredentials.class, Jenkins.get(), ACL.SYSTEM, Collections.emptyList()); IdMatcher matcher = new IdMatcher(id); for (StandardUsernamePasswordCredentials c : credentials) { if (matcher.matches(c)) { credential = c; } } return credential; } @RequirePOST public ListBoxModel doFillKafkaCredentialsIdItems(@AncestorInPath Item item, @QueryParameter String credentialsId) { return fillCredentialsIdItems(item, credentialsId); } public FormValidation doCheckKafkaCredentialsId(@AncestorInPath Item item, @QueryParameter String value) { FormValidation checkedResult = checkCredentialsId(item, value); boolean isAdminOrNullItem = (item != null || Jenkins.get().hasPermission(Jenkins.ADMINISTER)); if (isAdminOrNullItem && checkedResult.equals(FormValidation.ok())) { this.kafkaCredentialsId = value; } return checkedResult; } @RequirePOST public ListBoxModel doFillSslTruststoreCredentialsIdItems(@AncestorInPath Item item, @QueryParameter String credentialsId) { return fillCredentialsIdItems(item, credentialsId); } public FormValidation doCheckSslTruststoreCredentialsId(@AncestorInPath Item item, @QueryParameter String value) { FormValidation checkedResult = checkCredentialsId(item, value); boolean isAdminOrNullItem = (item != null || Jenkins.get().hasPermission(Jenkins.ADMINISTER)); if (isAdminOrNullItem && checkedResult.equals(FormValidation.ok())) { this.sslTruststoreCredentialsId = value; } return checkedResult; } @RequirePOST public ListBoxModel doFillSslKeystoreCredentialsIdItems(@AncestorInPath Item item, @QueryParameter String credentialsId) { return fillCredentialsIdItems(item, credentialsId); } public FormValidation doCheckSslKeystoreCredentialsId(@AncestorInPath Item item, @QueryParameter String value) { FormValidation checkedResult = checkCredentialsId(item, value); boolean isAdminOrNullItem = (item != null || Jenkins.get().hasPermission(Jenkins.ADMINISTER)); if (isAdminOrNullItem && checkedResult.equals(FormValidation.ok())) { this.sslKeystoreCredentialsId = value; } return checkedResult; } @RequirePOST public ListBoxModel doFillSslKeyCredentialsIdItems(@AncestorInPath Item item, @QueryParameter String credentialsId) { return fillCredentialsIdItems(item, credentialsId); } public FormValidation doCheckSslKeyCredentialsId(@AncestorInPath Item item, @QueryParameter String value) { FormValidation checkedResult = checkCredentialsId(item, value); boolean isAdminOrNullItem = (item != null || Jenkins.get().hasPermission(Jenkins.ADMINISTER)); if (isAdminOrNullItem && checkedResult.equals(FormValidation.ok())) { this.sslKeyCredentialsId = value; } return checkedResult; } @RequirePOST public ListBoxModel doFillKubernetesCredentialsIdItems() { Jenkins.get().checkPermission(Jenkins.ADMINISTER); return new StandardListBoxModel().withEmptySelection() .withMatching( CredentialsMatchers.anyOf( CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class), CredentialsMatchers.instanceOf(FileCredentials.class), CredentialsMatchers.instanceOf(TokenProducer.class), CredentialsMatchers.instanceOf(StandardCertificateCredentials.class), CredentialsMatchers.instanceOf(StringCredentials.class)), CredentialsProvider.lookupCredentials(StandardCredentials.class, Jenkins.get(), ACL.SYSTEM, Collections.EMPTY_LIST )); } private FormValidation checkCredentialsId(@AncestorInPath Item item, @QueryParameter String value) { if (item == null) { if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { return FormValidation.ok(); } } if (value.startsWith("${") && value.endsWith("}")) { return FormValidation.warning("Cannot validate expression based credentials"); } if (CredentialsProvider.listCredentials( StandardUsernamePasswordCredentials.class, Jenkins.get(), ACL.SYSTEM, Collections.singletonList(KAFKA_SCHEME), CredentialsMatchers.withId(value) ).isEmpty()) { return FormValidation.error("Cannot find currently selected credentials"); } return FormValidation.ok(); } private ListBoxModel fillCredentialsIdItems(@AncestorInPath Item item, @QueryParameter String credentialsId) { StandardListBoxModel result = new StandardListBoxModel(); if (item == null) { if (!Jenkins.get().hasPermission(Jenkins.ADMINISTER)) { return result.includeCurrentValue(credentialsId); } } return result .includeMatchingAs( ACL.SYSTEM, Jenkins.get(), StandardUsernamePasswordCredentials.class, Collections.singletonList(KAFKA_SCHEME), CredentialsMatchers.always() ) .includeCurrentValue(credentialsId); } private static String getURL(String host, int port) { String url = new URIBuilder() .setHost(host) .setPort(port) .toString(); if (url.startsWith("//")) { url = url.substring("//".length()); } return url; } }