Skip to content

Commit

Permalink
Add support for USER instruction (#86)
Browse files Browse the repository at this point in the history
  • Loading branch information
mthalman authored Dec 16, 2020
1 parent fff59e1 commit 74ed870
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ public void BuildAllConstructs()
.ParserDirective("escape", "\\")
.RunInstruction("echo hi")
.ShellInstruction("cmd")
.StopSignalInstruction("1");
.StopSignalInstruction("1")
.UserInstruction("test");

string expectedOutput =
"ADD src dst" + Environment.NewLine +
Expand All @@ -64,7 +65,8 @@ public void BuildAllConstructs()
"# escape=\\" + Environment.NewLine +
"RUN echo hi" + Environment.NewLine +
"SHELL [\"cmd\"]" + Environment.NewLine +
"STOPSIGNAL 1" + Environment.NewLine;
"STOPSIGNAL 1" + Environment.NewLine +
"USER test" + Environment.NewLine;

Assert.Equal(expectedOutput, builder.Dockerfile.ToString());
Assert.Equal(expectedOutput, builder.ToString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public void BuildAllTokens()
{
TokenBuilder builder = new TokenBuilder();
builder
.ChangeOwner("user")
.UserAccount("user")
.Comment("comment")
.ExecFormCommand("cmd1", "cmd2")
.FromFlag("stage")
Expand Down
187 changes: 187 additions & 0 deletions src/DockerfileModel/DockerfileModel.Tests/UserInstructionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
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 UserInstructionTests
{
[Theory]
[MemberData(nameof(ParseTestInput))]
public void Parse(UserInstructionParseTestScenario scenario)
{
if (scenario.ParseExceptionPosition is null)
{
UserInstruction result = UserInstruction.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>(
() => UserInstruction.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)
{
UserInstruction result = new UserInstruction(scenario.User, scenario.Group);

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

[Fact]
public void Maintainer()
{
UserInstruction result = new UserInstruction("test");
Assert.Equal("test", result.UserAccount.ToString());
Assert.Equal("USER test", result.ToString());

result.UserAccount = new UserAccount("testa", "testb");
Assert.Equal("testa:testb", result.UserAccount.ToString());
Assert.Equal("USER testa:testb", result.ToString());

Assert.Throws<ArgumentNullException>(() => result.UserAccount = null);
}

public static IEnumerable<object[]> ParseTestInput()
{
UserInstructionParseTestScenario[] testInputs = new UserInstructionParseTestScenario[]
{
new UserInstructionParseTestScenario
{
Text = "USER name",
TokenValidators = new Action<Token>[]
{
token => ValidateKeyword(token, "USER"),
token => ValidateWhitespace(token, " "),
token => ValidateAggregate<UserAccount>(token, "name",
token => ValidateLiteral(token, "name"))
},
Validate = result =>
{
Assert.Empty(result.Comments);
Assert.Equal("USER", result.InstructionName);
Assert.Equal("name", result.UserAccount.ToString());
}
},
new UserInstructionParseTestScenario
{
Text = "USER user:group",
TokenValidators = new Action<Token>[]
{
token => ValidateKeyword(token, "USER"),
token => ValidateWhitespace(token, " "),
token => ValidateAggregate<UserAccount>(token, "user:group",
token => ValidateLiteral(token, "user"),
token => ValidateSymbol(token, ':'),
token => ValidateLiteral(token, "group"))
},
Validate = result =>
{
Assert.Empty(result.Comments);
Assert.Equal("USER", result.InstructionName);
Assert.Equal("user:group", result.UserAccount.ToString());
}
},
new UserInstructionParseTestScenario
{
Text = "USER $var",
TokenValidators = new Action<Token>[]
{
token => ValidateKeyword(token, "USER"),
token => ValidateWhitespace(token, " "),
token => ValidateAggregate<UserAccount>(token, "$var",
token => ValidateQuotableAggregate<LiteralToken>(token, "$var", null,
token => ValidateAggregate<VariableRefToken>(token, "$var",
token => ValidateString(token, "var"))))
},
Validate = result =>
{
Assert.Empty(result.Comments);
Assert.Equal("USER", result.InstructionName);
Assert.Equal("$var", result.UserAccount.ToString());
}
},
new UserInstructionParseTestScenario
{
Text = "USER `\n name",
EscapeChar = '`',
TokenValidators = new Action<Token>[]
{
token => ValidateKeyword(token, "USER"),
token => ValidateWhitespace(token, " "),
token => ValidateLineContinuation(token, '`', "\n"),
token => ValidateWhitespace(token, " "),
token => ValidateAggregate<UserAccount>(token, "name",
token => ValidateLiteral(token, "name"))
},
Validate = result =>
{
Assert.Empty(result.Comments);
Assert.Equal("USER", result.InstructionName);
Assert.Equal("name", result.UserAccount.ToString());
}
}
};

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

public static IEnumerable<object[]> CreateTestInput()
{
CreateTestScenario[] testInputs = new CreateTestScenario[]
{
new CreateTestScenario
{
User = "name",
TokenValidators = new Action<Token>[]
{
token => ValidateKeyword(token, "USER"),
token => ValidateWhitespace(token, " "),
token => ValidateAggregate<UserAccount>(token, "name",
token => ValidateLiteral(token, "name"))
}
},
new CreateTestScenario
{
User = "user",
Group = "group",
TokenValidators = new Action<Token>[]
{
token => ValidateKeyword(token, "USER"),
token => ValidateWhitespace(token, " "),
token => ValidateAggregate<UserAccount>(token, "user:group",
token => ValidateLiteral(token, "user"),
token => ValidateSymbol(token, ':'),
token => ValidateLiteral(token, "group"))
}
}
};

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

public class UserInstructionParseTestScenario : ParseTestScenario<UserInstruction>
{
public char EscapeChar { get; set; }
}

public class CreateTestScenario : TestScenario<UserInstruction>
{
public string User { get; set; }
public string Group { get; set; }
}
}
}
9 changes: 9 additions & 0 deletions src/DockerfileModel/DockerfileModel/DockerfileBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,15 @@ public DockerfileBuilder StopSignalInstruction(string signal) =>
public DockerfileBuilder StopSignalInstruction(Action<TokenBuilder> configureBuilder) =>
ParseTokens(configureBuilder, DockerfileModel.StopSignalInstruction.Parse);

public DockerfileBuilder UserInstruction(string user, string? group = null) =>
AddConstruct(new UserInstruction(user, group, EscapeChar));

public DockerfileBuilder UserInstruction(UserAccount userAccount) =>
AddConstruct(new UserInstruction(userAccount, EscapeChar));

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

private DockerfileBuilder ParseTokens(Action<TokenBuilder> configureBuilder, Func<string, DockerfileConstruct> parseConstruct)
{
TokenBuilder builder = new TokenBuilder
Expand Down
2 changes: 1 addition & 1 deletion src/DockerfileModel/DockerfileModel/Instruction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public abstract class Instruction : DockerfileConstruct, ICommentable
{ "RUN", RunInstruction.Parse },
{ "SHELL", ShellInstruction.Parse },
{ "STOPSIGNAL", StopSignalInstruction.Parse },
{ "USER", GenericInstruction.Parse },
{ "USER", UserInstruction.Parse },
{ "VOLUME", GenericInstruction.Parse },
{ "WORKDIR", GenericInstruction.Parse },
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ private static IEnumerable<Token> GetTokens(string maintainer, char escapeChar)
return GetTokens($"MAINTAINER {(String.IsNullOrEmpty(maintainer) ? "\"\"" : maintainer)}", GetInnerParser(escapeChar));
}

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

private static Parser<IEnumerable<Token>> GetArgsParser(char escapeChar) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ private static IEnumerable<Token> GetTokens(string signal, char escapeChar)
return GetTokens($"STOPSIGNAL {signal}", GetInnerParser(escapeChar));
}

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

private static Parser<IEnumerable<Token>> GetArgsParser(char escapeChar) =>
Expand Down
2 changes: 1 addition & 1 deletion src/DockerfileModel/DockerfileModel/Tokens/TokenBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class TokenBuilder

public IList<Token> Tokens { get; } = new List<Token>();

public TokenBuilder ChangeOwner(string user, string? group = null) =>
public TokenBuilder UserAccount(string user, string? group = null) =>
AddToken(new UserAccount(user, group, EscapeChar));

public TokenBuilder Comment(string comment) =>
Expand Down
38 changes: 15 additions & 23 deletions src/DockerfileModel/DockerfileModel/UserInstruction.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using DockerfileModel.Tokens;
using Sprache;
Expand All @@ -10,32 +9,27 @@ namespace DockerfileModel
{
public class UserInstruction : Instruction
{
public UserInstruction(string maintainer, char escapeChar = Dockerfile.DefaultEscapeChar)
: this(GetTokens(maintainer, escapeChar))
public UserInstruction(string user, string? group = null, char escapeChar = Dockerfile.DefaultEscapeChar)
: this(new UserAccount(user, group, escapeChar))
{
}

private UserInstruction(IEnumerable<Token> tokens) : base(tokens)
public UserInstruction(UserAccount userAccount, char escapeChar = Dockerfile.DefaultEscapeChar)
: this(GetTokens(userAccount, escapeChar))
{
}

public string Maintainer
private UserInstruction(IEnumerable<Token> tokens) : base(tokens)
{
get => MaintainerToken.Value;
set
{
Requires.NotNull(value, nameof(value));
MaintainerToken.Value = value;
}
}

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

Expand All @@ -46,18 +40,16 @@ public static Parser<UserInstruction> GetParser(char escapeChar = Dockerfile.Def
from tokens in GetInnerParser(escapeChar)
select new UserInstruction(tokens);

private static IEnumerable<Token> GetTokens(string maintainer, char escapeChar)
private static IEnumerable<Token> GetTokens(UserAccount userAccount, char escapeChar)
{
Requires.NotNull(maintainer, nameof(maintainer));
return GetTokens($"MAINTAINER {(String.IsNullOrEmpty(maintainer) ? "\"\"" : maintainer)}", GetInnerParser(escapeChar));
Requires.NotNull(userAccount, nameof(userAccount));
return GetTokens($"USER {userAccount}", GetInnerParser(escapeChar));
}

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

private static Parser<IEnumerable<Token>> GetArgsParser(char escapeChar) =>
ArgTokens(
LiteralWithVariables(
escapeChar, whitespaceMode: WhitespaceMode.Allowed).AsEnumerable(), escapeChar, excludeTrailingWhitespace: true);
ArgTokens(UserAccount.GetParser(escapeChar).AsEnumerable(), escapeChar, excludeTrailingWhitespace: true);
}
}

0 comments on commit 74ed870

Please sign in to comment.