Skip to content

Commit 8ed777d

Browse files
authored
Merge pull request #3786 from austinvazquez/feat-build-add-host
Add nerdctl build --add-host option support
2 parents 35213f3 + f3f8310 commit 8ed777d

File tree

8 files changed

+170
-17
lines changed

8 files changed

+170
-17
lines changed

cmd/nerdctl/builder/builder_build.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ If Dockerfile is not present and -f is not specified, it will look for Container
4545
SilenceErrors: true,
4646
}
4747
helpers.AddStringFlag(buildCommand, "buildkit-host", nil, "", "BUILDKIT_HOST", "BuildKit address")
48+
buildCommand.Flags().StringArray("add-host", nil, "Add a custom host-to-IP mapping (format: \"host:ip\")")
4849
buildCommand.Flags().StringArrayP("tag", "t", nil, "Name and optionally a tag in the 'name:tag' format")
4950
buildCommand.Flags().StringP("file", "f", "", "Name of the Dockerfile")
5051
buildCommand.Flags().String("target", "", "Set the target build stage to build")
@@ -92,6 +93,10 @@ func processBuildCommandFlag(cmd *cobra.Command, args []string) (types.BuilderBu
9293
if err != nil {
9394
return types.BuilderBuildOptions{}, err
9495
}
96+
extraHosts, err := cmd.Flags().GetStringArray("add-host")
97+
if err != nil {
98+
return types.BuilderBuildOptions{}, err
99+
}
95100
platform, err := cmd.Flags().GetStringSlice("platform")
96101
if err != nil {
97102
return types.BuilderBuildOptions{}, err
@@ -232,6 +237,7 @@ func processBuildCommandFlag(cmd *cobra.Command, args []string) (types.BuilderBu
232237
Stdin: cmd.InOrStdin(),
233238
NetworkMode: network,
234239
ExtendedBuildContext: extendedBuildCtx,
240+
ExtraHosts: extraHosts,
235241
}, nil
236242
}
237243

cmd/nerdctl/builder/builder_build_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -957,3 +957,32 @@ func TestBuildAttestation(t *testing.T) {
957957

958958
testCase.Run(t)
959959
}
960+
961+
func TestBuildAddHost(t *testing.T) {
962+
nerdtest.Setup()
963+
964+
testCase := &test.Case{
965+
Require: test.Require(
966+
nerdtest.Build,
967+
),
968+
Cleanup: func(data test.Data, helpers test.Helpers) {
969+
helpers.Anyhow("rmi", "-f", data.Identifier())
970+
},
971+
Setup: func(data test.Data, helpers test.Helpers) {
972+
dockerfile := fmt.Sprintf(`FROM %s
973+
RUN ping -c 5 alpha
974+
RUN ping -c 5 beta
975+
`, testutil.CommonImage)
976+
buildCtx := data.TempDir()
977+
err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
978+
assert.NilError(helpers.T(), err)
979+
data.Set("buildCtx", buildCtx)
980+
},
981+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
982+
return helpers.Command("build", data.Get("buildCtx"), "-t", data.Identifier(), "--add-host", "alpha:127.0.0.1", "--add-host", "beta:127.0.0.1")
983+
},
984+
Expected: test.Expects(0, nil, nil),
985+
}
986+
987+
testCase.Run(t)
988+
}

docs/command-reference.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -714,8 +714,9 @@ Flags:
714714
- :whale: `--label`: Set metadata for an image
715715
- :whale: `--network=(default|host|none)`: Set the networking mode for the RUN instructions during build.(compatible with `buildctl build`)
716716
- :whale: `--build-context`: Set additional contexts for build (e.g. dir2=/path/to/dir2, myorg/myapp=docker-image://path/to/myorg/myapp)
717+
- :whale: `--add-host`: Add a custom host-to-IP mapping (format: `host:ip`)
717718

718-
Unimplemented `docker build` flags: `--add-host`, `--squash`
719+
Unimplemented `docker build` flags: `--squash`
719720

720721
### :whale: nerdctl commit
721722

pkg/api/types/builder_types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ type BuilderBuildOptions struct {
7171
NetworkMode string
7272
// Pull determines if we should try to pull latest image from remote. Default is buildkit's default.
7373
Pull *bool
74+
// ExtraHosts is a set of custom host-to-IP mappings.
75+
ExtraHosts []string
7476
}
7577

7678
// BuilderPruneOptions specifies options for `nerdctl builder prune`.

pkg/cmd/builder/build.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040
"github.com/containerd/nerdctl/v2/pkg/api/types"
4141
"github.com/containerd/nerdctl/v2/pkg/buildkitutil"
4242
"github.com/containerd/nerdctl/v2/pkg/clientutil"
43+
"github.com/containerd/nerdctl/v2/pkg/containerutil"
4344
"github.com/containerd/nerdctl/v2/pkg/platformutil"
4445
"github.com/containerd/nerdctl/v2/pkg/referenceutil"
4546
"github.com/containerd/nerdctl/v2/pkg/strutil"
@@ -453,6 +454,14 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option
453454
}
454455
}
455456

457+
if len(options.ExtraHosts) > 0 {
458+
extraHosts, err := containerutil.ParseExtraHosts(options.ExtraHosts, options.GOptions.HostGatewayIP, "=")
459+
if err != nil {
460+
return "", nil, false, "", nil, nil, err
461+
}
462+
buildctlArgs = append(buildctlArgs, "--opt=add-hosts="+strings.Join(extraHosts, ","))
463+
}
464+
456465
return buildctlBinary, buildctlArgs, needsLoading, metaFile, tags, cleanup, nil
457466
}
458467

pkg/cmd/container/create.go

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import (
3030
"strings"
3131

3232
dockercliopts "github.com/docker/cli/opts"
33-
dockeropts "github.com/docker/docker/opts"
3433
"github.com/opencontainers/runtime-spec/specs-go"
3534

3635
containerd "github.com/containerd/containerd/v2/client"
@@ -323,22 +322,12 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa
323322
}
324323
internalLabels.name = options.Name
325324
internalLabels.pidFile = options.PidFile
326-
internalLabels.extraHosts = strutil.DedupeStrSlice(netManager.NetworkOptions().AddHost)
327-
for i, host := range internalLabels.extraHosts {
328-
if _, err := dockercliopts.ValidateExtraHost(host); err != nil {
329-
return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err
330-
}
331-
parts := strings.SplitN(host, ":", 2)
332-
// If the IP Address is a string called "host-gateway", replace this value with the IP address stored
333-
// in the daemon level HostGateway IP config variable.
334-
if len(parts) == 2 && parts[1] == dockeropts.HostGatewayName {
335-
if options.GOptions.HostGatewayIP == "" {
336-
return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), fmt.Errorf("unable to derive the IP value for host-gateway")
337-
}
338-
parts[1] = options.GOptions.HostGatewayIP
339-
internalLabels.extraHosts[i] = fmt.Sprintf(`%s:%s`, parts[0], parts[1])
340-
}
325+
326+
extraHosts, err := containerutil.ParseExtraHosts(netManager.NetworkOptions().AddHost, options.GOptions.HostGatewayIP, ":")
327+
if err != nil {
328+
return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err
341329
}
330+
internalLabels.extraHosts = extraHosts
342331

343332
internalLabels.rm = containerutil.EncodeContainerRmOptLabel(options.Rm)
344333

pkg/containerutil/containerutil.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import (
2828
"strings"
2929
"time"
3030

31+
dockercliopts "github.com/docker/cli/opts"
32+
dockeropts "github.com/docker/docker/opts"
3133
"github.com/moby/sys/signal"
3234
"github.com/opencontainers/runtime-spec/specs-go"
3335

@@ -49,6 +51,7 @@ import (
4951
"github.com/containerd/nerdctl/v2/pkg/portutil"
5052
"github.com/containerd/nerdctl/v2/pkg/rootlessutil"
5153
"github.com/containerd/nerdctl/v2/pkg/signalutil"
54+
"github.com/containerd/nerdctl/v2/pkg/strutil"
5255
"github.com/containerd/nerdctl/v2/pkg/taskutil"
5356
)
5457

@@ -606,3 +609,34 @@ func EncodeContainerRmOptLabel(rmOpt bool) string {
606609
func DecodeContainerRmOptLabel(rmOptLabel string) (bool, error) {
607610
return strconv.ParseBool(rmOptLabel)
608611
}
612+
613+
// ParseExtraHosts takes an array of host-to-IP mapping strings, e.g. "localhost:127.0.0.1",
614+
// and a hostGatewayIP for resolving mappings to "host-gateway".
615+
//
616+
// Returns a map of host-to-IPs or errors if any mapping strings are not correctly formatted.
617+
func ParseExtraHosts(extraHosts []string, hostGatewayIP, separator string) ([]string, error) {
618+
hosts := make([]string, 0, len(extraHosts))
619+
for _, hostToIP := range strutil.DedupeStrSlice(extraHosts) {
620+
if _, err := dockercliopts.ValidateExtraHost(hostToIP); err != nil {
621+
return nil, err
622+
}
623+
624+
parts := strings.SplitN(hostToIP, ":", 2)
625+
if len(parts) != 2 {
626+
return nil, fmt.Errorf("invalid host-to-IP map %s", hostToIP)
627+
}
628+
629+
host, ip := parts[0], parts[1]
630+
631+
// If the IP address is a string called "host-gateway", replace this value with the IP address stored
632+
// in the daemon level HostGatewayIP config variable.
633+
if ip == dockeropts.HostGatewayName && hostGatewayIP == "" {
634+
return nil, errors.New("unable to derive the IP value for host-gateway")
635+
} else if ip == dockeropts.HostGatewayName {
636+
ip = hostGatewayIP
637+
}
638+
639+
hosts = append(hosts, host+separator+ip)
640+
}
641+
return hosts, nil
642+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package containerutil
18+
19+
import (
20+
"reflect"
21+
"testing"
22+
)
23+
24+
func TestParseExtraHosts(t *testing.T) {
25+
tests := []struct {
26+
name string
27+
extraHosts []string
28+
hostGateway string
29+
separator string
30+
expected []string
31+
expectedErrStr string
32+
}{
33+
{
34+
name: "NoExtraHosts",
35+
expected: []string{},
36+
},
37+
{
38+
name: "ExtraHosts",
39+
extraHosts: []string{"localhost:127.0.0.1", "localhost:[::1]"},
40+
separator: ":",
41+
expected: []string{"localhost:127.0.0.1", "localhost:[::1]"},
42+
},
43+
{
44+
name: "EqualsSeperator",
45+
extraHosts: []string{"localhost:127.0.0.1", "localhost:[::1]"},
46+
separator: "=",
47+
expected: []string{"localhost=127.0.0.1", "localhost=[::1]"},
48+
},
49+
{
50+
name: "InvalidExtraHostFormat",
51+
extraHosts: []string{"localhost"},
52+
expectedErrStr: "bad format for add-host: \"localhost\"",
53+
},
54+
{
55+
name: "ErrorOnHostGatewayExtraHostWithNoHostGatewayIPSet",
56+
extraHosts: []string{"localhost:host-gateway"},
57+
separator: ":",
58+
expectedErrStr: "unable to derive the IP value for host-gateway",
59+
},
60+
{
61+
name: "HostGatewayIP",
62+
extraHosts: []string{"localhost:host-gateway"},
63+
hostGateway: "10.10.0.1",
64+
separator: ":",
65+
expected: []string{"localhost:10.10.0.1"},
66+
},
67+
}
68+
69+
for _, test := range tests {
70+
t.Run(test.name, func(t *testing.T) {
71+
extraHosts, err := ParseExtraHosts(test.extraHosts, test.hostGateway, test.separator)
72+
if err != nil && err.Error() != test.expectedErrStr {
73+
t.Fatalf("expected '%s', actual '%v'", test.expectedErrStr, err)
74+
} else if err == nil && test.expectedErrStr != "" {
75+
t.Fatalf("expected error '%s' but got none", test.expectedErrStr)
76+
}
77+
78+
if !reflect.DeepEqual(test.expected, extraHosts) {
79+
t.Fatalf("expected %v, actual %v", test.expected, extraHosts)
80+
}
81+
})
82+
}
83+
}

0 commit comments

Comments
 (0)