From 8b941c7465634c01a3162bbf9d367ff376d5bcd0 Mon Sep 17 00:00:00 2001 From: Ethan Rogers Date: Mon, 8 Jul 2019 13:09:56 -0400 Subject: [PATCH] feat(k8s): dynamic account config (#3824) * feat(k8s): dynamic account config implement credential synchronization for kubernetes accounts. defines `CredentialsInitializerSynchronizable` on the `KubernetesV*ProviderConfig` classes. Each of these classes is responsible for handling accounts for each provider version. * feat(k8s): detemine if account has changed * refactor(k8s): rename provider config classes renames provider config classes to synchronizable since there is no longer an `@Configuration` annotation. * refactor(k8s): add file hash signature adds a `kubeconfigFileHash` property which enables us to determine if the file contents of the kubeconfig file changed even if the path hasn't changed. this kind of change warrants an account refresh becuase the credentials used by the account may have possibly changed. * refactor(k8s): defer to computedOmitKinds previously, we were modifying omitKinds when an account was initialized. this caused calls to `equals` to fail because they didn't match configured omitKinds. this change defers to `omitKindsComputed` so that checking new and existing credentials is accurate according to how the account it configured. this allows us to only initialize modified or new accounts rather than every credential. --- .../KubernetesConfigurationProperties.groovy | 7 + .../security/KubeconfigFileHasher.java | 36 ++++ .../KubernetesNamedAccountCredentials.java | 41 ++-- ...sNamedAccountCredentialsInitializer.groovy | 75 ------- .../KubernetesV1ProviderConfig.groovy | 88 -------- .../KubernetesV1ProviderSynchronizable.java | 168 +++++++++++++++ .../v1/security/KubernetesV1Credentials.java | 42 ++-- .../v2/caching/KubernetesV2Provider.java | 2 +- .../caching/KubernetesV2ProviderConfig.java | 124 ----------- .../KubernetesV2ProviderSynchronizable.java | 203 ++++++++++++++++++ .../KubernetesV2CachingAgentDispatcher.java | 2 +- .../KubernetesV2LiveManifestProvider.java | 2 +- .../v2/security/KubernetesV2Credentials.java | 93 +++++--- .../config/KubernetesConfiguration.groovy | 78 ++++++- ...rnetesV1ProviderSynchronizableSpec.groovy} | 79 +++---- ...ernetesV2ProviderSynchronizableSpec.groovy | 119 ++++++++++ 16 files changed, 753 insertions(+), 406 deletions(-) create mode 100644 clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubeconfigFileHasher.java delete mode 100644 clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubernetesNamedAccountCredentialsInitializer.groovy delete mode 100644 clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v1/provider/KubernetesV1ProviderConfig.groovy create mode 100644 clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v1/provider/KubernetesV1ProviderSynchronizable.java delete mode 100644 clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/KubernetesV2ProviderConfig.java create mode 100644 clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/KubernetesV2ProviderSynchronizable.java rename clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/{v2/security/KubernetesNamedAccountCredentialsInitializerSpec.groovy => v1/provider/KubernetesV1ProviderSynchronizableSpec.groovy} (61%) create mode 100644 clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/KubernetesV2ProviderSynchronizableSpec.groovy diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/config/KubernetesConfigurationProperties.groovy b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/config/KubernetesConfigurationProperties.groovy index 03b3b93935b..dc8c7bc127b 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/config/KubernetesConfigurationProperties.groovy +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/config/KubernetesConfigurationProperties.groovy @@ -20,6 +20,7 @@ import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesSpi import com.netflix.spinnaker.clouddriver.security.ProviderVersion import com.netflix.spinnaker.fiat.model.resources.Permissions import groovy.transform.ToString +import lombok.EqualsAndHashCode @ToString(includeNames = true) class KubernetesConfigurationProperties { @@ -66,12 +67,17 @@ class KubernetesConfigurationProperties { } @ToString(includeNames = true) +@EqualsAndHashCode class LinkedDockerRegistryConfiguration { + @EqualsAndHashCode.Include String accountName + + @EqualsAndHashCode.Include List namespaces } @ToString(includeNames = true) +@EqualsAndHashCode class CustomKubernetesResource { String kubernetesKind String spinnakerKind = KubernetesSpinnakerKindMap.SpinnakerKind.UNCLASSIFIED.toString() @@ -81,6 +87,7 @@ class CustomKubernetesResource { } @ToString(includeNames = true) +@EqualsAndHashCode class KubernetesCachingPolicy { String kubernetesKind int maxEntriesPerAgent diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubeconfigFileHasher.java b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubeconfigFileHasher.java new file mode 100644 index 00000000000..77ed6955cea --- /dev/null +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubeconfigFileHasher.java @@ -0,0 +1,36 @@ +/* + * Copyright 2019 Armory + * + * 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.security; + +import com.google.common.hash.Hashing; +import java.nio.file.Files; +import java.nio.file.Paths; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class KubeconfigFileHasher { + + public static String hashKubeconfigFile(String filepath) { + try { + byte[] contents = Files.readAllBytes(Paths.get(filepath)); + return Hashing.sha256().hashBytes(contents).toString(); + } catch (Exception e) { + log.warn("failed to hash kubeconfig file at {}: {}", filepath, e); + return ""; + } + } +} diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubernetesNamedAccountCredentials.java b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubernetesNamedAccountCredentials.java index f0763399dac..3f41c7e5da1 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubernetesNamedAccountCredentials.java +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubernetesNamedAccountCredentials.java @@ -16,6 +16,8 @@ package com.netflix.spinnaker.clouddriver.kubernetes.security; +import static lombok.EqualsAndHashCode.Include; + import com.netflix.spectator.api.Registry; import com.netflix.spinnaker.clouddriver.data.ConfigFileService; import com.netflix.spinnaker.clouddriver.kubernetes.KubernetesCloudProvider; @@ -30,30 +32,39 @@ import com.netflix.spinnaker.clouddriver.security.AccountCredentialsRepository; import com.netflix.spinnaker.clouddriver.security.ProviderVersion; import com.netflix.spinnaker.fiat.model.resources.Permissions; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; @Getter +@EqualsAndHashCode(onlyExplicitlyIncluded = true) public class KubernetesNamedAccountCredentials implements AccountCredentials { private final String cloudProvider = "kubernetes"; - private final String name; - private final ProviderVersion providerVersion; - private final String environment; - private final String accountType; - private final String skin; - private final int cacheThreads; - private final C credentials; - private final List requiredGroupMembership; - private final Permissions permissions; - private final Long cacheIntervalSeconds; + + @Include private final String name; + + @Include private final ProviderVersion providerVersion; + + @Include private final String environment; + + @Include private final String accountType; + + @Include private final String skin; + + @Include private final int cacheThreads; + + @Include private final C credentials; + + @Include private final List requiredGroupMembership; + + @Include private final Permissions permissions; + + @Include private final Long cacheIntervalSeconds; + private final KubernetesSpinnakerKindMap kubernetesSpinnakerKindMap; public KubernetesNamedAccountCredentials( diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubernetesNamedAccountCredentialsInitializer.groovy b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubernetesNamedAccountCredentialsInitializer.groovy deleted file mode 100644 index 6d6d5042664..00000000000 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/security/KubernetesNamedAccountCredentialsInitializer.groovy +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2015 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.security - -import com.netflix.spectator.api.Registry -import com.netflix.spinnaker.cats.module.CatsModule -import com.netflix.spinnaker.clouddriver.kubernetes.config.KubernetesConfigurationProperties -import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesSpinnakerKindMap -import com.netflix.spinnaker.clouddriver.kubernetes.v2.op.job.KubectlJobExecutor -import com.netflix.spinnaker.clouddriver.security.AccountCredentialsRepository -import com.netflix.spinnaker.clouddriver.security.ProviderUtils -import groovy.util.logging.Slf4j -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration - -@Slf4j -@Configuration -class KubernetesNamedAccountCredentialsInitializer { - @Autowired Registry spectatorRegistry - @Autowired KubectlJobExecutor jobExecutor - @Autowired KubernetesSpinnakerKindMap kubernetesSpinnakerKindMap - - @Bean - List kubernetesNamedAccountCredentials( - KubernetesNamedAccountCredentials.CredentialFactory credentialFactory, - KubernetesConfigurationProperties kubernetesConfigurationProperties, - AccountCredentialsRepository accountCredentialsRepository) { - - synchronizeKubernetesAccounts(credentialFactory, kubernetesConfigurationProperties, - null, accountCredentialsRepository) - } - - private List synchronizeKubernetesAccounts( - KubernetesNamedAccountCredentials.CredentialFactory credentialFactory, - KubernetesConfigurationProperties kubernetesConfigurationProperties, - CatsModule catsModule, - AccountCredentialsRepository accountCredentialsRepository) { - def (ArrayList accountsToAdd, List namesOfDeletedAccounts) = - ProviderUtils.calculateAccountDeltas(accountCredentialsRepository, - KubernetesNamedAccountCredentials, - kubernetesConfigurationProperties.accounts) - - // TODO(lwander): Modify accounts when their dockerRegistries attribute is updated as well -- need to ask @duftler. - accountsToAdd.each { KubernetesConfigurationProperties.ManagedAccount managedAccount -> - try { - def kubernetesAccount = new KubernetesNamedAccountCredentials(managedAccount, kubernetesSpinnakerKindMap, credentialFactory) - - accountCredentialsRepository.save(managedAccount.name, kubernetesAccount) - } catch (e) { - log.info "Could not load account ${managedAccount.name} for Kubernetes.", e - } - } - - ProviderUtils.unscheduleAndDeregisterAgents(namesOfDeletedAccounts, catsModule) - - accountCredentialsRepository.all.findAll { - it instanceof KubernetesNamedAccountCredentials - } as List - } -} diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v1/provider/KubernetesV1ProviderConfig.groovy b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v1/provider/KubernetesV1ProviderConfig.groovy deleted file mode 100644 index efb1e3afcb8..00000000000 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v1/provider/KubernetesV1ProviderConfig.groovy +++ /dev/null @@ -1,88 +0,0 @@ -/* - * 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.v1.provider - -import com.netflix.spinnaker.cats.agent.Agent -import com.netflix.spinnaker.cats.thread.NamedThreadFactory -import com.netflix.spinnaker.clouddriver.kubernetes.KubernetesCloudProvider -import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesNamedAccountCredentials -import com.netflix.spinnaker.clouddriver.kubernetes.v1.provider.agent.KubernetesV1CachingAgentDispatcher -import com.netflix.spinnaker.clouddriver.security.AccountCredentialsRepository -import com.netflix.spinnaker.clouddriver.security.ProviderUtils -import com.netflix.spinnaker.clouddriver.security.ProviderVersion -import groovy.util.logging.Slf4j -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.DependsOn - -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.Executors -import java.util.concurrent.ScheduledExecutorService -import java.util.concurrent.TimeUnit - -@Configuration -@Slf4j -class KubernetesV1ProviderConfig implements Runnable { - @Bean - @DependsOn('kubernetesNamedAccountCredentials') - KubernetesV1Provider kubernetesV1Provider(KubernetesCloudProvider kubernetesCloudProvider, - AccountCredentialsRepository accountCredentialsRepository, - KubernetesV1CachingAgentDispatcher kubernetesV1CachingAgentDispatcher) { - this.kubernetesV1Provider = new KubernetesV1Provider(kubernetesCloudProvider, Collections.newSetFromMap(new ConcurrentHashMap())) - this.kubernetesCloudProvider = kubernetesCloudProvider - this.accountCredentialsRepository = accountCredentialsRepository - this.kubernetesV1CachingAgentDispatcher = kubernetesV1CachingAgentDispatcher - - ScheduledExecutorService poller = Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory(KubernetesV1ProviderConfig.class.getSimpleName())) - - poller.scheduleAtFixedRate(this, 0, 30, TimeUnit.SECONDS) - - kubernetesV1Provider - } - - private KubernetesV1Provider kubernetesV1Provider - private KubernetesCloudProvider kubernetesCloudProvider - private AccountCredentialsRepository accountCredentialsRepository - private KubernetesV1CachingAgentDispatcher kubernetesV1CachingAgentDispatcher - - @Override - void run() { - synchronizeKubernetesV1Provider(kubernetesV1Provider, accountCredentialsRepository) - } - - private void synchronizeKubernetesV1Provider(KubernetesV1Provider kubernetesV1Provider, - AccountCredentialsRepository accountCredentialsRepository) { - def allAccounts = ProviderUtils.buildThreadSafeSetOfAccounts(accountCredentialsRepository, KubernetesNamedAccountCredentials, ProviderVersion.v1) - - kubernetesV1Provider.agents.clear() - - for (KubernetesNamedAccountCredentials credentials : allAccounts) { - def newlyAddedAgents = kubernetesV1CachingAgentDispatcher.buildAllCachingAgents(credentials) - - log.info "Adding ${newlyAddedAgents.size()} agents for account ${credentials.name}" - - // If there is an agent scheduler, then this provider has been through the AgentController in the past. - // In that case, we need to do the scheduling here (because accounts have been added to a running system). - if (kubernetesV1Provider.agentScheduler) { - ProviderUtils.rescheduleAgents(kubernetesV1Provider, newlyAddedAgents) - } - - kubernetesV1Provider.agents.addAll(newlyAddedAgents) - } - } -} diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v1/provider/KubernetesV1ProviderSynchronizable.java b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v1/provider/KubernetesV1ProviderSynchronizable.java new file mode 100644 index 00000000000..23c6e8b8a4d --- /dev/null +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v1/provider/KubernetesV1ProviderSynchronizable.java @@ -0,0 +1,168 @@ +/* + * 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.v1.provider; + +import com.netflix.spinnaker.cats.module.CatsModule; +import com.netflix.spinnaker.cats.thread.NamedThreadFactory; +import com.netflix.spinnaker.clouddriver.kubernetes.KubernetesCloudProvider; +import com.netflix.spinnaker.clouddriver.kubernetes.caching.KubernetesCachingAgent; +import com.netflix.spinnaker.clouddriver.kubernetes.config.KubernetesConfigurationProperties; +import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesNamedAccountCredentials; +import com.netflix.spinnaker.clouddriver.kubernetes.v1.provider.agent.KubernetesV1CachingAgentDispatcher; +import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesSpinnakerKindMap; +import com.netflix.spinnaker.clouddriver.security.AccountCredentials; +import com.netflix.spinnaker.clouddriver.security.AccountCredentialsRepository; +import com.netflix.spinnaker.clouddriver.security.CredentialsInitializerSynchronizable; +import com.netflix.spinnaker.clouddriver.security.ProviderUtils; +import com.netflix.spinnaker.clouddriver.security.ProviderVersion; +import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.stream.Collectors; +import javax.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class KubernetesV1ProviderSynchronizable implements CredentialsInitializerSynchronizable { + + private KubernetesV1Provider kubernetesV1Provider; + private AccountCredentialsRepository accountCredentialsRepository; + private KubernetesV1CachingAgentDispatcher kubernetesV1CachingAgentDispatcher; + private KubernetesConfigurationProperties kubernetesConfigurationProperties; + private KubernetesNamedAccountCredentials.CredentialFactory credentialFactory; + private KubernetesSpinnakerKindMap kubernetesSpinnakerKindMap; + private CatsModule catsModule; + + KubernetesV1ProviderSynchronizable( + KubernetesV1Provider kubernetesV1Provider, + AccountCredentialsRepository accountCredentialsRepository, + KubernetesV1CachingAgentDispatcher kubernetesV1CachingAgentDispatcher, + KubernetesConfigurationProperties kubernetesConfigurationProperties, + KubernetesNamedAccountCredentials.CredentialFactory credentialFactory, + KubernetesSpinnakerKindMap kubernetesSpinnakerKindMap, + CatsModule catsModule) { + this.kubernetesV1Provider = kubernetesV1Provider; + this.accountCredentialsRepository = accountCredentialsRepository; + this.kubernetesV1CachingAgentDispatcher = kubernetesV1CachingAgentDispatcher; + this.kubernetesConfigurationProperties = kubernetesConfigurationProperties; + this.credentialFactory = credentialFactory; + this.kubernetesSpinnakerKindMap = kubernetesSpinnakerKindMap; + this.catsModule = catsModule; + + ScheduledExecutorService poller = + Executors.newSingleThreadScheduledExecutor( + new NamedThreadFactory(KubernetesV1ProviderSynchronizable.class.getSimpleName())); + } + + @Override + @PostConstruct + public void synchronize() { + Set newAndChangedAccounts = synchronizeAccountCredentials(); + + Set allAccounts = + ProviderUtils.buildThreadSafeSetOfAccounts( + accountCredentialsRepository, + KubernetesNamedAccountCredentials.class, + ProviderVersion.v1) + .stream() + .filter(account -> newAndChangedAccounts.contains(account.getName())) + .collect(Collectors.toSet()); + + if (allAccounts.size() < 1) { + log.info( + "No changes detected to V1 Kubernetes accounts. Skipping caching agent synchronization."); + return; + } + + log.info("Synchronizing {} caching agents for V1 Kubernetes accounts.", allAccounts.size()); + synchronizeKubernetesV1Provider(allAccounts); + } + + private Set synchronizeAccountCredentials() { + List deletedAccounts = getDeletedAccountNames(); + List changedAccounts = new ArrayList<>(); + Set newAndChangedAccounts = new HashSet<>(); + + deletedAccounts.stream().forEach(accountCredentialsRepository::delete); + + kubernetesConfigurationProperties.getAccounts().stream() + .filter(a -> ProviderVersion.v1.equals(a.getProviderVersion())) + .forEach( + managedAccount -> { + KubernetesNamedAccountCredentials credentials = + new KubernetesNamedAccountCredentials( + managedAccount, kubernetesSpinnakerKindMap, credentialFactory); + + AccountCredentials existingCredentials = + accountCredentialsRepository.getOne(managedAccount.getName()); + if (existingCredentials == null) { + newAndChangedAccounts.add(managedAccount.getName()); + } else if (!existingCredentials.equals(credentials)) { + changedAccounts.add(managedAccount.getName()); + newAndChangedAccounts.add(managedAccount.getName()); + } + + accountCredentialsRepository.save(managedAccount.getName(), credentials); + }); + + ProviderUtils.unscheduleAndDeregisterAgents(deletedAccounts, catsModule); + ProviderUtils.unscheduleAndDeregisterAgents(changedAccounts, catsModule); + + return newAndChangedAccounts; + } + + private List getDeletedAccountNames() { + Set existingNames = + accountCredentialsRepository.getAll().stream() + .filter(c -> KubernetesCloudProvider.getID().equals(c.getCloudProvider())) + .filter(c -> ProviderVersion.v1.equals(c.getProviderVersion())) + .map(it -> it.getName()) + .collect(Collectors.toSet()); + + Set newNames = + kubernetesConfigurationProperties.getAccounts().stream() + .map(it -> it.getName()) + .collect(Collectors.toSet()); + + return existingNames.stream() + .filter(name -> !newNames.contains(name)) + .collect(Collectors.toList()); + } + + private void synchronizeKubernetesV1Provider(Set allAccounts) { + + kubernetesV1Provider.getAgents().clear(); + + for (KubernetesNamedAccountCredentials credentials : allAccounts) { + Collection newlyAddedAgents = + kubernetesV1CachingAgentDispatcher.buildAllCachingAgents(credentials); + + log.info("Adding {} agents for account {}", newlyAddedAgents.size(), credentials.getName()); + + // If there is an agent scheduler, then this provider has been through the AgentController in + // the past. + // In that case, we need to do the scheduling here (because accounts have been added to a + // running system). + if (kubernetesV1Provider.getAgentScheduler() != null) { + ProviderUtils.rescheduleAgents(kubernetesV1Provider, new ArrayList<>(newlyAddedAgents)); + } + + kubernetesV1Provider.getAgents().addAll(newlyAddedAgents); + } + } +} diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v1/security/KubernetesV1Credentials.java b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v1/security/KubernetesV1Credentials.java index 8ad6f8de255..12ca2b9ead8 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v1/security/KubernetesV1Credentials.java +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v1/security/KubernetesV1Credentials.java @@ -17,11 +17,14 @@ package com.netflix.spinnaker.clouddriver.kubernetes.v1.security; +import static lombok.EqualsAndHashCode.Include; + import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Lists; import com.netflix.spectator.api.Registry; import com.netflix.spinnaker.clouddriver.docker.registry.security.DockerRegistryNamedAccountCredentials; import com.netflix.spinnaker.clouddriver.kubernetes.config.LinkedDockerRegistryConfiguration; +import com.netflix.spinnaker.clouddriver.kubernetes.security.KubeconfigFileHasher; import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesApiClientConfig; import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesCredentials; import com.netflix.spinnaker.clouddriver.kubernetes.v1.api.KubernetesApiAdaptor; @@ -33,30 +36,37 @@ import io.fabric8.kubernetes.api.model.SecretBuilder; import io.fabric8.kubernetes.client.Config; import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.Base64; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import javax.validation.ConstraintViolationException; +import lombok.Data; +import lombok.EqualsAndHashCode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +@EqualsAndHashCode(onlyExplicitlyIncluded = true) +@Data public class KubernetesV1Credentials implements KubernetesCredentials { private final KubernetesApiAdaptor apiAdaptor; private KubernetesClientApiAdapter apiClientAdaptor; - private final List namespaces; - private final List omitNamespaces; - private final List dockerRegistries; - private final HashMap> imagePullSecrets = new HashMap<>(); + + @Include private final List namespaces; + + @Include private final List omitNamespaces; + + @Include private final List dockerRegistries; + + @Include private final HashMap> imagePullSecrets = new HashMap<>(); private final Logger LOG; private final AccountCredentialsRepository repository; private final HashSet dynamicRegistries = new HashSet<>(); - private final boolean configureImagePullSecrets; + + @Include private final boolean configureImagePullSecrets; private List oldNamespaces; + @Include private final String kubeconfigFile; + + @Include private final String kubeconfigFileHash; + public KubernetesV1Credentials( String name, String kubeconfigFile, @@ -71,6 +81,8 @@ public KubernetesV1Credentials( List dockerRegistries, Registry spectatorRegistry, AccountCredentialsRepository accountCredentialsRepository) { + this.kubeconfigFile = kubeconfigFile; + if (dockerRegistries == null || dockerRegistries.size() == 0) { throw new IllegalArgumentException( "Docker registries for Kubernetes account " + name + " are required."); @@ -85,6 +97,8 @@ public KubernetesV1Credentials( new KubernetesApiClientConfig( kubeconfigFile, context, cluster, user, userAgent, serviceAccount); + this.kubeconfigFileHash = KubeconfigFileHasher.hashKubeconfigFile(this.kubeconfigFile); + this.apiAdaptor = new KubernetesApiAdaptor(name, config, spectatorRegistry); this.apiClientAdaptor = new KubernetesClientApiAdapter(name, configClient, spectatorRegistry); this.namespaces = namespaces != null ? namespaces : new ArrayList<>(); @@ -93,7 +107,6 @@ public KubernetesV1Credentials( this.repository = accountCredentialsRepository; this.LOG = LoggerFactory.getLogger(KubernetesV1Credentials.class); this.configureImagePullSecrets = configureImagePullSecrets; - configureDockerRegistries(); } @@ -111,7 +124,8 @@ protected KubernetesV1Credentials( this.repository = repository; this.LOG = LoggerFactory.getLogger(KubernetesV1Credentials.class); this.configureImagePullSecrets = true; - + this.kubeconfigFile = ""; + this.kubeconfigFileHash = ""; configureDockerRegistries(); } diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/KubernetesV2Provider.java b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/KubernetesV2Provider.java index 6ea7e377c14..a7647858d2f 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/KubernetesV2Provider.java +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/KubernetesV2Provider.java @@ -31,7 +31,7 @@ @EqualsAndHashCode(callSuper = true) @Slf4j @Data -class KubernetesV2Provider extends AgentSchedulerAware implements Provider { +public class KubernetesV2Provider extends AgentSchedulerAware implements Provider { public static final String PROVIDER_NAME = KubernetesCloudProvider.getID(); private Collection agents = emptyAgentCollection(); diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/KubernetesV2ProviderConfig.java b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/KubernetesV2ProviderConfig.java deleted file mode 100644 index 331c7ecba61..00000000000 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/KubernetesV2ProviderConfig.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * 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.caching; - -import com.netflix.spinnaker.cats.agent.Agent; -import com.netflix.spinnaker.cats.thread.NamedThreadFactory; -import com.netflix.spinnaker.clouddriver.kubernetes.KubernetesCloudProvider; -import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesNamedAccountCredentials; -import com.netflix.spinnaker.clouddriver.kubernetes.v2.caching.agent.KubernetesV2CachingAgentDispatcher; -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.security.KubernetesV2Credentials; -import com.netflix.spinnaker.clouddriver.security.AccountCredentialsRepository; -import com.netflix.spinnaker.clouddriver.security.ProviderUtils; -import com.netflix.spinnaker.clouddriver.security.ProviderVersion; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.stream.Collectors; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.DependsOn; - -@Configuration -@Slf4j -class KubernetesV2ProviderConfig { - @Bean - @DependsOn("kubernetesNamedAccountCredentials") - KubernetesV2Provider kubernetesV2Provider( - KubernetesCloudProvider kubernetesCloudProvider, - AccountCredentialsRepository accountCredentialsRepository, - KubernetesV2CachingAgentDispatcher kubernetesV2CachingAgentDispatcher, - KubernetesResourcePropertyRegistry kubernetesResourcePropertyRegistry) { - this.kubernetesV2Provider = new KubernetesV2Provider(); - this.accountCredentialsRepository = accountCredentialsRepository; - this.kubernetesV2CachingAgentDispatcher = kubernetesV2CachingAgentDispatcher; - this.kubernetesResourcePropertyRegistry = kubernetesResourcePropertyRegistry; - - ScheduledExecutorService poller = - Executors.newSingleThreadScheduledExecutor( - new NamedThreadFactory(KubernetesV2ProviderConfig.class.getSimpleName())); - - synchronizeKubernetesV2Provider(kubernetesV2Provider, accountCredentialsRepository); - - return kubernetesV2Provider; - } - - private KubernetesV2Provider kubernetesV2Provider; - private AccountCredentialsRepository accountCredentialsRepository; - private KubernetesV2CachingAgentDispatcher kubernetesV2CachingAgentDispatcher; - private KubernetesResourcePropertyRegistry kubernetesResourcePropertyRegistry; - - private void synchronizeKubernetesV2Provider( - KubernetesV2Provider kubernetesV2Provider, - AccountCredentialsRepository accountCredentialsRepository) { - Set allAccounts = - ProviderUtils.buildThreadSafeSetOfAccounts( - accountCredentialsRepository, - KubernetesNamedAccountCredentials.class, - ProviderVersion.v2); - - try { - for (KubernetesNamedAccountCredentials credentials : allAccounts) { - KubernetesV2Credentials v2Credentials = - (KubernetesV2Credentials) credentials.getCredentials(); - v2Credentials - .getCustomResources() - .forEach( - cr -> { - try { - KubernetesResourceProperties properties = - KubernetesResourceProperties.fromCustomResource(cr); - kubernetesResourcePropertyRegistry.registerAccountProperty( - credentials.getName(), properties); - } catch (Exception e) { - log.warn("Error encountered registering {}: ", cr, e); - } - }); - v2Credentials.initialize(); - - List newlyAddedAgents = - kubernetesV2CachingAgentDispatcher.buildAllCachingAgents(credentials).stream() - .map(c -> (Agent) c) - .collect(Collectors.toList()); - - log.info("Adding {} agents for account {}", newlyAddedAgents.size(), credentials.getName()); - - kubernetesV2Provider.addAllAgents(newlyAddedAgents); - } - } catch (Exception e) { - log.warn("Error encountered scheduling new agents -- using old agent set instead", e); - kubernetesV2Provider.clearNewAgentSet(); - } - - // If there is an agent scheduler, then this provider has been through the AgentController in - // the past. - // In that case, we need to do the scheduling here (because accounts have been added to a - // running system). - if (kubernetesV2Provider.getAgentScheduler() != null) { - ProviderUtils.rescheduleAgents( - kubernetesV2Provider, new ArrayList<>(kubernetesV2Provider.getNextAgentSet())); - } - - kubernetesV2Provider.switchToNewAgents(); - } -} diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/KubernetesV2ProviderSynchronizable.java b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/KubernetesV2ProviderSynchronizable.java new file mode 100644 index 00000000000..52218590bdd --- /dev/null +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/KubernetesV2ProviderSynchronizable.java @@ -0,0 +1,203 @@ +/* + * 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.caching; + +import com.netflix.spinnaker.cats.agent.Agent; +import com.netflix.spinnaker.cats.module.CatsModule; +import com.netflix.spinnaker.cats.thread.NamedThreadFactory; +import com.netflix.spinnaker.clouddriver.kubernetes.KubernetesCloudProvider; +import com.netflix.spinnaker.clouddriver.kubernetes.config.KubernetesConfigurationProperties; +import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesNamedAccountCredentials; +import com.netflix.spinnaker.clouddriver.kubernetes.v2.caching.agent.KubernetesV2CachingAgentDispatcher; +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.KubernetesSpinnakerKindMap; +import com.netflix.spinnaker.clouddriver.kubernetes.v2.security.KubernetesV2Credentials; +import com.netflix.spinnaker.clouddriver.security.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.stream.Collectors; +import javax.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class KubernetesV2ProviderSynchronizable implements CredentialsInitializerSynchronizable { + + private KubernetesV2Provider kubernetesV2Provider; + private AccountCredentialsRepository accountCredentialsRepository; + private KubernetesV2CachingAgentDispatcher kubernetesV2CachingAgentDispatcher; + private KubernetesResourcePropertyRegistry kubernetesResourcePropertyRegistry; + private KubernetesConfigurationProperties kubernetesConfigurationProperties; + private KubernetesNamedAccountCredentials.CredentialFactory credentialFactory; + private KubernetesSpinnakerKindMap kubernetesSpinnakerKindMap; + private CatsModule catsModule; + + public KubernetesV2ProviderSynchronizable( + KubernetesV2Provider kubernetesV2Provider, + AccountCredentialsRepository accountCredentialsRepository, + KubernetesV2CachingAgentDispatcher kubernetesV2CachingAgentDispatcher, + KubernetesResourcePropertyRegistry kubernetesResourcePropertyRegistry, + KubernetesConfigurationProperties kubernetesConfigurationProperties, + KubernetesNamedAccountCredentials.CredentialFactory credentialFactory, + KubernetesSpinnakerKindMap kubernetesSpinnakerKindMap, + CatsModule catsModule) { + this.kubernetesV2Provider = kubernetesV2Provider; + this.accountCredentialsRepository = accountCredentialsRepository; + this.kubernetesV2CachingAgentDispatcher = kubernetesV2CachingAgentDispatcher; + this.kubernetesResourcePropertyRegistry = kubernetesResourcePropertyRegistry; + this.kubernetesConfigurationProperties = kubernetesConfigurationProperties; + this.credentialFactory = credentialFactory; + this.kubernetesSpinnakerKindMap = kubernetesSpinnakerKindMap; + this.catsModule = catsModule; + + ScheduledExecutorService poller = + Executors.newSingleThreadScheduledExecutor( + new NamedThreadFactory(KubernetesV2ProviderSynchronizable.class.getSimpleName())); + } + + @Override + @PostConstruct + public void synchronize() { + Set newAndChangedAccounts = synchronizeAccountCredentials(); + + // we only want to initialize caching agents for new or updated accounts + Set allAccounts = + ProviderUtils.buildThreadSafeSetOfAccounts( + accountCredentialsRepository, + KubernetesNamedAccountCredentials.class, + ProviderVersion.v2) + .stream() + .filter(account -> newAndChangedAccounts.contains(account.getName())) + .collect(Collectors.toSet()); + + if (allAccounts.size() < 1) { + log.info( + "No changes detected to V2 Kubernetes accounts. Skipping caching agent synchronization."); + return; + } + + log.info("Synchronizing {} caching agents for V2 Kubernetes accounts.", allAccounts.size()); + synchronizeKubernetesV2Provider(allAccounts); + } + + private Set synchronizeAccountCredentials() { + List deletedAccounts = getDeletedAccountNames(); + List changedAccounts = new ArrayList<>(); + Set newAndChangedAccounts = new HashSet<>(); + + deletedAccounts.forEach(accountCredentialsRepository::delete); + + kubernetesConfigurationProperties.getAccounts().stream() + .filter(a -> ProviderVersion.v2.equals(a.getProviderVersion())) + .forEach( + managedAccount -> { + KubernetesNamedAccountCredentials credentials = + new KubernetesNamedAccountCredentials( + managedAccount, kubernetesSpinnakerKindMap, credentialFactory); + + AccountCredentials existingCredentials = + accountCredentialsRepository.getOne(managedAccount.getName()); + + if (existingCredentials == null) { + // account didn't previously exist + newAndChangedAccounts.add(managedAccount.getName()); + } else if (!existingCredentials.equals(credentials)) { + // account exists but has changed + changedAccounts.add(managedAccount.getName()); + newAndChangedAccounts.add(managedAccount.getName()); + } + + accountCredentialsRepository.save(managedAccount.getName(), credentials); + }); + + ProviderUtils.unscheduleAndDeregisterAgents(deletedAccounts, catsModule); + ProviderUtils.unscheduleAndDeregisterAgents(changedAccounts, catsModule); + + return newAndChangedAccounts; + } + + private List getDeletedAccountNames() { + List existingNames = + accountCredentialsRepository.getAll().stream() + .filter( + (AccountCredentials c) -> + KubernetesCloudProvider.getID().equals(c.getCloudProvider())) + .filter((AccountCredentials c) -> ProviderVersion.v2.equals(c.getProviderVersion())) + .map(AccountCredentials::getName) + .collect(Collectors.toList()); + + Set newNames = + kubernetesConfigurationProperties.getAccounts().stream() + .map(KubernetesConfigurationProperties.ManagedAccount::getName) + .collect(Collectors.toSet()); + + return existingNames.stream() + .filter(name -> !newNames.contains(name)) + .collect(Collectors.toList()); + } + + private void synchronizeKubernetesV2Provider(Set allAccounts) { + + try { + for (KubernetesNamedAccountCredentials credentials : allAccounts) { + KubernetesV2Credentials v2Credentials = + (KubernetesV2Credentials) credentials.getCredentials(); + v2Credentials + .getCustomResources() + .forEach( + cr -> { + try { + KubernetesResourceProperties properties = + KubernetesResourceProperties.fromCustomResource(cr); + kubernetesResourcePropertyRegistry.registerAccountProperty( + credentials.getName(), properties); + } catch (Exception e) { + log.warn("Error encountered registering {}: ", cr, e); + } + }); + v2Credentials.initialize(); + List newlyAddedAgents = + kubernetesV2CachingAgentDispatcher.buildAllCachingAgents(credentials).stream() + .map(c -> (Agent) c) + .collect(Collectors.toList()); + + log.info("Adding {} agents for account {}", newlyAddedAgents.size(), credentials.getName()); + + kubernetesV2Provider.addAllAgents(newlyAddedAgents); + } + } catch (Exception e) { + log.warn("Error encountered scheduling new agents -- using old agent set instead", e); + kubernetesV2Provider.clearNewAgentSet(); + } + + // If there is an agent scheduler, then this provider has been through the AgentController in + // the past. + // In that case, we need to do the scheduling here (because accounts have been added to a + // running system). + if (kubernetesV2Provider.getAgentScheduler() != null) { + ProviderUtils.rescheduleAgents( + kubernetesV2Provider, new ArrayList<>(kubernetesV2Provider.getNextAgentSet())); + } + + kubernetesV2Provider.switchToNewAgents(); + } +} diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/agent/KubernetesV2CachingAgentDispatcher.java b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/agent/KubernetesV2CachingAgentDispatcher.java index 9c6ce79795e..6a46661893e 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/agent/KubernetesV2CachingAgentDispatcher.java +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/agent/KubernetesV2CachingAgentDispatcher.java @@ -79,7 +79,7 @@ public Collection buildAllCachingAgents( .filter(Objects::nonNull) .forEach(c -> result.add((KubernetesCachingAgent) c))); - if (v2Credentials.isMetrics()) { + if (v2Credentials.isMetricsComputed()) { IntStream.range(0, credentials.getCacheThreads()) .boxed() .forEach( diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/view/provider/KubernetesV2LiveManifestProvider.java b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/view/provider/KubernetesV2LiveManifestProvider.java index 5f4040dcdb1..57ca7335e4f 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/view/provider/KubernetesV2LiveManifestProvider.java +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/view/provider/KubernetesV2LiveManifestProvider.java @@ -91,7 +91,7 @@ public KubernetesV2Manifest getManifest( : Collections.emptyList(); List metrics = Collections.emptyList(); - if (kind.equals(KubernetesKind.POD) && credentials.isMetrics()) { + if (kind.equals(KubernetesKind.POD) && credentials.isMetricsComputed()) { metrics = credentials.topPod(namespace, parsedName.getRight()).stream() .map(KubernetesPodMetric::getContainerMetrics) diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/security/KubernetesV2Credentials.java b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/security/KubernetesV2Credentials.java index 4a3874bf3cb..46ac4e410cc 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/security/KubernetesV2Credentials.java +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/security/KubernetesV2Credentials.java @@ -17,6 +17,8 @@ package com.netflix.spinnaker.clouddriver.kubernetes.v2.security; +import static lombok.EqualsAndHashCode.Include; + import com.fasterxml.jackson.annotation.JsonIgnore; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; @@ -25,6 +27,7 @@ import com.netflix.spinnaker.clouddriver.kubernetes.config.CustomKubernetesResource; import com.netflix.spinnaker.clouddriver.kubernetes.config.KubernetesCachingPolicy; import com.netflix.spinnaker.clouddriver.kubernetes.config.KubernetesConfigurationProperties; +import com.netflix.spinnaker.clouddriver.kubernetes.security.KubeconfigFileHasher; import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesCredentials; import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.JsonPatch; import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesPatchOptions; @@ -40,22 +43,18 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import java.util.stream.Collectors; import javax.annotation.Nonnull; +import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @Slf4j +@EqualsAndHashCode(onlyExplicitlyIncluded = true) public class KubernetesV2Credentials implements KubernetesCredentials { private static final int CRD_EXPIRY_SECONDS = 30; private static final int NAMESPACE_EXPIRY_SECONDS = 30; @@ -67,29 +66,49 @@ public class KubernetesV2Credentials implements KubernetesCredentials { private final Clock clock; private final KubectlJobExecutor jobExecutor; - private final String accountName; - @Getter private final List namespaces; - @Getter private final List omitNamespaces; - private final List kinds; - private final Map omitKinds; - @Getter private final List customResources; + @Include private final String accountName; + + @Include @Getter private final List namespaces; + + @Include @Getter private final List omitNamespaces; + + @Include private final List kinds; + + @Include List omitKinds; + + private final Map omitKindsComputed; + + @Include @Getter private final List customResources; + + @Include @Getter private final String kubectlExecutable; + + @Include @Getter private final Integer kubectlRequestTimeoutSeconds; + + @Include @Getter private final String kubeconfigFile; + + @Include private final String kubeconfigFileHash; - @Getter private final String kubectlExecutable; - @Getter private final Integer kubectlRequestTimeoutSeconds; - @Getter private final String kubeconfigFile; - @Getter private final boolean serviceAccount; - @Getter private final String context; + @Include @Getter private final boolean serviceAccount; - @Getter private final boolean onlySpinnakerManaged; - @Getter private final boolean liveManifestCalls; - private final boolean checkPermissionsOnStartup; - @Getter private final List cachingPolicies; + @Include @Getter private final String context; - @JsonIgnore @Getter private final String oAuthServiceAccount; - @JsonIgnore @Getter private final List oAuthScopes; + @Include @Getter private final boolean onlySpinnakerManaged; - @Getter private boolean metrics; - @Getter private final boolean debug; + @Include @Getter private final boolean liveManifestCalls; + + @Include private final boolean checkPermissionsOnStartup; + + @Include @Getter private final List cachingPolicies; + + @Include @JsonIgnore @Getter private final String oAuthServiceAccount; + + @Include @JsonIgnore @Getter private final List oAuthScopes; + + @Include @Getter private boolean metrics; + + @Getter private boolean metricsComputed; + + @Include @Getter private final boolean debug; private String cachedDefaultNamespace; private final Supplier> liveNamespaceSupplier; @@ -108,17 +127,23 @@ public KubernetesV2Credentials( this.namespaces = managedAccount.getNamespaces(); this.omitNamespaces = managedAccount.getOmitNamespaces(); this.kinds = KubernetesKind.getOrRegisterKinds(managedAccount.getKinds()); - this.omitKinds = + // omitKinds is a simple placeholder that we can use to compare one instance to another + // when refreshing credentials. + this.omitKinds = managedAccount.getOmitKinds(); + + this.omitKindsComputed = managedAccount.getOmitKinds().stream() .map(KubernetesKind::fromString) .collect( Collectors.toMap( k -> k, k -> InvalidKindReason.EXPLICITLY_OMITTED_BY_CONFIGURATION)); + this.customResources = managedAccount.getCustomResources(); this.kubectlExecutable = managedAccount.getKubectlExecutable(); this.kubectlRequestTimeoutSeconds = managedAccount.getKubectlRequestTimeoutSeconds(); this.kubeconfigFile = kubeconfigFile; + this.kubeconfigFileHash = KubeconfigFileHasher.hashKubeconfigFile(kubeconfigFile); this.serviceAccount = managedAccount.getServiceAccount(); this.context = managedAccount.getContext(); @@ -131,6 +156,8 @@ public KubernetesV2Credentials( this.oAuthScopes = managedAccount.getoAuthScopes(); this.metrics = managedAccount.getMetrics(); + this.metricsComputed = managedAccount.getMetrics(); + this.debug = managedAccount.getDebug(); this.liveNamespaceSupplier = @@ -240,7 +267,7 @@ public InvalidKindReason getInvalidKindReason(@Nonnull KubernetesKind kind) { } else if (!this.kinds.isEmpty()) { return !kinds.contains(kind) ? InvalidKindReason.MISSING_FROM_ALLOWED_KINDS : null; } else { - return this.omitKinds.getOrDefault(kind, null); + return this.omitKindsComputed.getOrDefault(kind, null); } } @@ -341,13 +368,13 @@ private void determineOmitKinds() { Map unreadableKinds = allKinds .parallelStream() - .filter(k -> !k.equals(KubernetesKind.NONE)) - .filter(k -> !omitKinds.keySet().contains(k)) + .filter(k -> k != KubernetesKind.NONE) + .filter(k -> !omitKindsComputed.keySet().contains(k)) .filter(k -> !canReadKind(k, checkNamespace)) .collect(Collectors.toConcurrentMap(k -> k, k -> InvalidKindReason.READ_ERROR)); - omitKinds.putAll(unreadableKinds); + omitKindsComputed.putAll(unreadableKinds); - if (metrics) { + if (metricsComputed) { try { log.info("Checking if pod metrics are readable for account {}...", accountName); topPod(checkNamespace, null); @@ -357,7 +384,7 @@ private void determineOmitKinds() { accountName, e.getMessage()); log.debug("Reading logs for account '{}' failed with exception: ", accountName, e); - metrics = false; + metricsComputed = false; } } } diff --git a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/config/KubernetesConfiguration.groovy b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/config/KubernetesConfiguration.groovy index 1bef6f2254a..4468085583e 100644 --- a/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/config/KubernetesConfiguration.groovy +++ b/clouddriver-kubernetes/src/main/groovy/com/netflix/spinnaker/config/KubernetesConfiguration.groovy @@ -16,25 +16,37 @@ package com.netflix.spinnaker.config +import com.netflix.spinnaker.cats.agent.Agent +import com.netflix.spinnaker.cats.module.CatsModule +import com.netflix.spinnaker.clouddriver.kubernetes.KubernetesCloudProvider import com.netflix.spinnaker.clouddriver.kubernetes.config.KubernetesConfigurationProperties import com.netflix.spinnaker.clouddriver.kubernetes.health.KubernetesHealthIndicator -import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesNamedAccountCredentialsInitializer +import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesNamedAccountCredentials import com.netflix.spinnaker.clouddriver.kubernetes.v1.deploy.KubernetesUtil +import com.netflix.spinnaker.clouddriver.kubernetes.v1.provider.KubernetesV1Provider +import com.netflix.spinnaker.clouddriver.kubernetes.v1.provider.KubernetesV1ProviderSynchronizable +import com.netflix.spinnaker.clouddriver.kubernetes.v1.provider.agent.KubernetesV1CachingAgentDispatcher +import com.netflix.spinnaker.clouddriver.kubernetes.v2.caching.KubernetesV2Provider +import com.netflix.spinnaker.clouddriver.kubernetes.v2.caching.KubernetesV2ProviderSynchronizable +import com.netflix.spinnaker.clouddriver.kubernetes.v2.caching.agent.KubernetesV2CachingAgentDispatcher +import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesResourcePropertyRegistry +import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesSpinnakerKindMap +import com.netflix.spinnaker.clouddriver.security.AccountCredentialsRepository import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean import org.springframework.context.annotation.ComponentScan import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Import import org.springframework.scheduling.annotation.EnableScheduling +import java.util.concurrent.ConcurrentHashMap + @Configuration @EnableConfigurationProperties @EnableScheduling @ConditionalOnProperty('kubernetes.enabled') @ComponentScan(["com.netflix.spinnaker.clouddriver.kubernetes"]) -@Import([ KubernetesNamedAccountCredentialsInitializer ]) class KubernetesConfiguration { @Bean @ConfigurationProperties("kubernetes") @@ -51,4 +63,64 @@ class KubernetesConfiguration { KubernetesUtil kubernetesUtil() { new KubernetesUtil() } + + @Bean + KubernetesV1Provider kubernetesV1Provider( + KubernetesCloudProvider kubernetesCloudProvider + ) { + new KubernetesV1Provider( + kubernetesCloudProvider, + Collections.newSetFromMap(new ConcurrentHashMap()) + ) + } + + @Bean + KubernetesV2Provider kubernetesV2Provider() { + new KubernetesV2Provider() + } + + @Bean + KubernetesV2ProviderSynchronizable kubernetesV2ProviderSynchronizable( + KubernetesV2Provider kubernetesV2Provider, + AccountCredentialsRepository accountCredentialsRepository, + KubernetesV2CachingAgentDispatcher kubernetesV2CachingAgentDispatcher, + KubernetesResourcePropertyRegistry kubernetesResourcePropertyRegistry, + KubernetesConfigurationProperties kubernetesConfigurationProperties, + KubernetesNamedAccountCredentials.CredentialFactory credentialFactory, + KubernetesSpinnakerKindMap kubernetesSpinnakerKindMap, + CatsModule catsModule + ){ + new KubernetesV2ProviderSynchronizable( + kubernetesV2Provider, + accountCredentialsRepository, + kubernetesV2CachingAgentDispatcher, + kubernetesResourcePropertyRegistry, + kubernetesConfigurationProperties, + credentialFactory, + kubernetesSpinnakerKindMap, + catsModule + ) + } + + @Bean + KubernetesV1ProviderSynchronizable kubernetesV1ProviderSynchronizable( + KubernetesV1Provider kubernetesV1Provider, + AccountCredentialsRepository accountCredentialsRepository, + KubernetesV1CachingAgentDispatcher kubernetesV1CachingAgentDispatcher, + KubernetesConfigurationProperties kubernetesConfigurationProperties, + KubernetesNamedAccountCredentials.CredentialFactory credentialFactory, + KubernetesSpinnakerKindMap kubernetesSpinnakerKindMap, + CatsModule catsModule + ) { + new KubernetesV1ProviderSynchronizable( + kubernetesV1Provider, + accountCredentialsRepository, + kubernetesV1CachingAgentDispatcher, + kubernetesConfigurationProperties, + credentialFactory, + kubernetesSpinnakerKindMap, + catsModule + ) + } + } diff --git a/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/security/KubernetesNamedAccountCredentialsInitializerSpec.groovy b/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v1/provider/KubernetesV1ProviderSynchronizableSpec.groovy similarity index 61% rename from clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/security/KubernetesNamedAccountCredentialsInitializerSpec.groovy rename to clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v1/provider/KubernetesV1ProviderSynchronizableSpec.groovy index 92826276e56..8fe73d63749 100644 --- a/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/security/KubernetesNamedAccountCredentialsInitializerSpec.groovy +++ b/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v1/provider/KubernetesV1ProviderSynchronizableSpec.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2019 Google, Inc. + * Copyright 2019 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License") * you may not use this file except in compliance with the License. @@ -14,26 +14,30 @@ * limitations under the License. */ -package com.netflix.spinnaker.clouddriver.kubernetes.v2.security +package com.netflix.spinnaker.clouddriver.kubernetes.v1.provider import com.netflix.spectator.api.NoopRegistry import com.netflix.spinnaker.cats.module.CatsModule import com.netflix.spinnaker.clouddriver.data.ConfigFileService import com.netflix.spinnaker.clouddriver.kubernetes.config.KubernetesConfigurationProperties -import com.netflix.spinnaker.clouddriver.kubernetes.config.LinkedDockerRegistryConfiguration import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesNamedAccountCredentials -import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesNamedAccountCredentialsInitializer +import com.netflix.spinnaker.clouddriver.kubernetes.v1.provider.agent.KubernetesV1CachingAgentDispatcher import com.netflix.spinnaker.clouddriver.kubernetes.v1.security.KubernetesV1Credentials -import com.netflix.spinnaker.clouddriver.kubernetes.v2.names.KubernetesManifestNamer +import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesSpinnakerKindMap import com.netflix.spinnaker.clouddriver.kubernetes.v2.op.job.KubectlJobExecutor import com.netflix.spinnaker.clouddriver.names.NamerRegistry +import com.netflix.spinnaker.clouddriver.security.AccountCredentials import com.netflix.spinnaker.clouddriver.security.AccountCredentialsRepository +import com.netflix.spinnaker.clouddriver.kubernetes.config.LinkedDockerRegistryConfiguration import com.netflix.spinnaker.clouddriver.security.ProviderVersion import spock.lang.Specification -class KubernetesNamedAccountCredentialsInitializerSpec extends Specification { +class KubernetesV1ProviderSynchronizableSpec extends Specification { + CatsModule catsModule = Mock(CatsModule) + KubernetesV1Provider v1Provider = Mock(KubernetesV1Provider) AccountCredentialsRepository accountCredentialsRepository = Mock(AccountCredentialsRepository) + KubernetesV1CachingAgentDispatcher agentDispatcher = Mock(KubernetesV1CachingAgentDispatcher) NamerRegistry namerRegistry = Mock(NamerRegistry) ConfigFileService configFileService = new ConfigFileService() @@ -43,62 +47,32 @@ class KubernetesNamedAccountCredentialsInitializerSpec extends Specification { namerRegistry, accountCredentialsRepository, Mock(KubectlJobExecutor), - configFileService) - - KubernetesNamedAccountCredentialsInitializer kubernetesNamedAccountCredentialsInitializer = new KubernetesNamedAccountCredentialsInitializer( - spectatorRegistry: new NoopRegistry() + configFileService ) - def synchronizeAccounts(KubernetesConfigurationProperties kubernetesConfigurationProperties) { - return kubernetesNamedAccountCredentialsInitializer.synchronizeKubernetesAccounts( - credentialFactory, kubernetesConfigurationProperties, catsModule, accountCredentialsRepository) + def synchronizeAccounts(KubernetesConfigurationProperties configurationProperties) { + KubernetesV1ProviderSynchronizable synchronizable = new KubernetesV1ProviderSynchronizable( + v1Provider, + accountCredentialsRepository, + agentDispatcher, + configurationProperties, + credentialFactory, + new KubernetesSpinnakerKindMap(), + catsModule + ) + + synchronizable.synchronize() } void "is a no-op when there are no configured accounts"() { when: KubernetesConfigurationProperties kubernetesConfigurationProperties = new KubernetesConfigurationProperties() - synchronizeAccounts(kubernetesConfigurationProperties) - - then: - 0 * accountCredentialsRepository.save(*_) - } - - void "correctly creates a v2 account and defaults properties"() { - given: - KubernetesNamedAccountCredentials credentials - - when: - KubernetesConfigurationProperties kubernetesConfigurationProperties = new KubernetesConfigurationProperties() - kubernetesConfigurationProperties.setAccounts([ - new KubernetesConfigurationProperties.ManagedAccount( - name: "test-account", - namespaces: ["default"], - providerVersion: ProviderVersion.v2) - ]) + accountCredentialsRepository.getAll() >> new HashSet() synchronizeAccounts(kubernetesConfigurationProperties) then: - 1 * accountCredentialsRepository.save("test-account", _ as KubernetesNamedAccountCredentials) >> { _, creds -> - credentials = creds - } - 1 * namerRegistry.getNamingStrategy("kubernetesAnnotations") >> Mock(KubernetesManifestNamer) - - credentials.getName() == "test-account" - credentials.getProviderVersion() == ProviderVersion.v2 - credentials.getEnvironment() == "test-account" - credentials.getAccountType() == "test-account" - credentials.getSkin() == "v2" - credentials.getCacheThreads() == 1 - credentials.getCacheIntervalSeconds() == null - - credentials.getCredentials() instanceof KubernetesV2Credentials - KubernetesV2Credentials accountCredentials = (KubernetesV2Credentials) credentials.getCredentials() - accountCredentials.isServiceAccount() == false - accountCredentials.isOnlySpinnakerManaged() == false - accountCredentials.isDebug() == false - accountCredentials.isMetrics() == true - accountCredentials.isLiveManifestCalls() == false + 0 * accountCredentialsRepository.save(*_) } void "correctly creates a v1 account and defaults properties"() { @@ -107,6 +81,8 @@ class KubernetesNamedAccountCredentialsInitializerSpec extends Specification { when: KubernetesConfigurationProperties kubernetesConfigurationProperties = new KubernetesConfigurationProperties() + accountCredentialsRepository.getAll() >> new HashSet() + kubernetesConfigurationProperties.setAccounts([ new KubernetesConfigurationProperties.ManagedAccount( name: "test-account", @@ -146,4 +122,5 @@ clusters: KubernetesV1Credentials accountCredentials = (KubernetesV1Credentials) credentials.getCredentials() accountCredentials.getDeclaredNamespaces() == ["default"] } + } diff --git a/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/KubernetesV2ProviderSynchronizableSpec.groovy b/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/KubernetesV2ProviderSynchronizableSpec.groovy new file mode 100644 index 00000000000..47600660aee --- /dev/null +++ b/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/caching/KubernetesV2ProviderSynchronizableSpec.groovy @@ -0,0 +1,119 @@ +/* + * Copyright 2019 Armory + * + * 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.caching + +import com.netflix.spectator.api.NoopRegistry +import com.netflix.spinnaker.cats.module.CatsModule +import com.netflix.spinnaker.clouddriver.data.ConfigFileService +import com.netflix.spinnaker.clouddriver.kubernetes.config.KubernetesConfigurationProperties +import com.netflix.spinnaker.clouddriver.kubernetes.security.KubernetesNamedAccountCredentials +import com.netflix.spinnaker.clouddriver.kubernetes.v2.caching.agent.KubernetesV2CachingAgentDispatcher +import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesResourcePropertyRegistry +import com.netflix.spinnaker.clouddriver.kubernetes.v2.description.KubernetesSpinnakerKindMap +import com.netflix.spinnaker.clouddriver.kubernetes.v2.names.KubernetesManifestNamer +import com.netflix.spinnaker.clouddriver.kubernetes.v2.op.job.KubectlJobExecutor +import com.netflix.spinnaker.clouddriver.kubernetes.v2.security.KubernetesV2Credentials +import com.netflix.spinnaker.clouddriver.names.NamerRegistry +import com.netflix.spinnaker.clouddriver.security.AccountCredentials +import com.netflix.spinnaker.clouddriver.security.AccountCredentialsRepository +import com.netflix.spinnaker.clouddriver.security.ProviderVersion +import spock.lang.Specification + +class KubernetesV2ProviderSynchronizableSpec extends Specification { + + CatsModule catsModule = Mock(CatsModule) + AccountCredentialsRepository accountCredentialsRepository = Mock(AccountCredentialsRepository) + NamerRegistry namerRegistry = Mock(NamerRegistry) + ConfigFileService configFileService = Mock(ConfigFileService) + KubernetesV2Provider kubernetesV2Provider = new KubernetesV2Provider() + KubernetesV2CachingAgentDispatcher agentDispatcher = Mock(KubernetesV2CachingAgentDispatcher) + KubernetesResourcePropertyRegistry kubernetesResourcePropertyRegistry = Mock(KubernetesResourcePropertyRegistry) + + KubernetesNamedAccountCredentials.CredentialFactory credentialFactory = new KubernetesNamedAccountCredentials.CredentialFactory( + "userAgent", + new NoopRegistry(), + namerRegistry, + accountCredentialsRepository, + Mock(KubectlJobExecutor), + configFileService + ) + + def synchronizeAccounts(KubernetesConfigurationProperties configurationProperties) { + KubernetesV2ProviderSynchronizable synchronizable = new KubernetesV2ProviderSynchronizable( + kubernetesV2Provider, + accountCredentialsRepository, + agentDispatcher, + kubernetesResourcePropertyRegistry, + configurationProperties, + credentialFactory, + new KubernetesSpinnakerKindMap(), + catsModule + ) + + synchronizable.synchronize() + } + + void "is a no-op when there are no configured accounts"() { + when: + KubernetesConfigurationProperties kubernetesConfigurationProperties = new KubernetesConfigurationProperties() + accountCredentialsRepository.getAll() >> new HashSet() + synchronizeAccounts(kubernetesConfigurationProperties) + + + then: + 0 * accountCredentialsRepository.save(*_) + } + + void "correctly creates a v2 account and defaults properties"() { + given: + KubernetesNamedAccountCredentials credentials + + when: + KubernetesConfigurationProperties configurationProperties = new KubernetesConfigurationProperties() + configurationProperties.setAccounts([ + new KubernetesConfigurationProperties.ManagedAccount( + name: "test-account", + namespaces: ["default"], + providerVersion: ProviderVersion.v2, + ) + ]) + accountCredentialsRepository.getAll() >> new HashSet() + synchronizeAccounts(configurationProperties) + + then: + 1 * accountCredentialsRepository.save("test-account", _ as KubernetesNamedAccountCredentials) >> { _, creds -> + credentials = creds + } + 1 * namerRegistry.getNamingStrategy("kubernetesAnnotations") >> Mock(KubernetesManifestNamer) + + credentials.getName() == "test-account" + credentials.getProviderVersion() == ProviderVersion.v2 + credentials.getEnvironment() == "test-account" + credentials.getAccountType() == "test-account" + credentials.getSkin() == "v2" + credentials.getCacheIntervalSeconds() == null + credentials.getCacheThreads() == 1 + + credentials.getCredentials() instanceof KubernetesV2Credentials + KubernetesV2Credentials accountCredentials = (KubernetesV2Credentials) credentials.getCredentials() + accountCredentials.isServiceAccount() == false + accountCredentials.isOnlySpinnakerManaged() == false + accountCredentials.isDebug() == false + accountCredentials.isMetrics() == true + accountCredentials.isLiveManifestCalls() == false + } +}