From 36fa649c8da1d0d74db04350b6894abcc4028e73 Mon Sep 17 00:00:00 2001 From: Matt Overstreet Date: Wed, 18 May 2016 09:15:09 -0400 Subject: [PATCH 01/29] Initial support for testing More to come --- pom.xml | 13 ++++ .../kubernetes/KubernetesConfiguration.java | 15 +++- .../grandcentral/kubernetes/PodManager.java | 3 +- .../kubernetes/PodManagerTest.java | 77 +++++++++++++++++++ 4 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 src/test/com/o19s/grandcentral/kubernetes/PodManagerTest.java diff --git a/pom.xml b/pom.xml index dd6a3dc..46e89b5 100644 --- a/pom.xml +++ b/pom.xml @@ -12,6 +12,7 @@ 0.9.1 9.2.13.v20150730 + 4.12 @@ -30,6 +31,18 @@ jetty-proxy ${jetty.version} + + junit + junit + test + ${junit.version} + + + com.github.tomakehurst + wiremock + 1.58 + + diff --git a/src/main/java/com/o19s/grandcentral/kubernetes/KubernetesConfiguration.java b/src/main/java/com/o19s/grandcentral/kubernetes/KubernetesConfiguration.java index c7a2cfb..098adf0 100644 --- a/src/main/java/com/o19s/grandcentral/kubernetes/KubernetesConfiguration.java +++ b/src/main/java/com/o19s/grandcentral/kubernetes/KubernetesConfiguration.java @@ -2,8 +2,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import io.dropwizard.jackson.JsonSnakeCase; -import io.dropwizard.setup.Environment; -import org.apache.http.client.HttpClient; import org.hibernate.validator.constraints.NotEmpty; import javax.validation.constraints.NotNull; @@ -27,6 +25,9 @@ public class KubernetesConfiguration { @NotEmpty private String namespace; + // this can be null + private String protocol; + @JsonProperty public String getMasterIp() { return masterIp; @@ -42,6 +43,16 @@ public String getUsername() { return username; } + @JsonProperty + public String getProtocol() { + return (protocol == null) ? "https" : protocol; + } + + @JsonProperty + public void setProtocol(String protocol) { + this.protocol = protocol; + } + @JsonProperty public void setUsername(String username) { this.username = username; diff --git a/src/main/java/com/o19s/grandcentral/kubernetes/PodManager.java b/src/main/java/com/o19s/grandcentral/kubernetes/PodManager.java index fa3f567..fbebed2 100644 --- a/src/main/java/com/o19s/grandcentral/kubernetes/PodManager.java +++ b/src/main/java/com/o19s/grandcentral/kubernetes/PodManager.java @@ -75,6 +75,7 @@ public class PodManager { * @param keystorePath Path to the Java Keystore containing trusted certificates * @param maximumPodCount Maximum number of pods to ever have running at once * @param refreshIntervalInMs Interval with which to refresh the pods + * @param podYamlPath the location of the yaml config for the application pod */ public PodManager(KubernetesConfiguration k8sConfiguration, String keystorePath, @@ -343,7 +344,7 @@ else if (left.getLastRequest() < right.getLastRequest()) * @throws IOException */ private void refreshPods() throws IOException { - HttpGet podsGet = new HttpGet("https://" + k8sConfiguration.getMasterIp() + ":443/api/v1/namespaces/" + k8sConfiguration.getNamespace() + "/pods"); + HttpGet podsGet = new HttpGet(k8sConfiguration.getProtocol() + "://" + k8sConfiguration.getMasterIp() + "/api/v1/namespaces/" + k8sConfiguration.getNamespace() + "/pods"); try (CloseableHttpResponse response = httpClient.execute(podsGet, httpContext)) { HttpEntity entity = response.getEntity(); diff --git a/src/test/com/o19s/grandcentral/kubernetes/PodManagerTest.java b/src/test/com/o19s/grandcentral/kubernetes/PodManagerTest.java new file mode 100644 index 0000000..cce2480 --- /dev/null +++ b/src/test/com/o19s/grandcentral/kubernetes/PodManagerTest.java @@ -0,0 +1,77 @@ +package com.o19s.grandcentral.kubernetes; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; + +; +/** + * Created by Omnifroodle on 5/17/16. + */ +public class PodManagerTest { + private KubernetesConfiguration kubecfg; + @Rule + public WireMockRule wireMockRule = new WireMockRule(8888); + + @Before + public void setUp() throws Exception { + this.kubecfg = new KubernetesConfiguration(); + kubecfg.setMasterIp("127.0.0.1:8888"); + kubecfg.setProtocol("http"); + kubecfg.setNamespace("test"); + kubecfg.setUsername("fred"); + kubecfg.setPassword("flintstone"); + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void testGet() throws Exception { + + } + + @Test + public void testContains() throws Exception { + + } + + @Test + public void testAdd() throws Exception { + + } + + @Test + public void testRefreshPods() throws Exception { + stubFor(get(urlEqualTo("/api/v1/namespaces/test/pods")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/json") + .withBody("{\"items\":[" + + " {" + + " \"metadata\":{" + + " \"name\": \"abc\"" + + " }," + + " \"status\": {" + + " \"phase\": \"Running\"," + + " \"podIP\": \"1.1.1.1\"" + + " }" + + " }," + + " {" + + " \"metadata\":{" + + " \"name\": \"abcd\"" + + " }," + + " \"status\": {" + + " \"phase\": \"Running\"," + + " \"podIP\": \"1.1.1.2\"" + + " }" + + " }" + + " ]}"))); + + PodManager manager = new PodManager(this.kubecfg, "config/grandcentral.jks", 100, 1, "./config/configuration.yml"); + } +} \ No newline at end of file From 581ea2d1b7245b4aed146e3fa3a5e77cea418e2b Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Mon, 23 May 2016 15:07:16 -0400 Subject: [PATCH 02/29] move unit test to the src/test/java so it is properly picked up by Maven IDE generators --- .../com/o19s/grandcentral/kubernetes/PodManagerTest.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/test/{ => java}/com/o19s/grandcentral/kubernetes/PodManagerTest.java (100%) diff --git a/src/test/com/o19s/grandcentral/kubernetes/PodManagerTest.java b/src/test/java/com/o19s/grandcentral/kubernetes/PodManagerTest.java similarity index 100% rename from src/test/com/o19s/grandcentral/kubernetes/PodManagerTest.java rename to src/test/java/com/o19s/grandcentral/kubernetes/PodManagerTest.java From e8e11e32e304a385894b89111a89c129ec8b583f Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Mon, 23 May 2016 15:07:42 -0400 Subject: [PATCH 03/29] scope normally is the last attribute --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 46e89b5..c27f505 100644 --- a/pom.xml +++ b/pom.xml @@ -34,8 +34,8 @@ junit junit - test ${junit.version} + test com.github.tomakehurst @@ -92,4 +92,4 @@ - \ No newline at end of file + From eee6e6bcf9918d2ba761963ece723761cdffe544 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Mon, 23 May 2016 15:54:47 -0400 Subject: [PATCH 04/29] small fixes from running through the process. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 01da35a..cd3bc16 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,9 @@ The K8S cluster has a self-signed SSL certificate. It must be added to a keystor ``` brew install openssl -echo -n | /usr/local/Cellar/openssl/1.0.2e/bin/openssl s_client -connect :443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > config/local.pem +echo -n | /usr/local/Cellar/openssl/1.0.2g/bin/openssl s_client -connect :443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > config/local.pem keytool -importkeystore -srckeystore $JAVA_HOME/jre/lib/security/cacerts -destkeystore config/grandcentral.jks -srcstorepass changeit -deststorepass changeit -ho "yes" | keytool -import -v -trustcacerts -alias local_k8s -file k8s/local.pem -keystore config/grandcentral.jks -keypass changeit -storepass changeit +echo "yes" | keytool -import -v -trustcacerts -alias local_k8s -file config/local.pem -keystore config/grandcentral.jks -keypass changeit -storepass changeit ``` **Linux** @@ -74,7 +74,7 @@ ho "yes" | keytool -import -v -trustcacerts -alias local_k8s -file k8s/local.pem ``` echo -n | openssl s_client -connect 172.17.4.99:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > config/local.pem keytool -importkeystore -srckeystore $JAVA_HOME/jre/lib/security/cacerts -destkeystore config/grandcentral.jks -srcstorepass changeit -deststorepass changeit -echo "yes" | keytool -import -v -trustcacerts -alias local_k8s -file k8s/local.pem -keystore config/grandcentral.jks -keypass changeit -storepass changeit +echo "yes" | keytool -import -v -trustcacerts -alias local_k8s -file config/local.pem -keystore config/grandcentral.jks -keypass changeit -storepass changeit ``` ### Logging in to GCR.io From 74e8861339f1ead1937d9a0f640a86fb68b04da6 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Fri, 26 Aug 2016 17:22:00 -0400 Subject: [PATCH 05/29] eclipse friendly ignores --- .gitignore | 5 +++- README_dockercloud.md | 56 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 README_dockercloud.md diff --git a/.gitignore b/.gitignore index 28472d9..8e7f5bb 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,7 @@ dependency-reduced-pom.xml buildNumber.properties .mvn/timing.properties - +# Eclipse +.classpath +.settings +.project diff --git a/README_dockercloud.md b/README_dockercloud.md new file mode 100644 index 0000000..e9535b3 --- /dev/null +++ b/README_dockercloud.md @@ -0,0 +1,56 @@ +# Grand Central +*Quepid's automated review deployment tool. Gut-check in the cloud* + +Grand Central is a tool for automated deployment and cleanup of developer review environments / containers. Requests are parsed and routed based on their URL structure. If the target container exists the request is proxied along. If not Grand Central will spin up a container and forward the request once it comes online. + +## URL Structure +The appropriate container is determined by parsing the first part of the domain name. `*.review.quepid.com` in DNS is directed at the Grand Central service. The application parses the domain name to retrieve the appropriate Git version to deploy. `http://db139cf.review.quepid.com/secure` would route to a container running version `db139cf` if it exists. + +## Request Flow +When a request is received by the system the following processing takes place. + +1. Validate the git version supplied in the `Host` header. *Does it match a valid short hash signature?* +1. Verify if the version is currently running + * If so, proxy the request + * If not, continue +1. Verify version exists in Container Registry. *We can't deploy a version which doesn't exist. + * If so, continue + * If not, 404 +1. Create DockerCloud Stack (?) containing app version, database, and loader +1. Create Service for Stack (routes requests internally within the cluster) +1. Proxy the original request + +*Note* that all requests will have some metrics stored to determine activity for a give stack. This is useful when reaping old stacks. + +**BUT WAIT!** *What happens when two requests come in for the same version?* + +Easy, state is maintained within an Atomically accessed Map. As soon as a version is detected to not be running we instantiate it before releasing the lock. If a version exists, but isn't running we pause the request until the creation process is complete on another thread. + +## Stack Creation +Should a container not be running in the DockerCloud cluster when the request is performed the following process starts. + +1. Create a Stack with the following components (ERIC: IS THIS AT ALL RIGHT?) + * Rails Application + * MySQL Instance + * Data dump loader (golden review image stored block store) +1. Create a service referencing that stack + +## Stack Clean-up +Every hour a janitorial task runs which cleans up stacks / services that have not received a request in the past *x* seconds. This keeps cluster resources available. Since stacks are trivial to create they can be re-instantiated easily. + +## Resource Constraints +There is a hard limit to the number of simultaneous stacks running on the cluster. To prevent Denial of Service by our own team the maximum number of review environments is capped at *y*. Should a new stack be requested when the currently running count is already maxed the stack with the oldest most recent request will be removed. + +## Future Features +* Persistent hashes - the ability to mark a version as persistent. This prevents the janitor from reaping the pod. +* Admin interface - allow a RESTful interface to manage stacks. List all stacks, create a new one, delete an old one out of the janitorial process etc. +* Security?! + + +## curling + +curl --user username:apikey "https://cloud.docker.com/api/app/v1/stack/" + +## Implementation + +All logic for checking / creation of pods may be performed in a [`javax.servlet.Filter`](http://docs.oracle.com/javaee/7/api/javax/servlet/Filter.html?is-external=true). Requests may then be passed along to a [`org.eclipse.jetty.proxy.ProxyServlet`](http://download.eclipse.org/jetty/stable-9/apidocs/org/eclipse/jetty/proxy/ProxyServlet.html) after stack management is complete. From 9db597fe897fdd2273924c5aed5f5b6bd918c4ae Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Fri, 26 Aug 2016 17:23:43 -0400 Subject: [PATCH 06/29] rolled in basics of Docker Cloud support --- README.md | 13 + pom.xml | 20 + .../grandcentral/GrandCentralApplication.java | 19 +- .../GrandCentralApplication2.java | 75 +++ .../GrandCentralConfiguration.java | 10 +- .../GrandCentralConfiguration2.java | 141 +++++ .../com/o19s/grandcentral/ImageRegistry.java | 8 + .../dockercloud/DockercloudConfiguration.java | 82 +++ .../dockercloud/DockercloudRegistry.java | 22 + .../dockercloud/StackManager.java | 539 ++++++++++++++++++ .../gcloud/GCloudConfiguration.java | 3 +- .../grandcentral/gcloud/GCloudRegistry.java | 19 +- .../ContainerRegistryHealthCheck.java | 14 +- .../KubernetesMasterHealthCheck.java | 10 +- .../o19s/grandcentral/http/HttpDelete.java | 4 +- .../kubernetes/KubernetesConfiguration.java | 6 +- .../kubernetes/LinkedContainerManager.java | 27 + .../com/o19s/grandcentral/kubernetes/Pod.java | 4 +- .../grandcentral/kubernetes/PodManager.java | 70 +-- .../servlets/PodProxyServlet.java | 9 +- .../servlets/PodServletFilter.java | 26 +- .../dockercloud/DockercloudRegistryTest.java | 41 ++ .../kubernetes/PodManagerTest.java | 10 +- src/test/resources/local-dockercloud.yml | 12 + 24 files changed, 1099 insertions(+), 85 deletions(-) create mode 100644 src/main/java/com/o19s/grandcentral/GrandCentralApplication2.java create mode 100644 src/main/java/com/o19s/grandcentral/GrandCentralConfiguration2.java create mode 100644 src/main/java/com/o19s/grandcentral/ImageRegistry.java create mode 100644 src/main/java/com/o19s/grandcentral/dockercloud/DockercloudConfiguration.java create mode 100644 src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java create mode 100644 src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java create mode 100644 src/main/java/com/o19s/grandcentral/kubernetes/LinkedContainerManager.java create mode 100644 src/test/java/com/o19s/grandcentral/dockercloud/DockercloudRegistryTest.java create mode 100644 src/test/resources/local-dockercloud.yml diff --git a/README.md b/README.md index cd3bc16..7634fd6 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,19 @@ sudo route add -net 10.2.47 172.17.4.99 ## Local K8S Certificate The K8S cluster has a self-signed SSL certificate. It must be added to a keystore as a trusted certificate before requests are permitted. +To retrieve K8S master ip, assuming using GCP, you need to first get your username/password via: + +``` +gcloud container clusters describe hello-zeppelin --zone us-central1-f +``` + +Then you can look up the IP of the K8S dashboard via + +``` +kubectl cluster-info | grep kubernetes-dashboard +``` + + **OS X** ``` diff --git a/pom.xml b/pom.xml index c27f505..eb69d84 100644 --- a/pom.xml +++ b/pom.xml @@ -43,6 +43,26 @@ 1.58 + + + org.apache.commons + commons-exec + 1.3 + + + com.machinepublishers + jbrowserdriver + 0.16.4 + + + jaunt + jaunt + 1.2 + system + ${project.basedir}/lib/jaunt1.2.jar + + + diff --git a/src/main/java/com/o19s/grandcentral/GrandCentralApplication.java b/src/main/java/com/o19s/grandcentral/GrandCentralApplication.java index 1c0339d..e092064 100644 --- a/src/main/java/com/o19s/grandcentral/GrandCentralApplication.java +++ b/src/main/java/com/o19s/grandcentral/GrandCentralApplication.java @@ -1,17 +1,20 @@ package com.o19s.grandcentral; +import io.dropwizard.Application; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; + +import java.util.EnumSet; + +import javax.servlet.DispatcherType; + import com.o19s.grandcentral.gcloud.GCloudRegistry; import com.o19s.grandcentral.healthchecks.ContainerRegistryHealthCheck; import com.o19s.grandcentral.healthchecks.KubernetesMasterHealthCheck; import com.o19s.grandcentral.kubernetes.PodManager; +import com.o19s.grandcentral.kubernetes.LinkedContainerManager; import com.o19s.grandcentral.servlets.PodProxyServlet; import com.o19s.grandcentral.servlets.PodServletFilter; -import io.dropwizard.Application; -import io.dropwizard.setup.Bootstrap; -import io.dropwizard.setup.Environment; - -import javax.servlet.DispatcherType; -import java.util.EnumSet; public class GrandCentralApplication extends Application { public static void main(String[] args) throws Exception { @@ -44,7 +47,7 @@ public void run(GrandCentralConfiguration config, Environment environment) throw config.getKubernetesConfiguration().getNamespace())); // Build the PodManager - PodManager podManager = new PodManager( + LinkedContainerManager podManager = new PodManager( config.getKubernetesConfiguration(), config.getKeystorePath(), config.getRefreshIntervalInMs(), @@ -52,7 +55,7 @@ public void run(GrandCentralConfiguration config, Environment environment) throw config.getPodYamlPath() ); - GCloudRegistry gCloudRegistry = new GCloudRegistry(config.getGCloudConfiguration(), config.getKeystorePath()); + ImageRegistry gCloudRegistry = new GCloudRegistry(config.getGCloudConfiguration(), config.getKeystorePath()); // Define the filter and proxy final PodServletFilter psv = new PodServletFilter(config.getGrandcentralDomain(), podManager, gCloudRegistry); diff --git a/src/main/java/com/o19s/grandcentral/GrandCentralApplication2.java b/src/main/java/com/o19s/grandcentral/GrandCentralApplication2.java new file mode 100644 index 0000000..c716a0a --- /dev/null +++ b/src/main/java/com/o19s/grandcentral/GrandCentralApplication2.java @@ -0,0 +1,75 @@ +package com.o19s.grandcentral; + +import io.dropwizard.Application; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; + +import java.util.EnumSet; + +import javax.servlet.DispatcherType; + +import com.o19s.grandcentral.dockercloud.DockercloudRegistry; +import com.o19s.grandcentral.dockercloud.StackManager; +import com.o19s.grandcentral.kubernetes.LinkedContainerManager; +import com.o19s.grandcentral.servlets.PodProxyServlet; +import com.o19s.grandcentral.servlets.PodServletFilter; +//import com.o19s.grandcentral.healthchecks.ContainerRegistryHealthCheck; +//import com.o19s.grandcentral.healthchecks.KubernetesMasterHealthCheck; + +public class GrandCentralApplication2 extends Application { + public static void main(String[] args) throws Exception { + new GrandCentralApplication2().run(args); + } + + @Override + public String getName() { + return "Grand Central"; + } + + @Override + public void initialize(Bootstrap bootstrap) {} + + @Override + public void run(GrandCentralConfiguration2 config, Environment environment) throws Exception { + // Add health checks + /* + environment.healthChecks().register("container_registry", new ContainerRegistryHealthCheck( + config.getKeystorePath(), + config.getGCloudConfiguration().getRegistryDomain(), + config.getGCloudConfiguration().getProject(), + config.getGCloudConfiguration().getContainerName(), + config.getGCloudConfiguration().getRegistryUsername(), + config.getGCloudConfiguration().getRegistryPassword())); + environment.healthChecks().register("kubernetes_master", new KubernetesMasterHealthCheck( + config.getKubernetesConfiguration().getMasterIp(), + config.getKeystorePath(), + config.getKubernetesConfiguration().getUsername(), + config.getKubernetesConfiguration().getPassword(), + config.getKubernetesConfiguration().getNamespace())); + */ + + // Build the StackManager + LinkedContainerManager stuffManagerInterfaceNeedBetterName = new StackManager( + config.getDockercloudConfiguration(), + config.getRefreshIntervalInMs(), + config.getMaximumStackCount() + ); + + + ImageRegistry gCloudRegistry = new DockercloudRegistry(config.getDockercloudConfiguration()); + + + // Define the filter and proxy + final PodServletFilter psv = new PodServletFilter(config.getGrandcentralDomain(), stuffManagerInterfaceNeedBetterName, gCloudRegistry); + final PodProxyServlet pps = new PodProxyServlet(config.getPodPort()); + + // Disable Jersey in the proxy environment + environment.jersey().disable(); + + // Setup Servlet filters and proxies + environment.servlets().addFilter("Pod Servlet Filter", psv) + .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*"); + environment.servlets().addServlet("Pod Proxy Servlet", pps) + .addMapping("/*"); + } +} diff --git a/src/main/java/com/o19s/grandcentral/GrandCentralConfiguration.java b/src/main/java/com/o19s/grandcentral/GrandCentralConfiguration.java index a519555..892c6ce 100644 --- a/src/main/java/com/o19s/grandcentral/GrandCentralConfiguration.java +++ b/src/main/java/com/o19s/grandcentral/GrandCentralConfiguration.java @@ -1,15 +1,17 @@ package com.o19s.grandcentral; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.o19s.grandcentral.gcloud.GCloudConfiguration; -import com.o19s.grandcentral.kubernetes.KubernetesConfiguration; import io.dropwizard.Configuration; import io.dropwizard.jackson.JsonSnakeCase; -import org.hibernate.validator.constraints.NotEmpty; import javax.validation.Valid; import javax.validation.constraints.NotNull; +import org.hibernate.validator.constraints.NotEmpty; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.o19s.grandcentral.gcloud.GCloudConfiguration; +import com.o19s.grandcentral.kubernetes.KubernetesConfiguration; + /** * Configuration values for the {@link GrandCentralApplication}. Data is loaded from a provided YAML file on start or * passed in via Environment variables. diff --git a/src/main/java/com/o19s/grandcentral/GrandCentralConfiguration2.java b/src/main/java/com/o19s/grandcentral/GrandCentralConfiguration2.java new file mode 100644 index 0000000..a76331b --- /dev/null +++ b/src/main/java/com/o19s/grandcentral/GrandCentralConfiguration2.java @@ -0,0 +1,141 @@ +package com.o19s.grandcentral; + +import io.dropwizard.Configuration; +import io.dropwizard.jackson.JsonSnakeCase; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + +import org.hibernate.validator.constraints.NotEmpty; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.o19s.grandcentral.dockercloud.DockercloudConfiguration; + +/** + * Configuration values for the {@link GrandCentralApplication}. Data is loaded from a provided YAML file on start or + * passed in via Environment variables. + */ +@JsonSnakeCase +public class GrandCentralConfiguration2 extends Configuration { + @Valid + @NotNull + private long janitorCleanupThreshold; + + @Valid + @NotNull + private int maximumStackCount; + + @Valid + @NotEmpty + @NotNull + private String grandcentralDomain; + + @Valid + @NotNull + private long refreshIntervalInMs; + + @Valid + @NotNull + private int podPort; + + + + + @Valid + @NotNull + private DockercloudConfiguration dockercloud = new DockercloudConfiguration(); + + /* + @Valid + @NotNull + private KubernetesConfiguration kubernetes = new KubernetesConfiguration(); + + @Valid + @NotNull + private GCloudConfiguration gcloud = new GCloudConfiguration(); +*/ + @JsonProperty + public long getJanitorCleanupThreshold() { + return janitorCleanupThreshold; + } + + @JsonProperty + public void setJanitorCleanupThreshold(long janitorCleanupThreshold) { + this.janitorCleanupThreshold = janitorCleanupThreshold; + } + + + @JsonProperty + public int getMaximumStackCount() { + return maximumStackCount; + } + + @JsonProperty + public void setMaximumStackCount(int maximumStackCount) { + this.maximumStackCount = maximumStackCount; + } + + @JsonProperty + public String getGrandcentralDomain() { + return grandcentralDomain; + } + + @JsonProperty + public void setGrandcentralDomain(String grandcentralDomain) { + this.grandcentralDomain = grandcentralDomain; + } + + @JsonProperty + public long getRefreshIntervalInMs() { + return refreshIntervalInMs; + } + + + @JsonProperty + public void setRefreshIntervalInMs(long refreshIntervalInMs) { + this.refreshIntervalInMs = refreshIntervalInMs; + } + + +/* + @JsonProperty + public int getPodPort() { + return podPort; + } + + @JsonProperty + public void setPodPort(int podPort) { + this.podPort = podPort; + } +*/ + @JsonProperty("dockercloud") + public DockercloudConfiguration getDockercloudConfiguration() { + return dockercloud; + } + + @JsonProperty("dockercloud") + public void setDockercloudConfiguration(DockercloudConfiguration factory) { + this.dockercloud = factory; + } +/* + @JsonProperty("gcloud") + public GCloudConfiguration getGCloudConfiguration() { + return gcloud; + } + + @JsonProperty("gcloud") + public void setGCloudConfiguration(GCloudConfiguration gcloud) { + this.gcloud = gcloud; + } + */ + + @JsonProperty + public int getPodPort() { + return podPort; + } + + @JsonProperty + public void setPodPort(int podPort) { + this.podPort = podPort; + } +} diff --git a/src/main/java/com/o19s/grandcentral/ImageRegistry.java b/src/main/java/com/o19s/grandcentral/ImageRegistry.java new file mode 100644 index 0000000..3ad676a --- /dev/null +++ b/src/main/java/com/o19s/grandcentral/ImageRegistry.java @@ -0,0 +1,8 @@ +package com.o19s.grandcentral; + +public interface ImageRegistry { + + public boolean imageExistsInRegistry(String dockerTag) + throws Exception; + +} \ No newline at end of file diff --git a/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudConfiguration.java b/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudConfiguration.java new file mode 100644 index 0000000..5f45839 --- /dev/null +++ b/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudConfiguration.java @@ -0,0 +1,82 @@ +package com.o19s.grandcentral.dockercloud; + +import io.dropwizard.jackson.JsonSnakeCase; + +import javax.validation.constraints.NotNull; + +import org.hibernate.validator.constraints.NotEmpty; + +import com.fasterxml.jackson.annotation.JsonProperty; + + +@JsonSnakeCase +public class DockercloudConfiguration { + @NotNull + @NotEmpty + private String hostname; + + @NotNull + @NotEmpty + private String username; + + @NotNull + @NotEmpty + private String apikey; + + @NotNull + @NotEmpty + private String namespace; + + // this can be null + private String protocol; + + @JsonProperty + public String getHostname() { + return hostname; + } + + @JsonProperty + public void setHostname(String masterIp) { + this.hostname = masterIp; + } + + @JsonProperty + public String getUsername() { + return username; + } + + @JsonProperty + public String getProtocol() { + return (protocol == null) ? "https" : protocol; + } + + @JsonProperty + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + @JsonProperty + public void setUsername(String username) { + this.username = username; + } + + @JsonProperty + public String getApikey() { + return apikey; + } + + @JsonProperty + public void setApikey(String apikey) { + this.apikey = apikey; + } + + @JsonProperty + public String getNamespace() { + return namespace; + } + + @JsonProperty + public void setNamespace(String namespace) { + this.namespace = namespace; + } +} diff --git a/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java b/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java new file mode 100644 index 0000000..2b21cff --- /dev/null +++ b/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java @@ -0,0 +1,22 @@ +package com.o19s.grandcentral.dockercloud; + +import com.o19s.grandcentral.ImageRegistry; + +public class DockercloudRegistry implements ImageRegistry { + + public DockercloudRegistry(DockercloudConfiguration dockercloudConfiguration) { + // TODO Auto-generated constructor stub + } + + @Override + public boolean imageExistsInRegistry(String dockerTag) throws Exception { + + if (dockerTag.equals("v1")){ + return true; + } + else { + return false; + } + } + +} diff --git a/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java b/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java new file mode 100644 index 0000000..eb383bc --- /dev/null +++ b/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java @@ -0,0 +1,539 @@ +package com.o19s.grandcentral.dockercloud; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpStatus; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.auth.BasicScheme; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.joda.time.DateTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.o19s.grandcentral.http.HttpDelete; // IMPORTANT, allows DELETE requests with bodies +import com.o19s.grandcentral.kubernetes.Pod; +import com.o19s.grandcentral.kubernetes.LinkedContainerManager; + +/** + * Manages all stacks present within a namespace + * + * Currently reusing the K8N Pod object. + * Should be implementing some sort of interface along with PodManager! + * Using the word Stack and Pod interchangable right now! + */ +public class StackManager implements LinkedContainerManager { + private static final Logger LOGGER = LoggerFactory.getLogger(StackManager.class); + + private long lastRefresh; + private static final Map pods = new HashMap<>(); + + private DockercloudConfiguration dockercloudConfiguration; + + private long refreshIntervalInMs; + private int maximumPodCount; + + private CloseableHttpClient httpClient; + private HttpClientContext httpContext; + + private final JsonFactory jsonFactory = new JsonFactory(); + private final YAMLFactory yamlFactory = new YAMLFactory(); + private final ObjectMapper jsonObjectMapper = new ObjectMapper(jsonFactory); + private final ObjectMapper yamlObjectMapper = new ObjectMapper(yamlFactory); + private final ObjectNode podDefinition = null; + + static final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(true); + static final Lock readLock = readWriteLock.readLock(); + static final Lock writeLock = readWriteLock.writeLock(); + + /** + * Instantiates a new manages with the specified settings + * @param k8sConfiguration Kubernetes Configuration + * @param keystorePath Path to the Java Keystore containing trusted certificates + * @param maximumPodCount Maximum number of pods to ever have running at once + * @param refreshIntervalInMs Interval with which to refresh the pods + * @param podYamlPath the location of the yaml config for the application pod + */ + public StackManager(DockercloudConfiguration dockercloudConfiguration, + + long refreshIntervalInMs, + int maximumPodCount) throws IOException { + lastRefresh = 0; + + this.dockercloudConfiguration = dockercloudConfiguration; + + this.refreshIntervalInMs = refreshIntervalInMs; + this.maximumPodCount = maximumPodCount; + + // podDefinition = jsonObjectMapper.createObjectNode(); +// podDefinition.setAll((ObjectNode) yamlObjectMapper.readTree(new File(podYamlPath))); + + LOGGER.info("Loaded Pod Definition: " /*+ podDefinition*/); + + try { + // Setup SSL and plain connection socket factories +/* SSLContext sslContext = null; SSLContexts.custom() + .loadTrustMaterial(new File(keystorePath), "changeit".toCharArray()) + .build(); + */ + + // LayeredConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext); + // PlainConnectionSocketFactory plainsf = PlainConnectionSocketFactory.getSocketFactory(); +/* + Registry r = RegistryBuilder.create() + .register("http", plainsf) + .register("https", sslsf) + .build(); + HttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(r); + */ + + // Build the HTTP Client + httpClient = HttpClients.createDefault(); +// httpClient = HttpClients.custom() + // .setConnectionManager(cm) + // .build(); + + // Configure K8S HTTP Context (Authentication) + httpContext = HttpClientContext.create(); + CredentialsProvider dockercloudCredentialsProvider = new BasicCredentialsProvider(); + dockercloudCredentialsProvider.setCredentials( + new AuthScope(dockercloudConfiguration.getHostname(), 80), + new UsernamePasswordCredentials(dockercloudConfiguration.getUsername(), dockercloudConfiguration.getApikey())); + httpContext.setCredentialsProvider(dockercloudCredentialsProvider); + + } catch (Exception e) { + LOGGER.error("Error configuring HTTP clients", e); + } + + // Initial loading of pod information + refreshPods(); + } + + /** + * Get pod information for the given name + * @param dockerTag Git hash / name of the pod to return + * @return The pod which matches the given key. + */ + public Pod get(String dockerTag) throws IOException { + Pod pod = null; + + // Force a refresh of the data from K8S if the interval has passed + if (DateTime.now().getMillis() - lastRefresh > refreshIntervalInMs) { + refreshPods(); + } + + + if (contains(dockerTag)) { + readLock.lock(); + try { + pod = pods.get(dockerTag); + } finally { + readLock.unlock(); + } + } + + return pod; + } + + /** + * Does the provided dockerTag currently exist within the cluster + * @param dockerTag Git hash / name of the pod to check + * @return True if the pod exists + */ + public Boolean contains(String dockerTag) { + readLock.lock(); + boolean contains = false; + + try { + contains = pods.containsKey(dockerTag); + } finally { + readLock.unlock(); + } + + return contains; + } + + /** + * Adds a pod with the docker tag + * @param dockerTag Git hash / name of the pod to deploy + */ + public Pod add(String dockerTag) throws Exception { + if (!contains(dockerTag)) { + Pod pod = null; + + // Get the read lock + readLock.lock(); + + try { + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + String s = String.join("\n" + , "{" + , " \"name\": \"" + dockercloudConfiguration.getNamespace() + "-" + dockerTag + "\"," + , " \"services\": [" + , " {" + , " \"name\":\"hello-world\"," + , " \"image\":\"dep4b/datastart:v1\"," + ," \"ports\": [" + , " \"81:8080\"" + , " ]" + , " }" + , " ]" + , "}" + ); + + baos.write(s.getBytes()); + + // Schedule the new Pod +/* + JsonGenerator generator = jsonFactory.createGenerator(baos); + + ObjectNode newPodDefinition = podDefinition.deepCopy(); + ((ObjectNode) newPodDefinition.get("metadata")).put("name", dockerTag); + String image; + for (JsonNode containerNode : newPodDefinition.get("spec").get("containers")) { + image = containerNode.get("image").asText(); + if (image.endsWith("__DOCKER_TAG__")) { + ((ObjectNode) containerNode).put("image", image.replace("__DOCKER_TAG__", dockerTag)); + } + } + + LOGGER.info("Generated definition for \"" + dockerTag + "\": " + newPodDefinition); + + generator.writeObject(newPodDefinition); + generator.flush(); + generator.close(); +*/ + HttpPost stackCreate = new HttpPost(dockercloudConfiguration.getProtocol() + "://" + dockercloudConfiguration.getHostname() + "/api/app/v1/stack/"); + stackCreate.addHeader("accept", "application/json"); + stackCreate.addHeader(BasicScheme.authenticate( + new UsernamePasswordCredentials(dockercloudConfiguration.getUsername(), dockercloudConfiguration.getApikey()), + "UTF-8", false)); + + + HttpEntity podJson = new ByteArrayEntity(baos.toByteArray()); + stackCreate.setEntity(podJson); + + String podUUID = null; + try (CloseableHttpResponse response = httpClient.execute(stackCreate)) { + int status = response.getStatusLine().getStatusCode(); + HttpEntity entity = response.getEntity(); + InputStream responseBody = entity.getContent(); + + JsonNode rootNode = jsonObjectMapper.readTree(responseBody); + JsonNode objectsNode = rootNode.get("objects"); + if (status == HttpStatus.SC_CREATED) { + LOGGER.info("Pod " + dockerTag + ": Scheduled"); + podUUID = rootNode.get("uuid").asText(); + + } else if (status == HttpStatus.SC_CONFLICT) { + LOGGER.info("Pod " + dockerTag + ": Already running"); + } else { + LOGGER.info("Pod " + dockerTag + ": Not scheduled (" + response.getStatusLine().toString() + ")"); + } + } catch (IOException ioe) { + LOGGER.error("Pod " + dockerTag + ": Error scheduling pod", ioe); + } + + // Wait until Pod is running + boolean podRunning = false; + boolean podCreated = true; + + podRunning = true; + // Here should be a check to see if it the stack was created, not sure how long it takes!!! + + + if (podCreated && podUUID != null){ + // Start the stack + HttpPost stackStart = new HttpPost(dockercloudConfiguration.getProtocol() + "://" + dockercloudConfiguration.getHostname() + "/api/app/v1/stack/" + podUUID + "/start/"); + stackStart.addHeader("accept", "application/json"); + stackStart.addHeader(BasicScheme.authenticate( + new UsernamePasswordCredentials(dockercloudConfiguration.getUsername(), dockercloudConfiguration.getApikey()), + "UTF-8", false)); + + try (CloseableHttpResponse response = httpClient.execute(stackStart)) { + int status = response.getStatusLine().getStatusCode(); + HttpEntity entity = response.getEntity(); + InputStream responseBody = entity.getContent(); + JsonNode rootNode = jsonObjectMapper.readTree(responseBody); + } + catch (IOException ioe) { + LOGGER.error("Pod " + dockerTag + ": Error scheduling pod", ioe); + } + + + } + + + + + do { + HttpGet stackStatus = new HttpGet(dockercloudConfiguration.getProtocol() + "://" + dockercloudConfiguration.getHostname() + "/api/app/v1/stack/" + podUUID); + stackStatus.addHeader("accept", "application/json"); + stackStatus.addHeader(BasicScheme.authenticate( + new UsernamePasswordCredentials(dockercloudConfiguration.getUsername(), dockercloudConfiguration.getApikey()), + "UTF-8", false)); + + LOGGER.info("Pod " + dockerTag + ": Waiting for start"); + Thread.sleep(1000); + + try (CloseableHttpResponse response = httpClient.execute(stackStatus)) { + HttpEntity entity = response.getEntity(); + try (InputStream responseBody = entity.getContent()) { + + + JsonNode rootNode = jsonObjectMapper.readTree(responseBody); + String name = rootNode.get("name").asText(); + String status = rootNode.get("state").asText(); + JsonNode servicesNode = rootNode.get("services"); + + + String serviceURI = servicesNode.get(0).asText(); + String publicDNS = getDNSForStack(serviceURI); + + pod = new Pod(dockerTag, publicDNS, status); + + podRunning = pod != null && pod.isRunning(); + } catch (IOException ioe) { + LOGGER.error("Pod " + dockerTag + ": Error getting DNS for pod", ioe); + } + } + } while (!podRunning); + + LOGGER.info("Pod " + dockerTag + ": Started"); + } finally { + readLock.unlock(); + } + + // Force a refresh of the pod list + refreshPods(); + + return pod; + } + + return null; + } + + /** + * Stops the pod containing the specified docker tag. Note this does not force a refresh of the pods state + * @param dockerTag + * @throws IOException + */ + private void remove(String dockerTag) throws IOException { + if (contains(dockerTag)) { + readLock.lock(); + + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = jsonFactory.createGenerator(baos); + + ObjectNode root = jsonObjectMapper.createObjectNode(); + root.put("gracePeriodSeconds", 0); + + generator.writeObject(root); + generator.flush(); + + HttpDelete podDelete = new HttpDelete("https://" /*+ k8sConfiguration.getMasterIp()*/ + ":443/api/v1/namespaces/" /*+ k8sConfiguration.getNamespace() */+ "/pods/" + dockerTag); + podDelete.setEntity(new ByteArrayEntity(baos.toByteArray())); + + try (CloseableHttpResponse response = httpClient.execute(podDelete, httpContext)) { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + LOGGER.info("Pod " + dockerTag + ": Removed"); + } else { + LOGGER.info("Pod " + dockerTag + ": Error removing pod (" + response.getStatusLine().toString() + ")"); + } + } catch (IOException ioe) { + LOGGER.error("Pod " + dockerTag + ": Error removing pod", ioe); + } + } finally { + readLock.unlock(); + } + } else { + throw new IllegalArgumentException("Pod doesn't exist"); + } + } + + /** + * Removes the oldest running pods until maximumPodCount is reached + * @throws IOException + */ + private void removeExtraPods() throws IOException { + readLock.lock(); + if (pods.size() > maximumPodCount) { + readLock.unlock(); + writeLock.lock(); + + try { + // Check again since there was a time where we didn't have the lock + if (pods.size() > maximumPodCount) { + LOGGER.info("Removing extra pods"); + + // Determine the pods to remove + Pod[] sortedPodsByRequestAge = null; + sortedPodsByRequestAge = pods.values().toArray(new Pod[pods.size()]); + Arrays.sort( + sortedPodsByRequestAge, + (Pod left, Pod right) -> { + if (left.getLastRequest() > right.getLastRequest()) + return 1; + else if (left.getLastRequest() < right.getLastRequest()) + return -1; + else + return 0; + } + ); + + // Remove the pods + int amountToRemove = sortedPodsByRequestAge.length - maximumPodCount; + for (int i = 0; i < amountToRemove; i++) { + remove(sortedPodsByRequestAge[i].getDockerTag()); + } + } + } finally { + writeLock.unlock(); + } + + refreshPods(); + } else { + readLock.unlock(); + } + } + + /** + * Refreshes the internal map which tracks all running pods + * @throws IOException + */ + private void refreshPods() throws IOException { + HttpGet stacksGet = new HttpGet(dockercloudConfiguration.getProtocol() + "://" + dockercloudConfiguration.getHostname() + "/api/app/v1/stack/"); + stacksGet.addHeader("accept", "application/json"); + stacksGet.addHeader(BasicScheme.authenticate( + new UsernamePasswordCredentials(dockercloudConfiguration.getUsername(), dockercloudConfiguration.getApikey()), + "UTF-8", false)); + +// try (CloseableHttpResponse response = httpClient.execute(stacksGet, httpContext)) { + try (CloseableHttpResponse response = httpClient.execute(stacksGet)) { + + HttpEntity entity = response.getEntity(); + if (entity != null) { + // Grab the write lock + System.out.println("writelock:" + writeLock.toString()); + writeLock.lock(); + + try (InputStream responseBody = entity.getContent()) { + + JsonNode rootNode = jsonObjectMapper.readTree(responseBody); + JsonNode objectsNode = rootNode.get("objects"); + + // Update our internal pod hash + Set toDelete = new HashSet<>(pods.size()); + toDelete.addAll(pods.keySet()); + for (int i = 0; i < objectsNode.size(); i++) { + Pod pod = null; + String name = objectsNode.get(i).get("name").asText(); + String dockerTag = null; + String podName = name; + String state = objectsNode.get(i).get("state").asText(); + String servicesURI = objectsNode.get(i).get("services").get(0).asText(); + + if (name.indexOf("-")> -1){ + dockerTag = name.split("-")[1]; + } + + if (dockerTag != null && servicesURI != "" && podName.contains(dockercloudConfiguration.getNamespace())){ + + String publicDNS = getDNSForStack(servicesURI); + + pod = new Pod(dockerTag, publicDNS, state); + } + + + + if (pod != null && pod.isRunning()) { + // The pod is valid and should be managed + if (pods.containsKey(pod.getDockerTag())) { + LOGGER.info("Refresh: Updating pod " + pod.getDockerTag() + " in internal hash"); + + // Update the pod's address + pods.get(pod.getDockerTag()).setAddress(pod.getAddress()); + + // Remove the pending delete task for pods that exist + toDelete.remove(pod.getDockerTag()); + } else { + LOGGER.info("Refresh: Adding pod " + pod.getDockerTag() + " to internal hash"); + pods.put(pod.getDockerTag(), pod); + } + } + } + + // Delete pods that have been removed (delete refers to our hash, not k8s). This calls remove on the hash, not the manager. +// toDelete.forEach((dockerTag) -> LOGGER.info("Refresh: Removing pod " + dockerTag + " from internal hash")); +// toDelete.forEach(pods::remove); + } catch (IOException ioe) { + LOGGER.error("Pod Refresh: Error parsing pods", ioe); + } finally { + writeLock.unlock(); + } + } + } catch (IOException ioe) { + LOGGER.error("Pod Refresh: Error retrieving pods", ioe); + } + + // Cleanup old pods +// removeExtraPods(); + + // Update the lastRefresh time + lastRefresh = DateTime.now().getMillis(); + } + + private String getDNSForStack(String serviceURI){ + String publicDNS = null; + HttpGet stackServices = new HttpGet(dockercloudConfiguration.getProtocol() + "://" + dockercloudConfiguration.getHostname() + serviceURI); + stackServices.addHeader("accept", "application/json"); + stackServices.addHeader(BasicScheme.authenticate( + new UsernamePasswordCredentials(dockercloudConfiguration.getUsername(), dockercloudConfiguration.getApikey()), + "UTF-8", false)); + try (CloseableHttpResponse response2 = httpClient.execute(stackServices)) { + HttpEntity entity2 = response2.getEntity(); + try (InputStream responseBody2 = entity2.getContent()) { + + + JsonNode rootNode2 = jsonObjectMapper.readTree(responseBody2); + publicDNS = rootNode2.get("public_dns").asText(); + + } catch (IOException ioe) { + LOGGER.error("Pod at " + serviceURI + ": Error starting pod", ioe); + } + + + } catch (IOException ioe) { + LOGGER.error("Pod " + serviceURI + ": Error getting public dns for pod", ioe); + } + return publicDNS; + + } +} diff --git a/src/main/java/com/o19s/grandcentral/gcloud/GCloudConfiguration.java b/src/main/java/com/o19s/grandcentral/gcloud/GCloudConfiguration.java index f96ccb5..6a718b7 100644 --- a/src/main/java/com/o19s/grandcentral/gcloud/GCloudConfiguration.java +++ b/src/main/java/com/o19s/grandcentral/gcloud/GCloudConfiguration.java @@ -1,8 +1,9 @@ package com.o19s.grandcentral.gcloud; -import com.fasterxml.jackson.annotation.JsonProperty; import io.dropwizard.jackson.JsonSnakeCase; +import com.fasterxml.jackson.annotation.JsonProperty; + @JsonSnakeCase public class GCloudConfiguration { private String registryDomain; diff --git a/src/main/java/com/o19s/grandcentral/gcloud/GCloudRegistry.java b/src/main/java/com/o19s/grandcentral/gcloud/GCloudRegistry.java index 5d0d82b..5a79c0b 100644 --- a/src/main/java/com/o19s/grandcentral/gcloud/GCloudRegistry.java +++ b/src/main/java/com/o19s/grandcentral/gcloud/GCloudRegistry.java @@ -1,5 +1,11 @@ package com.o19s.grandcentral.gcloud; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import javax.net.ssl.SSLContext; + import org.apache.http.HttpStatus; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; @@ -20,15 +26,12 @@ import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.ssl.SSLContexts; -import javax.net.ssl.SSLContext; -import java.io.File; -import java.nio.charset.StandardCharsets; -import java.util.Base64; +import com.o19s.grandcentral.ImageRegistry; /** * Created by cbradford on 1/18/16. */ -public class GCloudRegistry { +public class GCloudRegistry implements ImageRegistry { private CloseableHttpClient httpClient; private HttpClientContext httpContext; private GCloudConfiguration config; @@ -68,7 +71,11 @@ public GCloudRegistry(GCloudConfiguration config, String keystorePath) throws Ex httpContext.setCredentialsProvider(gcloudCredentialsProvider); } - public boolean imageExistsInRegistry(String dockerTag) throws Exception { + /* (non-Javadoc) + * @see com.o19s.grandcentral.gcloud.ImageRegistry#imageExistsInRegistry(java.lang.String) + */ +@Override +public boolean imageExistsInRegistry(String dockerTag) throws Exception { // Verify the image is available from GCR.io HttpGet verificationGet = new HttpGet("https://" + config.getRegistryDomain() + ":443/v2/" + config.getProject() + "/" + config.getContainerName() + "/manifests/" + dockerTag); diff --git a/src/main/java/com/o19s/grandcentral/healthchecks/ContainerRegistryHealthCheck.java b/src/main/java/com/o19s/grandcentral/healthchecks/ContainerRegistryHealthCheck.java index cc923bf..827bf7c 100644 --- a/src/main/java/com/o19s/grandcentral/healthchecks/ContainerRegistryHealthCheck.java +++ b/src/main/java/com/o19s/grandcentral/healthchecks/ContainerRegistryHealthCheck.java @@ -1,6 +1,12 @@ package com.o19s.grandcentral.healthchecks; -import com.codahale.metrics.health.HealthCheck; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import javax.net.ssl.SSLContext; + import org.apache.http.HttpStatus; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; @@ -23,11 +29,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.net.ssl.SSLContext; -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Base64; +import com.codahale.metrics.health.HealthCheck; public class ContainerRegistryHealthCheck extends HealthCheck { private static final Logger LOGGER = LoggerFactory.getLogger(ContainerRegistryHealthCheck.class); diff --git a/src/main/java/com/o19s/grandcentral/healthchecks/KubernetesMasterHealthCheck.java b/src/main/java/com/o19s/grandcentral/healthchecks/KubernetesMasterHealthCheck.java index 883700a..752ebed 100644 --- a/src/main/java/com/o19s/grandcentral/healthchecks/KubernetesMasterHealthCheck.java +++ b/src/main/java/com/o19s/grandcentral/healthchecks/KubernetesMasterHealthCheck.java @@ -1,6 +1,10 @@ package com.o19s.grandcentral.healthchecks; -import com.codahale.metrics.health.HealthCheck; +import java.io.File; +import java.io.IOException; + +import javax.net.ssl.SSLContext; + import org.apache.http.HttpStatus; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; @@ -23,9 +27,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.net.ssl.SSLContext; -import java.io.File; -import java.io.IOException; +import com.codahale.metrics.health.HealthCheck; public class KubernetesMasterHealthCheck extends HealthCheck { private static final Logger LOGGER = LoggerFactory.getLogger(KubernetesMasterHealthCheck.class); diff --git a/src/main/java/com/o19s/grandcentral/http/HttpDelete.java b/src/main/java/com/o19s/grandcentral/http/HttpDelete.java index 9301640..93b8406 100644 --- a/src/main/java/com/o19s/grandcentral/http/HttpDelete.java +++ b/src/main/java/com/o19s/grandcentral/http/HttpDelete.java @@ -1,9 +1,9 @@ package com.o19s.grandcentral.http; -import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; - import java.net.URI; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; + /** * HTTP Delete request with a request body */ diff --git a/src/main/java/com/o19s/grandcentral/kubernetes/KubernetesConfiguration.java b/src/main/java/com/o19s/grandcentral/kubernetes/KubernetesConfiguration.java index 098adf0..afb09df 100644 --- a/src/main/java/com/o19s/grandcentral/kubernetes/KubernetesConfiguration.java +++ b/src/main/java/com/o19s/grandcentral/kubernetes/KubernetesConfiguration.java @@ -1,11 +1,13 @@ package com.o19s.grandcentral.kubernetes; -import com.fasterxml.jackson.annotation.JsonProperty; import io.dropwizard.jackson.JsonSnakeCase; -import org.hibernate.validator.constraints.NotEmpty; import javax.validation.constraints.NotNull; +import org.hibernate.validator.constraints.NotEmpty; + +import com.fasterxml.jackson.annotation.JsonProperty; + @JsonSnakeCase public class KubernetesConfiguration { diff --git a/src/main/java/com/o19s/grandcentral/kubernetes/LinkedContainerManager.java b/src/main/java/com/o19s/grandcentral/kubernetes/LinkedContainerManager.java new file mode 100644 index 0000000..9f0c037 --- /dev/null +++ b/src/main/java/com/o19s/grandcentral/kubernetes/LinkedContainerManager.java @@ -0,0 +1,27 @@ +package com.o19s.grandcentral.kubernetes; + +import java.io.IOException; + +public interface LinkedContainerManager { + + /** + * Get pod information for the given name + * @param dockerTag Git hash / name of the pod to return + * @return The pod which matches the given key. + */ + public abstract Pod get(String dockerTag) throws IOException; + + /** + * Does the provided dockerTag currently exist within the cluster + * @param dockerTag Git hash / name of the pod to check + * @return True if the pod exists + */ + public abstract Boolean contains(String dockerTag); + + /** + * Adds a pod with the docker tag + * @param dockerTag Git hash / name of the pod to deploy + */ + public abstract Pod add(String dockerTag) throws Exception; + +} \ No newline at end of file diff --git a/src/main/java/com/o19s/grandcentral/kubernetes/Pod.java b/src/main/java/com/o19s/grandcentral/kubernetes/Pod.java index ce8accf..2743cf3 100644 --- a/src/main/java/com/o19s/grandcentral/kubernetes/Pod.java +++ b/src/main/java/com/o19s/grandcentral/kubernetes/Pod.java @@ -1,9 +1,9 @@ package com.o19s.grandcentral.kubernetes; -import org.joda.time.DateTime; - import java.util.concurrent.atomic.AtomicLong; +import org.joda.time.DateTime; + /** * Represents a Kubernetes pod. */ diff --git a/src/main/java/com/o19s/grandcentral/kubernetes/PodManager.java b/src/main/java/com/o19s/grandcentral/kubernetes/PodManager.java index fbebed2..3db4818 100644 --- a/src/main/java/com/o19s/grandcentral/kubernetes/PodManager.java +++ b/src/main/java/com/o19s/grandcentral/kubernetes/PodManager.java @@ -1,18 +1,25 @@ package com.o19s.grandcentral.kubernetes; -import com.fasterxml.jackson.core.JsonFactory; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import javax.net.ssl.SSLContext; + import org.apache.http.HttpEntity; import org.apache.http.HttpStatus; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; import org.apache.http.client.methods.CloseableHttpResponse; -import com.o19s.grandcentral.http.HttpDelete; // IMPORTANT, allows DELETE requests with bodies import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.protocol.HttpClientContext; @@ -33,19 +40,18 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.net.ssl.SSLContext; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.*; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantReadWriteLock; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.o19s.grandcentral.http.HttpDelete; // IMPORTANT, allows DELETE requests with bodies /** * Manages all pods present within a namespace */ -public class PodManager { +public class PodManager implements LinkedContainerManager { private static final Logger LOGGER = LoggerFactory.getLogger(PodManager.class); private long lastRefresh; @@ -128,12 +134,11 @@ public PodManager(KubernetesConfiguration k8sConfiguration, refreshPods(); } - /** - * Get pod information for the given name - * @param dockerTag Git hash / name of the pod to return - * @return The pod which matches the given key. - */ - public Pod get(String dockerTag) throws IOException { + /* (non-Javadoc) + * @see com.o19s.grandcentral.kubernetes.StuffManagerInterfaceNeedBetterName#get(java.lang.String) + */ + @Override +public Pod get(String dockerTag) throws IOException { Pod pod = null; // Force a refresh of the data from K8S if the interval has passed @@ -154,12 +159,11 @@ public Pod get(String dockerTag) throws IOException { return pod; } - /** - * Does the provided dockerTag currently exist within the cluster - * @param dockerTag Git hash / name of the pod to check - * @return True if the pod exists - */ - public Boolean contains(String dockerTag) { + /* (non-Javadoc) + * @see com.o19s.grandcentral.kubernetes.StuffManagerInterfaceNeedBetterName#contains(java.lang.String) + */ + @Override +public Boolean contains(String dockerTag) { readLock.lock(); boolean contains = false; @@ -172,11 +176,11 @@ public Boolean contains(String dockerTag) { return contains; } - /** - * Adds a pod with the docker tag - * @param dockerTag Git hash / name of the pod to deploy - */ - public Pod add(String dockerTag) throws Exception { + /* (non-Javadoc) + * @see com.o19s.grandcentral.kubernetes.StuffManagerInterfaceNeedBetterName#add(java.lang.String) + */ + @Override +public Pod add(String dockerTag) throws Exception { if (!contains(dockerTag)) { Pod pod = null; diff --git a/src/main/java/com/o19s/grandcentral/servlets/PodProxyServlet.java b/src/main/java/com/o19s/grandcentral/servlets/PodProxyServlet.java index f5dba8c..bfac7ed 100644 --- a/src/main/java/com/o19s/grandcentral/servlets/PodProxyServlet.java +++ b/src/main/java/com/o19s/grandcentral/servlets/PodProxyServlet.java @@ -1,13 +1,14 @@ package com.o19s.grandcentral.servlets; -import org.eclipse.jetty.proxy.ProxyServlet; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import java.net.URI; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; -import java.net.URI; + +import org.eclipse.jetty.proxy.ProxyServlet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Proxies requests to Pods running within the Kubernetes cluster. diff --git a/src/main/java/com/o19s/grandcentral/servlets/PodServletFilter.java b/src/main/java/com/o19s/grandcentral/servlets/PodServletFilter.java index 0cea849..8555bd3 100644 --- a/src/main/java/com/o19s/grandcentral/servlets/PodServletFilter.java +++ b/src/main/java/com/o19s/grandcentral/servlets/PodServletFilter.java @@ -1,16 +1,22 @@ package com.o19s.grandcentral.servlets; -import com.o19s.grandcentral.gcloud.GCloudRegistry; -import com.o19s.grandcentral.kubernetes.Pod; -import com.o19s.grandcentral.kubernetes.PodManager; +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import org.eclipse.jetty.http.HttpStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.servlet.*; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; +import com.o19s.grandcentral.ImageRegistry; +import com.o19s.grandcentral.kubernetes.Pod; +import com.o19s.grandcentral.kubernetes.LinkedContainerManager; /** * Filter which drops requests that do not match the appropriate host header format. @@ -19,15 +25,15 @@ public class PodServletFilter implements javax.servlet.Filter { private static final Logger LOGGER = LoggerFactory.getLogger(PodServletFilter.class); private String grandCentralDomain; - private PodManager podManager; - private GCloudRegistry gCloudRegistry; + private LinkedContainerManager podManager; + private ImageRegistry gCloudRegistry; /** * * @param grandCentralDomain The domain grand central is running on. This helps determine the portion of the URL representing the Git hash. * @param podManager */ - public PodServletFilter(String grandCentralDomain, PodManager podManager, GCloudRegistry gCloudRegistry) { + public PodServletFilter(String grandCentralDomain, LinkedContainerManager podManager, ImageRegistry gCloudRegistry) { this.grandCentralDomain = grandCentralDomain; this.podManager = podManager; this.gCloudRegistry = gCloudRegistry; diff --git a/src/test/java/com/o19s/grandcentral/dockercloud/DockercloudRegistryTest.java b/src/test/java/com/o19s/grandcentral/dockercloud/DockercloudRegistryTest.java new file mode 100644 index 0000000..173d2a3 --- /dev/null +++ b/src/test/java/com/o19s/grandcentral/dockercloud/DockercloudRegistryTest.java @@ -0,0 +1,41 @@ +package com.o19s.grandcentral.dockercloud; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Ignore; +import org.junit.Test; + +import com.jaunt.UserAgent; +import com.jaunt.component.Label; + +public class DockercloudRegistryTest { + + + @Test + @Ignore + public void testCheck() throws Exception { + DockercloudRegistry dockercloudRegistry = new DockercloudRegistry(null); + + assertTrue(dockercloudRegistry.imageExistsInRegistry("dep4b/datastart:v1")); + assertFalse(dockercloudRegistry.imageExistsInRegistry("dep4b/datastart:v2")); + + } + @Test + @Ignore + public void testScrapeDockerHub() throws Exception { + + + + UserAgent userAgent = new UserAgent(); + userAgent.visit("https://hub.docker.com/login/"); + + userAgent.doc.fillout("FancyInput__default___1Iybp", "dep4b"); //fill out the component labelled 'Username:' with "tom" + userAgent.doc.fillout("FancyInput__error___TIz2p", "your password"); //fill out the component labelled 'Password:' with "secret" +// userAgent.doc.choose(Label.RIGHT, "Remember me");//choose the component right-labelled 'Remember me'. + userAgent.doc.submit(); //submit the form + System.out.println(userAgent.getLocation()); //print the current location (url) + + } + +} diff --git a/src/test/java/com/o19s/grandcentral/kubernetes/PodManagerTest.java b/src/test/java/com/o19s/grandcentral/kubernetes/PodManagerTest.java index cce2480..de1d2a5 100644 --- a/src/test/java/com/o19s/grandcentral/kubernetes/PodManagerTest.java +++ b/src/test/java/com/o19s/grandcentral/kubernetes/PodManagerTest.java @@ -1,11 +1,15 @@ package com.o19s.grandcentral.kubernetes; -import com.github.tomakehurst.wiremock.junit.WireMockRule; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; + import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import static com.github.tomakehurst.wiremock.client.WireMock.*; +import com.github.tomakehurst.wiremock.junit.WireMockRule; ; /** @@ -72,6 +76,6 @@ public void testRefreshPods() throws Exception { " }" + " ]}"))); - PodManager manager = new PodManager(this.kubecfg, "config/grandcentral.jks", 100, 1, "./config/configuration.yml"); + LinkedContainerManager manager = new PodManager(this.kubecfg, "config/grandcentral.jks", 100, 1, "./config/configuration.yml"); } } \ No newline at end of file diff --git a/src/test/resources/local-dockercloud.yml b/src/test/resources/local-dockercloud.yml new file mode 100644 index 0000000..d3a9299 --- /dev/null +++ b/src/test/resources/local-dockercloud.yml @@ -0,0 +1,12 @@ +janitor_cleanup_threshold: 3600 +maximum_stack_count: 2 +grandcentral_domain: datastart.gc.com +refresh_interval_in_ms: 300000 + +pod_port: 81 + +dockercloud: + hostname: cloud.docker.com + namespace: datastart + username: dep4b + apikey: YOUR_API_KEY From 0d092f6d398c8f9a543ae46a3bcaded48ea3d839 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Fri, 26 Aug 2016 17:24:58 -0400 Subject: [PATCH 07/29] move LinkedContainerManager interface to better home --- .../java/com/o19s/grandcentral/GrandCentralApplication.java | 1 - .../java/com/o19s/grandcentral/GrandCentralApplication2.java | 1 - .../grandcentral/{kubernetes => }/LinkedContainerManager.java | 4 +++- .../java/com/o19s/grandcentral/dockercloud/StackManager.java | 2 +- .../java/com/o19s/grandcentral/kubernetes/PodManager.java | 1 + .../java/com/o19s/grandcentral/servlets/PodServletFilter.java | 2 +- .../java/com/o19s/grandcentral/kubernetes/PodManagerTest.java | 1 + 7 files changed, 7 insertions(+), 5 deletions(-) rename src/main/java/com/o19s/grandcentral/{kubernetes => }/LinkedContainerManager.java (89%) diff --git a/src/main/java/com/o19s/grandcentral/GrandCentralApplication.java b/src/main/java/com/o19s/grandcentral/GrandCentralApplication.java index e092064..e6fe306 100644 --- a/src/main/java/com/o19s/grandcentral/GrandCentralApplication.java +++ b/src/main/java/com/o19s/grandcentral/GrandCentralApplication.java @@ -12,7 +12,6 @@ import com.o19s.grandcentral.healthchecks.ContainerRegistryHealthCheck; import com.o19s.grandcentral.healthchecks.KubernetesMasterHealthCheck; import com.o19s.grandcentral.kubernetes.PodManager; -import com.o19s.grandcentral.kubernetes.LinkedContainerManager; import com.o19s.grandcentral.servlets.PodProxyServlet; import com.o19s.grandcentral.servlets.PodServletFilter; diff --git a/src/main/java/com/o19s/grandcentral/GrandCentralApplication2.java b/src/main/java/com/o19s/grandcentral/GrandCentralApplication2.java index c716a0a..8e613d3 100644 --- a/src/main/java/com/o19s/grandcentral/GrandCentralApplication2.java +++ b/src/main/java/com/o19s/grandcentral/GrandCentralApplication2.java @@ -10,7 +10,6 @@ import com.o19s.grandcentral.dockercloud.DockercloudRegistry; import com.o19s.grandcentral.dockercloud.StackManager; -import com.o19s.grandcentral.kubernetes.LinkedContainerManager; import com.o19s.grandcentral.servlets.PodProxyServlet; import com.o19s.grandcentral.servlets.PodServletFilter; //import com.o19s.grandcentral.healthchecks.ContainerRegistryHealthCheck; diff --git a/src/main/java/com/o19s/grandcentral/kubernetes/LinkedContainerManager.java b/src/main/java/com/o19s/grandcentral/LinkedContainerManager.java similarity index 89% rename from src/main/java/com/o19s/grandcentral/kubernetes/LinkedContainerManager.java rename to src/main/java/com/o19s/grandcentral/LinkedContainerManager.java index 9f0c037..fd34164 100644 --- a/src/main/java/com/o19s/grandcentral/kubernetes/LinkedContainerManager.java +++ b/src/main/java/com/o19s/grandcentral/LinkedContainerManager.java @@ -1,7 +1,9 @@ -package com.o19s.grandcentral.kubernetes; +package com.o19s.grandcentral; import java.io.IOException; +import com.o19s.grandcentral.kubernetes.Pod; + public interface LinkedContainerManager { /** diff --git a/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java b/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java index eb383bc..56cdf46 100644 --- a/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java +++ b/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java @@ -35,9 +35,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.o19s.grandcentral.LinkedContainerManager; import com.o19s.grandcentral.http.HttpDelete; // IMPORTANT, allows DELETE requests with bodies import com.o19s.grandcentral.kubernetes.Pod; -import com.o19s.grandcentral.kubernetes.LinkedContainerManager; /** * Manages all stacks present within a namespace diff --git a/src/main/java/com/o19s/grandcentral/kubernetes/PodManager.java b/src/main/java/com/o19s/grandcentral/kubernetes/PodManager.java index 3db4818..8239df5 100644 --- a/src/main/java/com/o19s/grandcentral/kubernetes/PodManager.java +++ b/src/main/java/com/o19s/grandcentral/kubernetes/PodManager.java @@ -46,6 +46,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.o19s.grandcentral.LinkedContainerManager; import com.o19s.grandcentral.http.HttpDelete; // IMPORTANT, allows DELETE requests with bodies /** diff --git a/src/main/java/com/o19s/grandcentral/servlets/PodServletFilter.java b/src/main/java/com/o19s/grandcentral/servlets/PodServletFilter.java index 8555bd3..b74c37b 100644 --- a/src/main/java/com/o19s/grandcentral/servlets/PodServletFilter.java +++ b/src/main/java/com/o19s/grandcentral/servlets/PodServletFilter.java @@ -15,8 +15,8 @@ import org.slf4j.LoggerFactory; import com.o19s.grandcentral.ImageRegistry; +import com.o19s.grandcentral.LinkedContainerManager; import com.o19s.grandcentral.kubernetes.Pod; -import com.o19s.grandcentral.kubernetes.LinkedContainerManager; /** * Filter which drops requests that do not match the appropriate host header format. diff --git a/src/test/java/com/o19s/grandcentral/kubernetes/PodManagerTest.java b/src/test/java/com/o19s/grandcentral/kubernetes/PodManagerTest.java index de1d2a5..6914f39 100644 --- a/src/test/java/com/o19s/grandcentral/kubernetes/PodManagerTest.java +++ b/src/test/java/com/o19s/grandcentral/kubernetes/PodManagerTest.java @@ -10,6 +10,7 @@ import org.junit.Test; import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.o19s.grandcentral.LinkedContainerManager; ; /** From 3bab944bcc3a22857c249c600e63891b87fac1a4 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Fri, 26 Aug 2016 17:38:52 -0400 Subject: [PATCH 08/29] little bits of cleanup --- README_dockercloud.md | 18 ++++++++++++++---- .../grandcentral/GrandCentralApplication2.java | 6 +++--- src/test/resources/local-dockercloud.yml | 2 +- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/README_dockercloud.md b/README_dockercloud.md index e9535b3..f73e4f9 100644 --- a/README_dockercloud.md +++ b/README_dockercloud.md @@ -1,10 +1,13 @@ # Grand Central -*Quepid's automated review deployment tool. Gut-check in the cloud* +*Your automated review deployment tool. Gut-check in the cloud* Grand Central is a tool for automated deployment and cleanup of developer review environments / containers. Requests are parsed and routed based on their URL structure. If the target container exists the request is proxied along. If not Grand Central will spin up a container and forward the request once it comes online. ## URL Structure -The appropriate container is determined by parsing the first part of the domain name. `*.review.quepid.com` in DNS is directed at the Grand Central service. The application parses the domain name to retrieve the appropriate Git version to deploy. `http://db139cf.review.quepid.com/secure` would route to a container running version `db139cf` if it exists. + +URLs tells Grand Central how to route to the correct container. A url like `db139cf.datastart.grandcentral.com` in DNS is directed at the Grand Central service. Grand Central parses out the first part of the name, `db139cf` to retrieve the appropriate Git version to deploy. + +The appropriate container is determined by parsing the first part of the domain name. `*.review.grandcentral.com` in DNS is directed at the Grand Central service. The application parses the domain name to retrieve the appropriate Git version to deploy. `http://db139cf.review.quepid.com/secure` would route to a container running version `db139cf` if it exists. ## Request Flow When a request is received by the system the following processing takes place. @@ -16,8 +19,7 @@ When a request is received by the system the following processing takes place. 1. Verify version exists in Container Registry. *We can't deploy a version which doesn't exist. * If so, continue * If not, 404 -1. Create DockerCloud Stack (?) containing app version, database, and loader -1. Create Service for Stack (routes requests internally within the cluster) +1. Create DockerCloud Stack containing everything required. Stack's are named after the git version. 1. Proxy the original request *Note* that all requests will have some metrics stored to determine activity for a give stack. This is useful when reaping old stacks. @@ -51,6 +53,14 @@ There is a hard limit to the number of simultaneous stacks running on the cluste curl --user username:apikey "https://cloud.docker.com/api/app/v1/stack/" +## Local Development +Add something like below to your `/etc/hosts` file. This will let you simulate hitting a deployed GrandCentral +server, though only on the specified FQDN! + +``` +127.0.0.1 v1.datastart.grandcentral.com +``` + ## Implementation All logic for checking / creation of pods may be performed in a [`javax.servlet.Filter`](http://docs.oracle.com/javaee/7/api/javax/servlet/Filter.html?is-external=true). Requests may then be passed along to a [`org.eclipse.jetty.proxy.ProxyServlet`](http://download.eclipse.org/jetty/stable-9/apidocs/org/eclipse/jetty/proxy/ProxyServlet.html) after stack management is complete. diff --git a/src/main/java/com/o19s/grandcentral/GrandCentralApplication2.java b/src/main/java/com/o19s/grandcentral/GrandCentralApplication2.java index 8e613d3..33e2785 100644 --- a/src/main/java/com/o19s/grandcentral/GrandCentralApplication2.java +++ b/src/main/java/com/o19s/grandcentral/GrandCentralApplication2.java @@ -48,18 +48,18 @@ public void run(GrandCentralConfiguration2 config, Environment environment) thro */ // Build the StackManager - LinkedContainerManager stuffManagerInterfaceNeedBetterName = new StackManager( + LinkedContainerManager linkedContainerManager = new StackManager( config.getDockercloudConfiguration(), config.getRefreshIntervalInMs(), config.getMaximumStackCount() ); - ImageRegistry gCloudRegistry = new DockercloudRegistry(config.getDockercloudConfiguration()); + ImageRegistry imageRegistry = new DockercloudRegistry(config.getDockercloudConfiguration()); // Define the filter and proxy - final PodServletFilter psv = new PodServletFilter(config.getGrandcentralDomain(), stuffManagerInterfaceNeedBetterName, gCloudRegistry); + final PodServletFilter psv = new PodServletFilter(config.getGrandcentralDomain(), linkedContainerManager, imageRegistry); final PodProxyServlet pps = new PodProxyServlet(config.getPodPort()); // Disable Jersey in the proxy environment diff --git a/src/test/resources/local-dockercloud.yml b/src/test/resources/local-dockercloud.yml index d3a9299..10a8f20 100644 --- a/src/test/resources/local-dockercloud.yml +++ b/src/test/resources/local-dockercloud.yml @@ -1,6 +1,6 @@ janitor_cleanup_threshold: 3600 maximum_stack_count: 2 -grandcentral_domain: datastart.gc.com +grandcentral_domain: datastart.grandcentral.com refresh_interval_in_ms: 300000 pod_port: 81 From 217d49e5a79e0c341a38be249b80aef25b5640b6 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sat, 27 Aug 2016 10:09:31 -0400 Subject: [PATCH 09/29] now actually testing out a docker image to see if it is deployable or not cause we cant query the DockerCloud Hub for our images. Oh my eyes! --- .../dockercloud/DockercloudRegistry.java | 177 +++++++++++++++++- .../dockercloud/StackManager.java | 34 +--- .../dockercloud/DockercloudRegistryTest.java | 15 +- src/test/resources/local-dockercloud.yml | 1 + 4 files changed, 193 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java b/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java index 2b21cff..e3ece35 100644 --- a/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java +++ b/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java @@ -1,22 +1,189 @@ package com.o19s.grandcentral.dockercloud; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpStatus; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.auth.BasicScheme; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.o19s.grandcentral.ImageRegistry; public class DockercloudRegistry implements ImageRegistry { + private static final Logger LOGGER = LoggerFactory + .getLogger(DockercloudRegistry.class); + private DockercloudConfiguration dockercloudConfiguration; + private CloseableHttpClient httpClient; + private final JsonFactory jsonFactory = new JsonFactory(); + private final ObjectMapper jsonObjectMapper = new ObjectMapper(jsonFactory); public DockercloudRegistry(DockercloudConfiguration dockercloudConfiguration) { - // TODO Auto-generated constructor stub + this.dockercloudConfiguration = dockercloudConfiguration; + + httpClient = HttpClients.createDefault(); } @Override public boolean imageExistsInRegistry(String dockerTag) throws Exception { + boolean imageExists = false; + String containerName = null; + String podUUID = null; + + // Sometimes we hand in v1 and other times its dep4b/datastart:v1! + // Maybe a code smell? + if (dockerTag.indexOf(":") > -1) { + String[] parts = dockerTag.split(":"); + dockerTag = parts[1]; + containerName = parts[0]; + } else { + containerName = "dep4b/datastart"; + } + + String serviceName = "test-img-exst-" + containerName + "-" + dockerTag; + serviceName = serviceName.replace('/', '-'); + + String imageName = containerName + ":" + dockerTag; + + podUUID = createValidityCheckService(serviceName, imageName); - if (dockerTag.equals("v1")){ - return true; + imageExists = startService(podUUID); + + deleteService(podUUID); + + return imageExists; + } + + private String createValidityCheckService(String serviceName, String imageName){ + String podUUID = null; + try { + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + String s = String.join("\n", "{", " \"name\": \"" + serviceName + + "\",", " \"image\": \"" + imageName + "\"" + + ",", " \"run_command\": \"ls\"" + , "}"); + baos.write(s.getBytes()); + + HttpPost serviceCreate = new HttpPost( + dockercloudConfiguration.getProtocol() + "://" + + dockercloudConfiguration.getHostname() + + "/api/app/v1/service/"); + serviceCreate.addHeader("accept", "application/json"); + serviceCreate.addHeader(BasicScheme.authenticate( + new UsernamePasswordCredentials(dockercloudConfiguration + .getUsername(), dockercloudConfiguration + .getApikey()), "UTF-8", false)); + + HttpEntity podJson = new ByteArrayEntity(baos.toByteArray()); + serviceCreate.setEntity(podJson); + + try (CloseableHttpResponse response = httpClient + .execute(serviceCreate)) { + int status = response.getStatusLine().getStatusCode(); + HttpEntity entity = response.getEntity(); + InputStream responseBody = entity.getContent(); + + JsonNode rootNode = jsonObjectMapper.readTree(responseBody); + JsonNode objectsNode = rootNode.get("objects"); + if (status == HttpStatus.SC_CREATED) { + LOGGER.info("Pod " + serviceName + ": Scheduled"); + podUUID = rootNode.get("uuid").asText(); + + } else if (status == HttpStatus.SC_CONFLICT) { + LOGGER.info("Pod " + serviceName + ": Already running"); + } else { + LOGGER.info("Pod " + serviceName + ": Not scheduled (" + + response.getStatusLine().toString() + ")"); + } + } catch (IOException ioe) { + LOGGER.error("Pod " + serviceName + ": Error scheduling pod", ioe); + } + } catch (IOException ioe) { + LOGGER.error("Pod " + serviceName + ": Error scheduling pod", ioe); + } + return podUUID; + } + + private void deleteService(String podUUID) { + + HttpDelete serviceDelete = new HttpDelete( + dockercloudConfiguration.getProtocol() + "://" + + dockercloudConfiguration.getHostname() + + "/api/app/v1/service/" + podUUID + "/"); + serviceDelete.addHeader("accept", "application/json"); + serviceDelete.addHeader(BasicScheme.authenticate( + new UsernamePasswordCredentials(dockercloudConfiguration + .getUsername(), dockercloudConfiguration.getApikey()), + "UTF-8", false)); + + try (CloseableHttpResponse response = httpClient.execute(serviceDelete)) { + int status = response.getStatusLine().getStatusCode(); + HttpEntity entity = response.getEntity(); + InputStream responseBody = entity.getContent(); + + JsonNode rootNode = jsonObjectMapper.readTree(responseBody); + + if (status == HttpStatus.SC_ACCEPTED) { + LOGGER.info("Pod " + podUUID + ": Deleted"); + + } + + } catch (IOException ioe) { + LOGGER.error("Pod " + podUUID + ": Error pod", ioe); } - else { - return false; + + } + + private boolean startService(String podUUID) { + boolean result = false; + + HttpPost serviceStart = new HttpPost( + dockercloudConfiguration.getProtocol() + "://" + + dockercloudConfiguration.getHostname() + + "/api/app/v1/service/" + podUUID + "/start/"); + serviceStart.addHeader("accept", "application/json"); + serviceStart.addHeader(BasicScheme.authenticate( + new UsernamePasswordCredentials(dockercloudConfiguration + .getUsername(), dockercloudConfiguration.getApikey()), + "UTF-8", false)); + + try (CloseableHttpResponse response = httpClient.execute(serviceStart)) { + int status = response.getStatusLine().getStatusCode(); + HttpEntity entity = response.getEntity(); + InputStream responseBody = entity.getContent(); + + JsonNode rootNode = jsonObjectMapper.readTree(responseBody); + + if (status == HttpStatus.SC_ACCEPTED) { + LOGGER.info("Pod " + podUUID + ": Started"); + result = true; + + } + else if (status == HttpStatus.SC_BAD_REQUEST){ + LOGGER.info("Pod " + podUUID + ": attempted start, and image not found"); + result = false; + } + + } catch (IOException ioe) { + LOGGER.error("Pod " + podUUID + ": Error pod", ioe); } + + return result; + } } diff --git a/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java b/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java index 56cdf46..6203ff0 100644 --- a/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java +++ b/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java @@ -61,10 +61,8 @@ public class StackManager implements LinkedContainerManager { private HttpClientContext httpContext; private final JsonFactory jsonFactory = new JsonFactory(); - private final YAMLFactory yamlFactory = new YAMLFactory(); private final ObjectMapper jsonObjectMapper = new ObjectMapper(jsonFactory); - private final ObjectMapper yamlObjectMapper = new ObjectMapper(yamlFactory); - private final ObjectNode podDefinition = null; + static final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(true); static final Lock readLock = readWriteLock.readLock(); @@ -209,26 +207,6 @@ public Pod add(String dockerTag) throws Exception { baos.write(s.getBytes()); - // Schedule the new Pod -/* - JsonGenerator generator = jsonFactory.createGenerator(baos); - - ObjectNode newPodDefinition = podDefinition.deepCopy(); - ((ObjectNode) newPodDefinition.get("metadata")).put("name", dockerTag); - String image; - for (JsonNode containerNode : newPodDefinition.get("spec").get("containers")) { - image = containerNode.get("image").asText(); - if (image.endsWith("__DOCKER_TAG__")) { - ((ObjectNode) containerNode).put("image", image.replace("__DOCKER_TAG__", dockerTag)); - } - } - - LOGGER.info("Generated definition for \"" + dockerTag + "\": " + newPodDefinition); - - generator.writeObject(newPodDefinition); - generator.flush(); - generator.close(); -*/ HttpPost stackCreate = new HttpPost(dockercloudConfiguration.getProtocol() + "://" + dockercloudConfiguration.getHostname() + "/api/app/v1/stack/"); stackCreate.addHeader("accept", "application/json"); stackCreate.addHeader(BasicScheme.authenticate( @@ -453,12 +431,16 @@ private void refreshPods() throws IOException { Set toDelete = new HashSet<>(pods.size()); toDelete.addAll(pods.keySet()); for (int i = 0; i < objectsNode.size(); i++) { + JsonNode serviceNode = objectsNode.get(i); Pod pod = null; - String name = objectsNode.get(i).get("name").asText(); + String name = serviceNode.get("name").asText(); + + LOGGER.info("Refresh: Checking if stack " + name + " is jumping aboard the Grand Central Express!"); + String dockerTag = null; String podName = name; - String state = objectsNode.get(i).get("state").asText(); - String servicesURI = objectsNode.get(i).get("services").get(0).asText(); + String state = serviceNode.get("state").asText(); + String servicesURI = serviceNode.get("services").get(0).asText(); if (name.indexOf("-")> -1){ dockerTag = name.split("-")[1]; diff --git a/src/test/java/com/o19s/grandcentral/dockercloud/DockercloudRegistryTest.java b/src/test/java/com/o19s/grandcentral/dockercloud/DockercloudRegistryTest.java index 173d2a3..ca508f5 100644 --- a/src/test/java/com/o19s/grandcentral/dockercloud/DockercloudRegistryTest.java +++ b/src/test/java/com/o19s/grandcentral/dockercloud/DockercloudRegistryTest.java @@ -7,15 +7,24 @@ import org.junit.Test; import com.jaunt.UserAgent; -import com.jaunt.component.Label; public class DockercloudRegistryTest { @Test - @Ignore +// @Ignore public void testCheck() throws Exception { - DockercloudRegistry dockercloudRegistry = new DockercloudRegistry(null); + + DockercloudConfiguration dockercloudConfig = new DockercloudConfiguration(); + dockercloudConfig.setProtocol("https"); + dockercloudConfig.setHostname("cloud.docker.com"); + dockercloudConfig.setNamespace("datastart"); + dockercloudConfig.setUsername("dep4b"); + dockercloudConfig.setApikey("YOUR_API_KEY"); + + + + DockercloudRegistry dockercloudRegistry = new DockercloudRegistry(dockercloudConfig); assertTrue(dockercloudRegistry.imageExistsInRegistry("dep4b/datastart:v1")); assertFalse(dockercloudRegistry.imageExistsInRegistry("dep4b/datastart:v2")); diff --git a/src/test/resources/local-dockercloud.yml b/src/test/resources/local-dockercloud.yml index 10a8f20..36f6fd1 100644 --- a/src/test/resources/local-dockercloud.yml +++ b/src/test/resources/local-dockercloud.yml @@ -6,6 +6,7 @@ refresh_interval_in_ms: 300000 pod_port: 81 dockercloud: + protocol: https hostname: cloud.docker.com namespace: datastart username: dep4b From 5f17b9c65d137c37f0ffecef8afa0611933aaeea Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sat, 27 Aug 2016 11:27:45 -0400 Subject: [PATCH 10/29] upgrade to Dropwizard 1.0.0, deal with wiremock dependency issues --- pom.xml | 41 ++++++++----------- .../GrandCentralConfiguration2.java | 29 ------------- .../dockercloud/DockercloudRegistryTest.java | 19 --------- 3 files changed, 18 insertions(+), 71 deletions(-) diff --git a/pom.xml b/pom.xml index eb69d84..d023465 100644 --- a/pom.xml +++ b/pom.xml @@ -10,8 +10,8 @@ 1.0-SNAPSHOT - 0.9.1 - 9.2.13.v20150730 + 1.0.0 + 9.3.9.v20160517 4.12 @@ -40,29 +40,24 @@ com.github.tomakehurst wiremock - 1.58 - + 2.1.11 + test + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-databind + + - - - org.apache.commons - commons-exec - 1.3 - - - com.machinepublishers - jbrowserdriver - 0.16.4 - - jaunt - jaunt - 1.2 - system - ${project.basedir}/lib/jaunt1.2.jar - - - diff --git a/src/main/java/com/o19s/grandcentral/GrandCentralConfiguration2.java b/src/main/java/com/o19s/grandcentral/GrandCentralConfiguration2.java index a76331b..895ce47 100644 --- a/src/main/java/com/o19s/grandcentral/GrandCentralConfiguration2.java +++ b/src/main/java/com/o19s/grandcentral/GrandCentralConfiguration2.java @@ -45,15 +45,7 @@ public class GrandCentralConfiguration2 extends Configuration { @NotNull private DockercloudConfiguration dockercloud = new DockercloudConfiguration(); - /* - @Valid - @NotNull - private KubernetesConfiguration kubernetes = new KubernetesConfiguration(); - @Valid - @NotNull - private GCloudConfiguration gcloud = new GCloudConfiguration(); -*/ @JsonProperty public long getJanitorCleanupThreshold() { return janitorCleanupThreshold; @@ -97,17 +89,6 @@ public void setRefreshIntervalInMs(long refreshIntervalInMs) { } -/* - @JsonProperty - public int getPodPort() { - return podPort; - } - - @JsonProperty - public void setPodPort(int podPort) { - this.podPort = podPort; - } -*/ @JsonProperty("dockercloud") public DockercloudConfiguration getDockercloudConfiguration() { return dockercloud; @@ -117,17 +98,7 @@ public DockercloudConfiguration getDockercloudConfiguration() { public void setDockercloudConfiguration(DockercloudConfiguration factory) { this.dockercloud = factory; } -/* - @JsonProperty("gcloud") - public GCloudConfiguration getGCloudConfiguration() { - return gcloud; - } - @JsonProperty("gcloud") - public void setGCloudConfiguration(GCloudConfiguration gcloud) { - this.gcloud = gcloud; - } - */ @JsonProperty public int getPodPort() { diff --git a/src/test/java/com/o19s/grandcentral/dockercloud/DockercloudRegistryTest.java b/src/test/java/com/o19s/grandcentral/dockercloud/DockercloudRegistryTest.java index ca508f5..0f49ae2 100644 --- a/src/test/java/com/o19s/grandcentral/dockercloud/DockercloudRegistryTest.java +++ b/src/test/java/com/o19s/grandcentral/dockercloud/DockercloudRegistryTest.java @@ -3,11 +3,8 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import org.junit.Ignore; import org.junit.Test; -import com.jaunt.UserAgent; - public class DockercloudRegistryTest { @@ -30,21 +27,5 @@ public void testCheck() throws Exception { assertFalse(dockercloudRegistry.imageExistsInRegistry("dep4b/datastart:v2")); } - @Test - @Ignore - public void testScrapeDockerHub() throws Exception { - - - - UserAgent userAgent = new UserAgent(); - userAgent.visit("https://hub.docker.com/login/"); - - userAgent.doc.fillout("FancyInput__default___1Iybp", "dep4b"); //fill out the component labelled 'Username:' with "tom" - userAgent.doc.fillout("FancyInput__error___TIz2p", "your password"); //fill out the component labelled 'Password:' with "secret" -// userAgent.doc.choose(Label.RIGHT, "Remember me");//choose the component right-labelled 'Remember me'. - userAgent.doc.submit(); //submit the form - System.out.println(userAgent.getLocation()); //print the current location (url) - - } } From 6ab71f6684c4cad689c1473e669dedf5d06b8200 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sat, 27 Aug 2016 11:42:33 -0400 Subject: [PATCH 11/29] Java is hard! one more set of dependencies, now the Dropwizard starts up nicely --- pom.xml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pom.xml b/pom.xml index d023465..7622d01 100644 --- a/pom.xml +++ b/pom.xml @@ -55,6 +55,26 @@ com.fasterxml.jackson.core jackson-databind + + org.eclipse.jetty + jetty-server + + + org.eclipse.jetty + jetty-security + + + org.eclipse.jetty + jetty-webapp + + + org.eclipse.jetty + jetty-servlet + + + org.eclipse.jetty + jetty-servlets + From 53e2248058868951421ffe2c7d892cc519608270 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sat, 27 Aug 2016 11:59:04 -0400 Subject: [PATCH 12/29] enough changes going on to call this a 1.1! --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7622d01..90edd33 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ Grand Central com.o19s grand-central - 1.0-SNAPSHOT + 1.1-SNAPSHOT 1.0.0 From f8ba333d9b43fb81125c619979d78715f19baafe Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sat, 27 Aug 2016 11:59:29 -0400 Subject: [PATCH 13/29] trying to provide better docs --- README_dockercloud.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/README_dockercloud.md b/README_dockercloud.md index f73e4f9..3040821 100644 --- a/README_dockercloud.md +++ b/README_dockercloud.md @@ -1,13 +1,10 @@ # Grand Central -*Your automated review deployment tool. Gut-check in the cloud* +*Quepid's automated review deployment tool. Gut-check in the cloud* Grand Central is a tool for automated deployment and cleanup of developer review environments / containers. Requests are parsed and routed based on their URL structure. If the target container exists the request is proxied along. If not Grand Central will spin up a container and forward the request once it comes online. ## URL Structure - -URLs tells Grand Central how to route to the correct container. A url like `db139cf.datastart.grandcentral.com` in DNS is directed at the Grand Central service. Grand Central parses out the first part of the name, `db139cf` to retrieve the appropriate Git version to deploy. - -The appropriate container is determined by parsing the first part of the domain name. `*.review.grandcentral.com` in DNS is directed at the Grand Central service. The application parses the domain name to retrieve the appropriate Git version to deploy. `http://db139cf.review.quepid.com/secure` would route to a container running version `db139cf` if it exists. +The appropriate container is determined by parsing the first part of the domain name. `*.review.quepid.com` in DNS is directed at the Grand Central service. The application parses the domain name to retrieve the appropriate Git version to deploy. `http://db139cf.review.quepid.com/secure` would route to a container running version `db139cf` if it exists. ## Request Flow When a request is received by the system the following processing takes place. @@ -19,7 +16,8 @@ When a request is received by the system the following processing takes place. 1. Verify version exists in Container Registry. *We can't deploy a version which doesn't exist. * If so, continue * If not, 404 -1. Create DockerCloud Stack containing everything required. Stack's are named after the git version. +1. Create DockerCloud Stack (?) containing app version, database, and loader +1. Create Service for Stack (routes requests internally within the cluster) 1. Proxy the original request *Note* that all requests will have some metrics stored to determine activity for a give stack. This is useful when reaping old stacks. @@ -53,14 +51,18 @@ There is a hard limit to the number of simultaneous stacks running on the cluste curl --user username:apikey "https://cloud.docker.com/api/app/v1/stack/" -## Local Development -Add something like below to your `/etc/hosts` file. This will let you simulate hitting a deployed GrandCentral -server, though only on the specified FQDN! +## /etc/hosts + +Add to make testing your local set up easier this to your `/etc/hosts` file: ``` 127.0.0.1 v1.datastart.grandcentral.com +127.0.0.1 v2.datastart.grandcentral.com ``` + + + ## Implementation All logic for checking / creation of pods may be performed in a [`javax.servlet.Filter`](http://docs.oracle.com/javaee/7/api/javax/servlet/Filter.html?is-external=true). Requests may then be passed along to a [`org.eclipse.jetty.proxy.ProxyServlet`](http://download.eclipse.org/jetty/stable-9/apidocs/org/eclipse/jetty/proxy/ProxyServlet.html) after stack management is complete. From d577bb0d07102dbf0c92d6081a7a9414d4318730 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sat, 27 Aug 2016 21:26:46 -0400 Subject: [PATCH 14/29] standardize on .yml instead of the mix we have --- Dockerfile.example | 6 +++--- config/{pod.yaml.example => pod.yml.example} | 0 2 files changed, 3 insertions(+), 3 deletions(-) rename config/{pod.yaml.example => pod.yml.example} (100%) diff --git a/Dockerfile.example b/Dockerfile.example index d042689..1538421 100644 --- a/Dockerfile.example +++ b/Dockerfile.example @@ -9,11 +9,11 @@ RUN yum install -y openssl RUN mkdir -p /srv/app/config COPY target/grand-central-1.0-SNAPSHOT.jar /srv/app/ -COPY config/configuration.yaml /srv/app/config/ -COPY config/pod.yaml /srv/app/config/ +COPY config/configuration.yml /srv/app/config/ +COPY config/pod.yml /srv/app/config/ RUN echo -n | openssl s_client -connect :443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > /srv/app/config/k8s.pem RUN keytool -importkeystore -srckeystore /usr/java/latest/lib/security/cacerts -destkeystore /srv/app/config/grandcentral.jks -srcstorepass changeit -deststorepass changeit RUN echo "yes" | keytool -import -v -trustcacerts -alias local_k8s -file /srv/app/config/k8s.pem -keystore /srv/app/config/grandcentral.jks -keypass changeit -storepass changeit -CMD cd /srv/app && /usr/bin/java -jar /srv/app/grand-central-1.0-SNAPSHOT.jar server /srv/app/config/configuration.yaml +CMD cd /srv/app && /usr/bin/java -jar /srv/app/grand-central-1.0-SNAPSHOT.jar server /srv/app/config/configuration.yml diff --git a/config/pod.yaml.example b/config/pod.yml.example similarity index 100% rename from config/pod.yaml.example rename to config/pod.yml.example From d671e3b56d936829ade3ff32d4ad3283256331d8 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sat, 27 Aug 2016 21:42:30 -0400 Subject: [PATCH 15/29] dont just blindly assume a stack has services, cause it may be just starting --- .../com/o19s/grandcentral/dockercloud/StackManager.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java b/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java index 6203ff0..a7c1083 100644 --- a/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java +++ b/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java @@ -11,6 +11,7 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; +import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpStatus; import org.apache.http.auth.AuthScope; @@ -440,13 +441,16 @@ private void refreshPods() throws IOException { String dockerTag = null; String podName = name; String state = serviceNode.get("state").asText(); - String servicesURI = serviceNode.get("services").get(0).asText(); + String servicesURI = null; + if (serviceNode.get("services").size()> 0){ // A stack that is starting up may not yet have services! + servicesURI = serviceNode.get("services").get(0).asText(); + } if (name.indexOf("-")> -1){ dockerTag = name.split("-")[1]; } - if (dockerTag != null && servicesURI != "" && podName.contains(dockercloudConfiguration.getNamespace())){ + if (dockerTag != null && StringUtils.isBlank(servicesURI) && podName.contains(dockercloudConfiguration.getNamespace())){ String publicDNS = getDNSForStack(servicesURI); From aa0f83da058917d686276869d168cea2d7b86ad9 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sat, 27 Aug 2016 21:42:58 -0400 Subject: [PATCH 16/29] allow property interpolation and hopefully keep my crednetiasl out ouf github --- .../o19s/grandcentral/GrandCentralApplication2.java | 12 +++++++++++- src/test/resources/local-dockercloud.yml | 4 ++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/o19s/grandcentral/GrandCentralApplication2.java b/src/main/java/com/o19s/grandcentral/GrandCentralApplication2.java index 33e2785..9099e2b 100644 --- a/src/main/java/com/o19s/grandcentral/GrandCentralApplication2.java +++ b/src/main/java/com/o19s/grandcentral/GrandCentralApplication2.java @@ -1,6 +1,8 @@ package com.o19s.grandcentral; import io.dropwizard.Application; +import io.dropwizard.configuration.EnvironmentVariableSubstitutor; +import io.dropwizard.configuration.SubstitutingSourceProvider; import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Environment; @@ -26,7 +28,15 @@ public String getName() { } @Override - public void initialize(Bootstrap bootstrap) {} + public void initialize(Bootstrap bootstrap) { + // Enable variable substitution with environment variables + bootstrap.setConfigurationSourceProvider( + new SubstitutingSourceProvider(bootstrap.getConfigurationSourceProvider(), + new EnvironmentVariableSubstitutor(false) + ) + ); + + } @Override public void run(GrandCentralConfiguration2 config, Environment environment) throws Exception { diff --git a/src/test/resources/local-dockercloud.yml b/src/test/resources/local-dockercloud.yml index 36f6fd1..3ae32ba 100644 --- a/src/test/resources/local-dockercloud.yml +++ b/src/test/resources/local-dockercloud.yml @@ -9,5 +9,5 @@ dockercloud: protocol: https hostname: cloud.docker.com namespace: datastart - username: dep4b - apikey: YOUR_API_KEY + username: ${DOCKERCLOUD_USERNAME} + apikey: ${DOCKERCLOUD_APIKEY} From 6f8b47a43dee1c466b8e18cf7c6a41b0e129093f Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sat, 27 Aug 2016 21:43:24 -0400 Subject: [PATCH 17/29] for now, grab the default application, still needs reworking --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 90edd33..a04e971 100644 --- a/pom.xml +++ b/pom.xml @@ -118,7 +118,7 @@ - com.o19s.grandcentral.GrandCentralApplication + com.o19s.grandcentral.GrandCentralApplication2 From 326e48bfb4a954cccf16233b7401634a907bdc2e Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sun, 28 Aug 2016 20:08:44 -0400 Subject: [PATCH 18/29] one itsy bitsy ! was needed --- .../java/com/o19s/grandcentral/dockercloud/StackManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java b/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java index a7c1083..c78cd02 100644 --- a/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java +++ b/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java @@ -450,7 +450,7 @@ private void refreshPods() throws IOException { dockerTag = name.split("-")[1]; } - if (dockerTag != null && StringUtils.isBlank(servicesURI) && podName.contains(dockercloudConfiguration.getNamespace())){ + if (dockerTag != null && !StringUtils.isBlank(servicesURI) && podName.contains(dockercloudConfiguration.getNamespace())){ String publicDNS = getDNSForStack(servicesURI); From 8fe81397a1c58eef503f86d50f01600540d08697 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Fri, 9 Sep 2016 10:00:30 -0700 Subject: [PATCH 19/29] removed hard coded stack definition, following more pattern laid out by PodManager in the StackManager --- config/docker-cloud.json.example | 11 ++++ .../dockercloud/DockercloudConfiguration.java | 14 +++++ .../dockercloud/StackManager.java | 53 ++++++++++--------- .../dockercloud/StackManagerTest.java | 44 +++++++++++++++ src/test/resources/docker-cloud.json | 11 ++++ src/test/resources/local-dockercloud.yml | 1 + 6 files changed, 108 insertions(+), 26 deletions(-) create mode 100644 config/docker-cloud.json.example create mode 100644 src/main/java/com/o19s/grandcentral/dockercloud/StackManagerTest.java create mode 100644 src/test/resources/docker-cloud.json diff --git a/config/docker-cloud.json.example b/config/docker-cloud.json.example new file mode 100644 index 0000000..2b38f18 --- /dev/null +++ b/config/docker-cloud.json.example @@ -0,0 +1,11 @@ +{ + "name": "echostack", + "services": [ + { + "name":"echoheaders", + "please_note": "Grand Central replaces __DOCKER_TAG__ in the image with the version", + "image": "gcr.io/google_containers/echoserver:__DOCKER_TAG__", + "ports": ["8080:8080"] + } + ] +} diff --git a/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudConfiguration.java b/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudConfiguration.java index 5f45839..fe8b968 100644 --- a/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudConfiguration.java +++ b/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudConfiguration.java @@ -29,6 +29,10 @@ public class DockercloudConfiguration { // this can be null private String protocol; + + @NotNull + @NotEmpty + private String stackJsonPath; @JsonProperty public String getHostname() { @@ -79,4 +83,14 @@ public String getNamespace() { public void setNamespace(String namespace) { this.namespace = namespace; } + + @JsonProperty + public String getStackJsonPath() { + return stackJsonPath; + } + + @JsonProperty + public void setStackJsonPath(String stackJsonPath) { + this.stackJsonPath = stackJsonPath; + } } diff --git a/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java b/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java index c78cd02..2cac796 100644 --- a/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java +++ b/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java @@ -1,6 +1,7 @@ package com.o19s.grandcentral.dockercloud; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; @@ -63,6 +64,7 @@ public class StackManager implements LinkedContainerManager { private final JsonFactory jsonFactory = new JsonFactory(); private final ObjectMapper jsonObjectMapper = new ObjectMapper(jsonFactory); + private final ObjectNode stackDefinition; static final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(true); @@ -70,12 +72,10 @@ public class StackManager implements LinkedContainerManager { static final Lock writeLock = readWriteLock.writeLock(); /** - * Instantiates a new manages with the specified settings - * @param k8sConfiguration Kubernetes Configuration - * @param keystorePath Path to the Java Keystore containing trusted certificates - * @param maximumPodCount Maximum number of pods to ever have running at once - * @param refreshIntervalInMs Interval with which to refresh the pods - * @param podYamlPath the location of the yaml config for the application pod + * Instantiates a new manages with the specified settings. + * TODO think about refactoring StackManager to being "LinkedContainerManager" and abstract + * the specific HTTP calls away to a seperate DockerCloudAPI so we could drop in a MockDockerCloudAPI. + * */ public StackManager(DockercloudConfiguration dockercloudConfiguration, @@ -88,10 +88,10 @@ public StackManager(DockercloudConfiguration dockercloudConfiguration, this.refreshIntervalInMs = refreshIntervalInMs; this.maximumPodCount = maximumPodCount; - // podDefinition = jsonObjectMapper.createObjectNode(); -// podDefinition.setAll((ObjectNode) yamlObjectMapper.readTree(new File(podYamlPath))); + stackDefinition = jsonObjectMapper.createObjectNode(); + stackDefinition.setAll((ObjectNode)jsonObjectMapper.readTree(new File(dockercloudConfiguration.getStackJsonPath()))); - LOGGER.info("Loaded Pod Definition: " /*+ podDefinition*/); + LOGGER.info("Loaded Stack Definition: " + stackDefinition); try { // Setup SSL and plain connection socket factories @@ -188,25 +188,26 @@ public Pod add(String dockerTag) throws Exception { readLock.lock(); try { + // Schedule the new Stack + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JsonGenerator generator = jsonFactory.createGenerator(baos); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - - String s = String.join("\n" - , "{" - , " \"name\": \"" + dockercloudConfiguration.getNamespace() + "-" + dockerTag + "\"," - , " \"services\": [" - , " {" - , " \"name\":\"hello-world\"," - , " \"image\":\"dep4b/datastart:v1\"," - ," \"ports\": [" - , " \"81:8080\"" - , " ]" - , " }" - , " ]" - , "}" - ); + ObjectNode newStackDefinition = stackDefinition.deepCopy(); + newStackDefinition.put("name", dockercloudConfiguration.getNamespace() + "-" + dockerTag); + + String image; + for (JsonNode serviceNode : newStackDefinition.get("services")) { + image = serviceNode.get("image").asText(); + if (image.endsWith("__DOCKER_TAG__")) { + ((ObjectNode) serviceNode).put("image", image.replace("__DOCKER_TAG__", dockerTag)); + } + } + + LOGGER.info("Generated definition for \"" + dockerTag + "\": " + newStackDefinition); - baos.write(s.getBytes()); + generator.writeObject(newStackDefinition); + generator.flush(); + generator.close(); HttpPost stackCreate = new HttpPost(dockercloudConfiguration.getProtocol() + "://" + dockercloudConfiguration.getHostname() + "/api/app/v1/stack/"); stackCreate.addHeader("accept", "application/json"); diff --git a/src/main/java/com/o19s/grandcentral/dockercloud/StackManagerTest.java b/src/main/java/com/o19s/grandcentral/dockercloud/StackManagerTest.java new file mode 100644 index 0000000..29a7359 --- /dev/null +++ b/src/main/java/com/o19s/grandcentral/dockercloud/StackManagerTest.java @@ -0,0 +1,44 @@ +package com.o19s.grandcentral.dockercloud; + +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Test; + +import com.o19s.grandcentral.kubernetes.Pod; + +public class StackManagerTest { + + private DockercloudConfiguration dockercloudConfig; + @Before + public void setUp() throws Exception { + dockercloudConfig = new DockercloudConfiguration(); + dockercloudConfig.setProtocol("https"); + dockercloudConfig.setHostname("cloud.docker.com"); + dockercloudConfig.setNamespace("datastart"); + dockercloudConfig.setUsername("dep4b"); + dockercloudConfig.setApikey("YOUR_API_KEY"); + dockercloudConfig.setStackJsonPath("./src/test/resources/docker-cloud.json"); + + } + + @Test + public void testGet() { + fail("Not yet implemented"); + } + + @Test + public void testContains() { + fail("Not yet implemented"); + } + + @Test + public void testAdd() throws Exception{ + StackManager stackManager = new StackManager(dockercloudConfig,10000,2); + Pod pod = stackManager.add("v1"); + assertEquals("v1", pod.getDockerTag()); + assertTrue(pod.isRunning()); + + } + +} diff --git a/src/test/resources/docker-cloud.json b/src/test/resources/docker-cloud.json new file mode 100644 index 0000000..2b38f18 --- /dev/null +++ b/src/test/resources/docker-cloud.json @@ -0,0 +1,11 @@ +{ + "name": "echostack", + "services": [ + { + "name":"echoheaders", + "please_note": "Grand Central replaces __DOCKER_TAG__ in the image with the version", + "image": "gcr.io/google_containers/echoserver:__DOCKER_TAG__", + "ports": ["8080:8080"] + } + ] +} diff --git a/src/test/resources/local-dockercloud.yml b/src/test/resources/local-dockercloud.yml index 3ae32ba..f2933be 100644 --- a/src/test/resources/local-dockercloud.yml +++ b/src/test/resources/local-dockercloud.yml @@ -9,5 +9,6 @@ dockercloud: protocol: https hostname: cloud.docker.com namespace: datastart + stack_json_path: src/test/resources/docker-cloud.json username: ${DOCKERCLOUD_USERNAME} apikey: ${DOCKERCLOUD_APIKEY} From 5a56be31a0725f990fa57d7654896399efe1866e Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sat, 10 Sep 2016 14:31:28 -0400 Subject: [PATCH 20/29] fixes to running GC w dockercloud, better tests --- README_dockercloud.md | 5 +- config/configuration.yml.example | 8 +- .../dockercloud/DockercloudConfiguration.java | 14 +++ .../dockercloud/DockercloudRegistry.java | 18 +-- .../dockercloud/StackManager.java | 116 +++++++++++------- .../com/o19s/grandcentral/kubernetes/Pod.java | 9 ++ .../dockercloud/DockercloudRegistryTest.java | 5 +- .../dockercloud/StackManagerTest.java | 30 ++--- src/test/resources/docker-cloud.json | 9 +- src/test/resources/local-dockercloud.yml | 1 + 10 files changed, 136 insertions(+), 79 deletions(-) rename src/{main => test}/java/com/o19s/grandcentral/dockercloud/StackManagerTest.java (58%) diff --git a/README_dockercloud.md b/README_dockercloud.md index 3040821..134ed16 100644 --- a/README_dockercloud.md +++ b/README_dockercloud.md @@ -47,7 +47,9 @@ There is a hard limit to the number of simultaneous stacks running on the cluste * Security?! -## curling +## Curling + +curl --user username:apikey "https://cloud.docker.com/api/app/v1/stack/" curl --user username:apikey "https://cloud.docker.com/api/app/v1/stack/" @@ -61,6 +63,7 @@ Add to make testing your local set up easier this to your `/etc/hosts` file: ``` +## Dockerizing GrandCentral ## Implementation diff --git a/config/configuration.yml.example b/config/configuration.yml.example index 8af0532..d3b3f79 100644 --- a/config/configuration.yml.example +++ b/config/configuration.yml.example @@ -21,4 +21,10 @@ gcloud: project: quepid-1051 container_name: quails - +dockercloud: + protocol: https + hostname: cloud.docker.com + namespace: datastart + stack_json_path: src/test/resources/docker-cloud.json + username: ${DOCKERCLOUD_USERNAME} + apikey: ${DOCKERCLOUD_APIKEY} diff --git a/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudConfiguration.java b/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudConfiguration.java index fe8b968..b43baa8 100644 --- a/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudConfiguration.java +++ b/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudConfiguration.java @@ -30,6 +30,10 @@ public class DockercloudConfiguration { // this can be null private String protocol; + @NotNull + @NotEmpty + private String stackExistsTestImage; // We do a hacky test of a image with a version to see if it exists or not. what image? + @NotNull @NotEmpty private String stackJsonPath; @@ -93,4 +97,14 @@ public String getStackJsonPath() { public void setStackJsonPath(String stackJsonPath) { this.stackJsonPath = stackJsonPath; } + + @JsonProperty + public String getStackExistsTestImage() { + return stackExistsTestImage; + } + + @JsonProperty + public void setStackExistsTestImage(String stackExistsTestImage) { + this.stackExistsTestImage = stackExistsTestImage; + } } diff --git a/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java b/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java index e3ece35..26f1f8a 100644 --- a/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java +++ b/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java @@ -38,24 +38,16 @@ public DockercloudRegistry(DockercloudConfiguration dockercloudConfiguration) { @Override public boolean imageExistsInRegistry(String dockerTag) throws Exception { + + LOGGER.info("Checking if Docker tag exists in registry: " + dockerTag); + boolean imageExists = false; - String containerName = null; String podUUID = null; - // Sometimes we hand in v1 and other times its dep4b/datastart:v1! - // Maybe a code smell? - if (dockerTag.indexOf(":") > -1) { - String[] parts = dockerTag.split(":"); - dockerTag = parts[1]; - containerName = parts[0]; - } else { - containerName = "dep4b/datastart"; - } - - String serviceName = "test-img-exst-" + containerName + "-" + dockerTag; + String serviceName = "test-img-exst-" + dockercloudConfiguration.getStackExistsTestImage().replace("/", "-") + "-" + dockerTag; serviceName = serviceName.replace('/', '-'); - String imageName = containerName + ":" + dockerTag; + String imageName = dockercloudConfiguration.getStackExistsTestImage() + ":" + dockerTag; podUUID = createValidityCheckService(serviceName, imageName); diff --git a/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java b/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java index 2cac796..49fcc71 100644 --- a/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java +++ b/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java @@ -142,7 +142,11 @@ public Pod get(String dockerTag) throws IOException { // Force a refresh of the data from K8S if the interval has passed if (DateTime.now().getMillis() - lastRefresh > refreshIntervalInMs) { - refreshPods(); + try { + refreshPods(); + } catch (Exception e) { + throw new IOException(e); + } } @@ -193,7 +197,7 @@ public Pod add(String dockerTag) throws Exception { JsonGenerator generator = jsonFactory.createGenerator(baos); ObjectNode newStackDefinition = stackDefinition.deepCopy(); - newStackDefinition.put("name", dockercloudConfiguration.getNamespace() + "-" + dockerTag); + newStackDefinition.put("name", dockercloudConfiguration.getNamespace() + "-" + dockerTag.replace(".", "-")); String image; for (JsonNode serviceNode : newStackDefinition.get("services")) { @@ -219,20 +223,25 @@ public Pod add(String dockerTag) throws Exception { HttpEntity podJson = new ByteArrayEntity(baos.toByteArray()); stackCreate.setEntity(podJson); + boolean podCreated = false; String podUUID = null; try (CloseableHttpResponse response = httpClient.execute(stackCreate)) { int status = response.getStatusLine().getStatusCode(); HttpEntity entity = response.getEntity(); InputStream responseBody = entity.getContent(); - - JsonNode rootNode = jsonObjectMapper.readTree(responseBody); - JsonNode objectsNode = rootNode.get("objects"); + if (status == HttpStatus.SC_CREATED) { - LOGGER.info("Pod " + dockerTag + ": Scheduled"); + LOGGER.info("Pod " + dockerTag + ": Scheduled"); + + JsonNode rootNode = jsonObjectMapper.readTree(responseBody); + JsonNode objectsNode = rootNode.get("objects"); podUUID = rootNode.get("uuid").asText(); + podCreated = true; } else if (status == HttpStatus.SC_CONFLICT) { LOGGER.info("Pod " + dockerTag + ": Already running"); + } else if (status == HttpStatus.SC_BAD_REQUEST) { + LOGGER.info("Pod " + dockerTag + ": Couldn't create stack. Check definition: " + newStackDefinition); } else { LOGGER.info("Pod " + dockerTag + ": Not scheduled (" + response.getStatusLine().toString() + ")"); } @@ -240,14 +249,13 @@ public Pod add(String dockerTag) throws Exception { LOGGER.error("Pod " + dockerTag + ": Error scheduling pod", ioe); } - // Wait until Pod is running - boolean podRunning = false; - boolean podCreated = true; + + - podRunning = true; // Here should be a check to see if it the stack was created, not sure how long it takes!!! + // Start the stack if it was created if (podCreated && podUUID != null){ // Start the stack HttpPost stackStart = new HttpPost(dockercloudConfiguration.getProtocol() + "://" + dockercloudConfiguration.getHostname() + "/api/app/v1/stack/" + podUUID + "/start/"); @@ -269,17 +277,20 @@ public Pod add(String dockerTag) throws Exception { } + // Wait until Pod is running + if (podCreated){ + boolean podRunning = false; + int numberOfSecondsWaiting = 0; + HttpGet stackStatus = new HttpGet(dockercloudConfiguration.getProtocol() + "://" + dockercloudConfiguration.getHostname() + "/api/app/v1/stack/" + podUUID); + stackStatus.addHeader("accept", "application/json"); + stackStatus.addHeader(BasicScheme.authenticate( + new UsernamePasswordCredentials(dockercloudConfiguration.getUsername(), dockercloudConfiguration.getApikey()), + "UTF-8", false)); - do { - HttpGet stackStatus = new HttpGet(dockercloudConfiguration.getProtocol() + "://" + dockercloudConfiguration.getHostname() + "/api/app/v1/stack/" + podUUID); - stackStatus.addHeader("accept", "application/json"); - stackStatus.addHeader(BasicScheme.authenticate( - new UsernamePasswordCredentials(dockercloudConfiguration.getUsername(), dockercloudConfiguration.getApikey()), - "UTF-8", false)); - - LOGGER.info("Pod " + dockerTag + ": Waiting for start"); + numberOfSecondsWaiting++; + LOGGER.info("Pod " + dockerTag + ": Waiting for start. Seconds:" + numberOfSecondsWaiting); Thread.sleep(1000); try (CloseableHttpResponse response = httpClient.execute(stackStatus)) { @@ -297,15 +308,22 @@ public Pod add(String dockerTag) throws Exception { String publicDNS = getDNSForStack(serviceURI); pod = new Pod(dockerTag, publicDNS, status); + pod.setUuid(podUUID); podRunning = pod != null && pod.isRunning(); } catch (IOException ioe) { LOGGER.error("Pod " + dockerTag + ": Error getting DNS for pod", ioe); } } - } while (!podRunning); + } while (!podRunning | numberOfSecondsWaiting > 120); - LOGGER.info("Pod " + dockerTag + ": Started"); + if (podRunning){ + LOGGER.info("Pod " + dockerTag + ": Started"); + } + } + else { + LOGGER.error("Pod " + dockerTag + ": Timed out checking for start"); + } } finally { readLock.unlock(); } @@ -324,37 +342,37 @@ public Pod add(String dockerTag) throws Exception { * @param dockerTag * @throws IOException */ - private void remove(String dockerTag) throws IOException { + public void remove(String dockerTag, String stackUUID) throws IOException { if (contains(dockerTag)) { readLock.lock(); try { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JsonGenerator generator = jsonFactory.createGenerator(baos); - - ObjectNode root = jsonObjectMapper.createObjectNode(); - root.put("gracePeriodSeconds", 0); - - generator.writeObject(root); - generator.flush(); - - HttpDelete podDelete = new HttpDelete("https://" /*+ k8sConfiguration.getMasterIp()*/ + ":443/api/v1/namespaces/" /*+ k8sConfiguration.getNamespace() */+ "/pods/" + dockerTag); - podDelete.setEntity(new ByteArrayEntity(baos.toByteArray())); + + // Start the stack + HttpDelete stackDelete = new HttpDelete(dockercloudConfiguration.getProtocol() + "://" + dockercloudConfiguration.getHostname() + "/api/app/v1/stack/" + stackUUID +"/"); + stackDelete.addHeader("accept", "application/json"); + stackDelete.addHeader(BasicScheme.authenticate( + new UsernamePasswordCredentials(dockercloudConfiguration.getUsername(), dockercloudConfiguration.getApikey()), + "UTF-8", false)); + - try (CloseableHttpResponse response = httpClient.execute(podDelete, httpContext)) { + try (CloseableHttpResponse response = httpClient.execute(stackDelete)) { if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { - LOGGER.info("Pod " + dockerTag + ": Removed"); + LOGGER.info("Stack " + dockerTag + ": Removed"); + Thread.sleep(10000); } else { - LOGGER.info("Pod " + dockerTag + ": Error removing pod (" + response.getStatusLine().toString() + ")"); + LOGGER.info("Stack " + dockerTag + ": Error removing stack (" + response.getStatusLine().toString() + ")"); } } catch (IOException ioe) { - LOGGER.error("Pod " + dockerTag + ": Error removing pod", ioe); + LOGGER.error("Stack " + dockerTag + ": Error removing stack", ioe); + } catch (InterruptedException e) { + LOGGER.error("Stack " + dockerTag + ": Error sleeping"); } } finally { readLock.unlock(); } } else { - throw new IllegalArgumentException("Pod doesn't exist"); + throw new IllegalArgumentException("Stack doesn't exist"); } } @@ -391,7 +409,7 @@ else if (left.getLastRequest() < right.getLastRequest()) // Remove the pods int amountToRemove = sortedPodsByRequestAge.length - maximumPodCount; for (int i = 0; i < amountToRemove; i++) { - remove(sortedPodsByRequestAge[i].getDockerTag()); + remove(sortedPodsByRequestAge[i].getDockerTag(), sortedPodsByRequestAge[i].getUuid()); } } } finally { @@ -406,7 +424,7 @@ else if (left.getLastRequest() < right.getLastRequest()) /** * Refreshes the internal map which tracks all running pods - * @throws IOException + * @throws Exception */ private void refreshPods() throws IOException { HttpGet stacksGet = new HttpGet(dockercloudConfiguration.getProtocol() + "://" + dockercloudConfiguration.getHostname() + "/api/app/v1/stack/"); @@ -424,6 +442,12 @@ private void refreshPods() throws IOException { System.out.println("writelock:" + writeLock.toString()); writeLock.lock(); + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + LOGGER.info("Stack status query suceeded"); + } else { + LOGGER.error("Statck status query failed. (" + response.getStatusLine().toString() + ")"); + } + try (InputStream responseBody = entity.getContent()) { JsonNode rootNode = jsonObjectMapper.readTree(responseBody); @@ -448,7 +472,13 @@ private void refreshPods() throws IOException { } if (name.indexOf("-")> -1){ - dockerTag = name.split("-")[1]; + String[] namebits = name.split("-"); + if (namebits.length==2){ + dockerTag = namebits[1]; + } + else { + dockerTag = namebits[1] + "." + namebits[2]; + } } if (dockerTag != null && !StringUtils.isBlank(servicesURI) && podName.contains(dockercloudConfiguration.getNamespace())){ @@ -478,8 +508,8 @@ private void refreshPods() throws IOException { } // Delete pods that have been removed (delete refers to our hash, not k8s). This calls remove on the hash, not the manager. -// toDelete.forEach((dockerTag) -> LOGGER.info("Refresh: Removing pod " + dockerTag + " from internal hash")); -// toDelete.forEach(pods::remove); + toDelete.forEach((dockerTag) -> LOGGER.info("Refresh: Removing pod " + dockerTag + " from internal hash")); + toDelete.forEach(pods::remove); } catch (IOException ioe) { LOGGER.error("Pod Refresh: Error parsing pods", ioe); } finally { @@ -491,7 +521,7 @@ private void refreshPods() throws IOException { } // Cleanup old pods -// removeExtraPods(); + removeExtraPods(); // Update the lastRefresh time lastRefresh = DateTime.now().getMillis(); diff --git a/src/main/java/com/o19s/grandcentral/kubernetes/Pod.java b/src/main/java/com/o19s/grandcentral/kubernetes/Pod.java index 2743cf3..c6a5839 100644 --- a/src/main/java/com/o19s/grandcentral/kubernetes/Pod.java +++ b/src/main/java/com/o19s/grandcentral/kubernetes/Pod.java @@ -12,6 +12,7 @@ public class Pod { private String address; private String status; private AtomicLong lastRequest; + private String uuid; // Needed for Dockercloud, we pass UUID around. /** * Creates a new Pod @@ -62,4 +63,12 @@ public long getLastRequest() { public void setLastRequest(long requestedAt) { this.lastRequest.set(requestedAt); } + +public String getUuid() { + return uuid; +} + +public void setUuid(String uuid) { + this.uuid = uuid; +} } diff --git a/src/test/java/com/o19s/grandcentral/dockercloud/DockercloudRegistryTest.java b/src/test/java/com/o19s/grandcentral/dockercloud/DockercloudRegistryTest.java index 0f49ae2..ab90c00 100644 --- a/src/test/java/com/o19s/grandcentral/dockercloud/DockercloudRegistryTest.java +++ b/src/test/java/com/o19s/grandcentral/dockercloud/DockercloudRegistryTest.java @@ -18,13 +18,14 @@ public void testCheck() throws Exception { dockercloudConfig.setNamespace("datastart"); dockercloudConfig.setUsername("dep4b"); dockercloudConfig.setApikey("YOUR_API_KEY"); + dockercloudConfig.setStackExistsTestImage("dep4b/datastart"); DockercloudRegistry dockercloudRegistry = new DockercloudRegistry(dockercloudConfig); - assertTrue(dockercloudRegistry.imageExistsInRegistry("dep4b/datastart:v1")); - assertFalse(dockercloudRegistry.imageExistsInRegistry("dep4b/datastart:v2")); + assertTrue(dockercloudRegistry.imageExistsInRegistry("v1")); + assertFalse(dockercloudRegistry.imageExistsInRegistry("v2")); } diff --git a/src/main/java/com/o19s/grandcentral/dockercloud/StackManagerTest.java b/src/test/java/com/o19s/grandcentral/dockercloud/StackManagerTest.java similarity index 58% rename from src/main/java/com/o19s/grandcentral/dockercloud/StackManagerTest.java rename to src/test/java/com/o19s/grandcentral/dockercloud/StackManagerTest.java index 29a7359..3e01e5e 100644 --- a/src/main/java/com/o19s/grandcentral/dockercloud/StackManagerTest.java +++ b/src/test/java/com/o19s/grandcentral/dockercloud/StackManagerTest.java @@ -15,30 +15,32 @@ public void setUp() throws Exception { dockercloudConfig = new DockercloudConfiguration(); dockercloudConfig.setProtocol("https"); dockercloudConfig.setHostname("cloud.docker.com"); - dockercloudConfig.setNamespace("datastart"); + dockercloudConfig.setNamespace("gctest"); dockercloudConfig.setUsername("dep4b"); dockercloudConfig.setApikey("YOUR_API_KEY"); dockercloudConfig.setStackJsonPath("./src/test/resources/docker-cloud.json"); + dockercloudConfig.setStackExistsTestImage("mysql"); } - @Test - public void testGet() { - fail("Not yet implemented"); - } @Test - public void testContains() { - fail("Not yet implemented"); - } - - @Test - public void testAdd() throws Exception{ + public void testAddRemoveLifycycle() throws Exception{ StackManager stackManager = new StackManager(dockercloudConfig,10000,2); - Pod pod = stackManager.add("v1"); - assertEquals("v1", pod.getDockerTag()); + + Pod pod = stackManager.add("bogus_tag"); + assertNull(pod); + + pod = stackManager.add("latest"); + assertEquals("latest", pod.getDockerTag()); + assertTrue(pod.isRunning()); + + stackManager.remove(pod.getDockerTag(), pod.getUuid()); + + pod = stackManager.add("5.5"); + assertEquals("5.5", pod.getDockerTag()); assertTrue(pod.isRunning()); - + stackManager.remove(pod.getDockerTag(), pod.getUuid()); } } diff --git a/src/test/resources/docker-cloud.json b/src/test/resources/docker-cloud.json index 2b38f18..6093d33 100644 --- a/src/test/resources/docker-cloud.json +++ b/src/test/resources/docker-cloud.json @@ -1,11 +1,10 @@ { - "name": "echostack", + "name": "mysql", "services": [ { - "name":"echoheaders", - "please_note": "Grand Central replaces __DOCKER_TAG__ in the image with the version", - "image": "gcr.io/google_containers/echoserver:__DOCKER_TAG__", - "ports": ["8080:8080"] + "name":"mysql", + "image": "mysql:__DOCKER_TAG__", + "environment": ["MYSQL_ROOT_PASSWORD=root"] } ] } diff --git a/src/test/resources/local-dockercloud.yml b/src/test/resources/local-dockercloud.yml index f2933be..2f99194 100644 --- a/src/test/resources/local-dockercloud.yml +++ b/src/test/resources/local-dockercloud.yml @@ -10,5 +10,6 @@ dockercloud: hostname: cloud.docker.com namespace: datastart stack_json_path: src/test/resources/docker-cloud.json + stack_exists_test_image: dep4b/datastart username: ${DOCKERCLOUD_USERNAME} apikey: ${DOCKERCLOUD_APIKEY} From 2a2b50f1908073936a930cd962c1515149697e75 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sat, 10 Sep 2016 15:14:48 -0400 Subject: [PATCH 21/29] shorten name, and add more debuggin --- .../o19s/grandcentral/dockercloud/DockercloudRegistry.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java b/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java index 26f1f8a..989dfef 100644 --- a/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java +++ b/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java @@ -44,8 +44,7 @@ public boolean imageExistsInRegistry(String dockerTag) throws Exception { boolean imageExists = false; String podUUID = null; - String serviceName = "test-img-exst-" + dockercloudConfiguration.getStackExistsTestImage().replace("/", "-") + "-" + dockerTag; - serviceName = serviceName.replace('/', '-'); + String serviceName = "chk-" + dockerTag; String imageName = dockercloudConfiguration.getStackExistsTestImage() + ":" + dockerTag; @@ -68,6 +67,8 @@ private String createValidityCheckService(String serviceName, String imageName){ + "\",", " \"image\": \"" + imageName + "\"" + ",", " \"run_command\": \"ls\"" , "}"); + + LOGGER.info("Checking for " + imageName + " via " + s); baos.write(s.getBytes()); HttpPost serviceCreate = new HttpPost( @@ -99,7 +100,7 @@ private String createValidityCheckService(String serviceName, String imageName){ LOGGER.info("Pod " + serviceName + ": Already running"); } else { LOGGER.info("Pod " + serviceName + ": Not scheduled (" - + response.getStatusLine().toString() + ")"); + + response.getStatusLine().toString() + ":" + rootNode+ ")"); } } catch (IOException ioe) { LOGGER.error("Pod " + serviceName + ": Error scheduling pod", ioe); From 73fdcc192f7c6502c84cda4949b16db86eaad07c Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sun, 11 Sep 2016 11:48:25 -0400 Subject: [PATCH 22/29] Running and Partly Running both count to be in GC --- .../o19s/grandcentral/dockercloud/DockercloudRegistry.java | 3 +++ src/main/java/com/o19s/grandcentral/kubernetes/Pod.java | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java b/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java index 989dfef..63a52ed 100644 --- a/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java +++ b/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java @@ -52,6 +52,9 @@ public boolean imageExistsInRegistry(String dockerTag) throws Exception { imageExists = startService(podUUID); + //FIXME: Just try three times to delete the service, with 2, 8, 16 second timeouts.. + Thread.sleep(16000); // Need to give time for the service to finish starting before we nuke it. + deleteService(podUUID); return imageExists; diff --git a/src/main/java/com/o19s/grandcentral/kubernetes/Pod.java b/src/main/java/com/o19s/grandcentral/kubernetes/Pod.java index c6a5839..8f69236 100644 --- a/src/main/java/com/o19s/grandcentral/kubernetes/Pod.java +++ b/src/main/java/com/o19s/grandcentral/kubernetes/Pod.java @@ -52,8 +52,10 @@ public void setAddress(String address) { public String getStatus() { return status; } + //FIXME the Partly running is a Dockercloud thing, we have many bits that may or may not ALL be running. + // For example, a init script... public boolean isRunning() { - return status != null && status.equals("Running"); + return status != null && (status.equals("Running") || status.equals("Partly running")); } public long getLastRequest() { From eaa604d6c818abf1a7abe2587b625190348796de Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Wed, 28 Sep 2016 18:56:04 -0400 Subject: [PATCH 23/29] making starting up faster by backgrounding delete, use a better test example --- .../GrandCentralApplication2.java | 3 ++ .../dockercloud/DockercloudRegistry.java | 40 ++++++++++++++----- .../dockercloud/StackManager.java | 4 +- .../servlets/PodServletFilter.java | 5 ++- src/test/resources/docker-cloud.json | 8 ++-- src/test/resources/local-dockercloud.yml | 8 ++-- 6 files changed, 47 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/o19s/grandcentral/GrandCentralApplication2.java b/src/main/java/com/o19s/grandcentral/GrandCentralApplication2.java index 9099e2b..6a77441 100644 --- a/src/main/java/com/o19s/grandcentral/GrandCentralApplication2.java +++ b/src/main/java/com/o19s/grandcentral/GrandCentralApplication2.java @@ -40,6 +40,9 @@ public void initialize(Bootstrap bootstrap) { @Override public void run(GrandCentralConfiguration2 config, Environment environment) throws Exception { + + // FIXME: Should be a healthcheck that confirms access to DockerCloud. + System.out.println("Username:" + config.getDockercloudConfiguration().getUsername()); // Add health checks /* environment.healthChecks().register("container_registry", new ContainerRegistryHealthCheck( diff --git a/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java b/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java index 63a52ed..bdacf74 100644 --- a/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java +++ b/src/main/java/com/o19s/grandcentral/dockercloud/DockercloudRegistry.java @@ -3,6 +3,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.concurrent.CompletableFuture; import org.apache.http.HttpEntity; import org.apache.http.HttpStatus; @@ -42,20 +43,38 @@ public boolean imageExistsInRegistry(String dockerTag) throws Exception { LOGGER.info("Checking if Docker tag exists in registry: " + dockerTag); boolean imageExists = false; - String podUUID = null; String serviceName = "chk-" + dockerTag; String imageName = dockercloudConfiguration.getStackExistsTestImage() + ":" + dockerTag; - podUUID = createValidityCheckService(serviceName, imageName); - - imageExists = startService(podUUID); + final String podUUID = createValidityCheckService(serviceName, imageName); - //FIXME: Just try three times to delete the service, with 2, 8, 16 second timeouts.. - Thread.sleep(16000); // Need to give time for the service to finish starting before we nuke it. + imageExists = startService(podUUID); + - deleteService(podUUID); + + CompletableFuture futureDelete = CompletableFuture.supplyAsync( + () -> { + boolean deleteSuccessful = false; + try { + + int sleepTime = 2000; + // Simulate long running task + do { + sleepTime = sleepTime * 2;// Double each iteration. + LOGGER.info("Sleeping for " + sleepTime + "ms before delete on pod: " + podUUID); + Thread.sleep(sleepTime); + deleteSuccessful = deleteService(podUUID); + LOGGER.info("Was delete successful for " + podUUID +": " + deleteSuccessful); + + } + while (!deleteSuccessful && sleepTime <= 64000); + + + } catch (InterruptedException e) { } + return deleteSuccessful; + }); return imageExists; } @@ -114,8 +133,8 @@ private String createValidityCheckService(String serviceName, String imageName){ return podUUID; } - private void deleteService(String podUUID) { - + private boolean deleteService(String podUUID) { + boolean result = false; HttpDelete serviceDelete = new HttpDelete( dockercloudConfiguration.getProtocol() + "://" + dockercloudConfiguration.getHostname() @@ -135,12 +154,15 @@ private void deleteService(String podUUID) { if (status == HttpStatus.SC_ACCEPTED) { LOGGER.info("Pod " + podUUID + ": Deleted"); + result = true; } } catch (IOException ioe) { LOGGER.error("Pod " + podUUID + ": Error pod", ioe); } + + return result; } diff --git a/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java b/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java index 49fcc71..809d038 100644 --- a/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java +++ b/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java @@ -427,6 +427,7 @@ else if (left.getLastRequest() < right.getLastRequest()) * @throws Exception */ private void refreshPods() throws IOException { + HttpGet stacksGet = new HttpGet(dockercloudConfiguration.getProtocol() + "://" + dockercloudConfiguration.getHostname() + "/api/app/v1/stack/"); stacksGet.addHeader("accept", "application/json"); stacksGet.addHeader(BasicScheme.authenticate( @@ -439,13 +440,12 @@ private void refreshPods() throws IOException { HttpEntity entity = response.getEntity(); if (entity != null) { // Grab the write lock - System.out.println("writelock:" + writeLock.toString()); writeLock.lock(); if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { LOGGER.info("Stack status query suceeded"); } else { - LOGGER.error("Statck status query failed. (" + response.getStatusLine().toString() + ")"); + LOGGER.error("Stack status query failed. (" + response.getStatusLine().toString() + ")"); } try (InputStream responseBody = entity.getContent()) { diff --git a/src/main/java/com/o19s/grandcentral/servlets/PodServletFilter.java b/src/main/java/com/o19s/grandcentral/servlets/PodServletFilter.java index b74c37b..a347969 100644 --- a/src/main/java/com/o19s/grandcentral/servlets/PodServletFilter.java +++ b/src/main/java/com/o19s/grandcentral/servlets/PodServletFilter.java @@ -56,7 +56,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo String host = request.getHeader("Host"); String hostWithoutPort, dockerTag; - + // FIXME: if the host is null, should GC blow up? Can you have a null host? if (host != null && host.contains(":")) { hostWithoutPort = host.substring(0, host.indexOf(":")); } else { @@ -92,7 +92,8 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo } } } else { - return_error(servletResponse, HttpStatus.BAD_REQUEST_400, "Host Header was not specified or is invalid"); + LOGGER.info("Host:" + host + ", dockerTag:" + dockerTag + "," + "grandCentralDomain:" + this.grandCentralDomain); + return_error(servletResponse, HttpStatus.BAD_REQUEST_400, "Host Header was not specified or is invalid:" +"Host:" + host + ", dockerTag:" + dockerTag + "," + "grandCentralDomain:" + this.grandCentralDomain); } } catch (Exception e) { LOGGER.error("Exception filtering request", e); diff --git a/src/test/resources/docker-cloud.json b/src/test/resources/docker-cloud.json index 6093d33..276eeab 100644 --- a/src/test/resources/docker-cloud.json +++ b/src/test/resources/docker-cloud.json @@ -1,10 +1,10 @@ { - "name": "mysql", + "name": "apache", "services": [ { - "name":"mysql", - "image": "mysql:__DOCKER_TAG__", - "environment": ["MYSQL_ROOT_PASSWORD=root"] + "name":"apache", + "image": "eboraas/apache:__DOCKER_TAG__", + "ports": ["80:80"] } ] } diff --git a/src/test/resources/local-dockercloud.yml b/src/test/resources/local-dockercloud.yml index 2f99194..aa082d1 100644 --- a/src/test/resources/local-dockercloud.yml +++ b/src/test/resources/local-dockercloud.yml @@ -1,15 +1,15 @@ janitor_cleanup_threshold: 3600 maximum_stack_count: 2 -grandcentral_domain: datastart.grandcentral.com +grandcentral_domain: apache.grandcentral.com refresh_interval_in_ms: 300000 -pod_port: 81 +pod_port: 80 dockercloud: protocol: https hostname: cloud.docker.com - namespace: datastart + namespace: apache stack_json_path: src/test/resources/docker-cloud.json - stack_exists_test_image: dep4b/datastart + stack_exists_test_image: eboraas/apache username: ${DOCKERCLOUD_USERNAME} apikey: ${DOCKERCLOUD_APIKEY} From 86d3e43c0691f0dd61d19fa584e4dabe2fcad732 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Thu, 29 Sep 2016 08:01:32 -0400 Subject: [PATCH 24/29] instead of running admin on a seperate port, put it on main port, but under /grandcentral path --- src/test/resources/local-dockercloud.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test/resources/local-dockercloud.yml b/src/test/resources/local-dockercloud.yml index aa082d1..0068a06 100644 --- a/src/test/resources/local-dockercloud.yml +++ b/src/test/resources/local-dockercloud.yml @@ -13,3 +13,11 @@ dockercloud: stack_exists_test_image: eboraas/apache username: ${DOCKERCLOUD_USERNAME} apikey: ${DOCKERCLOUD_APIKEY} + +server: + type: simple + applicationContextPath: / + adminContextPath: /admin + connector: + type: http + port: 8080 From 24143547c51c72021b434972ab45e960f06cc11e Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Thu, 29 Sep 2016 08:01:45 -0400 Subject: [PATCH 25/29] fix up some docs on how to develop --- README_dockercloud.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README_dockercloud.md b/README_dockercloud.md index 134ed16..e847e6b 100644 --- a/README_dockercloud.md +++ b/README_dockercloud.md @@ -53,6 +53,23 @@ curl --user username:apikey "https://cloud.docker.com/api/app/v1/stack/" curl --user username:apikey "https://cloud.docker.com/api/app/v1/stack/" +## Development/Testing +There are a lot of moving pieces to GrandCentral, you need GC itself, plus the configuration to work with either Kubernetes or DockerCloud. This is how I set up my local environment: + +1. Set up your /etc/hosts with a couple of fake DNS entries that map to two released versions of Apache. You can see these tags at https://hub.docker.com/r/eboraas/apache/tags/. + +``` +127.0.0.1 latest.apache.grandcentral.com +127.0.0.1 stretch.apache.grandcentral.com +``` + +1. I like to run the core GrandCentral application in Eclipse in debug mode. Fortunately that is very easy. Just setup a _Java Application_ run/debug configuration. In the _Arguments_ tab tell Dropwizard to run in _server_ mode and pass in the configuration file: `server src/test/resources/local-dockercloud.yml`. Then in the _Enrivonment_ tab, add an entry for `DOCKERCLOUD_APIKEY` and `DOCKERCLOUD_USERNAME`. + +1. Fire up the application, and you'll see some startup checks that verify access to DockerCloud. + +1. Browse to http://latest.apache.grandcentral.com:8080 and in about 10 seconds you should see a default Debian Apache install page load up! Check your DockerCloud dashboard, you'll see the service fired up and running on an internal port. + + ## /etc/hosts Add to make testing your local set up easier this to your `/etc/hosts` file: From 69ba1ef509dc8e0805b88aa3867a2118804d108b Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Thu, 29 Sep 2016 09:46:38 -0400 Subject: [PATCH 26/29] Version bump --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a04e971..c23f371 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ 1.1-SNAPSHOT - 1.0.0 + 1.0.2 9.3.9.v20160517 4.12 From 00a265ec71e302dba4f6adc37d189ab6ae39d200 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Thu, 29 Sep 2016 09:46:55 -0400 Subject: [PATCH 27/29] fix the deleteing of extra stacks --- .../java/com/o19s/grandcentral/dockercloud/StackManager.java | 4 +++- src/test/resources/local-dockercloud.yml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java b/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java index 809d038..55884e1 100644 --- a/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java +++ b/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java @@ -357,7 +357,7 @@ public void remove(String dockerTag, String stackUUID) throws IOException { try (CloseableHttpResponse response = httpClient.execute(stackDelete)) { - if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_ACCEPTED) { LOGGER.info("Stack " + dockerTag + ": Removed"); Thread.sleep(10000); } else { @@ -465,6 +465,7 @@ private void refreshPods() throws IOException { String dockerTag = null; String podName = name; + String podUUID = serviceNode.get("uuid").asText(); String state = serviceNode.get("state").asText(); String servicesURI = null; if (serviceNode.get("services").size()> 0){ // A stack that is starting up may not yet have services! @@ -486,6 +487,7 @@ private void refreshPods() throws IOException { String publicDNS = getDNSForStack(servicesURI); pod = new Pod(dockerTag, publicDNS, state); + pod.setUuid(podUUID); } diff --git a/src/test/resources/local-dockercloud.yml b/src/test/resources/local-dockercloud.yml index 0068a06..02ea25a 100644 --- a/src/test/resources/local-dockercloud.yml +++ b/src/test/resources/local-dockercloud.yml @@ -1,5 +1,5 @@ janitor_cleanup_threshold: 3600 -maximum_stack_count: 2 +maximum_stack_count: 1 grandcentral_domain: apache.grandcentral.com refresh_interval_in_ms: 300000 From 10027770ad4c1b8ad3e8f007d7f95f8da1b6d2ad Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sat, 1 Oct 2016 16:44:54 -0400 Subject: [PATCH 28/29] mention why nodes are deleted --- README_dockercloud.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_dockercloud.md b/README_dockercloud.md index e847e6b..b23b905 100644 --- a/README_dockercloud.md +++ b/README_dockercloud.md @@ -67,7 +67,7 @@ There are a lot of moving pieces to GrandCentral, you need GC itself, plus the c 1. Fire up the application, and you'll see some startup checks that verify access to DockerCloud. -1. Browse to http://latest.apache.grandcentral.com:8080 and in about 10 seconds you should see a default Debian Apache install page load up! Check your DockerCloud dashboard, you'll see the service fired up and running on an internal port. +1. Browse to http://latest.apache.grandcentral.com:8080 and in about 10 seconds you should see a default Debian Apache install page load up! Check your DockerCloud dashboard, you'll see the service fired up and running on an internal port. Then, pull up http://stretch.apache.grandcentral.com:8080 and you'll see the new pod started, and the old pod deleted due to the _maximum_stack_count=1_. ## /etc/hosts From 08f41b3fdc633674f6a109e24d8f61c44cd16a67 Mon Sep 17 00:00:00 2001 From: Eric Pugh Date: Sat, 1 Oct 2016 16:45:22 -0400 Subject: [PATCH 29/29] need to pick the right front end service to point GC incoming traffic to, so reuse the stack_exists_test_image parameter! --- .../dockercloud/StackManager.java | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java b/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java index 55884e1..6617b6f 100644 --- a/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java +++ b/src/main/java/com/o19s/grandcentral/dockercloud/StackManager.java @@ -4,9 +4,11 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.Lock; @@ -36,7 +38,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.o19s.grandcentral.LinkedContainerManager; import com.o19s.grandcentral.http.HttpDelete; // IMPORTANT, allows DELETE requests with bodies import com.o19s.grandcentral.kubernetes.Pod; @@ -302,10 +303,13 @@ public Pod add(String dockerTag) throws Exception { String name = rootNode.get("name").asText(); String status = rootNode.get("state").asText(); JsonNode servicesNode = rootNode.get("services"); - - - String serviceURI = servicesNode.get(0).asText(); - String publicDNS = getDNSForStack(serviceURI); + List servicesURI = new ArrayList(); + for (JsonNode node : servicesNode){ + servicesURI.add(node.asText()); + } + + + String publicDNS = getTargetDNSForStack(dockercloudConfiguration.getStackExistsTestImage(),servicesURI); pod = new Pod(dockerTag, publicDNS, status); pod.setUuid(podUUID); @@ -467,11 +471,7 @@ private void refreshPods() throws IOException { String podName = name; String podUUID = serviceNode.get("uuid").asText(); String state = serviceNode.get("state").asText(); - String servicesURI = null; - if (serviceNode.get("services").size()> 0){ // A stack that is starting up may not yet have services! - servicesURI = serviceNode.get("services").get(0).asText(); - } - + if (name.indexOf("-")> -1){ String[] namebits = name.split("-"); if (namebits.length==2){ @@ -482,9 +482,16 @@ private void refreshPods() throws IOException { } } - if (dockerTag != null && !StringUtils.isBlank(servicesURI) && podName.contains(dockercloudConfiguration.getNamespace())){ + List servicesURI2 = new ArrayList(); + for (JsonNode node : serviceNode.get("services")){ + servicesURI2.add(node.asText()); + } + + + + if (dockerTag != null && !servicesURI2.isEmpty() && podName.contains(dockercloudConfiguration.getNamespace())){ - String publicDNS = getDNSForStack(servicesURI); + String publicDNS = getTargetDNSForStack(dockercloudConfiguration.getStackExistsTestImage(), servicesURI2); pod = new Pod(dockerTag, publicDNS, state); pod.setUuid(podUUID); @@ -529,8 +536,9 @@ private void refreshPods() throws IOException { lastRefresh = DateTime.now().getMillis(); } - private String getDNSForStack(String serviceURI){ - String publicDNS = null; + private String getTargetDNSForStack(String targetImage, List servicesURI) throws IOException{ + + for (String serviceURI : servicesURI){ HttpGet stackServices = new HttpGet(dockercloudConfiguration.getProtocol() + "://" + dockercloudConfiguration.getHostname() + serviceURI); stackServices.addHeader("accept", "application/json"); stackServices.addHeader(BasicScheme.authenticate( @@ -542,17 +550,22 @@ private String getDNSForStack(String serviceURI){ JsonNode rootNode2 = jsonObjectMapper.readTree(responseBody2); - publicDNS = rootNode2.get("public_dns").asText(); + if (rootNode2.get("image_name").asText().contains(targetImage)){ + String publicDNS = rootNode2.get("public_dns").asText(); + return publicDNS; + } } catch (IOException ioe) { LOGGER.error("Pod at " + serviceURI + ": Error starting pod", ioe); } - } catch (IOException ioe) { - LOGGER.error("Pod " + serviceURI + ": Error getting public dns for pod", ioe); - } - return publicDNS; + } catch (IOException ioe) { + LOGGER.error("Pod " + serviceURI + ": Error getting public dns for pod", ioe); + } + } + + throw new IOException("Dude, wheres my DNS"); } }