Skip to content
This repository has been archived by the owner on Jan 8, 2024. It is now read-only.

Support daemonless, unprivileged Docker builds with img #970

Merged
merged 14 commits into from
Jan 14, 2021
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
76 changes: 75 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# syntax = hashicorp.jfrog.io/docker/docker/dockerfile:experimental

#--------------------------------------------------------------------
# builder builds the Waypoint binaries
#--------------------------------------------------------------------

FROM hashicorp.jfrog.io/docker/golang:alpine AS builder

RUN apk add --no-cache git gcc libc-dev openssh
Expand All @@ -26,18 +30,88 @@ RUN go get github.com/kevinburke/go-bindata/...
RUN --mount=type=cache,target=/root/.cache/go-build make bin
RUN --mount=type=cache,target=/root/.cache/go-build make bin/entrypoint

#--------------------------------------------------------------------
# imgbase builds the "img" tool and all of its dependencies
#--------------------------------------------------------------------

# We build a fork of img for now so we can get the `img inspect` CLI
# Watch this PR: https://github.com/genuinetools/img/pull/324
FROM hashicorp.jfrog.io/docker/golang:alpine AS imgbuilder

RUN apk add --no-cache \
bash \
build-base \
gcc \
git \
libseccomp-dev \
linux-headers \
make

RUN git clone https://github.com/mitchellh/img.git /img
WORKDIR /img
RUN git checkout inspect
RUN go get github.com/go-bindata/go-bindata/go-bindata
krantzinator marked this conversation as resolved.
Show resolved Hide resolved
RUN make static && mv img /usr/bin/img

# Copied from img repo, see notes for specific reasons:
# https://github.com/genuinetools/img/blob/d858ac71f93cc5084edd2ba2d425b90234cf2ead/Dockerfile
FROM hashicorp.jfrog.io/docker/alpine AS imgbase
RUN apk add --no-cache autoconf automake build-base byacc gettext gettext-dev \
gcc git libcap-dev libtool libxslt img runc
RUN git clone https://github.com/shadow-maint/shadow.git /shadow
WORKDIR /shadow
RUN git checkout 59c2dabb264ef7b3137f5edb52c0b31d5af0cf76
RUN ./autogen.sh --disable-nls --disable-man --without-audit \
--without-selinux --without-acl --without-attr --without-tcb \
--without-nscd \
&& make \
&& cp src/newuidmap src/newgidmap /usr/bin

#--------------------------------------------------------------------
# final image
#--------------------------------------------------------------------

# Notes on img and what is required to make it work, since there's a lot
# of small details below that are absolutely required for everything to
# come together:
#
# - img, runc, newuidmap, newgidmap need to be installed
# - libseccomp-dev must be installed for runc
# - newuidmap/newgidmap need to have suid set (u+s)
# - /etc/subuid and /etc/subgid need to have an entry for the user
# - USER, HOME, and XDG_RUNTIME_DIR all need to be set
#

FROM hashicorp.jfrog.io/docker/alpine

COPY --from=imgbuilder /usr/bin/img /usr/bin/img
COPY --from=imgbase /usr/bin/runc /usr/bin/runc
COPY --from=imgbase /usr/bin/newuidmap /usr/bin/newuidmap
COPY --from=imgbase /usr/bin/newgidmap /usr/bin/newgidmap

# libseccomp-dev is required for runc
# git is for gitrefpretty() and other calls for Waypoint
RUN apk add --no-cache libseccomp-dev git

COPY --from=builder /tmp/wp-src/waypoint /usr/bin/waypoint
COPY --from=builder /tmp/wp-src/waypoint-entrypoint /usr/bin/waypoint-entrypoint

VOLUME ["/data"]

RUN addgroup waypoint && \
adduser -S -G waypoint waypoint && \
adduser -S -u 1000 -G waypoint waypoint && \
mkdir /data/ && \
chown -R waypoint:waypoint /data

# configure newuidmap/newgidmap to work with our waypoint user
RUN chmod u+s /usr/bin/newuidmap /usr/bin/newgidmap \
&& mkdir -p /run/user/1000 \
&& chown -R waypoint /run/user/1000 /home/waypoint \
&& echo waypoint:100000:65536 | tee /etc/subuid | tee /etc/subgid

USER waypoint
ENV USER waypoint
ENV HOME /home/waypoint
ENV XDG_RUNTIME_DIR=/run/user/1000

ENTRYPOINT ["/usr/bin/waypoint"]
168 changes: 115 additions & 53 deletions builtin/docker/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/golang/protobuf/ptypes/empty"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/waypoint-plugin-sdk/component"
"github.com/hashicorp/waypoint-plugin-sdk/docs"
"github.com/hashicorp/waypoint-plugin-sdk/terminal"
Expand Down Expand Up @@ -107,26 +109,27 @@ func (b *Builder) Build(
ctx context.Context,
ui terminal.UI,
src *component.Source,
log hclog.Logger,
) (*Image, error) {
stdout, _, err := ui.OutputWriters()
if err != nil {
return nil, err
}

sg := ui.StepGroup()
defer sg.Wait()
step := sg.Add("Initializing Docker client...")
defer step.Abort()
defer func() {
if step != nil {
step.Abort()
}
}()

result := &Image{
Image: fmt.Sprintf("waypoint.local/%s", src.App),
Tag: "latest",
Image: fmt.Sprintf("waypoint.local/%s", src.App),
Tag: "latest",
Location: &Image_Docker{Docker: &empty.Empty{}},
}

cli, err := wpdockerclient.NewClientWithOpts(client.FromEnv)
if err != nil {
return nil, status.Errorf(codes.FailedPrecondition, "unable to create Docker client: %s", err)
}

cli.NegotiateAPIVersion(ctx)

dockerfile := b.config.Dockerfile
Expand Down Expand Up @@ -160,13 +163,102 @@ func (b *Builder) Build(
return nil, status.Errorf(codes.FailedPrecondition, "unable to create Docker context: %s", err)
}

// We now test if Docker is actually functional. We do this here because we
// need all of the above to complete the actual build.
log.Debug("testing if we should use a Docker fallback")
useImg := false
if fallback, err := wpdockerclient.Fallback(ctx, log, cli); err != nil {
log.Warn("error during check if we should use Docker fallback", "err", err)
return nil, status.Errorf(codes.Internal,
"error validating Docker connection: %s", err)
} else if fallback && hasImg() {
// If we're falling back and have "img" available, use that. If we
// don't have "img" available, we continue to try to use Docker. We'll
// fail but that error message should help the user.
step.Update("Docker isn't available. Falling back to daemonless image build...")
step.Done()
step = nil
if err := b.buildWithImg(
ctx, ui, sg, relDockerfile, contextDir, result.Name(),
); err != nil {
return nil, err
}

// Our image is in the img registry now. We set this so that
// future users of this result type know where to look.
result.Location = &Image_Img{Img: &empty.Empty{}}

// We set this to true so we use the img-based injector later
useImg = true
} else {
// No fallback, build with Docker
step.Done()
step = nil
if err := b.buildWithDocker(
ctx, ui, sg, cli, contextDir, relDockerfile, result.Name(),
); err != nil {
return nil, err
}
}

if !b.config.DisableCEB {
step = sg.Add("Injecting Waypoint Entrypoint...")

asset, err := assets.Asset("ceb/ceb")
if err != nil {
return nil, status.Errorf(codes.Internal, "unable to restore custom entry point binary: %s", err)
}

assetInfo, err := assets.AssetInfo("ceb/ceb")
if err != nil {
return nil, status.Errorf(codes.Internal, "unable to restore custom entry point binary: %s", err)
}

callback := func(cur []string) (*epinject.NewEntrypoint, error) {
ep := &epinject.NewEntrypoint{
Entrypoint: append([]string{"/waypoint-entrypoint"}, cur...),
InjectFiles: map[string]epinject.InjectFile{
"/waypoint-entrypoint": {
Reader: bytes.NewReader(asset),
Info: assetInfo,
},
},
}

return ep, nil
}

if !useImg {
_, err = epinject.AlterEntrypoint(ctx, result.Name(), callback)
} else {
_, err = epinject.AlterEntrypointImg(ctx, result.Name(), callback)
}
if err != nil {
return nil, status.Errorf(codes.Internal, "unable to set modify Docker entrypoint: %s", err)
}

step.Done()
}

return result, nil
}

func (b *Builder) buildWithDocker(
ctx context.Context,
ui terminal.UI,
sg terminal.StepGroup,
cli *client.Client,
contextDir string,
relDockerfile string,
tag string,
) error {
excludes, err := build.ReadDockerignore(contextDir)
if err != nil {
return nil, status.Errorf(codes.Internal, "unable to read .dockerignore: %s", err)
return status.Errorf(codes.Internal, "unable to read .dockerignore: %s", err)
}

if err := build.ValidateContextDirectory(contextDir, excludes); err != nil {
return nil, status.Errorf(codes.Internal, "error checking context: %s", err)
return status.Errorf(codes.Internal, "error checking context: %s", err)
}

// And canonicalize dockerfile name to a platform-independent one
Expand All @@ -179,25 +271,30 @@ func (b *Builder) Build(
})

if err != nil {
return nil, status.Errorf(codes.Internal, "unable to compress context: %s", err)
return status.Errorf(codes.Internal, "unable to compress context: %s", err)
}

ver := types.BuilderV1
if b.config.UseBuildKit {
ver = types.BuilderBuildKit
}

step.Done()
step = sg.Add("Building image...")
step := sg.Add("Building image...")
defer step.Abort()

stdout, _, err := ui.OutputWriters()
if err != nil {
return err
}

resp, err := cli.ImageBuild(ctx, buildCtx, types.ImageBuildOptions{
Version: ver,
Dockerfile: relDockerfile,
Tags: []string{result.Name()},
Tags: []string{tag},
Remove: true,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "error building image: %s", err)
return status.Errorf(codes.Internal, "error building image: %s", err)
}
defer resp.Body.Close()

Expand All @@ -208,44 +305,9 @@ func (b *Builder) Build(

err = jsonmessage.DisplayJSONMessagesStream(resp.Body, step.TermOutput(), termFd, true, nil)
if err != nil {
return nil, status.Errorf(codes.Internal, "unable to stream build logs to the terminal: %s", err)
return status.Errorf(codes.Internal, "unable to stream build logs to the terminal: %s", err)
}

step.Done()

if !b.config.DisableCEB {
step = sg.Add("Injecting Waypoint Entrypoint...")

asset, err := assets.Asset("ceb/ceb")
if err != nil {
return nil, status.Errorf(codes.Internal, "unable to restore custom entry point binary: %s", err)
}

assetInfo, err := assets.AssetInfo("ceb/ceb")
if err != nil {
return nil, status.Errorf(codes.Internal, "unable to restore custom entry point binary: %s", err)
}

_, err = epinject.AlterEntrypoint(ctx, result.Name(), func(cur []string) (*epinject.NewEntrypoint, error) {
ep := &epinject.NewEntrypoint{
Entrypoint: append([]string{"/waypoint-entrypoint"}, cur...),
InjectFiles: map[string]epinject.InjectFile{
"/waypoint-entrypoint": {
Reader: bytes.NewReader(asset),
Info: assetInfo,
},
},
}

return ep, nil
})

if err != nil {
return nil, status.Errorf(codes.Internal, "unable to set modify Docker entrypoint: %s", err)
}

step.Done()
}

return result, nil
return nil
}
Loading