Skip to content

Commit

Permalink
Support ContainerEnvironmentVariables (#181)
Browse files Browse the repository at this point in the history
  • Loading branch information
benvillalobos authored Oct 13, 2022
1 parent 548091b commit 9aca15f
Show file tree
Hide file tree
Showing 11 changed files with 255 additions and 10 deletions.
9 changes: 8 additions & 1 deletion Microsoft.NET.Build.Containers/ContainerBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Microsoft.NET.Build.Containers;

public static class ContainerBuilder
{
public static async Task Containerize(DirectoryInfo folder, string workingDir, string registryName, string baseName, string baseTag, string[] entrypoint, string[] entrypointArgs, string imageName, string[] imageTags, string outputRegistry, string[] labels, Port[] exposedPorts)
public static async Task Containerize(DirectoryInfo folder, string workingDir, string registryName, string baseName, string baseTag, string[] entrypoint, string[] entrypointArgs, string imageName, string[] imageTags, string outputRegistry, string[] labels, Port[] exposedPorts, string[] envVars)
{
var isDockerPull = String.IsNullOrEmpty(registryName);
if (isDockerPull) {
Expand Down Expand Up @@ -40,6 +40,13 @@ public static async Task Containerize(DirectoryInfo folder, string workingDir, s
img.Label(labelPieces[0], labelPieces[1]);
}

foreach (string envVar in envVars)
{
string[] envPieces = envVar.Split('=', 2);

img.AddEnvironmentVariable(envPieces[0], envPieces[1]);
}

foreach (var (number, type) in exposedPorts)
{
// ports are validated by System.CommandLine API
Expand Down
11 changes: 11 additions & 0 deletions Microsoft.NET.Build.Containers/ContainerHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public record Port(int number, PortType type);

public static class ContainerHelpers
{
private static Regex envVarRegex = new Regex(@"^[a-zA-Z_]+$");

/// <summary>
/// DefaultRegistry is the canonical representation of something that lives in the local docker daemon. It's used as the inferred registry for repositories
Expand Down Expand Up @@ -73,6 +74,16 @@ public static Uri TryExpandRegistryToUri(string alreadyValidatedDomain)
return new Uri($"{prefix}://{alreadyValidatedDomain}");
}

/// <summary>
/// Ensures a given environment variable is valid.
/// </summary>
/// <param name="envVar"></param>
/// <returns></returns>
public static bool IsValidEnvironmentVariable(string envVar)
{
return envVarRegex.IsMatch(envVar);
}

/// <summary>
/// Parse a fully qualified container name (e.g. https://mcr.microsoft.com/dotnet/runtime:6.0)
/// Note: Tag not required.
Expand Down
15 changes: 15 additions & 0 deletions Microsoft.NET.Build.Containers/CreateNewImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ public class CreateNewImage : Microsoft.Build.Utilities.Task
/// </summary>
public ITaskItem[] Labels { get; set; }

/// <summary>
/// Container environment variables to set.
/// </summary>
public ITaskItem[] ContainerEnvironmentVariables { get; set; }

private bool IsDockerPush { get => String.IsNullOrEmpty(OutputRegistry); }

private bool IsDockerPull { get => String.IsNullOrEmpty(BaseRegistry); }
Expand All @@ -115,6 +120,7 @@ public CreateNewImage()
EntrypointArgs = Array.Empty<ITaskItem>();
Labels = Array.Empty<ITaskItem>();
ExposedPorts = Array.Empty<ITaskItem>();
ContainerEnvironmentVariables = Array.Empty<ITaskItem>();
}

private void SetPorts(Image image, ITaskItem[] exposedPorts)
Expand Down Expand Up @@ -161,7 +167,14 @@ private void SetPorts(Image image, ITaskItem[] exposedPorts)
}
}
}
}

private void SetEnvironmentVariables(Image img, ITaskItem[] envVars)
{
foreach (ITaskItem envVar in envVars)
{
img.AddEnvironmentVariable(envVar.ItemSpec, envVar.GetMetadata("Value"));
}
}

private Image GetBaseImage() {
Expand Down Expand Up @@ -198,6 +211,8 @@ public override bool Execute()
image.Label(label.ItemSpec, label.GetMetadata("Value"));
}

SetEnvironmentVariables(image, ContainerEnvironmentVariables);

SetPorts(image, ExposedPorts);

// at the end of this step, if any failed then bail out.
Expand Down
9 changes: 8 additions & 1 deletion Microsoft.NET.Build.Containers/CreateNewImageToolTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ public class CreateNewImage : ToolTask
/// it's mostly documentation.
/// </summary>
public ITaskItem[] ExposedPorts { get; set; }

/// <summary>
/// Container environment variables to set.
/// </summary>
public ITaskItem[] ContainerEnvironmentVariables { get; set; }

// Unused, ToolExe is set via targets and overrides this.
protected override string ToolName => "dotnet";
Expand Down Expand Up @@ -119,6 +124,7 @@ public CreateNewImage()
EntrypointArgs = Array.Empty<ITaskItem>();
Labels = Array.Empty<ITaskItem>();
ExposedPorts = Array.Empty<ITaskItem>();
ContainerEnvironmentVariables = Array.Empty<ITaskItem>();
}

protected override string GenerateFullPathToTool() => Quote(Path.Combine(DotNetPath, ToolExe));
Expand All @@ -137,7 +143,8 @@ protected override string GenerateCommandLineCommands()
(Labels.Length > 0 ? " --labels " + Labels.Select((i) => i.ItemSpec + "=" + i.GetMetadata("Value")).Aggregate((i, s) => s += i + " ") : "") +
(ImageTags.Length > 0 ? " --imagetags " + ImageTags.Select((i) => i.ItemSpec).Aggregate((i, s) => s += i + " ") : "") +
(EntrypointArgs.Length > 0 ? " --entrypointargs " + EntrypointArgs.Select((i) => i.ItemSpec).Aggregate((i, s) => s += i + " ") : "") +
(ExposedPorts.Length > 0 ? " --ports " + ExposedPorts.Select((i) => i.ItemSpec + "/" + i.GetMetadata("Type")).Aggregate((i, s) => s += i + " ") : "");
(ExposedPorts.Length > 0 ? " --ports " + ExposedPorts.Select((i) => i.ItemSpec + "/" + i.GetMetadata("Type")).Aggregate((i, s) => s += i + " ") : "") +
(ContainerEnvironmentVariables.Length > 0 ? " --environmentvariables " + ContainerEnvironmentVariables.Select((i) => i.ItemSpec + "=" + i.GetMetadata("Value")).Aggregate((i, s) => s += i + " ") : "");
}

private string Quote(string path)
Expand Down
53 changes: 53 additions & 0 deletions Microsoft.NET.Build.Containers/Image.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public class Image

internal HashSet<Port> exposedPorts;

internal Dictionary<string, string> environmentVariables;

public Image(JsonNode manifest, JsonNode config, string name, Registry? registry)
{
this.manifest = manifest;
Expand All @@ -29,6 +31,7 @@ public Image(JsonNode manifest, JsonNode config, string name, Registry? registry
// these next values are inherited from the parent image, so we need to seed our new image with them.
this.labels = ReadLabelsFromConfig(config);
this.exposedPorts = ReadPortsFromConfig(config);
this.environmentVariables = ReadEnvVarsFromConfig(config);
}

public IEnumerable<Descriptor> LayerDescriptors
Expand Down Expand Up @@ -81,6 +84,17 @@ private JsonObject CreatePortMap()
return container;
}

private JsonArray CreateEnvironmentVarMapping()
{
// Env is a JSON array where each value is of the format: "VAR=value"
var envVarJson = new JsonArray();
foreach (var envVar in environmentVariables)
{
envVarJson.Add<string>($"{envVar.Key}={envVar.Value}");
}
return envVarJson;
}

private static HashSet<Label> ReadLabelsFromConfig(JsonNode inputConfig)
{
if (inputConfig is JsonObject config && config["Labels"] is JsonObject labelsJson)
Expand All @@ -103,6 +117,31 @@ private static HashSet<Label> ReadLabelsFromConfig(JsonNode inputConfig)
}
}

private static Dictionary<string, string> ReadEnvVarsFromConfig(JsonNode inputConfig)
{
if (inputConfig is JsonObject config && config["config"]!["Env"] is JsonArray envVarJson)
{
var envVars = new Dictionary<string, string>();
foreach (var entry in envVarJson)
{
if (entry is null)
continue;

var val = entry.GetValue<string>().Split('=', 2);

if (val.Length != 2)
continue;

envVars.Add(val[0], val[1]);
}
return envVars;
}
else
{
return new Dictionary<string, string>();
}
}

private static HashSet<Port> ReadPortsFromConfig(JsonNode inputConfig)
{
if (inputConfig is JsonObject config && config["ExposedPorts"] is JsonObject portsJson)
Expand Down Expand Up @@ -179,6 +218,20 @@ public void Label(string name, string value)
RecalculateDigest();
}

public void AddEnvironmentVariable(string envVarName, string value)
{
if (!environmentVariables.ContainsKey(envVarName))
{
environmentVariables.Add(envVarName, value);
}
else
{
environmentVariables[envVarName] = value;
}
config["config"]!["Env"] = CreateEnvironmentVarMapping();
RecalculateDigest();
}

public void ExposePort(int number, PortType type)
{
exposedPorts.Add(new(number, type));
Expand Down
33 changes: 33 additions & 0 deletions Microsoft.NET.Build.Containers/ParseContainerProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ public class ParseContainerProperties : Microsoft.Build.Utilities.Task
/// </summary>
public string[] ContainerImageTags { get; set; }

/// <summary>
/// Container environment variables to set.
/// </summary>
public ITaskItem[] ContainerEnvironmentVariables { get; set; }

[Output]
public string ParsedContainerRegistry { get; private set; }

Expand All @@ -52,19 +57,24 @@ public class ParseContainerProperties : Microsoft.Build.Utilities.Task
[Output]
public string[] NewContainerTags { get; private set; }

[Output]
public ITaskItem[] NewContainerEnvironmentVariables { get; private set; }

public ParseContainerProperties()
{
FullyQualifiedBaseImageName = "";
ContainerRegistry = "";
ContainerImageName = "";
ContainerImageTag = "";
ContainerImageTags = Array.Empty<string>();
ContainerEnvironmentVariables = Array.Empty<ITaskItem>();
ParsedContainerRegistry = "";
ParsedContainerImage = "";
ParsedContainerTag = "";
NewContainerRegistry = "";
NewContainerImageName = "";
NewContainerTags = Array.Empty<string>();
NewContainerEnvironmentVariables = Array.Empty<ITaskItem>();
}

private static bool TryValidateTags(string[] inputTags, out string[] validTags, out string[] invalidTags)
Expand Down Expand Up @@ -133,6 +143,8 @@ public override bool Execute()
return !Log.HasLoggedErrors;
}

ValidateEnvironmentVariables();

if (FullyQualifiedBaseImageName.Contains(' ') && BuildEngine != null)
{
Log.LogWarning($"{nameof(FullyQualifiedBaseImageName)} had spaces in it, replacing with dashes.");
Expand Down Expand Up @@ -186,4 +198,25 @@ public override bool Execute()

return !Log.HasLoggedErrors;
}

public void ValidateEnvironmentVariables()
{
var filteredEnvVars = ContainerEnvironmentVariables.Where((x) => ContainerHelpers.IsValidEnvironmentVariable(x.ItemSpec)).ToArray<ITaskItem>();
var badEnvVars = ContainerEnvironmentVariables.Where((x) => !ContainerHelpers.IsValidEnvironmentVariable(x.ItemSpec));

foreach (var badEnvVar in badEnvVars)
{
if (BuildEngine != null)
{
Log.LogWarning($"{nameof(ContainerEnvironmentVariables)}: '{badEnvVar.ItemSpec}' was not a valid Environment Variable. Ignoring.");
}
}

NewContainerEnvironmentVariables = new ITaskItem[filteredEnvVars.Length];

for (int i = 0; i < filteredEnvVars.Length; i++)
{
NewContainerEnvironmentVariables[i] = filteredEnvVars[i];
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,91 @@ public void ParseContainerProperties_EndToEnd()
Assert.IsTrue(cni.Execute());
newProjectDir.Delete(true);
}

/// <summary>
/// Creates a console app that outputs the environment variable added to the image.
/// </summary>
[TestMethod]
public void Tasks_EndToEnd_With_EnvironmentVariable_Validation()
{
DirectoryInfo newProjectDir = new DirectoryInfo(Path.Combine(Path.GetTempPath(), nameof(Tasks_EndToEnd_With_EnvironmentVariable_Validation)));

if (newProjectDir.Exists)
{
newProjectDir.Delete(recursive: true);
}

newProjectDir.Create();

ProcessStartInfo info = new ProcessStartInfo
{
WorkingDirectory = newProjectDir.FullName,
FileName = "dotnet",
Arguments = "new console -f net7.0",
RedirectStandardOutput = true,
RedirectStandardError = true
};

Process dotnetNew = Process.Start(info);
Assert.IsNotNull(dotnetNew);
dotnetNew.WaitForExit();
Assert.AreEqual(0, dotnetNew.ExitCode, dotnetNew.StandardOutput.ReadToEnd());

File.WriteAllText(Path.Combine(newProjectDir.FullName, "Program.cs"), $"Console.Write(Environment.GetEnvironmentVariable(\"GoodEnvVar\"));");

info.Arguments = "build --configuration release /p:runtimeidentifier=linux-x64";

Process dotnetBuildRelease = Process.Start(info);
Assert.IsNotNull(dotnetBuildRelease);
dotnetBuildRelease.WaitForExit();
dotnetBuildRelease.Kill();
Assert.AreEqual(0, dotnetBuildRelease.ExitCode);

ParseContainerProperties pcp = new ParseContainerProperties();
pcp.FullyQualifiedBaseImageName = "mcr.microsoft.com/dotnet/runtime:6.0";
pcp.ContainerRegistry = "";
pcp.ContainerImageName = "dotnet/envvarvalidation";
pcp.ContainerImageTag = "latest";

Dictionary<string, string> dict = new Dictionary<string, string>();
dict.Add("Value", "Foo");

pcp.ContainerEnvironmentVariables = new[] { new TaskItem("[email protected]", dict), new TaskItem("GoodEnvVar", dict) };

Assert.IsTrue(pcp.Execute());
Assert.AreEqual("mcr.microsoft.com", pcp.ParsedContainerRegistry);
Assert.AreEqual("dotnet/runtime", pcp.ParsedContainerImage);
Assert.AreEqual("6.0", pcp.ParsedContainerTag);
Assert.AreEqual(1, pcp.NewContainerEnvironmentVariables.Length);
Assert.AreEqual("Foo", pcp.NewContainerEnvironmentVariables[0].GetMetadata("Value"));

Assert.AreEqual("dotnet/envvarvalidation", pcp.NewContainerImageName);
Assert.AreEqual("latest", pcp.NewContainerTags[0]);

CreateNewImage cni = new CreateNewImage();
cni.BaseRegistry = pcp.ParsedContainerRegistry;
cni.BaseImageName = pcp.ParsedContainerImage;
cni.BaseImageTag = pcp.ParsedContainerTag;
cni.ImageName = pcp.NewContainerImageName;
cni.OutputRegistry = pcp.NewContainerRegistry;
cni.PublishDirectory = Path.Combine(newProjectDir.FullName, "bin", "release", "net7.0", "linux-x64");
cni.WorkingDirectory = "/app";
cni.Entrypoint = new TaskItem[] { new("/app/Tasks_EndToEnd_With_EnvironmentVariable_Validation") };
cni.ImageTags = pcp.NewContainerTags;
cni.ContainerEnvironmentVariables = pcp.NewContainerEnvironmentVariables;

Assert.IsTrue(cni.Execute());

ProcessStartInfo runInfo = new("docker", $"run --rm {pcp.NewContainerImageName}:latest")
{
RedirectStandardOutput = true,
RedirectStandardError = true,
};

Process run = Process.Start(runInfo);
Assert.IsNotNull(run);
run.WaitForExit();
Assert.AreEqual(0, run.ExitCode);
Assert.AreEqual("Foo", run.StandardOutput.ReadToEnd());
}
}
Loading

0 comments on commit 9aca15f

Please sign in to comment.