From 2ed6c6c4be1ee826c5316e3a5a15dfe60d3809cd Mon Sep 17 00:00:00 2001 From: Alexey Khivin Date: Mon, 3 Oct 2016 14:35:14 +0300 Subject: [PATCH] [Kubernetes] Deploy Kubernetes using murano. Change-Id: Ic09f5ca401fe60d06d606d327335d0ebe1e13628 --- kubernetes/pom.xml | 71 ++ .../plugins/murano/ConfigurationSection.java | 30 + .../plugins/murano/MuranoBuilder.java | 765 ++++++++++++++++++ .../murano/client/OpenstackClient.java | 94 +++ .../plugins/murano/MuranoBuilder/config.jelly | 59 ++ kubernetes/src/main/resources/index.jelly | 7 + 6 files changed, 1026 insertions(+) create mode 100644 kubernetes/pom.xml create mode 100644 kubernetes/src/main/java/com/mirantis/plugins/murano/ConfigurationSection.java create mode 100644 kubernetes/src/main/java/com/mirantis/plugins/murano/MuranoBuilder.java create mode 100644 kubernetes/src/main/java/com/mirantis/plugins/murano/client/OpenstackClient.java create mode 100644 kubernetes/src/main/resources/com/mirantis/plugins/murano/MuranoBuilder/config.jelly create mode 100644 kubernetes/src/main/resources/index.jelly diff --git a/kubernetes/pom.xml b/kubernetes/pom.xml new file mode 100644 index 0000000..e075a81 --- /dev/null +++ b/kubernetes/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + + org.jenkins-ci.plugins + plugin + 2.11 + + + + com.mirantis.plugins + murano + 1.0-SNAPSHOT + hpi + + + 1.625.3 + 7 + 2.13 + + + Murano Kubernetes Plugin + Murano plugin with K8S to deploy Tomcat WAR + https://wiki.jenkins-ci.org/display/JENKINS/TODO+Plugin + + + + MIT License + http://opensource.org/licenses/MIT + + + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + + + org.pacesys + openstack4j-core + 3.0.1 + + + org.pacesys.openstack4j.connectors + openstack4j-httpclient + 3.0.1 + + + com.googlecode.json-simple + json-simple + 1.1.1 + + + io.fabric8 + kubernetes-client + 1.4.4 + + + + diff --git a/kubernetes/src/main/java/com/mirantis/plugins/murano/ConfigurationSection.java b/kubernetes/src/main/java/com/mirantis/plugins/murano/ConfigurationSection.java new file mode 100644 index 0000000..68ac50c --- /dev/null +++ b/kubernetes/src/main/java/com/mirantis/plugins/murano/ConfigurationSection.java @@ -0,0 +1,30 @@ +package com.mirantis.plugins.murano; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Configuration Helper to understand the Mirantis UI definition + */ +public class ConfigurationSection { + Map> conf; + + public ConfigurationSection() { + conf = new HashMap(); + } + + public void addSection(String sectionName, List fields) { + conf.put(sectionName, fields); + } + + public List getSection(String sectionName){ + return conf.get(sectionName); + } + + public Set getSections(){ + return conf.keySet(); + } +} + diff --git a/kubernetes/src/main/java/com/mirantis/plugins/murano/MuranoBuilder.java b/kubernetes/src/main/java/com/mirantis/plugins/murano/MuranoBuilder.java new file mode 100644 index 0000000..0e55614 --- /dev/null +++ b/kubernetes/src/main/java/com/mirantis/plugins/murano/MuranoBuilder.java @@ -0,0 +1,765 @@ +package com.mirantis.plugins.murano; + + +import com.mirantis.plugins.murano.client.OpenstackClient; +import hudson.Extension; +import hudson.Launcher; +import hudson.Util; +import hudson.model.AbstractBuild; +import hudson.model.AbstractProject; +import hudson.model.BuildListener; +import hudson.tasks.BuildStepDescriptor; +import hudson.tasks.BuildStepMonitor; +import hudson.tasks.Notifier; +import hudson.tasks.Publisher; +import hudson.util.FormValidation; +import hudson.util.ListBoxModel; +import io.fabric8.kubernetes.api.model.ReplicationController; +import io.fabric8.kubernetes.api.model.ReplicationControllerBuilder; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.ConfigBuilder; +import io.fabric8.kubernetes.client.DefaultKubernetesClient; +import okhttp3.ConnectionSpec; +import okhttp3.OkHttpClient; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; +import org.openstack4j.connectors.httpclient.HttpCommand; +import org.openstack4j.core.transport.HttpMethod; +import org.openstack4j.core.transport.HttpRequest; +import org.yaml.snakeyaml.Yaml; + +import javax.servlet.ServletException; +import java.io.*; +import java.security.SecureRandom; +import java.util.*; + +/** + * Main Jenkins plugin class that acts as a PostBuilder + */ +public class MuranoBuilder extends Notifier { + + private String serverUrl; + private String username; + private String password; + private String tenantName; + private String dockerImage; + private String flavor; + private String keypairs; + private String clusterName; + private String slaveCount; + private String gatewayCount; + + @DataBoundConstructor + public MuranoBuilder(String serverUrl, + String username, + String password, + String tenantName, + String clusterName, + String dockerRegistry, + String flavor, + String keypairs, + String dockerImage, + String slaveCount, + String gatewayCount) { + this.serverUrl = serverUrl; + this.username = username; + this.password = password; + this.tenantName = tenantName; + this.dockerImage = dockerImage; + this.clusterName = clusterName; + this.flavor = flavor; + this.keypairs = keypairs; + this.slaveCount = slaveCount; + this.gatewayCount = gatewayCount; + } + + public String getServerUrl() { + return serverUrl; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public String getTenantName() { + return tenantName; + } + + public String getDockerImage() { + return dockerImage; + } + + public String getFlavor() { + return flavor; + } + + public String getClusterName() { + return clusterName; + } + + public String getGatewayCount() { + return gatewayCount; + } + + public String getSlaveCount() { + return slaveCount; + } + + /** + * Postbuild implementation, gets the parameters from the values filled in the Jenkins + * Configuration. + * + * It the uses the Environment name to check if the environment exists and if it does, uses the + * same environment to deploy the image. This also assumes that a build solution is available that + * pushes the Jenkins build to dockerhub or whatever Docker registry has been configured. Thats out + * of scope of this plugin to build the Docker image. + * + * If there is no environment available, it will build the K8S environment and then use it to + * deploy the image. + * + * @param build The Build object from Jenkins + * @param launcher The Launcher + * @param listener Listener for Logging events + * @return whether the post-build was successful + * @throws IOException + * @throws InterruptedException + */ + @Override + public boolean perform(AbstractBuild build, Launcher launcher, + BuildListener listener) throws IOException, InterruptedException { + + // Replace the image so that $BUILD_NUMBER is replaced with an actual image. This is because + // we tag the docker images with the build numbers + String image = Util.replaceMacro(this.dockerImage, build.getEnvironment(listener)); + + OpenstackClient client = new OpenstackClient(serverUrl, + username, + password, + tenantName); + if (!client.authenticate()) { + listener.getLogger().println("Cannot connect to Openstack server. Pls check configuration"); + return false; + } + + // Get the UI Definition for Kubernetes. This is hardcoded based on the K8S Definition in Murano + // TODO: See if this can be changed dynamically + String token = client.getOSClient().getAccess().getToken().getId(); + HttpRequest request = HttpRequest.builder().method(HttpMethod.GET) + .endpoint(serverUrl + ":9292") + .path("/v3/artifacts/murano/v1/f07d4447-d479-401d-86f2-902f3e260f60/ui_definition/download") // K8S cluster id + .header("X-Auth-Token", token) + .build(); + HttpCommand command = HttpCommand.create(request); + CloseableHttpResponse response = null; + try { + response = command.execute(); + } catch(Exception ex) { + ex.printStackTrace();; + return false; + } + if (response == null) { + listener.getLogger().println("Error getting response for UI definition"); + return false; + } + + StringBuffer jsonString = new StringBuffer(); + try { + BufferedReader br = new BufferedReader(new InputStreamReader( + response.getEntity().getContent())); + + //Print the raw output of murano api from the server + String output; + + while ((output = br.readLine()) != null) { + jsonString.append(output + "\n"); + } + } catch(Exception ex) { + listener.getLogger().println("Error getting output for UI definition"); + return false; + } + + // Fill in the templates according to the jenkins job + // TODO: Could be done only if the image needs to be created. Otehrwise its a waste of resouces. + Yaml yaml = new Yaml(); + String appTemplate = ""; + ConfigurationSection sections = new ConfigurationSection(); + Map templates = new HashMap(); + Map> values = (Map>) yaml.load(jsonString.toString()); + for (String key : values.keySet()) { + System.out.println(key); + if (key.equals("Forms")) { + List formElements = (ArrayList)values.get(key); + + for (Map type: formElements) { + String typeName = (String)type.keySet().toArray()[0]; // gets the form types like : appConfiguration, instanceConfiguration etc + + Map> fields = (HashMap>) type.get(typeName); + List fieldArr = (ArrayList)fields.get("fields"); + + sections.addSection(typeName, fieldArr); + + } + } else if (key.equals("Application")) { + System.out.println("Application section : "); + Map applicationTemplate = (Map)values.get(key); + appTemplate = "{" + getApplicationJsonTemplate(applicationTemplate) + "}"; + + } else if (key.equals("Templates")) { + System.out.println("Templates"); + Map templateMap = values.get(key); + for(String templateName: templateMap.keySet()) { + Object templateValue = templateMap.get(templateName); + if (templateValue instanceof Map) { + Map internalTemplateHash = (Map) templateValue; + //String templateExpanded = "{" + getApplicationJsonTemplate(internalTemplateHash) + "}"; + templates.put(templateName, internalTemplateHash); + } + } + } + } + + String envId = checkIfDeploymentExists(token, this.clusterName); + if (envId == null) { + listener.getLogger().println("Creating new enviroment"); + // No Environment, create the cluster + String filledTemplate = fillTemplatesForKubernetes(templates, + appTemplate, + build.getId(), + this.flavor, + this.keypairs); + + // Create Env + envId = this.createEnvironment(token, this.clusterName); + + // Create Session + String sessionId = this.createEnvironmentSession(token, envId); + + // Add App to Environment + addApplicationToEnvironment(token, envId, sessionId, filledTemplate); + + // Deploy + deployEnvironment(token, envId, sessionId); + listener.getLogger().println("Waiting for Deployment to finish..."); + if (!isDeploymentSuccess(token, envId)) { + listener.getLogger().println("Taking longer to deploy, please check the murano console"); + return false; + } + listener.getLogger().println("Deployed K8S"); + } + + + // Get the Floating IP to deploy the image + String kubeMasterAddress = null; + try { + kubeMasterAddress = getFloatingIp(token, envId); + kubeMasterAddress = "http://" + kubeMasterAddress + ":8080"; // By default the mirantis instance runs on 8080 + } catch(Exception ex) { + listener.getLogger().println("Error getting Floating ip"); + return false; + } + + return deployImage(kubeMasterAddress, this.clusterName, image, listener.getLogger()); + } + + /** + * Loop around to see if the deployment is a success. This waits for about 10 secs 300 times hoping that + * it finishes. This all depends on teh number of nodes and the speed of the boxes. But seems sufficient. + * + * @param token Environment Token + * @param envId Environemnt Id + * @return whether the deployment is a success + */ + private boolean isDeploymentSuccess(String token, String envId) { + boolean status = false; + for (int i=0; i<300; i++) { + + try { + Thread.sleep(10000); + String payload = getResponseForJsonPost(serverUrl, + 8082, + "/v1/environments/" + envId + "/deployments", + HttpMethod.GET, + token, + null, + null); + JSONParser parser = new JSONParser(); + try { + JSONObject deployments = (JSONObject) parser.parse(payload); + JSONArray deploymentList = (JSONArray) deployments.get("deployments"); + JSONObject thisDeployment = (JSONObject) deploymentList.get(0); + if ("success".equals((String) thisDeployment.get("state"))) { + status = true; + break; + } + } catch (ParseException pe) { + System.out.println("position: " + pe.getPosition()); + System.out.println(pe); + } + } catch (Exception ex) { + status = false; + break; + } + } + return status; + } + + // ServiceName is the same as Cluster Name + private boolean deployImage(String floatingIp, String serviceName, String image, PrintStream logger) { + Config config = new ConfigBuilder() + .withMasterUrl(floatingIp) + .withNamespace("default") // Use default namespace + .build(); + + // Redo the dance till the cleartext is setup with kubernetes libraries. + // TODO: This is fixed in https://github.com/fabric8io/kubernetes-client/issues/498 + DefaultKubernetesClient client = new DefaultKubernetesClient(config); + ArrayList specs = new ArrayList(); + specs.add(ConnectionSpec.CLEARTEXT); + OkHttpClient httpClient = client.getHttpClient().newBuilder().connectionSpecs(specs).build(); + client = new DefaultKubernetesClient(httpClient, config); + + Service service = client.services().withName(serviceName).get(); + if (service == null) { + // There is no service, create one with port on 9999 targeting 8080 internally + logger.println("There is no service named: " + serviceName + ", Creating one"); + HashMap labels = new HashMap(); + labels.put("server", "javawebapp"); + client.services().createNew(). + withNewMetadata().withName(serviceName).endMetadata(). + withNewSpec(). + addNewPort().withPort(9999).withNewTargetPort().withIntVal(8080).endTargetPort().endPort(). + withSelector(labels). + withType("NodePort"). + endSpec(). + done(); + service = client.services().withName(serviceName).get(); + if (service == null) { + logger.print("Still cannot create servce, Aborting"); + return false; + } + } else { + logger.println("Obtained Service with name: " + serviceName); + } + + // Create the RC + logger.print("Creating Replication Controller now"); + + ReplicationController rc = client.replicationControllers().withName(serviceName + "-rc").get(); + if (rc == null) { + logger.println("No RC Found with name: " + serviceName + "-rc, Creating one"); + rc = new ReplicationControllerBuilder(). + withNewMetadata().withName(serviceName + "-rc").addToLabels("server", "javawebapp").endMetadata(). + withNewSpec().withReplicas(1). + withNewTemplate(). + withNewMetadata().addToLabels("server", "javawebapp").endMetadata(). + withNewSpec(). + addNewContainer().withName(serviceName).withImage(image). + addNewPort().withContainerPort(8080).endPort(). + endContainer(). + endSpec(). + endTemplate(). + endSpec(). + build(); + client.replicationControllers().create(rc); + logger.println("Created image with: " + image); + } else { + logger.println("RC Found with name: " + serviceName + "-rc, updating with new image"); + client.replicationControllers(). + withName(serviceName + "-rc"). + rolling().updateImage(image); + logger.println("Updated image to : " + image); + } + return true; + } + + + /* Standard output is of form + [{"gatewayCount": 1, "gatewayNodes": [{"instance": {"availabilityZone": "nova", "openstackId": "184c7bca-b757-429e-8565-96a4b32927c8", "name": "$.instanceConfiguration.unitNamingPattern-e094401173c7716f88cc36d62cbb31fe.com", "securityGroupName": null, "image": "ubuntu14.04-x64-kubernetes", "assignFloatingIp": true, "floatingIpAddress": "172.17.10.116", "keyname": "Raja_macbookpro", "?": {"classVersion": "0.0.0", "name": null, "package": "io.murano", "type": "io.murano.resources.LinuxMuranoInstance", "_actions": {}, "id": "ed3fc4457be3056cf500fcd216dbf25c"}, "ipAddresses": ["10.0.31.5", "172.17.10.116"], "flavor": "m1.medium", "networks": {"useFlatNetwork": false, "primaryNetwork": null, "useEnvironmentNetwork": true, "customNetworks": []}, "sharedIps": []}, "?": {"classVersion": "0.0.0", "name": null, "package": "io.murano.apps.docker.kubernetes.KubernetesCluster", "type": "io.murano.apps.docker.kubernetes.KubernetesGatewayNode", "_actions": {}, "id": "b8b3ebc3dd3cce4deed702c48c9475bf"}}], "?": {"classVersion": "0.0.0", "status": "ready", "name": null, "package": "io.murano.apps.docker.kubernetes.KubernetesCluster", "type": "io.murano.apps.docker.kubernetes.KubernetesCluster", "_actions": {"e7c2d5b5cf8291fdb0149188f2b8b9ee_scaleNodesUp": {"enabled": true, "name": "scaleNodesUp"}, "e7c2d5b5cf8291fdb0149188f2b8b9ee_scaleGatewaysDown": {"enabled": true, "name": "scaleGatewaysDown"}, "e7c2d5b5cf8291fdb0149188f2b8b9ee_scaleGatewaysUp": {"enabled": true, "name": "scaleGatewaysUp"}, "e7c2d5b5cf8291fdb0149188f2b8b9ee_exportConfig": {"enabled": true, "name": "exportConfig"}, "e7c2d5b5cf8291fdb0149188f2b8b9ee_scaleNodesDown": {"enabled": true, "name": "scaleNodesDown"}}, "id": "e7c2d5b5cf8291fdb0149188f2b8b9ee"}, "serviceEndpoints": [], "nodeCount": 1, "dockerRegistry": null, "masterNode": {"instance": {"availabilityZone": "nova", "openstackId": "ee92608e-8eb7-41ac-a53a-e8c5a8c107b4", "name": "$.instanceConfiguration.unitNamingPattern-92a53dbcd69636a793587c564e2f708a.com", "securityGroupName": null, "image": "ubuntu14.04-x64-kubernetes", "assignFloatingIp": true, "floatingIpAddress": "172.17.10.115", "keyname": "Raja_macbookpro", "?": {"classVersion": "0.0.0", "name": null, "package": "io.murano", "type": "io.murano.resources.LinuxMuranoInstance", "_actions": {}, "id": "dabbc3a6f0fa18d781ed88369b738a90"}, "ipAddresses": ["10.0.31.4", "172.17.10.115"], "flavor": "m1.medium", "networks": {"useFlatNetwork": false, "primaryNetwork": null, "useEnvironmentNetwork": true, "customNetworks": []}, "sharedIps": []}, "?": {"classVersion": "0.0.0", "name": null, "package": "io.murano.apps.docker.kubernetes.KubernetesCluster", "type": "io.murano.apps.docker.kubernetes.KubernetesMasterNode", "_actions": {}, "id": "882094bb08fa2f78c0607e613d79f428"}}, "minionNodes": [{"instance": {"availabilityZone": "nova", "openstackId": "db281114-019c-4b57-9fb1-11ae7e3b4fbc", "name": "$.instanceConfiguration.unitNamingPattern-d1b3affe5146ad9dcaa4a55d8d69725b.com", "securityGroupName": null, "image": "ubuntu14.04-x64-kubernetes", "assignFloatingIp": true, "floatingIpAddress": "172.17.10.117", "keyname": "Raja_macbookpro", "?": {"classVersion": "0.0.0", "name": null, "package": "io.murano", "type": "io.murano.resources.LinuxMuranoInstance", "_actions": {}, "id": "f890a089cbf645f8f3599fffb6553fb6"}, "ipAddresses": ["10.0.31.6", "172.17.10.117"], "flavor": "m1.medium", "networks": {"useFlatNetwork": false, "primaryNetwork": null, "useEnvironmentNetwork": true, "customNetworks": []}, "sharedIps": []}, "?": {"classVersion": "0.0.0", "name": null, "package": "io.murano.apps.docker.kubernetes.KubernetesCluster", "type": "io.murano.apps.docker.kubernetes.KubernetesMinionNode", "_actions": {}, "id": "d967c9cf3c53fd1deaae4c4af2de6379"}, "exposeCAdvisor": true}], "name": "K8ST1"}] + */ + private String getFloatingIp(String token, String envId) throws Exception { + String payload = getResponseForJsonPost(serverUrl, + 8082, + "/v1/environments/" + envId + "/services", + HttpMethod.GET, + token, + null, + null); + String floatingIp = null; + if (payload != null) { + JSONParser parser = new JSONParser(); + JSONArray array = (JSONArray) parser.parse(payload); + floatingIp = + (String) ((JSONArray) ((JSONObject) ((JSONObject) ((JSONObject) array.get(0)).get("masterNode")). + get("instance")).get("ipAddresses")).get(0); + } + + return floatingIp; + } + /** + * Return the Environment id if it exists + * + * @param token + * @param name + * @return + */ + private String checkIfDeploymentExists(String token, String name) { + String payload = getResponseForJsonPost(serverUrl, + 8082, + "/v1/environments", + HttpMethod.GET, + token, + null, + null); + String envId = null; + JSONParser parser = new JSONParser(); + try{ + Object obj = parser.parse(payload); + JSONObject response = (JSONObject)obj; + JSONArray environmentArray = (JSONArray) response.get("environments"); + for (Object env: environmentArray) { + JSONObject thisEnv = (JSONObject) env; + String envName = (String) thisEnv.get("name"); + if (envName.equals(name)) { + envId = (String) thisEnv.get("id"); + break; + } + } + }catch(ParseException pe){ + System.out.println("position: " + pe.getPosition()); + System.out.println(pe); + } + return envId; + } + + /** + * Deploy the environment given the environment id and Session Token + */ + private void deployEnvironment(String token, String envId, String sessionId) { + String response = getResponseForJsonPost(serverUrl, + 8082, + "/v1/environments/" + envId + "/sessions/" + sessionId + "/deploy", + HttpMethod.POST, + token, + null, + null); + } + + /** + * Add the app(K8S) to the environment + * @param token + * @param envId + * @param sessionId + * @param jsonReq + */ + private void addApplicationToEnvironment(String token, String envId, String sessionId, String jsonReq) { + String response = getResponseForJsonPost(serverUrl, + 8082, + "/v1/environments/" + envId + "/services", + HttpMethod.POST, + token, + jsonReq, + sessionId); + System.out.println("add application response : " + response); + //return sessionId; + } + + private String createEnvironmentSession(String token, String envId) { + String payload = getResponseForJsonPost(serverUrl, + 8082, + "/v1/environments/" + envId + "/configure", + HttpMethod.POST, + token, + null, + null); + + String sessionId = ""; + JSONParser parser = new JSONParser(); + try{ + Object obj = parser.parse(payload); + JSONObject response = (JSONObject)obj; + sessionId = (String)response.get("id"); + }catch(ParseException pe){ + System.out.println("position: " + pe.getPosition()); + System.out.println(pe); + } + System.out.println("Session Id : " + sessionId); + return sessionId; + } + + private String createEnvironment(String token, String envname) { + String reqPayload = "{\"name\":\"" + envname + "\"}"; + String payload = getResponseForJsonPost(serverUrl, 8082, "/v1/environments", HttpMethod.POST, token, reqPayload, null); + + String envId = ""; + JSONParser parser = new JSONParser(); + try{ + Object obj = parser.parse(payload); + JSONObject response = (JSONObject)obj; + envId = (String)response.get("id"); + }catch(ParseException pe){ + System.out.println("position: " + pe.getPosition()); + System.out.println(pe); + } + System.out.println("Envid : " + envId); + return envId; + } + + /** + * Main helper method to call the Murano API and return the response. Accepts both GET and POST. + * + * @param url Base URL to connect + * @param port Which port is murano listening on + * @param requestPath Path on Murano URL + * @param method GET or POST + * @param token Auth Token + * @param jsonPayload Payload for the message + * @param muranoSessionId Optional Session Id + * @return Response from the Call + */ + private String getResponseForJsonPost(String url, + int port, + String requestPath, + HttpMethod method, + String token, + String jsonPayload, + String muranoSessionId) { + HttpRequest request = HttpRequest.builder().method(method) + .endpoint(url + ":" + port) + .path(requestPath) // K8S cluster id + .header("X-Auth-Token", token) + .json(jsonPayload) + .build(); + if (muranoSessionId != null) { + request.getHeaders().put("X-Configuration-Session", muranoSessionId); + } + if (jsonPayload != null) { + request = request.toBuilder().json(jsonPayload).build(); + } + + HttpCommand command = HttpCommand.create(request); + CloseableHttpResponse response = null; + try { + response = command.execute(); + } catch(Exception ex) { + ex.printStackTrace();; + return null; + } + + StringBuffer jsonString = new StringBuffer(); + try { + BufferedReader br = new BufferedReader(new InputStreamReader( + response.getEntity().getContent())); + + //Print the raw output of murano api from the server + String output; + + while ((output = br.readLine()) != null) { + jsonString.append(output + "\n"); + } + } catch(Exception ex) { + return null; + } + + return jsonString.toString(); + } + + private String fillTemplatesForKubernetes(Map templates, + String template, + String buildId, + String flavor, + String keypair) { + String appTemplate = template.replace("$.appConfiguration.name", "Kubernetes_" + buildId); + appTemplate = appTemplate.replace("$.appConfiguration.minionCount", this.slaveCount); + appTemplate = appTemplate.replace("$.appConfiguration.gatewayCount", this.gatewayCount); + appTemplate = appTemplate.replace("dockerRegistry", ""); + + // Setup KubeMaster + Map masterTemplate = (Map) templates.get("masterNode"); + String kubeMasterTemplate = "{" + getApplicationJsonTemplate(masterTemplate) + "}"; + kubeMasterTemplate = kubeMasterTemplate.replaceAll("$.instanceConfiguration.unitNamingPattern-.*?.com", "K8sMaster" + buildId); //Name + kubeMasterTemplate = kubeMasterTemplate.replace("$.instanceConfiguration.flavor", flavor); + kubeMasterTemplate = kubeMasterTemplate.replace("$.appConfiguration.assignFloatingIP", "True"); + kubeMasterTemplate = kubeMasterTemplate.replace("$.instanceConfiguration.keyPair", keypair); + kubeMasterTemplate = kubeMasterTemplate.replace("$.instanceConfiguration.availabilityZone", "nova"); + appTemplate = appTemplate.replace("\"masterNode\":\"$masterNode\"", "\"masterNode\":" + kubeMasterTemplate); + + // Kube Minion + StringBuffer sbMinionTemplate = new StringBuffer(); + String delim = ""; + + for(int i = 1; i<= Integer.parseInt(this.slaveCount); i++) { + Map minionTemplate = (Map) templates.get("minionNode"); + String kubeMinionTemplate = "{" + getApplicationJsonTemplate(minionTemplate) + "}"; + + kubeMinionTemplate = kubeMinionTemplate.replaceAll("\\$.instanceConfiguration.unitNamingPattern-.*?.com", "K8sMinion-" + generateUUID()); //Name + kubeMinionTemplate = kubeMinionTemplate.replace("$.instanceConfiguration.flavor", flavor); + kubeMinionTemplate = kubeMinionTemplate.replace("$.appConfiguration.assignFloatingIP", "True"); + kubeMinionTemplate = kubeMinionTemplate.replace("$.instanceConfiguration.keyPair", keypair); + kubeMinionTemplate = kubeMinionTemplate.replace("$.instanceConfiguration.availabilityZone", "nova"); + + sbMinionTemplate.append(delim).append(kubeMinionTemplate); + delim = ","; + } + appTemplate = appTemplate.replace("\"minionNodes\":\"repeat($minionNode, $.appConfiguration.maxMinionCount)\"", "\"minionNodes\": [" + sbMinionTemplate.toString() + "]"); + + // Kube Gateway + StringBuffer sbGatewayTemplate = new StringBuffer(); + delim = ""; + + for(int i = 1; i<= Integer.parseInt(this.gatewayCount); i++) { + Map gatewayTemplate = (Map) templates.get("gatewayNode"); + String gatewayMinionTemplate = "{" + getApplicationJsonTemplate(gatewayTemplate ) + "}"; + + gatewayMinionTemplate = gatewayMinionTemplate.replaceAll("\\$.instanceConfiguration.unitNamingPattern-.*?.com", "K8sGateway-" + generateUUID()); //Name + gatewayMinionTemplate = gatewayMinionTemplate.replace("$.instanceConfiguration.flavor", flavor); + gatewayMinionTemplate = gatewayMinionTemplate.replace("$.appConfiguration.assignFloatingIP", "True"); + gatewayMinionTemplate = gatewayMinionTemplate.replace("$.instanceConfiguration.keyPair", keypair); + gatewayMinionTemplate = gatewayMinionTemplate.replace("$.instanceConfiguration.availabilityZone", "nova"); + + sbGatewayTemplate.append(delim).append(gatewayMinionTemplate); + delim = ","; + } + appTemplate = appTemplate.replace("\"gatewayNodes\":\"repeat($gatewayNode, $.appConfiguration.maxGatewayCount)\"", "\"gatewayNodes\": [" + sbGatewayTemplate.toString() + "]"); + + return appTemplate; + } + + private String generateUUID() { + SecureRandom ng = new SecureRandom(); + long MSB = 0x8000000000000000L; + + String str = Long.toHexString(MSB | ng.nextLong()) + Long.toHexString(MSB | ng.nextLong()); + return str; + + } + + private String getApplicationJsonTemplate(Map template) { + String templateStr = ""; + boolean firstStr = true; + for (String k : template.keySet()) { + if (!firstStr) { + templateStr += ","; + } + firstStr = false; + Object v = template.get(k); + if (v instanceof Map) { + templateStr += "\"" + k + "\": {" + this.getApplicationJsonTemplate((Map)v) ; + templateStr += "}"; + } else if (v instanceof String) { + String uuid = generateUUID(); + if (k.equals("type")) { + templateStr += "\"" + k + "\":\"" + v + "\""; + templateStr += ",\"id\":\"" + uuid + "\""; + }else if (k.equals("name") && (((String)v).contains("generateHostname"))) { + templateStr += "\"name\":\"$.instanceConfiguration.unitNamingPattern-" + uuid + ".com\""; + }else { + templateStr += "\"" + k + "\":\"" + v + "\""; + } + } + } + return templateStr; + } + + @Override + public BuildStepMonitor getRequiredMonitorService() { + return BuildStepMonitor.BUILD; + } + + @Extension + public static final class DescriptorImpl extends BuildStepDescriptor { + + public DescriptorImpl() { + load(); + } + + @Override + public boolean isApplicable(Class aClass) { + return true; + } + + + public FormValidation doCheckServerUrl(@QueryParameter String serverUrl) { + if (serverUrl.length() == 0) + return FormValidation.error("Please enter a server url"); + + if (serverUrl.indexOf("://") == -1) + return FormValidation.error("Enter a url of the format http(s)://"); + + return FormValidation.ok(); + } + + public FormValidation doTestConnection(@QueryParameter("serverUrl") final String serverUrl, + @QueryParameter("username") final String username, + @QueryParameter("password") final String password, + @QueryParameter("tenantName") final String tenantName) + throws IOException, ServletException { + try { + OpenstackClient client = new OpenstackClient(serverUrl, + username, + password, + tenantName); + if (client.authenticate()) { + return FormValidation.ok("Success"); + } else { + return FormValidation.error("Unable to connect to server. Please check credentials"); + } + } catch (Exception e) { + return FormValidation.error("Error: "+e.getMessage()); + } + } + + public ListBoxModel doFillFlavorItems(@QueryParameter("serverUrl") final String serverUrl, + @QueryParameter("username") final String username, + @QueryParameter("password") final String password, + @QueryParameter("tenantName") final String tenantName) { + ListBoxModel flavor = new ListBoxModel(); + OpenstackClient client = new OpenstackClient(serverUrl, + username, + password, + tenantName); + client.authenticate(); // Authenticate to Openstack + if (client.getOSClient() != null) { + for (String fl: client.getFlavors()) { + flavor.add(fl); + } + } + return flavor; + } + + public ListBoxModel doFillKeypairsItems(@QueryParameter("serverUrl") final String serverUrl, + @QueryParameter("username") final String username, + @QueryParameter("password") final String password, + @QueryParameter("tenantName") final String tenantName) { + ListBoxModel keypairs = new ListBoxModel(); + OpenstackClient client = new OpenstackClient(serverUrl, + username, + password, + tenantName); + client.authenticate(); // Authenticate to Openstack + if (client.getOSClient() != null) { + for (String fl: client.getKeypairs()) { + keypairs.add(fl); + } + } + return keypairs; + } + + /** + * This human readable name is used in the configuration screen. + */ + public String getDisplayName() { + return "Murano Kubernetes Plugin"; + } + } +} + diff --git a/kubernetes/src/main/java/com/mirantis/plugins/murano/client/OpenstackClient.java b/kubernetes/src/main/java/com/mirantis/plugins/murano/client/OpenstackClient.java new file mode 100644 index 0000000..f640fd3 --- /dev/null +++ b/kubernetes/src/main/java/com/mirantis/plugins/murano/client/OpenstackClient.java @@ -0,0 +1,94 @@ +package com.mirantis.plugins.murano.client; + +import org.openstack4j.api.OSClient; +import org.openstack4j.model.compute.Flavor; +import org.openstack4j.model.compute.Keypair; +import org.openstack4j.openstack.OSFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Client Class to talk to Openstack/Murano APIs + */ +public class OpenstackClient { + + private final static Logger LOG = Logger.getLogger(OpenstackClient.class.getName()); + private String serverUrl; + private String username; + private String password; + private String tenantName; + + private OSClient.OSClientV2 os = null; + + public OpenstackClient(String serverUrl, + String username, + String password, + String tenantName) { + this.serverUrl = serverUrl; + this.username = username; + this.password = password; + this.tenantName = tenantName; + } + + /** + * Authenticate to the Openstack instance given the credentials in constructor + * @return whether the auth was successful + */ + public boolean authenticate() { + boolean success = false; + try { + this.os = OSFactory.builderV2() + .endpoint(this.serverUrl + ":5000/v2.0") + .credentials(this.username, this.password) + .tenantName(this.tenantName) + .authenticate(); + success = true; + } catch(Exception ex) { + LOG.log(Level.SEVERE, "Error connecting to Client", ex); + success = false; + } + return success; + } + + /** + * Get all the Flavors the user has access to + * @return A List of Flavor objects + */ + public ArrayList getFlavors() { + ArrayList flavors = new ArrayList(); + if (this.os != null) { + List flavorsList = this.os.compute().flavors().list(); + for (Flavor f : flavorsList) { + flavors.add(f.getName()); + } + } + return flavors; + + } + + /** + * Get all the Keypairs the user has access to + * @return A List of Keypair object + */ + public ArrayList getKeypairs() { + ArrayList keypairs = new ArrayList(); + if (this.os != null) { + List kpList = this.os.compute().keypairs().list(); + for (Keypair k : kpList) { + keypairs.add(k.getName()); + } + } + return keypairs; + } + + /** + * Helper object to return the OSClient + * @return OSClient V2 + */ + public OSClient.OSClientV2 getOSClient() { + return this.os; + } +} diff --git a/kubernetes/src/main/resources/com/mirantis/plugins/murano/MuranoBuilder/config.jelly b/kubernetes/src/main/resources/com/mirantis/plugins/murano/MuranoBuilder/config.jelly new file mode 100644 index 0000000..584af7e --- /dev/null +++ b/kubernetes/src/main/resources/com/mirantis/plugins/murano/MuranoBuilder/config.jelly @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kubernetes/src/main/resources/index.jelly b/kubernetes/src/main/resources/index.jelly new file mode 100644 index 0000000..0361d55 --- /dev/null +++ b/kubernetes/src/main/resources/index.jelly @@ -0,0 +1,7 @@ + + +
+ This plugin is a sample to explain how to write a Jenkins plugin. +