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

Support remote operations for docker-pull #3253

Merged
merged 10 commits into from
Apr 27, 2022
3 changes: 3 additions & 0 deletions .changelog/3253.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
plugin/docker: support remote operations for docker-pull plugin
```
30 changes: 27 additions & 3 deletions builtin/docker/pull/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ import (
"context"
"encoding/base64"
"encoding/json"
"os"
"os/exec"

"github.com/docker/cli/cli/config"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
Expand All @@ -16,9 +13,12 @@ import (
"github.com/docker/docker/registry"
"github.com/hashicorp/go-argmapper"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/waypoint-plugin-sdk/component"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
empty "google.golang.org/protobuf/types/known/emptypb"
"os"
"os/exec"

"github.com/hashicorp/waypoint-plugin-sdk/docs"
"github.com/hashicorp/waypoint-plugin-sdk/terminal"
Expand All @@ -39,6 +39,11 @@ func (b *Builder) BuildFunc() interface{} {
return b.Build
}

// BuildFunc implements component.BuilderODR
func (b *Builder) BuildODRFunc() interface{} {
return b.BuildODR
}

// Config is the configuration structure for the registry.
type BuilderConfig struct {
// Image to pull
Expand Down Expand Up @@ -158,6 +163,25 @@ type buildArgs struct {
HasRegistry bool
}

// Build
func (b *Builder) BuildODR(
ctx context.Context,
ui terminal.UI,
src *component.Source,
log hclog.Logger,
ai *wpdocker.AccessInfo,
) (*wpdocker.Image, error) {
sg := ui.StepGroup()
defer sg.Wait()

result, err := b.pullWithKaniko(ctx, ui, sg, log, ai)
if err != nil {
return nil, err
}

return result, nil
}

// Build
func (b *Builder) Build(args buildArgs) (*wpdocker.Image, error) {
// Pull all the args out to top-level values. This is mostly done
Expand Down
159 changes: 159 additions & 0 deletions builtin/docker/pull/kaniko.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package dockerpull

import (
"context"
"fmt"
"github.com/docker/distribution/reference"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/waypoint-plugin-sdk/terminal"
wpdocker "github.com/hashicorp/waypoint/builtin/docker"
"github.com/hashicorp/waypoint/internal/assets"
"github.com/hashicorp/waypoint/internal/pkg/epinject/ociregistry"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
empty "google.golang.org/protobuf/types/known/emptypb"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
)

func (b *Builder) pullWithKaniko(
ctx context.Context,
ui terminal.UI,
sg terminal.StepGroup,
log hclog.Logger,
ai *wpdocker.AccessInfo,
) (*wpdocker.Image, error) {
step := sg.Add("Pulling Docker image with Kaniko...")
defer func() {
if step != nil {
step.Abort()
}
}()

target := &wpdocker.Image{
Image: b.config.Image,
Tag: b.config.Tag,
Location: &wpdocker.Image_Docker{Docker: &empty.Empty{}},
}

var oci ociregistry.Server
oci.DisableEntrypoint = b.config.DisableCEB
oci.Logger = log

if ai.Auth != nil {
switch sv := ai.Auth.(type) {
case *wpdocker.AccessInfo_Encoded:
user, pass, err := wpdocker.CredentialsFromConfig(sv.Encoded)
if err != nil {
return nil, err
}
oci.AuthConfig.Username = user
oci.AuthConfig.Password = pass
case *wpdocker.AccessInfo_Header:
oci.AuthConfig.Auth = sv.Header
case *wpdocker.AccessInfo_UserPass_:
oci.AuthConfig.Username = sv.UserPass.Username
oci.AuthConfig.Password = sv.UserPass.Password
paladin-devops marked this conversation as resolved.
Show resolved Hide resolved
default:
return nil, status.Error(codes.Unauthenticated, "Unexpected auth type.")
}
}

// Determine the host that we're setting auth for. We have to parse the
// image for this cause it may not contain a host. Luckily Docker has
// libs to normalize this all for us.
log.Trace("determining host for auth configuration", "image", target.Name())
ref, err := reference.ParseNormalizedNamed(target.Image)
if err != nil {
return nil, status.Errorf(codes.Internal, "unable to parse image name: %s", err)
}

host := reference.Domain(ref)
if host == "docker.io" {
// The normalized name parse above will turn short names like "foo/bar"
// into "docker.io/foo/bar" but the actual registry host for these
// is "index.docker.io".
host = "index.docker.io"
}
log.Trace("auth host", "host", host)

if ai.Insecure {
oci.Upstream = "http://" + host
} else {
oci.Upstream = "https://" + host
}

refPath := reference.Path(ref)

if !b.config.DisableCEB {
step.Update("Injecting entrypoint...")
// For Kaniko we can use our runtime arch because the image we build
paladin-devops marked this conversation as resolved.
Show resolved Hide resolved
// always matches the architecture of our Kaniko environment.
assetName, ok := assets.CEBArch[runtime.GOARCH]
if !ok {
return nil, status.Errorf(codes.FailedPrecondition,
"automatic entrypoint injection not supported for architecture: %s", runtime.GOARCH)
}

data, err := assets.Asset(assetName)
if err != nil {
return nil, status.Errorf(codes.Internal, "unable to restore custom entrypoint binary: %s", err)
}

step = sg.Add("Testing registry and uploading entrypoint layer")

err = oci.SetupEntrypointLayer(refPath, data)
if err != nil {
return nil, status.Errorf(codes.Internal, "error setting up entrypoint layer: %s", err)
}
}

// Setting up local registry to which Kaniko will push
li, err := net.Listen("tcp", "localhost:0")
paladin-devops marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}

defer li.Close()
go http.Serve(li, &oci)

port := li.Addr().(*net.TCPAddr).Port

localRef := fmt.Sprintf("localhost:%d/%s:%s", port, refPath, ai.Tag)

dockerfileBS := []byte(fmt.Sprintf("FROM %s:%s\n", target.Image, target.Tag))
err = os.WriteFile("Dockerfile", dockerfileBS, 0644)
if err != nil {
return nil, err
}
// Start constructing our arg string for img
args := []string{
"/kaniko/executor",
"-f", filepath.Dir("Dockerfile"),
"-d", localRef,
}

log.Debug("executing kaniko", "args", args)
step.Update("Executing Kaniko...")

// Command output should go to the step
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
cmd.Stdout = step.TermOutput()
cmd.Stderr = cmd.Stdout

// check for error from executor run
if err := cmd.Run(); err != nil {
return nil, err
}

step.Done()

step = sg.Add("Image pull completed.")
step.Done()

return target, nil
}