From 3947e31aff089d6543577c7a4b8fdb44c5865dbe Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Sat, 24 Dec 2022 00:09:19 +0530 Subject: [PATCH] feat (kubernetes-client) : Add DSL support for creating ConfigMap from file (#4184) + Added ConfigMapResource for modifying default ConfigMap operations + Added FromFileCreatable DSL method to provide file + Move KubernetesResourceUtilTest to `io.fabric8.kubernetes.client.utils` Signed-off-by: Rohan Kumar --- CHANGELOG.md | 1 + .../kubernetes/client/KubernetesClient.java | 3 +- .../NamespacedKubernetesClientAdapter.java | 3 +- .../client/dsl/ConfigMapResource.java | 22 ++++ .../client/dsl/FromFileCreatable.java | 26 ++++ .../client/utils/KubernetesResourceUtil.java | 108 +++++++++++++++++ .../resourcelock/ConfigMapLockTest.java | 14 ++- .../KubernetesResourceUtilTest.java | 84 ++++++++++++- .../configmap-from-file/game.properties | 23 ++++ .../configmap-from-file/ui.properties | 20 +++ .../src/test/resources/test.bin | 1 + .../core/v1/ConfigMapOperationsImpl.java | 69 +++++++++++ .../client/impl/KubernetesClientImpl.java | 7 +- .../core/v1/ConfigMapOperationsImplTest.java | 114 ++++++++++++++++++ .../io/fabric8/kubernetes/ConfigMapIT.java | 64 +++++++++- .../configmap-sources/buffalo.properties | 20 +++ .../configmap-sources/cow.properties | 18 +++ .../kubernetes/client/mock/ConfigMapTest.java | 4 +- .../server/mock/NamespacedItemTest.java | 3 +- 19 files changed, 587 insertions(+), 17 deletions(-) create mode 100644 kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/ConfigMapResource.java create mode 100644 kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/FromFileCreatable.java rename kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/{internal => utils}/KubernetesResourceUtilTest.java (74%) create mode 100644 kubernetes-client-api/src/test/resources/configmap-from-file/game.properties create mode 100644 kubernetes-client-api/src/test/resources/configmap-from-file/ui.properties create mode 100644 kubernetes-client-api/src/test/resources/test.bin create mode 100644 kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/core/v1/ConfigMapOperationsImpl.java create mode 100644 kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/core/v1/ConfigMapOperationsImplTest.java create mode 100644 kubernetes-itests/src/test/resources/configmap-sources/buffalo.properties create mode 100644 kubernetes-itests/src/test/resources/configmap-sources/cow.properties diff --git a/CHANGELOG.md b/CHANGELOG.md index b2d7c49adbc..f414839dd41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ #### Dependency Upgrade #### New Features +* Fix #4184: Add DSL support for creating ConfigMap from file #### _**Note**_: Breaking changes * Fix #3972: deprecated Parameterizable and methods on Serialization accepting parameters - that was only needed as a workaround for non-string parameters. You should instead include those parameter values in the map passed to processLocally. diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/KubernetesClient.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/KubernetesClient.java index 6d2f84ab984..8cd70fd2bda 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/KubernetesClient.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/KubernetesClient.java @@ -64,6 +64,7 @@ import io.fabric8.kubernetes.client.dsl.AutoscalingAPIGroupDSL; import io.fabric8.kubernetes.client.dsl.BatchAPIGroupDSL; import io.fabric8.kubernetes.client.dsl.CertificatesAPIGroupDSL; +import io.fabric8.kubernetes.client.dsl.ConfigMapResource; import io.fabric8.kubernetes.client.dsl.DiscoveryAPIGroupDSL; import io.fabric8.kubernetes.client.dsl.EventingAPIGroupDSL; import io.fabric8.kubernetes.client.dsl.ExtensionsAPIGroupDSL; @@ -450,7 +451,7 @@ NamespaceListVisitFromServerGetDeleteRecreateWaitApplicable resourc * * @return MixedOperation object for ConfigMap related operations. */ - MixedOperation> configMaps(); + MixedOperation configMaps(); /** * API entrypoint for LimitRange related operations. LimitRange (core/v1) diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/NamespacedKubernetesClientAdapter.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/NamespacedKubernetesClientAdapter.java index 7bd8982778a..c2b89346f32 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/NamespacedKubernetesClientAdapter.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/NamespacedKubernetesClientAdapter.java @@ -64,6 +64,7 @@ import io.fabric8.kubernetes.client.dsl.AutoscalingAPIGroupDSL; import io.fabric8.kubernetes.client.dsl.BatchAPIGroupDSL; import io.fabric8.kubernetes.client.dsl.CertificatesAPIGroupDSL; +import io.fabric8.kubernetes.client.dsl.ConfigMapResource; import io.fabric8.kubernetes.client.dsl.DiscoveryAPIGroupDSL; import io.fabric8.kubernetes.client.dsl.EventingAPIGroupDSL; import io.fabric8.kubernetes.client.dsl.ExtensionsAPIGroupDSL; @@ -354,7 +355,7 @@ public NonNamespaceOperation> a } @Override - public MixedOperation> configMaps() { + public MixedOperation configMaps() { return getClient().configMaps(); } diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/ConfigMapResource.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/ConfigMapResource.java new file mode 100644 index 00000000000..f67266ef277 --- /dev/null +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/ConfigMapResource.java @@ -0,0 +1,22 @@ +/** + * Copyright (C) 2015 Red Hat, 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 io.fabric8.kubernetes.client.dsl; + +import io.fabric8.kubernetes.api.model.ConfigMap; + +public interface ConfigMapResource extends Resource, + FromFileCreatable> { +} diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/FromFileCreatable.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/FromFileCreatable.java new file mode 100644 index 00000000000..996c9e3c84f --- /dev/null +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/FromFileCreatable.java @@ -0,0 +1,26 @@ +/** + * Copyright (C) 2015 Red Hat, 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 io.fabric8.kubernetes.client.dsl; + +public interface FromFileCreatable { + /** + * Create new ConfigMap from a directory or file contents. + * + * @param dirOrFilePath a file or directory path + * @return {@link Resource for operations to do with this resource + */ + T fromFile(String dirOrFilePath); +} diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/KubernetesResourceUtil.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/KubernetesResourceUtil.java index bf74d5a48b4..61186d0a09e 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/KubernetesResourceUtil.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/utils/KubernetesResourceUtil.java @@ -18,6 +18,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import io.fabric8.kubernetes.api.builder.VisitableBuilder; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.fabric8.kubernetes.api.model.DefaultKubernetesResourceList; import io.fabric8.kubernetes.api.model.EnvVar; import io.fabric8.kubernetes.api.model.EnvVarBuilder; @@ -31,9 +33,18 @@ import io.fabric8.kubernetes.api.model.SecretBuilder; import io.fabric8.kubernetes.client.readiness.Readiness; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.Duration; import java.time.Instant; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.Base64; import java.util.Collections; @@ -42,7 +53,9 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.regex.Pattern; +import java.util.stream.Stream; public class KubernetesResourceUtil { private KubernetesResourceUtil() { @@ -457,6 +470,101 @@ public static Secret createDockerRegistrySecret(String dockerServer, String user return createDockerSecret(secretName, dockerConfigAsStr); } + /** + * Create New ConfigMap from a file or a directory + * + * @param name name of the ConfigMap + * @param key (optional) if it's a file key for ConfigMap entry + * @param dirOrFilePath file or directory path + * @return a ConfigMap object + * @throws IOException in case of error while reading file + */ + public static ConfigMap createNewConfigMapFromDirOrFile(final String name, final String key, + final String dirOrFilePath) throws IOException { + final Path path = Paths.get(dirOrFilePath); + + if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) { + ConfigMapBuilder configMapBuilder = new ConfigMapBuilder() + .withNewMetadata().withName(name).endMetadata(); + addConfigMapEntriesFromDirectoryToExistingConfigMap(configMapBuilder, path); + return configMapBuilder.build(); + } else { + return createNewConfigMapFromFile(name, key, path); + } + } + + /** + * Create new ConfigMap from file contents + * + * @param name name of ConfigMap + * @param key key + * @param file file whose content would be used in ConfigMap entry + * @return a ConfigMap with data containing file contents + * @throws IOException in case of error while reading file + */ + public static ConfigMap createNewConfigMapFromFile(final String name, final String key, final Path file) + throws IOException { + ConfigMapBuilder configMapBuilder = new ConfigMapBuilder(); + configMapBuilder.withNewMetadata().withName(name).endMetadata(); + String entryKey = Optional.ofNullable(key).orElse(file.toFile().getName()); + Map.Entry configMapEntry = createConfigMapEntry(entryKey, file); + addConfigMapEntry(configMapBuilder, configMapEntry, file); + return configMapBuilder.build(); + } + + /** + * Create a ConfigMap entry based on file contents + * + * @param key key for entry + * @param file file path whose contents would be used in value of entry + * @return an entry containing key and value + * @throws IOException in case of error while reading file + */ + public static Map.Entry createConfigMapEntry(final String key, final Path file) throws IOException { + final byte[] bytes = Files.readAllBytes(file); + if (isFileWithBinaryContent(file)) { + final String value = Base64.getEncoder().encodeToString(bytes); + return new AbstractMap.SimpleEntry<>(key, value); + } else { + return new AbstractMap.SimpleEntry<>(key, new String(bytes)); + } + } + + public static boolean isFileWithBinaryContent(final Path file) throws IOException { + final byte[] bytes = Files.readAllBytes(file); + try { + StandardCharsets.UTF_8.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT) + .decode(ByteBuffer.wrap(bytes)); + return false; + } catch (CharacterCodingException e) { + return true; + } + } + + public static void addConfigMapEntriesFromDirectoryToExistingConfigMap(ConfigMapBuilder configMapBuilder, final Path path) + throws IOException { + try (Stream files = Files.list(path)) { + files.filter(p -> !Files.isDirectory(p, LinkOption.NOFOLLOW_LINKS)).forEach(file -> { + try { + addConfigMapEntry(configMapBuilder, createConfigMapEntry(file.getFileName().toString(), file), file); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + }); + } + } + + public static void addConfigMapEntry(ConfigMapBuilder configMapBuilder, Map.Entry entry, final Path file) + throws IOException { + if (isFileWithBinaryContent(file)) { + configMapBuilder.addToBinaryData(entry.getKey(), entry.getValue()); + } else { + configMapBuilder.addToData(entry.getKey(), entry.getValue()); + } + } + private static Map createDockerRegistryConfigMap(String dockerServer, String username, String password) { Map dockerConfigMap = new HashMap<>(); Map auths = new HashMap<>(); diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ConfigMapLockTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ConfigMapLockTest.java index a6f2bb8bc47..2867462e34f 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ConfigMapLockTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/extended/leaderelection/resourcelock/ConfigMapLockTest.java @@ -20,15 +20,14 @@ import io.fabric8.kubernetes.api.model.ConfigMapList; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.ConfigMapResource; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.ReplaceDeletable; -import io.fabric8.kubernetes.client.dsl.Resource; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; import org.mockito.Answers; -import org.mockito.ArgumentMatchers; import org.mockito.Mockito; import java.time.Duration; @@ -52,16 +51,19 @@ class ConfigMapLockTest { private KubernetesClient kc; - private MixedOperation> configMaps; + private MixedOperation configMaps; + private ConfigMapResource configMapResource; private ConfigMapBuilder configMapBuilder; private ConfigMapBuilder.MetadataNested metadata; @BeforeEach void setUp() { kc = mock(KubernetesClient.class, RETURNS_DEEP_STUBS); - configMaps = mock(MixedOperation.class, RETURNS_DEEP_STUBS); + configMaps = mock(MixedOperation.class); + configMapResource = mock(ConfigMapResource.class); configMapBuilder = Mockito.mock(ConfigMapBuilder.class, RETURNS_DEEP_STUBS); metadata = mock(ConfigMapBuilder.MetadataNested.class, RETURNS_DEEP_STUBS); + when(configMaps.withName("name")).thenReturn(configMapResource); when(kc.configMaps().inNamespace(anyString())).thenReturn(configMaps); when(configMapBuilder.editOrNewMetadata()).thenReturn(metadata); } @@ -102,7 +104,7 @@ void missingIdentityShouldThrowException() { void getWithExistingConfigMapShouldReturnLeaderElectionRecord() { // Given final ConfigMap cm = new ConfigMap(); - when(configMaps.withName(ArgumentMatchers.eq("name")).get()).thenReturn(cm); + when(configMapResource.get()).thenReturn(cm); cm.setMetadata(new ObjectMetaBuilder() .withAnnotations( Collections.singletonMap("control-plane.alpha.kubernetes.io/leader", @@ -125,6 +127,7 @@ void createWithValidLeaderElectionRecordShouldSendPostRequest() throws Exception final LeaderElectionRecord record = new LeaderElectionRecord( "1", Duration.ofSeconds(1), ZonedDateTime.now(), ZonedDateTime.now(), 0); final ConfigMapLock lock = new ConfigMapLock("namespace", "name", "1337"); + when(configMapResource.get()).thenReturn(new ConfigMap()); // When lock.create(kc, record); // Then @@ -134,7 +137,6 @@ void createWithValidLeaderElectionRecordShouldSendPostRequest() throws Exception @Test void updateWithValidLeaderElectionRecordShouldSendPutRequest() throws Exception { // Given - final Resource configMapResource = configMaps.withName("name"); final ReplaceDeletable replaceable = mock(ReplaceDeletable.class, Answers.RETURNS_DEEP_STUBS); when(configMapResource.lockResourceVersion(any())).thenReturn(replaceable); final ConfigMap configMapInTheCluster = new ConfigMap(); diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/internal/KubernetesResourceUtilTest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/KubernetesResourceUtilTest.java similarity index 74% rename from kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/internal/KubernetesResourceUtilTest.java rename to kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/KubernetesResourceUtilTest.java index 91f5f6d7f37..277f66bc2d4 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/internal/KubernetesResourceUtilTest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/utils/KubernetesResourceUtilTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.fabric8.kubernetes.client.internal; +package io.fabric8.kubernetes.client.utils; import com.fasterxml.jackson.core.JsonProcessingException; import io.fabric8.kubernetes.api.model.ConfigMap; @@ -32,11 +32,17 @@ import io.fabric8.kubernetes.api.model.batch.v1beta1.CronJob; import io.fabric8.kubernetes.api.model.batch.v1beta1.CronJobList; import io.fabric8.kubernetes.client.Good; -import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; +import org.assertj.core.api.InstanceOfAssertFactories; +import org.assertj.core.data.MapEntry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.Duration; import java.util.ArrayList; import java.util.Base64; @@ -44,6 +50,8 @@ import java.util.List; import java.util.Map; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -253,4 +261,76 @@ void testCreateDockerRegistrySecret() throws JsonProcessingException { header); assertEquals("TestSecretName", secret.getMetadata().getName()); } + + @Test + void createNewConfigMapFromDirOrFile_whenFileAndCustomKeyProvided_shouldCreateConfigMapFromFileWithCustomKey() + throws IOException { + // Given + URL fileUrl = getClass().getResource("/configmap-from-file/game.properties"); + assertThat(fileUrl).isNotNull(); + + // When + ConfigMap configMap = KubernetesResourceUtil.createNewConfigMapFromDirOrFile("test-configmap", "custom-key", + fileUrl.getFile()); + + // Then + assertConfigMapContainsData(configMap, createExpectedEntry("custom-key", Paths.get(fileUrl.getFile()))); + } + + @Test + void createNewConfigMapFromDirOrFile_whenFileProvided_shouldCreateConfigMapFromFile() throws IOException { + // Given + URL fileUrl = getClass().getResource("/configmap-from-file/game.properties"); + assertThat(fileUrl).isNotNull(); + + // When + ConfigMap configMap = KubernetesResourceUtil.createNewConfigMapFromDirOrFile("test-configmap", null, fileUrl.getFile()); + + // Then + assertConfigMapContainsData(configMap, createExpectedEntry("game.properties", Paths.get(fileUrl.getFile()))); + } + + @Test + void createNewConfigMapFromDirOrFile_whenDirProvided_shouldCreateConfigMapFromDir() throws IOException { + // Given + URL fileUrl = getClass().getResource("/configmap-from-file"); + assertThat(fileUrl).isNotNull(); + + // When + ConfigMap configMap = KubernetesResourceUtil.createNewConfigMapFromDirOrFile("test-configmap", null, fileUrl.getFile()); + + // Then + assertConfigMapContainsData(configMap, + createExpectedEntry("game.properties", Paths.get(fileUrl.getFile()).resolve("game.properties")), + createExpectedEntry("ui.properties", Paths.get(fileUrl.getFile()).resolve("ui.properties"))); + } + + @Test + void createNewConfigMapFromDirOrFile_whenBinaryFileProvided_shouldCreateConfigMapFromFile() throws IOException { + // Given + URL fileUrl = getClass().getResource("/test.bin"); + assertThat(fileUrl).isNotNull(); + + // When + ConfigMap configMap = KubernetesResourceUtil.createNewConfigMapFromDirOrFile("test-configmap", null, fileUrl.getFile()); + + // Then + assertThat(configMap) + .extracting(ConfigMap::getBinaryData) + .asInstanceOf(InstanceOfAssertFactories.MAP) + .contains(entry("test.bin", "wA==")); + } + + @SafeVarargs + private final void assertConfigMapContainsData(ConfigMap configMap, MapEntry... mapEntries) { + assertThat(configMap) + .extracting(ConfigMap::getData) + .asInstanceOf(InstanceOfAssertFactories.MAP) + .contains(mapEntries); + } + + private MapEntry createExpectedEntry(String key, Path filePath) throws IOException { + String fileContent = new String(Files.readAllBytes(filePath)); + return entry(key, fileContent); + } } diff --git a/kubernetes-client-api/src/test/resources/configmap-from-file/game.properties b/kubernetes-client-api/src/test/resources/configmap-from-file/game.properties new file mode 100644 index 00000000000..8b5144c28ce --- /dev/null +++ b/kubernetes-client-api/src/test/resources/configmap-from-file/game.properties @@ -0,0 +1,23 @@ +# +# Copyright (C) 2015 Red Hat, 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. +# + +enemies=aliens +lives=3 +enemies.cheat=true +enemies.cheat.level=noGoodRotten +secret.code.passphrase=UUDDLRLRBABAS +secret.code.allowed=true +secret.code.lives=30 diff --git a/kubernetes-client-api/src/test/resources/configmap-from-file/ui.properties b/kubernetes-client-api/src/test/resources/configmap-from-file/ui.properties new file mode 100644 index 00000000000..a0113ca404f --- /dev/null +++ b/kubernetes-client-api/src/test/resources/configmap-from-file/ui.properties @@ -0,0 +1,20 @@ +# +# Copyright (C) 2015 Red Hat, 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. +# + +color.good=purple +color.bad=yellow +allow.textmode=true +how.nice.to.look=fairlyNice diff --git a/kubernetes-client-api/src/test/resources/test.bin b/kubernetes-client-api/src/test/resources/test.bin new file mode 100644 index 00000000000..e7754cae5ad --- /dev/null +++ b/kubernetes-client-api/src/test/resources/test.bin @@ -0,0 +1 @@ +� \ No newline at end of file diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/core/v1/ConfigMapOperationsImpl.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/core/v1/ConfigMapOperationsImpl.java new file mode 100644 index 00000000000..73d5d8f72de --- /dev/null +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/core/v1/ConfigMapOperationsImpl.java @@ -0,0 +1,69 @@ +/** + * Copyright (C) 2015 Red Hat, 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 io.fabric8.kubernetes.client.dsl.internal.core.v1; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapList; +import io.fabric8.kubernetes.client.Client; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.ConfigMapResource; +import io.fabric8.kubernetes.client.dsl.internal.HasMetadataOperation; +import io.fabric8.kubernetes.client.dsl.internal.HasMetadataOperationsImpl; +import io.fabric8.kubernetes.client.dsl.internal.OperationContext; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; + +import static io.fabric8.kubernetes.client.utils.KubernetesResourceUtil.createNewConfigMapFromDirOrFile; + +public class ConfigMapOperationsImpl extends HasMetadataOperation + implements ConfigMapResource { + + public ConfigMapOperationsImpl(Client client) { + this(HasMetadataOperationsImpl.defaultContext(client)); + } + + public ConfigMapOperationsImpl(OperationContext context) { + super(context.withPlural("configmaps"), ConfigMap.class, ConfigMapList.class); + } + + @Override + public ConfigMapOperationsImpl newInstance(OperationContext context) { + return new ConfigMapOperationsImpl(context); + } + + @Override + public ConfigMapResource fromFile(String dirOrFilePath) { + if (dirOrFilePath == null || dirOrFilePath.isEmpty()) { + throw new IllegalArgumentException("invalid file path provided"); + } + File file = Paths.get(dirOrFilePath).toFile(); + if (!file.exists()) { + throw new IllegalArgumentException(String.format("File %s doesn't exist", dirOrFilePath)); + } + + return resource(createNewConfigMap(file.getName(), dirOrFilePath)); + } + + private ConfigMap createNewConfigMap(String fileName, String dirOrFilePath) { + try { + return createNewConfigMapFromDirOrFile(name, fileName, dirOrFilePath); + } catch (IOException ioException) { + throw new KubernetesClientException("Unable to create ConfigMap " + name, ioException); + } + } +} diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/impl/KubernetesClientImpl.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/impl/KubernetesClientImpl.java index e5a7f72eec8..b306bfcb6b2 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/impl/KubernetesClientImpl.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/impl/KubernetesClientImpl.java @@ -97,6 +97,7 @@ import io.fabric8.kubernetes.client.dsl.AutoscalingAPIGroupDSL; import io.fabric8.kubernetes.client.dsl.BatchAPIGroupDSL; import io.fabric8.kubernetes.client.dsl.CertificatesAPIGroupDSL; +import io.fabric8.kubernetes.client.dsl.ConfigMapResource; import io.fabric8.kubernetes.client.dsl.DiscoveryAPIGroupDSL; import io.fabric8.kubernetes.client.dsl.EventingAPIGroupDSL; import io.fabric8.kubernetes.client.dsl.ExtensionsAPIGroupDSL; @@ -141,6 +142,7 @@ import io.fabric8.kubernetes.client.dsl.internal.apps.v1.StatefulSetOperationsImpl; import io.fabric8.kubernetes.client.dsl.internal.batch.v1.JobOperationsImpl; import io.fabric8.kubernetes.client.dsl.internal.certificates.v1.CertificateSigningRequestOperationsImpl; +import io.fabric8.kubernetes.client.dsl.internal.core.v1.ConfigMapOperationsImpl; import io.fabric8.kubernetes.client.dsl.internal.core.v1.PodOperationsImpl; import io.fabric8.kubernetes.client.dsl.internal.core.v1.ReplicationControllerOperationsImpl; import io.fabric8.kubernetes.client.dsl.internal.core.v1.ServiceOperationsImpl; @@ -239,6 +241,7 @@ public KubernetesClientImpl(HttpClient httpClient, Config config, ExecutorSuppli this.getHandlers().register(Pod.class, PodOperationsImpl::new); this.getHandlers().register(Job.class, JobOperationsImpl::new); this.getHandlers().register(Service.class, ServiceOperationsImpl::new); + this.getHandlers().register(ConfigMap.class, ConfigMapOperationsImpl::new); this.getHandlers().register(Deployment.class, DeploymentOperationsImpl::new); this.getHandlers().register(io.fabric8.kubernetes.api.model.extensions.Deployment.class, io.fabric8.kubernetes.client.dsl.internal.extensions.v1beta1.DeploymentOperationsImpl::new); @@ -484,8 +487,8 @@ public NonNamespaceOperation> a * {@inheritDoc} */ @Override - public MixedOperation> configMaps() { - return resources(ConfigMap.class, ConfigMapList.class); + public MixedOperation configMaps() { + return new ConfigMapOperationsImpl(this); } /** diff --git a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/core/v1/ConfigMapOperationsImplTest.java b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/core/v1/ConfigMapOperationsImplTest.java new file mode 100644 index 00000000000..b674e498c01 --- /dev/null +++ b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/core/v1/ConfigMapOperationsImplTest.java @@ -0,0 +1,114 @@ +/** + * Copyright (C) 2015 Red Hat, 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 io.fabric8.kubernetes.client.dsl.internal.core.v1; + +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.ConfigMapResource; +import io.fabric8.kubernetes.client.dsl.internal.OperationContext; +import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; + +import java.io.IOException; +import java.net.URL; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockStatic; + +class ConfigMapOperationsImplTest { + private ConfigMapOperationsImpl configMapOperations; + + @BeforeEach + void setUp() { + OperationContext operationContext = new OperationContext(); + configMapOperations = new ConfigMapOperationsImpl(operationContext); + } + + @Test + void fromFile_withBlankFilePath_shouldThrowException() { + // Given + ConfigMapResource configMapOp = configMapOperations + .inNamespace("default") + .withName("game-config"); + + // When + assertThatIllegalArgumentException() + .isThrownBy(() -> configMapOp.fromFile("")) + .withMessage("invalid file path provided"); + } + + @Test + void fromFile_withInvalidFilePath_shouldThrowException() { + // Given + ConfigMapResource configMapOp = configMapOperations + .inNamespace("default") + .withName("game-config"); + + // When + assertThatIllegalArgumentException() + .isThrownBy(() -> configMapOp.fromFile("invalid-file")) + .withMessage("File invalid-file doesn't exist"); + } + + @Test + void fromFile_withValidFilePath_shouldDelegateCallToHelpermethod() { + try (MockedStatic kubernetesResourceUtilMockedStatic = mockStatic(KubernetesResourceUtil.class)) { + // Given + ConfigMapResource configMapOp = configMapOperations + .inNamespace("default") + .withName("game-config"); + ArgumentCaptor filePathCaptor = ArgumentCaptor.forClass(String.class); + URL fileUrl = getClass().getResource("/test-config.yml"); + assertThat(fileUrl).isNotNull(); + + // When + configMapOp.fromFile(fileUrl.getFile()); + + // Then + kubernetesResourceUtilMockedStatic + .verify(() -> KubernetesResourceUtil.createNewConfigMapFromDirOrFile(eq("game-config"), eq("test-config.yml"), + filePathCaptor.capture())); + assertThat(filePathCaptor.getValue()).isEqualTo(fileUrl.getFile()); + } + } + + @Test + void fromFile_whenConfigMapCreationFailed_shouldThrowException() { + try (MockedStatic kubernetesResourceUtilMockedStatic = mockStatic(KubernetesResourceUtil.class)) { + // Given + kubernetesResourceUtilMockedStatic + .when(() -> KubernetesResourceUtil.createNewConfigMapFromDirOrFile(any(), any(), any())) + .thenThrow(new IOException("I/O error")); + ConfigMapResource configMapOp = configMapOperations + .inNamespace("default") + .withName("game-config"); + URL fileUrl = getClass().getResource("/test-config.yml"); + assertThat(fileUrl).isNotNull(); + String filePath = fileUrl.getFile(); + + // When + Then + assertThatExceptionOfType(KubernetesClientException.class) + .isThrownBy(() -> configMapOp.fromFile(filePath)) + .withMessage("Unable to create ConfigMap game-config"); + } + } +} diff --git a/kubernetes-itests/src/test/java/io/fabric8/kubernetes/ConfigMapIT.java b/kubernetes-itests/src/test/java/io/fabric8/kubernetes/ConfigMapIT.java index 2e83c15f69d..da84f172c1a 100644 --- a/kubernetes-itests/src/test/java/io/fabric8/kubernetes/ConfigMapIT.java +++ b/kubernetes-itests/src/test/java/io/fabric8/kubernetes/ConfigMapIT.java @@ -20,10 +20,15 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.fabric8.kubernetes.api.model.ConfigMapList; +import io.fabric8.kubernetes.api.model.StatusDetails; import io.fabric8.kubernetes.client.KubernetesClient; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import java.net.URL; +import java.util.List; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -62,8 +67,63 @@ void update() { assertEquals("Microsoft Database", configMap.getData().get("MSSQL")); } + @Test + void fromFile() { + // Given + URL fileUrl = getClass().getResource("/configmap-sources/buffalo.properties"); + assertThat(fileUrl).isNotNull(); + + // When + ConfigMap configMap = client.configMaps().withName("buffalo-from-file") + .fromFile(fileUrl.getFile()) + .create(); + + // Then + assertThat(configMap) + .hasFieldOrPropertyWithValue("metadata.name", "buffalo-from-file") + .extracting(ConfigMap::getData) + .asInstanceOf(InstanceOfAssertFactories.MAP) + .containsEntry("buffalo.properties", "murrah-buffalo=India\n" + + "italian-mediterranean-buffalo=Italy\n" + + "romanian-buffalo=Romania\n" + + "buffalypso=Trinidad\n"); + client.configMaps().withName("buffalo-from-file").delete(); + } + + @Test + void fromDir() { + // Given + URL fileUrl = getClass().getResource("/configmap-sources"); + assertThat(fileUrl).isNotNull(); + + // When + ConfigMap configMap = client.configMaps().withName("cattle") + .fromFile(fileUrl.getFile()) + .create(); + + // Then + assertThat(configMap) + .hasFieldOrPropertyWithValue("metadata.name", "cattle") + .extracting(ConfigMap::getData) + .asInstanceOf(InstanceOfAssertFactories.MAP) + .containsEntry("buffalo.properties", "murrah-buffalo=India\n" + + "italian-mediterranean-buffalo=Italy\n" + + "romanian-buffalo=Romania\n" + + "buffalypso=Trinidad\n") + .containsEntry("cow.properties", "red-angus=Australia\n" + + "jersey=Britain\n"); + client.configMaps().withName("cattle").delete(); + } + @Test void delete() { - assertTrue(client.configMaps().withName("configmap-delete").delete().size() == 1); + // Given + String configMapName = "configmap-delete"; + + // When + List deleteStatus = client.configMaps().withName(configMapName).delete(); + + // Then + assertThat(deleteStatus).hasSize(1); } } diff --git a/kubernetes-itests/src/test/resources/configmap-sources/buffalo.properties b/kubernetes-itests/src/test/resources/configmap-sources/buffalo.properties new file mode 100644 index 00000000000..1811ce4b3ff --- /dev/null +++ b/kubernetes-itests/src/test/resources/configmap-sources/buffalo.properties @@ -0,0 +1,20 @@ +# +# Copyright (C) 2015 Red Hat, 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. +# + +murrah-buffalo=India +italian-mediterranean-buffalo=Italy +romanian-buffalo=Romania +buffalypso=Trinidad diff --git a/kubernetes-itests/src/test/resources/configmap-sources/cow.properties b/kubernetes-itests/src/test/resources/configmap-sources/cow.properties new file mode 100644 index 00000000000..1c75ea572f7 --- /dev/null +++ b/kubernetes-itests/src/test/resources/configmap-sources/cow.properties @@ -0,0 +1,18 @@ +# +# Copyright (C) 2015 Red Hat, 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. +# + +red-angus=Australia +jersey=Britain diff --git a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/ConfigMapTest.java b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/ConfigMapTest.java index a9b2942fe23..c0979e33773 100644 --- a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/ConfigMapTest.java +++ b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/ConfigMapTest.java @@ -20,8 +20,8 @@ import io.fabric8.kubernetes.api.model.ConfigMapList; import io.fabric8.kubernetes.api.model.ConfigMapListBuilder; import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.ConfigMapResource; import io.fabric8.kubernetes.client.dsl.MixedOperation; -import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; import org.junit.jupiter.api.Test; @@ -74,7 +74,7 @@ void testDeletes() throws InterruptedException { server.expect().withPath("/api/v1/namespaces/test/configmaps").andReturn(200, new ConfigMapListBuilder().withItems(cm).build()).always(); - MixedOperation> configMaps = client.configMaps(); + MixedOperation configMaps = client.configMaps(); assertTrue(configMaps.delete().size() == 1); server.expect().delete().withPath("/api/v1/namespaces/test/configmaps/cfg1").andReturn(200, diff --git a/kubernetes-tests/src/test/java/io/fabric8/openshift/client/server/mock/NamespacedItemTest.java b/kubernetes-tests/src/test/java/io/fabric8/openshift/client/server/mock/NamespacedItemTest.java index 8c0b1b26253..59db31597f8 100644 --- a/kubernetes-tests/src/test/java/io/fabric8/openshift/client/server/mock/NamespacedItemTest.java +++ b/kubernetes-tests/src/test/java/io/fabric8/openshift/client/server/mock/NamespacedItemTest.java @@ -22,6 +22,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.NamespacedKubernetesClient; +import io.fabric8.kubernetes.client.dsl.ConfigMapResource; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.NamespaceListVisitFromServerGetDeleteRecreateWaitApplicable; import io.fabric8.kubernetes.client.dsl.Resource; @@ -118,7 +119,7 @@ void testClientNullNamespace() { @Test void testOperationNullNamespace() { - MixedOperation> configMaps = this.client.configMaps(); + MixedOperation configMaps = this.client.configMaps(); assertThrows(KubernetesClientException.class, () -> configMaps.inNamespace(null)); }