diff --git a/clouds/clouds-azure/pom.xml b/clouds/clouds-azure/pom.xml index 9481dcda..2817c84f 100644 --- a/clouds/clouds-azure/pom.xml +++ b/clouds/clouds-azure/pom.xml @@ -29,6 +29,12 @@ ${project.version} + + commons-net + commons-net + + + com.azure.resourcemanager diff --git a/clouds/clouds-azure/src/main/java/azure/core/AzureArchiveDeployer.java b/clouds/clouds-azure/src/main/java/azure/core/AzureArchiveDeployer.java new file mode 100644 index 00000000..384f3abc --- /dev/null +++ b/clouds/clouds-azure/src/main/java/azure/core/AzureArchiveDeployer.java @@ -0,0 +1,118 @@ +package azure.core; + + +import azure.core.AzureIdentifiableSunstoneResource.Identification; +import com.azure.resourcemanager.appservice.models.DeployType; +import com.azure.resourcemanager.appservice.models.PublishingProfile; +import com.azure.resourcemanager.appservice.models.WebApp; +import org.apache.commons.net.ftp.FTP; +import org.apache.commons.net.ftp.FTPClient; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.wildfly.extras.creaper.commands.deployments.Deploy; +import org.wildfly.extras.creaper.commands.deployments.Undeploy; +import org.wildfly.extras.creaper.core.CommandFailedException; +import org.wildfly.extras.creaper.core.online.OnlineManagementClient; +import sunstone.core.api.SunstoneArchiveDeployer; +import sunstone.core.exceptions.IllegalArgumentSunstoneException; +import sunstone.core.exceptions.SunstoneException; +import sunstone.core.exceptions.UnsupportedSunstoneOperationException; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + + +/** + * Purpose: handle deploy operation to WildFly. + * + * Heavily uses {@link AzureIdentifiableSunstoneResource} to determine the destination of deploy operation + * + * To retrieve Azure cloud resources, the class relies on {@link AzureIdentifiableSunstoneResource#get(Annotation, AzureSunstoneStore, Class)}. + * + * Undeploy operations are registered in the extension store so that they are closed once the store is closed + */ +public class AzureArchiveDeployer implements SunstoneArchiveDeployer { + + static void deployToWebApp(Identification resourceIdentification, InputStream is, AzureSunstoneStore store) throws Exception { + Path tempFile = Files.createTempFile("sunstone-war-deployment-", ".war"); + Files.copy(is, tempFile, StandardCopyOption.REPLACE_EXISTING); + WebApp azureWebApp = resourceIdentification.get(store, WebApp.class); + azureWebApp.deployAsync(DeployType.WAR, tempFile.toFile()).block(); + + store.addClosable(() -> undeployFromWebApp(azureWebApp)); + + azureWebApp.restartAsync().block(); + AzureUtils.waitForWebAppDeployment(azureWebApp); + } + + static void undeployFromWebApp(WebApp webApp) { + PublishingProfile profile = webApp.getPublishingProfile(); + FTPClient ftpClient = new FTPClient(); + String[] ftpUrlSegments = profile.ftpUrl().split("/", 2); + String server = ftpUrlSegments[0]; + try { + ftpClient.connect(server); + ftpClient.enterLocalPassiveMode(); + ftpClient.login(profile.ftpUsername(), profile.ftpPassword()); + ftpClient.setFileType(FTP.BINARY_FILE_TYPE); + + FtpUtils.cleanDirectory(ftpClient, "/site/wwwroot/"); + + ftpClient.disconnect(); + + webApp.restartAsync().block(); + AzureUtils.waitForWebAppCleanState(webApp); + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + static void deployToVmInstance(String deploymentName, Identification resourceIdentification, InputStream is, AzureSunstoneStore store) throws SunstoneException { + try { + OnlineManagementClient client = AzureIdentifiableSunstoneResourceUtils.resolveOnlineManagementClient(resourceIdentification, store); + client.apply(new Deploy.Builder(is, deploymentName, true).build()); + store.addClosable((AutoCloseable) () -> { + client.apply(new Undeploy.Builder(deploymentName).build()); + client.close(); + }); + } catch (CommandFailedException e) { + throw new RuntimeException(e); + } + } + + @Override + public void deployAndRegisterUndeploy(String deploymentName, Annotation targetAnnotation, InputStream deployment, ExtensionContext ctx) throws SunstoneException { + AzureSunstoneStore store = AzureSunstoneStore.get(ctx); + Identification identification = new Identification(targetAnnotation); + + if (!identification.type.deployToWildFlySupported()) { + throw new UnsupportedSunstoneOperationException("todo"); + } + + switch (identification.type) { + case VM_INSTANCE: + if (deploymentName.isEmpty()) { + throw new IllegalArgumentSunstoneException("Deployment name can not be empty for Azure virtual machine."); + } + deployToVmInstance(deploymentName, identification, deployment, store); + break; + case WEB_APP: + try { + if (!deploymentName.isEmpty()) { + throw new IllegalArgumentSunstoneException("Deployment name must be empty for Azure Web App. It is always ROOT.war and only WAR is supported."); + } + deployToWebApp(identification, deployment, store); + } catch (Exception e) { + throw new RuntimeException(e); + } + break; + default: + throw new UnsupportedSunstoneOperationException("todo"); + } + } +} diff --git a/clouds/clouds-azure/src/main/java/azure/core/AzureArchiveDeployerProvider.java b/clouds/clouds-azure/src/main/java/azure/core/AzureArchiveDeployerProvider.java new file mode 100644 index 00000000..df323fa6 --- /dev/null +++ b/clouds/clouds-azure/src/main/java/azure/core/AzureArchiveDeployerProvider.java @@ -0,0 +1,26 @@ +package azure.core; + + +import azure.core.AzureIdentifiableSunstoneResource.Identification; +import azure.core.identification.AzureArchiveDeploymentAnnotation; +import sunstone.core.AnnotationUtils; +import sunstone.core.api.SunstoneArchiveDeployer; +import sunstone.core.spi.SunstoneArchiveDeployerProvider; + +import java.lang.annotation.Annotation; +import java.util.Optional; + + +public class AzureArchiveDeployerProvider implements SunstoneArchiveDeployerProvider { + + @Override + public Optional create(Annotation annotation) { + if (AnnotationUtils.isAnnotatedBy(annotation.annotationType(), AzureArchiveDeploymentAnnotation.class)) { + Identification identification = new Identification(annotation); + if (identification.type != AzureIdentifiableSunstoneResource.UNSUPPORTED && identification.type.deployToWildFlySupported()) { + return Optional.of(new AzureArchiveDeployer()); + } + } + return Optional.empty(); + } +} diff --git a/clouds/clouds-azure/src/main/java/azure/core/AzureIdentifiableSunstoneResource.java b/clouds/clouds-azure/src/main/java/azure/core/AzureIdentifiableSunstoneResource.java index 2a06a390..eff00da0 100644 --- a/clouds/clouds-azure/src/main/java/azure/core/AzureIdentifiableSunstoneResource.java +++ b/clouds/clouds-azure/src/main/java/azure/core/AzureIdentifiableSunstoneResource.java @@ -5,7 +5,7 @@ import azure.core.identification.AzureVirtualMachine; import azure.core.identification.AzureWebApplication; import com.azure.resourcemanager.AzureResourceManager; -import com.azure.resourcemanager.appservice.models.WebAppBasic; +import com.azure.resourcemanager.appservice.models.WebApp; import com.azure.resourcemanager.compute.models.VirtualMachine; import org.wildfly.extras.creaper.core.online.OnlineManagementClient; import org.wildfly.extras.sunstone.api.impl.ObjectProperties; @@ -69,6 +69,10 @@ boolean isTypeSupportedForInject(Class type) { boolean isTypeSupportedForInject(Class type) { return Arrays.stream(supportedTypesForInjection).anyMatch(clazz -> clazz.isAssignableFrom(type)); } + @Override + boolean deployToWildFlySupported() { + return true; + } @Override T get(Annotation injectionAnnotation, AzureSunstoneStore store, Class clazz) throws SunstoneException { @@ -99,6 +103,10 @@ boolean isTypeSupportedForInject(Class type) { return Arrays.stream(supportedTypesForInjection).anyMatch(clazz -> clazz.isAssignableFrom(type)); } @Override + boolean deployToWildFlySupported() { + return true; + } + @Override T get(Annotation injectionAnnotation, AzureSunstoneStore store, Class clazz) throws SunstoneException { if(!getRepresentedInjectionAnnotation().isAssignableFrom(injectionAnnotation.annotationType())) { throw new IllegalArgumentSunstoneException(format("Expected %s annotation type but got %s", @@ -107,7 +115,7 @@ T get(Annotation injectionAnnotation, AzureSunstoneStore store, Class cla AzureWebApplication webApp = (AzureWebApplication) injectionAnnotation; String appName = replaceSystemProperties(webApp.name()); String appGroup = webApp.group().isEmpty() ? objectProperties.getProperty(AzureConfig.GROUP) : replaceSystemProperties(webApp.group()); - Optional azureWebApp = AzureUtils.findAzureWebApp(store.getAzureArmClientOrCreate(), appName, appGroup); + Optional azureWebApp = AzureUtils.findAzureWebApp(store.getAzureArmClientOrCreate(), appName, appGroup); return clazz.cast(azureWebApp.orElseThrow(() -> new SunstoneCloudResourceException(format("Unable to find '%s' Azure Web App in '%s' resource group.", appName, appGroup)))); } }; @@ -135,6 +143,9 @@ public String toString() { boolean isTypeSupportedForInject(Class type) { return false; } + boolean deployToWildFlySupported() { + return false; + } T get(Annotation injectionAnnotation, AzureSunstoneStore store, Class clazz) throws SunstoneException { throw new UnsupportedSunstoneOperationException(format("%s annotation is nto supported for the type %s", injectionAnnotation.annotationType().getName(), this.toString())); diff --git a/clouds/clouds-azure/src/main/java/azure/core/AzureIdentifiableSunstoneResourceUtils.java b/clouds/clouds-azure/src/main/java/azure/core/AzureIdentifiableSunstoneResourceUtils.java new file mode 100644 index 00000000..a8a7fa95 --- /dev/null +++ b/clouds/clouds-azure/src/main/java/azure/core/AzureIdentifiableSunstoneResourceUtils.java @@ -0,0 +1,49 @@ +package azure.core; + + +import azure.core.identification.AzureVirtualMachine; +import com.azure.resourcemanager.appservice.models.WebApp; +import com.azure.resourcemanager.compute.models.VirtualMachine; +import org.wildfly.extras.creaper.core.online.OnlineManagementClient; +import sunstone.api.EapMode; +import sunstone.api.inject.Hostname; +import sunstone.core.CreaperUtils; +import sunstone.core.exceptions.SunstoneException; +import sunstone.core.exceptions.UnsupportedSunstoneOperationException; + +import java.io.IOException; + +import static azure.core.AzureIdentifiableSunstoneResource.VM_INSTANCE; + +public class AzureIdentifiableSunstoneResourceUtils { + + static Hostname resolveHostname(AzureIdentifiableSunstoneResource.Identification identification, AzureSunstoneStore store) throws SunstoneException { + switch (identification.type) { + case VM_INSTANCE: + VirtualMachine vm = identification.get(store, VirtualMachine.class); + return vm.getPrimaryPublicIPAddress()::ipAddress; + case WEB_APP: + WebApp app = identification.get(store, WebApp.class); + return app::defaultHostname; + default: + throw new UnsupportedSunstoneOperationException("Unsupported type for getting hostname: " + identification.type); + } + } + + static OnlineManagementClient resolveOnlineManagementClient(AzureIdentifiableSunstoneResource.Identification identification, AzureSunstoneStore store) throws SunstoneException { + try { + if (identification.type == VM_INSTANCE) { + AzureVirtualMachine annotation = (AzureVirtualMachine) identification.identification; + if (annotation.mode() == EapMode.STANDALONE) { + return CreaperUtils.createStandaloneManagementClient(resolveHostname(identification, store).get(), annotation.standalone()); + } else { + throw new UnsupportedSunstoneOperationException("Only standalone mode is supported for injecting OnlineManagementClient."); + } + } else { + throw new UnsupportedSunstoneOperationException("Only Azure VM instance is supported for injecting OnlineManagementClient."); + } + } catch (IOException e) { + throw new SunstoneException(e); + } + } +} diff --git a/clouds/clouds-azure/src/main/java/azure/core/AzureSunstoneDeployer.java b/clouds/clouds-azure/src/main/java/azure/core/AzureSunstoneDeployer.java index 394c2061..ea88366e 100644 --- a/clouds/clouds-azure/src/main/java/azure/core/AzureSunstoneDeployer.java +++ b/clouds/clouds-azure/src/main/java/azure/core/AzureSunstoneDeployer.java @@ -12,8 +12,7 @@ import java.util.Map; /** - * Purpose: handles creating resources on clouds. Resources may be defined by AWS CloudFormation template, - * Azure template, JCloud Sunstone properties, ... + * Purpose: handles creating resources on clouds. Resources may be defined by Azure ARM template *

* Used by {@link SunstoneExtension} which delegate handling TestClass annotations such as {@link WithAzureArmTemplate}. * Lambda function to undeploy resources is also registered for the AfterAllCallback phase. @@ -40,7 +39,7 @@ public void deploy(Annotation annotation, ExtensionContext ctx) { } String region = resolveOrGetFromSunstoneProperties(armTemplateDefinition.region(), AzureConfig.REGION); if (region == null) { - throw new IllegalArgumentException("Region for AWS template is not defined. It must be specified either" + throw new IllegalArgumentException("Region for Azure ARM template is not defined. It must be specified either" + "in the annotation or in sunstone.properties file"); } diff --git a/clouds/clouds-azure/src/main/java/azure/core/AzureSunstoneResourceInjector.java b/clouds/clouds-azure/src/main/java/azure/core/AzureSunstoneResourceInjector.java index 25422a75..82c7d896 100644 --- a/clouds/clouds-azure/src/main/java/azure/core/AzureSunstoneResourceInjector.java +++ b/clouds/clouds-azure/src/main/java/azure/core/AzureSunstoneResourceInjector.java @@ -1,29 +1,20 @@ package azure.core; -import azure.core.AzureIdentifiableSunstoneResource.Identification; import azure.core.identification.AzureInjectionAnnotation; -import azure.core.identification.AzureVirtualMachine; import com.azure.resourcemanager.AzureResourceManager; -import com.azure.resourcemanager.appservice.models.WebApp; -import com.azure.resourcemanager.compute.models.VirtualMachine; import org.junit.jupiter.api.extension.ExtensionContext; import org.wildfly.extras.creaper.core.online.OnlineManagementClient; -import sunstone.api.EapMode; import sunstone.api.inject.Hostname; import sunstone.core.AnnotationUtils; -import sunstone.core.CreaperUtils; import sunstone.core.api.SunstoneResourceInjector; import sunstone.core.exceptions.SunstoneException; -import sunstone.core.exceptions.UnsupportedSunstoneOperationException; -import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.Arrays; import java.util.Objects; -import static azure.core.AzureIdentifiableSunstoneResource.VM_INSTANCE; import static java.lang.String.format; @@ -35,38 +26,9 @@ * To retrieve Azure cloud resources, the class relies on {@link AzureIdentifiableSunstoneResource#get(Annotation, AzureSunstoneStore, Class)}. * If needed, it can inject resources directly or form the resources (get a hostname of AZ VM and create a {@link Hostname}) lambda * - * Closable resources are registered in root extension store so that they are closed once the root store is closed (end of suite) + * Closable resources are registered in the extension store so that they are closed once the store is closed */ public class AzureSunstoneResourceInjector implements SunstoneResourceInjector { - static Hostname resolveHostnameDI(Identification identification, AzureSunstoneStore store) throws SunstoneException { - switch (identification.type) { - case VM_INSTANCE: - VirtualMachine vm = identification.get(store, VirtualMachine.class); - return vm.getPrimaryPublicIPAddress()::ipAddress; - case WEB_APP: - WebApp app = identification.get(store, WebApp.class); - return app::defaultHostname; - default: - throw new UnsupportedSunstoneOperationException("Unsupported type for getting hostname: " + identification.type); - } - } - - static OnlineManagementClient resolveOnlineManagementClientDI(Identification identification, AzureSunstoneStore store) throws SunstoneException { - try { - if (identification.type == VM_INSTANCE) { - AzureVirtualMachine annotation = (AzureVirtualMachine) identification.identification; - if (annotation.mode() == EapMode.STANDALONE) { - return CreaperUtils.createStandaloneManagementClient(resolveHostnameDI(identification, store).get(), annotation.standalone()); - } else { - throw new UnsupportedSunstoneOperationException("Only standalone mode is supported for injecting OnlineManagementClient."); - } - } else { - throw new UnsupportedSunstoneOperationException("Only Azure VM instance is supported for injecting OnlineManagementClient."); - } - } catch (IOException e) { - throw new SunstoneException(e); - } - } static boolean canInject (Field field) { return Arrays.stream(field.getAnnotations()) @@ -87,16 +49,16 @@ public Object getAndRegisterResource(Annotation annotation, Class fieldType, identification.identification.annotationType(), fieldType)); } if (Hostname.class.isAssignableFrom(fieldType)) { - injected = resolveHostnameDI(identification, store); + injected = AzureIdentifiableSunstoneResourceUtils.resolveHostname(identification, store); Objects.requireNonNull(injected, "Unable to determine hostname."); } else if (AzureResourceManager.class.isAssignableFrom(fieldType)) { // we can inject cached client because it is not closable and a user can not change it injected = store.getAzureArmClientOrCreate(); Objects.requireNonNull(injected, "Unable to determine Azure ARM client."); } else if (OnlineManagementClient.class.isAssignableFrom(fieldType)) { - OnlineManagementClient client = resolveOnlineManagementClientDI(identification, store); + OnlineManagementClient client = AzureIdentifiableSunstoneResourceUtils.resolveOnlineManagementClient(identification, store); Objects.requireNonNull(client, "Unable to determine management client."); - store.addSuiteLevelClosable(client); + store.addClosable(client); injected = client; } return injected; diff --git a/clouds/clouds-azure/src/main/java/azure/core/AzureUtils.java b/clouds/clouds-azure/src/main/java/azure/core/AzureUtils.java index 5860155a..92fa1cc4 100644 --- a/clouds/clouds-azure/src/main/java/azure/core/AzureUtils.java +++ b/clouds/clouds-azure/src/main/java/azure/core/AzureUtils.java @@ -7,7 +7,6 @@ import com.azure.identity.ClientSecretCredentialBuilder; import com.azure.resourcemanager.AzureResourceManager; import com.azure.resourcemanager.appservice.models.WebApp; -import com.azure.resourcemanager.appservice.models.WebAppBasic; import com.azure.resourcemanager.compute.models.VirtualMachine; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -50,12 +49,68 @@ static Optional findAzureVM(AzureResourceManager arm, String nam .findAny(); } - static Optional findAzureWebApp(AzureResourceManager arm, String name, String resourceGroup) { - return arm.webApps().listByResourceGroup(resourceGroup) .stream() - .filter(webApp -> webApp.name().equals(name)) - .findAny(); + static Optional findAzureWebApp(AzureResourceManager arm, String name, String resourceGroup) { + try { + return Optional.ofNullable(arm.webApps().getByResourceGroup(resourceGroup, name)); + } catch (Exception e) { + e.printStackTrace(); + return Optional.empty(); + } + } + + static void waitForWebAppDeployment(WebApp app) throws InterruptedException, IOException { + OkHttpClient client = new OkHttpClient(); + // 20 minutes in millis + long timeout = TimeoutUtils.adjust(1200000); + long start = System.currentTimeMillis(); + while (System.currentTimeMillis() - start < timeout) { + try { + Response response = client.newCall(new Request.Builder() + .url("http://" + app.defaultHostname()) + .method("GET", null) + .build()) + .execute(); + int code = response.code(); + String body = response.body() != null ? response.body().string() : null; + if (code < 500 && !isWebAppDnsProblem(code, body) && !isEapWebAppWelcomePage(code, body)) { + return; + } + } catch (SocketTimeoutException e) { + // skipping timeout + Thread.sleep(TimeoutUtils.adjust(500)); + } + } + throw new RuntimeException("Timeout!"); + } + static void waitForWebAppCleanState(WebApp app) throws InterruptedException, IOException { + OkHttpClient client = new OkHttpClient(); + // 20 minutes in millis + long timeout = TimeoutUtils.adjust(1200000); + long start = System.currentTimeMillis(); + while (System.currentTimeMillis() - start < timeout) { + try { + Response response = client.newCall(new Request.Builder() + .url("http://" + app.defaultHostname()) + .method("GET", null) + .build()) + .execute(); + int code = response.code(); + String body = response.body() != null ? response.body().string() : null; + // several strings from EAP Web APP welcome page + // expecting to be any deployment undeployed and waiting for the welcome page + if (isEapWebAppWelcomePage(code, body)) { + return; + } + } catch (SocketTimeoutException e) { + // skipping timeout + Thread.sleep(TimeoutUtils.adjust(500)); + } + } + throw new RuntimeException("Timeout!"); + + } static void waitForWebApp(WebApp app) throws InterruptedException, IOException { OkHttpClient client = new OkHttpClient(); // 20 minutes in millis @@ -69,16 +124,8 @@ static void waitForWebApp(WebApp app) throws InterruptedException, IOException { .build()) .execute(); int code = response.code(); - /* - Skip 404 with very specific error. - The error comes from Azure portal when web app is already deployed but DNS has some troubles with redirecting - the connection - */ - if (code == 404 && response.body() != null - && response.body().string().contains("404 Web Site not found.") - && response.body().string().contains("Custom domain has not been configured inside Azure. See how to map an existing domain to resolve this.") - && response.body().string().contains("Client cache is still pointing the domain to old IP address. Clear the cache by running the command ipconfig/flushdns.") - ) { + String body = response.body() != null ? response.body().string() : null; + if (isWebAppDnsProblem(code, body)) { continue; } if (code < 500) { @@ -91,4 +138,22 @@ static void waitForWebApp(WebApp app) throws InterruptedException, IOException { } throw new RuntimeException("Timeout!"); } + + /* + Skip 404 with very specific error. + The error comes from Azure portal when web app is already deployed but DNS has some troubles with redirecting + the connection + */ + private static boolean isWebAppDnsProblem(int code, String body) { + return code == 404 && body != null + && body.contains("404 Web Site not found.") + && body.contains("Custom domain has not been configured inside Azure. See how to map an existing domain to resolve this.") + && body.contains("Client cache is still pointing the domain to old IP address. Clear the cache by running the command ipconfig/flushdns."); + } + private static boolean isEapWebAppWelcomePage(int code, String body) { + return code == 200 && body != null + && body.contains("Use deployment center to get code published from your client or setup continuous deployment.") + && body.contains("Follow our quickstart guide and you'll have a full app ready in 5 minutes or less.
") + && body.contains("

Your app service is up and running.

"); + } } diff --git a/clouds/clouds-azure/src/main/java/azure/core/FtpUtils.java b/clouds/clouds-azure/src/main/java/azure/core/FtpUtils.java new file mode 100644 index 00000000..c6b8d54c --- /dev/null +++ b/clouds/clouds-azure/src/main/java/azure/core/FtpUtils.java @@ -0,0 +1,69 @@ +package azure.core; + + +import org.apache.commons.net.ftp.FTPClient; +import org.apache.commons.net.ftp.FTPFile; + +import java.io.IOException; + +public class FtpUtils { + /** + * Clean a directory by delete all its sub files and + * sub directories recursively. + */ + public static void cleanDirectory(FTPClient ftpClient, String dir) throws IOException { + removeDirectory(ftpClient, dir, "", true); + } + /** + * Removes a non-empty directory by delete all its sub files and + * sub directories recursively. And finally remove the directory if requested. + */ + public static void removeDirectory(FTPClient ftpClient, String parentDir, + String currentDir, boolean removeOnlyContent) throws IOException { + String dirToList = parentDir; + if (!currentDir.equals("")) { + dirToList += "/" + currentDir; + } + + FTPFile[] subFiles = ftpClient.listFiles(dirToList); + + if (subFiles != null && subFiles.length > 0) { + for (FTPFile aFile : subFiles) { + String currentFileName = aFile.getName(); + if (currentFileName.equals(".") || currentFileName.equals("..")) { + // skip parent directory and the directory itself + continue; + } + String filePath = parentDir + "/" + currentDir + "/" + + currentFileName; + if (currentDir.equals("")) { + filePath = parentDir + "/" + currentFileName; + } + + if (aFile.isDirectory()) { + // remove the sub directory + removeDirectory(ftpClient, dirToList, currentFileName, false); + } else { + // delete the file + boolean deleted = ftpClient.deleteFile(filePath); + if (deleted) { + System.out.println("DELETED the file: " + filePath); + } else { + System.out.println("CANNOT delete the file: " + + filePath); + } + } + } + + // finally, remove the directory itself + if (!removeOnlyContent) { + boolean removed = ftpClient.removeDirectory(dirToList); + if (removed) { + System.out.println("REMOVED the directory: " + dirToList); + } else { + System.out.println("CANNOT remove the directory: " + dirToList); + } + } + } + } +} diff --git a/clouds/clouds-azure/src/main/java/azure/core/identification/AzureArchiveDeploymentAnnotation.java b/clouds/clouds-azure/src/main/java/azure/core/identification/AzureArchiveDeploymentAnnotation.java new file mode 100644 index 00000000..e01125ce --- /dev/null +++ b/clouds/clouds-azure/src/main/java/azure/core/identification/AzureArchiveDeploymentAnnotation.java @@ -0,0 +1,60 @@ +package azure.core.identification; + +import com.azure.resourcemanager.appservice.models.WebApp; +import org.wildfly.extras.creaper.commands.deployments.Deploy; +import org.wildfly.extras.creaper.commands.deployments.Undeploy; +import org.wildfly.extras.creaper.core.online.OnlineManagementClient; +import sunstone.api.SunstoneArchiveDeployTargetAnotation; + +import java.io.File; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/** + * Aggregates {@link SunstoneArchiveDeployTargetAnotation} annotation for Azure module purposes. + *
+ * Used to determine that the method annotated by {@link sunstone.api.Deployment} has annotation marking Azure module ability + * to deploy to the resource. + *
+ * This is for JavaDoc only. Aggregates information about what resources are supported for archive (JAR, WAR, EAR) deployment. + *
+ *
+ * All values in annotations are resolvable - ${my.system.property:default_value}. May be used more than once + *
+ *
+ * + * Supported resources: + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Supported Azure identification annotationsnotes
+ * {@link AzureVirtualMachine} + * + * {@link OnlineManagementClient} client is used and {@link Deploy} is used. {@link Undeploy} is run on after all callback. + *
+ * {@link AzureWebApplication} + * + * Azure SDK {@link WebApp#warDeploy(File)} is used. The module restart the app and waits until welcome page is gone. + * Undeploy operation: purge directory {@code /site/wwwroot/}, restarts the app and waits for the welcome page + *
+ *
+ * + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +@SunstoneArchiveDeployTargetAnotation +public @interface AzureArchiveDeploymentAnnotation { +} diff --git a/clouds/clouds-azure/src/main/java/azure/core/identification/AzureVirtualMachine.java b/clouds/clouds-azure/src/main/java/azure/core/identification/AzureVirtualMachine.java index a28f20f0..84afc3c0 100644 --- a/clouds/clouds-azure/src/main/java/azure/core/identification/AzureVirtualMachine.java +++ b/clouds/clouds-azure/src/main/java/azure/core/identification/AzureVirtualMachine.java @@ -2,6 +2,7 @@ import org.wildfly.extras.creaper.core.online.OnlineManagementClient; +import sunstone.api.Deployment; import sunstone.api.DomainMode; import sunstone.api.EapMode; import sunstone.api.StandaloneMode; @@ -18,11 +19,16 @@ * Injectable: {@link Hostname} and {@link OnlineManagementClient} *
* For more information about possible injection, see {@link AzureInjectionAnnotation} + *
+ * Archive deploy operation (using {@link Deployment}) is supported under the name defined by {@link Deployment#name()}. + *
+ * For more information about possible archive deploy operation, see {@link AzureArchiveDeploymentAnnotation} */ // represented by AzureIdentifiableSunstoneResource#VM_INSTANCE @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) +@Target({ElementType.FIELD, ElementType.METHOD}) @AzureInjectionAnnotation +@AzureArchiveDeploymentAnnotation public @interface AzureVirtualMachine { String name(); String group() default ""; diff --git a/clouds/clouds-azure/src/main/java/azure/core/identification/AzureWebApplication.java b/clouds/clouds-azure/src/main/java/azure/core/identification/AzureWebApplication.java index 77e8d415..661d08a2 100644 --- a/clouds/clouds-azure/src/main/java/azure/core/identification/AzureWebApplication.java +++ b/clouds/clouds-azure/src/main/java/azure/core/identification/AzureWebApplication.java @@ -1,6 +1,7 @@ package azure.core.identification; +import sunstone.api.Deployment; import sunstone.api.inject.Hostname; import java.lang.annotation.ElementType; @@ -14,11 +15,16 @@ * Injectable: {@link Hostname} *
* For more information about possible injection, see {@link AzureInjectionAnnotation} + *
+ * Archive deploy operation (using {@link sunstone.api.Deployment}) is supported. Always deployed as a ROOT.war ignoring {@link Deployment#name()}. + *
+ * For more information about possible archive deploy operation, see {@link AzureArchiveDeploymentAnnotation} */ // represented by AzureIdentifiableSunstoneResource#WEB_APP @Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) +@Target({ElementType.FIELD, ElementType.METHOD}) @AzureInjectionAnnotation +@AzureArchiveDeploymentAnnotation public @interface AzureWebApplication { String name(); diff --git a/clouds/clouds-azure/src/main/resources/META-INF/services/sunstone.core.spi.SunstoneArchiveDeployerProvider b/clouds/clouds-azure/src/main/resources/META-INF/services/sunstone.core.spi.SunstoneArchiveDeployerProvider new file mode 100644 index 00000000..5eeffb60 --- /dev/null +++ b/clouds/clouds-azure/src/main/resources/META-INF/services/sunstone.core.spi.SunstoneArchiveDeployerProvider @@ -0,0 +1 @@ +azure.core.AzureArchiveDeployerProvider \ No newline at end of file diff --git a/clouds/clouds-azure/src/test/java/azure/armTemplates/archiveDeploy/vm/VmDeploySuiteTests.java b/clouds/clouds-azure/src/test/java/azure/armTemplates/archiveDeploy/vm/VmDeploySuiteTests.java new file mode 100644 index 00000000..7f6ce54c --- /dev/null +++ b/clouds/clouds-azure/src/test/java/azure/armTemplates/archiveDeploy/vm/VmDeploySuiteTests.java @@ -0,0 +1,17 @@ +package azure.armTemplates.archiveDeploy.vm; + + +import azure.armTemplates.archiveDeploy.vm.suitetests.AzureVmDeployFirstTest; +import azure.armTemplates.archiveDeploy.vm.suitetests.AzureVmUndeployedSecondTest; +import org.junit.platform.suite.api.SelectClasses; +import org.junit.platform.suite.api.Suite; + +/** + * The order of the classes matters. The first one verify the archive is deployed. The second one doesn't deploy any and + * verifies that undeplou operation works. + */ +@Suite +@SelectClasses({AzureVmDeployFirstTest.class, AzureVmUndeployedSecondTest.class}) +public class VmDeploySuiteTests { + public static final String vmDeployGroup = "deploytestVM"; +} diff --git a/clouds/clouds-azure/src/test/java/azure/armTemplates/archiveDeploy/vm/suitetests/AzureVmDeployFirstTest.java b/clouds/clouds-azure/src/test/java/azure/armTemplates/archiveDeploy/vm/suitetests/AzureVmDeployFirstTest.java new file mode 100644 index 00000000..0305c235 --- /dev/null +++ b/clouds/clouds-azure/src/test/java/azure/armTemplates/archiveDeploy/vm/suitetests/AzureVmDeployFirstTest.java @@ -0,0 +1,52 @@ +package azure.armTemplates.archiveDeploy.vm.suitetests; + + +import azure.core.identification.AzureVirtualMachine; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.assertj.core.api.Assertions; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.jupiter.api.Test; +import sunstone.api.Parameter; +import sunstone.api.Deployment; +import sunstone.api.WithAzureArmTemplate; +import sunstone.api.inject.Hostname; + +import java.io.IOException; + +import static azure.armTemplates.AzureTestConstants.IMAGE_REF; +import static azure.armTemplates.AzureTestConstants.instanceName; +import static azure.armTemplates.archiveDeploy.vm.VmDeploySuiteTests.vmDeployGroup; + +@WithAzureArmTemplate(parameters = { + @Parameter(k = "virtualMachineName", v = instanceName), + @Parameter(k = "imageRefId", v = IMAGE_REF) +}, + template = "azure/armTemplates/eap.json", group = vmDeployGroup, perSuite = true) +public class AzureVmDeployFirstTest { + + @Deployment(name = "testapp.war") + @AzureVirtualMachine(name = instanceName, group = vmDeployGroup) + static WebArchive deploy() { + return ShrinkWrap.create(WebArchive.class) + .addAsWebResource(new StringAsset("Hello World"), "index.jsp"); + } + + @AzureVirtualMachine(name = instanceName, group = vmDeployGroup) + Hostname hostname; + + @Test + public void test() throws IOException { + OkHttpClient client = new OkHttpClient(); + + Request request = new Request.Builder() + .url("http://" + hostname.get() + ":8080/testapp") + .method("GET", null) + .build(); + Response response = client.newCall(request).execute(); + Assertions.assertThat(response.body().string()).isEqualTo("Hello World"); + } +} diff --git a/clouds/clouds-azure/src/test/java/azure/armTemplates/archiveDeploy/vm/suitetests/AzureVmUndeployedSecondTest.java b/clouds/clouds-azure/src/test/java/azure/armTemplates/archiveDeploy/vm/suitetests/AzureVmUndeployedSecondTest.java new file mode 100644 index 00000000..b6b113dd --- /dev/null +++ b/clouds/clouds-azure/src/test/java/azure/armTemplates/archiveDeploy/vm/suitetests/AzureVmUndeployedSecondTest.java @@ -0,0 +1,43 @@ +package azure.armTemplates.archiveDeploy.vm.suitetests; + + +import azure.core.identification.AzureVirtualMachine; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import sunstone.api.Parameter; +import sunstone.api.WithAzureArmTemplate; +import sunstone.api.inject.Hostname; + +import java.io.IOException; + +import static azure.armTemplates.AzureTestConstants.IMAGE_REF; +import static azure.armTemplates.AzureTestConstants.instanceName; +import static azure.armTemplates.archiveDeploy.vm.VmDeploySuiteTests.vmDeployGroup; + +/** + * The test is supposed to run after AzureWebAppDeployFirstTest and verifies undeploy operation + */ +@WithAzureArmTemplate(parameters = { + @Parameter(k = "virtualMachineName", v = instanceName), + @Parameter(k = "imageRefId", v = IMAGE_REF) +}, + template = "azure/armTemplates/eap.json", group = vmDeployGroup, perSuite = true) +public class AzureVmUndeployedSecondTest { + @AzureVirtualMachine(name = instanceName, group = vmDeployGroup) + Hostname hostname; + + @Test + public void test() throws IOException { + OkHttpClient client = new OkHttpClient(); + + Request request = new Request.Builder() + .url("http://" + hostname.get() + ":8080/testapp") + .method("GET", null) + .build(); + Response response = client.newCall(request).execute(); + Assertions.assertThat(response.body().string()).isNotEqualTo("Hello World"); + } +} diff --git a/clouds/clouds-azure/src/test/java/azure/armTemplates/archiveDeploy/webapp/WebAppDeploySuiteTests.java b/clouds/clouds-azure/src/test/java/azure/armTemplates/archiveDeploy/webapp/WebAppDeploySuiteTests.java new file mode 100644 index 00000000..75da9d43 --- /dev/null +++ b/clouds/clouds-azure/src/test/java/azure/armTemplates/archiveDeploy/webapp/WebAppDeploySuiteTests.java @@ -0,0 +1,18 @@ +package azure.armTemplates.archiveDeploy.webapp; + + +import azure.armTemplates.archiveDeploy.webapp.suitetests.AzureWebAppDeployFirstTest; +import azure.armTemplates.archiveDeploy.webapp.suitetests.AzureWebAppUndeployedSecondTest; +import org.junit.platform.suite.api.SelectClasses; +import org.junit.platform.suite.api.Suite; + + +/** + * The order of the classes matters. The first one verify the archive is deployed. The second one doesn't deploy any and + * verifies that undeplou operation works. + */ +@Suite +@SelectClasses({AzureWebAppDeployFirstTest.class, AzureWebAppUndeployedSecondTest.class}) +public class WebAppDeploySuiteTests { + public static final String webAppDeployGroup = "deploytestWebApp"; +} diff --git a/clouds/clouds-azure/src/test/java/azure/armTemplates/archiveDeploy/webapp/suitetests/AzureWebAppDeployFirstTest.java b/clouds/clouds-azure/src/test/java/azure/armTemplates/archiveDeploy/webapp/suitetests/AzureWebAppDeployFirstTest.java new file mode 100644 index 00000000..5f6df8e4 --- /dev/null +++ b/clouds/clouds-azure/src/test/java/azure/armTemplates/archiveDeploy/webapp/suitetests/AzureWebAppDeployFirstTest.java @@ -0,0 +1,47 @@ +package azure.armTemplates.archiveDeploy.webapp.suitetests; + + +import azure.core.identification.AzureWebApplication; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.assertj.core.api.Assertions; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.jupiter.api.Test; +import sunstone.api.Deployment; +import sunstone.api.Parameter; +import sunstone.api.WithAzureArmTemplate; +import sunstone.api.inject.Hostname; + +import java.io.IOException; + +import static azure.armTemplates.AzureTestConstants.instanceName; +import static azure.armTemplates.archiveDeploy.webapp.WebAppDeploySuiteTests.webAppDeployGroup; + +@WithAzureArmTemplate(template = "azure/armTemplates/eapWebApp.json", + parameters = {@Parameter(k = "appName", v = instanceName)}, group = webAppDeployGroup, perSuite = true) +public class AzureWebAppDeployFirstTest { + @Deployment + @AzureWebApplication(name = instanceName, group = webAppDeployGroup) + static WebArchive deploy() { + return ShrinkWrap.create(WebArchive.class) + .addAsWebResource(new StringAsset("Hello World"), "index.jsp"); + } + + @AzureWebApplication(name = instanceName, group = webAppDeployGroup) + Hostname hostname; + + @Test + public void test() throws IOException { + OkHttpClient client = new OkHttpClient(); + + Request request = new Request.Builder() + .url("http://" + hostname.get()) + .method("GET", null) + .build(); + Response response = client.newCall(request).execute(); + Assertions.assertThat(response.body().string()).isEqualTo("Hello World"); + } +} diff --git a/clouds/clouds-azure/src/test/java/azure/armTemplates/archiveDeploy/webapp/suitetests/AzureWebAppUndeployedSecondTest.java b/clouds/clouds-azure/src/test/java/azure/armTemplates/archiveDeploy/webapp/suitetests/AzureWebAppUndeployedSecondTest.java new file mode 100644 index 00000000..2d7936bb --- /dev/null +++ b/clouds/clouds-azure/src/test/java/azure/armTemplates/archiveDeploy/webapp/suitetests/AzureWebAppUndeployedSecondTest.java @@ -0,0 +1,40 @@ +package azure.armTemplates.archiveDeploy.webapp.suitetests; + + +import azure.core.identification.AzureWebApplication; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import sunstone.api.Parameter; +import sunstone.api.WithAzureArmTemplate; +import sunstone.api.inject.Hostname; + +import java.io.IOException; + +import static azure.armTemplates.AzureTestConstants.instanceName; +import static azure.armTemplates.archiveDeploy.webapp.WebAppDeploySuiteTests.webAppDeployGroup; + +/** + * The test is supposed to run after AzureWebAppDeployFirstTest and verifies undeploy operation + */ +@WithAzureArmTemplate(template = "azure/armTemplates/eapWebApp.json", + parameters = {@Parameter(k = "appName", v = instanceName)}, group = webAppDeployGroup, perSuite = true) +public class AzureWebAppUndeployedSecondTest { + + @AzureWebApplication(name = instanceName, group = webAppDeployGroup) + Hostname hostname; + + @Test + public void test() throws IOException { + OkHttpClient client = new OkHttpClient(); + + Request request = new Request.Builder() + .url("http://" + hostname.get()) + .method("GET", null) + .build(); + Response response = client.newCall(request).execute(); + Assertions.assertThat(response.body().string()).isNotEqualTo("Hello World"); + } +} diff --git a/clouds/clouds-azure/src/test/resources/azure/armTemplates/eapWebApp.json b/clouds/clouds-azure/src/test/resources/azure/armTemplates/eapWebApp.json new file mode 100644 index 00000000..5d648d09 --- /dev/null +++ b/clouds/clouds-azure/src/test/resources/azure/armTemplates/eapWebApp.json @@ -0,0 +1,103 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appName": { + "type": "string", + "defaultValue": "eapAppService" + }, + "linuxFxVersion": { + "type": "string", + "defaultValue": "JBOSSEAP|7-java11" + } + }, + "variables": { + "planName": "[concat(parameters('appName'),'-plan')]", + "vnetName": "[concat(parameters('appName'),'-vnet')]" + }, + "resources": [ + { + "apiVersion": "2018-11-01", + "name": "[variables('planName')]", + "type": "Microsoft.Web/serverfarms", + "location": "[resourceGroup().location]", + "kind": "linux", + "tags": null, + "properties": { + "name": "[variables('planName')]", + "workerSize": "1", + "workerSizeId": "1", + "numberOfWorkers": "1", + "reserved": true + }, + "sku": { + "Tier": "PremiumV3", + "Name": "P1V3" + } + }, + { + "name": "[variables('vnetName')]", + "type": "Microsoft.Network/VirtualNetworks", + "apiVersion": "2021-01-01", + "location": "[resourceGroup().location]", + "extendedLocation": null, + "dependsOn": [], + "tags": {}, + "properties": { + "addressSpace": { + "addressPrefixes": [ + "10.1.0.0/16" + ] + }, + "subnets": [ + { + "name": "jbossnodes", + "properties": { + "addressPrefix": "10.1.0.0/24", + "serviceEndpoints": [ + { + "service": "Microsoft.Web" + } + ], + "delegations": [ + { + "name": "delegation", + "properties": { + "serviceName": "Microsoft.Web/serverfarms" + } + } + ], + "privateEndpointNetworkPolicies": "Enabled", + "privateLinkServiceNetworkPolicies": "Enabled" + } + } + ], + "enableDdosProtection": "false" + } + }, + { + "apiVersion": "2018-11-01", + "name": "[parameters('appName')]", + "type": "Microsoft.Web/sites", + "location": "[resourceGroup().location]", + "tags": null, + "dependsOn": [ + "[concat('Microsoft.Web/serverfarms/', variables('planName'))]", + "[concat('Microsoft.Network/VirtualNetworks/', variables('vnetName'))]" + ], + "properties": { + "name": "[parameters('appName')]", + "siteConfig": { + "appCommandLine": "/home/site/deployments/tools/startup_script.sh", + "vnetRouteAllEnabled": true, + "appSettings": [], + "linuxFxVersion": "[parameters('linuxFxVersion')]", + "alwaysOn": "true" + }, + "serverFarmId": "[extensionResourceId(resourceGroup().Id , 'Microsoft.Web/serverfarms', variables('planName'))]", + "clientAffinityEnabled": false, + "virtualNetworkSubnetId": "[concat(extensionResourceId(resourceGroup().Id , 'Microsoft.Network/VirtualNetworks', variables('vnetName')), '/subnets/jbossnodes')]" + } + } + ] +} \ No newline at end of file diff --git a/clouds/clouds-core/src/main/java/sunstone/api/Deployment.java b/clouds/clouds-core/src/main/java/sunstone/api/Deployment.java new file mode 100644 index 00000000..5e65aed2 --- /dev/null +++ b/clouds/clouds-core/src/main/java/sunstone/api/Deployment.java @@ -0,0 +1,27 @@ +package sunstone.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotate method to automate WildFly deploy operation. + *
+ * Deployment operation is done before static resources are injected. + *
+ * The method also needs an annotation annotated with {@link SunstoneArchiveDeployTargetAnotation} that marks an annotation + * which is used to identify Cloud resource, i.e. virtual machine. Those annotations bring modules like sunstone-clouds-azure. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Inherited +public @interface Deployment { + /** + * Name of the deployment. Some resources may not support the name and will ignore it. + *
+ * For example Azure App services - the way Azure platform works, it is always deployed as ROOT.war + */ + String name() default ""; +} diff --git a/clouds/clouds-core/src/main/java/sunstone/api/SunstoneArchiveDeployTargetAnotation.java b/clouds/clouds-core/src/main/java/sunstone/api/SunstoneArchiveDeployTargetAnotation.java new file mode 100644 index 00000000..20dc71e2 --- /dev/null +++ b/clouds/clouds-core/src/main/java/sunstone/api/SunstoneArchiveDeployTargetAnotation.java @@ -0,0 +1,11 @@ +package sunstone.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.ANNOTATION_TYPE) +public @interface SunstoneArchiveDeployTargetAnotation { +} diff --git a/clouds/clouds-core/src/main/java/sunstone/core/SunstoneExtension.java b/clouds/clouds-core/src/main/java/sunstone/core/SunstoneExtension.java index 046bf92b..f75ea5a7 100644 --- a/clouds/clouds-core/src/main/java/sunstone/core/SunstoneExtension.java +++ b/clouds/clouds-core/src/main/java/sunstone/core/SunstoneExtension.java @@ -1,23 +1,41 @@ package sunstone.core; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.exporter.ZipExporter; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.TestInstancePostProcessor; +import org.junit.platform.commons.support.AnnotationSupport; +import org.junit.platform.commons.support.HierarchyTraversalMode; +import org.wildfly.extras.sunstone.api.impl.ObjectProperties; import sunstone.api.SunstoneInjectionAnnotation; +import sunstone.api.SunstoneArchiveDeployTargetAnotation; +import sunstone.api.Deployment; import sunstone.api.WithAwsCfTemplate; import sunstone.api.WithAzureArmTemplate; +import sunstone.core.api.SunstoneArchiveDeployer; import sunstone.core.api.SunstoneCloudDeployer; import sunstone.core.api.SunstoneResourceInjector; import sunstone.core.exceptions.IllegalArgumentSunstoneException; import sunstone.core.exceptions.SunstoneException; +import sunstone.core.exceptions.UnsupportedSunstoneOperationException; +import sunstone.core.spi.SunstoneArchiveDeployerProvider; import sunstone.core.spi.SunstoneCloudDeployerProvider; import sunstone.core.spi.SunstoneResourceInjectorProvider; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -38,6 +56,9 @@ * a resource that needs to be cleaned/closed is also responsible for registering it. */ public class SunstoneExtension implements BeforeAllCallback, AfterAllCallback, TestInstancePostProcessor { + static ServiceLoader sunstoneArchiveDeployerProviderLoader = ServiceLoader.load(SunstoneArchiveDeployerProvider.class); + static ServiceLoader sunstoneCloudDeployerProviderLoader = ServiceLoader.load(SunstoneCloudDeployerProvider.class); + static ServiceLoader sunstoneResourceInjectorProviderLoader = ServiceLoader.load(SunstoneResourceInjectorProvider.class); @Override public void beforeAll(ExtensionContext ctx) throws Exception { @@ -48,6 +69,7 @@ public void beforeAll(ExtensionContext ctx) throws Exception { if (ctx.getRequiredTestClass().getAnnotationsByType(WithAwsCfTemplate.class).length > 0) { handleAwsCloudFormationAnnotations(ctx); } + performDeploymentOperation(ctx); injectStaticResources(ctx, ctx.getRequiredTestClass()); } @@ -68,8 +90,7 @@ protected static void handleAzureArmTemplateAnnotations(ExtensionContext ctx) { } static Optional getDeployer(Class annotation) { - ServiceLoader loader = ServiceLoader.load(SunstoneCloudDeployerProvider.class); - for (SunstoneCloudDeployerProvider sunstoneCloudDeployerProvider : loader) { + for (SunstoneCloudDeployerProvider sunstoneCloudDeployerProvider : sunstoneCloudDeployerProviderLoader) { Optional deployer = sunstoneCloudDeployerProvider.create(annotation); if (deployer.isPresent()) { return deployer; @@ -79,8 +100,7 @@ static Optional getDeployer(Class< } static Optional getSunstoneResourceInjector(Field field) { - ServiceLoader loader = ServiceLoader.load(SunstoneResourceInjectorProvider.class); - for (SunstoneResourceInjectorProvider provider : loader) { + for (SunstoneResourceInjectorProvider provider : sunstoneResourceInjectorProviderLoader) { Optional injector = provider.create(field); if (injector.isPresent()) { return injector; @@ -97,6 +117,9 @@ static Optional getAndCheckInjectionAnnotation(Field field) throws I throw new IllegalArgumentSunstoneException(format("More than one annotation (in)direrectly annotated by %s found on %s %s in %s class", SunstoneInjectionAnnotation.class, field.getType().getName(), field.getName(), field.getDeclaringClass())); } + if (injectionAnnotations.isEmpty()) { + return Optional.empty(); + } return Optional.ofNullable(injectionAnnotations.get(0)); } static List getAllFieldsList(final Class cls, Predicate filter) { @@ -130,6 +153,67 @@ static void handleInjection(ExtensionContext ctx, Object instance, Field field) } } + static Optional getArchiveDeployer(Annotation annotation) { + for (SunstoneArchiveDeployerProvider sunstoneCloudDeployerProvider : sunstoneArchiveDeployerProviderLoader) { + Optional deployer = sunstoneCloudDeployerProvider.create(annotation); + if (deployer.isPresent()) { + return deployer; + } + } + return Optional.empty(); + } + static void performDeploymentOperation(ExtensionContext ctx) throws SunstoneException { + List annotatedMethods = AnnotationSupport.findAnnotatedMethods(ctx.getRequiredTestClass(), Deployment.class, HierarchyTraversalMode.TOP_DOWN); + + try { + for (Method method : annotatedMethods) { + if (!Modifier.isStatic(method.getModifiers())) { + throw new IllegalArgumentSunstoneException("Deployment method must be static"); + } + if (method.getParameterCount() != 0) { + throw new IllegalArgumentSunstoneException("Deployment method must have 0 parameters"); + } + Deployment annotation = method.getAnnotation(Deployment.class); + String deploymentName = ObjectProperties.replaceSystemProperties(annotation.name()); + + method.setAccessible(true); + Object invoke = method.invoke(null); + if (invoke == null) { + throw new RuntimeException(format("%s in %s returned null", method.getName(), method.getDeclaringClass().getName())); + } + InputStream is; + if (invoke instanceof Archive) { + is = ((Archive) invoke).as(ZipExporter.class).exportAsInputStream(); + } else if (invoke instanceof File) { + is = new FileInputStream((File) invoke); + } else if (invoke instanceof Path) { + is = new FileInputStream(((Path) invoke).toFile()); + } else if (invoke instanceof InputStream) { + is = (InputStream) invoke; + } else { + throw new UnsupportedSunstoneOperationException("Unsupported type " + method.getName()); + } + for (Annotation ann : AnnotationUtils.findAnnotationsAnnotatedBy(method.getAnnotations(), SunstoneArchiveDeployTargetAnotation.class)) { + Optional archiveDeployer = getArchiveDeployer(ann); + archiveDeployer.orElseThrow(() -> new SunstoneException("todo")); + archiveDeployer.get().deployAndRegisterUndeploy(deploymentName, ann, is, ctx); + } + + + is.close(); + + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + static void injectInstanceResources(ExtensionContext ctx, Object instance) { for (Field field : getAllFieldsList(instance.getClass(), field -> !Modifier.isStatic(field.getModifiers()))) { handleInjection(ctx, instance, field); diff --git a/clouds/clouds-core/src/main/java/sunstone/core/api/SunstoneArchiveDeployer.java b/clouds/clouds-core/src/main/java/sunstone/core/api/SunstoneArchiveDeployer.java new file mode 100644 index 00000000..6ab536c6 --- /dev/null +++ b/clouds/clouds-core/src/main/java/sunstone/core/api/SunstoneArchiveDeployer.java @@ -0,0 +1,14 @@ +package sunstone.core.api; + +import org.junit.jupiter.api.extension.ExtensionContext; +import sunstone.core.exceptions.SunstoneException; + +import java.io.InputStream; +import java.lang.annotation.Annotation; + +/** + * + */ +public interface SunstoneArchiveDeployer { + void deployAndRegisterUndeploy(String deploymentName, Annotation targetAnnotation, InputStream deployment, ExtensionContext ctx) throws SunstoneException; +} diff --git a/clouds/clouds-core/src/main/java/sunstone/core/spi/SunstoneArchiveDeployerProvider.java b/clouds/clouds-core/src/main/java/sunstone/core/spi/SunstoneArchiveDeployerProvider.java new file mode 100644 index 00000000..e3d6c867 --- /dev/null +++ b/clouds/clouds-core/src/main/java/sunstone/core/spi/SunstoneArchiveDeployerProvider.java @@ -0,0 +1,11 @@ +package sunstone.core.spi; + + +import sunstone.core.api.SunstoneArchiveDeployer; + +import java.lang.annotation.Annotation; +import java.util.Optional; + +public interface SunstoneArchiveDeployerProvider { + Optional create(Annotation annotation); +} diff --git a/clouds/clouds-core/src/test/java/sunstone/core/archiveDeploy/AbstractArchiveDeployTest.java b/clouds/clouds-core/src/test/java/sunstone/core/archiveDeploy/AbstractArchiveDeployTest.java new file mode 100644 index 00000000..40951f2c --- /dev/null +++ b/clouds/clouds-core/src/test/java/sunstone/core/archiveDeploy/AbstractArchiveDeployTest.java @@ -0,0 +1,47 @@ +package sunstone.core.archiveDeploy; + + +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.extension.ExtendWith; +import sunstone.api.Deployment; +import sunstone.core.SunstoneExtension; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; + +@ExtendWith(SunstoneExtension.class) +public abstract class AbstractArchiveDeployTest { + + @Deployment + @DirectlyAnnotatedArchiveDeployTarget + @IndirectlyAnnotatedSunstoneArchiveDeployTarget + static File deployFileAbstract() throws IOException { + return File.createTempFile("sunstne-test-file", ""); + } + + @Deployment + @DirectlyAnnotatedArchiveDeployTarget + @IndirectlyAnnotatedSunstoneArchiveDeployTarget + static Path deployPathAbstract() throws IOException { + return (File.createTempFile("sunstne-test-file", "")).toPath(); + } + + @Deployment + @DirectlyAnnotatedArchiveDeployTarget + @IndirectlyAnnotatedSunstoneArchiveDeployTarget + static Archive deployArchiveAbstract() { + return ShrinkWrap.create(JavaArchive.class) + .addClass(AbstractArchiveDeployTest.class); + } + + @Deployment + @DirectlyAnnotatedArchiveDeployTarget + @IndirectlyAnnotatedSunstoneArchiveDeployTarget + static InputStream deployInpuStreamAbstract() { + return InputStream.nullInputStream(); + } +} diff --git a/clouds/clouds-core/src/test/java/sunstone/core/archiveDeploy/ArchiveDeployTest.java b/clouds/clouds-core/src/test/java/sunstone/core/archiveDeploy/ArchiveDeployTest.java new file mode 100644 index 00000000..e0f3fa4e --- /dev/null +++ b/clouds/clouds-core/src/test/java/sunstone/core/archiveDeploy/ArchiveDeployTest.java @@ -0,0 +1,62 @@ +package sunstone.core.archiveDeploy; + + +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import sunstone.api.Deployment; +import sunstone.core.di.TestSunstoneResourceInjector; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ArchiveDeployTest extends AbstractArchiveDeployTest { + + @Deployment + @DirectlyAnnotatedArchiveDeployTarget + @IndirectlyAnnotatedSunstoneArchiveDeployTarget + static File deployFile() throws IOException { + return File.createTempFile("sunstne-test-file", ""); + } + + @Deployment + @DirectlyAnnotatedArchiveDeployTarget + @IndirectlyAnnotatedSunstoneArchiveDeployTarget + static Path deployPath() throws IOException { + return (File.createTempFile("sunstne-test-file", "")).toPath(); + } + + @Deployment + @DirectlyAnnotatedArchiveDeployTarget + @IndirectlyAnnotatedSunstoneArchiveDeployTarget + static Archive deployArchive() { + return ShrinkWrap.create(JavaArchive.class) + .addClass(ArchiveDeployTest.class); + } + + @Deployment + @DirectlyAnnotatedArchiveDeployTarget + @IndirectlyAnnotatedSunstoneArchiveDeployTarget + static InputStream deployInpuStream() { + return InputStream.nullInputStream(); + } + + + @AfterAll + public static void reset() { + TestSunstoneResourceInjector.reset(); + } + + @Test + public void test() { + assertThat(TestSunstoneArchiveDeployer.called).isTrue(); + assertThat(TestSunstoneArchiveDeployer.counter).isEqualTo(16); + + } +} diff --git a/clouds/clouds-core/src/test/java/sunstone/core/archiveDeploy/CustomTargetAnnotation.java b/clouds/clouds-core/src/test/java/sunstone/core/archiveDeploy/CustomTargetAnnotation.java new file mode 100644 index 00000000..df9a9086 --- /dev/null +++ b/clouds/clouds-core/src/test/java/sunstone/core/archiveDeploy/CustomTargetAnnotation.java @@ -0,0 +1,14 @@ +package sunstone.core.archiveDeploy; + +import sunstone.api.SunstoneArchiveDeployTargetAnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.ANNOTATION_TYPE) +@Retention(RetentionPolicy.RUNTIME) +@SunstoneArchiveDeployTargetAnotation +public @interface CustomTargetAnnotation { +} diff --git a/clouds/clouds-core/src/test/java/sunstone/core/archiveDeploy/DirectlyAnnotatedArchiveDeployTarget.java b/clouds/clouds-core/src/test/java/sunstone/core/archiveDeploy/DirectlyAnnotatedArchiveDeployTarget.java new file mode 100644 index 00000000..f22a8742 --- /dev/null +++ b/clouds/clouds-core/src/test/java/sunstone/core/archiveDeploy/DirectlyAnnotatedArchiveDeployTarget.java @@ -0,0 +1,14 @@ +package sunstone.core.archiveDeploy; + +import sunstone.api.SunstoneArchiveDeployTargetAnotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@SunstoneArchiveDeployTargetAnotation +public @interface DirectlyAnnotatedArchiveDeployTarget { +} diff --git a/clouds/clouds-core/src/test/java/sunstone/core/archiveDeploy/IndirectlyAnnotatedSunstoneArchiveDeployTarget.java b/clouds/clouds-core/src/test/java/sunstone/core/archiveDeploy/IndirectlyAnnotatedSunstoneArchiveDeployTarget.java new file mode 100644 index 00000000..81658dd5 --- /dev/null +++ b/clouds/clouds-core/src/test/java/sunstone/core/archiveDeploy/IndirectlyAnnotatedSunstoneArchiveDeployTarget.java @@ -0,0 +1,13 @@ +package sunstone.core.archiveDeploy; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@CustomTargetAnnotation +public @interface IndirectlyAnnotatedSunstoneArchiveDeployTarget { +} diff --git a/clouds/clouds-core/src/test/java/sunstone/core/archiveDeploy/TestSunstoneArchiveDeployer.java b/clouds/clouds-core/src/test/java/sunstone/core/archiveDeploy/TestSunstoneArchiveDeployer.java new file mode 100644 index 00000000..cea65727 --- /dev/null +++ b/clouds/clouds-core/src/test/java/sunstone/core/archiveDeploy/TestSunstoneArchiveDeployer.java @@ -0,0 +1,26 @@ +package sunstone.core.archiveDeploy; + + +import org.junit.jupiter.api.extension.ExtensionContext; +import sunstone.core.api.SunstoneArchiveDeployer; +import sunstone.core.exceptions.SunstoneException; + +import java.io.InputStream; +import java.lang.annotation.Annotation; + + +public class TestSunstoneArchiveDeployer implements SunstoneArchiveDeployer { + static boolean called; + static int counter = 0; + + public static void reset() { + called = false; + counter = 0; + } + + @Override + public void deployAndRegisterUndeploy(String deploymentName, Annotation targetAnnotation, InputStream deployment, ExtensionContext ctx) throws SunstoneException { + called = true; + counter++; + } +} diff --git a/clouds/clouds-core/src/test/java/sunstone/core/archiveDeploy/TestSunstoneArchiveDeployerProvider.java b/clouds/clouds-core/src/test/java/sunstone/core/archiveDeploy/TestSunstoneArchiveDeployerProvider.java new file mode 100644 index 00000000..ab0c10bc --- /dev/null +++ b/clouds/clouds-core/src/test/java/sunstone/core/archiveDeploy/TestSunstoneArchiveDeployerProvider.java @@ -0,0 +1,15 @@ +package sunstone.core.archiveDeploy; + + +import sunstone.core.api.SunstoneArchiveDeployer; +import sunstone.core.spi.SunstoneArchiveDeployerProvider; + +import java.lang.annotation.Annotation; +import java.util.Optional; + +public class TestSunstoneArchiveDeployerProvider implements SunstoneArchiveDeployerProvider { + @Override + public Optional create(Annotation field) { + return Optional.of(new TestSunstoneArchiveDeployer()); + } +} diff --git a/clouds/clouds-core/src/test/resources/META-INF/services/sunstone.core.spi.SunstoneArchiveDeployerProvider b/clouds/clouds-core/src/test/resources/META-INF/services/sunstone.core.spi.SunstoneArchiveDeployerProvider new file mode 100644 index 00000000..044169b6 --- /dev/null +++ b/clouds/clouds-core/src/test/resources/META-INF/services/sunstone.core.spi.SunstoneArchiveDeployerProvider @@ -0,0 +1 @@ +sunstone.core.archiveDeploy.TestSunstoneArchiveDeployerProvider \ No newline at end of file diff --git a/clouds/pom.xml b/clouds/pom.xml index 480cb0c3..a75ef410 100644 --- a/clouds/pom.xml +++ b/clouds/pom.xml @@ -44,6 +44,7 @@ 2.17.174 2.9.0 + 3.9.0 3.23.1 3.0.0-M7 @@ -88,6 +89,11 @@ + + commons-net + commons-net + ${version.commons-net} + software.amazon.awssdk