Skip to content

Commit

Permalink
feat(provider/kuberenetes): V2 deployments (#1868)
Browse files Browse the repository at this point in the history
  • Loading branch information
lwander authored Sep 1, 2017
1 parent 0650d1c commit daec1b1
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 17 deletions.
1 change: 1 addition & 0 deletions clouddriver-kubernetes/clouddriver-kubernetes.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -47,6 +48,9 @@ public class KubernetesManifestDeployer implements AtomicOperation<DeploymentRes
@Autowired
private KubernetesIngressDeployer ingressDeployer;

@Autowired
private KubernetesDeploymentDeployer deploymentDeployer;

public KubernetesManifestDeployer(KubernetesManifestOperationDescription description) {
this.description = description;
this.credentials = (KubernetesV2Credentials) description.getCredentials().getCredentials();
Expand Down Expand Up @@ -78,6 +82,8 @@ private KubernetesDeployer findDeployer(KubernetesKind kind) {
return serviceDeployer;
case INGRESS:
return ingressDeployer;
case DEPLOYMENT:
return deploymentDeployer;
default:
throw new IllegalArgumentException("Kind " + kind + " is not supported yet");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2017 Google, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package com.netflix.spinnaker.clouddriver.kubernetes.v2.op.deployer;

import com.netflix.spinnaker.clouddriver.kubernetes.v2.security.KubernetesV2Credentials;
import io.kubernetes.client.models.AppsV1beta1Deployment;
import org.springframework.stereotype.Component;

@Component
public class KubernetesDeploymentDeployer extends KubernetesDeployer<AppsV1beta1Deployment> {
@Override
Class<AppsV1beta1Deployment> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -31,6 +30,6 @@ Class<V1beta1Ingress> getDeployedClass() {

@Override
void deploy(KubernetesV2Credentials credentials, V1beta1Ingress resource) {
credentials.deployIngress(resource);
credentials.createIngress(resource);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ Class<V1beta1ReplicaSet> getDeployedClass() {

@Override
void deploy(KubernetesV2Credentials credentials, V1beta1ReplicaSet resource) {
credentials.deployReplicaSet(resource);
credentials.createReplicaSet(resource);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ Class<V1Service> getDeployedClass() {

@Override
void deploy(KubernetesV2Credentials credentials, V1Service resource) {
credentials.deployService(resource);
credentials.createService(resource);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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";
Expand All @@ -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);
}
Expand All @@ -78,31 +91,59 @@ public List<String> 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, () -> {
Expand All @@ -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> T runAndRecordMetrics(String methodName, String namespace, Supplier<T> op) {
T result = null;
Throwable failure = null;
Expand All @@ -129,8 +194,12 @@ private <T> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ metadata:
def result = deployOp.operate([])

then:
1 * credentialsMock.deployReplicaSet(_) >> null
1 * credentialsMock.createReplicaSet(_) >> null
result.serverGroupNames == ["$NAMESPACE:$KIND/$NAME"]
}

Expand All @@ -110,7 +110,7 @@ metadata:
def result = deployOp.operate([])

then:
1 * credentialsMock.deployReplicaSet(_) >> null
1 * credentialsMock.createReplicaSet(_) >> null
result.serverGroupNames == ["$BACKUP_NAMESPACE:$KIND/$NAME"]
}
}

0 comments on commit daec1b1

Please sign in to comment.