Skip to content

Commit

Permalink
feat(kubernetes): Add raw resources endpoint (#5057)
Browse files Browse the repository at this point in the history
* feat(kubernetes): Add raw resources endpoint

This allows a Spinnaker user to query every kind of resource for an application, so you could have a nice view (which we're working on) that shows:
secrets, hpas, config maps, webhooks, roles, pvcs, etc

The raw resource only has a provider for Kubernetes now but it's a generic format so it could very well support other providers

* Moved entire raw resource implementation to the Kubernetes provider

* Add rawResource provider + cacheAllRelationships tests

* Remove cloudProvider attribute
  • Loading branch information
julienduchesne committed Nov 17, 2020
1 parent c1b8bbe commit bd35114
Show file tree
Hide file tree
Showing 11 changed files with 457 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ public static void convertAsResource(
KubernetesSpinnakerKindMap kindMap,
Namer<KubernetesManifest> namer,
KubernetesManifest manifest,
List<KubernetesManifest> resourceRelationships) {
List<KubernetesManifest> resourceRelationships,
boolean cacheAllRelationships) {
KubernetesKind kind = manifest.getKind();
String name = manifest.getName();
String namespace = manifest.getNamespace();
Expand All @@ -127,8 +128,8 @@ public static void convertAsResource(
kubernetesCacheData.addItem(key, attributes);

SpinnakerKind spinnakerKind = kindMap.translateKubernetesKind(kind);
if (logicalRelationshipKinds.contains(spinnakerKind)
&& !Strings.isNullOrEmpty(moniker.getApp())) {

if (cacheAllRelationships || logicalRelationshipKinds.contains(spinnakerKind)) {
addLogicalRelationships(
kubernetesCacheData,
key,
Expand Down Expand Up @@ -166,6 +167,9 @@ private static void addLogicalRelationships(
Moniker moniker,
boolean hasClusterRelationship) {
String application = moniker.getApp();
if (Strings.isNullOrEmpty(application)) {
return;
}
Keys.CacheKey applicationKey = new Keys.ApplicationCacheKey(application);
kubernetesCacheData.addRelationship(infrastructureKey, applicationKey);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ protected CacheResult buildCacheResult(Map<KubernetesKind, List<KubernetesManife
credentials.getKubernetesSpinnakerKindMap(),
credentials.getNamer(),
rs,
relationships.getOrDefault(rs, ImmutableList.of()));
relationships.getOrDefault(rs, ImmutableList.of()),
credentials.isCacheAllApplicationRelationships());
} catch (RuntimeException e) {
log.warn("{}: Failure converting {}", getAgentType(), rs, e);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 2020 Coveo, 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.caching.view.model;

import com.google.common.collect.ImmutableMap;
import com.netflix.spinnaker.cats.cache.CacheData;
import com.netflix.spinnaker.clouddriver.kubernetes.caching.Keys;
import com.netflix.spinnaker.clouddriver.kubernetes.caching.agent.KubernetesCacheDataConverter;
import com.netflix.spinnaker.clouddriver.kubernetes.description.manifest.KubernetesApiVersion;
import com.netflix.spinnaker.clouddriver.kubernetes.description.manifest.KubernetesKind;
import com.netflix.spinnaker.clouddriver.kubernetes.description.manifest.KubernetesManifest;
import com.netflix.spinnaker.moniker.Moniker;
import java.util.Map;
import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import lombok.Value;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Value
public final class KubernetesRawResource implements KubernetesResource {
private static final Logger log = LoggerFactory.getLogger(KubernetesRawResource.class);
private final String account;
private final String name;
private final String namespace;
private final String displayName;
private final KubernetesApiVersion apiVersion;
private final KubernetesKind kind;
private final Map<String, String> labels;
private final Moniker moniker;
private final Long createdTime;

private KubernetesRawResource(KubernetesManifest manifest, String key, Moniker moniker) {
this.account = ((Keys.InfrastructureCacheKey) Keys.parseKey(key).get()).getAccount();
this.name = manifest.getFullResourceName();
this.displayName = manifest.getName();
this.apiVersion = manifest.getApiVersion();
this.kind = manifest.getKind();
this.namespace = manifest.getNamespace();
this.labels = ImmutableMap.copyOf(manifest.getLabels());
this.moniker = moniker;
this.createdTime = manifest.getCreationTimestampEpochMillis();
}

@Nullable
@ParametersAreNonnullByDefault
public static KubernetesRawResource fromCacheData(CacheData cd) {
KubernetesManifest manifest = KubernetesCacheDataConverter.getManifest(cd);
if (manifest == null) {
log.warn("Cache data {} inserted without a manifest", cd.getId());
return null;
}
Moniker moniker = KubernetesCacheDataConverter.getMoniker(cd);
return new KubernetesRawResource(manifest, cd.getId(), moniker);
}

public String getRegion() {
return namespace;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ Optional<CacheData> getSingleEntryWithRelationships(
ImmutableCollection<String> getRelationshipKeys(
CacheData cacheData, SpinnakerKind spinnakerKind) {
return relationshipTypes(spinnakerKind)
.flatMap(t -> relationshipKeys(cacheData, t))
.flatMap(t -> getRelationshipKeys(cacheData, t))
.collect(toImmutableSet());
}

Expand All @@ -126,13 +126,23 @@ private ImmutableMultimap<String, String> getRelationshipKeys(
Collection<CacheData> cacheData, String type) {
return cacheData.stream()
.collect(
flatteningToImmutableSetMultimap(CacheData::getId, cd -> relationshipKeys(cd, type)));
flatteningToImmutableSetMultimap(
CacheData::getId, cd -> getRelationshipKeys(cd, type)));
}

/** Gets the data for all relationships of a given type for a CacheData item. */
Collection<CacheData> getRelationships(CacheData cacheData, String relationshipType) {
return cache.getAll(
relationshipType, relationshipKeys(cacheData, relationshipType).collect(toImmutableSet()));
return getRelationships(
cacheData, relationshipType, getRelationshipKeys(cacheData, relationshipType));
}

/**
* Gets the data for all relationships of a given type for a CacheData item and all its
* relationship keys
*/
Collection<CacheData> getRelationships(
CacheData cacheData, String relationshipType, Stream<String> relationshipKeys) {
return cache.getAll(relationshipType, relationshipKeys.collect(toImmutableSet()));
}

/** Gets the data for all relationships of a given Spinnaker kind for a single CacheData item. */
Expand All @@ -141,6 +151,17 @@ ImmutableCollection<CacheData> getRelationships(
return getRelationships(ImmutableList.of(cacheData), spinnakerKind).get(cacheData.getId());
}

/** Gets the data for all relationships for a single CacheData item. */
ImmutableCollection<CacheData> getAllRelationships(CacheData cacheData) {
ImmutableList.Builder<CacheData> result = ImmutableList.builder();
cacheData
.getRelationships()
.forEach(
(kind, relationships) ->
result.addAll(getRelationships(cacheData, kind, relationships.stream())));
return result.build();
}

/**
* Gets the data for all relationships of a given Spinnaker kind for a collection of CacheData
* items.
Expand Down Expand Up @@ -175,7 +196,7 @@ private Multimap<String, CacheData> getRelationships(
}

/** Returns a stream of all relationships of a given type for a given CacheData. */
private Stream<String> relationshipKeys(CacheData cacheData, String type) {
private Stream<String> getRelationshipKeys(CacheData cacheData, String type) {
Collection<String> relationships = cacheData.getRelationships().get(type);
// Avoiding creating an Optional here as this is deeply nested in performance-sensitive code.
if (relationships == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2020 Coveo, 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.caching.view.provider;

import static com.netflix.spinnaker.clouddriver.kubernetes.caching.Keys.LogicalKind.APPLICATIONS;

import com.google.common.collect.ImmutableSet;
import com.netflix.spinnaker.cats.cache.CacheData;
import com.netflix.spinnaker.clouddriver.kubernetes.caching.Keys.ApplicationCacheKey;
import com.netflix.spinnaker.clouddriver.kubernetes.caching.view.model.KubernetesRawResource;
import com.netflix.spinnaker.clouddriver.kubernetes.config.KubernetesConfigurationProperties;
import com.netflix.spinnaker.clouddriver.kubernetes.config.RawResourcesEndpointConfig;
import java.util.Collection;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class KubernetesRawResourceProvider {
private final KubernetesCacheUtils cacheUtils;
private final RawResourcesEndpointConfig configuration;

@Autowired
KubernetesRawResourceProvider(
KubernetesCacheUtils cacheUtils, KubernetesConfigurationProperties globalConfig) {
this.cacheUtils = cacheUtils;
this.configuration = globalConfig.getRawResourcesEndpointConfig();
this.configuration.validate();
}

public Set<KubernetesRawResource> getApplicationRawResources(String application) {
return cacheUtils
.getSingleEntry(APPLICATIONS.toString(), ApplicationCacheKey.createKey(application))
.map(
applicationData ->
fromRawResourceCacheData(cacheUtils.getAllRelationships(applicationData)))
.orElseGet(ImmutableSet::of);
}

private Set<KubernetesRawResource> fromRawResourceCacheData(
Collection<CacheData> rawResourceData) {
Set<String> kinds = configuration.getKinds();
Set<String> omitKinds = configuration.getOmitKinds();
return rawResourceData.stream()
.map(KubernetesRawResource::fromCacheData)
.filter(Objects::nonNull)
.filter(
resource ->
(kinds.isEmpty() || kinds.contains(resource.getKind().toString()))
&& !omitKinds.contains(resource.getKind().toString()))
.collect(Collectors.toSet());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
public class KubernetesConfigurationProperties {
private static final int DEFAULT_CACHE_THREADS = 1;
private List<ManagedAccount> accounts = new ArrayList<>();
private RawResourcesEndpointConfig rawResourcesEndpointConfig = new RawResourcesEndpointConfig();

@Data
public static class ManagedAccount implements CredentialsDefinition {
Expand Down Expand Up @@ -56,6 +57,7 @@ public static class ManagedAccount implements CredentialsDefinition {
private List<String> omitKinds = new ArrayList<>();
private boolean onlySpinnakerManaged = false;
private Long cacheIntervalSeconds;
private boolean cacheAllApplicationRelationships = false;

public void validate() {
if (Strings.isNullOrEmpty(name)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2020 Coveo, 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.config;

import java.util.HashSet;
import java.util.Set;
import lombok.Data;

@Data
public class RawResourcesEndpointConfig {
private Set<String> kinds = new HashSet<>();
private Set<String> omitKinds = new HashSet<>();

public void validate() {
if (!omitKinds.isEmpty() && !kinds.isEmpty()) {
throw new IllegalArgumentException("At most one of 'kinds' and 'omitKinds' can be specified");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2020 Netflix, 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.controllers;

import com.netflix.spinnaker.clouddriver.kubernetes.caching.view.model.KubernetesRawResource;
import com.netflix.spinnaker.clouddriver.kubernetes.caching.view.provider.KubernetesRawResourceProvider;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
class RawResourceController {
private final KubernetesRawResourceProvider rawResourceProvider;

@Autowired
public RawResourceController(KubernetesRawResourceProvider rawResourceProvider) {
this.rawResourceProvider = rawResourceProvider;
}

@PreAuthorize("hasPermission(#application, 'APPLICATION', 'READ')")
@PostAuthorize("@authorizationSupport.filterForAccounts(returnObject)")
@RequestMapping(value = "/applications/{application}/rawResources", method = RequestMethod.GET)
List<KubernetesRawResource> list(@PathVariable String application) {
return new ArrayList<>(rawResourceProvider.getApplicationRawResources(application));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ public class KubernetesCredentials {

@Include @Getter private final boolean onlySpinnakerManaged;

@Include @Getter private final boolean cacheAllApplicationRelationships;

@Include private final boolean checkPermissionsOnStartup;

@Include @Getter private final List<KubernetesCachingPolicy> cachingPolicies;
Expand Down Expand Up @@ -193,6 +195,7 @@ private KubernetesCredentials(

this.debug = managedAccount.isDebug();
this.namer = manifestNamer;
this.cacheAllApplicationRelationships = managedAccount.isCacheAllApplicationRelationships();
}

/**
Expand Down
Loading

0 comments on commit bd35114

Please sign in to comment.