* 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
")
+ && 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:
+ *
+ *
+ *
+ */
+@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}
*
+ *
+ * Supported Azure identification annotations
+ * notes
+ *
+ *
+ *
+ * {@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
+ *
+ *
+ *
* 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