Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(provider/kuberenetes): V2 deployments #1868

Merged
merged 1 commit into from
Sep 1, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shoulda named it Actuator...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh man you're right...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or Deployinator

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upstaller

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"]
}
}