Skip to content

Commit 441217c

Browse files
committed
Add support for installing additional packages
Clean up docker output. Closes #672 Closes #640
1 parent 933ac36 commit 441217c

File tree

6 files changed

+81
-37
lines changed

6 files changed

+81
-37
lines changed

src/Azure.Functions.Cli/Actions/AzureActions/PublishFunctionAppAction.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ internal class PublishFunctionAppAction : BaseFunctionAppAction
3838
public bool Ignore { get; set; }
3939
public bool Csx { get; set; }
4040
public bool BuildNativeDeps { get; set; }
41+
public string AdditionalPackages { get; set; } = string.Empty;
4142

4243
public PublishFunctionAppAction(ISettings settings, ISecretsManager secretsManager)
4344
{
@@ -76,6 +77,10 @@ public override ICommandLineParserResult ParseArgs(string[] args)
7677
.SetDefault(false)
7778
.WithDescription("Skips generating .wheels folder when publishing python function apps.")
7879
.Callback(f => BuildNativeDeps = f);
80+
Parser
81+
.Setup<string>("additional-packages")
82+
.WithDescription("List of packages to install when building native dependencies. For example: \"python3-dev libevent-dev\"")
83+
.Callback(p => AdditionalPackages = p);
7984
Parser
8085
.Setup<bool>("force")
8186
.WithDescription("Depending on the publish scenario, this will ignore pre-publish checks")
@@ -238,7 +243,7 @@ private async Task PublishFunctionApp(Site functionApp, GitIgnoreParser ignorePa
238243
throw new CliException("Publishing Python functions is only supported for Linux FunctionApps");
239244
}
240245

241-
var zipStream = await ZipHelper.GetAppZipFile(workerRuntimeEnum, functionAppRoot, BuildNativeDeps, ignoreParser);
246+
var zipStream = await ZipHelper.GetAppZipFile(workerRuntimeEnum, functionAppRoot, BuildNativeDeps, ignoreParser, AdditionalPackages);
242247

243248
// if consumption Linux, or --zip, run from zip
244249
if ((functionApp.IsLinux && functionApp.IsDynamic) || RunFromZipDeploy)

src/Azure.Functions.Cli/Actions/LocalActions/PackAction.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ internal class PackAction : BaseAction
2121
public string FolderName { get; set; } = string.Empty;
2222
public string OutputPath { get; set; }
2323
public bool BuildNativeDeps { get; set; }
24+
public string AdditionalPackages { get; set; } = string.Empty;
2425

2526
public PackAction(ISecretsManager secretsManager)
2627
{
@@ -38,6 +39,10 @@ public override ICommandLineParserResult ParseArgs(string[] args)
3839
.SetDefault(false)
3940
.WithDescription("Skips generating .wheels folder when publishing python function apps.")
4041
.Callback(f => BuildNativeDeps = f);
42+
Parser
43+
.Setup<string>("additional-packages")
44+
.WithDescription("List of packages to install when building native dependencies. For example: \"python3-dev libevent-dev\"")
45+
.Callback(p => AdditionalPackages = p);
4146

4247
if (args.Any() && !args.First().StartsWith("-"))
4348
{
@@ -74,7 +79,7 @@ public override async Task RunAsync()
7479

7580
var workerRuntime = WorkerRuntimeLanguageHelper.GetCurrentWorkerRuntimeLanguage(_secretsManager);
7681

77-
var zipStream = await ZipHelper.GetAppZipFile(workerRuntime, functionAppRoot, BuildNativeDeps);
82+
var zipStream = await ZipHelper.GetAppZipFile(workerRuntime, functionAppRoot, BuildNativeDeps, additionalPackages: AdditionalPackages);
7883
await FileSystemHelpers.WriteToFile(outputPath, zipStream);
7984
}
8085
}

src/Azure.Functions.Cli/Helpers/DockerHelpers.cs

+54-30
Original file line numberDiff line numberDiff line change
@@ -9,44 +9,25 @@ namespace Azure.Functions.Cli.Helpers
99
{
1010
public static class DockerHelpers
1111
{
12-
private static async Task RunDockerCommand(string args, bool ignoreError = false)
13-
{
14-
var docker = new Executable("docker", args);
15-
ColoredConsole.WriteLine($"Running {docker.Command}");
16-
var exitCode = await docker.RunAsync(l => ColoredConsole.WriteLine(l), e => ColoredConsole.Error.WriteLine(ErrorColor(e)));
17-
if (exitCode != 0 && !ignoreError)
18-
{
19-
throw new CliException($"Error running {docker.Command}");
20-
}
21-
}
22-
2312
public static Task DockerPull(string image) => RunDockerCommand($"pull {image}");
13+
2414
public static Task DockerPush(string image) => RunDockerCommand($"push {image}");
15+
2516
public static Task DockerBuild(string tag, string dir) => RunDockerCommand($"build -t {tag} {dir}");
2617

27-
public static async Task<string> DockerRun(string image)
28-
{
29-
var docker = new Executable("docker", $"run -d {image}");
30-
var sb = new StringBuilder();
31-
ColoredConsole.WriteLine($"Running {docker.Command}");
32-
var exitCode = await docker.RunAsync(l => sb.Append(l), e => ColoredConsole.Error.WriteLine(ErrorColor(e)));
33-
if (exitCode != 0)
34-
{
35-
throw new CliException($"Error running {docker.Command}");
36-
}
37-
else
38-
{
39-
return sb.ToString().Trim();
40-
}
41-
}
18+
public static Task CopyToContainer(string containerId, string source, string target) => RunDockerCommand($"cp {source} {containerId}:{target}", containerId);
4219

43-
public static Task CopyToContainer(string containerId, string source, string target) => RunDockerCommand($"cp {source} {containerId}:{target}");
20+
public static Task ExecInContainer(string containerId, string command) => RunDockerCommand($"exec -t {containerId} {command}", containerId);
4421

45-
public static Task ExecInContainer(string containerId, string command) => RunDockerCommand($"exec -t {containerId} {command}");
22+
public static Task CopyFromContainer(string containerId, string source, string target) => RunDockerCommand($"cp {containerId}:{source} {target}", containerId);
4623

47-
public static Task CopyFromContainer(string containerId, string source, string target) => RunDockerCommand($"cp {containerId}:{source} {target}");
24+
public static Task KillContainer(string containerId, bool ignoreError = false) => RunDockerCommand($"kill {containerId}", containerId, ignoreError);
4825

49-
public static Task KillContainer(string containerId, bool ignoreError = false) => RunDockerCommand($"kill {containerId}", ignoreError);
26+
public static async Task<string> DockerRun(string image)
27+
{
28+
(var output, _) = await RunDockerCommand($"run --rm -d {image}");
29+
return output.ToString().Trim();
30+
}
5031

5132
internal static async Task<bool> VerifyDockerAccess()
5233
{
@@ -59,5 +40,48 @@ internal static async Task<bool> VerifyDockerAccess()
5940
}
6041
return true;
6142
}
43+
44+
private static async Task<(string output, string error)> InternalRunDockerCommand(string args, bool ignoreError)
45+
{
46+
var docker = new Executable("docker", args);
47+
var sbError = new StringBuilder();
48+
var sbOutput = new StringBuilder();
49+
50+
var exitCode = await docker.RunAsync(l => sbOutput.AppendLine(l), e => sbError.AppendLine(e));
51+
52+
if (exitCode != 0 && !ignoreError)
53+
{
54+
throw new CliException($"Error running {docker.Command}.\n" +
55+
$"output: {sbOutput.ToString()}\n{sbError.ToString()}");
56+
}
57+
58+
return (sbOutput.ToString(), sbError.ToString());
59+
}
60+
61+
private static async Task<(string output, string error)> RunDockerCommand(string args, string containerId = null, bool ignoreError = false)
62+
{
63+
var printArgs = string.IsNullOrWhiteSpace(containerId)
64+
? args
65+
: args.Replace(containerId, containerId.Substring(0, 6));
66+
ColoredConsole.Write($"Running 'docker {printArgs}'.");
67+
var task = InternalRunDockerCommand(args, ignoreError);
68+
69+
while (!task.IsCompleted)
70+
{
71+
await Task.Delay(TimeSpan.FromSeconds(1));
72+
ColoredConsole.Write(".");
73+
}
74+
ColoredConsole.WriteLine("done");
75+
76+
(var output, var error) = await task;
77+
78+
if (StaticSettings.IsDebug)
79+
{
80+
ColoredConsole
81+
.WriteLine($"Output: {output}")
82+
.WriteLine($"Error: {error}");
83+
}
84+
return (output, error);
85+
}
6286
}
6387
}

src/Azure.Functions.Cli/Helpers/PythonHelpers.cs

+10-3
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ private static async Task VerifyVersion()
9999
}
100100
}
101101

102-
internal static async Task<Stream> GetPythonDeploymentPackage(IEnumerable<string> files, string functionAppRoot, bool buildNativeDeps)
102+
internal static async Task<Stream> GetPythonDeploymentPackage(IEnumerable<string> files, string functionAppRoot, bool buildNativeDeps, string additionalPackages)
103103
{
104104
if (!FileSystemHelpers.FileExists(Path.Combine(functionAppRoot, Constants.RequirementsTxt)))
105105
{
@@ -111,7 +111,7 @@ internal static async Task<Stream> GetPythonDeploymentPackage(IEnumerable<string
111111
{
112112
if (CommandChecker.CommandExists("docker") && await DockerHelpers.VerifyDockerAccess())
113113
{
114-
return await InternalPreparePythonDeploymentInDocker(files, functionAppRoot);
114+
return await InternalPreparePythonDeploymentInDocker(files, functionAppRoot, additionalPackages);
115115
}
116116
else
117117
{
@@ -164,7 +164,7 @@ private static async Task InstallDislib()
164164
}
165165
}
166166

167-
private static async Task<Stream> InternalPreparePythonDeploymentInDocker(IEnumerable<string> files, string functionAppRoot)
167+
private static async Task<Stream> InternalPreparePythonDeploymentInDocker(IEnumerable<string> files, string functionAppRoot, string additionalPackages)
168168
{
169169
var appContentPath = CopyToTemp(files, functionAppRoot);
170170

@@ -178,6 +178,13 @@ private static async Task<Stream> InternalPreparePythonDeploymentInDocker(IEnume
178178

179179
var scriptFilePath = Path.GetTempFileName();
180180
await FileSystemHelpers.WriteAllTextToFileAsync(scriptFilePath, (await StaticResources.PythonDockerBuildScript).Replace("\r\n", "\n"));
181+
if (!string.IsNullOrWhiteSpace(additionalPackages))
182+
{
183+
// Give the container time to start up
184+
await Task.Delay(TimeSpan.FromSeconds(3));
185+
await DockerHelpers.ExecInContainer(containerId, $"apt-get update");
186+
await DockerHelpers.ExecInContainer(containerId, $"apt-get install -y {additionalPackages}");
187+
}
181188
await DockerHelpers.CopyToContainer(containerId, scriptFilePath, Constants.StaticResourcesNames.PythonDockerBuild);
182189
await DockerHelpers.ExecInContainer(containerId, $"chmod +x /{Constants.StaticResourcesNames.PythonDockerBuild}");
183190
await DockerHelpers.ExecInContainer(containerId, $"/{Constants.StaticResourcesNames.PythonDockerBuild}");

src/Azure.Functions.Cli/Helpers/ZipHelper.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace Azure.Functions.Cli.Helpers
1010
{
1111
public static class ZipHelper
1212
{
13-
public static async Task<Stream> GetAppZipFile(WorkerRuntime workerRuntime, string functionAppRoot, bool buildNativeDeps, GitIgnoreParser ignoreParser = null)
13+
public static async Task<Stream> GetAppZipFile(WorkerRuntime workerRuntime, string functionAppRoot, bool buildNativeDeps, GitIgnoreParser ignoreParser = null, string additionalPackages = null)
1414
{
1515
var gitIgnorePath = Path.Combine(functionAppRoot, Constants.FuncIgnoreFile);
1616
if (ignoreParser == null && FileSystemHelpers.FileExists(gitIgnorePath))
@@ -20,7 +20,7 @@ public static async Task<Stream> GetAppZipFile(WorkerRuntime workerRuntime, stri
2020

2121
if (workerRuntime == WorkerRuntime.python)
2222
{
23-
return await PythonHelpers.GetPythonDeploymentPackage(FileSystemHelpers.GetLocalFiles(functionAppRoot, ignoreParser), functionAppRoot, buildNativeDeps);
23+
return await PythonHelpers.GetPythonDeploymentPackage(FileSystemHelpers.GetLocalFiles(functionAppRoot, ignoreParser), functionAppRoot, buildNativeDeps, additionalPackages);
2424
}
2525
else if (workerRuntime == WorkerRuntime.dotnet)
2626
{

src/Azure.Functions.Cli/StaticResources/python_docker_build.sh.template

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#! /bin/bash
22

3+
# Exit on errors
4+
set -e
5+
36
cd /home/site/wwwroot
47
if [ -d worker_venv ]; then
58
rm -rf worker_venv

0 commit comments

Comments
 (0)