Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Generate native binary from java code #2302

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public abstract class AbstractFunctionMojo extends AbstractAppServiceMojo {
private static final String FUNCTION_REGION_KEY = "region";
private static final String FUNCTION_PRICING_KEY = "pricingTier";
private static final String FUNCTION_DEPLOY_TO_SLOT_KEY = "isDeployToFunctionSlot";
private static final String FUNCTION_NATIVE_EXECUTABLE_PATH = "nativeExecutablePath";
private static final String FUNCTION_IS_NATIVE = "isNativeFunction";

//region Properties
@Parameter(defaultValue = "${project.build.finalName}", readonly = true, required = true)
Expand Down Expand Up @@ -214,6 +216,24 @@ public abstract class AbstractFunctionMojo extends AbstractAppServiceMojo {
@Parameter
protected String storageAccountResourceGroup;

/**
* Boolean flag to run function with custom runtime instead of java
*
* @since 1.27.0
*/
@Getter
@Parameter(property = "functions.nativeExecutablePath")
protected String nativeExecutablePath;

/**
* Args to be used by custom handler.
*
* @since 1.27.0
*/
@Getter
@Parameter(property = "functions.customHandlerArgs", defaultValue = "")
protected String customHandlerArgs;

@Getter
protected final ConfigParser parser = new ConfigParser(this);

Expand Down Expand Up @@ -271,6 +291,10 @@ protected File getArtifact() throws AzureToolkitRuntimeException {
return result;
}

protected boolean isNativeExecutable() {
return StringUtils.isNotEmpty(getNativeExecutablePath());
}

protected File getHostJsonFile() {
final Path path = Paths.get(getHostJson());
return path.isAbsolute() ? path.toFile() :
Expand Down Expand Up @@ -355,6 +379,7 @@ public Map<String, String> getTelemetryProperties() {
result.put(DISABLE_APP_INSIGHTS_KEY, String.valueOf(isDisableAppInsights()));
final boolean isDeployToFunctionSlot = getDeploymentSlotSetting() != null && StringUtils.isNotEmpty(getDeploymentSlotSetting().getName());
result.put(FUNCTION_DEPLOY_TO_SLOT_KEY, String.valueOf(isDeployToFunctionSlot));
result.put(FUNCTION_IS_NATIVE, String.valueOf(isNativeExecutable()));
return result;
}
//endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public FunctionAppConfig parseConfig() throws AzureExecutionException {
.appInsightsInstance(mojo.getAppInsightsInstance())
.storageAccountName(mojo.getStorageAccountName())
.storageAccountResourceGroup(StringUtils.firstNonBlank(mojo.getStorageAccountResourceGroup(), mojo.getResourceGroup()))
.nativeExecutablePath(mojo.getNativeExecutablePath())
.customHandlerArgs(mojo.getCustomHandlerArgs())
.subscriptionId(mojo.getSubscriptionId())
.resourceGroup(mojo.getResourceGroup())
.appName(mojo.getAppName())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.microsoft.azure.toolkit.lib.legacy.function.handlers.CommandHandlerImpl;
import com.microsoft.azure.toolkit.lib.legacy.function.handlers.FunctionCoreToolsHandler;
import com.microsoft.azure.toolkit.lib.legacy.function.handlers.FunctionCoreToolsHandlerImpl;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.SetUtils;
import org.apache.commons.io.FileUtils;
Expand Down Expand Up @@ -61,6 +62,7 @@
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Paths;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand Down Expand Up @@ -95,6 +97,7 @@ public class PackageMojo extends AbstractFunctionMojo {
public static final String SAVE_FUNCTION_JSON = "Starting processing function: ";
public static final String SAVE_SUCCESS = "Successfully saved to ";
public static final String COPY_JARS = "Step 7 of 8: Copying JARs to staging directory ";
public static final String COPY_BINARY = "Step 7 of 8: Copying Binary to staging directory ";
public static final String COPY_SUCCESS = "Copied successfully.";
public static final String INSTALL_EXTENSIONS = "Step 8 of 8: Installing function extensions if needed";
public static final String SKIP_INSTALL_EXTENSIONS_HTTP = "Skip install Function extension for HTTP Trigger Functions";
Expand All @@ -104,10 +107,12 @@ public class PackageMojo extends AbstractFunctionMojo {
public static final String FUNCTION_JSON = "function.json";
public static final String EXTENSION_BUNDLE = "extensionBundle";
private static final String AZURE_FUNCTIONS_JAVA_CORE_LIBRARY = "azure-functions-java-core-library";
private static final String DEFAULT_LOCAL_SETTINGS_JSON = "{ \"IsEncrypted\": false, \"Values\": " +
"{ \"FUNCTIONS_WORKER_RUNTIME\": \"java\" } }";
private static final String DEFAULT_LOCAL_SETTINGS_JSON = "'{' \"IsEncrypted\": false, \"Values\": " +
"'{' \"FUNCTIONS_WORKER_RUNTIME\": \"{0}\" '}' '}'";
private static final String DEFAULT_HOST_JSON = "{\"version\":\"2.0\",\"extensionBundle\":" +
"{\"id\":\"Microsoft.Azure.Functions.ExtensionBundle\",\"version\":\"[4.*, 5.0.0)\"}}\n";
private static final String DEFAULT_CUSTOM_HOST_JSON = "'{'\"version\":\"2.0\",\"extensionBundle\":" +
"'{'\"id\":\"Microsoft.Azure.Functions.ExtensionBundle\",\"version\":\"[4.*, 5.0.0)\"'}',\"customHandler\": '{'\"description\": '{'\"defaultExecutablePath\": \"{0}\",\"workingDirectory\": \"\",\"arguments\": [\"{1}\"]'}',\"enableForwardingHttpRequest\": true'}}'\n";

private static final BindingEnum[] FUNCTION_WITHOUT_FUNCTION_EXTENSION = {BindingEnum.HttpOutput, BindingEnum.HttpTrigger};
private static final String EXTENSION_BUNDLE_ID = "Microsoft.Azure.Functions.ExtensionBundle";
Expand Down Expand Up @@ -170,7 +175,12 @@ protected void doExecute() throws AzureExecutionException {

writeFunctionJsonFiles(objectWriter, configMap);

copyJarsToStageDirectory();
if (isNativeExecutable()) {
copyBinaryToStageDirectory();
} else {
copyJarsToStageDirectory();
}

} catch (IOException | MojoExecutionException e) {
throw new AzureExecutionException("Cannot perform IO operations due to error:" + e.getMessage(), e);
}
Expand All @@ -184,6 +194,21 @@ protected void doExecute() throws AzureExecutionException {
log.info(BUILD_SUCCESS);
}

protected void copyBinaryToStageDirectory() throws IOException, MojoExecutionException {
final File stagingDirectory = new File(getDeploymentStagingDirectoryPath());
log.info("");
log.info(COPY_BINARY + stagingDirectory.getAbsolutePath());
final File originalArtifact = new File(getArtifact().getParentFile(), nativeExecutablePath);

File destFile = new File(stagingDirectory, nativeExecutablePath);
log.info("src: {}", originalArtifact.getAbsolutePath());
log.info("dest: {}", destFile.getAbsolutePath());

FileUtils.copyFile(originalArtifact, destFile);

log.info(COPY_SUCCESS);
}

public static void buildArtifactWithDependencies(@Nonnull final File artifactFile, @Nullable final Set<File> dependencies, final File target) {
AzureMessager.getMessager().info("Building artifact with dependencies...");
final Shader shader = new DefaultShader();
Expand Down Expand Up @@ -316,6 +341,7 @@ protected void writeFunctionJsonFiles(final ObjectWriter objectWriter,
final Map<String, FunctionConfiguration> configMap) throws IOException {
log.info("");
log.info(SAVE_FUNCTION_JSONS);
log.info("Native: "+isNativeExecutable());
if (configMap.size() == 0) {
log.info(SAVE_SKIP);
} else {
Expand All @@ -330,6 +356,11 @@ protected void writeFunctionJsonFile(final ObjectWriter objectWriter, final Stri
log.info(SAVE_FUNCTION_JSON + functionName);
final File functionJsonFile = Paths.get(getDeploymentStagingDirectoryPath(),
functionName, FUNCTION_JSON).toFile();

if (isNativeExecutable()) {
config.setEntryPoint(null);
config.setScriptFile(null);
}
writeObjectToFile(objectWriter, config, functionJsonFile);
log.info(SAVE_SUCCESS + functionJsonFile.getAbsolutePath());
}
Expand All @@ -339,7 +370,14 @@ protected void copyHostJson() throws IOException {
log.info(SAVING_HOST_JSON);
final File sourceHostJsonFile = getHostJsonFile();
final File destHostJsonFile = Paths.get(getDeploymentStagingDirectoryPath(), HOST_JSON).toFile();
copyFilesWithDefaultContent(sourceHostJsonFile, destHostJsonFile, DEFAULT_HOST_JSON);
if (isNativeExecutable()) {
File nativeFile = new File(getNativeExecutablePath());
String newDefaultContent = MessageFormat.format(DEFAULT_CUSTOM_HOST_JSON, nativeFile.getName(), getCustomHandlerArgs());
copyFilesWithDefaultContent(sourceHostJsonFile, destHostJsonFile, newDefaultContent);
} else {
copyFilesWithDefaultContent(sourceHostJsonFile, destHostJsonFile, DEFAULT_HOST_JSON);
}

log.info(SAVE_SUCCESS + destHostJsonFile.getAbsolutePath());
}

Expand All @@ -348,10 +386,15 @@ protected void copyLocalSettingsJson() throws IOException {
log.info(SAVING_LOCAL_SETTINGS_JSON);
final File sourceLocalSettingsJsonFile = getLocalSettingsJsonFile();
final File destLocalSettingsJsonFile = Paths.get(getDeploymentStagingDirectoryPath(), LOCAL_SETTINGS_JSON).toFile();
copyFilesWithDefaultContent(sourceLocalSettingsJsonFile, destLocalSettingsJsonFile, DEFAULT_LOCAL_SETTINGS_JSON);
String defaultContent = MessageFormat.format(DEFAULT_LOCAL_SETTINGS_JSON, getWorkerRuntime());
Copy link
Member

@Flanker32 Flanker32 May 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to overwrite users configuration for native artifact? As default content will not work if users have a local.settings.json in their workspace. Same for the host.json

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have one newbie question: local.settins.json is enough to test locally (without host.json)?
I think the plugin should not overwrite any json files created by the user.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you can test locally without the host.json file.
local.settings.json is used for local development and will not be published to Azure. [On the other hand, host.json is used for configuring the Functions host process when you run projects locally and in Azure, for log level for instance]

https://learn.microsoft.com/en-us/azure/azure-functions/functions-develop-local

Copy link
Member

@Flanker32 Flanker32 Jun 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@viniciusfcf @SandraAhlgrimm Thank you all for your help. Just want to confirm whether customHandler configuration is required in host.json? I just tried to remove following configuration and func seems broken.

If its required, do we need to add validation throw exception if user did not set it in host.json? Besides, I'm not sure whether we should provide default configuration as the following configuration should only works for quarkus, or is there any way we could detect whether the binary is a quarkus one or spring boot?

    "customHandler": {
        "description": {
            "defaultExecutablePath": "quarkus-azure-functions-http-1.0.0-SNAPSHOT",
            "workingDirectory": "",
            "arguments": [
                "-Dquarkus.http.port=${FUNCTIONS_CUSTOMHANDLER_PORT}"
            ]
        },
        "enableForwardingHttpRequest": true
    }

copyFilesWithDefaultContent(sourceLocalSettingsJsonFile, destLocalSettingsJsonFile, defaultContent);
log.info(SAVE_SUCCESS + destLocalSettingsJsonFile.getAbsolutePath());
}

private String getWorkerRuntime() {
return isNativeExecutable()? "custom": "java";
}

private static void copyFilesWithDefaultContent(File source, File dest, String defaultContent)
throws IOException {
if (source != null && source.exists()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public class FunctionAppConfig extends AppServiceConfig {
private boolean disableAppInsights;
private String storageAccountName;
private String storageAccountResourceGroup;
private String nativeExecutablePath;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove these new parameters as FunctionAppConfig is used to create resource in Azure, which should not contains native binary configurations

private String customHandlerArgs;
private LogAnalyticsWorkspaceConfig workspaceConfig;
private FlexConsumptionConfiguration flexConsumptionConfiguration;
}