diff --git a/cli/azd/grpc/proto/models.proto b/cli/azd/grpc/proto/models.proto index f3c292ad983..e63c8134aae 100644 --- a/cli/azd/grpc/proto/models.proto +++ b/cli/azd/grpc/proto/models.proto @@ -117,6 +117,7 @@ message DockerProjectOptions { string tag = 7; bool remote_build = 8; repeated string build_args = 9; + string network = 10; } // ServiceContext defines the shared pipeline state across all phases of the service lifecycle diff --git a/cli/azd/pkg/azdext/mcp_security_test.go b/cli/azd/pkg/azdext/mcp_security_test.go index d4d568c41dc..1b2934e8551 100644 --- a/cli/azd/pkg/azdext/mcp_security_test.go +++ b/cli/azd/pkg/azdext/mcp_security_test.go @@ -12,6 +12,8 @@ import ( "path/filepath" "strings" "testing" + + "github.com/stretchr/testify/require" ) func TestMCPSecurityCheckURL_BlocksMetadataEndpoints(t *testing.T) { @@ -318,25 +320,12 @@ func TestMCPSecurityFluentBuilder(t *testing.T) { RedactHeaders("Authorization"). ValidatePathsWithinBase("/tmp") - if policy == nil { - t.Fatal("fluent builder should return non-nil policy") - } - - if !policy.blockMetadata { - t.Error("blockMetadata should be true") - } - if !policy.blockPrivate { - t.Error("blockPrivate should be true") - } - if !policy.requireHTTPS { - t.Error("requireHTTPS should be true") - } - if !policy.IsHeaderBlocked("Authorization") { - t.Error("Authorization should be blocked") - } - if len(policy.allowedBasePaths) != 1 { - t.Errorf("expected 1 base path, got %d", len(policy.allowedBasePaths)) - } + require.NotNil(t, policy, "fluent builder should return non-nil policy") + require.True(t, policy.blockMetadata, "blockMetadata should be true") + require.True(t, policy.blockPrivate, "blockPrivate should be true") + require.True(t, policy.requireHTTPS, "requireHTTPS should be true") + require.True(t, policy.IsHeaderBlocked("Authorization"), "Authorization should be blocked") + require.Len(t, policy.allowedBasePaths, 1, "expected 1 base path") } func TestSSRFSafeRedirect_SchemeDowngrade(t *testing.T) { diff --git a/cli/azd/pkg/azdext/models.pb.go b/cli/azd/pkg/azdext/models.pb.go index 731e5fd2227..33b0c307f7c 100644 --- a/cli/azd/pkg/azdext/models.pb.go +++ b/cli/azd/pkg/azdext/models.pb.go @@ -3,8 +3,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v6.32.1 +// protoc-gen-go v1.36.11 +// protoc v7.34.1 // source: models.proto package azdext @@ -1064,6 +1064,7 @@ type DockerProjectOptions struct { Tag string `protobuf:"bytes,7,opt,name=tag,proto3" json:"tag,omitempty"` RemoteBuild bool `protobuf:"varint,8,opt,name=remote_build,json=remoteBuild,proto3" json:"remote_build,omitempty"` BuildArgs []string `protobuf:"bytes,9,rep,name=build_args,json=buildArgs,proto3" json:"build_args,omitempty"` + Network string `protobuf:"bytes,10,opt,name=network,proto3" json:"network,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1161,6 +1162,13 @@ func (x *DockerProjectOptions) GetBuildArgs() []string { return nil } +func (x *DockerProjectOptions) GetNetwork() string { + if x != nil { + return x.Network + } + return "" +} + // ServiceContext defines the shared pipeline state across all phases of the service lifecycle type ServiceContext struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1428,7 +1436,7 @@ const file_models_proto_rawDesc = "" + "\fInfraOptions\x12\x1a\n" + "\bprovider\x18\x01 \x01(\tR\bprovider\x12\x12\n" + "\x04path\x18\x02 \x01(\tR\x04path\x12\x16\n" + - "\x06module\x18\x03 \x01(\tR\x06module\"\xfe\x01\n" + + "\x06module\x18\x03 \x01(\tR\x06module\"\x98\x02\n" + "\x14DockerProjectOptions\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12\x18\n" + "\acontext\x18\x02 \x01(\tR\acontext\x12\x1a\n" + @@ -1439,7 +1447,9 @@ const file_models_proto_rawDesc = "" + "\x03tag\x18\a \x01(\tR\x03tag\x12!\n" + "\fremote_build\x18\b \x01(\bR\vremoteBuild\x12\x1d\n" + "\n" + - "build_args\x18\t \x03(\tR\tbuildArgs\"\xe6\x01\n" + + "build_args\x18\t \x03(\tR\tbuildArgs\x12\x18\n" + + "\anetwork\x18\n" + + " \x01(\tR\anetwork\"\xe6\x01\n" + "\x0eServiceContext\x12*\n" + "\arestore\x18\x01 \x03(\v2\x10.azdext.ArtifactR\arestore\x12&\n" + "\x05build\x18\x02 \x03(\v2\x10.azdext.ArtifactR\x05build\x12*\n" + diff --git a/cli/azd/pkg/project/container_helper.go b/cli/azd/pkg/project/container_helper.go index 7d4f37d5f07..74c5d15e0ba 100644 --- a/cli/azd/pkg/project/container_helper.go +++ b/cli/azd/pkg/project/container_helper.go @@ -477,6 +477,7 @@ func (ch *ContainerHelper) Build( resolvedBuildArgs, dockerOptions.BuildSecrets, dockerEnv, + dockerOptions.Network, previewerWriter, ) ch.console.StopPreviewer(ctx, false) diff --git a/cli/azd/pkg/project/framework_service_docker.go b/cli/azd/pkg/project/framework_service_docker.go index d2d19409a29..68c8915deb1 100644 --- a/cli/azd/pkg/project/framework_service_docker.go +++ b/cli/azd/pkg/project/framework_service_docker.go @@ -28,6 +28,7 @@ type DockerProjectOptions struct { Image osutil.ExpandableString `yaml:"image,omitempty" json:"image"` Tag osutil.ExpandableString `yaml:"tag,omitempty" json:"tag"` RemoteBuild bool `yaml:"remoteBuild,omitempty" json:"remoteBuild,omitempty"` + Network string `yaml:"network,omitempty" json:"network,omitempty"` BuildArgs []osutil.ExpandableString `yaml:"buildArgs,omitempty" json:"buildArgs,omitempty"` // not supported from azure.yaml directly yet. Adding it for Aspire to use it, initially. // Aspire would pass the secret keys, which are env vars that azd will set just to run docker build. diff --git a/cli/azd/pkg/project/mapper_registry.go b/cli/azd/pkg/project/mapper_registry.go index 11d6fcf21af..a46285947a6 100644 --- a/cli/azd/pkg/project/mapper_registry.go +++ b/cli/azd/pkg/project/mapper_registry.go @@ -200,6 +200,7 @@ func registerProjectMappings() { Tag: tag, RemoteBuild: src.RemoteBuild, BuildArgs: buildArgs, + Network: src.Network, }, nil }) @@ -413,6 +414,7 @@ func registerProjectMappings() { Image: osutil.NewExpandableString(src.Image), Tag: osutil.NewExpandableString(src.Tag), RemoteBuild: src.RemoteBuild, + Network: src.Network, } if len(src.BuildArgs) > 0 { @@ -440,6 +442,7 @@ func registerProjectMappings() { Image: osutil.NewExpandableString(src.Image), Tag: osutil.NewExpandableString(src.Tag), RemoteBuild: src.RemoteBuild, + Network: src.Network, } if len(src.BuildArgs) > 0 { diff --git a/cli/azd/pkg/project/mapper_registry_test.go b/cli/azd/pkg/project/mapper_registry_test.go index 0da9294b52d..8d1a3a25cf3 100644 --- a/cli/azd/pkg/project/mapper_registry_test.go +++ b/cli/azd/pkg/project/mapper_registry_test.go @@ -401,6 +401,7 @@ func TestDockerProjectOptionsMapping(t *testing.T) { Context: ".", Platform: "linux/amd64", Target: "production", + Network: "host", RemoteBuild: true, } @@ -412,6 +413,7 @@ func TestDockerProjectOptionsMapping(t *testing.T) { require.Equal(t, ".", protoOptions.Context) require.Equal(t, "linux/amd64", protoOptions.Platform) require.Equal(t, "production", protoOptions.Target) + require.Equal(t, "host", protoOptions.Network) require.True(t, protoOptions.RemoteBuild) } @@ -669,6 +671,7 @@ func TestFromProtoDockerProjectOptionsMapping(t *testing.T) { Context: "..", Platform: "linux/arm64", Target: "test", + Network: "host", Registry: "testregistry.azurecr.io", Image: "testimage", Tag: "v2.0.0", @@ -684,6 +687,7 @@ func TestFromProtoDockerProjectOptionsMapping(t *testing.T) { require.Equal(t, "..", dockerOptions.Context) require.Equal(t, "linux/arm64", dockerOptions.Platform) require.Equal(t, "test", dockerOptions.Target) + require.Equal(t, "host", dockerOptions.Network) require.Equal(t, "testregistry.azurecr.io", dockerOptions.Registry.MustEnvsubst(func(string) string { return "" })) require.Equal(t, "testimage", dockerOptions.Image.MustEnvsubst(func(string) string { return "" })) require.Equal(t, "v2.0.0", dockerOptions.Tag.MustEnvsubst(func(string) string { return "" })) diff --git a/cli/azd/pkg/tools/docker/acceptance/acceptance_test.go b/cli/azd/pkg/tools/docker/acceptance/acceptance_test.go index 1ae7cdce616..1151949858b 100644 --- a/cli/azd/pkg/tools/docker/acceptance/acceptance_test.go +++ b/cli/azd/pkg/tools/docker/acceptance/acceptance_test.go @@ -97,6 +97,7 @@ func Test_DockerAcceptance(t *testing.T) { buildArgs, buildSecrets, buildEnv, + "", &buildOutput, ) require.NoError(t, err, "build should succeed") diff --git a/cli/azd/pkg/tools/docker/docker.go b/cli/azd/pkg/tools/docker/docker.go index d15356cc027..c670fe2f24c 100644 --- a/cli/azd/pkg/tools/docker/docker.go +++ b/cli/azd/pkg/tools/docker/docker.go @@ -77,6 +77,7 @@ func (d *Cli) Build( buildArgs []string, buildSecrets []string, buildEnv []string, + buildNetwork string, buildProgress io.Writer, ) (string, error) { if strings.TrimSpace(platform) == "" { @@ -105,6 +106,10 @@ func (d *Cli) Build( args = append(args, "--target", target) } + if buildNetwork != "" { + args = append(args, "--network", buildNetwork) + } + if tagName != "" { args = append(args, "-t", tagName) } diff --git a/cli/azd/pkg/tools/docker/docker_test.go b/cli/azd/pkg/tools/docker/docker_test.go index b13fefac23f..280bac76853 100644 --- a/cli/azd/pkg/tools/docker/docker_test.go +++ b/cli/azd/pkg/tools/docker/docker_test.go @@ -72,6 +72,7 @@ func Test_DockerBuild(t *testing.T) { buildArgs, nil, nil, + "", nil, ) @@ -131,6 +132,7 @@ func Test_DockerBuild(t *testing.T) { buildArgs, nil, nil, + "", nil, ) @@ -188,7 +190,7 @@ func Test_DockerBuildEmptyPlatform(t *testing.T) { }) result, err := docker.Build( - context.Background(), cwd, dockerFile, "", "", dockerContext, imageName, buildArgs, nil, nil, nil) + context.Background(), cwd, dockerFile, "", "", dockerContext, imageName, buildArgs, nil, nil, "", nil) require.Equal(t, true, ran) require.Nil(t, err) @@ -237,7 +239,7 @@ func Test_DockerBuildArgsEmpty(t *testing.T) { }) result, err := docker.Build( - context.Background(), cwd, dockerFile, "", "", dockerContext, imageName, buildArgs, nil, nil, nil) + context.Background(), cwd, dockerFile, "", "", dockerContext, imageName, buildArgs, nil, nil, "", nil) require.Equal(t, true, ran) require.Nil(t, err) @@ -288,13 +290,73 @@ func Test_DockerBuildArgsMultiple(t *testing.T) { }) result, err := docker.Build( - context.Background(), cwd, dockerFile, "", "", dockerContext, imageName, buildArgs, nil, nil, nil) + context.Background(), cwd, dockerFile, "", "", dockerContext, imageName, buildArgs, nil, nil, "", nil) require.Equal(t, true, ran) require.Nil(t, err) require.Equal(t, mockedDockerImgId, result) } +func Test_DockerBuildNetwork(t *testing.T) { + tests := []struct { + name string + network string + expectIn bool + }{ + {"WithHostNetwork", "host", true}, + {"WithEmptyNetwork", "", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ran := false + cwd := "." + dockerFile := "./Dockerfile" + dockerContext := "../" + imageName := "IMAGE_NAME" + + mockContext := mocks.NewMockContext(context.Background()) + docker := NewCli(mockContext.CommandRunner) + + mockContext.CommandRunner.When(func(args exec.RunArgs, command string) bool { + return strings.Contains(command, "docker build") + }).RespondFn(func(args exec.RunArgs) (exec.RunResult, error) { + ran = true + + argsNoFile := args.Args[:len(args.Args)-2] + value := args.Args[len(args.Args)-1] + + if tt.expectIn { + require.Contains(t, argsNoFile, "--network") + require.Contains(t, argsNoFile, tt.network) + } else { + require.NotContains(t, argsNoFile, "--network") + } + + err := os.WriteFile(value, []byte(mockedDockerImgId), 0600) + require.NoError(t, err) + + return exec.RunResult{ + Stdout: mockedDockerImgId, + ExitCode: 0, + }, nil + }) + + result, err := docker.Build( + context.Background(), + cwd, dockerFile, "", "", + dockerContext, imageName, + nil, nil, nil, + tt.network, nil, + ) + + require.True(t, ran) + require.NoError(t, err) + require.Equal(t, mockedDockerImgId, result) + }) + } +} + func Test_DockerTag(t *testing.T) { cwd := "." imageName := "image-name" diff --git a/cli/azd/pkg/ux/ux_additional_test.go b/cli/azd/pkg/ux/ux_additional_test.go index b77e390729c..e81ae449b6a 100644 --- a/cli/azd/pkg/ux/ux_additional_test.go +++ b/cli/azd/pkg/ux/ux_additional_test.go @@ -40,16 +40,19 @@ func TestConsoleWidth_empty_COLUMNS_uses_default(t *testing.T) { func TestPtr(t *testing.T) { intVal := 42 p := Ptr(intVal) - if p == nil { + switch { + case p == nil: t.Fatal("Ptr should return non-nil pointer") - } - if *p != 42 { + case *p != 42: t.Fatalf("*Ptr(42) = %d, want 42", *p) } strVal := "hello" sp := Ptr(strVal) - if *sp != "hello" { + switch { + case sp == nil: + t.Fatal("Ptr should return non-nil pointer for string") + case *sp != "hello": t.Fatalf("*Ptr(hello) = %q, want hello", *sp) } } diff --git a/schemas/alpha/azure.yaml.json b/schemas/alpha/azure.yaml.json index d5f2e179393..deddd0b3766 100644 --- a/schemas/alpha/azure.yaml.json +++ b/schemas/alpha/azure.yaml.json @@ -957,6 +957,11 @@ "type": "string" } }, + "network": { + "type": "string", + "title": "Optional. The networking mode for RUN instructions during docker build", + "description": "Sets the networking mode for RUN instructions during build. Passed as --network to docker build. For example, use 'host' to allow the build container to access the host network." + }, "remoteBuild": { "type": "boolean", "title": "Optional. Whether to build the image remotely", diff --git a/schemas/v1.0/azure.yaml.json b/schemas/v1.0/azure.yaml.json index ccc1e3a877a..8a3a5965ce7 100644 --- a/schemas/v1.0/azure.yaml.json +++ b/schemas/v1.0/azure.yaml.json @@ -863,6 +863,11 @@ "type": "string" } }, + "network": { + "type": "string", + "title": "Optional. The networking mode for RUN instructions during docker build", + "description": "Sets the networking mode for RUN instructions during build. Passed as --network to docker build. For example, use 'host' to allow the build container to access the host network." + }, "remoteBuild": { "type": "boolean", "title": "Optional. Whether to build the image remotely",