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

Add support for passing build args to signers #275

Merged
merged 1 commit into from
Jun 20, 2024
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
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