Skip to content

Commit

Permalink
feat(initrd): Support Dockerfile secrets, targets and build arguments (
Browse files Browse the repository at this point in the history
…#1870)

Reviewed-by: Cezar Craciunoiu <[email protected]>
Approved-by: Cezar Craciunoiu <[email protected]>
  • Loading branch information
craciunoiuc committed Sep 2, 2024
2 parents 2490458 + aec52e0 commit c71f600
Show file tree
Hide file tree
Showing 3 changed files with 274 additions and 10 deletions.
94 changes: 94 additions & 0 deletions cmdfactory/flags_string_array.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: BSD-3-Clause
// Copyright (c) 2012 Alex Ogier.
// Copyright (c) 2012 The Go Authors.
// Copyright (c) 2022, Unikraft GmbH and The KraftKit Authors.
// Licensed under the BSD-3-Clause License (the "License").
// You may not use this file except in compliance with the License.
package cmdfactory

import (
"bytes"
"encoding/csv"
"strings"

"github.com/spf13/pflag"
)

// -- stringArray Value
type stringArrayValue struct {
value *[]string
changed bool
}

func newStringArrayValue(val []string, p *[]string) *stringArrayValue {
ssv := new(stringArrayValue)
ssv.value = p
*ssv.value = val
return ssv
}

func (s *stringArrayValue) Set(val string) error {
if !s.changed {
*s.value = []string{val}
s.changed = true
} else {
*s.value = append(*s.value, val)
}
return nil
}

func (s *stringArrayValue) Append(val string) error {
*s.value = append(*s.value, val)
return nil
}

func (s *stringArrayValue) Replace(val []string) error {
out := make([]string, len(val))
for i, d := range val {
var err error
out[i] = d
if err != nil {
return err
}
}
*s.value = out
return nil
}

func (s *stringArrayValue) GetSlice() []string {
out := make([]string, len(*s.value))
copy(out, *s.value)
return out
}

func (s *stringArrayValue) Type() string {
return "strings"
}

func (s *stringArrayValue) String() string {
str, _ := writeAsCSV(*s.value)
return "[" + str + "]"
}

func writeAsCSV(vals []string) (string, error) {
b := &bytes.Buffer{}
w := csv.NewWriter(b)
err := w.Write(vals)
if err != nil {
return "", err
}
w.Flush()
return strings.TrimSuffix(b.String(), "\n"), nil
}

// StringArrayVar defines a string flag with specified name, default value, and usage string.
// The argument p points to a []string variable in which to store the value of the flag.
// The value of each argument will not try to be separated by comma. Use a StringSlice for that.
func StringArrayVar(p *[]string, name string, value []string, usage string) *pflag.Flag {
return VarF(newStringArrayValue(value, p), name, usage)
}

// StringArrayVarP is like StringArrayVar, but accepts a shorthand letter that can be used after a single dash.
func StringArrayVarP(p *[]string, name, shorthand string, value []string, usage string) *pflag.Flag {
return VarPF(newStringArrayValue(value, p), name, shorthand, usage)
}
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,8 @@ github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FK
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
Expand Down
188 changes: 178 additions & 10 deletions initrd/dockerfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package initrd
import (
"archive/tar"
"context"
"encoding/csv"
"fmt"
"io"
"net"
Expand All @@ -18,6 +19,7 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"kraftkit.sh/cmdfactory"
"kraftkit.sh/config"
"kraftkit.sh/cpio"
"kraftkit.sh/log"
Expand All @@ -29,6 +31,8 @@ import (
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/auth"
"github.com/moby/buildkit/session/filesync"
"github.com/moby/buildkit/session/secrets/secretsprovider"
"github.com/moby/buildkit/session/sshforward/sshprovider"
"github.com/moby/buildkit/util/progress/progressui"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
Expand All @@ -40,6 +44,52 @@ import (
_ "github.com/moby/buildkit/client/connhelper/ssh"
)

var (
buildArgs = []string{}
buildSecrets = []string{}
buildTarget string
)

func init() {
for _, cmd := range []string{
"kraft build",
"kraft cloud compose build",
"kraft cloud compose up",
"kraft cloud deploy",
"kraft compose build",
"kraft compose up",
"kraft pkg",
} {
cmdfactory.RegisterFlag(
cmd,
cmdfactory.StringArrayVar(
&buildArgs,
"build-arg",
[]string{},
"Supply build arguments when building a Dockerfile",
),
)
cmdfactory.RegisterFlag(
cmd,
cmdfactory.StringVar(
&buildTarget,
"build-target",
"",
"Supply multi-stage target when building Dockerfile",
),
)
cmdfactory.RegisterFlag(
cmd,
cmdfactory.StringArrayVar(
&buildSecrets,
"build-secret",
[]string{},
"Supply secrets when building Dockerfile",
),
)
}
}

var testcontainersLoggingHook = func(logger testcontainers.Logging) testcontainers.ContainerLifecycleHooks {
shortContainerID := func(c testcontainers.Container) string {
return c.GetContainerID()[:12]
Expand Down Expand Up @@ -282,13 +332,85 @@ func (initrd *dockerfile) Build(ctx context.Context) (string, error) {
}
}

solveOpt := &client.SolveOpt{
Ref: identity.NewID(),
Session: []session.Attachable{
&buildkitAuthProvider{
config.G[config.KraftKit](ctx).Auth,
},
attrs := map[string]string{
"filename": filepath.Base(initrd.dockerfile),
}

if len(buildTarget) > 0 {
attrs["target"] = buildTarget
}

for _, arg := range buildArgs {
k, v, ok := strings.Cut(arg, "=")
if !ok {
v, ok = os.LookupEnv(k)
if !ok {
log.G(ctx).
WithField("arg", k).
Warn("could not find build-arg in environment")
continue
}
}

attrs["build-arg:"+k] = v
}

session := []session.Attachable{
&buildkitAuthProvider{
config.G[config.KraftKit](ctx).Auth,
},
}

fs := make([]secretsprovider.Source, 0, len(buildSecrets))
for _, v := range buildSecrets {
s, err := parseSecret(v)
if err != nil {
return "", err
}
fs = append(fs, *s)
}

secretStore, err := secretsprovider.NewStore(fs)
if err != nil {
return "", err
}

session = append(session,
secretsprovider.NewSecretProvider(secretStore),
)

sshAgentPath := ""

// Only a single socket path is supported, prioritize ones targeting kraftkit.
if p, ok := os.LookupEnv("KRAFTKIT_BUILDKIT_SSH_AGENT"); ok {
p, err := filepath.Abs(p)
if err != nil {
return "", err
}
sshAgentPath = p
} else if p, ok := os.LookupEnv("SSH_AUTH_SOCK"); ok {
p, err := filepath.Abs(p)
if err != nil {
return "", err
}
sshAgentPath = p
}
if len(sshAgentPath) > 0 {
sshSession, err := sshprovider.NewSSHAgentProvider([]sshprovider.AgentConfig{{
Paths: []string{sshAgentPath},
}})
if err != nil {
return "", err
}

session = append(session,
sshSession,
)
}

solveOpt := &client.SolveOpt{
Ref: identity.NewID(),
Session: session,
Exports: []client.ExportEntry{
{
Type: client.ExporterTar,
Expand All @@ -304,10 +426,8 @@ func (initrd *dockerfile) Build(ctx context.Context) (string, error) {
"context": initrd.opts.workdir,
"dockerfile": initrd.opts.workdir,
},
Frontend: "dockerfile.v0",
FrontendAttrs: map[string]string{
"filename": filepath.Base(initrd.dockerfile),
},
Frontend: "dockerfile.v0",
FrontendAttrs: attrs,
}

if initrd.opts.arch != "" {
Expand Down Expand Up @@ -563,3 +683,51 @@ func (ap *buildkitAuthProvider) GetTokenAuthority(ctx context.Context, req *auth
func (ap *buildkitAuthProvider) VerifyTokenAuthority(ctx context.Context, req *auth.VerifyTokenAuthorityRequest) (*auth.VerifyTokenAuthorityResponse, error) {
return nil, status.Errorf(codes.Unavailable, "client side tokens disabled")
}

// parseSecret is derived from [0]
// [0]: https://github.com/moby/buildkit/blob/6737deb443f66e5da79a8ab9a9af36b64b5035cc/cmd/buildctl/build/secret.go#L29-L65
func parseSecret(val string) (*secretsprovider.Source, error) {
csvReader := csv.NewReader(strings.NewReader(val))
fields, err := csvReader.Read()
if err != nil {
return nil, fmt.Errorf("failed to parse csv secret: %w", err)
}

fs := secretsprovider.Source{}

var typ string
for _, field := range fields {
key, value, ok := strings.Cut(field, "=")
if !ok {
return nil, fmt.Errorf("invalid field '%s' must be a key=value pair", field)
}

key = strings.ToLower(key)
switch key {
case "type":
if value != "file" && value != "env" {
return nil, fmt.Errorf("unsupported secret type %q", value)
}
typ = value
case "id":
fs.ID = value
case "source", "src":
value, err = filepath.Abs(value)
if err != nil {
return nil, fmt.Errorf("secret path '%s' must be absolute: %w", value, err)
}
fs.FilePath = value
case "env":
fs.Env = value
default:
return nil, fmt.Errorf("unexpected key '%s' in '%s'", key, field)
}
}

if typ == "env" && fs.Env == "" {
fs.Env = fs.FilePath
fs.FilePath = ""
}

return &fs, nil
}

0 comments on commit c71f600

Please sign in to comment.