From b04520b122b7eab12ab49e8679fa9ffe7484061d Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Thu, 27 Jun 2019 10:34:56 -0500 Subject: [PATCH] feat(kubernetes,google): Support retrieving config files from config server. (#3812) --- .../AppengineCredentialsInitializer.groovy | 18 +- .../clouddriver-cloudfoundry.gradle | 1 - clouddriver-core/clouddriver-core.gradle | 6 + .../cache/CloudConfigRefreshConfig.java | 45 +++-- .../clouddriver/data/ConfigFileService.java | 186 ++++++++++++++++++ .../data/ConfigFileServiceTest.java | 167 ++++++++++++++++ .../config/GoogleCommonManagedAccount.groovy | 12 -- .../deploy/ops/GoogleUserDataProvider.groovy | 9 +- .../GoogleCredentialsInitializer.groovy | 17 +- .../GoogleCredentialsInitializerSpec.groovy | 8 +- .../KubernetesNamedAccountCredentials.java | 45 ++--- .../v2/security/KubernetesV2Credentials.java | 5 +- ...edAccountCredentialsInitializerSpec.groovy | 7 +- .../KubernetesV2CredentialsSpec.groovy | 2 +- 14 files changed, 446 insertions(+), 82 deletions(-) create mode 100644 clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/data/ConfigFileService.java create mode 100644 clouddriver-core/src/test/groovy/com/netflix/spinnaker/clouddriver/data/ConfigFileServiceTest.java diff --git a/clouddriver-appengine/src/main/groovy/com/netflix/spinnaker/clouddriver/appengine/security/AppengineCredentialsInitializer.groovy b/clouddriver-appengine/src/main/groovy/com/netflix/spinnaker/clouddriver/appengine/security/AppengineCredentialsInitializer.groovy index 3c6fcf93014..81f00b647b1 100644 --- a/clouddriver-appengine/src/main/groovy/com/netflix/spinnaker/clouddriver/appengine/security/AppengineCredentialsInitializer.groovy +++ b/clouddriver-appengine/src/main/groovy/com/netflix/spinnaker/clouddriver/appengine/security/AppengineCredentialsInitializer.groovy @@ -19,6 +19,7 @@ package com.netflix.spinnaker.clouddriver.appengine.security import com.netflix.spinnaker.cats.module.CatsModule import com.netflix.spinnaker.clouddriver.appengine.AppengineJobExecutor import com.netflix.spinnaker.clouddriver.appengine.config.AppengineConfigurationProperties +import com.netflix.spinnaker.clouddriver.data.ConfigFileService import com.netflix.spinnaker.clouddriver.security.AccountCredentialsRepository import com.netflix.spinnaker.clouddriver.security.ProviderUtils import groovy.util.logging.Slf4j @@ -34,13 +35,15 @@ class AppengineCredentialsInitializer { String clouddriverUserAgentApplicationName, AppengineConfigurationProperties appengineConfigurationProperties, AccountCredentialsRepository accountCredentialsRepository, - AppengineJobExecutor jobExecutor) { + AppengineJobExecutor jobExecutor, + ConfigFileService configFileService) { synchronizeAppengineAccounts(clouddriverUserAgentApplicationName, appengineConfigurationProperties, null, accountCredentialsRepository, - jobExecutor) + jobExecutor, + configFileService) } private List synchronizeAppengineAccounts( @@ -48,7 +51,8 @@ class AppengineCredentialsInitializer { AppengineConfigurationProperties appengineConfigurationProperties, CatsModule catsModule, AccountCredentialsRepository accountCredentialsRepository, - AppengineJobExecutor jobExecutor) { + AppengineJobExecutor jobExecutor, + ConfigFileService configFileService) { def (ArrayList accountsToAdd, List namesOfDeletedAccounts) = ProviderUtils.calculateAccountDeltas(accountCredentialsRepository, @@ -63,7 +67,7 @@ class AppengineCredentialsInitializer { } managedAccount.initialize(jobExecutor, gcloudPath) - def jsonKey = AppengineCredentialsInitializer.getJsonKey(managedAccount) + def jsonKey = configFileService.getContents(managedAccount.getJsonPath()) def appengineAccount = new AppengineNamedAccountCredentials.Builder() .name(managedAccount.name) .environment(managedAccount.environment ?: managedAccount.name) @@ -104,10 +108,4 @@ class AppengineCredentialsInitializer { it instanceof AppengineNamedAccountCredentials } as List } - - private static String getJsonKey(AppengineConfigurationProperties.ManagedAccount managedAccount) { - def inputStream = managedAccount.inputStream - - inputStream ? new String(inputStream.bytes) : null - } } diff --git a/clouddriver-cloudfoundry/clouddriver-cloudfoundry.gradle b/clouddriver-cloudfoundry/clouddriver-cloudfoundry.gradle index 0daa64e2654..5a8cb9be7a1 100644 --- a/clouddriver-cloudfoundry/clouddriver-cloudfoundry.gradle +++ b/clouddriver-cloudfoundry/clouddriver-cloudfoundry.gradle @@ -41,5 +41,4 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-engine" testImplementation "org.junit.jupiter:junit-jupiter-params" testImplementation "org.mockito:mockito-core" - testImplementation "org.mockito:mockito-core" } diff --git a/clouddriver-core/clouddriver-core.gradle b/clouddriver-core/clouddriver-core.gradle index cd02f892571..36cdd38cb3f 100644 --- a/clouddriver-core/clouddriver-core.gradle +++ b/clouddriver-core/clouddriver-core.gradle @@ -30,6 +30,7 @@ dependencies { implementation "org.codehaus.groovy:groovy-all" implementation "org.springframework.boot:spring-boot-actuator" implementation "org.springframework.cloud:spring-cloud-context" + implementation "org.springframework.cloud:spring-cloud-config-server" implementation "redis.clients:jedis" testImplementation project(":cats:cats-test") @@ -40,4 +41,9 @@ dependencies { testImplementation "org.objenesis:objenesis" testImplementation "org.spockframework:spock-core" testImplementation "org.spockframework:spock-spring" + testImplementation "org.assertj:assertj-core" + testImplementation "org.junit.jupiter:junit-jupiter-api" + testImplementation "org.junit.jupiter:junit-jupiter-engine" + testImplementation "org.junit.jupiter:junit-jupiter-params" + testImplementation "org.mockito:mockito-core" } diff --git a/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/cache/CloudConfigRefreshConfig.java b/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/cache/CloudConfigRefreshConfig.java index e8313333431..a2f0d509af9 100644 --- a/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/cache/CloudConfigRefreshConfig.java +++ b/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/cache/CloudConfigRefreshConfig.java @@ -17,12 +17,16 @@ package com.netflix.spinnaker.clouddriver.cache; import com.netflix.spinnaker.cats.cluster.DefaultAgentIntervalProvider; +import com.netflix.spinnaker.clouddriver.data.ConfigFileService; import com.netflix.spinnaker.clouddriver.refresh.CloudConfigRefreshScheduler; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cloud.config.server.EnableConfigServer; +import org.springframework.cloud.config.server.environment.EnvironmentRepository; +import org.springframework.cloud.config.server.resource.ResourceRepository; import org.springframework.cloud.context.refresh.ContextRefresher; import org.springframework.context.annotation.*; import org.springframework.core.type.AnnotatedTypeMetadata; @@ -33,21 +37,40 @@ */ @Configuration @AutoConfigureAfter({RedisCacheConfig.class, DynomiteCacheConfig.class}) -@Conditional(RemoteConfigSourceConfigured.class) public class CloudConfigRefreshConfig { - @Bean - @ConditionalOnBean(DefaultAgentIntervalProvider.class) - public CloudConfigRefreshScheduler intervalProviderConfigRefreshScheduler( - ContextRefresher contextRefresher, DefaultAgentIntervalProvider agentIntervalProvider) { - return new CloudConfigRefreshScheduler(contextRefresher, agentIntervalProvider.getInterval()); + @Configuration + @Conditional(RemoteConfigSourceConfigured.class) + @EnableConfigServer + static class RemoteConfigSourceConfiguration { + @Bean + ConfigFileService configFileService( + ResourceRepository resourceRepository, EnvironmentRepository environmentRepository) { + return new ConfigFileService(resourceRepository, environmentRepository); + } + + @Bean + @ConditionalOnBean(DefaultAgentIntervalProvider.class) + public CloudConfigRefreshScheduler intervalProviderConfigRefreshScheduler( + ContextRefresher contextRefresher, DefaultAgentIntervalProvider agentIntervalProvider) { + return new CloudConfigRefreshScheduler(contextRefresher, agentIntervalProvider.getInterval()); + } + + @Bean + @ConditionalOnMissingBean(DefaultAgentIntervalProvider.class) + public CloudConfigRefreshScheduler defaultIntervalConfigRefreshScheduler( + ContextRefresher contextRefresher) { + return new CloudConfigRefreshScheduler(contextRefresher, 60); + } } - @Bean - @ConditionalOnMissingBean(DefaultAgentIntervalProvider.class) - public CloudConfigRefreshScheduler defaultIntervalConfigRefreshScheduler( - ContextRefresher contextRefresher) { - return new CloudConfigRefreshScheduler(contextRefresher, 60); + @Configuration + static class DefaultConfiguration { + @Bean + @ConditionalOnMissingBean(ConfigFileService.class) + ConfigFileService configFileService() { + return new ConfigFileService(); + } } } diff --git a/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/data/ConfigFileService.java b/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/data/ConfigFileService.java new file mode 100644 index 00000000000..735f5e9c4c2 --- /dev/null +++ b/clouddriver-core/src/main/groovy/com/netflix/spinnaker/clouddriver/data/ConfigFileService.java @@ -0,0 +1,186 @@ +/* + * Copyright 2019 Pivotal, 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.data; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.config.server.environment.EnvironmentRepository; +import org.springframework.cloud.config.server.resource.NoSuchResourceException; +import org.springframework.cloud.config.server.resource.ResourceRepository; +import org.springframework.cloud.config.server.support.EnvironmentPropertySource; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.io.Resource; + +@Slf4j +public class ConfigFileService { + private static final String CONFIG_SERVER_RESOURCE_PREFIX = "configserver:"; + private static final String CLASSPATH_FILE_PREFIX = "classpath:"; + + private final ResourceRepository resourceRepository; + private final EnvironmentRepository environmentRepository; + + @Value("${spring.application.name:application}") + private String applicationName = "application"; + + public ConfigFileService() { + resourceRepository = null; + environmentRepository = null; + } + + public ConfigFileService( + ResourceRepository resourceRepository, EnvironmentRepository environmentRepository) { + this.resourceRepository = resourceRepository; + this.environmentRepository = environmentRepository; + } + + public String getLocalPath(String path, String tempFilePrefix, String tempFileSuffix) { + if (StringUtils.isNotEmpty(path)) { + if (isCloudConfigResource(path)) { + String configServerContents = retrieveFromConfigServer(path); + return writeToTempFile(configServerContents, tempFilePrefix, tempFileSuffix); + } else { + return verifyLocalPath(path); + } + } + + return null; + } + + public String getLocalPathForContents( + String contents, String tempFilePrefix, String tempFileSuffix) { + if (StringUtils.isNotEmpty(contents)) { + return writeToTempFile(contents, tempFilePrefix, tempFileSuffix); + } + + return null; + } + + public String getContents(String path) { + if (StringUtils.isNotEmpty(path)) { + if (isCloudConfigResource(path)) { + return retrieveFromConfigServer(path); + } + + if (isClasspathResource(path)) { + return retrieveFromClasspath(path); + } + + return retrieveFromLocalPath(path); + } + + return null; + } + + private boolean isCloudConfigResource(String path) { + return path.startsWith(CONFIG_SERVER_RESOURCE_PREFIX); + } + + private boolean isClasspathResource(String path) { + return path.startsWith(CLASSPATH_FILE_PREFIX); + } + + private String verifyLocalPath(String path) { + if (Files.isReadable(Paths.get(path))) { + return path; + } else { + throw new RuntimeException("File \"" + path + "\" not found or is not readable"); + } + } + + private String retrieveFromLocalPath(String path) { + try { + Path filePath = Paths.get(path); + return new String(Files.readAllBytes(filePath)); + } catch (FileNotFoundException e) { + throw new RuntimeException("File \"" + path + "\" not found or is not readable", e); + } catch (IOException e) { + throw new RuntimeException("Exception reading file " + path, e); + } + } + + private String retrieveFromClasspath(String path) { + try { + String filePath = path.substring(CLASSPATH_FILE_PREFIX.length()); + InputStream inputStream = getClass().getResourceAsStream(filePath); + return IOUtils.toString(inputStream, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Exception reading classpath resource \"" + path + "\"", e); + } + } + + private String retrieveFromConfigServer(String path) { + if (resourceRepository == null || environmentRepository == null) { + throw new RuntimeException( + "Config Server repository not configured for resource \"" + path + "\""); + } + + try { + String fileName = path.substring(CONFIG_SERVER_RESOURCE_PREFIX.length()); + Resource resource = this.resourceRepository.findOne(applicationName, null, null, fileName); + try (InputStream inputStream = resource.getInputStream()) { + Environment environment = this.environmentRepository.findOne(applicationName, null, null); + + String text = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + StandardEnvironment preparedEnvironment = + EnvironmentPropertySource.prepareEnvironment(environment); + return EnvironmentPropertySource.resolvePlaceholders(preparedEnvironment, text); + } + } catch (NoSuchResourceException e) { + throw new RuntimeException("The resource \"" + path + "\" was not found in config server", e); + } catch (IOException e) { + throw new RuntimeException("Exception reading config server resource \"" + path + "\"", e); + } + } + + private String writeToTempFile(String contents, String tempFilePrefix, String tempFileSuffix) { + try { + File tempFile = File.createTempFile(tempFilePrefix, tempFileSuffix); + tempFile.deleteOnExit(); + + BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile)); + writer.write(contents); + writer.close(); + + log.info( + "Configuration for {} written to temporary file {}", + tempFilePrefix, + tempFile.getAbsolutePath()); + + return tempFile.getAbsolutePath(); + } catch (IOException e) { + throw new RuntimeException( + "Exception writing temporary file with prefix \"" + + tempFilePrefix + + "\": " + + e.getMessage(), + e); + } + } +} diff --git a/clouddriver-core/src/test/groovy/com/netflix/spinnaker/clouddriver/data/ConfigFileServiceTest.java b/clouddriver-core/src/test/groovy/com/netflix/spinnaker/clouddriver/data/ConfigFileServiceTest.java new file mode 100644 index 00000000000..3a6df917642 --- /dev/null +++ b/clouddriver-core/src/test/groovy/com/netflix/spinnaker/clouddriver/data/ConfigFileServiceTest.java @@ -0,0 +1,167 @@ +/* + * Copyright 2019 Pivotal, 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.data; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.cloud.config.environment.Environment; +import org.springframework.cloud.config.server.environment.EnvironmentRepository; +import org.springframework.cloud.config.server.resource.NoSuchResourceException; +import org.springframework.cloud.config.server.resource.ResourceRepository; +import org.springframework.core.io.ByteArrayResource; + +class ConfigFileServiceTest { + private static final String TEST_FILE_NAME = "testfile"; + private static final String TEST_FILE_PATH = + Paths.get(System.getProperty("java.io.tmpdir"), TEST_FILE_NAME).toString(); + private static final String CLOUD_TEST_FILE_NAME = "configserver:" + TEST_FILE_NAME; + private static final String TEST_FILE_CONTENTS = "test file contents"; + + private ResourceRepository resourceRepository = mock(ResourceRepository.class); + private EnvironmentRepository environmentRepository = mock(EnvironmentRepository.class); + + private ConfigFileService configFileService; + + @BeforeEach + void setUp() { + configFileService = new ConfigFileService(resourceRepository, environmentRepository); + } + + @AfterEach + void tearDown() throws IOException { + Files.deleteIfExists(Paths.get(TEST_FILE_PATH)); + } + + @Test + void getLocalPathWhenFileExists() throws IOException { + createExpectedFile(); + + String fileName = configFileService.getLocalPath(TEST_FILE_PATH, "test", ".file"); + assertThat(fileName).isEqualTo(TEST_FILE_PATH); + } + + @Test + void getLocalPathWhenFileDoesNotExist() { + RuntimeException exception = + assertThrows( + RuntimeException.class, + () -> configFileService.getLocalPath(TEST_FILE_PATH, "test", ".file")); + assertThat(exception.getMessage()).contains(TEST_FILE_PATH); + } + + @Test + void getLocalPathWhenFileInConfigServer() { + expectFileInConfigServer(); + + String fileName = configFileService.getLocalPath(CLOUD_TEST_FILE_NAME, "test", ".file"); + assertThat(baseName(fileName)).startsWith("test").endsWith(".file"); + } + + @Test + void getLocalPathWhenFileNotInConfigServer() { + expectFileNotInConfigServer(); + + RuntimeException exception = + assertThrows( + RuntimeException.class, + () -> configFileService.getLocalPath(CLOUD_TEST_FILE_NAME, "test", ".file")); + assertThat(exception.getMessage()).contains(CLOUD_TEST_FILE_NAME); + } + + @Test + void getLocalPathWhenConfigServerNotConfigured() { + configFileService = new ConfigFileService(); + + RuntimeException exception = + assertThrows( + RuntimeException.class, + () -> configFileService.getLocalPath(CLOUD_TEST_FILE_NAME, "test", ".file")); + assertThat(exception.getMessage()).contains(CLOUD_TEST_FILE_NAME); + } + + @Test + void getLocalPathWhenContentProvided() { + String fileName = + configFileService.getLocalPathForContents(TEST_FILE_CONTENTS, "test", ".file"); + assertThat(baseName(fileName)).startsWith("test").endsWith(".file"); + } + + @Test + void getContentsWhenFileExists() throws IOException { + createExpectedFile(); + + String contents = configFileService.getContents(TEST_FILE_PATH); + assertThat(contents).isEqualTo(TEST_FILE_CONTENTS); + } + + @Test + void getContentsWhenFileInConfigServer() { + expectFileInConfigServer(); + + String contents = configFileService.getContents(CLOUD_TEST_FILE_NAME); + assertThat(contents).isEqualTo(TEST_FILE_CONTENTS); + } + + @Test + void getContentsWhenFileNotInConfigServer() { + expectFileNotInConfigServer(); + + RuntimeException exception = + assertThrows( + RuntimeException.class, () -> configFileService.getContents(CLOUD_TEST_FILE_NAME)); + assertThat(exception.getMessage()).contains(CLOUD_TEST_FILE_NAME); + } + + private String baseName(String fileName) { + return new File(fileName).getName(); + } + + private void createExpectedFile() throws IOException { + File file = new File(TEST_FILE_PATH); + file.deleteOnExit(); + + FileWriter fileWriter = new FileWriter(file); + fileWriter.write(TEST_FILE_CONTENTS); + fileWriter.close(); + } + + private void expectFileInConfigServer() { + when(resourceRepository.findOne(anyString(), isNull(), isNull(), eq(TEST_FILE_NAME))) + .thenReturn(new ByteArrayResource(TEST_FILE_CONTENTS.getBytes())); + when(environmentRepository.findOne(anyString(), isNull(), isNull())) + .thenReturn(new Environment("application")); + } + + private void expectFileNotInConfigServer() { + when(resourceRepository.findOne(anyString(), isNull(), isNull(), eq(TEST_FILE_NAME))) + .thenThrow(new NoSuchResourceException(TEST_FILE_NAME)); + } +} diff --git a/clouddriver-google-common/src/main/groovy/com/netflix/spinnaker/clouddriver/googlecommon/config/GoogleCommonManagedAccount.groovy b/clouddriver-google-common/src/main/groovy/com/netflix/spinnaker/clouddriver/googlecommon/config/GoogleCommonManagedAccount.groovy index 26d01799abc..e9f1607f38b 100644 --- a/clouddriver-google-common/src/main/groovy/com/netflix/spinnaker/clouddriver/googlecommon/config/GoogleCommonManagedAccount.groovy +++ b/clouddriver-google-common/src/main/groovy/com/netflix/spinnaker/clouddriver/googlecommon/config/GoogleCommonManagedAccount.groovy @@ -28,16 +28,4 @@ class GoogleCommonManagedAccount { String serviceAccountProject @Deprecated List requiredGroupMembership Permissions.Builder permissions = new Permissions.Builder() - - public InputStream getInputStream() { - if (jsonPath) { - if (jsonPath.startsWith("classpath:")) { - return getClass().getResourceAsStream(jsonPath.replace("classpath:", "")) - } else { - return new FileInputStream(new File(jsonPath)) - } - } else { - return null - } - } } diff --git a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/ops/GoogleUserDataProvider.groovy b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/ops/GoogleUserDataProvider.groovy index 1cf0cad2701..a3629f70663 100644 --- a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/ops/GoogleUserDataProvider.groovy +++ b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/deploy/ops/GoogleUserDataProvider.groovy @@ -17,10 +17,12 @@ package com.netflix.spinnaker.clouddriver.google.deploy.ops import com.netflix.frigga.Names +import com.netflix.spinnaker.clouddriver.data.ConfigFileService import com.netflix.spinnaker.clouddriver.google.deploy.description.BasicGoogleDeployDescription import com.netflix.spinnaker.clouddriver.google.security.GoogleNamedAccountCredentials import groovy.transform.PackageScope import groovy.util.logging.Slf4j +import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component /** @@ -32,6 +34,8 @@ import org.springframework.stereotype.Component @Slf4j @Component public class GoogleUserDataProvider { + @Autowired + private ConfigFileService configFileService /** * Returns the user data as a Map. @@ -62,9 +66,8 @@ public class GoogleUserDataProvider { return [] } try { - File file = new File(filename) - return file.readLines() - } catch (IOException e) { + return configFileService.getContents(filename).readLines() + } catch (Exception e) { log.warn("Failed to read user data file ${filename}; ${e.message}") return [] } diff --git a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/security/GoogleCredentialsInitializer.groovy b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/security/GoogleCredentialsInitializer.groovy index b68760792de..ba6a008f089 100644 --- a/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/security/GoogleCredentialsInitializer.groovy +++ b/clouddriver-google/src/main/groovy/com/netflix/spinnaker/clouddriver/google/security/GoogleCredentialsInitializer.groovy @@ -17,6 +17,7 @@ package com.netflix.spinnaker.clouddriver.google.security import com.netflix.spinnaker.cats.module.CatsModule +import com.netflix.spinnaker.clouddriver.data.ConfigFileService import com.netflix.spinnaker.clouddriver.google.ComputeVersion import com.netflix.spinnaker.clouddriver.names.NamerRegistry import com.netflix.spinnaker.config.GoogleConfiguration.DeployDefaults @@ -49,10 +50,11 @@ class GoogleCredentialsInitializer { String clouddriverUserAgentApplicationName, GoogleConfigurationProperties googleConfigurationProperties, AccountCredentialsRepository accountCredentialsRepository, - DeployDefaults googleDeployDefaults) { + DeployDefaults googleDeployDefaults, + ConfigFileService configFileService) { synchronizeGoogleAccounts(clouddriverUserAgentApplicationName, googleConfigurationProperties, - null, accountCredentialsRepository, googleDeployDefaults) + null, accountCredentialsRepository, googleDeployDefaults, configFileService) } List synchronizeGoogleAccounts( @@ -60,7 +62,8 @@ class GoogleCredentialsInitializer { GoogleConfigurationProperties googleConfigurationProperties, CatsModule catsModule, AccountCredentialsRepository accountCredentialsRepository, - DeployDefaults googleDeployDefaults) { + DeployDefaults googleDeployDefaults, + ConfigFileService configFileService) { def (ArrayList accountsToAdd, List namesOfDeletedAccounts) = ProviderUtils.calculateAccountDeltas(accountCredentialsRepository, @@ -69,7 +72,7 @@ class GoogleCredentialsInitializer { accountsToAdd.each { GoogleConfigurationProperties.ManagedAccount managedAccount -> try { - def jsonKey = GoogleCredentialsInitializer.getJsonKey(managedAccount) + def jsonKey = configFileService.getContents(managedAccount.getJsonPath()) def googleAccount = new GoogleNamedAccountCredentials.Builder() .name(managedAccount.name) .environment(managedAccount.environment ?: managedAccount.name) @@ -109,10 +112,4 @@ class GoogleCredentialsInitializer { it instanceof GoogleNamedAccountCredentials } as List } - - private static String getJsonKey(GoogleConfigurationProperties.ManagedAccount managedAccount) { - def inputStream = managedAccount.inputStream - - inputStream ? new String(managedAccount.inputStream.bytes) : null - } } diff --git a/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/security/GoogleCredentialsInitializerSpec.groovy b/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/security/GoogleCredentialsInitializerSpec.groovy index c1d6fb5996e..08f5568fd43 100644 --- a/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/security/GoogleCredentialsInitializerSpec.groovy +++ b/clouddriver-google/src/test/groovy/com/netflix/spinnaker/clouddriver/google/security/GoogleCredentialsInitializerSpec.groovy @@ -17,6 +17,7 @@ package com.netflix.spinnaker.clouddriver.google.security import com.netflix.spectator.api.DefaultRegistry +import com.netflix.spinnaker.clouddriver.data.ConfigFileService import com.netflix.spinnaker.config.GoogleConfiguration import com.netflix.spinnaker.clouddriver.google.config.GoogleConfigurationProperties import com.netflix.spinnaker.clouddriver.google.GoogleExecutor @@ -39,10 +40,11 @@ class GoogleCredentialsInitializerSpec extends Specification { ) def accountRepo = Mock(AccountCredentialsRepository) def deployDefaults = new GoogleConfiguration.DeployDefaults() + def configFileService = new ConfigFileService() when: - init.synchronizeGoogleAccounts("clouddriver", configProps, null, accountRepo, deployDefaults) + init.synchronizeGoogleAccounts("clouddriver", configProps, null, accountRepo, deployDefaults, configFileService) then: noExceptionThrown() @@ -63,10 +65,10 @@ class GoogleCredentialsInitializerSpec extends Specification { ) def accountRepo = Mock(AccountCredentialsRepository) def deployDefaults = new GoogleConfiguration.DeployDefaults() - + def configFileService = new ConfigFileService() when: - init.synchronizeGoogleAccounts("clouddriver", configProps, null, accountRepo, deployDefaults) + init.synchronizeGoogleAccounts("clouddriver", configProps, null, accountRepo, deployDefaults, configFileService) then: thrown(IllegalArgumentException) 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 eee829ef1f0..f0763399dac 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 @@ -17,6 +17,7 @@ package com.netflix.spinnaker.clouddriver.kubernetes.security; import com.netflix.spectator.api.Registry; +import com.netflix.spinnaker.clouddriver.data.ConfigFileService; import com.netflix.spinnaker.clouddriver.kubernetes.KubernetesCloudProvider; import com.netflix.spinnaker.clouddriver.kubernetes.config.KubernetesConfigurationProperties; import com.netflix.spinnaker.clouddriver.kubernetes.v1.security.KubernetesV1Credentials; @@ -29,10 +30,6 @@ import com.netflix.spinnaker.clouddriver.security.AccountCredentialsRepository; import com.netflix.spinnaker.clouddriver.security.ProviderVersion; import com.netflix.spinnaker.fiat.model.resources.Permissions; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -114,9 +111,9 @@ public Map getSpinnakerKindMap() { ((KubernetesV2Credentials) creds) .getCustomResources() .forEach( - customResource -> { - kindMap.put(customResource.getKubernetesKind(), customResource.getSpinnakerKind()); - }); + customResource -> + kindMap.put( + customResource.getKubernetesKind(), customResource.getSpinnakerKind())); } return kindMap; } @@ -129,8 +126,9 @@ public static class CredentialFactory { private final NamerRegistry namerRegistry; private final AccountCredentialsRepository accountCredentialsRepository; private final KubectlJobExecutor jobExecutor; + private final ConfigFileService configFileService; - public KubernetesV1Credentials buildV1Credentials( + KubernetesV1Credentials buildV1Credentials( KubernetesConfigurationProperties.ManagedAccount managedAccount) { validateAccount(managedAccount); return new KubernetesV1Credentials( @@ -149,7 +147,7 @@ public KubernetesV1Credentials buildV1Credentials( accountCredentialsRepository); } - public KubernetesV2Credentials buildV2Credentials( + KubernetesV2Credentials buildV2Credentials( KubernetesConfigurationProperties.ManagedAccount managedAccount) { validateAccount(managedAccount); NamerRegistry.lookup() @@ -158,7 +156,8 @@ public KubernetesV2Credentials buildV2Credentials( .setNamer( KubernetesManifest.class, namerRegistry.getNamingStrategy(managedAccount.getNamingStrategy())); - return new KubernetesV2Credentials(spectatorRegistry, jobExecutor, managedAccount); + return new KubernetesV2Credentials( + spectatorRegistry, jobExecutor, managedAccount, getKubeconfigFile(managedAccount)); } private void validateAccount(KubernetesConfigurationProperties.ManagedAccount managedAccount) { @@ -180,24 +179,16 @@ private void validateAccount(KubernetesConfigurationProperties.ManagedAccount ma private String getKubeconfigFile( KubernetesConfigurationProperties.ManagedAccount managedAccount) { - String kubeconfigFile = managedAccount.getKubeconfigFile(); - if (StringUtils.isEmpty(kubeconfigFile)) { - if (StringUtils.isEmpty(managedAccount.getKubeconfigContents())) { - kubeconfigFile = System.getProperty("user.home") + "/.kube/config"; - } else { - try { - File temp = File.createTempFile("kube", "config"); - BufferedWriter writer = new BufferedWriter(new FileWriter(temp)); - writer.write(managedAccount.getKubeconfigContents()); - writer.close(); - kubeconfigFile = temp.getAbsolutePath(); - } catch (IOException e) { - throw new RuntimeException( - "Unable to persist 'kubeconfigContents' parameter to disk: " + e.getMessage(), e); - } - } + if (StringUtils.isNotEmpty(managedAccount.getKubeconfigFile())) { + return configFileService.getLocalPath(managedAccount.getKubeconfigFile(), "kube", "config"); } - return kubeconfigFile; + + if (StringUtils.isNotEmpty(managedAccount.getKubeconfigContents())) { + return configFileService.getLocalPathForContents( + managedAccount.getKubeconfigContents(), "kube", "config"); + } + + return System.getProperty("user.home") + "/.kube/config"; } } } 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 c306da251fb..21cef1ab78d 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 @@ -97,7 +97,8 @@ public class KubernetesV2Credentials implements KubernetesCredentials { public KubernetesV2Credentials( Registry registry, KubectlJobExecutor jobExecutor, - KubernetesConfigurationProperties.ManagedAccount managedAccount) { + KubernetesConfigurationProperties.ManagedAccount managedAccount, + String kubeconfigFile) { this.registry = registry; this.clock = registry.clock(); this.jobExecutor = jobExecutor; @@ -116,7 +117,7 @@ public KubernetesV2Credentials( this.kubectlExecutable = managedAccount.getKubectlExecutable(); this.kubectlRequestTimeoutSeconds = managedAccount.getKubectlRequestTimeoutSeconds(); - this.kubeconfigFile = managedAccount.getKubeconfigFile(); + this.kubeconfigFile = kubeconfigFile; this.serviceAccount = managedAccount.getServiceAccount(); this.context = managedAccount.getContext(); 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/v2/security/KubernetesNamedAccountCredentialsInitializerSpec.groovy index 55e8775bc56..92826276e56 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/v2/security/KubernetesNamedAccountCredentialsInitializerSpec.groovy @@ -18,6 +18,7 @@ package com.netflix.spinnaker.clouddriver.kubernetes.v2.security 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 @@ -34,13 +35,15 @@ class KubernetesNamedAccountCredentialsInitializerSpec extends Specification { CatsModule catsModule = Mock(CatsModule) AccountCredentialsRepository accountCredentialsRepository = Mock(AccountCredentialsRepository) NamerRegistry namerRegistry = Mock(NamerRegistry) + ConfigFileService configFileService = new ConfigFileService() + KubernetesNamedAccountCredentials.CredentialFactory credentialFactory = new KubernetesNamedAccountCredentials.CredentialFactory( "userAgent", new NoopRegistry(), namerRegistry, accountCredentialsRepository, - Mock(KubectlJobExecutor) - ) + Mock(KubectlJobExecutor), + configFileService) KubernetesNamedAccountCredentialsInitializer kubernetesNamedAccountCredentialsInitializer = new KubernetesNamedAccountCredentialsInitializer( spectatorRegistry: new NoopRegistry() diff --git a/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/security/KubernetesV2CredentialsSpec.groovy b/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/security/KubernetesV2CredentialsSpec.groovy index 7f35da93f8c..06290aea660 100644 --- a/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/security/KubernetesV2CredentialsSpec.groovy +++ b/clouddriver-kubernetes/src/test/groovy/com/netflix/spinnaker/clouddriver/kubernetes/v2/security/KubernetesV2CredentialsSpec.groovy @@ -28,7 +28,7 @@ class KubernetesV2CredentialsSpec extends Specification { String NAMESPACE = "my-namespace" private buildCredentials(KubernetesConfigurationProperties.ManagedAccount managedAccount) { - return new KubernetesV2Credentials(registry, kubectlJobExecutor, managedAccount) + return new KubernetesV2Credentials(registry, kubectlJobExecutor, managedAccount, null) } void "Built-in Kubernetes kinds are considered valid by default"() {