diff --git a/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/agent/KubernetesCacheDataConverter.java b/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/agent/KubernetesCacheDataConverter.java index 8a943cd4f27..2aa1d14d82f 100644 --- a/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/agent/KubernetesCacheDataConverter.java +++ b/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/agent/KubernetesCacheDataConverter.java @@ -208,7 +208,7 @@ public static void convertAsResource( + ":" + manifest.getFullResourceName()); } else { - if (kindRegistry.getRegisteredKind(kind).hasClusterRelationship()) { + if (kindRegistry.getKindProperties(kind).hasClusterRelationship()) { addLogicalRelationships(kubernetesCacheData, key, account, moniker); } } @@ -324,7 +324,7 @@ private static void logMalformedManifest( } if (StringUtils.isEmpty(manifest.getNamespace()) - && kindRegistry.getRegisteredKind(manifest.getKind()).isNamespaced()) { + && kindRegistry.getKindProperties(manifest.getKind()).isNamespaced()) { log.warn("{}: manifest namespace may not be null, {}", contextMessage.get(), manifest); } } diff --git a/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/agent/KubernetesUnregisteredCustomResourceCachingAgent.java b/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/agent/KubernetesUnregisteredCustomResourceCachingAgent.java index ad257225de0..59f44782540 100644 --- a/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/agent/KubernetesUnregisteredCustomResourceCachingAgent.java +++ b/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/agent/KubernetesUnregisteredCustomResourceCachingAgent.java @@ -54,7 +54,7 @@ public Collection getProvidedDataTypes() { @Override protected List primaryKinds() { - return credentials.getCrds().stream() + return credentials.getCrds().keySet().stream() .filter(credentials::isValidKind) .collect(Collectors.toList()); } diff --git a/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/agent/KubernetesV2OnDemandCachingAgent.java b/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/agent/KubernetesV2OnDemandCachingAgent.java index 15b25bc3f55..1cf9c301dea 100644 --- a/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/agent/KubernetesV2OnDemandCachingAgent.java +++ b/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/agent/KubernetesV2OnDemandCachingAgent.java @@ -302,7 +302,7 @@ public OnDemandAgent.OnDemandResult handle(ProviderCache providerCache, Map { - log.info( - "Dynamically registering {}, (namespaced: {})", - kubernetesKind.toString(), - customResource.isNamespaced()); - return KubernetesKindProperties.create(kubernetesKind, customResource.isNamespaced()); - }); - KubernetesHandler handler = CustomKubernetesHandlerFactory.create( - kubernetesKind, + KubernetesKind.fromString(customResource.getKubernetesKind()), SpinnakerKind.fromString(customResource.getSpinnakerKind()), customResource.isVersioned(), deployPriorityValue); diff --git a/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/GlobalKubernetesKindRegistry.java b/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/GlobalKubernetesKindRegistry.java index 2236b56df7d..28dc9d4fbfe 100644 --- a/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/GlobalKubernetesKindRegistry.java +++ b/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/GlobalKubernetesKindRegistry.java @@ -48,7 +48,7 @@ public GlobalKubernetesKindRegistry( * that were registered for the kind; otherwise, returns an empty {@link Optional}. */ @Nonnull - public Optional getRegisteredKind(@Nonnull KubernetesKind kind) { + public Optional getKindProperties(@Nonnull KubernetesKind kind) { return Optional.ofNullable(nameMap.get(kind)); } diff --git a/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/KubernetesKind.java b/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/KubernetesKind.java index b12f71e6a30..a10724f192d 100644 --- a/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/KubernetesKind.java +++ b/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/KubernetesKind.java @@ -19,15 +19,18 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; +import io.kubernetes.client.models.V1beta1CustomResourceDefinition; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; import lombok.EqualsAndHashCode; import lombok.Getter; import org.apache.commons.lang3.StringUtils; @EqualsAndHashCode(onlyExplicitlyIncluded = true) +@ParametersAreNonnullByDefault public class KubernetesKind { private static final Map aliasMap = new ConcurrentHashMap<>(); @@ -103,7 +106,7 @@ public class KubernetesKind { @Getter @Nonnull private final KubernetesApiGroup apiGroup; @EqualsAndHashCode.Include @Nullable private final KubernetesApiGroup customApiGroup; - private KubernetesKind(@Nonnull String name, @Nullable KubernetesApiGroup apiGroup) { + private KubernetesKind(String name, @Nullable KubernetesApiGroup apiGroup) { this.name = name; this.lcName = name.toLowerCase(); this.apiGroup = apiGroup == null ? KubernetesApiGroup.NONE : apiGroup; @@ -115,7 +118,7 @@ private KubernetesKind(@Nonnull String name, @Nullable KubernetesApiGroup apiGro } private static KubernetesKind createWithAlias( - @Nonnull String name, @Nullable String alias, @Nullable KubernetesApiGroup apiGroup) { + String name, @Nullable String alias, @Nullable KubernetesApiGroup apiGroup) { KubernetesKind kind = new KubernetesKind(name, apiGroup); aliasMap.put(kind, kind); if (alias != null) { @@ -124,6 +127,7 @@ private static KubernetesKind createWithAlias( return kind; } + @Nonnull public static KubernetesKind from(@Nullable String name, @Nullable KubernetesApiGroup apiGroup) { if (name == null || name.isEmpty()) { return KubernetesKind.NONE; @@ -132,9 +136,16 @@ public static KubernetesKind from(@Nullable String name, @Nullable KubernetesApi return aliasMap.getOrDefault(result, result); } + @Nonnull + public static KubernetesKind fromCustomResourceDefinition(V1beta1CustomResourceDefinition crd) { + return from( + crd.getSpec().getNames().getKind(), + KubernetesApiGroup.fromString(crd.getSpec().getGroup())); + } + @Nonnull @JsonCreator - public static KubernetesKind fromString(@Nonnull String qualifiedKind) { + public static KubernetesKind fromString(String qualifiedKind) { KubernetesApiGroup apiGroup; String kindName; String[] parts = StringUtils.split(qualifiedKind, ".", 2); diff --git a/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/KubernetesKindProperties.java b/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/KubernetesKindProperties.java index a1890790515..840a97833ad 100644 --- a/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/KubernetesKindProperties.java +++ b/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/KubernetesKindProperties.java @@ -17,6 +17,7 @@ package com.netflix.spinnaker.clouddriver.kubernetes.v2.description.manifest; import com.google.common.collect.ImmutableList; +import io.kubernetes.client.models.V1beta1CustomResourceDefinition; import java.util.List; import javax.annotation.Nonnull; import javax.annotation.ParametersAreNonnullByDefault; @@ -76,6 +77,7 @@ private KubernetesKindProperties( this.hasClusterRelationship = hasClusterRelationship; } + @Nonnull public static KubernetesKindProperties withDefaultProperties(KubernetesKind kubernetesKind) { return new KubernetesKindProperties(kubernetesKind, true, false); } @@ -86,6 +88,14 @@ public static KubernetesKindProperties create( return new KubernetesKindProperties(kubernetesKind, isNamespaced, false); } + @Nonnull + public static KubernetesKindProperties fromCustomResourceDefinition( + V1beta1CustomResourceDefinition crd) { + return create( + KubernetesKind.fromCustomResourceDefinition(crd), + crd.getSpec().getScope().equalsIgnoreCase("namespaced")); + } + public boolean hasClusterRelationship() { return this.hasClusterRelationship; } diff --git a/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/KubernetesKindRegistry.java b/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/KubernetesKindRegistry.java index 23bbd2a0828..da6daf9b1e6 100644 --- a/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/KubernetesKindRegistry.java +++ b/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/KubernetesKindRegistry.java @@ -17,55 +17,73 @@ package com.netflix.spinnaker.clouddriver.kubernetes.v2.description.manifest; import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableList; +import java.util.Collection; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Supplier; +import java.util.function.Function; import javax.annotation.Nonnull; +import javax.annotation.ParametersAreNonnullByDefault; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +@ParametersAreNonnullByDefault +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Slf4j public class KubernetesKindRegistry { private final Map kindMap = new ConcurrentHashMap<>(); private final GlobalKubernetesKindRegistry globalKindRegistry; + private final Function> crdLookup; - private KubernetesKindRegistry(GlobalKubernetesKindRegistry globalKindRegistry) { + private KubernetesKindRegistry( + GlobalKubernetesKindRegistry globalKindRegistry, + Function> crdLookup, + Collection customProperties) { this.globalKindRegistry = globalKindRegistry; + this.crdLookup = crdLookup; + customProperties.forEach(this::registerKind); } /** Registers a given {@link KubernetesKindProperties} into the registry */ - public void registerKind(@Nonnull KubernetesKindProperties kind) { - kindMap.put(kind.getKubernetesKind(), kind); + private KubernetesKindProperties registerKind(KubernetesKindProperties kindProperties) { + return kindMap.computeIfAbsent( + kindProperties.getKubernetesKind(), + k -> { + log.info( + "Dynamically registering {}, (namespaced: {})", + kindProperties.getKubernetesKind().toString(), + kindProperties.isNamespaced()); + return kindProperties; + }); } /** * Searches the registry for a {@link KubernetesKindProperties} with the supplied {@link * KubernetesKind}. If the kind has been registered, returns the {@link KubernetesKindProperties} - * that were registered for the kind; otherwise, calls the provided {@link Supplier} and registers - * the resulting {@link KubernetesKindProperties}. + * that were registered for the kind. If the kind is not registered, tries to look up the + * properties using the registry's CRD lookup function. If the lookup returns properties, + * registers them for this kind and returns them; otherwise returns a {@link + * KubernetesKindProperties} with default properties. */ @Nonnull - public KubernetesKindProperties getOrRegisterKind( - @Nonnull KubernetesKind kind, @Nonnull Supplier supplier) { - return kindMap.computeIfAbsent(kind, k -> supplier.get()); - } + public KubernetesKindProperties getKindProperties(KubernetesKind kind) { + Optional globalResult = globalKindRegistry.getKindProperties(kind); + if (globalResult.isPresent()) { + return globalResult.get(); + } - /** - * Searches the registry for a {@link KubernetesKindProperties} with the supplied {@link - * KubernetesKind}. If the kind has been registered, returns the {@link KubernetesKindProperties} - * that were registered for the kind; otherwise, looks for the kind in the {@link - * GlobalKubernetesKindRegistry} and returns the properties found there. If the kind is not - * registered either globally or in the account, returns a {@link KubernetesKindProperties} with - * default properties. - */ - @Nonnull - public KubernetesKindProperties getRegisteredKind(@Nonnull KubernetesKind kind) { KubernetesKindProperties result = kindMap.get(kind); if (result != null) { return result; } - return globalKindRegistry - .getRegisteredKind(kind) - .orElse(KubernetesKindProperties.withDefaultProperties(kind)); + return crdLookup + .apply(kind) + .map(this::registerKind) + .orElseGet(() -> KubernetesKindProperties.withDefaultProperties(kind)); } /** Returns a list of all global kinds */ @@ -82,9 +100,17 @@ public Factory(GlobalKubernetesKindRegistry globalKindRegistry) { this.globalKindRegistry = globalKindRegistry; } + @Nonnull + public KubernetesKindRegistry create( + Function> crdLookup, + Collection customProperties) { + return new KubernetesKindRegistry(globalKindRegistry, crdLookup, customProperties); + } + @Nonnull public KubernetesKindRegistry create() { - return new KubernetesKindRegistry(globalKindRegistry); + return new KubernetesKindRegistry( + globalKindRegistry, k -> Optional.empty(), ImmutableList.of()); } } } diff --git a/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/manifest/KubernetesDeployManifestOperation.java b/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/manifest/KubernetesDeployManifestOperation.java index b6bc2034a4f..56e6e8dea9b 100644 --- a/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/manifest/KubernetesDeployManifestOperation.java +++ b/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/op/manifest/KubernetesDeployManifestOperation.java @@ -100,7 +100,7 @@ public OperationResult operate(List _unused) { validateManifestsForRolloutStrategies(inputManifests); for (KubernetesManifest manifest : inputManifests) { - if (credentials.getKindRegistry().getRegisteredKind(manifest.getKind()).isNamespaced()) { + if (credentials.getKindRegistry().getKindProperties(manifest.getKind()).isNamespaced()) { if (!StringUtils.isEmpty(description.getNamespaceOverride())) { manifest.setNamespace(description.getNamespaceOverride()); } else if (StringUtils.isEmpty(manifest.getNamespace())) { diff --git a/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/security/KubernetesV2Credentials.java b/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/security/KubernetesV2Credentials.java index 1ffdc3d25a9..11d828874d5 100644 --- a/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/security/KubernetesV2Credentials.java +++ b/clouddriver-kubernetes-v2/src/main/java/com/netflix/spinnaker/clouddriver/kubernetes/v2/security/KubernetesV2Credentials.java @@ -17,6 +17,8 @@ package com.netflix.spinnaker.clouddriver.kubernetes.v2.security; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.google.common.collect.ImmutableSet.toImmutableSet; import static lombok.EqualsAndHashCode.Include; @@ -24,7 +26,7 @@ import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; import com.google.common.base.Suppliers; -import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.netflix.spectator.api.Clock; import com.netflix.spectator.api.Registry; @@ -35,6 +37,7 @@ import com.netflix.spinnaker.clouddriver.kubernetes.security.KubeconfigFileHasher; import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesCredentialFactory; import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesCredentials; +import com.netflix.spinnaker.clouddriver.kubernetes.v2.caching.agent.KubernetesCacheDataConverter; import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.AccountResourcePropertyRegistry; import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.JsonPatch; import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesPatchOptions; @@ -42,7 +45,6 @@ import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesResourceProperties; import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesSpinnakerKindMap; import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.ResourcePropertyRegistry; -import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.manifest.KubernetesApiGroup; import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.manifest.KubernetesKind; import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.manifest.KubernetesKindProperties; import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.manifest.KubernetesKindRegistry; @@ -52,6 +54,7 @@ import com.netflix.spinnaker.clouddriver.names.NamerRegistry; import com.netflix.spinnaker.kork.configserver.ConfigFileService; import io.kubernetes.client.models.V1DeleteOptions; +import io.kubernetes.client.models.V1beta1CustomResourceDefinition; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -60,6 +63,7 @@ import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; import javax.annotation.Nonnull; @@ -125,24 +129,33 @@ public class KubernetesV2Credentials implements KubernetesCredentials { private String cachedDefaultNamespace; private final Supplier> liveNamespaceSupplier; - private final Supplier> liveCrdSupplier; @Getter private final ResourcePropertyRegistry resourcePropertyRegistry; @Getter private final KubernetesKindRegistry kindRegistry; private final KubernetesSpinnakerKindMap kubernetesSpinnakerKindMap; private final PermissionValidator permissionValidator; + private final Supplier> crdSupplier = + Suppliers.memoizeWithExpiration(this::crdSupplier, CRD_EXPIRY_SECONDS, TimeUnit.SECONDS); private KubernetesV2Credentials( Registry registry, KubectlJobExecutor jobExecutor, KubernetesConfigurationProperties.ManagedAccount managedAccount, AccountResourcePropertyRegistry.Factory resourcePropertyRegistryFactory, - KubernetesKindRegistry kindRegistry, + KubernetesKindRegistry.Factory kindRegistryFactory, KubernetesSpinnakerKindMap kubernetesSpinnakerKindMap, String kubeconfigFile) { this.registry = registry; this.clock = registry.clock(); this.jobExecutor = jobExecutor; - this.kindRegistry = kindRegistry; + this.kindRegistry = + kindRegistryFactory.create( + this::getCrdProperties, + managedAccount.getCustomResources().stream() + .map( + cr -> + KubernetesKindProperties.create( + KubernetesKind.fromString(cr.getKubernetesKind()), cr.isNamespaced())) + .collect(toImmutableList())); this.accountName = managedAccount.getName(); this.namespaces = managedAccount.getNamespaces(); @@ -150,19 +163,7 @@ private KubernetesV2Credentials( this.kinds = managedAccount.getKinds().stream() .map(KubernetesKind::fromString) - .map( - k -> - kindRegistry.getOrRegisterKind( - k, - () -> { - log.info( - "Dynamically registering {}, (namespaced: {})", k.toString(), true); - return KubernetesKindProperties.create(k, true); - })) - .map(KubernetesKindProperties::getKubernetesKind) .collect(toImmutableSet()); - // omitKinds is a simple placeholder that we can use to compare one instance to another - // when refreshing credentials. this.omitKinds = managedAccount.getOmitKinds().stream() .map(KubernetesKind::fromString) @@ -173,8 +174,8 @@ private KubernetesV2Credentials( this.resourcePropertyRegistry = resourcePropertyRegistryFactory.create( managedAccount.getCustomResources().stream() - .map(cr -> KubernetesResourceProperties.fromCustomResource(cr, kindRegistry)) - .collect(ImmutableList.toImmutableList())); + .map(KubernetesResourceProperties::fromCustomResource) + .collect(toImmutableList())); this.kubernetesSpinnakerKindMap = kubernetesSpinnakerKindMap; this.kubectlExecutable = managedAccount.getKubectlExecutable(); @@ -217,47 +218,6 @@ private KubernetesV2Credentials( }, NAMESPACE_EXPIRY_SECONDS, TimeUnit.SECONDS); - - this.liveCrdSupplier = - Memoizer.memoizeWithExpiration( - () -> { - try { - return this.list(KubernetesKind.CUSTOM_RESOURCE_DEFINITION, "").stream() - .map( - c -> { - Map spec = - (Map) c.getOrDefault("spec", new HashMap<>()); - String scope = (String) spec.getOrDefault("scope", ""); - Map names = - (Map) spec.getOrDefault("names", new HashMap<>()); - String name = names.get("kind"); - - String group = (String) spec.getOrDefault("group", ""); - KubernetesApiGroup kubernetesApiGroup = - KubernetesApiGroup.fromString(group); - boolean isNamespaced = scope.equalsIgnoreCase("namespaced"); - - KubernetesKind kind = KubernetesKind.from(name, kubernetesApiGroup); - return kindRegistry.getOrRegisterKind( - kind, - () -> { - log.info( - "Dynamically registering {}, (namespaced: {})", - kind.toString(), - isNamespaced); - return KubernetesKindProperties.create(kind, isNamespaced); - }); - }) - .map(KubernetesKindProperties::getKubernetesKind) - .collect(Collectors.toList()); - } catch (KubectlException e) { - // not logging here -- it will generate a lot of noise in cases where crds aren't - // available/registered in the first place - return new ArrayList<>(); - } - }, - CRD_EXPIRY_SECONDS, - TimeUnit.SECONDS); } /** @@ -334,6 +294,11 @@ public KubernetesKindStatus getKindStatus(@Nonnull KubernetesKind kind) { : KubernetesKindStatus.READ_ERROR; } + private Optional getCrdProperties( + @Nonnull KubernetesKind kubernetesKind) { + return Optional.ofNullable(getCrds().get(kubernetesKind)); + } + public String getDefaultNamespace() { if (StringUtils.isEmpty(cachedDefaultNamespace)) { cachedDefaultNamespace = lookupDefaultNamespace(); @@ -377,8 +342,31 @@ private String lookupDefaultNamespace() { } } - public List getCrds() { - return liveCrdSupplier.get(); + @Nonnull + public ImmutableMap getCrds() { + return crdSupplier.get(); + } + + @Nonnull + private ImmutableMap crdSupplier() { + // Short-circuit if the account is not configured (or does not have permission) to read CRDs + if (!isValidKind(KubernetesKind.CUSTOM_RESOURCE_DEFINITION)) { + return ImmutableMap.of(); + } + try { + return list(KubernetesKind.CUSTOM_RESOURCE_DEFINITION, "").stream() + .map( + manifest -> + KubernetesCacheDataConverter.getResource( + manifest, V1beta1CustomResourceDefinition.class)) + .map(KubernetesKindProperties::fromCustomResourceDefinition) + .collect( + toImmutableMap(KubernetesKindProperties::getKubernetesKind, Function.identity())); + } catch (KubectlException e) { + // not logging here -- it will generate a lot of noise in cases where crds aren't + // available/registered in the first place + return ImmutableMap.of(); + } } @Override @@ -668,7 +656,7 @@ private boolean canReadKind(KubernetesKind kind) { } log.info("Checking if {} is readable in account '{}'...", kind, accountName); try { - if (kindRegistry.getRegisteredKind(kind).isNamespaced()) { + if (kindRegistry.getKindProperties(kind).isNamespaced()) { list(kind, checkNamespace.get()); } else { list(kind, null); @@ -744,7 +732,7 @@ public KubernetesV2Credentials build( jobExecutor, managedAccount, resourcePropertyRegistryFactory, - kindRegistryFactory.create(), + kindRegistryFactory, kubernetesSpinnakerKindMap, getKubeconfigFile(configFileService, managedAccount)); } diff --git a/clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/GlobalKubernetesKindRegistrySpec.groovy b/clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/GlobalKubernetesKindRegistrySpec.groovy index 31701c4736d..a9889d49947 100644 --- a/clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/GlobalKubernetesKindRegistrySpec.groovy +++ b/clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/GlobalKubernetesKindRegistrySpec.groovy @@ -60,7 +60,7 @@ class GlobalKubernetesKindRegistrySpec extends Specification { ]) when: - def properties = kindRegistry.getRegisteredKind(KubernetesKind.from("customKind", CUSTOM_API_GROUP)) + def properties = kindRegistry.getKindProperties(KubernetesKind.from("customKind", CUSTOM_API_GROUP)) then: properties.get() == CUSTOM_KIND @@ -74,7 +74,7 @@ class GlobalKubernetesKindRegistrySpec extends Specification { ]) when: - def properties = kindRegistry.getRegisteredKind(KubernetesKind.from("otherKind", CUSTOM_API_GROUP)) + def properties = kindRegistry.getKindProperties(KubernetesKind.from("otherKind", CUSTOM_API_GROUP)) then: !properties.isPresent() diff --git a/clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/KubernetesAccountResolverSpec.groovy b/clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/KubernetesAccountResolverSpec.groovy index ed74d5627ca..654b0269eba 100644 --- a/clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/KubernetesAccountResolverSpec.groovy +++ b/clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/KubernetesAccountResolverSpec.groovy @@ -128,7 +128,7 @@ class KubernetesAccountResolverSpec extends Specification { getKindRegistry() >> v2KindRegistry } } - 0 * kindRegistryFactory.create() + 0 * kindRegistryFactory.create(*_) registry == v2KindRegistry when: @@ -138,7 +138,7 @@ class KubernetesAccountResolverSpec extends Specification { 1 * credentialsRepository.getOne(ACCOUNT_NAME) >> Mock(KubernetesNamedAccountCredentials) { getCredentials() >> Mock(KubernetesCredentials) } - 1 * kindRegistryFactory.create() >> v2KindRegistry + 1 * kindRegistryFactory.create(*_) >> v2KindRegistry registry == v2KindRegistry when: @@ -148,7 +148,7 @@ class KubernetesAccountResolverSpec extends Specification { 1 * credentialsRepository.getOne(ACCOUNT_NAME) >> Mock(AccountCredentials) { getCredentials() >> Mock(KubernetesCredentials) } - 1 * kindRegistryFactory.create() >> v2KindRegistry + 1 * kindRegistryFactory.create(*_) >> v2KindRegistry registry == v2KindRegistry @@ -157,7 +157,7 @@ class KubernetesAccountResolverSpec extends Specification { then: 1 * credentialsRepository.getOne(ACCOUNT_NAME) >> null - 1 * kindRegistryFactory.create() >> v2KindRegistry + 1 * kindRegistryFactory.create(*_) >> v2KindRegistry registry == v2KindRegistry } } diff --git a/clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/KubernetesKindRegistrySpec.groovy b/clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/KubernetesKindRegistrySpec.groovy index 74bdbae9e6d..002426a6b7c 100644 --- a/clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/KubernetesKindRegistrySpec.groovy +++ b/clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/KubernetesKindRegistrySpec.groovy @@ -20,9 +20,11 @@ import com.google.common.collect.ImmutableList import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.manifest.* import spock.lang.Specification import spock.lang.Subject -import spock.lang.Unroll + +import java.util.function.Function class KubernetesKindRegistrySpec extends Specification { + static final Function> NOOP_CRD_LOOKUP = { k -> Optional.empty() } static final KubernetesApiGroup CUSTOM_API_GROUP = KubernetesApiGroup.fromString("test") static final KubernetesKind CUSTOM_KIND = KubernetesKind.from("customKind", CUSTOM_API_GROUP) static final KubernetesKindProperties REPLICA_SET_PROPERTIES = KubernetesKindProperties.withDefaultProperties(KubernetesKind.REPLICA_SET) @@ -31,41 +33,43 @@ class KubernetesKindRegistrySpec extends Specification { final GlobalKubernetesKindRegistry globalRegistry = Mock(GlobalKubernetesKindRegistry) final KubernetesKindRegistry.Factory factory = new KubernetesKindRegistry.Factory(globalRegistry) - @Unroll - void "getRegisteredKind returns kinds that have been registered, and falls back to the global registry otherwise"() { + void "getRegisteredKind returns kinds that have been registered"() { given: - @Subject KubernetesKindRegistry kindRegistry = factory.create() + @Subject KubernetesKindRegistry kindRegistry = factory.create(NOOP_CRD_LOOKUP, ImmutableList.of(CUSTOM_KIND_PROPERTIES)) KubernetesKindProperties result - KubernetesKindProperties globalResult = KubernetesKindProperties.create(CUSTOM_KIND, false) when: - result = kindRegistry.getRegisteredKind(CUSTOM_KIND) + result = kindRegistry.getKindProperties(CUSTOM_KIND) then: - 1 * globalRegistry.getRegisteredKind(CUSTOM_KIND) >> Optional.of(globalResult) - result == globalResult + _ * globalRegistry.getKindProperties(_ as KubernetesKind) >> Optional.empty() + result == CUSTOM_KIND_PROPERTIES + } + + void "getRegisteredKind falls back to the global registry for kinds that are not registered"() { + given: + @Subject KubernetesKindRegistry kindRegistry = factory.create() + KubernetesKindProperties result + KubernetesKindProperties globalResult = KubernetesKindProperties.create(CUSTOM_KIND, false) when: - kindRegistry.registerKind(CUSTOM_KIND_PROPERTIES) - result = kindRegistry.getRegisteredKind(CUSTOM_KIND) + result = kindRegistry.getKindProperties(CUSTOM_KIND) then: - 0 * kindRegistry.getRegisteredKind(CUSTOM_KIND) - result == CUSTOM_KIND_PROPERTIES + _ * globalRegistry.getKindProperties(CUSTOM_KIND) >> Optional.of(globalResult) + result == globalResult } - @Unroll void "getGlobalKinds returns all global kinds that are registered"() { given: - @Subject KubernetesKindRegistry kindRegistry = factory.create() + @Subject KubernetesKindRegistry kindRegistry = factory.create(NOOP_CRD_LOOKUP, ImmutableList.of(CUSTOM_KIND_PROPERTIES)) Collection kinds when: - kindRegistry.registerKind(CUSTOM_KIND_PROPERTIES) kinds = kindRegistry.getGlobalKinds() then: - 1 * globalRegistry.getRegisteredKinds() >> ImmutableList.of(REPLICA_SET_PROPERTIES) + _ * globalRegistry.getRegisteredKinds() >> ImmutableList.of(REPLICA_SET_PROPERTIES) kinds.size() == 1 kinds.contains(REPLICA_SET_PROPERTIES) } @@ -76,10 +80,42 @@ class KubernetesKindRegistrySpec extends Specification { KubernetesKindProperties result when: - result = kindRegistry.getRegisteredKind(CUSTOM_KIND) + result = kindRegistry.getKindProperties(CUSTOM_KIND) then: - 1 * globalRegistry.getRegisteredKind(CUSTOM_KIND) >> Optional.empty() + _ * globalRegistry.getKindProperties(CUSTOM_KIND) >> Optional.empty() result == KubernetesKindProperties.withDefaultProperties(CUSTOM_KIND) } + + void "getRegisteredKind attempts to look up properties of an unkown kind"() { + given: + Function supplier = Mock(Function) + KubernetesKindProperties customProperties = KubernetesKindProperties.create(CUSTOM_KIND, false) + @Subject KubernetesKindRegistry kindRegistry = factory.create(supplier, ImmutableList.of()) + KubernetesKindProperties result + + when: + result = kindRegistry.getKindProperties(CUSTOM_KIND) + + then: + _ * globalRegistry.getKindProperties(CUSTOM_KIND) >> Optional.empty() + 1 * supplier.apply(CUSTOM_KIND) >> Optional.empty() + result == KubernetesKindProperties.withDefaultProperties(CUSTOM_KIND) + + when: + result = kindRegistry.getKindProperties(CUSTOM_KIND) + + then: + _ * globalRegistry.getKindProperties(CUSTOM_KIND) >> Optional.empty() + 1 * supplier.apply(CUSTOM_KIND) >> Optional.of(customProperties) + result == customProperties + + when: + result = kindRegistry.getKindProperties(CUSTOM_KIND) + + then: + _ * globalRegistry.getKindProperties(CUSTOM_KIND) >> Optional.empty() + 0 * supplier.apply(CUSTOM_KIND) >> Optional.of(customProperties) + result == customProperties + } } diff --git a/clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/KubernetesKindPropertiesSpec.groovy b/clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/KubernetesKindPropertiesSpec.groovy similarity index 64% rename from clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/KubernetesKindPropertiesSpec.groovy rename to clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/KubernetesKindPropertiesSpec.groovy index b1cb6e28497..12782c06121 100644 --- a/clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/KubernetesKindPropertiesSpec.groovy +++ b/clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/KubernetesKindPropertiesSpec.groovy @@ -14,12 +14,17 @@ * limitations under the License. */ -package com.netflix.spinnaker.clouddriver.kubernetes.v2.description +package com.netflix.spinnaker.clouddriver.kubernetes.v2.description.manifest import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.manifest.KubernetesKind import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.manifest.KubernetesKindProperties +import io.kubernetes.client.models.V1beta1CustomResourceDefinition +import io.kubernetes.client.models.V1beta1CustomResourceDefinitionBuilder +import io.kubernetes.client.models.V1beta1CustomResourceDefinitionNamesBuilder +import io.kubernetes.client.models.V1beta1CustomResourceDefinitionSpecBuilder import spock.lang.Specification +import spock.lang.Unroll class KubernetesKindPropertiesSpec extends Specification { def "creates and returns the supplied properties"() { @@ -68,4 +73,37 @@ class KubernetesKindPropertiesSpec extends Specification { !namespaceProperties.get().isNamespaced() !namespaceProperties.get().hasClusterRelationship() } + + @Unroll + void "creates properties from a custom resource definition spec"() { + when: + def kind = "TestKind" + def group = "stable.example.com" + V1beta1CustomResourceDefinition crd = + new V1beta1CustomResourceDefinitionBuilder() + .withSpec( + new V1beta1CustomResourceDefinitionSpecBuilder() + .withNames( + new V1beta1CustomResourceDefinitionNamesBuilder().withKind(kind).build()) + .withGroup(group) + .withScope(scope) + .build()) + .build() + def kindProperties = KubernetesKindProperties.fromCustomResourceDefinition(crd) + + then: + kindProperties.getKubernetesKind().equals(KubernetesKind.from(kind, KubernetesApiGroup.fromString(group))) + kindProperties.isNamespaced() == expectedNamespaced + + where: + scope | expectedNamespaced + "namespaced" | true + "Namespaced" | true + "NAMESPACED" | true + "nAmESpaceD" | true + "" | false + "cluster" | false + "Cluster" | false + "hello" | false + } } diff --git a/clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/KubernetesKindSpec.groovy b/clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/KubernetesKindSpec.groovy similarity index 85% rename from clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/KubernetesKindSpec.groovy rename to clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/KubernetesKindSpec.groovy index ecdb98764f3..3334fdd8a4c 100644 --- a/clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/KubernetesKindSpec.groovy +++ b/clouddriver-kubernetes-v2/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/description/manifest/KubernetesKindSpec.groovy @@ -14,11 +14,17 @@ * limitations under the License. */ -package com.netflix.spinnaker.clouddriver.kubernetes.v2.description +package com.netflix.spinnaker.clouddriver.kubernetes.v2.description.manifest import com.fasterxml.jackson.databind.ObjectMapper import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.manifest.KubernetesApiGroup import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.manifest.KubernetesKind +import io.kubernetes.client.models.V1beta1CustomResourceDefinition +import io.kubernetes.client.models.V1beta1CustomResourceDefinitionBuilder +import io.kubernetes.client.models.V1beta1CustomResourceDefinitionNames +import io.kubernetes.client.models.V1beta1CustomResourceDefinitionNamesBuilder +import io.kubernetes.client.models.V1beta1CustomResourceDefinitionSpec +import io.kubernetes.client.models.V1beta1CustomResourceDefinitionSpecBuilder import spock.lang.Shared import spock.lang.Specification import spock.lang.Unroll @@ -187,4 +193,24 @@ class KubernetesKindSpec extends Specification { KubernetesKind.SERVICE | "service" CUSTOM_RESOURCE_KIND | "deployment.stable.example.com" } + + void "creates a kind from a custom resource definition spec"() { + when: + def kind = "TestKind" + def group = "stable.example.com" + V1beta1CustomResourceDefinition crd = + new V1beta1CustomResourceDefinitionBuilder() + .withSpec( + new V1beta1CustomResourceDefinitionSpecBuilder() + .withNames( + new V1beta1CustomResourceDefinitionNamesBuilder().withKind(kind).build()) + .withGroup(group) + .build()) + .build() + def kubernetesKind = KubernetesKind.fromCustomResourceDefinition(crd) + + then: + kubernetesKind.getName() == kind + kubernetesKind.getApiGroup().toString() == group + } }