Skip to content

Commit 960d1df

Browse files
authored
feat: add CodeBuild support (#47)
1 parent 710a5d5 commit 960d1df

5 files changed

Lines changed: 202 additions & 0 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System.Collections.Generic;
2+
using JetBrains.Annotations;
3+
4+
namespace Testcontainers.Floci;
5+
6+
/// <summary>
7+
/// Configuration for Floci's CodeBuild emulation.
8+
/// </summary>
9+
/// <remarks>
10+
/// CodeBuild is a container-based service: Floci runs each build inside a Docker container spawned
11+
/// via the Docker socket (mounted automatically).
12+
/// <code>
13+
/// await using var floci = new FlociBuilder()
14+
/// .WithCodeBuild(new CodeBuildConfig())
15+
/// .Build();
16+
/// </code>
17+
/// </remarks>
18+
[PublicAPI]
19+
public sealed record CodeBuildConfig : FlociServiceConfig
20+
{
21+
/// <summary>
22+
/// Gets the Docker network that spawned build containers join, or <see langword="null" /> to
23+
/// use the default bridge network.
24+
/// </summary>
25+
public string? DockerNetwork { get; init; }
26+
27+
/// <inheritdoc />
28+
protected override string ServiceKey => "CODEBUILD";
29+
30+
/// <inheritdoc />
31+
internal override bool RequiresDockerAccess => true;
32+
33+
/// <inheritdoc />
34+
protected override void AddSettings(IDictionary<string, string> env, string prefix)
35+
{
36+
if (!string.IsNullOrEmpty(DockerNetwork))
37+
{
38+
env[prefix + "DOCKER_NETWORK"] = DockerNetwork!;
39+
}
40+
}
41+
}

src/Testcontainers.Floci/FlociBuilder.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,14 @@ public FlociBuilder WithAvailabilityZone(string availabilityZone)
221221
/// <returns>A configured instance of <see cref="FlociBuilder" />.</returns>
222222
public FlociBuilder WithEc2(Ec2Config config) => WithServiceConfig(config);
223223

224+
/// <summary>
225+
/// Configures Floci's CodeBuild emulation. Container-based: mounts the Docker socket so Floci
226+
/// can run each build inside a spawned container.
227+
/// </summary>
228+
/// <param name="config">The CodeBuild configuration.</param>
229+
/// <returns>A configured instance of <see cref="FlociBuilder" />.</returns>
230+
public FlociBuilder WithCodeBuild(CodeBuildConfig config) => WithServiceConfig(config);
231+
224232
/// <summary>
225233
/// Configures Floci's CodeDeploy emulation.
226234
/// </summary>
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using System.Collections.Generic;
2+
using System.Threading.Tasks;
3+
using Amazon.CodeBuild;
4+
using Amazon.CodeBuild.Model;
5+
using Testcontainers.Floci;
6+
using Xunit;
7+
8+
namespace Testcontainers.Floci.Tests;
9+
10+
public sealed class CodeBuildServiceTest : IAsyncLifetime
11+
{
12+
private const string ProjectName = "floci-test";
13+
14+
private readonly FlociContainer _floci = new FlociBuilder(TestImages.Floci)
15+
.WithCodeBuild(new CodeBuildConfig())
16+
.Build();
17+
18+
public Task InitializeAsync() => _floci.StartAsync();
19+
20+
public async Task DisposeAsync()
21+
{
22+
try
23+
{
24+
using var cb = CreateClient();
25+
await cb.DeleteProjectAsync(new DeleteProjectRequest { Name = ProjectName });
26+
}
27+
catch (AmazonCodeBuildException)
28+
{
29+
// The container is being disposed anyway; nothing actionable here.
30+
}
31+
32+
await _floci.DisposeAsync();
33+
}
34+
35+
private AmazonCodeBuildClient CreateClient()
36+
{
37+
return new AmazonCodeBuildClient(
38+
_floci.AccessKey,
39+
_floci.SecretKey,
40+
new AmazonCodeBuildConfig
41+
{
42+
ServiceURL = _floci.GetEndpoint(),
43+
AuthenticationRegion = _floci.Region,
44+
});
45+
}
46+
47+
[Fact]
48+
public async Task RunsABuildToCompletion()
49+
{
50+
using var cb = CreateClient();
51+
52+
// Unlike the upstream Java test (which only lists projects), we run a real build: Floci
53+
// spawns a build container, executes the buildspec, and reports the terminal status. A
54+
// tiny alpine image keeps the build container pull cheap. (Verified separately that a
55+
// failing buildspec yields FAILED, so SUCCEEDED genuinely reflects buildspec execution.)
56+
await cb.CreateProjectAsync(new CreateProjectRequest
57+
{
58+
Name = ProjectName,
59+
Source = new ProjectSource
60+
{
61+
Type = SourceType.NO_SOURCE,
62+
Buildspec = "version: 0.2\nphases:\n build:\n commands:\n - echo hello-from-floci",
63+
},
64+
Artifacts = new ProjectArtifacts { Type = ArtifactsType.NO_ARTIFACTS },
65+
Environment = new ProjectEnvironment
66+
{
67+
Type = EnvironmentType.LINUX_CONTAINER,
68+
Image = "public.ecr.aws/docker/library/alpine:3.20",
69+
ComputeType = ComputeType.BUILD_GENERAL1_SMALL,
70+
},
71+
ServiceRole = "arn:aws:iam::000000000000:role/codebuild-role",
72+
});
73+
74+
var started = await cb.StartBuildAsync(new StartBuildRequest { ProjectName = ProjectName });
75+
var buildId = started.Build.Id;
76+
77+
var status = await WaitForTerminalStatusAsync(cb, buildId);
78+
79+
Assert.Equal(StatusType.SUCCEEDED, status);
80+
}
81+
82+
private static async Task<StatusType> WaitForTerminalStatusAsync(AmazonCodeBuildClient cb, string buildId)
83+
{
84+
for (var attempt = 0; attempt < 60; attempt++)
85+
{
86+
var builds = await cb.BatchGetBuildsAsync(new BatchGetBuildsRequest
87+
{
88+
Ids = new List<string> { buildId },
89+
});
90+
91+
var status = builds.Builds[0].BuildStatus;
92+
if (status != StatusType.IN_PROGRESS)
93+
{
94+
return status;
95+
}
96+
97+
await Task.Delay(2000);
98+
}
99+
100+
throw new Xunit.Sdk.XunitException("CodeBuild build did not reach a terminal status within the timeout.");
101+
}
102+
}

tests/Testcontainers.Floci.IntegrationTests/Testcontainers.Floci.IntegrationTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<PackageReference Include="AWSSDK.CloudWatch" Version="4.0.3" />
2323
<PackageReference Include="AWSSDK.CloudFront" Version="4.0.3" />
2424
<PackageReference Include="AWSSDK.CloudWatchLogs" Version="4.0.3" />
25+
<PackageReference Include="AWSSDK.CodeBuild" Version="4.0.6.4" />
2526
<PackageReference Include="AWSSDK.CodeDeploy" Version="4.0.3" />
2627
<PackageReference Include="AWSSDK.ConfigService" Version="4.0.3" />
2728
<PackageReference Include="AWSSDK.CostExplorer" Version="4.0.3" />
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using Testcontainers.Floci;
2+
using Xunit;
3+
4+
namespace Testcontainers.Floci.Tests;
5+
6+
public sealed class CodeBuildConfigTest
7+
{
8+
[Fact]
9+
public void DefaultsMatchUpstream()
10+
{
11+
var config = new CodeBuildConfig();
12+
13+
Assert.True(config.Enabled);
14+
Assert.Null(config.DockerNetwork);
15+
}
16+
17+
[Fact]
18+
public void RequiresDockerAccess()
19+
{
20+
Assert.True(new CodeBuildConfig().RequiresDockerAccess);
21+
}
22+
23+
[Fact]
24+
public void DefaultConfigEmitsUpstreamDefaultEnvVars()
25+
{
26+
var env = new CodeBuildConfig().BuildEnvironment();
27+
28+
Assert.Equal("true", env["FLOCI_SERVICES_CODEBUILD_ENABLED"]);
29+
// DockerNetwork is only emitted when set.
30+
Assert.DoesNotContain("FLOCI_SERVICES_CODEBUILD_DOCKER_NETWORK", env.Keys);
31+
}
32+
33+
[Fact]
34+
public void CustomConfigEmitsCustomEnvVars()
35+
{
36+
var env = new CodeBuildConfig { DockerNetwork = "floci-net" }.BuildEnvironment();
37+
38+
Assert.Equal("true", env["FLOCI_SERVICES_CODEBUILD_ENABLED"]);
39+
Assert.Equal("floci-net", env["FLOCI_SERVICES_CODEBUILD_DOCKER_NETWORK"]);
40+
}
41+
42+
[Fact]
43+
public void DisabledConfigEmitsOnlyTheEnabledFlag()
44+
{
45+
var env = new CodeBuildConfig { Enabled = false }.BuildEnvironment();
46+
47+
Assert.Equal("false", env["FLOCI_SERVICES_CODEBUILD_ENABLED"]);
48+
Assert.DoesNotContain("FLOCI_SERVICES_CODEBUILD_DOCKER_NETWORK", env.Keys);
49+
}
50+
}

0 commit comments

Comments
 (0)