Skip to content

Commit

Permalink
Merge pull request #122 from istraka/junit5_pr_wfdeploy
Browse files Browse the repository at this point in the history
[native clouds] WildFly deploy operation support for Azure
  • Loading branch information
istraka authored Jan 23, 2023
2 parents ba91d06 + 1cd1ef8 commit 7bb6ffa
Show file tree
Hide file tree
Showing 34 changed files with 1,111 additions and 68 deletions.
6 changes: 6 additions & 0 deletions clouds/clouds-azure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
</dependency>


<!-- Azure-->
<dependency>
<groupId>com.azure.resourcemanager</groupId>
Expand Down
118 changes: 118 additions & 0 deletions clouds/clouds-azure/src/main/java/azure/core/AzureArchiveDeployer.java
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
Original file line number Diff line number Diff line change
@@ -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<SunstoneArchiveDeployer> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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> T get(Annotation injectionAnnotation, AzureSunstoneStore store, Class<T> clazz) throws SunstoneException {
Expand Down Expand Up @@ -99,6 +103,10 @@ boolean isTypeSupportedForInject(Class<?> type) {
return Arrays.stream(supportedTypesForInjection).anyMatch(clazz -> clazz.isAssignableFrom(type));
}
@Override
boolean deployToWildFlySupported() {
return true;
}
@Override
<T> T get(Annotation injectionAnnotation, AzureSunstoneStore store, Class<T> clazz) throws SunstoneException {
if(!getRepresentedInjectionAnnotation().isAssignableFrom(injectionAnnotation.annotationType())) {
throw new IllegalArgumentSunstoneException(format("Expected %s annotation type but got %s",
Expand All @@ -107,7 +115,7 @@ <T> T get(Annotation injectionAnnotation, AzureSunstoneStore store, Class<T> cla
AzureWebApplication webApp = (AzureWebApplication) injectionAnnotation;
String appName = replaceSystemProperties(webApp.name());
String appGroup = webApp.group().isEmpty() ? objectProperties.getProperty(AzureConfig.GROUP) : replaceSystemProperties(webApp.group());
Optional<WebAppBasic> azureWebApp = AzureUtils.findAzureWebApp(store.getAzureArmClientOrCreate(), appName, appGroup);
Optional<WebApp> 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))));
}
};
Expand Down Expand Up @@ -135,6 +143,9 @@ public String toString() {
boolean isTypeSupportedForInject(Class<?> type) {
return false;
}
boolean deployToWildFlySupported() {
return false;
}
<T> T get(Annotation injectionAnnotation, AzureSunstoneStore store, Class<T> clazz) throws SunstoneException {
throw new UnsupportedSunstoneOperationException(format("%s annotation is nto supported for the type %s",
injectionAnnotation.annotationType().getName(), this.toString()));
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
* <p>
* 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.
Expand All @@ -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");
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;


Expand All @@ -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())
Expand All @@ -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;
Expand Down
Loading

0 comments on commit 7bb6ffa

Please sign in to comment.