diff --git a/cli/src/admiral/cmd/templates.go b/cli/src/admiral/cmd/templates.go index 1aeeab6cb..94ed15dc6 100644 --- a/cli/src/admiral/cmd/templates.go +++ b/cli/src/admiral/cmd/templates.go @@ -121,20 +121,28 @@ var templateImportCmd = &cobra.Command{ }, } +var importKubernetes bool + func initTemplateImport() { + templateImportCmd.Flags().BoolVar(&importKubernetes, "k8s", false, "Import k8s object.") + templateImportCmd.Flags().MarkHidden("k8s") TemplatesRootCmd.AddCommand(templateImportCmd) } func RunTemplateImport(args []string) (string, error) { var ( - filePath string - ok bool + filePath, id string + ok bool + err error ) if filePath, ok = ValidateArgsCount(args); !ok { return "", MissingPathToFileError } - id, err := templates.Import(filePath) - + if importKubernetes { + id, err = templates.ImportKubernetes(filePath) + } else { + id, err = templates.Import(filePath) + } if err != nil { return "", err } else { diff --git a/cli/src/admiral/templates/templates.go b/cli/src/admiral/templates/templates.go index 7c71d0e3b..801b58a3d 100644 --- a/cli/src/admiral/templates/templates.go +++ b/cli/src/admiral/templates/templates.go @@ -352,6 +352,24 @@ func Export(id, dirF, format string) (string, error) { return fullId, nil } +func ImportKubernetes(dirF string) (string, error) { + importFile, err := ioutil.ReadFile(dirF) + + if err != nil { + return "", err + } + + url := config.URL + "/resources/kubernetes-templates" + + req, _ := http.NewRequest("POST", url, bytes.NewBuffer(importFile)) + _, respBody, respErr := client.ProcessRequest(req) + if respErr != nil { + return "", respErr + } + + return string(respBody), nil +} + //Function to verify if file can be created. //Returns the file and result of verification func verifyFile(dirF string) (*os.File, error) { diff --git a/common-test/src/test/java/com/vmware/admiral/common/test/BaseTestCase.java b/common-test/src/test/java/com/vmware/admiral/common/test/BaseTestCase.java index 75c2dc749..58aab8b85 100644 --- a/common-test/src/test/java/com/vmware/admiral/common/test/BaseTestCase.java +++ b/common-test/src/test/java/com/vmware/admiral/common/test/BaseTestCase.java @@ -231,6 +231,7 @@ protected boolean getPeerSynchronizationEnabled() { /** * Returns maintenance interval millis to be set to the host + * * @return milliseconds */ protected long getMaintenanceIntervalMillis() { @@ -362,7 +363,7 @@ protected void verifyService(URI factoryUri, FactoryService factoryInstance, assertTrue(childTemplate.documentDescription != null); assertTrue(childTemplate.documentDescription.propertyDescriptions != null && childTemplate.documentDescription.propertyDescriptions - .size() > 0); + .size() > 0); if (!TaskServiceDocument.class.isAssignableFrom(childTemplate.getClass())) { Field[] allFields = childTemplate.getClass().getDeclaredFields(); @@ -542,7 +543,7 @@ protected T waitForPropertyValue(String documentSelfLink, Class type, Str /** * Waits until the given task succeeds and returns its final state. - * + *

* Note: will stop polling if the task transitions to any final state. */ protected , E extends Enum> T waitForTaskSuccess( @@ -555,7 +556,7 @@ protected , E extends Enum> T waitForTaskSuc /** * Waits until the given task fails and returns its final state. - * + *

* Note: will stop polling if the task transitions to any final state. */ protected , E extends Enum> T waitForTaskError( @@ -727,7 +728,7 @@ protected T searchForDocument(Class type, String * Tries to retrieve the given document and creates it (through a POST request), if not found. * Assuming the {@code documentSelfLink} in the given document does not contain the full path * but only the relative path from the given {@code factoryLink}. - * + *

* Returns the document state as retrieved from the server. */ protected T getOrCreateDocument(T inState, String factoryLink) @@ -887,17 +888,40 @@ public List findResourceLinks(Class type, List result = new LinkedList<>(); new ServiceDocumentQuery<>( host, type).query(query, - (r) -> { - if (r.hasException()) { - ctx.failIteration(r.getException()); - return; - } - if (r.hasResult()) { - result.add(r.getDocumentSelfLink()); - return; - } + (r) -> { + if (r.hasException()) { + ctx.failIteration(r.getException()); + return; + } + if (r.hasResult()) { + result.add(r.getDocumentSelfLink()); + return; + } + ctx.completeIteration(); + }); + ctx.await(); + + return result; + } + + public List getDocumentLinksOfType(Class type) + throws Throwable { + TestContext ctx = testCreate(1); + QueryTask query = QueryUtil.buildQuery(type, true); + + List result = new LinkedList<>(); + new ServiceDocumentQuery<>( + host, type).query(query, + (r) -> { + if (r.hasException()) { + ctx.failIteration(r.getException()); + return; + } else if (r.hasResult()) { + result.add(r.getDocumentSelfLink()); + } else { ctx.completeIteration(); - }); + } + }); ctx.await(); return result; @@ -939,7 +963,8 @@ protected static void setPrivateField(Field field, Object instance, Object newVa field.set(instance, newValue); } - protected void validateLocalizableException(LocalizableExceptionHandler handler, String expectation) + protected void validateLocalizableException(LocalizableExceptionHandler handler, + String expectation) throws Throwable { try { handler.call(); @@ -952,7 +977,7 @@ protected void validateLocalizableException(LocalizableExceptionHandler handler, protected void waitForInitialBootServiceToBeSelfStopped(String bootServiceSelfLink) throws Throwable { waitFor("Failed waiting for " + bootServiceSelfLink - + " to self stop itself after all instances created.", + + " to self stop itself after all instances created.", () -> { TestContext ctx = testCreate(1); URI uri = UriUtils.buildUri(host, bootServiceSelfLink); diff --git a/common/src/main/java/com/vmware/admiral/common/ManagementUriParts.java b/common/src/main/java/com/vmware/admiral/common/ManagementUriParts.java index 402441a6a..bde3667bd 100644 --- a/common/src/main/java/com/vmware/admiral/common/ManagementUriParts.java +++ b/common/src/main/java/com/vmware/admiral/common/ManagementUriParts.java @@ -90,6 +90,9 @@ public interface ManagementUriParts { String COMPUTE_NETWORKS = RESOURCES + "/compute-networks"; String COMPUTE_NETWORK_DESC = RESOURCES + "/compute-network" + DESCRIPTION_SUFFIX; + String KUBERNETES_DESC = RESOURCES + "/kubernetes" + DESCRIPTION_SUFFIX; + String KUBERNETES_DESC_CONTENT = RESOURCES + "/kubernetes-templates"; + // Request tasks: String REQUEST = "/request"; String REQUESTS = "/requests"; diff --git a/common/src/main/java/com/vmware/admiral/common/util/OperationUtil.java b/common/src/main/java/com/vmware/admiral/common/util/OperationUtil.java index 602c7e5e9..0cd2db92e 100644 --- a/common/src/main/java/com/vmware/admiral/common/util/OperationUtil.java +++ b/common/src/main/java/com/vmware/admiral/common/util/OperationUtil.java @@ -11,6 +11,8 @@ package com.vmware.admiral.common.util; +import static com.vmware.admiral.common.util.UriUtilsExtended.MEDIA_TYPE_APPLICATION_YAML; + import java.net.URI; import java.util.function.Consumer; import java.util.logging.Level; @@ -83,4 +85,16 @@ public static void getDocumentState(Service service, ); } + public static boolean isApplicationYamlContent(String contentType) { + return (contentType != null) + && MEDIA_TYPE_APPLICATION_YAML.equals(contentType.split(";")[0]); + } + + public static boolean isApplicationYamlAccpetHeader(Operation op) { + String acceptHeader = op.getRequestHeader("Accept"); + if (acceptHeader == null || acceptHeader.trim().equals("")) { + return false; + } + return MEDIA_TYPE_APPLICATION_YAML.equals(acceptHeader); + } } diff --git a/common/src/main/java/com/vmware/admiral/common/util/YamlMapper.java b/common/src/main/java/com/vmware/admiral/common/util/YamlMapper.java index 7e393f620..045f54797 100644 --- a/common/src/main/java/com/vmware/admiral/common/util/YamlMapper.java +++ b/common/src/main/java/com/vmware/admiral/common/util/YamlMapper.java @@ -11,12 +11,14 @@ package com.vmware.admiral.common.util; +import java.io.IOException; import java.lang.reflect.Field; import java.util.Arrays; import java.util.Set; import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.ser.FilterProvider; @@ -26,6 +28,7 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.vmware.xenon.common.ServiceDocument; +import com.vmware.xenon.common.Utils; /** * YAML mapping functions @@ -63,4 +66,14 @@ public static Set getBuiltinFieldNames() { .filter(ServiceDocument::isBuiltInDocumentField) .collect(Collectors.toSet()); } + + public static String fromYamlToJson(String yaml) throws IOException { + Object obj = objectMapper().readValue(yaml, Object.class); + return Utils.toJson(obj); + } + + public static String fromJsonToYaml(String json) throws IOException { + JsonNode jsonNode = objectMapper().readTree(json); + return objectMapper().writeValueAsString(jsonNode); + } } diff --git a/common/src/test/java/com/vmware/admiral/service/common/util/YamlMapperTest.java b/common/src/test/java/com/vmware/admiral/service/common/util/YamlMapperTest.java new file mode 100644 index 000000000..da76ba215 --- /dev/null +++ b/common/src/test/java/com/vmware/admiral/service/common/util/YamlMapperTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2017 VMware, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product may include a number of subcomponents with separate copyright notices + * and license terms. Your use of these subcomponents is subject to the terms and + * conditions of the subcomponent's license, as noted in the LICENSE file. + */ + +package com.vmware.admiral.service.common.util; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; + +import org.junit.Test; + +import com.vmware.admiral.common.util.YamlMapper; + +public class YamlMapperTest { + + @Test + public void testConvertFromYamlToJson() throws IOException { + String yamlInput = "---\n" + + "person:\n" + + " name: test-name\n" + + " age: 14\n"; + + String expectedJsonOutput = "{\"person\":{\"name\":\"test-name\",\"age\":14}}"; + + String actualJsonOutput = YamlMapper.fromYamlToJson(yamlInput); + + assertEquals(expectedJsonOutput, actualJsonOutput); + } + + @Test + public void testConvertFromJsonToYaml() throws IOException { + String jsonInput = "{\"person\":{\"name\":\"test-name\",\"age\":14}}"; + + String expectedYamlOutput = "---\n" + + "person:\n" + + " name: \"test-name\"\n" + + " age: 14\n"; + + String actualYamlOutput = YamlMapper.fromJsonToYaml(jsonInput); + + assertEquals(expectedYamlOutput, actualYamlOutput); + } +} diff --git a/compute/src/main/java/com/vmware/admiral/compute/content/CompositeDescriptionContentService.java b/compute/src/main/java/com/vmware/admiral/compute/content/CompositeDescriptionContentService.java index e799c4826..418d73e51 100644 --- a/compute/src/main/java/com/vmware/admiral/compute/content/CompositeDescriptionContentService.java +++ b/compute/src/main/java/com/vmware/admiral/compute/content/CompositeDescriptionContentService.java @@ -12,6 +12,7 @@ package com.vmware.admiral.compute.content; import static com.vmware.admiral.common.util.AssertUtil.assertNotEmpty; +import static com.vmware.admiral.common.util.OperationUtil.isApplicationYamlContent; import static com.vmware.admiral.common.util.UriUtilsExtended.MEDIA_TYPE_APPLICATION_YAML; import static com.vmware.admiral.common.util.ValidationUtils.handleValidationException; import static com.vmware.admiral.compute.content.CompositeTemplateUtil.assertContainersComponentsOnly; @@ -228,8 +229,5 @@ private DeferredResult persistComponent(ComponentTemplate componen return nestedState.sendRequest(this, Action.POST); } - private boolean isApplicationYamlContent(String contentType) { - return (contentType != null) - && MEDIA_TYPE_APPLICATION_YAML.equals(contentType.split(";")[0]); - } + } diff --git a/compute/src/main/java/com/vmware/admiral/compute/kubernetes/KubernetesDescriptionContentService.java b/compute/src/main/java/com/vmware/admiral/compute/kubernetes/KubernetesDescriptionContentService.java new file mode 100644 index 000000000..34f26181c --- /dev/null +++ b/compute/src/main/java/com/vmware/admiral/compute/kubernetes/KubernetesDescriptionContentService.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2017 VMware, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product may include a number of subcomponents with separate copyright notices + * and license terms. Your use of these subcomponents is subject to the terms and + * conditions of the subcomponent's license, as noted in the LICENSE file. + */ + +package com.vmware.admiral.compute.kubernetes; + +import static com.vmware.admiral.compute.content.CompositeTemplateUtil.isNullOrEmpty; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import com.vmware.admiral.common.ManagementUriParts; +import com.vmware.admiral.compute.kubernetes.KubernetesDescriptionService.KubernetesDescription; +import com.vmware.xenon.common.LocalizableValidationException; +import com.vmware.xenon.common.Operation; +import com.vmware.xenon.common.OperationJoin; +import com.vmware.xenon.common.StatelessService; +import com.vmware.xenon.common.Utils; + +/** + * Service for parsing single YAML file which contains multiple + * YAML kubernetes definitions and creating multiple Kubernetes Descriptions. + */ +public class KubernetesDescriptionContentService extends StatelessService { + public static final String SELF_LINK = ManagementUriParts.KUBERNETES_DESC_CONTENT; + + private static final String FAIL_ON_CREATE_MSG = "Failed to create Kubernetes Descriptions."; + + @Override + public void handlePost(Operation post) { + if (!post.hasBody()) { + post.fail(new LocalizableValidationException("body is required", "compute.body" + + ".required")); + return; + } + + List kubernetesDefinitions = splitYaml(post.getBody(String.class)); + OperationJoin.create(createOperations(kubernetesDefinitions)) + .setCompletion((ops, errors) -> { + List resourceLinks = new ArrayList<>(); + ops.values().forEach(o -> { + if (o == null) { + return; + } + KubernetesDescription desc = o.getBody(KubernetesDescription.class); + if (!isNullOrEmpty(desc.documentSelfLink)) { + resourceLinks.add(desc.documentSelfLink); + } + }); + if (errors != null) { + errors.values().forEach(e -> logWarning("Failed to create " + + "KubernetesDescription: %s", Utils.toString(e))); + cleanKubernetesDescriptionsAndFail(resourceLinks, post); + } else { + post.setBody(resourceLinks); + post.complete(); + } + }).sendWith(this); + } + + private List splitYaml(String yaml) { + String[] yamls = yaml.split("(? result = Arrays.stream(yamls) + .filter(y -> !y.trim().equals("")) + .collect(Collectors.toList()); + + for (int i = 0; i < result.size(); i++) { + String tempYaml = "---\n" + result.get(i).trim(); + result.remove(i); + result.add(i, tempYaml); + } + + return result; + } + + private List createOperations(List kubernetesDefinitions) { + List ops = kubernetesDefinitions.stream() + .map(yaml -> { + KubernetesDescription description = new KubernetesDescription(); + description.kubernetesEntity = yaml; + return Operation.createPost(this, KubernetesDescriptionService.FACTORY_LINK) + .setBody(description); + }).collect(Collectors.toList()); + return ops; + } + + private void cleanKubernetesDescriptionsAndFail(List selfLinks, Operation op) { + if (selfLinks == null || selfLinks.isEmpty()) { + op.fail(new IllegalStateException(FAIL_ON_CREATE_MSG)); + return; + } + logWarning("Cleaning successfully created Kubernetes Descriptions"); + List deleteOps = new ArrayList<>(); + for (String selfLink : selfLinks) { + deleteOps.add(Operation.createDelete(this, selfLink)); + } + OperationJoin.create(deleteOps) + .setCompletion((ops, errors) -> { + if (errors != null) { + errors.values().forEach(e -> logWarning(Utils.toString(e))); + } + op.fail(new IllegalStateException(FAIL_ON_CREATE_MSG)); + }).sendWith(this); + } +} diff --git a/compute/src/main/java/com/vmware/admiral/compute/kubernetes/KubernetesDescriptionService.java b/compute/src/main/java/com/vmware/admiral/compute/kubernetes/KubernetesDescriptionService.java new file mode 100644 index 000000000..07999c590 --- /dev/null +++ b/compute/src/main/java/com/vmware/admiral/compute/kubernetes/KubernetesDescriptionService.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2017 VMware, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product may include a number of subcomponents with separate copyright notices + * and license terms. Your use of these subcomponents is subject to the terms and + * conditions of the subcomponent's license, as noted in the LICENSE file. + */ + +package com.vmware.admiral.compute.kubernetes; + +import static com.vmware.admiral.common.util.AssertUtil.assertNotNullOrEmpty; + +import java.io.IOException; + +import com.vmware.admiral.common.ManagementUriParts; +import com.vmware.admiral.common.util.YamlMapper; +import com.vmware.admiral.compute.content.kubernetes.CommonKubernetesEntity; +import com.vmware.xenon.common.Operation; +import com.vmware.xenon.common.ServiceDocument; +import com.vmware.xenon.common.StatefulService; + +public class KubernetesDescriptionService extends StatefulService { + public static final String FACTORY_LINK = ManagementUriParts.KUBERNETES_DESC; + + public static class KubernetesDescription extends ServiceDocument { + + /** + * Serialized kubernetes entity in YAML format. + */ + @Documentation(description = "Serialized kubernetes entity in YAML format.") + public String kubernetesEntity; + + /** + * The type of the kubernetes entity. + */ + @Documentation(description = "The type of the kubernetes entity.") + public String type; + + public T getKubernetesEntity(Class type) + throws IOException { + return YamlMapper.objectMapper().readValue(kubernetesEntity, type); + } + + public String getKubernetesEntityAsJson() throws IOException { + return YamlMapper.fromYamlToJson(kubernetesEntity); + } + } + + public KubernetesDescriptionService() { + super(KubernetesDescription.class); + super.toggleOption(ServiceOption.PERSISTENCE, true); + super.toggleOption(ServiceOption.REPLICATION, true); + super.toggleOption(ServiceOption.OWNER_SELECTION, true); + } + + @Override + public void handleCreate(Operation startPost) { + if (!checkForBody(startPost)) { + return; + } + + KubernetesDescription description = startPost.getBody(KubernetesDescription.class); + + try { + validateDescription(description); + startPost.setBody(description); + startPost.complete(); + } catch (Throwable e) { + logSevere(e); + startPost.fail(e); + } + } + + private void validateDescription(KubernetesDescription description) throws IOException { + CommonKubernetesEntity kubernetesEntity = description + .getKubernetesEntity(CommonKubernetesEntity.class); + + assertNotNullOrEmpty(kubernetesEntity.apiVersion, "apiVersion"); + assertNotNullOrEmpty(kubernetesEntity.kind, "kind"); + + description.type = description.getKubernetesEntity(CommonKubernetesEntity.class).kind; + } + + @Override + public void handlePut(Operation put) { + if (!checkForBody(put)) { + return; + } + + KubernetesDescription description = put.getBody(KubernetesDescription.class); + + try { + validateDescription(description); + this.setState(put, description); + put.setBody(description).complete(); + } catch (Throwable e) { + put.fail(e); + } + } + +} diff --git a/compute/src/main/java/com/vmware/admiral/host/HostInitComputeServicesConfig.java b/compute/src/main/java/com/vmware/admiral/host/HostInitComputeServicesConfig.java index 75051326a..3ecea1e04 100644 --- a/compute/src/main/java/com/vmware/admiral/host/HostInitComputeServicesConfig.java +++ b/compute/src/main/java/com/vmware/admiral/host/HostInitComputeServicesConfig.java @@ -60,6 +60,8 @@ import com.vmware.admiral.compute.env.EnvironmentService; import com.vmware.admiral.compute.env.NetworkProfileService; import com.vmware.admiral.compute.env.StorageProfileService; +import com.vmware.admiral.compute.kubernetes.KubernetesDescriptionContentService; +import com.vmware.admiral.compute.kubernetes.KubernetesDescriptionService; import com.vmware.admiral.compute.network.ComputeNetworkDescriptionService; import com.vmware.admiral.compute.network.ComputeNetworkDescriptionService.ComputeNetworkDescription; import com.vmware.admiral.compute.network.ComputeNetworkService; @@ -94,7 +96,8 @@ public static void startServices(ServiceHost host, boolean startMockContainerHos RegistryConfigCertificateDistributionService.class, ComputeInitialBootService.class, ElasticPlacementZoneConfigurationService.class, - EnvironmentMappingService.class); + EnvironmentMappingService.class, + KubernetesDescriptionContentService.class); startServiceFactories(host, CaSigningCertService.class, ContainerDescriptionService.class, @@ -115,7 +118,8 @@ public static void startServices(ServiceHost host, boolean startMockContainerHos ContainerVolumeDescriptionService.class, ElasticPlacementZoneService.class, EpzComputeEnumerationTaskService.class, - PlacementCapacityUpdateTaskService.class); + PlacementCapacityUpdateTaskService.class, + KubernetesDescriptionService.class); if (startMockContainerHostService) { startServices(host, MockContainerHostService.class); diff --git a/compute/src/test/java/com/vmware/admiral/compute/kubernetes/KubernetesDescriptionServiceTest.java b/compute/src/test/java/com/vmware/admiral/compute/kubernetes/KubernetesDescriptionServiceTest.java new file mode 100644 index 000000000..ab8e55816 --- /dev/null +++ b/compute/src/test/java/com/vmware/admiral/compute/kubernetes/KubernetesDescriptionServiceTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2017 VMware, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product may include a number of subcomponents with separate copyright notices + * and license terms. Your use of these subcomponents is subject to the terms and + * conditions of the subcomponent's license, as noted in the LICENSE file. + */ + +package com.vmware.admiral.compute.kubernetes; + +import static org.junit.Assert.assertEquals; + +import org.junit.Before; +import org.junit.Test; + +import com.vmware.admiral.compute.container.ComputeBaseTest; +import com.vmware.admiral.compute.kubernetes.KubernetesDescriptionService.KubernetesDescription; +import com.vmware.xenon.common.Operation; +import com.vmware.xenon.common.UriUtils; +import com.vmware.xenon.common.Utils; + +public class KubernetesDescriptionServiceTest extends ComputeBaseTest { + private String sampleYamlDefinition = "---\n" + + "apiVersion: v1\n" + + "kind: Service\n" + + "metadata:\n" + + " name: wordpress\n" + + " labels:\n" + + " app: wordpress\n" + + "spec:\n" + + " ports:\n" + + " - port: 80\n" + + " selector:\n" + + " app: wordpress\n" + + " tier: frontend\n"; + + private String sampleYamlDefinitionInvalid = "---\n" + + "apiVersion: v1\n" + + "#kind: Service\n" + + "metadata:\n" + + " name: wordpress\n" + + " labels:\n" + + " app: wordpress\n" + + "spec:\n" + + " ports:\n" + + " - port: 80\n" + + " selector:\n" + + " app: wordpress\n" + + " tier: frontend\n"; + + @Before + public void setUp() throws Throwable { + waitForServiceAvailability(KubernetesDescriptionService.FACTORY_LINK); + waitForServiceAvailability(KubernetesDescriptionContentService.SELF_LINK); + } + + @Test + public void testCreateKubernetesDescription() { + KubernetesDescription description = new KubernetesDescription(); + description.kubernetesEntity = sampleYamlDefinition; + + Operation op = Operation.createPost(UriUtils.buildUri(host, KubernetesDescriptionService + .FACTORY_LINK)) + .setBody(description) + .setCompletion((o, ex) -> { + if (ex != null) { + host.log("Creating kubernetes description failed."); + host.failIteration(ex); + return; + } else { + KubernetesDescription desc = o.getBody(KubernetesDescription.class); + try { + assertEquals(description.kubernetesEntity, desc.kubernetesEntity); + assertEquals("Service", desc.type); + assertEquals(description.getKubernetesEntityAsJson(), + desc.getKubernetesEntityAsJson()); + } catch (Throwable e) { + host.log(Utils.toString(e)); + host.failIteration(e); + } + host.completeIteration(); + } + }); + + host.testStart(1); + host.send(op); + host.testWait(); + } + + @Test + public void testCreateKubernetesDescriptionWithInvalidYamlShouldFail() { + KubernetesDescription description = new KubernetesDescription(); + description.kubernetesEntity = sampleYamlDefinitionInvalid; + + Operation op = Operation.createPost(UriUtils.buildUri(host, KubernetesDescriptionService + .FACTORY_LINK)) + .setBody(description) + .setCompletion((o, ex) -> { + if (ex != null) { + host.log("Creating kubernetes description failed."); + host.completeIteration(); + return; + } else { + host.failIteration(new IllegalStateException("Creation of Kubernetes " + + "Description with invalid yaml succeeded")); + } + }); + + host.testStart(1); + host.send(op); + host.testWait(); + } +} diff --git a/compute/src/test/java/com/vmware/admiral/compute/kubernetes/KubernetesDescrtiptionContentServiceTest.java b/compute/src/test/java/com/vmware/admiral/compute/kubernetes/KubernetesDescrtiptionContentServiceTest.java new file mode 100644 index 000000000..2a7838c66 --- /dev/null +++ b/compute/src/test/java/com/vmware/admiral/compute/kubernetes/KubernetesDescrtiptionContentServiceTest.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2017 VMware, Inc. All Rights Reserved. + * + * This product is licensed to you under the Apache License, Version 2.0 (the "License"). + * You may not use this product except in compliance with the License. + * + * This product may include a number of subcomponents with separate copyright notices + * and license terms. Your use of these subcomponents is subject to the terms and + * conditions of the subcomponent's license, as noted in the LICENSE file. + */ + +package com.vmware.admiral.compute.kubernetes; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import com.vmware.admiral.compute.container.ComputeBaseTest; +import com.vmware.admiral.compute.kubernetes.KubernetesDescriptionService.KubernetesDescription; +import com.vmware.xenon.common.Operation; +import com.vmware.xenon.common.UriUtils; +import com.vmware.xenon.common.Utils; + +public class KubernetesDescrtiptionContentServiceTest extends ComputeBaseTest { + private String sampleYamlDefinition = "---\n" + + "apiVersion: v1\n" + + "kind: Service\n" + + "metadata:\n" + + " name: wordpress\n" + + " labels:\n" + + " app: wordpress\n" + + "spec:\n" + + " ports:\n" + + " - port: 80\n" + + " selector:\n" + + " app: wordpress\n" + + " tier: frontend\n"; + + private String sampleYamlDefinitionInvalid = "---\n" + + "apiVersion: v1\n" + + "#kind: Service\n" + + "metadata:\n" + + " name: wordpress\n" + + " labels:\n" + + " app: wordpress\n" + + "spec:\n" + + " ports:\n" + + " - port: 80\n" + + " selector:\n" + + " app: wordpress\n" + + " tier: frontend\n"; + + @Before + public void setUp() throws Throwable { + waitForServiceAvailability(KubernetesDescriptionService.FACTORY_LINK); + waitForServiceAvailability(KubernetesDescriptionContentService.SELF_LINK); + } + + @Test + public void testCreateKubernetesDescriptionsFromMultipleYamls() { + StringBuilder multiYaml = new StringBuilder(); + multiYaml.append(sampleYamlDefinition); + multiYaml.append(sampleYamlDefinition); + multiYaml.append(sampleYamlDefinition); + + Operation op = Operation.createPost(UriUtils.buildUri(host, + KubernetesDescriptionContentService.SELF_LINK)) + .setBody(multiYaml.toString()) + .setCompletion((o, ex) -> { + if (ex != null) { + host.log("Creating k8s descriptions failed: %s", Utils.toString(ex)); + host.failIteration(ex); + return; + } else { + String[] resourceLinks = o.getBody(String[].class); + assertEquals(3, resourceLinks.length); + Arrays.stream(resourceLinks).forEach(r -> assertNotNull(r)); + host.completeIteration(); + } + }); + host.testStart(1); + host.send(op); + host.testWait(); + } + + @Test + public void testCreateKubernetesDescriptionsWithOneInvalidShouldFail() throws Throwable { + StringBuilder multiYaml = new StringBuilder(); + multiYaml.append(sampleYamlDefinition); + multiYaml.append(sampleYamlDefinitionInvalid); + multiYaml.append(sampleYamlDefinition); + + Operation op = Operation.createPost(UriUtils.buildUri(host, + KubernetesDescriptionContentService.SELF_LINK)) + .setBody(multiYaml.toString()) + .setCompletion((o, ex) -> { + if (ex != null) { + host.log("Creating k8s descriptions failed: %s", Utils.toString(ex)); + try { + verifyNoLeftovers(); + } catch (Throwable e) { + host.failIteration(e); + } + host.completeIteration(); + } else { + host.failIteration(new IllegalStateException("Operation had to fail but " + + "it succeeded.")); + } + }); + host.testStart(1); + host.send(op); + host.testWait(); + } + + @Test + public void testCreateKubernetesDescriptionWithAllInvalidShouldFail() { + StringBuilder multiYaml = new StringBuilder(); + multiYaml.append(sampleYamlDefinitionInvalid); + multiYaml.append(sampleYamlDefinitionInvalid); + multiYaml.append(sampleYamlDefinitionInvalid); + + Operation op = Operation.createPost(UriUtils.buildUri(host, + KubernetesDescriptionContentService.SELF_LINK)) + .setBody(multiYaml.toString()) + .setCompletion((o, ex) -> { + if (ex != null) { + host.log("Creating k8s descriptions failed: %s", Utils.toString(ex)); + host.completeIteration(); + return; + } else { + String[] resourceLinks = o.getBody(String[].class); + host.failIteration( + new IllegalStateException(String.format("Operation had to fail and " + + "no descriptions to be created. Count of resources created: %d," + + " resources: %s", resourceLinks.length, String.join(",", + resourceLinks)))); + } + }); + host.testStart(1); + host.send(op); + host.testWait(); + } + + private void verifyNoLeftovers() throws Throwable { + List leftovers = getDocumentLinksOfType(KubernetesDescription.class); + assertEquals(0, leftovers.size()); + } +}