From 4defe768267e21a91d5846d3cd99de31141c6789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wojciech=20Ma=C5=82ota-W=C3=B3jcik?= <59281144+outofforest@users.noreply.github.com> Date: Sat, 29 Jul 2023 09:59:15 +0200 Subject: [PATCH] Use new design of `isolator` (#253) --- go.mod | 4 +- go.sum | 10 +- infra/base/docker.go | 90 +++++----- infra/build.go | 314 +++++++++++++++------------------- infra/description/commands.go | 10 +- infra/description/types.go | 7 +- 6 files changed, 189 insertions(+), 246 deletions(-) diff --git a/go.mod b/go.mod index 92f59a5..66851ea 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/google/uuid v1.3.0 github.com/outofforest/go-zfs/v3 v3.1.14 github.com/outofforest/ioc/v2 v2.5.2 - github.com/outofforest/isolator v0.8.2 + github.com/outofforest/isolator v0.12.0 github.com/outofforest/logger v0.4.0 github.com/outofforest/parallel v0.2.3 github.com/outofforest/run v0.6.0 @@ -40,6 +40,6 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/net v0.11.0 // indirect + golang.org/x/net v0.12.0 // indirect golang.org/x/sync v0.3.0 // indirect ) diff --git a/go.sum b/go.sum index 5bb6d5c..aa399c1 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,8 @@ github.com/outofforest/go-zfs/v3 v3.1.14 h1:F0MosonFuGwiIotlMVKoMsvAmISnOWOAncev github.com/outofforest/go-zfs/v3 v3.1.14/go.mod h1:H8SgVKbvhso4bQGQMn6P21mFEd3oDOIaBH2w0Bcsu3c= github.com/outofforest/ioc/v2 v2.5.2 h1:4mNzLuzoZTXL/cO0qf1TrSYvejMgbZz5OUhdLzAUbek= github.com/outofforest/ioc/v2 v2.5.2/go.mod h1:yI+FHuHchC/t6nVo3WJ96qEgCXdHQKFI/4wW2/75YcU= -github.com/outofforest/isolator v0.8.2 h1:QbNe1kD+ktdgN2Haps6Mx3r1vB0aJFHDLSm8C/WDZI0= -github.com/outofforest/isolator v0.8.2/go.mod h1:CWQkLwr3qXyXxouqvS1QDC1nfUQ2FCdcUJSMrZVikJc= +github.com/outofforest/isolator v0.12.0 h1:9t5MZGvyfoqsCtrXRTGJ13QPt9JMD67wTqcOzFItaIs= +github.com/outofforest/isolator v0.12.0/go.mod h1:vxqlNlksXSf6MZazVN4ltWL0fSEt4p7YFGB6tst5mQw= github.com/outofforest/libexec v0.3.9 h1:KvVLuKDHqpydwNoKm+j36hi9DVPU61X4oHonlZ5cw88= github.com/outofforest/libexec v0.3.9/go.mod h1:J2rUB/m0ER8UNOHd3/UQM55bvh1cbMwhb8gibeF/zyo= github.com/outofforest/logger v0.3.3/go.mod h1:+M5sO17Va9V33t28Qs9VqRQ8bFV501Uhq2PtQY+R3Ms= @@ -61,8 +61,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= @@ -95,8 +95,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/infra/base/docker.go b/infra/base/docker.go index 926d271..f04b7ec 100644 --- a/infra/base/docker.go +++ b/infra/base/docker.go @@ -7,7 +7,6 @@ import ( "github.com/outofforest/isolator" "github.com/outofforest/isolator/wire" - "github.com/outofforest/parallel" "github.com/pkg/errors" "github.com/outofforest/osman/infra/types" @@ -22,61 +21,50 @@ type dockerInitializer struct { } // Init fetches image from docker registry and integrates it inside directory -func (f *dockerInitializer) Init(ctx context.Context, cacheDir, dir string, buildKey types.BuildKey) error { - return parallel.Run(ctx, func(ctx context.Context, spawn parallel.SpawnFn) error { - incoming := make(chan interface{}) - outgoing := make(chan interface{}) +func (f *dockerInitializer) Init(ctx context.Context, cacheDir, dir string, buildKey types.BuildKey) (retErr error) { + cacheDir = filepath.Join(cacheDir, "docker-images") + if err := os.MkdirAll(cacheDir, 0o700); err != nil { + return errors.WithStack(err) + } - cacheDir := filepath.Join(cacheDir, "docker-images") - if err := os.MkdirAll(cacheDir, 0o700); err != nil { - return errors.WithStack(err) - } - - spawn("isolator", parallel.Fail, func(ctx context.Context) error { - return isolator.Run(ctx, isolator.Config{ - Dir: dir, - Types: []interface{}{ - wire.Result{}, + return isolator.Run(ctx, isolator.Config{ + Dir: dir, + Types: []interface{}{ + wire.Result{}, + }, + Executor: wire.Config{ + UseHostNetwork: true, + Mounts: []wire.Mount{ + { + Host: cacheDir, + Namespace: "/.docker-cache", + Writable: true, }, - Executor: wire.Config{ - NoStandardMounts: true, - Mounts: []wire.Mount{ - { - Host: cacheDir, - Container: "/.docker-cache", - Writable: true, - }, - }, - }, - Incoming: incoming, - Outgoing: outgoing, - }) - }) - spawn("init", parallel.Exit, func(ctx context.Context) error { - select { - case <-ctx.Done(): - return errors.WithStack(ctx.Err()) - case outgoing <- wire.InflateDockerImage{ - CacheDir: "/.docker-cache", - Image: buildKey.Name, - Tag: string(buildKey.Tag), - }: - } + }, + }, + }, func(ctx context.Context, incoming <-chan interface{}, outgoing chan<- interface{}) error { + select { + case <-ctx.Done(): + return errors.WithStack(ctx.Err()) + case outgoing <- wire.InflateDockerImage{ + CacheDir: "/.docker-cache", + Image: buildKey.Name, + Tag: string(buildKey.Tag), + }: + } - select { - case <-ctx.Done(): - return errors.WithStack(ctx.Err()) - case content := <-incoming: - result, ok := content.(wire.Result) - if !ok { - return errors.Errorf("expected Result, got: %T", content) - } - if result.Error != "" { - return errors.New(result.Error) + for content := range incoming { + switch m := content.(type) { + case wire.Result: + if m.Error != "" { + return errors.New(m.Error) } return nil + default: + return errors.New("unexpected message received") } - }) - return nil + } + + return errors.WithStack(ctx.Err()) }) } diff --git a/infra/build.go b/infra/build.go index 188b98e..ee23046 100644 --- a/infra/build.go +++ b/infra/build.go @@ -7,7 +7,6 @@ import ( "github.com/outofforest/isolator" "github.com/outofforest/isolator/wire" - "github.com/outofforest/parallel" "github.com/pkg/errors" "github.com/outofforest/osman/config" @@ -18,8 +17,6 @@ import ( "github.com/outofforest/osman/infra/types" ) -type cloneFromFn func(srcBuildKey types.BuildKey) (types.BuildInfo, error) - // NewBuilder creates new image builder func NewBuilder(config config.Build, initializer base.Initializer, repo *Repository, storage storage.Driver, parser parser.Parser) *Builder { return &Builder{ @@ -119,7 +116,7 @@ func (b *Builder) build(ctx context.Context, cacheDir string, stack map[types.Bu } }() - if len(img.Commands()) == 0 { + if commands := img.Commands(); len(commands) == 0 { if len(tags) != 1 { return "", errors.New("for base image exactly one tag is required") } @@ -134,131 +131,62 @@ func (b *Builder) build(ctx context.Context, cacheDir string, stack map[types.Bu return "", err } } else { - incoming := make(chan interface{}) - outgoing := make(chan interface{}) - clonedCh := make(chan struct{}) - build := newImageBuild(incoming, outgoing, - func(srcBuildKey types.BuildKey) (types.BuildInfo, error) { - if !types.IsNameValid(srcBuildKey.Name) { - return types.BuildInfo{}, errors.Errorf("name %s is invalid", srcBuildKey.Name) - } - if !srcBuildKey.Tag.IsValid() { - return types.BuildInfo{}, errors.Errorf("tag %s is invalid", srcBuildKey.Tag) - } - - // Try to clone existing image - err := types.ErrImageDoesNotExist - var srcBuildID types.BuildID - if !b.rebuild || b.readyBuilds[srcBuildKey] { - srcBuildID, err = b.storage.BuildID(ctx, srcBuildKey) - } - - switch { - case err == nil: - case errors.Is(err, types.ErrImageDoesNotExist): - // If image does not exist try to build it from file in the current directory but only if tag is a default one - if srcBuildKey.Tag == description.DefaultTag { - _, err = b.buildFromFile(ctx, cacheDir, stack, srcBuildKey.Name, srcBuildKey.Name, description.DefaultTag) - } - default: - return types.BuildInfo{}, err - } - - switch { - case err == nil: - case errors.Is(err, types.ErrImageDoesNotExist): - if baseImage := b.repo.Retrieve(srcBuildKey); baseImage != nil { - // If spec file does not exist, try building from repository - _, err = b.build(ctx, cacheDir, stack, baseImage) - } else { - _, err = b.build(ctx, cacheDir, stack, description.Describe(srcBuildKey.Name, types.Tags{srcBuildKey.Tag})) - } - default: - return types.BuildInfo{}, err - } - - if err != nil { - return types.BuildInfo{}, err - } - - if !srcBuildID.IsValid() { - srcBuildID, err = b.storage.BuildID(ctx, srcBuildKey) - if err != nil { - return types.BuildInfo{}, err - } - } - if !srcBuildID.Type().Properties().Cloneable { - return types.BuildInfo{}, errors.Errorf("build %s is not cloneable", srcBuildKey) - } - - imgFinalize, path, err = b.storage.Clone(ctx, srcBuildID, img.Name(), buildID) - if err != nil { - return types.BuildInfo{}, err - } - - buildInfo, err := b.storage.Info(ctx, srcBuildID) - if err != nil { - return types.BuildInfo{}, err - } + fromCommand, ok := commands[0].(*description.FromCommand) + if !ok { + return "", errors.New("first command must be FROM") + } - if err != nil { - return types.BuildInfo{}, err - } + var err error + var buildInfo types.BuildInfo + imgFinalize, path, buildInfo, err = b.clone( + ctx, + fromCommand.BuildKey, + cacheDir, + stack, + img, + buildID, + ) + if err != nil { + return "", err + } - close(clonedCh) - return buildInfo, nil - }) - err := parallel.Run(ctx, func(ctx context.Context, spawn parallel.SpawnFn) error { - spawn("isolator", parallel.Fail, func(ctx context.Context) error { + err = isolator.Run(ctx, isolator.Config{ + Dir: path, + Types: []interface{}{ + wire.Result{}, + wire.Log{}, + }, + Executor: wire.Config{ + ConfigureSystem: true, + UseHostNetwork: true, + Mounts: []wire.Mount{ + { + Host: ".", + Namespace: "/.specdir", + Writable: true, + }, + }, + }, + }, func(ctx context.Context, incoming <-chan interface{}, outgoing chan<- interface{}) error { + build := newImageBuild(buildInfo, incoming, outgoing) + for _, cmd := range commands[1:] { select { case <-ctx.Done(): return errors.WithStack(ctx.Err()) - case <-clonedCh: + default: } - return isolator.Run(ctx, isolator.Config{ - Dir: path, - Types: []interface{}{ - wire.Result{}, - wire.Log{}, - }, - Executor: wire.Config{ - Mounts: []wire.Mount{ - { - Host: ".", - Container: "/.specdir", - Writable: true, - }, - }, - }, - Incoming: incoming, - Outgoing: outgoing, - }) - }) - spawn("commands", parallel.Exit, func(ctx context.Context) error { - for _, cmd := range img.Commands() { - select { - case <-ctx.Done(): - return errors.WithStack(ctx.Err()) - default: - } - - if err := cmd.Execute(ctx, build); err != nil { - return err - } + if err := cmd.Execute(ctx, build); err != nil { + return err } - return nil - }) - return nil + } + + build.manifest.BuildID = buildID + return b.storage.StoreManifest(ctx, build.manifest) }) if err != nil { return "", err } - - build.manifest.BuildID = buildID - if err := b.storage.StoreManifest(ctx, build.manifest); err != nil { - return "", err - } } for _, key := range keys { @@ -272,81 +200,122 @@ func (b *Builder) build(ctx context.Context, cacheDir string, stack map[types.Bu return buildID, nil } +func (b *Builder) clone( + ctx context.Context, + srcBuildKey types.BuildKey, + cacheDir string, + stack map[types.BuildKey]bool, + img *description.Descriptor, + buildID types.BuildID, +) (storage.FinalizeFn, string, types.BuildInfo, error) { + if !types.IsNameValid(srcBuildKey.Name) { + return nil, "", types.BuildInfo{}, errors.Errorf("name %s is invalid", srcBuildKey.Name) + } + if !srcBuildKey.Tag.IsValid() { + return nil, "", types.BuildInfo{}, errors.Errorf("tag %s is invalid", srcBuildKey.Tag) + } + + // Try to clone existing image + err := types.ErrImageDoesNotExist + var srcBuildID types.BuildID + if !b.rebuild || b.readyBuilds[srcBuildKey] { + srcBuildID, err = b.storage.BuildID(ctx, srcBuildKey) + } + + switch { + case err == nil: + case errors.Is(err, types.ErrImageDoesNotExist): + // If image does not exist try to build it from file in the current directory but only if tag is a default one + if srcBuildKey.Tag == description.DefaultTag { + _, err = b.buildFromFile(ctx, cacheDir, stack, srcBuildKey.Name, srcBuildKey.Name, description.DefaultTag) + } + default: + return nil, "", types.BuildInfo{}, err + } + + switch { + case err == nil: + case errors.Is(err, types.ErrImageDoesNotExist): + if baseImage := b.repo.Retrieve(srcBuildKey); baseImage != nil { + // If spec file does not exist, try building from repository + _, err = b.build(ctx, cacheDir, stack, baseImage) + } else { + _, err = b.build(ctx, cacheDir, stack, description.Describe(srcBuildKey.Name, types.Tags{srcBuildKey.Tag})) + } + default: + return nil, "", types.BuildInfo{}, err + } + + if err != nil { + return nil, "", types.BuildInfo{}, err + } + + if !srcBuildID.IsValid() { + srcBuildID, err = b.storage.BuildID(ctx, srcBuildKey) + if err != nil { + return nil, "", types.BuildInfo{}, err + } + } + if !srcBuildID.Type().Properties().Cloneable { + return nil, "", types.BuildInfo{}, errors.Errorf("build %s is not cloneable", srcBuildKey) + } + + imgFinalize, path, err := b.storage.Clone(ctx, srcBuildID, img.Name(), buildID) + if err != nil { + return nil, "", types.BuildInfo{}, err + } + + buildInfo, err := b.storage.Info(ctx, srcBuildID) + if err != nil { + return imgFinalize, "", types.BuildInfo{}, err + } + + if err != nil { + return imgFinalize, "", types.BuildInfo{}, err + } + + return imgFinalize, path, buildInfo, nil +} + var _ description.ImageBuild = &imageBuild{} -func newImageBuild(incoming <-chan interface{}, outgoing chan<- interface{}, cloneFn cloneFromFn) *imageBuild { +func newImageBuild(buildInfo types.BuildInfo, incoming <-chan interface{}, outgoing chan<- interface{}) *imageBuild { return &imageBuild{ incoming: incoming, outgoing: outgoing, - cloneFn: cloneFn, + manifest: types.ImageManifest{ + BasedOn: buildInfo.BuildID, + Params: buildInfo.Params, + }, } } type imageBuild struct { incoming <-chan interface{} outgoing chan<- interface{} - cloneFn cloneFromFn - - fromDone bool manifest types.ImageManifest } -// From is a handler for FROM -func (b *imageBuild) From(cmd *description.FromCommand) error { - if b.fromDone { - return errors.New("directive FROM may be specified only once") - } - buildInfo, err := b.cloneFn(cmd.BuildKey) - if err != nil { - return err - } - b.manifest.BasedOn = buildInfo.BuildID - b.manifest.Params = buildInfo.Params - b.fromDone = true - return nil -} - // Params sets kernel params for image -func (b *imageBuild) Params(cmd *description.ParamsCommand) error { - if !b.fromDone { - return errors.New("description has to start with FROM directive") - } +func (b *imageBuild) Params(cmd *description.ParamsCommand) { b.manifest.Params = append(b.manifest.Params, cmd.Params...) - return nil } // Run is a handler for RUN func (b *imageBuild) Run(ctx context.Context, cmd *description.RunCommand) error { - if !b.fromDone { - return errors.New("description has to start with FROM directive") - } - select { case <-ctx.Done(): return errors.WithStack(ctx.Err()) case b.outgoing <- wire.Execute{Command: cmd.Command}: } - for { - var content interface{} - var ok bool - - select { - case <-ctx.Done(): - return errors.WithStack(ctx.Err()) - case content, ok = <-b.incoming: - } - if !ok { - return errors.WithStack(ctx.Err()) - } - + for content := range b.incoming { switch m := content.(type) { case wire.Log: - stream, err := toStream(m.Stream) - if err != nil { + if _, err := os.Stderr.Write(m.Content); err != nil { return err } - if _, err := stream.WriteString(m.Text); err != nil { + if _, err := os.Stderr.Write([]byte{'\n'}); err != nil { return err } case wire.Result: @@ -358,26 +327,11 @@ func (b *imageBuild) Run(ctx context.Context, cmd *description.RunCommand) error return errors.New("unexpected message received") } } + + return errors.WithStack(ctx.Err()) } // Boot sets boot option for an image -func (b *imageBuild) Boot(cmd *description.BootCommand) error { - if !b.fromDone { - return errors.New("description has to start with FROM directive") - } +func (b *imageBuild) Boot(cmd *description.BootCommand) { b.manifest.Boots = append(b.manifest.Boots, types.Boot{Title: cmd.Title, Params: cmd.Params}) - return nil -} - -func toStream(stream wire.Stream) (*os.File, error) { - var f *os.File - switch stream { - case wire.StreamOut: - f = os.Stdout - case wire.StreamErr: - f = os.Stderr - default: - return nil, errors.Errorf("unknown stream: %d", stream) - } - return f, nil } diff --git a/infra/description/commands.go b/infra/description/commands.go index a901426..7d49535 100644 --- a/infra/description/commands.go +++ b/infra/description/commands.go @@ -3,6 +3,8 @@ package description import ( "context" + "github.com/pkg/errors" + "github.com/outofforest/osman/infra/types" ) @@ -52,7 +54,7 @@ type FromCommand struct { // Execute executes build command func (cmd *FromCommand) Execute(ctx context.Context, build ImageBuild) error { - return build.From(cmd) + return errors.New("this should not be called") } // ParamsCommand executes PARAMS command @@ -62,7 +64,8 @@ type ParamsCommand struct { // Execute executes build command func (cmd *ParamsCommand) Execute(ctx context.Context, build ImageBuild) error { - return build.Params(cmd) + build.Params(cmd) + return nil } // RunCommand executes RUN command @@ -83,5 +86,6 @@ type BootCommand struct { // Execute executes build command func (cmd *BootCommand) Execute(ctx context.Context, build ImageBuild) error { - return build.Boot(cmd) + build.Boot(cmd) + return nil } diff --git a/infra/description/types.go b/infra/description/types.go index f5346eb..807f4bc 100644 --- a/infra/description/types.go +++ b/infra/description/types.go @@ -17,15 +17,12 @@ type Command interface { // ImageBuild represents build in progress type ImageBuild interface { - // From executes FROM command - From(cmd *FromCommand) error - // Params executes PARAMS command - Params(cmd *ParamsCommand) error + Params(cmd *ParamsCommand) // Run executes RUN command Run(ctx context.Context, cmd *RunCommand) error // Boot executes BOOT command - Boot(cmd *BootCommand) error + Boot(cmd *BootCommand) }