From 92b7ea39a5e609824185e9898be221c494784950 Mon Sep 17 00:00:00 2001 From: Lars Wander Date: Fri, 19 Oct 2018 10:44:15 -0400 Subject: [PATCH] feat(provider/kubernetes): dynamic target selection (#3058) --- .../clouddriver/model/ManifestProvider.java | 9 ++- .../model/NoopManifestProvider.java | 7 +++ .../view/provider/KubernetesCacheUtils.java | 5 ++ .../KubernetesV2ManifestProvider.java | 45 ++++++++++--- .../manifest/KubernetesManifest.java | 2 +- .../v2/op/handler/KubernetesHandler.java | 24 +++++++ .../controllers/ManifestController.java | 63 ++++++++++++++++++- 7 files changed, 145 insertions(+), 10 deletions(-) diff --git a/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/model/ManifestProvider.java b/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/model/ManifestProvider.java index 8615d643a3e..ec02587cb39 100644 --- a/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/model/ManifestProvider.java +++ b/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/model/ManifestProvider.java @@ -17,6 +17,13 @@ package com.netflix.spinnaker.clouddriver.model; -public interface ManifestProvider { +import java.util.List; + +public interface ManifestProvider { + enum Sort { + AGE, SIZE + } + T getManifest(String account, String location, String name); + List getClusterAndSortAscending(String account, String location, String kind, String app, String cluster, Sort sort); } diff --git a/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/model/NoopManifestProvider.java b/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/model/NoopManifestProvider.java index f9fab41c320..ee01b814648 100644 --- a/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/model/NoopManifestProvider.java +++ b/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/model/NoopManifestProvider.java @@ -17,9 +17,16 @@ package com.netflix.spinnaker.clouddriver.model; +import java.util.List; + public class NoopManifestProvider implements ManifestProvider { @Override public Manifest getManifest(String account, String location, String name) { return null; } + + @Override + public List getClusterAndSortAscending(String account, String location, String kind, String app, String cluster, Sort sort) { + return null; + } } diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/view/provider/KubernetesCacheUtils.java b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/view/provider/KubernetesCacheUtils.java index 6d9ef003fb0..d2e02aeee96 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/view/provider/KubernetesCacheUtils.java +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/view/provider/KubernetesCacheUtils.java @@ -37,6 +37,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -113,6 +114,10 @@ public Collection getAllRelationshipsOfSpinnakerKind(Collection loadRelationshipsFromCache(CacheData source, String relationshipType) { + return loadRelationshipsFromCache(Collections.singleton(source), relationshipType); + } + public Collection loadRelationshipsFromCache(Collection sources, String relationshipType) { List keys = cleanupCollection(sources).stream() .map(CacheData::getRelationships) diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/view/provider/KubernetesV2ManifestProvider.java b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/view/provider/KubernetesV2ManifestProvider.java index 61ac00dee7a..160b9b18207 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/view/provider/KubernetesV2ManifestProvider.java +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/view/provider/KubernetesV2ManifestProvider.java @@ -21,7 +21,6 @@ import com.netflix.spinnaker.clouddriver.kubernetes.v2.caching.Keys; import com.netflix.spinnaker.clouddriver.kubernetes.v2.caching.agent.KubernetesCacheDataConverter; import com.netflix.spinnaker.clouddriver.kubernetes.v2.caching.view.model.KubernetesV2Manifest; -import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesPodMetric; import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesResourceProperties; import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesResourcePropertyRegistry; import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.manifest.KubernetesKind; @@ -35,15 +34,18 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import java.util.Collection; +import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; +import static com.netflix.spinnaker.clouddriver.kubernetes.v2.caching.Keys.LogicalKind.CLUSTERS; + @Component @Slf4j public class KubernetesV2ManifestProvider implements ManifestProvider { @@ -84,6 +86,35 @@ public KubernetesV2Manifest getManifest(String account, String location, String } CacheData data = dataOptional.get(); + + return fromCacheData(data, account); + } + + @Override + public List getClusterAndSortAscending(String account, String location, String kind, String app, String cluster, Sort sort) { + KubernetesResourceProperties properties = registry.get(account, KubernetesKind.fromString(kind)); + if (properties == null) { + return null; + } + + KubernetesHandler handler = properties.getHandler(); + + return cacheUtils.getSingleEntry(CLUSTERS.toString(), Keys.cluster(account, app, cluster)) + .map(c -> cacheUtils.loadRelationshipsFromCache(c, kind).stream() + .map(cd -> fromCacheData(cd, account)) // todo(lwander) perf improvement by checking namespace before converting + .filter(Objects::nonNull) + .filter(m -> m.getLocation().equals(location)) + .sorted((m1, m2) -> handler.comparatorFor(sort).compare(m1.getManifest(), m2.getManifest())) + .collect(Collectors.toList())) + .orElse(new ArrayList<>()); + } + + private KubernetesV2Manifest fromCacheData(CacheData data, String account) { + KubernetesManifest manifest = KubernetesCacheDataConverter.getManifest(data); + String namespace = manifest.getNamespace(); + KubernetesKind kind = manifest.getKind(); + String key = data.getId(); + KubernetesResourceProperties properties = registry.get(account, kind); if (properties == null) { return null; @@ -97,19 +128,18 @@ public KubernetesV2Manifest getManifest(String account, String location, String .sorted(Comparator.comparing(lastEventTimestamp)) .collect(Collectors.toList()); - String metricKey = Keys.metric(kind, account, location, parsedName.getRight()); + Moniker moniker = KubernetesCacheDataConverter.getMoniker(data); + + String metricKey = Keys.metric(kind, account, namespace, manifest.getName()); List metrics = cacheUtils.getSingleEntry(Keys.Kind.KUBERNETES_METRIC.toString(), metricKey) .map(KubernetesCacheDataConverter::getMetrics) .orElse(Collections.emptyList()); KubernetesHandler handler = properties.getHandler(); - KubernetesManifest manifest = KubernetesCacheDataConverter.getManifest(data); - Moniker moniker = KubernetesCacheDataConverter.getMoniker(data); - return new KubernetesV2Manifest().builder() .account(account) - .location(location) + .location(namespace) .manifest(manifest) .moniker(moniker) .status(handler.status(manifest)) @@ -118,5 +148,6 @@ public KubernetesV2Manifest getManifest(String account, String location, String .warnings(handler.listWarnings(manifest)) .metrics(metrics) .build(); + } } diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/KubernetesManifest.java b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/KubernetesManifest.java index 26be14d869a..284a61a267d 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/KubernetesManifest.java +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/KubernetesManifest.java @@ -103,7 +103,7 @@ public void setNamespace(String namespace) { public String getCreationTimestamp() { return getMetadata().containsKey("creationTimestamp") ? getMetadata().get("creationTimestamp").toString() - : null; + : ""; } @JsonIgnore diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/handler/KubernetesHandler.java b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/handler/KubernetesHandler.java index d1286e06b28..7a8e3301795 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/handler/KubernetesHandler.java +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/handler/KubernetesHandler.java @@ -33,6 +33,7 @@ import com.netflix.spinnaker.clouddriver.kubernetes.v2.security.KubernetesV2Credentials; import com.netflix.spinnaker.clouddriver.model.Manifest.Status; import com.netflix.spinnaker.clouddriver.model.Manifest.Warning; +import com.netflix.spinnaker.clouddriver.model.ManifestProvider; import com.netflix.spinnaker.kork.artifacts.model.Artifact; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -41,6 +42,7 @@ import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Set; @@ -183,4 +185,26 @@ public static DeployPriority fromString(String val) { .orElseThrow(() -> new IllegalArgumentException("No such priority '" + val + "'")); } } + + public Comparator comparatorFor(ManifestProvider.Sort sort) { + switch (sort) { + case AGE: + return ageComparator(); + case SIZE: + return sizeComparator(); + default: + throw new IllegalArgumentException("No comparator for " + sort + " found"); + + } + } + + // can be overridden by each handler + protected Comparator ageComparator() { + return Comparator.comparing(KubernetesManifest::getCreationTimestamp); + } + + // can be overridden by each handler + protected Comparator sizeComparator() { + return Comparator.comparing(m -> m.getReplicas() == null ? -1 : m.getReplicas()); + } } diff --git a/clouddriver-web/src/main/groovy/com/netflix/spinnaker/clouddriver/controllers/ManifestController.java b/clouddriver-web/src/main/groovy/com/netflix/spinnaker/clouddriver/controllers/ManifestController.java index 2bd80432d16..417baf97e7f 100644 --- a/clouddriver-web/src/main/groovy/com/netflix/spinnaker/clouddriver/controllers/ManifestController.java +++ b/clouddriver-web/src/main/groovy/com/netflix/spinnaker/clouddriver/controllers/ManifestController.java @@ -19,8 +19,10 @@ import com.netflix.spinnaker.clouddriver.model.Manifest; import com.netflix.spinnaker.clouddriver.model.ManifestProvider; +import com.netflix.spinnaker.clouddriver.model.ManifestProvider.Sort; import com.netflix.spinnaker.clouddriver.requestqueue.RequestQueue; import com.netflix.spinnaker.kork.web.exceptions.NotFoundException; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PostAuthorize; @@ -87,7 +89,66 @@ Manifest getForAccountLocationAndName(@PathVariable String account, @RequestMapping(value = "/{account:.+}/{name:.+}", method = RequestMethod.GET) Manifest getForAccountLocationAndName(@PathVariable String account, - @PathVariable String name) { + @PathVariable String name) { return getForAccountLocationAndName(account, "", name); } + + @RequestMapping(value = "/{account:.+}/{location:.+}/{kind:.+}/cluster/{app:.+}/{cluster:.+}/dynamic/{criteria:.+}", method = RequestMethod.GET) + Manifest getDynamicManifestFromCluster(@PathVariable String account, + @PathVariable String location, + @PathVariable String kind, + @PathVariable String app, + @PathVariable String cluster, + @PathVariable Criteria criteria) { + final String request = String.format("(account: %s, location: %s, kind: %s, app %s, cluster: %s, criteria: %s)", account, location, kind, app, cluster, criteria); + List> manifestSet = manifestProviders.stream() + .map(p -> { + try { + return (List) requestQueue.execute(account, () -> p.getClusterAndSortAscending(account, location, kind, app, cluster, criteria.getSort())); + } catch (Throwable t) { + log.warn("Failed to read {}", request, t); + return null; + } + }).filter(l -> l != null && !l.isEmpty()) + .collect(Collectors.toList()); + + if (manifestSet.isEmpty()) { + throw new NotFoundException("No manifests matching " + request + " found"); + } else if (manifestSet.size() > 1) { + throw new IllegalStateException("Multiple sets of manifests matching " + request + " found"); + } + + List manifests = manifestSet.get(0); + try { + switch (criteria) { + case oldest: + case smallest: + return manifests.get(0); + case newest: + case largest: + return manifests.get(manifests.size() - 1); + case second_newest: + return manifests.get(manifests.size() - 2); + default: + throw new IllegalArgumentException("Unknown criteria: " + criteria); + } + } catch (IndexOutOfBoundsException e) { + throw new NotFoundException("No manifests matching " + request + " found"); + } + } + + enum Criteria { + oldest(Sort.AGE), + newest(Sort.AGE), + second_newest(Sort.AGE), + largest(Sort.SIZE), + smallest(Sort.SIZE); + + @Getter + private final Sort sort; + + Criteria(Sort sort) { + this.sort = sort; + } + } }