Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for additional auth in docker build #18604

Closed
wants to merge 3 commits into from
Closed
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
2 changes: 1 addition & 1 deletion components/image-builder-bob/cmd/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ var daemonCmd = &cobra.Command{
}

skt := args[0]
cl, teardown, err := builder.StartBuildkit(skt)
cl, teardown, err := builder.StartBuildkit(skt, os.Getenv("WORKSPACEKIT_BOBPROXY_ADDITIONALAUTH"))
if err != nil {
log.WithError(err).Fatal("cannot start daemon")
}
Expand Down
2 changes: 1 addition & 1 deletion components/image-builder-bob/cmd/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ var proxyCmd = &cobra.Command{
Tag: targettag,
Auth: auth,
},
})
}, auth)
if err != nil {
log.Fatal(err)
}
Expand Down
94 changes: 63 additions & 31 deletions components/image-builder-bob/pkg/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
package builder

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
Expand All @@ -16,9 +16,8 @@ import (
"time"

"github.com/gitpod-io/gitpod/common-go/log"
toml "github.com/pelletier/go-toml"

"github.com/docker/cli/cli/config/configfile"
"github.com/docker/cli/cli/config/types"
"github.com/moby/buildkit/client"
"golang.org/x/xerrors"
)
Expand Down Expand Up @@ -49,10 +48,10 @@ func (b *Builder) Build() error {

if err != nil {
log.Warn("cannot connect to node-local buildkitd - falling back to pod-local one")
cl, teardown, err = StartBuildkit(buildkitdSocketPath)
cl, teardown, err = StartBuildkit(buildkitdSocketPath, b.Config.WorkspaceLayerAuth)
}
} else {
cl, teardown, err = StartBuildkit(buildkitdSocketPath)
cl, teardown, err = StartBuildkit(buildkitdSocketPath, b.Config.WorkspaceLayerAuth)
}
if err != nil {
return err
Expand Down Expand Up @@ -80,7 +79,7 @@ func (b *Builder) buildBaseLayer(ctx context.Context, cl *client.Client) error {
}

log.Info("building base image")
return buildImage(ctx, b.Config.ContextDir, b.Config.Dockerfile, b.Config.WorkspaceLayerAuth, b.Config.BaseRef)
return buildImage(ctx, b.Config.ContextDir, b.Config.Dockerfile, b.Config.BaseRef)
}

func (b *Builder) buildWorkspaceImage(ctx context.Context, cl *client.Client) (err error) {
Expand All @@ -96,10 +95,10 @@ func (b *Builder) buildWorkspaceImage(ctx context.Context, cl *client.Client) (e
return xerrors.Errorf("unexpected error creating temporal directory: %w", err)
}

return buildImage(ctx, contextDir, filepath.Join(contextDir, "Dockerfile"), b.Config.WorkspaceLayerAuth, b.Config.TargetRef)
return buildImage(ctx, contextDir, filepath.Join(contextDir, "Dockerfile"), b.Config.TargetRef)
}

func buildImage(ctx context.Context, contextDir, dockerfile, authLayer, target string) (err error) {
func buildImage(ctx context.Context, contextDir, dockerfile, target string) (err error) {
log.Info("waiting for build context")
waitctx, cancel := context.WithTimeout(ctx, 30*time.Minute)
defer cancel()
Expand All @@ -109,28 +108,6 @@ func buildImage(ctx context.Context, contextDir, dockerfile, authLayer, target s
return err
}

dockerConfig := "/tmp/config.json"
defer os.Remove(dockerConfig)

if authLayer != "" {
configFile := configfile.ConfigFile{
AuthConfigs: make(map[string]types.AuthConfig),
}

err := configFile.LoadFromReader(bytes.NewReader([]byte(fmt.Sprintf(`{"auths": %v }`, authLayer))))
if err != nil {
return xerrors.Errorf("unexpected error reading registry authentication: %w", err)
}

f, _ := os.OpenFile(dockerConfig, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
defer f.Close()

err = configFile.SaveToWriter(f)
if err != nil {
return xerrors.Errorf("unexpected error writing registry authentication: %w", err)
}
}

contextdir := contextDir
if contextdir == "" {
contextdir = "."
Expand Down Expand Up @@ -200,7 +177,7 @@ func waitForBuildContext(ctx context.Context) error {
}

// StartBuildkit starts a local buildkit daemon
func StartBuildkit(socketPath string) (cl *client.Client, teardown func() error, err error) {
func StartBuildkit(socketPath string, authLayer string) (cl *client.Client, teardown func() error, err error) {
stderr, err := ioutil.TempFile(os.TempDir(), "buildkitd_stderr")
if err != nil {
return nil, nil, xerrors.Errorf("cannot create buildkitd log file: %w", err)
Expand All @@ -210,7 +187,27 @@ func StartBuildkit(socketPath string) (cl *client.Client, teardown func() error,
return nil, nil, xerrors.Errorf("cannot create buildkitd log file: %w", err)
}

buildkitCfg, err := createBuildkitConfig(authLayer)
if err != nil {
return nil, nil, xerrors.Errorf("cannot create buildkitd config: %w", err)
}
buildkitCfgData, err := toml.Marshal(buildkitCfg)
if err != nil {
return nil, nil, xerrors.Errorf("cannot marshal buildkitd config: %w", err)
}
buildkitCfgFile := "/etc/buildkitd.toml"
f, err := os.Create(buildkitCfgFile)
defer f.Close()
if err != nil {
return nil, nil, xerrors.Errorf("unexpected error creating buildkitd config file: %w", err)
}
_, err = f.Write(buildkitCfgData)
if err != nil {
return nil, nil, xerrors.Errorf("unexpected error writing buildkitd config file: %w", err)
}

cmd := exec.Command("buildkitd",
"--config"+buildkitCfgFile,
"--debug",
"--addr="+socketPath,
"--oci-worker-net=host",
Expand Down Expand Up @@ -292,3 +289,38 @@ func connectToBuildkitd(socketPath string) (cl *client.Client, err error) {

return nil, xerrors.Errorf("cannot connect to buildkitd")
}

type BuildkitConfig struct {
Registry map[string]RegistryConfig `toml:"registry"`
}
type RegistryConfig struct {
Mirrors *[]string `toml:"mirrors"`
PlainHttp *bool `toml:"http"`
}

func createBuildkitConfig(authLayer string) (buildkitConfig BuildkitConfig, err error) {
registryConfig := make(map[string]RegistryConfig)

registries := []string{}

err = json.Unmarshal([]byte(authLayer), &registries)
if err != nil {
return buildkitConfig, xerrors.Errorf("unexpected error reading registry authentication: %w", err)
}

t := true
for _, host := range registries {
if host != "" {
registryConfig[host] = RegistryConfig{
Mirrors: &[]string{"localhost:8080"},
}
}
}

registryConfig["localhost:8080"] = RegistryConfig{
PlainHttp: &t,
}
return BuildkitConfig{
Registry: registryConfig,
}, nil
}
61 changes: 2 additions & 59 deletions components/image-builder-bob/pkg/builder/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
package builder

import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"os"
"path/filepath"
"strings"
Expand All @@ -21,7 +18,6 @@ type Config struct {
BaseRef string
BaseContext string
BuildBase bool
BaseLayerAuth string
WorkspaceLayerAuth string
Dockerfile string
ContextDir string
Expand All @@ -36,7 +32,6 @@ func GetConfigFromEnv() (*Config, error) {
BaseRef: os.Getenv("BOB_BASE_REF"),
BaseContext: os.Getenv("THEIA_WORKSPACE_ROOT"),
BuildBase: os.Getenv("BOB_BUILD_BASE") == "true",
BaseLayerAuth: os.Getenv("BOB_BASELAYER_AUTH"),
ChevronTango marked this conversation as resolved.
Show resolved Hide resolved
WorkspaceLayerAuth: os.Getenv("BOB_WSLAYER_AUTH"),
Dockerfile: os.Getenv("BOB_DOCKERFILE_PATH"),
ContextDir: os.Getenv("BOB_CONTEXT_DIR"),
Expand Down Expand Up @@ -66,61 +61,9 @@ func GetConfigFromEnv() (*Config, error) {
return nil, xerrors.Errorf("BOB_DOCKERFILE_PATH does not exist or isn't a file")
}
}

var authKey = os.Getenv("BOB_AUTH_KEY")
ChevronTango marked this conversation as resolved.
Show resolved Hide resolved
if authKey != "" {
if len(authKey) != 32 {
return nil, xerrors.Errorf("BOB_AUTH_KEY must be exactly 32 bytes long")
}

// we have an authkey, hence assume that the auth fields are base64 encoded and encrypted
if cfg.BaseLayerAuth != "" {
dec, err := base64.RawStdEncoding.DecodeString(cfg.BaseLayerAuth)
if err != nil {
return nil, xerrors.Errorf("BOB_BASELAYER_AUTH is not base64 encoded but BOB_AUTH_KEY is present")
}
cfg.BaseLayerAuth, err = decrypt(dec, authKey)
if err != nil {
return nil, xerrors.Errorf("cannot decrypt BOB_BASELAYER_AUTH: %w", err)
}
}
if cfg.WorkspaceLayerAuth != "" {
dec, err := base64.RawStdEncoding.DecodeString(cfg.WorkspaceLayerAuth)
if err != nil {
return nil, xerrors.Errorf("BOB_WSLAYER_AUTH is not base64 encoded but BOB_AUTH_KEY is present")
}
cfg.WorkspaceLayerAuth, err = decrypt(dec, authKey)
if err != nil {
return nil, xerrors.Errorf("cannot decrypt BOB_WSLAYER_AUTH: %w", err)
}
}
if cfg.WorkspaceLayerAuth == "" {
cfg.WorkspaceLayerAuth = "[]"
}

return cfg, nil
}

// source: https://astaxie.gitbooks.io/build-web-application-with-golang/en/09.6.html
func decrypt(ciphertext []byte, key string) (string, error) {
c, err := aes.NewCipher([]byte(key))
if err != nil {
return "", err
}

gcm, err := cipher.NewGCM(c)
if err != nil {
return "", err
}

nonceSize := gcm.NonceSize()
if len(ciphertext) < nonceSize {
return "", xerrors.Errorf("ciphertext too short")
}

nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
res, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", err
}

return string(res), nil
}
43 changes: 29 additions & 14 deletions components/image-builder-bob/pkg/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (

const authKey = "authKey"

func NewProxy(host *url.URL, aliases map[string]Repo) (*Proxy, error) {
func NewProxy(host *url.URL, aliases map[string]Repo, defaultAuth func() docker.Authorizer) (*Proxy, error) {
if host.Host == "" || host.Scheme == "" {
return nil, fmt.Errorf("host Host or Scheme are missing")
}
Expand All @@ -31,18 +31,20 @@ func NewProxy(host *url.URL, aliases map[string]Repo) (*Proxy, error) {
aliases[k] = v
}
return &Proxy{
Host: *host,
Aliases: aliases,
proxies: make(map[string]*httputil.ReverseProxy),
Host: *host,
Aliases: aliases,
proxies: make(map[string]*httputil.ReverseProxy),
defaultAuth: defaultAuth,
}, nil
}

type Proxy struct {
Host url.URL
Aliases map[string]Repo

mu sync.Mutex
proxies map[string]*httputil.ReverseProxy
mu sync.Mutex
proxies map[string]*httputil.ReverseProxy
defaultAuth func() docker.Authorizer
}

type Repo struct {
Expand Down Expand Up @@ -126,14 +128,21 @@ func (proxy *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
break
}
}
var auth docker.Authorizer
ns := r.URL.Query().Get("ns")
if repo == nil {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
auth = proxy.defaultAuth()
if ns == "" {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
log.Debug("no repo found, using direct proxy.")
} else {
auth = repo.Auth()
}

r.Host = r.URL.Host

auth := repo.Auth()
r = r.WithContext(context.WithValue(ctx, authKey, auth))

err := auth.Authorize(ctx, r)
Expand All @@ -146,11 +155,11 @@ func (proxy *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.WithField("req", r.URL.Path).Info("serving request")

r.RequestURI = ""
proxy.reverse(alias).ServeHTTP(w, r)
proxy.reverse(alias, ns).ServeHTTP(w, r)
}

// reverse produces an authentication-adding reverse proxy for a given repo alias
func (proxy *Proxy) reverse(alias string) *httputil.ReverseProxy {
func (proxy *Proxy) reverse(alias string, namespace string) *httputil.ReverseProxy {
proxy.mu.Lock()
defer proxy.mu.Unlock()

Expand All @@ -159,12 +168,18 @@ func (proxy *Proxy) reverse(alias string) *httputil.ReverseProxy {
}

repo, ok := proxy.Aliases[alias]
var host string
if !ok {
// we don't have an alias, hence don't know what to do other than try and proxy.
// At this poing things will probably fail.
return nil
if namespace == "" {
// At this poing things will probably fail.
return nil
}
host = namespace
} else {
host = repo.Host
}
rp := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: "https", Host: repo.Host})
rp := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: "https", Host: host})

client := retryablehttp.NewClient()
client.RetryMax = 3
Expand Down
Loading