diff --git a/clouddriver-kubernetes/clouddriver-kubernetes.gradle b/clouddriver-kubernetes/clouddriver-kubernetes.gradle index db336dd5629..ef78b8bbd48 100644 --- a/clouddriver-kubernetes/clouddriver-kubernetes.gradle +++ b/clouddriver-kubernetes/clouddriver-kubernetes.gradle @@ -9,4 +9,5 @@ dependencies { // TODO(lwander) move to spinnaker-dependencies when library stabilizes compile 'io.kubernetes:client-java-util:0.1' + compile 'com.github.fge:json-patch:1.9' } diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/KubernetesManifestDeployer.java b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/KubernetesManifestDeployer.java index e28e91ac896..d74110373e6 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/KubernetesManifestDeployer.java +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/KubernetesManifestDeployer.java @@ -24,6 +24,7 @@ import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesKind; import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesManifestOperationDescription; import com.netflix.spinnaker.clouddriver.kubernetes.v2.op.deployer.KubernetesDeployer; +import com.netflix.spinnaker.clouddriver.kubernetes.v2.op.deployer.KubernetesDeploymentDeployer; import com.netflix.spinnaker.clouddriver.kubernetes.v2.op.deployer.KubernetesIngressDeployer; import com.netflix.spinnaker.clouddriver.kubernetes.v2.op.deployer.KubernetesReplicaSetDeployer; import com.netflix.spinnaker.clouddriver.kubernetes.v2.op.deployer.KubernetesServiceDeployer; @@ -47,6 +48,9 @@ public class KubernetesManifestDeployer implements AtomicOperation { + @Override + Class getDeployedClass() { + return AppsV1beta1Deployment.class; + } + + @Override + void deploy(KubernetesV2Credentials credentials, AppsV1beta1Deployment resource) { + String namespace = resource.getMetadata().getNamespace(); + String name = resource.getMetadata().getName(); + AppsV1beta1Deployment current = credentials.readDeployment(namespace, name); + if (current != null) { + credentials.patchDeployment(current, resource); + } else { + credentials.createDeployment(resource); + } + } +} diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/deployer/KubernetesIngressDeployer.java b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/deployer/KubernetesIngressDeployer.java index bc318fe56c3..f0d65412a2a 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/deployer/KubernetesIngressDeployer.java +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/deployer/KubernetesIngressDeployer.java @@ -19,7 +19,6 @@ import com.netflix.spinnaker.clouddriver.kubernetes.v2.security.KubernetesV2Credentials; import io.kubernetes.client.models.V1beta1Ingress; -import io.kubernetes.client.models.V1beta1ReplicaSet; import org.springframework.stereotype.Component; @Component @@ -31,6 +30,6 @@ Class getDeployedClass() { @Override void deploy(KubernetesV2Credentials credentials, V1beta1Ingress resource) { - credentials.deployIngress(resource); + credentials.createIngress(resource); } } diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/deployer/KubernetesReplicaSetDeployer.java b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/deployer/KubernetesReplicaSetDeployer.java index 9f4d87fd8e1..d3c7e4046cc 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/deployer/KubernetesReplicaSetDeployer.java +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/deployer/KubernetesReplicaSetDeployer.java @@ -30,6 +30,6 @@ Class getDeployedClass() { @Override void deploy(KubernetesV2Credentials credentials, V1beta1ReplicaSet resource) { - credentials.deployReplicaSet(resource); + credentials.createReplicaSet(resource); } } diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/deployer/KubernetesServiceDeployer.java b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/deployer/KubernetesServiceDeployer.java index 82c145c2b86..d9f44c7f22e 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/deployer/KubernetesServiceDeployer.java +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/deployer/KubernetesServiceDeployer.java @@ -30,6 +30,6 @@ Class getDeployedClass() { @Override void deploy(KubernetesV2Credentials credentials, V1Service resource) { - credentials.deployService(resource); + credentials.createService(resource); } } diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/security/KubernetesV2Credentials.java b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/security/KubernetesV2Credentials.java index 9478d6bfbf7..4e75729967d 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/security/KubernetesV2Credentials.java +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/security/KubernetesV2Credentials.java @@ -17,13 +17,19 @@ package com.netflix.spinnaker.clouddriver.kubernetes.v2.security; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.fge.jsonpatch.diff.JsonDiff; +import com.google.gson.Gson; import com.netflix.spectator.api.Clock; import com.netflix.spectator.api.Registry; import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesCredentials; import io.kubernetes.client.ApiClient; import io.kubernetes.client.ApiException; +import io.kubernetes.client.apis.AppsV1beta1Api; import io.kubernetes.client.apis.CoreV1Api; import io.kubernetes.client.apis.ExtensionsV1beta1Api; +import io.kubernetes.client.models.AppsV1beta1Deployment; import io.kubernetes.client.models.V1Service; import io.kubernetes.client.models.V1beta1Ingress; import io.kubernetes.client.models.V1beta1ReplicaSet; @@ -43,9 +49,15 @@ public class KubernetesV2Credentials implements KubernetesCredentials { private final ApiClient client; private final CoreV1Api coreV1Api; private final ExtensionsV1beta1Api extensionsV1beta1Api; + private final AppsV1beta1Api appsV1beta1Api; private final Registry registry; private final Clock clock; private final String accountName; + private final ObjectMapper mapper = new ObjectMapper(); + private final Gson gson = new Gson(); + private final String PRETTY = ""; + private final boolean EXACT = true; + private final boolean EXPORT = true; @Getter private final String defaultNamespace = "default"; @@ -60,6 +72,7 @@ public KubernetesV2Credentials(String accountName, Registry registry) { client.setDebugging(true); coreV1Api = new CoreV1Api(client); extensionsV1beta1Api = new ExtensionsV1beta1Api(client); + appsV1beta1Api = new AppsV1beta1Api(client); } catch (IOException e) { throw new RuntimeException("Failed to instantiate Kubernetes credentials", e); } @@ -78,31 +91,59 @@ public List getDeclaredNamespaces() { } } - public void deployReplicaSet(V1beta1ReplicaSet replicaSet) { - final String methodName = "replicaSets.create"; - final String namespace = replicaSet.getMetadata().getNamespace(); + private boolean notFound(ApiException e) { + return e.getCode() == 404; + } + + private Map[] determineJsonPatch(Object current, Object desired) { + JsonNode desiredNode = mapper.convertValue(desired, JsonNode.class); + JsonNode currentNode = mapper.convertValue(current, JsonNode.class); + + return mapper.convertValue(JsonDiff.asJson(currentNode, desiredNode), Map[].class); + } + + public void createDeployment(AppsV1beta1Deployment deployment) { + final String methodName = "deployments.create"; + final String namespace = deployment.getMetadata().getNamespace(); runAndRecordMetrics(methodName, namespace, () -> { try { - return extensionsV1beta1Api.createNamespacedReplicaSet(namespace, replicaSet, null); + return appsV1beta1Api.createNamespacedDeployment(namespace, deployment, null); } catch (ApiException e) { throw new KubernetesApiException(methodName, e); } }); } - public void deployService(V1Service service) { - final String methodName = "services.create"; - final String namespace = service.getMetadata().getNamespace(); + public void patchDeployment(AppsV1beta1Deployment current, AppsV1beta1Deployment desired) { + final String methodName = "deployments.patch"; + final String namespace = current.getMetadata().getNamespace(); + final String name = current.getMetadata().getName(); + final Map[] jsonPatch = determineJsonPatch(current, desired); runAndRecordMetrics(methodName, namespace, () -> { try { - return coreV1Api.createNamespacedService(namespace, service, null); + return appsV1beta1Api.patchNamespacedDeployment(name, namespace, jsonPatch, null); } catch (ApiException e) { throw new KubernetesApiException(methodName, e); } }); } - public void deployIngress(V1beta1Ingress ingress) { + public AppsV1beta1Deployment readDeployment(String namespace, String name) { + final String methodName = "deployments.read"; + return runAndRecordMetrics(methodName, namespace, () -> { + try { + return appsV1beta1Api.readNamespacedDeployment(name, namespace, PRETTY, EXACT, EXPORT); + } catch (ApiException e) { + if (notFound(e)) { + return null; + } + + throw new KubernetesApiException(methodName, e); + } + }); + } + + public void createIngress(V1beta1Ingress ingress) { final String methodName = "ingresses.create"; final String namespace = ingress.getMetadata().getNamespace(); runAndRecordMetrics(methodName, namespace, () -> { @@ -114,6 +155,30 @@ public void deployIngress(V1beta1Ingress ingress) { }); } + public void createReplicaSet(V1beta1ReplicaSet replicaSet) { + final String methodName = "replicaSets.create"; + final String namespace = replicaSet.getMetadata().getNamespace(); + runAndRecordMetrics(methodName, namespace, () -> { + try { + return extensionsV1beta1Api.createNamespacedReplicaSet(namespace, replicaSet, null); + } catch (ApiException e) { + throw new KubernetesApiException(methodName, e); + } + }); + } + + public void createService(V1Service service) { + final String methodName = "services.create"; + final String namespace = service.getMetadata().getNamespace(); + runAndRecordMetrics(methodName, namespace, () -> { + try { + return coreV1Api.createNamespacedService(namespace, service, null); + } catch (ApiException e) { + throw new KubernetesApiException(methodName, e); + } + }); + } + private T runAndRecordMetrics(String methodName, String namespace, Supplier op) { T result = null; Throwable failure = null; @@ -129,8 +194,12 @@ private T runAndRecordMetrics(String methodName, String namespace, Supplier< tags.put("method", methodName); tags.put("account", accountName); tags.put("namespace", StringUtils.isEmpty(namespace) ? "none" : namespace); - tags.put("success", failure == null ? "true" : "false"); - tags.put("reason", failure == null ? null : failure.getClass().getSimpleName() + ": " + failure.getMessage()); + if (failure == null) { + tags.put("success", "true"); + } else { + tags.put("success", "false"); + tags.put("reason", failure.getClass().getSimpleName() + ": " + failure.getMessage()); + } registry.timer(registry.createId("kubernetes.api", tags)) .record(clock.monotonicTime() - startTime, TimeUnit.NANOSECONDS); diff --git a/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/KubernetesManifestDeployerSpec.groovy b/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/KubernetesManifestDeployerSpec.groovy index f273a9d9cd8..66c09b30f29 100644 --- a/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/KubernetesManifestDeployerSpec.groovy +++ b/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/KubernetesManifestDeployerSpec.groovy @@ -96,7 +96,7 @@ metadata: def result = deployOp.operate([]) then: - 1 * credentialsMock.deployReplicaSet(_) >> null + 1 * credentialsMock.createReplicaSet(_) >> null result.serverGroupNames == ["$NAMESPACE:$KIND/$NAME"] } @@ -110,7 +110,7 @@ metadata: def result = deployOp.operate([]) then: - 1 * credentialsMock.deployReplicaSet(_) >> null + 1 * credentialsMock.createReplicaSet(_) >> null result.serverGroupNames == ["$BACKUP_NAMESPACE:$KIND/$NAME"] } }