Skip to content

Commit

Permalink
Add support for passing build args to signers
Browse files Browse the repository at this point in the history
I considered extending this to allow this for any kind of frontend (and
as such sticking it into the `dalec.Frontend` type), however there
really shouldn't be different vars passed to target frontends.
Hence why I am restricting this to just signers for now.

If we decide to later we can add this more generically, however it will
be much more difficult to take it away if we decide it was a bad idea.

Signed-off-by: Brian Goff <cpuguy83@gmail.com>
  • Loading branch information
cpuguy83 committed Jun 14, 2024
1 parent b417234 commit 4b98860
Show file tree
Hide file tree
Showing 11 changed files with 356 additions and 41 deletions.
30 changes: 29 additions & 1 deletion docs/spec.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@
"PackageConfig": {
"properties": {
"signer": {
"$ref": "#/$defs/Frontend",
"$ref": "#/$defs/PackageSigner",
"description": "Signer is the configuration to use for signing packages"
}
},
Expand Down Expand Up @@ -478,6 +478,34 @@
"type": "object",
"description": "PackageDependencies is a list of dependencies for a package."
},
"PackageSigner": {
"properties": {
"image": {
"type": "string",
"description": "Image specifies the frontend image to forward the build to.\nThis can be left unspecified *if* the original frontend has builtin support for the distro.\n\nIf the original frontend does not have builtin support for the distro, this must be specified or the build will fail.\nIf this is specified then it MUST be used.",
"examples": [
"docker.io/my/frontend:latest"
]
},
"cmdline": {
"type": "string",
"description": "CmdLine is the command line to use to forward the build to the frontend.\nBy default the frontend image's entrypoint/cmd is used."
},
"args": {
"additionalProperties": {
"type": "string"
},
"type": "object",
"description": "Args are passed along to the signer frontend as build args"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"image"
],
"description": "PackageSigner is the configuration for defining how to sign a package"
},
"PatchSpec": {
"properties": {
"source": {
Expand Down
20 changes: 18 additions & 2 deletions frontend/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,22 @@ func withTarget(t string) solveRequestOpt {
}
}

func withBuildArgs(args map[string]string) solveRequestOpt {
return func(req *gwclient.SolveRequest) error {
if len(args) == 0 {
return nil
}

if req.FrontendOpt == nil {
req.FrontendOpt = make(map[string]string)
}
for k, v := range args {
req.FrontendOpt["build-arg:"+k] = v
}
return nil
}
}

func toDockerfile(ctx context.Context, bctx llb.State, dt []byte, spec *dalec.SourceBuild, opts ...llb.ConstraintsOpt) solveRequestOpt {
return func(req *gwclient.SolveRequest) error {
req.Frontend = "dockerfile.v0"
Expand Down Expand Up @@ -118,7 +134,7 @@ func marshalDockerfile(ctx context.Context, dt []byte, opts ...llb.ConstraintsOp
return st.Marshal(ctx)
}

func ForwardToSigner(ctx context.Context, client gwclient.Client, cfg *dalec.Frontend, s llb.State) (llb.State, error) {
func ForwardToSigner(ctx context.Context, client gwclient.Client, cfg *dalec.PackageSigner, s llb.State) (llb.State, error) {
const (
sourceKey = "source"
contextKey = "context"
Expand All @@ -127,7 +143,7 @@ func ForwardToSigner(ctx context.Context, client gwclient.Client, cfg *dalec.Fro

opts := client.BuildOpts().Opts

req, err := newSolveRequest(toFrontend(cfg))
req, err := newSolveRequest(toFrontend(cfg.Frontend), withBuildArgs(cfg.Args))
if err != nil {
return llb.Scratch(), err
}
Expand Down
2 changes: 1 addition & 1 deletion helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ func (s *Spec) GetSymlinks(target string) map[string]SymlinkTarget {
return lm
}

func (s *Spec) GetSigner(targetKey string) (*Frontend, bool) {
func (s *Spec) GetSigner(targetKey string) (*PackageSigner, bool) {
if s.Targets != nil {
if t, ok := s.Targets[targetKey]; ok && hasValidSigner(t.PackageConfig) {
return t.PackageConfig.Signer, true
Expand Down
55 changes: 49 additions & 6 deletions load.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ func (s *Source) validate(failContext ...string) (retErr error) {
return retErr
}

var errUnknownArg = errors.New("unknown arg")

func (s *Spec) SubstituteArgs(env map[string]string) error {
lex := shell.NewLex('\\')

Expand All @@ -202,7 +204,7 @@ func (s *Spec) SubstituteArgs(env map[string]string) error {
for k, v := range env {
if _, ok := args[k]; !ok {
if !knownArg(k) {
return fmt.Errorf("unknown arg %q", k)
return fmt.Errorf("%w: %q", errUnknownArg, k)
}

// if the build arg isn't present in args by opt-in, skip
Expand Down Expand Up @@ -259,11 +261,15 @@ func (s *Spec) SubstituteArgs(env map[string]string) error {
}
}

for name, target := range s.Targets {
for _, t := range target.Tests {
if err := t.processBuildArgs(lex, args, path.Join(name, t.Name)); err != nil {
return err
}
for name, t := range s.Targets {
if err := t.processBuildArgs(name, lex, args); err != nil {
return fmt.Errorf("error processing build args for target %q: %w", name, err)
}
}

if s.PackageConfig != nil {
if err := s.PackageConfig.processBuildArgs(lex, args); err != nil {
return fmt.Errorf("could not process build args for base spec package config: %w", err)
}
}

Expand Down Expand Up @@ -496,3 +502,40 @@ func (g *SourceGenerator) Validate() error {
}
return nil
}

func (s *PackageSigner) processBuildArgs(lex *shell.Lex, args map[string]string) error {
for k, v := range s.Args {
updated, err := lex.ProcessWordWithMap(v, args)
if err != nil {
return fmt.Errorf("error performing shell expansion on env var %q: %w", k, err)
}
s.Args[k] = updated
}
return nil
}

func (t *Target) processBuildArgs(name string, lex *shell.Lex, args map[string]string) error {
for _, tt := range t.Tests {
if err := tt.processBuildArgs(lex, args, path.Join(name, tt.Name)); err != nil {
return err
}
}

if t.PackageConfig != nil {
if err := t.PackageConfig.processBuildArgs(lex, args); err != nil {
return fmt.Errorf("error processing package config build args: %w", err)
}
}

return nil
}

func (cfg *PackageConfig) processBuildArgs(lex *shell.Lex, args map[string]string) error {
if cfg.Signer != nil {
if err := cfg.Signer.processBuildArgs(lex, args); err != nil {
return fmt.Errorf("could not process build args for signer config: %w", err)
}
}

return nil
}
71 changes: 71 additions & 0 deletions load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import (
"encoding/json"
"errors"
"fmt"
"maps"
"os"
"reflect"
"testing"

"gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"
)

//go:embed test/fixtures/unmarshall/source-inline.yml
Expand Down Expand Up @@ -470,3 +474,70 @@ sources:
}
})
}

func TestSpec_SubstituteBuildArgs(t *testing.T) {
spec := &Spec{}
assert.NilError(t, spec.SubstituteArgs(nil))

env := map[string]string{}
assert.NilError(t, spec.SubstituteArgs(env))

// some values we'll be using throughout the test
const (
foo = "foo"
bar = "bar"
argWithDefault = "some default value"
plainOleValue = "some plain old value"
)

env["FOO"] = foo
err := spec.SubstituteArgs(env)
assert.ErrorIs(t, err, errUnknownArg, "args not defined in the spec should error out")

spec.Args = map[string]string{}

spec.Args["FOO"] = ""
assert.NilError(t, spec.SubstituteArgs(env))

pairs := map[string]string{
"FOO": "$FOO",
"BAR": "$BAR",
"WHATEVER": "$VAR_WITH_DEFAULT",
"REGULAR": plainOleValue,
}
spec.PackageConfig = &PackageConfig{
Signer: &PackageSigner{
Args: maps.Clone(pairs),
},
}
spec.Targets = map[string]Target{
"t1": {}, // nil signer
"t2": {
PackageConfig: &PackageConfig{
Signer: &PackageSigner{
Args: maps.Clone(pairs),
},
},
},
}

env["BAR"] = bar
assert.ErrorIs(t, err, errUnknownArg, "args not defined in the spec should error out")

spec.Args["BAR"] = ""
spec.Args["VAR_WITH_DEFAULT"] = argWithDefault

assert.NilError(t, spec.SubstituteArgs(env))

// Base package config
assert.Check(t, cmp.Equal(spec.PackageConfig.Signer.Args["FOO"], foo))
assert.Check(t, cmp.Equal(spec.PackageConfig.Signer.Args["BAR"], bar))
assert.Check(t, cmp.Equal(spec.PackageConfig.Signer.Args["WHATEVER"], argWithDefault))
assert.Check(t, cmp.Equal(spec.PackageConfig.Signer.Args["REGULAR"], plainOleValue))

// targets
assert.Check(t, cmp.Nil(spec.Targets["t1"].Frontend))
assert.Check(t, cmp.Equal(spec.Targets["t2"].PackageConfig.Signer.Args["BAR"], bar))
assert.Check(t, cmp.Equal(spec.Targets["t2"].PackageConfig.Signer.Args["WHATEVER"], argWithDefault))
assert.Check(t, cmp.Equal(spec.Targets["t2"].PackageConfig.Signer.Args["REGULAR"], plainOleValue))
}
9 changes: 8 additions & 1 deletion spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -523,10 +523,17 @@ type Target struct {
PackageConfig *PackageConfig `yaml:"package_config,omitempty" json:"package_config,omitempty"`
}

// PackageSigner is the configuration for defining how to sign a package
type PackageSigner struct {
*Frontend `yaml:",inline" json:",inline"`
// Args are passed along to the signer frontend as build args
Args map[string]string `yaml:"args,omitempty" json:"args,omitempty"`
}

// PackageConfig encapsulates the configuration for artifact targets
type PackageConfig struct {
// Signer is the configuration to use for signing packages
Signer *Frontend `yaml:"signer,omitempty" json:"signer,omitempty"`
Signer *PackageSigner `yaml:"signer,omitempty" json:"signer,omitempty"`
}

// TestSpec is used to execute tests against a container with the package installed in it.
Expand Down
74 changes: 46 additions & 28 deletions test/azlinux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,42 +303,60 @@ echo "$BAR" > bar.txt
}

t.Run("test signing", func(t *testing.T) {
t.Parallel()
spec := dalec.Spec{
Name: "foo",
Version: "v0.0.1",
Description: "foo bar baz",
Website: "https://foo.bar.baz",
Revision: "1",
PackageConfig: &dalec.PackageConfig{
Signer: &dalec.Frontend{
Image: phonySignerRef,
},
},
Sources: map[string]dalec.Source{
"foo": {
Inline: &dalec.SourceInline{
File: &dalec.SourceInlineFile{
Contents: "#!/usr/bin/env bash\necho \"hello, world!\"\n",
newSpec := func() *dalec.Spec {
return &dalec.Spec{
Name: "foo",
Version: "v0.0.1",
Description: "foo bar baz",
Website: "https://foo.bar.baz",
Revision: "1",
PackageConfig: &dalec.PackageConfig{
Signer: &dalec.PackageSigner{
Frontend: &dalec.Frontend{
Image: phonySignerRef,
},
},
},
Sources: map[string]dalec.Source{
"foo": {
Inline: &dalec.SourceInline{
File: &dalec.SourceInlineFile{
Contents: "#!/usr/bin/env bash\necho \"hello, world!\"\n",
},
},
},
},
},
Build: dalec.ArtifactBuild{
Steps: []dalec.BuildStep{
{
Command: "/bin/true",
Build: dalec.ArtifactBuild{
Steps: []dalec.BuildStep{
{
Command: "/bin/true",
},
},
},
},
Artifacts: dalec.Artifacts{
Binaries: map[string]dalec.ArtifactConfig{
"foo": {},
Artifacts: dalec.Artifacts{
Binaries: map[string]dalec.ArtifactConfig{
"foo": {},
},
},
},
}
}

runTest(t, distroSigningTest(t, &spec, signTarget))
t.Run("no args", func(t *testing.T) {
t.Parallel()
spec := newSpec()
runTest(t, distroSigningTest(t, spec, signTarget))
})

t.Run("with args", func(t *testing.T) {
t.Parallel()

spec := newSpec()
spec.PackageConfig.Signer.Args = map[string]string{
"HELLO": "world",
"FOO": "bar",
}
runTest(t, distroSigningTest(t, spec, signTarget))
})
})

t.Run("test systemd unit", func(t *testing.T) {
Expand Down
13 changes: 13 additions & 0 deletions test/fixtures/signer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ func main() {
File(llb.Mkfile("/target", 0o600, []byte(target))).
File(llb.Mkfile("/config.json", 0o600, configBytes))

// For any build-arg seen, write a file to /env/<KEY> with the contents
// being the value of the arg.
for k, v := range c.BuildOpts().Opts {
_, key, ok := strings.Cut(k, "build-arg:")
if !ok {
// not a build arg
continue
}
output = output.
File(llb.Mkdir("/env", 0o755)).
File(llb.Mkfile("/env/"+key, 0o600, []byte(v)))
}

def, err := output.Marshal(ctx)
if err != nil {
return nil, err
Expand Down
Loading

0 comments on commit 4b98860

Please sign in to comment.