From aa6408fefddfb08af49dd325639ea34d321d5dd7 Mon Sep 17 00:00:00 2001 From: TiR <70480807+TIR44@users.noreply.github.com> Date: Sun, 31 May 2026 16:46:37 +0300 Subject: [PATCH] Added build container MAC address support Signed-off-by: TiR <70480807+TIR44@users.noreply.github.com> --- internal/build/fakes/fake_builder.go | 14 +++++++++ internal/build/lifecycle_executor.go | 2 ++ internal/build/phase.go | 7 +++-- internal/build/phase_config_provider.go | 32 ++++++++++++++++++++ internal/build/phase_config_provider_test.go | 26 ++++++++++++++++ internal/build/phase_factory.go | 1 + internal/commands/build.go | 14 +++++++-- internal/commands/build_test.go | 26 ++++++++++++++++ pkg/client/build.go | 24 +++++++++++++++ pkg/client/build_test.go | 25 +++++++++++++++ 10 files changed, 167 insertions(+), 4 deletions(-) diff --git a/internal/build/fakes/fake_builder.go b/internal/build/fakes/fake_builder.go index 2472dd8a73..0de847d867 100644 --- a/internal/build/fakes/fake_builder.go +++ b/internal/build/fakes/fake_builder.go @@ -1,10 +1,13 @@ package fakes import ( + "net" + "github.com/Masterminds/semver" "github.com/buildpacks/imgutil" ifakes "github.com/buildpacks/imgutil/fakes" "github.com/buildpacks/lifecycle/api" + "github.com/moby/moby/api/types/network" "github.com/buildpacks/pack/internal/build" "github.com/buildpacks/pack/internal/builder" @@ -132,6 +135,17 @@ func WithEnableUsernsHost() func(*build.LifecycleOptions) { } } +// WithMacAddress creates a LifecycleOptions option that sets the container MAC address. +func WithMacAddress(macAddress string) func(*build.LifecycleOptions) { + return func(opts *build.LifecycleOptions) { + parsed, err := net.ParseMAC(macAddress) + if err != nil { + panic(err) + } + opts.MacAddress = network.HardwareAddr(parsed) + } +} + // WithExecutionEnvironment creates a LifecycleOptions option that sets the execution environment func WithExecutionEnvironment(execEnv string) func(*build.LifecycleOptions) { return func(opts *build.LifecycleOptions) { diff --git a/internal/build/lifecycle_executor.go b/internal/build/lifecycle_executor.go index c15dcc5a78..efa9b31b4b 100644 --- a/internal/build/lifecycle_executor.go +++ b/internal/build/lifecycle_executor.go @@ -11,6 +11,7 @@ import ( "github.com/buildpacks/lifecycle/platform/files" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" + "github.com/moby/moby/api/types/network" "github.com/buildpacks/pack/internal/builder" "github.com/buildpacks/pack/internal/container" @@ -95,6 +96,7 @@ type LifecycleOptions struct { HTTPSProxy string NoProxy string Network string + MacAddress network.HardwareAddr AdditionalTags []string Volumes []string InsecureRegistries []string diff --git a/internal/build/phase.go b/internal/build/phase.go index 613fad653e..45149354eb 100644 --- a/internal/build/phase.go +++ b/internal/build/phase.go @@ -5,6 +5,7 @@ import ( "io" dcontainer "github.com/moby/moby/api/types/container" + dnetwork "github.com/moby/moby/api/types/network" "github.com/moby/moby/client" "github.com/pkg/errors" @@ -19,6 +20,7 @@ type Phase struct { handler container.Handler ctrConf *dcontainer.Config hostConf *dcontainer.HostConfig + networkConf *dnetwork.NetworkingConfig ctr client.ContainerCreateResult uid, gid int appPath string @@ -30,8 +32,9 @@ type Phase struct { func (p *Phase) Run(ctx context.Context) error { var err error p.ctr, err = p.docker.ContainerCreate(ctx, client.ContainerCreateOptions{ - Config: p.ctrConf, - HostConfig: p.hostConf, + Config: p.ctrConf, + HostConfig: p.hostConf, + NetworkingConfig: p.networkConf, }) if err != nil { return errors.Wrapf(err, "failed to create '%s' container", p.name) diff --git a/internal/build/phase_config_provider.go b/internal/build/phase_config_provider.go index 36e452931e..0fc7c2b043 100644 --- a/internal/build/phase_config_provider.go +++ b/internal/build/phase_config_provider.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/network" pcontainer "github.com/buildpacks/pack/internal/container" "github.com/buildpacks/pack/internal/style" @@ -25,6 +26,7 @@ type PhaseConfigProviderOperation func(*PhaseConfigProvider) type PhaseConfigProvider struct { ctrConf *container.Config hostConf *container.HostConfig + networkConf *network.NetworkingConfig name string os string containerOps []ContainerOperation @@ -72,6 +74,10 @@ func NewPhaseConfigProvider(name string, lifecycleExec *LifecycleExecution, ops op(provider) } + if len(lifecycleExec.opts.MacAddress) > 0 { + provider.withMACAddress(lifecycleExec.opts.MacAddress) + } + provider.ctrConf.Entrypoint = []string{""} // override entrypoint in case it is set provider.ctrConf.Cmd = append([]string{"/cnb/lifecycle/" + name}, provider.ctrConf.Cmd...) @@ -86,6 +92,9 @@ func NewPhaseConfigProvider(name string, lifecycleExec *LifecycleExecution, ops lifecycleExec.logger.Debug("Host Settings:") lifecycleExec.logger.Debugf(" Binds: %s", style.Symbol(strings.Join(provider.hostConf.Binds, " "))) lifecycleExec.logger.Debugf(" Network Mode: %s", style.Symbol(string(provider.hostConf.NetworkMode))) + if len(lifecycleExec.opts.MacAddress) > 0 { + lifecycleExec.logger.Debugf(" MAC Address: %s", style.Symbol(lifecycleExec.opts.MacAddress.String())) + } if lifecycleExec.opts.Interactive { provider.handler = lifecycleExec.opts.Termui.Handler() @@ -106,10 +115,33 @@ func sanitized(origEnv []string) []string { return sanitizedEnv } +func (p *PhaseConfigProvider) withMACAddress(macAddress network.HardwareAddr) { + if p.networkConf == nil { + p.networkConf = new(network.NetworkingConfig) + } + if p.networkConf.EndpointsConfig == nil { + p.networkConf.EndpointsConfig = make(map[string]*network.EndpointSettings) + } + + networkName := p.hostConf.NetworkMode.NetworkName() + if networkName == "" { + networkName = network.NetworkDefault + } + + if p.networkConf.EndpointsConfig[networkName] == nil { + p.networkConf.EndpointsConfig[networkName] = &network.EndpointSettings{} + } + p.networkConf.EndpointsConfig[networkName].MacAddress = macAddress +} + func (p *PhaseConfigProvider) ContainerConfig() *container.Config { return p.ctrConf } +func (p *PhaseConfigProvider) NetworkConfig() *network.NetworkingConfig { + return p.networkConf +} + func (p *PhaseConfigProvider) ContainerOps() []ContainerOperation { return p.containerOps } diff --git a/internal/build/phase_config_provider_test.go b/internal/build/phase_config_provider_test.go index b204935e87..d63543d62c 100644 --- a/internal/build/phase_config_provider_test.go +++ b/internal/build/phase_config_provider_test.go @@ -9,6 +9,7 @@ import ( "github.com/buildpacks/lifecycle/api" "github.com/heroku/color" dcontainer "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/network" "github.com/moby/moby/client" "github.com/pkg/errors" "github.com/sclevine/spec" @@ -77,6 +78,31 @@ func testPhaseConfigProvider(t *testing.T, when spec.G, it spec.S) { }) }) + when("mac address is set", func() { + it("sets the mac address on the default network endpoint", func() { + expectedMACAddress := "01:23:45:67:89:ab" + lifecycle := newTestLifecycleExec(t, false, "some-temp-dir", fakes.WithMacAddress(expectedMACAddress)) + + phaseConfigProvider := build.NewPhaseConfigProvider("some-name", lifecycle) + + endpoint := phaseConfigProvider.NetworkConfig().EndpointsConfig[network.NetworkDefault] + h.AssertNotNil(t, endpoint) + h.AssertEq(t, endpoint.MacAddress.String(), expectedMACAddress) + }) + + it("sets the mac address on the selected network endpoint", func() { + expectedMACAddress := "01:23:45:67:89:ab" + expectedNetwork := "some-network" + lifecycle := newTestLifecycleExec(t, false, "some-temp-dir", fakes.WithMacAddress(expectedMACAddress)) + + phaseConfigProvider := build.NewPhaseConfigProvider("some-name", lifecycle, build.WithNetwork(expectedNetwork)) + + endpoint := phaseConfigProvider.NetworkConfig().EndpointsConfig[expectedNetwork] + h.AssertNotNil(t, endpoint) + h.AssertEq(t, endpoint.MacAddress.String(), expectedMACAddress) + }) + }) + when("building for Windows", func() { it("sets process isolation", func() { fakeBuilderImage := ifakes.NewImage("fake-builder", "", nil) diff --git a/internal/build/phase_factory.go b/internal/build/phase_factory.go index 97ae788349..7d2ea2ce41 100644 --- a/internal/build/phase_factory.go +++ b/internal/build/phase_factory.go @@ -25,6 +25,7 @@ func (m *DefaultPhaseFactory) New(provider *PhaseConfigProvider) RunnerCleaner { return &Phase{ ctrConf: provider.ContainerConfig(), hostConf: provider.HostConfig(), + networkConf: provider.NetworkConfig(), name: provider.Name(), docker: m.lifecycleExec.docker, infoWriter: provider.InfoWriter(), diff --git a/internal/commands/build.go b/internal/commands/build.go index d3db7a69b9..57438f16ae 100644 --- a/internal/commands/build.go +++ b/internal/commands/build.go @@ -2,6 +2,7 @@ package commands import ( "fmt" + "net" "os" "path/filepath" "regexp" @@ -45,6 +46,7 @@ type BuildFlags struct { Platform string Policy string Network string + MacAddress string DescriptorPath string DefaultProcessType string LifecycleImage string @@ -195,8 +197,9 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob Buildpacks: buildpacks, Extensions: extensions, ContainerConfig: client.ContainerConfig{ - Network: flags.Network, - Volumes: flags.Volumes, + Network: flags.Network, + MacAddress: flags.MacAddress, + Volumes: flags.Volumes, }, DefaultProcessType: flags.DefaultProcessType, ProjectDescriptorBaseDir: filepath.Dir(actualDescriptorPath), @@ -275,6 +278,7 @@ func buildCommandFlags(cmd *cobra.Command, buildFlags *BuildFlags, cfg config.Co cmd.Flags().StringArrayVarP(&buildFlags.Env, "env", "e", []string{}, "Build-time environment variable, in the form 'VAR=VALUE' or 'VAR'.\nWhen using latter value-less form, value will be taken from current\n environment at the time this command is executed.\nThis flag may be specified multiple times and will override\n individual values defined by --env-file."+stringArrayHelp("env")+"\nNOTE: These are NOT available at image runtime.") cmd.Flags().StringArrayVar(&buildFlags.EnvFiles, "env-file", []string{}, "Build-time environment variables file\nOne variable per line, of the form 'VAR=VALUE' or 'VAR'\nWhen using latter value-less form, value will be taken from current\n environment at the time this command is executed\nNOTE: These are NOT available at image runtime.\"") cmd.Flags().StringVar(&buildFlags.Network, "network", "", "Connect detect and build containers to network") + cmd.Flags().StringVar(&buildFlags.MacAddress, "mac-address", "", "MAC address to set on the build container network endpoint") cmd.Flags().StringArrayVar(&buildFlags.PreBuildpacks, "pre-buildpack", []string{}, "Buildpacks to prepend to the groups in the builder's order") cmd.Flags().StringArrayVar(&buildFlags.PostBuildpacks, "post-buildpack", []string{}, "Buildpacks to append to the groups in the builder's order") cmd.Flags().BoolVar(&buildFlags.Publish, "publish", false, "Publish the application image directly to the container registry specified in , instead of the daemon. The run image must also reside in the registry.") @@ -338,6 +342,12 @@ func validateBuildFlags(flags *BuildFlags, cfg config.Config, inputImageRef clie return errors.New("uid flag must be in the range of 0-2147483647") } + if flags.MacAddress != "" { + if _, err := net.ParseMAC(flags.MacAddress); err != nil { + return errors.Wrapf(err, "invalid MAC address %q", flags.MacAddress) + } + } + if flags.Interactive && !cfg.Experimental { return client.NewExperimentError("Interactive mode is currently experimental.") } diff --git a/internal/commands/build_test.go b/internal/commands/build_test.go index 254a9d2d2c..c1ffd535b3 100644 --- a/internal/commands/build_test.go +++ b/internal/commands/build_test.go @@ -207,6 +207,23 @@ func testBuildCommand(t *testing.T, when spec.G, it spec.S) { }) }) + when("a mac address is given", func() { + it("forwards the mac address onto the client", func() { + mockClient.EXPECT(). + Build(gomock.Any(), EqBuildOptionsWithMacAddress("01:23:45:67:89:ab")). + Return(nil) + + command.SetArgs([]string{"image", "--builder", "my-builder", "--mac-address", "01:23:45:67:89:ab"}) + h.AssertNil(t, command.Execute()) + }) + + it("returns an error for invalid mac addresses", func() { + command.SetArgs([]string{"image", "--builder", "my-builder", "--mac-address", "invalid-mac"}) + + h.AssertError(t, command.Execute(), `invalid MAC address "invalid-mac"`) + }) + }) + when("--platform", func() { it("sets platform", func() { mockClient.EXPECT(). @@ -1220,6 +1237,15 @@ func EqBuildOptionsWithNetwork(network string) gomock.Matcher { } } +func EqBuildOptionsWithMacAddress(macAddress string) gomock.Matcher { + return buildOptionsMatcher{ + description: fmt.Sprintf("MacAddress=%s", macAddress), + equals: func(o client.BuildOptions) bool { + return o.ContainerConfig.MacAddress == macAddress + }, + } +} + func EqBuildOptionsWithBuilder(builder string) gomock.Matcher { return buildOptionsMatcher{ description: fmt.Sprintf("Builder=%s", builder), diff --git a/pkg/client/build.go b/pkg/client/build.go index 1ed2ffec2a..a050835620 100644 --- a/pkg/client/build.go +++ b/pkg/client/build.go @@ -9,6 +9,7 @@ import ( "encoding/json" "fmt" "io" + "net" "os" "path/filepath" "sort" @@ -24,6 +25,7 @@ import ( "github.com/buildpacks/lifecycle/platform/files" "github.com/chainguard-dev/kaniko/pkg/util/proc" "github.com/google/go-containerregistry/pkg/name" + mnetwork "github.com/moby/moby/api/types/network" "github.com/moby/moby/client" "github.com/pkg/errors" ignore "github.com/sabhiram/go-gitignore" @@ -261,6 +263,9 @@ type ContainerConfig struct { // https://docs.docker.com/network/#network-drivers Network string + // Configure the MAC address of the build containers' network endpoint. + MacAddress string + // Volumes are accessible during both detect build phases // should have the form: /path/in/host:/path/in/container. // For more about volume mounts, and their permissions see: @@ -274,6 +279,19 @@ type ContainerConfig struct { Volumes []string } +func parseMACAddress(macAddress string) (mnetwork.HardwareAddr, error) { + if macAddress == "" { + return nil, nil + } + + parsed, err := net.ParseMAC(macAddress) + if err != nil { + return nil, errors.Wrapf(err, "invalid MAC address %q", macAddress) + } + + return mnetwork.HardwareAddr(parsed), nil +} + type LayoutConfig struct { // Application image reference provided by the user InputImage InputImageReference @@ -609,6 +627,11 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { c.logger.Warn(warning) } + macAddress, err := parseMACAddress(opts.ContainerConfig.MacAddress) + if err != nil { + return err + } + fileFilter, err := getFileFilter(opts.ProjectDescriptor) if err != nil { return err @@ -654,6 +677,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error { HTTPSProxy: proxyConfig.HTTPSProxy, NoProxy: proxyConfig.NoProxy, Network: opts.ContainerConfig.Network, + MacAddress: macAddress, AdditionalTags: opts.AdditionalTags, Volumes: processedVolumes, DefaultProcessType: opts.DefaultProcessType, diff --git a/pkg/client/build_test.go b/pkg/client/build_test.go index eb7ca2aef1..036d36d599 100644 --- a/pkg/client/build_test.go +++ b/pkg/client/build_test.go @@ -2564,6 +2564,31 @@ api = "0.2" }) }) + when("MacAddress option", func() { + it("passes the parsed value through", func() { + h.AssertNil(t, subject.Build(context.TODO(), BuildOptions{ + Image: "some/app", + Builder: defaultBuilderName, + ContainerConfig: ContainerConfig{ + MacAddress: "01:23:45:67:89:ab", + }, + })) + h.AssertEq(t, fakeLifecycle.Opts.MacAddress.String(), "01:23:45:67:89:ab") + }) + + it("returns an error for invalid values", func() { + err := subject.Build(context.TODO(), BuildOptions{ + Image: "some/app", + Builder: defaultBuilderName, + ContainerConfig: ContainerConfig{ + MacAddress: "invalid-mac", + }, + }) + + h.AssertError(t, err, `invalid MAC address "invalid-mac"`) + }) + }) + when("Lifecycle option", func() { when("Platform API", func() { for _, supportedPlatformAPI := range []string{"0.3", "0.4"} {