Skip to content

Commit 4089673

Browse files
aus-design-web-mobileRelatedItemSearchUsertrejjam
authored
add typed secrets and configs (#195)
* add typed secrets and configs * feat: add UnixFileMode type for file permission handling - Add UnixFileMode record struct with notation preservation (Octal, OctalWithO, RawInt) - Add UnixFileModeConverter for YAML serialization/deserialization - Support parsing: 0755 (octal), 0o755 (octal with 'o'), 256 (decimal) - Preserve original notation on round-trip serialization - Replace int? Mode with UnixFileMode? in ServiceSecret, ServiceConfig, ServiceVolumeTmpfs - Simplify collection converters to use rootDeserializer instead of manual parsing - Register UnixFileModeConverter in ComposeExtensions * test: add comprehensive UnixFileMode tests - Test parsing for all notations: octal (0755), octal with 'o' (0o755), decimal (493) - Test setuid/setgid/sticky bit modes (04777, 02755, etc.) - Test invalid octal values (0800, 0888) throw exceptions - Test notation preservation on ToNotationString() - Test implicit conversion roundtrip equality - Test equality includes notation (record struct semantics) - Add integration tests for mode serialization in compose files * refactor: extract ServiceItemCollection base class Extract common IList<T> implementation from ServiceSecretCollection, ServiceConfigCollection, and ServiceVolumeCollection into abstract ServiceItemCollection<T> base class. Each concrete collection still implements IList<string> explicitly due to C# interface unification limitations (CS0695). --------- Co-authored-by: Jasim Schluter <[email protected]> Co-authored-by: Jan Trejbal <[email protected]>
1 parent 5129d9f commit 4089673

23 files changed

+1570
-61
lines changed

src/DockerComposeBuilder.Tests/ComposeBuilderTests.cs

Lines changed: 662 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
using DockerComposeBuilder.Model.Services;
2+
using System;
3+
using Xunit;
4+
5+
namespace DockerComposeBuilder.Tests;
6+
7+
public class UnixFileModeTests
8+
{
9+
[Theory]
10+
[InlineData("0400", 256, UnixFileModeNotation.Octal)]
11+
[InlineData("0440", 288, UnixFileModeNotation.Octal)]
12+
[InlineData("0444", 292, UnixFileModeNotation.Octal)]
13+
[InlineData("0644", 420, UnixFileModeNotation.Octal)]
14+
[InlineData("0755", 493, UnixFileModeNotation.Octal)]
15+
[InlineData("0777", 511, UnixFileModeNotation.Octal)]
16+
public void Parse_OctalNotation_ReturnsCorrectValue(string input, int expectedIntValue, UnixFileModeNotation expectedNotation)
17+
{
18+
var mode = UnixFileMode.Parse(input);
19+
20+
Assert.Equal(expectedIntValue, mode.IntValue);
21+
Assert.Equal(expectedNotation, mode.Notation);
22+
}
23+
24+
[Theory]
25+
[InlineData("0o400", 256, UnixFileModeNotation.OctalWithO)]
26+
[InlineData("0o440", 288, UnixFileModeNotation.OctalWithO)]
27+
[InlineData("0o444", 292, UnixFileModeNotation.OctalWithO)]
28+
[InlineData("0o644", 420, UnixFileModeNotation.OctalWithO)]
29+
[InlineData("0o755", 493, UnixFileModeNotation.OctalWithO)]
30+
[InlineData("0o777", 511, UnixFileModeNotation.OctalWithO)]
31+
[InlineData("0O755", 493, UnixFileModeNotation.OctalWithO)]
32+
public void Parse_OctalWithONotation_ReturnsCorrectValue(string input, int expectedIntValue, UnixFileModeNotation expectedNotation)
33+
{
34+
var mode = UnixFileMode.Parse(input);
35+
36+
Assert.Equal(expectedIntValue, mode.IntValue);
37+
Assert.Equal(expectedNotation, mode.Notation);
38+
}
39+
40+
[Theory]
41+
[InlineData("256", 256, UnixFileModeNotation.RawInt)]
42+
[InlineData("288", 288, UnixFileModeNotation.RawInt)]
43+
[InlineData("292", 292, UnixFileModeNotation.RawInt)]
44+
[InlineData("420", 420, UnixFileModeNotation.RawInt)]
45+
[InlineData("493", 493, UnixFileModeNotation.RawInt)]
46+
[InlineData("511", 511, UnixFileModeNotation.RawInt)]
47+
public void Parse_RawIntNotation_ReturnsCorrectValue(string input, int expectedIntValue, UnixFileModeNotation expectedNotation)
48+
{
49+
var mode = UnixFileMode.Parse(input);
50+
51+
Assert.Equal(expectedIntValue, mode.IntValue);
52+
Assert.Equal(expectedNotation, mode.Notation);
53+
}
54+
55+
[Theory]
56+
[InlineData("04777", 2559)]
57+
[InlineData("02755", 1517)]
58+
[InlineData("01755", 1005)]
59+
[InlineData("06755", 3565)]
60+
[InlineData("07777", 4095)]
61+
public void Parse_SetuidSetgidStickyBit_ReturnsCorrectValue(string input, int expectedIntValue)
62+
{
63+
var mode = UnixFileMode.Parse(input);
64+
65+
Assert.Equal(expectedIntValue, mode.IntValue);
66+
Assert.Equal(UnixFileModeNotation.Octal, mode.Notation);
67+
}
68+
69+
[Theory]
70+
[InlineData("0o4777", 2559)]
71+
[InlineData("0o2755", 1517)]
72+
[InlineData("0o1755", 1005)]
73+
[InlineData("0o7777", 4095)]
74+
public void Parse_SetuidSetgidStickyBitWithO_ReturnsCorrectValue(string input, int expectedIntValue)
75+
{
76+
var mode = UnixFileMode.Parse(input);
77+
78+
Assert.Equal(expectedIntValue, mode.IntValue);
79+
Assert.Equal(UnixFileModeNotation.OctalWithO, mode.Notation);
80+
}
81+
82+
[Theory]
83+
[InlineData("2559", 2559)]
84+
[InlineData("1517", 1517)]
85+
[InlineData("1005", 1005)]
86+
[InlineData("4095", 4095)]
87+
public void Parse_SetuidSetgidStickyBitRawInt_ReturnsCorrectValue(string input, int expectedIntValue)
88+
{
89+
var mode = UnixFileMode.Parse(input);
90+
91+
Assert.Equal(expectedIntValue, mode.IntValue);
92+
Assert.Equal(UnixFileModeNotation.RawInt, mode.Notation);
93+
}
94+
95+
[Theory]
96+
[InlineData("0800")]
97+
[InlineData("0888")]
98+
[InlineData("0999")]
99+
[InlineData("0o800")]
100+
[InlineData("0o888")]
101+
public void Parse_InvalidOctalDigits_ThrowsException(string input)
102+
{
103+
Assert.ThrowsAny<Exception>(() => UnixFileMode.Parse(input));
104+
}
105+
106+
[Theory]
107+
[InlineData("800", 800)]
108+
[InlineData("888", 888)]
109+
[InlineData("999", 999)]
110+
[InlineData("789", 789)]
111+
[InlineData("918", 918)]
112+
public void Parse_RawIntWithAnyDigits_ParsesAsDecimal(string input, int expectedValue)
113+
{
114+
var mode = UnixFileMode.Parse(input);
115+
116+
Assert.Equal(expectedValue, mode.IntValue);
117+
Assert.Equal(UnixFileModeNotation.RawInt, mode.Notation);
118+
}
119+
120+
[Fact]
121+
public void Parse_EmptyString_ThrowsArgumentException()
122+
{
123+
Assert.Throws<ArgumentException>(() => UnixFileMode.Parse(""));
124+
}
125+
126+
[Fact]
127+
public void Parse_NullString_ThrowsArgumentException()
128+
{
129+
Assert.Throws<ArgumentException>(() => UnixFileMode.Parse(null!));
130+
}
131+
132+
[Theory]
133+
[InlineData("0400", "0400")]
134+
[InlineData("0755", "0755")]
135+
[InlineData("04777", "04777")]
136+
[InlineData("0o400", "0o400")]
137+
[InlineData("0o755", "0o755")]
138+
[InlineData("0o4777", "0o4777")]
139+
[InlineData("256", "256")]
140+
[InlineData("493", "493")]
141+
[InlineData("2559", "2559")]
142+
public void ToNotationString_PreservesFormat(string input, string expected)
143+
{
144+
var mode = UnixFileMode.Parse(input);
145+
Assert.Equal(expected, mode.ToNotationString());
146+
}
147+
148+
[Theory]
149+
[InlineData("0400", "0400")]
150+
[InlineData("0o400", "0400")]
151+
[InlineData("256", "0400")]
152+
public void ToOctalString_AllNotations_ReturnsOctalFormat(string input, string expected)
153+
{
154+
var mode = UnixFileMode.Parse(input);
155+
Assert.Equal(expected, mode.ToOctalString());
156+
}
157+
158+
[Fact]
159+
public void FromDecimal_ValidValue_CreatesMode()
160+
{
161+
var mode = UnixFileMode.FromDecimal(256);
162+
163+
Assert.Equal(256, mode.IntValue);
164+
Assert.Equal(UnixFileModeNotation.RawInt, mode.Notation);
165+
}
166+
167+
[Fact]
168+
public void ImplicitConversion_IntToMode_Works()
169+
{
170+
UnixFileMode mode = 256;
171+
172+
Assert.Equal(256, mode.IntValue);
173+
}
174+
175+
[Fact]
176+
public void ImplicitConversion_ModeToInt_Works()
177+
{
178+
var mode = UnixFileMode.Parse("0400");
179+
180+
int value = mode;
181+
182+
Assert.Equal(256, value);
183+
}
184+
185+
[Fact]
186+
public void ImplicitConversion_Roundtrip_IsEqual()
187+
{
188+
UnixFileMode original = 256;
189+
190+
int intValue = original;
191+
UnixFileMode roundtripped = intValue;
192+
193+
Assert.Equal(original, roundtripped);
194+
}
195+
196+
[Fact]
197+
public void Equality_SameValueAndNotation_AreEqual()
198+
{
199+
var mode1 = UnixFileMode.Parse("0400");
200+
var mode2 = UnixFileMode.Parse("0400");
201+
202+
Assert.Equal(mode1, mode2);
203+
Assert.True(mode1 == mode2);
204+
}
205+
206+
[Fact]
207+
public void Equality_SameValueDifferentNotation_AreNotEqual()
208+
{
209+
var mode1 = UnixFileMode.Parse("0400");
210+
var mode2 = UnixFileMode.Parse("0o400");
211+
var mode3 = UnixFileMode.Parse("256");
212+
213+
Assert.NotEqual(mode1, mode2);
214+
Assert.NotEqual(mode2, mode3);
215+
Assert.NotEqual(mode1, mode3);
216+
Assert.Equal(mode1.IntValue, mode2.IntValue);
217+
Assert.Equal(mode2.IntValue, mode3.IntValue);
218+
}
219+
220+
[Fact]
221+
public void Equality_DifferentValue_AreNotEqual()
222+
{
223+
var mode1 = UnixFileMode.Parse("0400");
224+
var mode2 = UnixFileMode.Parse("0755");
225+
226+
Assert.NotEqual(mode1, mode2);
227+
Assert.True(mode1 != mode2);
228+
}
229+
230+
[Fact]
231+
public void GetHashCode_SameValueAndNotation_SameHash()
232+
{
233+
var mode1 = UnixFileMode.Parse("0400");
234+
var mode2 = UnixFileMode.Parse("0400");
235+
236+
Assert.Equal(mode1.GetHashCode(), mode2.GetHashCode());
237+
}
238+
239+
[Fact]
240+
public void ParseAsOctetFromInt_ValidValue_Works()
241+
{
242+
var mode = UnixFileMode.ParseAsOctetFromInt(755);
243+
244+
Assert.Equal(493, mode.IntValue);
245+
Assert.Equal(UnixFileModeNotation.RawInt, mode.Notation);
246+
}
247+
248+
[Fact]
249+
public void ParseAsOctetFromInt_InvalidDigit_ThrowsArgumentException()
250+
{
251+
var ex = Assert.Throws<ArgumentException>(() => UnixFileMode.ParseAsOctetFromInt(800));
252+
Assert.Contains("Invalid octal digit 8", ex.Message);
253+
}
254+
}

src/DockerComposeBuilder/Builders/Builder.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ public static SecretBuilder MakeSecret(string name)
3737
return new SecretBuilder().WithName(name);
3838
}
3939

40+
public static ConfigBuilder MakeConfig()
41+
{
42+
return new ConfigBuilder();
43+
}
44+
45+
public static ConfigBuilder MakeConfig(string name)
46+
{
47+
return new ConfigBuilder().WithName(name);
48+
}
4049

4150
public static NetworkBuilder MakeNetwork()
4251
{

src/DockerComposeBuilder/Builders/ComposeBuilder.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,17 @@ public ComposeBuilder WithSecrets(params Secret[] secrets) => WithT(
5858
secrets
5959
);
6060

61+
/// <summary>
62+
/// Add configs to the compose object
63+
/// </summary>
64+
/// <param name="configs"></param>
65+
/// <returns></returns>
66+
public ComposeBuilder WithConfigs(params Config[] configs) => WithT(
67+
() => WorkingObject.Configs,
68+
x => WorkingObject.Configs = x,
69+
configs
70+
);
71+
6172
/// <summary>
6273
/// Add services to the Compose object
6374
/// </summary>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using DockerComposeBuilder.Builders.Base;
2+
using DockerComposeBuilder.Model;
3+
4+
namespace DockerComposeBuilder.Builders;
5+
6+
public class ConfigBuilder : BuilderBase<ConfigBuilder, Config>
7+
{
8+
internal ConfigBuilder()
9+
{
10+
}
11+
12+
public ConfigBuilder WithFile(string file)
13+
{
14+
return WithProperty("file", file);
15+
}
16+
17+
public ConfigBuilder SetExternal(bool isExternal)
18+
{
19+
return WithProperty("external", isExternal);
20+
}
21+
22+
public ConfigBuilder WithEnvironment(string environmentVariable)
23+
{
24+
return WithProperty("environment", environmentVariable);
25+
}
26+
27+
public ConfigBuilder WithContent(string content)
28+
{
29+
return WithProperty("content", content);
30+
}
31+
}

src/DockerComposeBuilder/Builders/SecretBuilder.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,9 @@ public SecretBuilder SetExternal(bool isExternal)
1818
{
1919
return WithProperty("external", isExternal);
2020
}
21+
22+
public SecretBuilder WithEnvironment(string environmentVariable)
23+
{
24+
return WithProperty("environment", environmentVariable);
25+
}
2126
}

src/DockerComposeBuilder/Builders/ServiceBuilder.cs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,10 +198,15 @@ public ServiceBuilder WithRestartPolicy(ERestartMode mode)
198198

199199
public ServiceBuilder WithSecrets(params string[] secrets)
200200
{
201-
if (WorkingObject.Secrets == null)
202-
{
203-
WorkingObject.Secrets = new List<string>();
204-
}
201+
WorkingObject.Secrets ??= new ServiceSecretCollection();
202+
203+
WorkingObject.Secrets.AddRange(secrets);
204+
return this;
205+
}
206+
207+
public ServiceBuilder WithSecrets(params ServiceSecret[] secrets)
208+
{
209+
WorkingObject.Secrets ??= new ServiceSecretCollection();
205210

206211
WorkingObject.Secrets.AddRange(secrets);
207212
return this;
@@ -212,6 +217,27 @@ public ServiceBuilder WithSecrets(params Secret[] secrets)
212217
return WithSecrets(secrets.Select(t => t.Name).ToArray());
213218
}
214219

220+
public ServiceBuilder WithConfigs(params string[] configs)
221+
{
222+
WorkingObject.Configs ??= new ServiceConfigCollection();
223+
224+
WorkingObject.Configs.AddRange(configs);
225+
return this;
226+
}
227+
228+
public ServiceBuilder WithConfigs(params ServiceConfig[] configs)
229+
{
230+
WorkingObject.Configs ??= new ServiceConfigCollection();
231+
232+
WorkingObject.Configs.AddRange(configs);
233+
return this;
234+
}
235+
236+
public ServiceBuilder WithConfigs(params Config[] configs)
237+
{
238+
return WithConfigs(configs.Select(t => t.Name).ToArray());
239+
}
240+
215241
public SwarmServiceBuilder WithSwarm()
216242
{
217243
return new SwarmServiceBuilder { WorkingObject = WorkingObject };

0 commit comments

Comments
 (0)