Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ Show the nerdctl version information
- :nerd_face: `--cni-netconfpath`: CNI netconf path (default: `/etc/cni/net.d`) [`$NETCONFPATH`]
- :nerd_face: `--data-root`: nerdctl data root, e.g. "/var/lib/nerdctl"
- :nerd_face: `--cgroup-manager=(cgroupfs|systemd)`: cgroup manager
- :nerd_face: `--insecure-registry`: skips verifying HTTPS certs, and allows falling back to plain HTTP

## Unimplemented Docker commands
Container management:
Expand Down
2 changes: 2 additions & 0 deletions Vagrantfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Vagrant.configure("2") do |config|
config.vm.provider :virtualbox do |v|
v.memory = memory
v.cpus = cpus
# The default CIDR conflicts with slirp4netns CIDR (10.0.2.0/24)
v.customize ["modifyvm", :id, "--natnet1", "192.168.42.0/24"]
end
config.vm.provider :libvirt do |v|
v.memory = memory
Expand Down
16 changes: 16 additions & 0 deletions docs/registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,19 @@
nerdctl uses `${DOCKER_CONFIG}/config.json` for the authentication with image registries.

`$DOCKER_CONFIG` defaults to `$HOME/.docker`.

## Using insecure registry

If you face `http: server gave HTTP response to HTTPS client` and you cannot configure TLS for the registry, try `--insecure-registry` flag:

e.g.,
```console
$ nerdctl --insecure-registry run --rm 192.168.12.34:5000/foo
```

## Accessing 127.0.0.1 from rootless nerdctl

Currently, rootless nerdctl cannot pull images from 127.0.0.1, because
the pull operation occurs in RootlessKit's network namespace.

See https://github.com/AkihiroSuda/nerdctl/issues/86 for the discussion about workarounds.
4 changes: 4 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ func newApp() *cli.App {
Usage: "Cgroup manager to use (\"cgroupfs\"|\"systemd\")",
Value: ncdefaults.CgroupManager(),
},
&cli.BoolFlag{
Name: "insecure-registry",
Usage: "skips verifying HTTPS certs, and allows falling back to plain HTTP",
},
}
app.Before = func(clicontext *cli.Context) error {
if debug {
Expand Down
80 changes: 71 additions & 9 deletions pkg/imgutil/dockerconfigresolver/dockerconfigresolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
package dockerconfigresolver

import (
"crypto/tls"
"net"
"net/http"
"net/url"

"github.com/containerd/containerd/remotes"
Expand All @@ -29,12 +31,77 @@ import (
"github.com/sirupsen/logrus"
)

type opts struct {
plainHTTP bool
skipVerifyCerts bool
}

// Opt for New
type Opt func(*opts)

// WithPlainHTTP enables insecure plain HTTP
func WithPlainHTTP(b bool) Opt {
return func(o *opts) {
o.plainHTTP = b
}
}

// WithSkipVerifyCerts skips verifying TLS certs
func WithSkipVerifyCerts(b bool) Opt {
return func(o *opts) {
o.skipVerifyCerts = b
}
}

// New instantiates a resolver using $DOCKER_CONFIG/config.json .
//
// $DOCKER_CONFIG defaults to "~/.docker".
//
// refHostname is like "docker.io".
func New(refHostname string) (remotes.Resolver, error) {
func New(refHostname string, optFuncs ...Opt) (remotes.Resolver, error) {
var o opts
for _, of := range optFuncs {
of(&o)
}
var authzOpts []docker.AuthorizerOpt
if authCreds, err := NewAuthCreds(refHostname); err != nil {
return nil, err
} else {
authzOpts = append(authzOpts, docker.WithAuthCreds(authCreds))
}
authz := docker.NewDockerAuthorizer(authzOpts...)
plainHTTPFunc := docker.MatchLocalhost
if o.plainHTTP {
plainHTTPFunc = docker.MatchAllHosts
}
regOpts := []docker.RegistryOpt{
docker.WithAuthorizer(authz),
docker.WithPlainHTTP(plainHTTPFunc),
}
if o.skipVerifyCerts {
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{
Transport: tr,
}
regOpts = append(regOpts, docker.WithClient(client))
}
resovlerOpts := docker.ResolverOptions{
Hosts: docker.ConfigureDefaultRegistries(regOpts...),
}
resolver := docker.NewResolver(resovlerOpts)
return resolver, nil
}

// AuthCreds is for docker.WithAuthCreds
type AuthCreds func(string) (string, string, error)

// NewAuthCreds returns AuthCreds that uses $DOCKER_CONFIG/config.json .
// AuthCreds can be nil.
func NewAuthCreds(refHostname string) (AuthCreds, error) {
// Load does not raise an error on ENOENT
dockerConfigFile, err := dockercliconfig.Load("")
if err != nil {
Expand All @@ -48,7 +115,7 @@ func New(refHostname string) (remotes.Resolver, error) {
return nil, err
}

var credFunc func(string) (string, string, error)
var credFunc AuthCreds

authConfigHostnames := []string{refHostname}
if refHostname == "docker.io" || refHostname == "registry-1.docker.io" {
Expand Down Expand Up @@ -110,13 +177,8 @@ func New(refHostname string) (remotes.Resolver, error) {
}
}
}

resovlerOpts := docker.ResolverOptions{
// FIXME: Credentials is deprecated
Credentials: credFunc,
}
resolver := docker.NewResolver(resovlerOpts)
return resolver, nil
// credsFunc can be nil here
return credFunc, nil
}

func isAuthConfigEmpty(ac dockercliconfigtypes.AuthConfig) bool {
Expand Down
48 changes: 46 additions & 2 deletions pkg/imgutil/imgutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/AkihiroSuda/nerdctl/pkg/imgutil/pull"
"github.com/containerd/containerd"
refdocker "github.com/containerd/containerd/reference/docker"
"github.com/containerd/containerd/remotes"
"github.com/containerd/stargz-snapshotter/fs/source"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
Expand All @@ -41,7 +42,10 @@ type EnsuredImage struct {
// PullMode is either one of "always", "missing", "never"
type PullMode = string

func EnsureImage(ctx context.Context, client *containerd.Client, stdout io.Writer, snapshotter, rawRef string, mode PullMode) (*EnsuredImage, error) {
// EnsureImage ensures the image.
//
// When insecure is set, skips verifying certs, and also falls back to HTTP when the registry does not speak HTTPS
func EnsureImage(ctx context.Context, client *containerd.Client, stdout io.Writer, snapshotter, rawRef string, mode PullMode, insecure bool) (*EnsuredImage, error) {
named, err := refdocker.ParseDockerRef(rawRef)
if err != nil {
return nil, err
Expand Down Expand Up @@ -70,11 +74,50 @@ func EnsureImage(ctx context.Context, client *containerd.Client, stdout io.Write
return nil, errors.Errorf("image %q is not available", rawRef)
}

resolver, err := dockerconfigresolver.New(refdocker.Domain(named))
refDomain := refdocker.Domain(named)

var dOpts []dockerconfigresolver.Opt
if insecure {
logrus.Warnf("skipping verifying HTTPS certs for %q", refDomain)
dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true))
}
resolver, err := dockerconfigresolver.New(refDomain, dOpts...)
if err != nil {
return nil, err
}

img, err := pullImage(ctx, client, stdout, snapshotter, resolver, ref)
if err != nil {
if !IsErrHTTPResponseToHTTPSClient(err) {
return nil, err
}
if insecure {
logrus.WithError(err).Warnf("server %q does not seem to support HTTPS, falling back to plain HTTP", refDomain)
dOpts = append(dOpts, dockerconfigresolver.WithPlainHTTP(true))
resolver, err = dockerconfigresolver.New(refDomain, dOpts...)
if err != nil {
return nil, err
}
return pullImage(ctx, client, stdout, snapshotter, resolver, ref)
} else {
logrus.WithError(err).Errorf("server %q does not seem to support HTTPS", refDomain)
logrus.Info("Hint: you may want to try --insecure-registry to allow plain HTTP (if you are in a trusted network)")
return nil, err
}
}
return img, nil
}

// IsErrHTTPResponseToHTTPSClient returns whether err is
// "http: server gave HTTP response to HTTPS client"
func IsErrHTTPResponseToHTTPSClient(err error) bool {
// The error string is unexposed as of Go 1.16, so we can't use `errors.Is`.
// https://github.com/golang/go/issues/44855
const unexposed = "server gave HTTP response to HTTPS client"
return strings.Contains(err.Error(), unexposed)
}

func pullImage(ctx context.Context, client *containerd.Client, stdout io.Writer, snapshotter string, resolver remotes.Resolver, ref string) (*EnsuredImage, error) {
ctx, done, err := client.WithLease(ctx)
if err != nil {
return nil, err
Expand Down Expand Up @@ -109,6 +152,7 @@ func EnsureImage(ctx context.Context, client *containerd.Client, stdout io.Write
Remote: sgz,
}
return res, nil

}

func isStargz(sn string) bool {
Expand Down
1 change: 1 addition & 0 deletions pkg/imgutil/pull/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func Pull(ctx context.Context, client *containerd.Client, ref string, config *Co
}
opts = append(opts, config.RemoteOpts...)

// client.Pull is for single-platform (TODO: support multi)
img, err := client.Pull(pctx, ref, opts...)
stopProgress()
if err != nil {
Expand Down
5 changes: 4 additions & 1 deletion pkg/imgutil/push/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,16 @@ import (
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/pkg/progress"
"github.com/containerd/containerd/platforms"
"github.com/containerd/containerd/remotes"
"github.com/containerd/containerd/remotes/docker"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
)

func Push(ctx context.Context, client *containerd.Client, resolver remotes.Resolver, stdout io.Writer, localRef, remoteRef string) error {
func Push(ctx context.Context, client *containerd.Client, resolver remotes.Resolver, stdout io.Writer,
localRef, remoteRef string, platform platforms.MatchComparer) error {
img, err := client.ImageService().Get(ctx, localRef)
if err != nil {
return errors.Wrap(err, "unable to resolve image to manifest")
Expand All @@ -66,6 +68,7 @@ func Push(ctx context.Context, client *containerd.Client, resolver remotes.Resol
return client.Push(ctx, remoteRef, desc,
containerd.WithResolver(resolver),
containerd.WithImageHandler(jobHandler),
containerd.WithPlatformMatcher(platform),
)
})

Expand Down
1 change: 1 addition & 0 deletions pkg/testutil/testutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,5 @@ const (
AlpineImage = "mirror.gcr.io/library/alpine:3.13"
NginxAlpineImage = "mirror.gcr.io/library/nginx:1.19-alpine"
NginxAlpineIndexHTMLSnippet = "<title>Welcome to nginx!</title>"
RegistryImage = "mirror.gcr.io/library/registry:2"
)
4 changes: 3 additions & 1 deletion pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ func pullAction(clicontext *cli.Context) error {
return err
}
defer cancel()
_, err = imgutil.EnsureImage(ctx, client, clicontext.App.Writer, clicontext.String("snapshotter"), clicontext.Args().First(), "always")
insecure := clicontext.Bool("insecure-registry")
_, err = imgutil.EnsureImage(ctx, client, clicontext.App.Writer, clicontext.String("snapshotter"), clicontext.Args().First(),
"always", insecure)
return err
}
58 changes: 54 additions & 4 deletions push.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,17 @@
package main

import (
"context"

"github.com/AkihiroSuda/nerdctl/pkg/imgutil"
"github.com/AkihiroSuda/nerdctl/pkg/imgutil/dockerconfigresolver"
"github.com/AkihiroSuda/nerdctl/pkg/imgutil/push"
"github.com/containerd/containerd/images/converter"
"github.com/containerd/containerd/platforms"
refdocker "github.com/containerd/containerd/reference/docker"
"github.com/containerd/containerd/remotes"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"

"github.com/urfave/cli/v2"
)
Expand All @@ -44,15 +51,58 @@ func pushAction(clicontext *cli.Context) error {
return err
}
ref := named.String()
resolver, err := dockerconfigresolver.New(refdocker.Domain(named))
refDomain := refdocker.Domain(named)

insecure := clicontext.Bool("insecure-registry")

client, ctx, cancel, err := newClient(clicontext)
if err != nil {
return err
}
defer cancel()

client, ctx, cancel, err := newClient(clicontext)
// Push fails with "400 Bad Request" when the manifest is multi-platform but we do not locally have multi-platform blobs.
// So we create a tmp single-platform image to avoid the error.
// TODO: support pushing multi-platform
singlePlatform := platforms.DefaultStrict()
singlePlatformRef := ref + "-tmp-single"
singlePlatformImg, err := converter.Convert(ctx, client, singlePlatformRef, ref,
converter.WithPlatform(singlePlatform))
if err != nil {
return errors.Wrapf(err, "failed to create a tmp single-platform image %q", singlePlatformRef)
}
defer client.ImageService().Delete(context.TODO(), singlePlatformImg.Name)

pushFunc := func(r remotes.Resolver) error {
return push.Push(ctx, client, r, clicontext.App.Writer, singlePlatformRef, ref, singlePlatform)
}

var dOpts []dockerconfigresolver.Opt
if insecure {
logrus.Warnf("skipping verifying HTTPS certs for %q", refDomain)
dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true))
}
resolver, err := dockerconfigresolver.New(refDomain, dOpts...)
if err != nil {
return err
}
defer cancel()
return push.Push(ctx, client, resolver, clicontext.App.Writer, ref, ref)
if err = pushFunc(resolver); err != nil {
if !imgutil.IsErrHTTPResponseToHTTPSClient(err) {
return err
}
if insecure {
logrus.WithError(err).Warnf("server %q does not seem to support HTTPS, falling back to plain HTTP", refDomain)
dOpts = append(dOpts, dockerconfigresolver.WithPlainHTTP(true))
resolver, err = dockerconfigresolver.New(refDomain, dOpts...)
if err != nil {
return err
}
return pushFunc(resolver)
} else {
logrus.WithError(err).Errorf("server %q does not seem to support HTTPS", refDomain)
logrus.Info("Hint: you may want to try --insecure-registry to allow plain HTTP (if you are in a trusted network)")
return err
}
}
return nil
}
Loading