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

Add support for ENTRYPOINT instruction #54

Merged
merged 1 commit into from
Nov 13, 2020
Merged
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
@@ -0,0 +1,238 @@
using System;
using System.Collections.Generic;
using System.Linq;
using DockerfileModel.Tokens;
using Sprache;
using Xunit;

using static DockerfileModel.Tests.TokenValidator;

namespace DockerfileModel.Tests
{
public class EntrypointInstructionTests
{
[Theory]
[MemberData(nameof(ParseTestInput))]
public void Parse(EntrypointInstructionParseTestScenario scenario)
{
if (scenario.ParseExceptionPosition is null)
{
EntrypointInstruction result = EntrypointInstruction.Parse(scenario.Text, scenario.EscapeChar);
Assert.Equal(scenario.Text, result.ToString());
Assert.Collection(result.Tokens, scenario.TokenValidators);
scenario.Validate?.Invoke(result);
}
else
{
ParseException exception = Assert.Throws<ParseException>(
() => EntrypointInstruction.Parse(scenario.Text, scenario.EscapeChar));
Assert.Equal(scenario.ParseExceptionPosition.Line, exception.Position.Line);
Assert.Equal(scenario.ParseExceptionPosition.Column, exception.Position.Column);
}
}

[Theory]
[MemberData(nameof(CreateTestInput))]
public void Create(CreateTestScenario scenario)
{
EntrypointInstruction result;
if (scenario.Command != null)
{
result = EntrypointInstruction.Create(scenario.Command);
}
else
{
result = EntrypointInstruction.Create(scenario.Commands);
}

Assert.Collection(result.Tokens, scenario.TokenValidators);
scenario.Validate?.Invoke(result);
}

public static IEnumerable<object[]> ParseTestInput()
{
EntrypointInstructionParseTestScenario[] testInputs = new EntrypointInstructionParseTestScenario[]
{
new EntrypointInstructionParseTestScenario
{
Text = "ENTRYPOINT echo hello",
TokenValidators = new Action<Token>[]
{
token => ValidateKeyword(token, "ENTRYPOINT"),
token => ValidateWhitespace(token, " "),
token => ValidateAggregate<ShellFormCommand>(token, "echo hello",
token => ValidateLiteral(token, "echo hello"))
},
Validate = result =>
{
Assert.Empty(result.Comments);
Assert.Equal("ENTRYPOINT", result.InstructionName);
Assert.Equal(CommandType.ShellForm, result.Command.CommandType);
Assert.Equal("echo hello", result.Command.ToString());
Assert.IsType<ShellFormCommand>(result.Command);
ShellFormCommand cmd = (ShellFormCommand)result.Command;
Assert.Equal("echo hello", cmd.Value);
}
},
new EntrypointInstructionParseTestScenario
{
Text = "ENTRYPOINT $TEST",
TokenValidators = new Action<Token>[]
{
token => ValidateKeyword(token, "ENTRYPOINT"),
token => ValidateWhitespace(token, " "),
token => ValidateAggregate<ShellFormCommand>(token, "$TEST",
token => ValidateLiteral(token, "$TEST"))
}
},
new EntrypointInstructionParseTestScenario
{
Text = "ENTRYPOINT echo $TEST",
TokenValidators = new Action<Token>[]
{
token => ValidateKeyword(token, "ENTRYPOINT"),
token => ValidateWhitespace(token, " "),
token => ValidateAggregate<ShellFormCommand>(token, "echo $TEST",
token => ValidateLiteral(token, "echo $TEST"))
}
},
new EntrypointInstructionParseTestScenario
{
Text = "ENTRYPOINT T\\$EST",
TokenValidators = new Action<Token>[]
{
token => ValidateKeyword(token, "ENTRYPOINT"),
token => ValidateWhitespace(token, " "),
token => ValidateAggregate<ShellFormCommand>(token, "T\\$EST",
token => ValidateLiteral(token, "T\\$EST"))
}
},
new EntrypointInstructionParseTestScenario
{
Text = "ENTRYPOINT echo `\n#test comment\nhello",
EscapeChar = '`',
TokenValidators = new Action<Token>[]
{
token => ValidateKeyword(token, "ENTRYPOINT"),
token => ValidateWhitespace(token, " "),
token => ValidateAggregate<ShellFormCommand>(token, "echo `\n#test comment\nhello",
token => ValidateQuotableAggregate<LiteralToken>(token, "echo `\n#test comment\nhello", null,
token => ValidateString(token, "echo "),
token => ValidateAggregate<LineContinuationToken>(token, "`\n",
token => ValidateSymbol(token, '`'),
token => ValidateNewLine(token, "\n")),
token => ValidateAggregate<CommentToken>(token, "#test comment\n",
token => ValidateSymbol(token, '#'),
token => ValidateString(token, "test comment"),
token => ValidateNewLine(token, "\n")),
token => ValidateString(token, "hello")))
},
Validate = result =>
{
Assert.Single(result.Comments);
Assert.Equal("test comment", result.Comments.First());
Assert.Equal("ENTRYPOINT", result.InstructionName);
Assert.Equal(CommandType.ShellForm, result.Command.CommandType);
Assert.Equal("echo `\n#test comment\nhello", result.Command.ToString());
Assert.IsType<ShellFormCommand>(result.Command);
ShellFormCommand cmd = (ShellFormCommand)result.Command;
Assert.Equal("echo hello", cmd.Value);
}
},
new EntrypointInstructionParseTestScenario
{
Text = "ENTRYPOINT [\"/bin/bash\", \"-c\", \"echo hello\"]",
TokenValidators = new Action<Token>[]
{
token => ValidateKeyword(token, "ENTRYPOINT"),
token => ValidateWhitespace(token, " "),
token => ValidateAggregate<ExecFormCommand>(token, "[\"/bin/bash\", \"-c\", \"echo hello\"]",
token => ValidateSymbol(token, '['),
token => ValidateLiteral(token, "/bin/bash", ParseHelper.DoubleQuote),
token => ValidateSymbol(token, ','),
token => ValidateWhitespace(token, " "),
token => ValidateLiteral(token, "-c", ParseHelper.DoubleQuote),
token => ValidateSymbol(token, ','),
token => ValidateWhitespace(token, " "),
token => ValidateLiteral(token, "echo hello", ParseHelper.DoubleQuote),
token => ValidateSymbol(token, ']'))
},
Validate = result =>
{
Assert.Empty(result.Comments);
Assert.Equal("ENTRYPOINT", result.InstructionName);
Assert.Equal(CommandType.ExecForm, result.Command.CommandType);
Assert.Equal("[\"/bin/bash\", \"-c\", \"echo hello\"]", result.Command.ToString());
Assert.IsType<ExecFormCommand>(result.Command);
ExecFormCommand cmd = (ExecFormCommand)result.Command;
Assert.Equal(
new string[]
{
"/bin/bash",
"-c",
"echo hello"
},
cmd.CommandArgs.ToArray());
}
}
};

return testInputs.Select(input => new object[] { input });
}

public static IEnumerable<object[]> CreateTestInput()
{
CreateTestScenario[] testInputs = new CreateTestScenario[]
{
new CreateTestScenario
{
Command = "echo hello",
TokenValidators = new Action<Token>[]
{
token => ValidateKeyword(token, "ENTRYPOINT"),
token => ValidateWhitespace(token, " "),
token => ValidateAggregate<ShellFormCommand>(token, "echo hello",
token => ValidateLiteral(token, "echo hello"))
}
},
new CreateTestScenario
{
Commands = new string[]
{
"/bin/bash",
"-c",
"echo hello"
},
TokenValidators = new Action<Token>[]
{
token => ValidateKeyword(token, "ENTRYPOINT"),
token => ValidateWhitespace(token, " "),
token => ValidateAggregate<ExecFormCommand>(token, "[\"/bin/bash\", \"-c\", \"echo hello\"]",
token => ValidateSymbol(token, '['),
token => ValidateLiteral(token, "/bin/bash", ParseHelper.DoubleQuote),
token => ValidateSymbol(token, ','),
token => ValidateWhitespace(token, " "),
token => ValidateLiteral(token, "-c", ParseHelper.DoubleQuote),
token => ValidateSymbol(token, ','),
token => ValidateWhitespace(token, " "),
token => ValidateLiteral(token, "echo hello", ParseHelper.DoubleQuote),
token => ValidateSymbol(token, ']'))
}
}
};

return testInputs.Select(input => new object[] { input });
}

public class EntrypointInstructionParseTestScenario : ParseTestScenario<EntrypointInstruction>
{
public char EscapeChar { get; set; }
}

public class CreateTestScenario : TestScenario<EntrypointInstruction>
{
public string Command { get; set; }
public IEnumerable<string> Commands { get; set; }
}
}
}
15 changes: 15 additions & 0 deletions src/DockerfileModel/DockerfileModel/DockerfileBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ public DockerfileBuilder NewLine() =>
public DockerfileBuilder AddInstruction(IEnumerable<string> sources, string destination, ChangeOwnerFlag? changeOwnerFlag = null) =>
AddConstruct(DockerfileModel.AddInstruction.Create(sources, destination, changeOwnerFlag, EscapeChar));

public DockerfileBuilder AddInstruction(Action<TokenBuilder> configureBuilder) =>
ParseTokens(configureBuilder, DockerfileModel.AddInstruction.Parse);

public DockerfileBuilder ArgInstruction(string argName, string? argValue = null) =>
AddConstruct(DockerfileModel.ArgInstruction.Create(argName, argValue, EscapeChar));

Expand All @@ -68,6 +71,18 @@ public DockerfileBuilder Comment(Action<TokenBuilder> configureBuilder) =>
public DockerfileBuilder CopyInstruction(IEnumerable<string> sources, string destination, ChangeOwnerFlag? changeOwnerFlag = null) =>
AddConstruct(DockerfileModel.CopyInstruction.Create(sources, destination, changeOwnerFlag, EscapeChar));

public DockerfileBuilder CopyInstruction(Action<TokenBuilder> configureBuilder) =>
ParseTokens(configureBuilder, DockerfileModel.CopyInstruction.Parse);

public DockerfileBuilder EntrypointInstruction(string command) =>
AddConstruct(DockerfileModel.EntrypointInstruction.Create(command, EscapeChar));

public DockerfileBuilder EntrypointInstruction(IEnumerable<string> commands) =>
AddConstruct(DockerfileModel.EntrypointInstruction.Create(commands, EscapeChar));

public DockerfileBuilder EntrypointInstruction(Action<TokenBuilder> configureBuilder) =>
ParseTokens(configureBuilder, DockerfileModel.EntrypointInstruction.Parse);

public DockerfileBuilder FromInstruction(string imageName, string? stageName = null, string? platform = null) =>
AddConstruct(
DockerfileModel.FromInstruction.Create(imageName, stageName, platform, EscapeChar));
Expand Down
2 changes: 1 addition & 1 deletion src/DockerfileModel/DockerfileModel/DockerfileParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ internal static class DockerfileParser
{ "ARG", ArgInstruction.Parse },
{ "CMD", CommandInstruction.Parse },
{ "COPY", CopyInstruction.Parse },
{ "ENTRYPOINT", GenericInstruction.Parse },
{ "ENTRYPOINT", EntrypointInstruction.Parse },
{ "EXPOSE", GenericInstruction.Parse },
{ "ENV", GenericInstruction.Parse },
{ "FROM", FromInstruction.Parse },
Expand Down
67 changes: 67 additions & 0 deletions src/DockerfileModel/DockerfileModel/EntrypointInstruction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using System.Collections.Generic;
using System.Linq;
using DockerfileModel.Tokens;
using Sprache;
using Validation;
using static DockerfileModel.ParseHelper;

namespace DockerfileModel
{
public class EntrypointInstruction : Instruction
{
private EntrypointInstruction(IEnumerable<Token> tokens) : base(tokens)
{
}

public Command Command
{
get => this.Tokens.OfType<Command>().First();
set
{
Requires.NotNull(value, nameof(value));
SetToken(Command, value);
}
}

public static EntrypointInstruction Parse(string text, char escapeChar = Dockerfile.DefaultEscapeChar) =>
new EntrypointInstruction(GetTokens(text, GetInnerParser(escapeChar)));

public static Parser<EntrypointInstruction> GetParser(char escapeChar = Dockerfile.DefaultEscapeChar) =>
from tokens in GetInnerParser(escapeChar)
select new EntrypointInstruction(tokens);

public static EntrypointInstruction Create(string command, char escapeChar = Dockerfile.DefaultEscapeChar)
{
Requires.NotNullOrEmpty(command, nameof(command));
return Parse($"ENTRYPOINT {command}", escapeChar);
}

public static EntrypointInstruction Create(IEnumerable<string> commands, char escapeChar = Dockerfile.DefaultEscapeChar)
{
Requires.NotNullEmptyOrNullElements(commands, nameof(commands));
return Parse($"ENTRYPOINT {StringHelper.FormatAsJson(commands)}", escapeChar);
}

public override string? ResolveVariables(char escapeChar, IDictionary<string, string?>? variables = null, ResolutionOptions? options = null)
{
// Do not resolve variables for the command of an ENTRYPOINT instruction. It is shell/runtime-specific.
return ToString();
}

private static Parser<IEnumerable<Token>> GetInnerParser(char escapeChar = Dockerfile.DefaultEscapeChar) =>
Instruction("ENTRYPOINT", escapeChar,
GetArgsParser(escapeChar));

private static Parser<IEnumerable<Token>> GetArgsParser(char escapeChar) =>
from mounts in ArgTokens(MountFlag.GetParser(escapeChar).AsEnumerable(), escapeChar).Many()
from whitespace in Whitespace()
from command in ArgTokens(GetCommandParser(escapeChar).AsEnumerable(), escapeChar)
select ConcatTokens(
mounts.Flatten(), whitespace, command);

private static Parser<Command> GetCommandParser(char escapeChar) =>
ExecFormCommand.GetParser(escapeChar)
.Cast<ExecFormCommand, Command>()
.XOr(ShellFormCommand.GetParser(escapeChar));
}
}