Skip to content

Commit

Permalink
Implement routing to frontend
Browse files Browse the repository at this point in the history
Signed-off-by: Peter Engelbert <[email protected]>
  • Loading branch information
pmengelbert committed Feb 28, 2024
1 parent 3152ed6 commit 2b2344b
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 83 deletions.
170 changes: 145 additions & 25 deletions frontend/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ package frontend
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"slices"
"strings"

"github.com/Azure/dalec"
"github.com/containerd/containerd/platforms"
Expand All @@ -14,36 +18,138 @@ import (
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
)

func loadSpec(ctx context.Context, client *dockerui.Client) (*dalec.Spec, error) {
project, err := loadProject(ctx, client)
if err != nil {
return nil, err
var HandlerNotFound = errors.New("handler not found")

// `projectWrapper` provides some additional functionality to the
// project struct while leaving the Project struct as simple data.
type projectWrapper struct {
*dalec.Project
target string
isSingleSpec bool
}

func (p *projectWrapper) GetProject() *dalec.Project {
return p.Project
}

// `GetSpec` returns the main spec to build. When the Project is a
// single spec, return that spec. When the Project is a slice of
// specs, return the first matching spec with the `name` as specified
// when `NewProjectWrapper` was called. If no name was specified,
// return the last.
func (p *projectWrapper) GetSpec() *dalec.Spec {
if p.isSingleSpec {
return p.Spec
}

for _, spec := range p.Specs {
if spec.Name == p.target {
return &spec
}
}

// The `loadProject` function has already ensured there is at
// least one spec.
return &p.Specs[len(p.Specs)-1]
}

// This is a placeholder until it is implemented by PR #146
func (p *projectWrapper) GetGraph() *dalec.Graph {
panic("unimplemented")
}

type projectConfig struct {
target string
}

type projectOpt func(*projectConfig) error

func withTarget(name string) projectOpt {
return func(cfg *projectConfig) error {
cfg.target = name
return nil
}
}

func newProjectWrapper(p *dalec.Project, opts ...projectOpt) (*projectWrapper, error) {
config := &projectConfig{}

switch len(project.Specs) {
case 0:
if project.Spec == nil {
return nil, fmt.Errorf("no specs provided")
for _, o := range opts {
if err := o(config); err != nil {
return nil, fmt.Errorf("failed to set up project wrapper config: %w", err)
}
return project.Spec, nil
case 1:
return &project.Specs[0], nil
default:
return nil, fmt.Errorf("multi-spec not yet supported")
}

pw := projectWrapper{
Project: p,
target: config.target,
isSingleSpec: false,
}

// Specs cannot be directly compared because they contain
// slices and maps.
if pw.Spec != nil {
var e dalec.Spec
j, _ := json.Marshal(&e)
k, _ := json.Marshal(pw.Spec)
pw.isSingleSpec = slices.Compare(j, k) != 0
}

if pw.isSingleSpec && pw.target != "" {
return nil, fmt.Errorf("name %q requested as project target, but project is a single spec", pw.target)
}

return &pw, nil

}

func loadProject(ctx context.Context, client *dockerui.Client) (*dalec.Project, error) {
func loadProject(ctx context.Context, client *dockerui.Client, target string) (*projectWrapper, error) {
src, err := client.ReadEntrypoint(ctx, "Dockerfile")
if err != nil {
return nil, fmt.Errorf("could not read spec file: %w", err)
}

project, err := dalec.LoadProject(bytes.TrimSpace(src.Data))
if err != nil {
return nil, fmt.Errorf("error loading spec: %w", err)
return nil, err
}

pw, err := newProjectWrapper(project, withTarget(target))
if err != nil {
return nil, fmt.Errorf("error initializing project: %w", err)
}

if !pw.isSingleSpec && len(project.Specs) == 0 {
return nil, fmt.Errorf("no specs provided")
}

if pw.isSingleSpec && len(project.Specs) != 0 {
return nil, fmt.Errorf("format of project must be either a single spec or a list of specs nested under the `specs` key")
}

validateAndFillDefaults := func(s *dalec.Spec) error {
if err := s.Validate(); err != nil {
return err
}

s.FillDefaults()
return nil
}

switch {
case pw.isSingleSpec:
if err := validateAndFillDefaults(project.Spec); err != nil {
return nil, fmt.Errorf("error loading project: %w", err)
}
case len(project.Specs) != 0:
for i := range project.Specs {
if err := validateAndFillDefaults(&project.Specs[i]); err != nil {
return nil, fmt.Errorf("error validating project spec with name %q: %w", project.Specs[i].Name, err)
}
}
}
return project, nil

return pw, nil
}

func listBuildTargets(group string) []*targetWrapper {
Expand All @@ -60,7 +166,7 @@ func lookupHandler(target string) (BuildFunc, error) {

t := registeredHandlers.Get(target)
if t == nil {
return nil, fmt.Errorf("unknown target %q", target)
return nil, HandlerNotFound
}
return t.Build, nil
}
Expand Down Expand Up @@ -119,16 +225,35 @@ func Build(ctx context.Context, client gwclient.Client) (*gwclient.Result, error
return nil, fmt.Errorf("could not create build client: %w", err)
}

spec, err := loadSpec(ctx, bc)
dalecTarget := bc.Target
specTarget := ""

f, err := lookupHandler(bc.Target)
if errors.Is(err, HandlerNotFound) {
tgt, rest, ok := strings.Cut(bc.Target, "/")
if !ok {
return nil, fmt.Errorf("unable to parse target %q", bc.Target)
}

specTarget = tgt
dalecTarget = rest

f, err = lookupHandler(dalecTarget)
if err != nil {
return nil, fmt.Errorf("can't route target %q: %w", bc.Target, err)
}
}

project, err := loadProject(ctx, bc, specTarget)
if err != nil {
return nil, fmt.Errorf("error loading spec: %w", err)
}

if err := registerSpecHandlers(ctx, spec, client); err != nil {
if err := registerSpecHandlers(ctx, project, client); err != nil {
return nil, err
}

res, handled, err := bc.HandleSubrequest(ctx, makeRequestHandler(bc.Target))
res, handled, err := bc.HandleSubrequest(ctx, makeRequestHandler(dalecTarget))
if err != nil || handled {
return res, err
}
Expand All @@ -144,11 +269,6 @@ func Build(ctx context.Context, client gwclient.Client) (*gwclient.Result, error
}
}

f, err := lookupHandler(bc.Target)
if err != nil {
return nil, err
}

rb, err := bc.Build(ctx, func(ctx context.Context, platform *ocispecs.Platform, idx int) (gwclient.Reference, *image.Image, error) {
var targetPlatform, buildPlatform ocispecs.Platform
if platform != nil {
Expand Down
8 changes: 5 additions & 3 deletions frontend/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,9 @@ func SetDefault(group, name string) {
registeredHandlers.defaultHandler = t
}

func registerSpecHandlers(ctx context.Context, spec *dalec.Spec, client gwclient.Client) error {
func registerSpecHandlers(ctx context.Context, project *projectWrapper, client gwclient.Client) error {
var def *pb.Definition
spec := project.GetSpec()
marshlSpec := func() (*pb.Definition, error) {
if def != nil {
return def, nil
Expand Down Expand Up @@ -161,9 +162,10 @@ func registerSpecHandlers(ctx context.Context, spec *dalec.Spec, client gwclient
}

register := func(group string) error {
spec := spec
project := project

grp, _, _ := strings.Cut(group, "/")
t, ok := spec.Targets[grp]
t, ok := project.Targets[grp]
if !ok {
bklog.G(ctx).WithField("group", group).Debug("No target found in forwarded build")
return nil
Expand Down
53 changes: 0 additions & 53 deletions load.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package dalec

import (
"encoding/json"
goerrors "errors"
"fmt"
"os"
Expand All @@ -12,7 +11,6 @@ import (
"github.com/moby/buildkit/frontend/dockerfile/shell"
"github.com/moby/buildkit/frontend/dockerui"
"github.com/pkg/errors"
"golang.org/x/exp/slices"
)

func knownArg(key string) bool {
Expand Down Expand Up @@ -325,57 +323,6 @@ func LoadProject(dt []byte) (*Project, error) {
return nil, fmt.Errorf("error unmarshalling spec: %w", err)
}

// Because the Spec is `yaml:",inline", it will never be nil, so we need to
// check if it was provided some other way.`
var empty *bool
isEmpty := func(s *Spec) bool {
if empty != nil {
return *empty
}

if s == nil {
return true
}

// Specs cannot be directly compared because they contain slices and maps
var e Spec
j, _ := json.Marshal(&e)
k, _ := json.Marshal(s)
b := slices.Compare(j, k) == 0
empty = &b
return b
}

if isEmpty(project.Spec) && len(project.Specs) == 0 {
return nil, fmt.Errorf("no specs provided")
}

if !isEmpty(project.Spec) && len(project.Specs) != 0 {
return nil, fmt.Errorf("format of project must be either a single spec or a list of specs nested under the `specs` key")
}

validateAndFillDefaults := func(s *Spec) error {
if err := s.Validate(); err != nil {
return err
}

s.FillDefaults()
return nil
}

switch {
case !isEmpty(project.Spec):
if err := validateAndFillDefaults(project.Spec); err != nil {
return nil, fmt.Errorf("error loading project: %w", err)
}
case len(project.Specs) != 0:
for i := range project.Specs {
if err := validateAndFillDefaults(&project.Specs[i]); err != nil {
return nil, fmt.Errorf("error loading project spec with name %q: %w", project.Specs[i].Name, err)
}
}
}

return &project, nil
}

Expand Down
11 changes: 9 additions & 2 deletions project.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
package dalec

// `Project` is either a) A single spec or b) a list of specs nested
// under the `specs` key. In the case of the latter, the specs may
// or may not depend on one another.
type Project struct {
*Spec `json:",inline,omitempty" yaml:",inline,omitempty"`
Specs []Spec `json:"specs" yaml:"specs"`
*Spec `json:",inline,omitempty" yaml:",inline,omitempty"`
Frontends map[string]Frontend `json:"frontend,omitempty" yaml:"frontend,omitempty"`
Specs []Spec `json:"specs,omitempty" yaml:"specs,omitempty"`
}

// This is a placeholder until it is implemented by PR #146
type Graph struct{}

0 comments on commit 2b2344b

Please sign in to comment.