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

Miscellaneous updates based on feedback #10

Merged
merged 11 commits into from
Jan 30, 2023
Merged
253 changes: 161 additions & 92 deletions README.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
"loc.input.label.acrName": "Azure Container Registry name",
"loc.input.help.acrName": "The name of the Azure Container Registry that the runnable application image will be pushed to.",
"loc.input.label.acrUsername": "Azure Container Registry username",
"loc.input.help.acrUsername": "The username used to authenticate push requests to the provided Azure Contrainer Registry. If not provided, an access token will be generated via 'az acr login' and provided to 'docker login' to authenticate the requests.",
"loc.input.help.acrUsername": "The username used to authenticate push requests to the provided Azure Container Registry. If not provided, an access token will be generated via 'az acr login' and provided to 'docker login' to authenticate the requests.",
"loc.input.label.acrPassword": "Azure Container Registry password",
"loc.input.help.acrPassword": "The password used to authenticate push requests to the provided Azure Contrainer Registry. If not provided, an access token will be generated via 'az acr login' and provided to 'docker login' to authenticate the requests.",
"loc.input.help.acrPassword": "The password used to authenticate push requests to the provided Azure Container Registry. If not provided, an access token will be generated via 'az acr login' and provided to 'docker login' to authenticate the requests.",
"loc.input.label.dockerfilePath": "Dockerfile path",
"loc.input.help.dockerfilePath": "Relative path to the Dockerfile in the provided application source that should be used to build the image that is then pushed to ACR and deployed to the Container App. If not provided, this task will check if there is a file named 'Dockerfile' at the root of the provided application source and use that to build the image. Otherwise, the Oryx++ Builder will be used to create the image.",
"loc.input.label.imageToBuild": "Docker image to build",
Expand All @@ -32,6 +32,10 @@
"loc.input.help.runtimeStack": "The platform version stack that the application runs in when deployed to the Azure Container App. This should be provided in the form of <platform>:<version>. If not provided, this value is determined by Oryx based on the contents of the provided application. Please view the following document for more information on the supported runtime stacks for Oryx: https://github.com/microsoft/Oryx/blob/main/doc/supportedRuntimeVersions.md",
"loc.input.label.targetPort": "Application target port",
"loc.input.help.targetPort": "The designated port for the application to run on. If not provided, this value is 80 for Python applications, and 8080 for all other supposed platforms.",
"loc.input.label.location": "Location of the Container App",
"loc.input.help.location": "The location that the Container App (and other created resources) will be deployed to.",
"loc.input.label.environmentVariables": "Environment variables",
"loc.input.help.environmentVariables": "A list of environment variable(s) for the container. Space-separated values in 'key=value' format. Empty string to clear existing values. Prefix value with 'secretref:' to reference a secret.",
"loc.messages.AcrAccessTokenAuthFailed": "Unable to authenticate against ACR instance '%s.azurecr.io' with access token.",
"loc.messages.AcrAccessTokenLoginMessage": "Logging in to Azure Container Registry using access token to be generated via Azure CLI.",
"loc.messages.AcrUsernamePasswordAuthFailed": "Unable to authenticate against ACR instance '%s.azurecr.io' with username/password.",
Expand All @@ -54,7 +58,8 @@
"loc.messages.ErrorCodeFormat": "Error Code: [%s]",
"loc.messages.ErrorMessageFormat": "Error: %s",
"loc.messages.FoundAppSourceDockerfileMessage": "Found existing Dockerfile in provided application source at path '%s'; image will be built from this Dockerfile.",
"loc.messages.InvalidArgumentsMessage": "Either appSourcePath or imageToDeploy must be provided, but not both.",
"loc.messages.InvalidArgumentsMessage": "The arguments appSourcePath and acrName must either be provided together or not at all.",
"loc.messages.MissingImageToDeployMessage": "The argument imageToDeploy must be provided if neither appSourcePath nor acrName are provided.",
"loc.messages.PackCliInstallFailed": "Unable to install pack CLI.",
"loc.messages.PushImageToAcrFailed": "Unable to push image '%s' to ACR.",
"loc.messages.SetDefaultBuilderFailed": "Unable to set the Oryx++ Builder as the default builder."
Expand Down
91 changes: 68 additions & 23 deletions azurecontainerapps/azurecontainerapps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { ContainerAppHelper } from './src/ContainerAppHelper';
import { AzureAuthenticationHelper } from './src/AzureAuthenticationHelper';
import { ContainerRegistryHelper } from './src/ContainerRegistryHelper';

const util = new Utility();

export class azurecontainerapps {

public static async runMain(): Promise<void> {
Expand All @@ -26,34 +28,42 @@ export class azurecontainerapps {
// Set up array to store optional arguments for the 'az containerapp up' command
const optionalCmdArgs: string[] = [];

// Get the path to the application source to build and run
// Get the path to the application source to build and run, if provided
const appSourcePath: string = tl.getInput('appSourcePath', false);

// Get the previously built image to deploy
// Get the name of the ACR instance to push images to, if provided
const acrName: string = tl.getInput('acrName', false);

// Get the previously built image to deploy, if provided
let imageToDeploy: string = tl.getInput('imageToDeploy', false);

// Ensure that either the application source or a previously built image is provided, but not both
if ((!appSourcePath && !imageToDeploy) || (!!appSourcePath && !!imageToDeploy)) {
// Ensure that appSourcePath and acrName are either provided together or not at all
if (util.isNullOrEmpty(appSourcePath) != util.isNullOrEmpty(acrName)) {
tl.error(tl.loc('InvalidArgumentsMessage'));
throw Error(tl.loc('InvalidArgumentsMessage'));
}

// Ensure that if neither appSourcePath nor acrName are provided that imageToDeploy is provided
if (util.isNullOrEmpty(appSourcePath) && util.isNullOrEmpty(acrName) && util.isNullOrEmpty(imageToDeploy)) {
tl.error(tl.loc('MissingImageToDeployMessage'));
throw Error(tl.loc('MissingImageToDeployMessage'));
}

// Install the pack CLI
await new ContainerAppHelper().installPackCliAsync();

// Set the Azure CLI to dynamically install missing extensions
new Utility().setAzureCliDynamicInstall();
util.setAzureCliDynamicInstall();

// Log in to Azure with the service connection provided
const connectedService: string = tl.getInput('connectedServiceNameARM', true);
authHelper.loginAzureRM(connectedService);

const acrName: string = tl.getInput('acrName', true);
const acrUsername: string = tl.getInput('acrUsername', false);
const acrPassword: string = tl.getInput('acrPassword', false);

// Login to ACR if credentials were provided
if (!!acrUsername && !!acrPassword) {
if (!util.isNullOrEmpty(acrUsername) && !util.isNullOrEmpty(acrPassword)) {
console.log(tl.loc('AcrUsernamePasswordLoginMessage'));
new ContainerRegistryHelper().loginAcrWithUsernamePassword(acrName, acrUsername, acrPassword);
optionalCmdArgs.push(
Expand All @@ -63,71 +73,94 @@ export class azurecontainerapps {
}

// Login to ACR with access token if no credentials were provided
if (!acrUsername || !acrPassword) {
if (util.isNullOrEmpty(acrUsername) || util.isNullOrEmpty(acrPassword)) {
console.log(tl.loc('AcrAccessTokenLoginMessage'));
await new ContainerRegistryHelper().loginAcrWithAccessTokenAsync(acrName);
}

// Signals whether the Oryx builder should be used to create a runnable application image
let shouldUseBuilder: boolean = false;

// Signals whether an image will be created locally and pushed to ACR to use for the Container App
let shouldBuildAndPushImage = !util.isNullOrEmpty(appSourcePath);

// Get Dockerfile to build, if provided, or check if one exists at the root of the provided application
let dockerfilePath: string = tl.getInput('dockerfilePath', false);
if (!!appSourcePath && !dockerfilePath) {
if (shouldBuildAndPushImage) {
if (util.isNullOrEmpty(dockerfilePath)) {
console.log(tl.loc('CheckForAppSourceDockerfileMessage', appSourcePath));
const rootDockerfilePath = path.join(appSourcePath, 'Dockerfile');
if (fs.existsSync(rootDockerfilePath)) {
cormacpayne marked this conversation as resolved.
Show resolved Hide resolved
console.log(tl.loc('FoundAppSourceDockerfileMessage', rootDockerfilePath));
dockerfilePath = rootDockerfilePath;
} else {
// No Dockerfile found or provided, use the builder
shouldUseBuilder = true;
}
} else {
dockerfilePath = path.join(appSourcePath, dockerfilePath);
}
}

if (!util.isNullOrEmpty(appSourcePath) && util.isNullOrEmpty(dockerfilePath)) {
console.log(tl.loc('CheckForAppSourceDockerfileMessage', appSourcePath));
const rootDockerfilePath = path.join(appSourcePath, 'Dockerfile');
if (fs.existsSync(rootDockerfilePath)) {
console.log(tl.loc('FoundAppSourceDockerfileMessage', rootDockerfilePath));
dockerfilePath = rootDockerfilePath;
} else {
// No Dockerfile found or provided, use the builder
shouldUseBuilder = true;
}
} else if (!!appSourcePath && !!dockerfilePath) {
} else if (!util.isNullOrEmpty(appSourcePath) && !util.isNullOrEmpty(dockerfilePath)) {
dockerfilePath = path.join(appSourcePath, dockerfilePath);
}

// Get the name of the image to build if it was provided, or generate it from build variables
let imageToBuild: string = tl.getInput('imageToBuild', false);
if (!imageToBuild) {
if (util.isNullOrEmpty(imageToBuild)) {
imageToBuild = `${acrName}.azurecr.io/ado-task/container-app:${buildId}.${buildNumber}`;
console.log(tl.loc('DefaultImageToBuildMessage', imageToBuild));
}

// Get the name of the image to deploy if it was provided, or set it to the value of 'imageToBuild'
let shouldBuildAndPushImage = false;
if (!imageToDeploy) {
if (util.isNullOrEmpty(imageToDeploy)) {
imageToDeploy = imageToBuild;
shouldBuildAndPushImage = true;
console.log(tl.loc('DefaultImageToDeployMessage', imageToDeploy));
}

// Get the Container App name if it was provided, or generate it from build variables
let containerAppName: string = tl.getInput('containerAppName', false);
if (!containerAppName) {
if (util.isNullOrEmpty(containerAppName)) {
containerAppName = `ado-task-app-${buildId}-${buildNumber}`;
console.log(tl.loc('DefaultContainerAppNameMessage', containerAppName));
}

// Get the resource group to deploy to if it was provided, or generate it from the Container App name
let resourceGroup: string = tl.getInput('resourceGroup', false);
if (!resourceGroup) {
if (util.isNullOrEmpty(resourceGroup)) {
resourceGroup = `${containerAppName}-rg`;
console.log(tl.loc('DefaultResourceGroupMessage', resourceGroup));
}

// Get the Container App environment if provided
const containerAppEnvironment: string = tl.getInput('containerAppEnvironment', false);
if (!!containerAppEnvironment) {
if (!util.isNullOrEmpty(containerAppEnvironment)) {
console.log(tl.loc('ContainerAppEnvironmentUsedMessage', containerAppEnvironment));
optionalCmdArgs.push(`--environment ${containerAppEnvironment}`);
}

// Get the runtime stack if provided, or determine it using Oryx
let runtimeStack: string = tl.getInput('runtimeStack', false);
if (!runtimeStack && shouldBuildAndPushImage) {
if (util.isNullOrEmpty(runtimeStack) && shouldUseBuilder) {
runtimeStack = await new ContainerAppHelper().determineRuntimeStackAsync(appSourcePath);
console.log(tl.loc('DefaultRuntimeStackMessage', runtimeStack));
}

// Get the target port if provided, or determine it based on the application type
let targetPort: string = tl.getInput('targetPort', false);
if (!targetPort && !dockerfilePath) {
if (!!runtimeStack && runtimeStack.startsWith('python:')) {
if (util.isNullOrEmpty(targetPort) && shouldUseBuilder) {
if (!util.isNullOrEmpty(runtimeStack) && runtimeStack.startsWith('python:')) {
targetPort = '80';
} else {
targetPort = '8080';
Expand All @@ -137,12 +170,24 @@ export class azurecontainerapps {
}

// Add the target port to the optional arguments array
if (!!targetPort) {
if (!util.isNullOrEmpty(targetPort)) {
optionalCmdArgs.push(`--target-port ${targetPort}`);
}

// Set Container App deployment location, if provided
const location: string = tl.getInput('location', false);
if (!util.isNullOrEmpty(location)) {
optionalCmdArgs.push(`--location ${location}`);
}

// Add user specified environment variables
const environmentVariables: string = tl.getInput('environmentVariables', false);
if (!util.isNullOrEmpty(environmentVariables)) {
optionalCmdArgs.push(`--env-vars ${environmentVariables}`);
}

// If using the Oryx++ Builder to produce an image, create a runnable application image
if (!dockerfilePath && shouldBuildAndPushImage) {
if (shouldUseBuilder) {
console.log(tl.loc('CreateImageWithBuilderMessage'));

// Set the Oryx++ Builder as the default builder locally
Expand All @@ -153,7 +198,7 @@ export class azurecontainerapps {
}

// If a Dockerfile was found or provided, create a runnable application image from that
if (!!dockerfilePath && shouldBuildAndPushImage) {
if (!util.isNullOrEmpty(dockerfilePath) && shouldBuildAndPushImage) {
console.log(tl.loc('CreateImageWithDockerfileMessage', dockerfilePath));
new ContainerAppHelper().createRunnableAppImageFromDockerfile(imageToDeploy, appSourcePath, dockerfilePath);
}
Expand Down
Loading