From 8d25c2ed81d9af6d136c8c1d4fe6bda1df3d47b3 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 8 Sep 2021 11:10:43 +0200 Subject: [PATCH 01/94] implements first version of new transport command --- cmd/component-cli/app/app.go | 2 + pkg/commands/oci/oci.go | 2 +- pkg/commands/transport/transport.go | 166 ++++++++++++++++++++ pkg/transport/download/local_oci_blob.go | 84 ++++++++++ pkg/transport/filter/component_filter.go | 39 +++++ pkg/transport/filter/filter.go | 9 ++ pkg/transport/pipeline/pipeline.go | 123 +++++++++++++++ pkg/transport/processor/processor.go | 10 ++ pkg/transport/processor/stdio_executable.go | 60 +++++++ pkg/transport/processor/uds_executable.go | 92 +++++++++++ pkg/transport/upload/local_oci_blob.go | 59 +++++++ pkg/transport/util/archive.go | 163 +++++++++++++++++++ 12 files changed, 808 insertions(+), 1 deletion(-) create mode 100644 pkg/commands/transport/transport.go create mode 100644 pkg/transport/download/local_oci_blob.go create mode 100644 pkg/transport/filter/component_filter.go create mode 100644 pkg/transport/filter/filter.go create mode 100644 pkg/transport/pipeline/pipeline.go create mode 100644 pkg/transport/processor/processor.go create mode 100644 pkg/transport/processor/stdio_executable.go create mode 100644 pkg/transport/processor/uds_executable.go create mode 100644 pkg/transport/upload/local_oci_blob.go create mode 100644 pkg/transport/util/archive.go diff --git a/cmd/component-cli/app/app.go b/cmd/component-cli/app/app.go index d0ea635c..9a6ec9cb 100644 --- a/cmd/component-cli/app/app.go +++ b/cmd/component-cli/app/app.go @@ -16,6 +16,7 @@ import ( "github.com/gardener/component-cli/pkg/commands/ctf" "github.com/gardener/component-cli/pkg/commands/imagevector" "github.com/gardener/component-cli/pkg/commands/oci" + "github.com/gardener/component-cli/pkg/commands/transport" "github.com/gardener/component-cli/pkg/logcontext" "github.com/gardener/component-cli/pkg/logger" "github.com/gardener/component-cli/pkg/version" @@ -48,6 +49,7 @@ func NewComponentsCliCommand(ctx context.Context) *cobra.Command { cmd.AddCommand(imagevector.NewImageVectorCommand(ctx)) cmd.AddCommand(oci.NewOCICommand(ctx)) cmd.AddCommand(cachecmd.NewCacheCommand(ctx)) + cmd.AddCommand(transport.NewTransportCommand(ctx)) return cmd } diff --git a/pkg/commands/oci/oci.go b/pkg/commands/oci/oci.go index 2894dbb1..f61c0811 100644 --- a/pkg/commands/oci/oci.go +++ b/pkg/commands/oci/oci.go @@ -10,7 +10,7 @@ import ( "github.com/spf13/cobra" ) -// NewOCICommand creates a new ctf command. +// NewOCICommand creates a new oci command. func NewOCICommand(ctx context.Context) *cobra.Command { cmd := &cobra.Command{ Use: "oci", diff --git a/pkg/commands/transport/transport.go b/pkg/commands/transport/transport.go new file mode 100644 index 00000000..64f8d2f0 --- /dev/null +++ b/pkg/commands/transport/transport.go @@ -0,0 +1,166 @@ +package transport + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "sync" + "time" + + ociopts "github.com/gardener/component-cli/ociclient/options" + "github.com/gardener/component-cli/pkg/commands/constants" + "github.com/gardener/component-cli/pkg/logger" + "github.com/gardener/component-cli/pkg/transport/pipeline" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + cdoci "github.com/gardener/component-spec/bindings-go/oci" + "github.com/go-logr/logr" + "github.com/mandelsoft/vfs/pkg/osfs" + "github.com/mandelsoft/vfs/pkg/vfs" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "sigs.k8s.io/yaml" +) + +type Options struct { + // BaseUrl is the oci registry where the component is stored. + BaseUrl string + // ComponentName is the unique name of the component in the registry. + ComponentName string + // Version is the component Version in the oci registry. + Version string + + ComponentNameMapping string + + // OCIOptions contains all oci client related options. + OCIOptions ociopts.Options +} + +// NewTransportCommand creates a new transport command. +func NewTransportCommand(ctx context.Context) *cobra.Command { + opts := &Options{} + cmd := &cobra.Command{ + Use: "transport", + Run: func(cmd *cobra.Command, args []string) { + if err := opts.Complete(args); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + if err := opts.Run(ctx, logger.Log, osfs.New()); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + }, + } + opts.AddFlags(cmd.Flags()) + return cmd +} + +func (o *Options) AddFlags(fs *pflag.FlagSet) { + fs.StringVar(&o.ComponentNameMapping, "component-name-mapping", string(cdv2.OCIRegistryURLPathMapping), "[OPTIONAL] repository context name mapping") + o.OCIOptions.AddFlags(fs) +} + +func (o *Options) Complete(args []string) error { + // todo: validate args + o.BaseUrl = args[0] + o.ComponentName = args[1] + o.Version = args[2] + + cliHomeDir, err := constants.CliHomeDir() + if err != nil { + return err + } + o.OCIOptions.CacheDir = filepath.Join(cliHomeDir, "components") + if err := os.MkdirAll(o.OCIOptions.CacheDir, os.ModePerm); err != nil { + return fmt.Errorf("unable to create cache directory %s: %w", o.OCIOptions.CacheDir, err) + } + + if len(o.BaseUrl) == 0 { + return errors.New("the base url must be defined") + } + if len(o.ComponentName) == 0 { + return errors.New("a component name must be defined") + } + if len(o.Version) == 0 { + return errors.New("a component's Version must be defined") + } + + return nil +} + +func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) error { + repoCtx := cdv2.OCIRegistryRepository{ + ObjectType: cdv2.ObjectType{ + Type: cdv2.OCIRegistryType, + }, + BaseURL: o.BaseUrl, + ComponentNameMapping: cdv2.ComponentNameMapping(o.ComponentNameMapping), + } + ociRef, err := cdoci.OCIRef(repoCtx, o.ComponentName, o.Version) + if err != nil { + return fmt.Errorf("invalid component reference: %w", err) + } + + ociClient, _, err := o.OCIOptions.Build(log, fs) + if err != nil { + return fmt.Errorf("unable to build oci client: %s", err.Error()) + } + + cdresolver := cdoci.NewResolver(ociClient) + cd, err := cdresolver.Resolve(ctx, &repoCtx, o.ComponentName, o.Version) + if err != nil { + return fmt.Errorf("unable to to fetch component descriptor %s: %w", ociRef, err) + } + + const parallelRuns = 1 + cds := []*cdv2.ComponentDescriptor{} + for i := 0; i < parallelRuns; i++ { + cds = append(cds, cd) + } + + wg := sync.WaitGroup{} + + for _, cd := range cds { + for _, resource := range cd.Resources { + resource := resource + + wg.Add(1) + go func() { + defer wg.Done() + + pip, err := pipeline.NewSequentialPipeline() + if err != nil { + log.Error(err, "unable to build pipeline") + } + + processedCD, processedRes, err := pip.Process(ctx, cd, resource) + if err != nil { + log.Error(err, "unable to process resource") + } + + mcd, err := yaml.Marshal(processedCD) + if err != nil { + log.Error(err, "unable to marshal cd") + } + + mres, err := yaml.Marshal(processedRes) + if err != nil { + log.Error(err, "unable to marshal res") + } + + fmt.Println(string(mcd)) + fmt.Println(string(mres)) + }() + } + } + + fmt.Println("waiting for goroutines to finish") + wg.Wait() + fmt.Println("avg_duration =", pipeline.TotalTime/time.Millisecond/parallelRuns, "ms") + fmt.Println("main finished") + + return nil +} diff --git a/pkg/transport/download/local_oci_blob.go b/pkg/transport/download/local_oci_blob.go new file mode 100644 index 00000000..0114a431 --- /dev/null +++ b/pkg/transport/download/local_oci_blob.go @@ -0,0 +1,84 @@ +package download + +import ( + "archive/tar" + "context" + "fmt" + "io" + "io/ioutil" + + "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/pkg/transport/processor" + "github.com/gardener/component-cli/pkg/transport/util" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + cdoci "github.com/gardener/component-spec/bindings-go/oci" + "github.com/go-logr/logr" +) + +type localOCIBlobDownloader struct{} + +func NewLocalOCIBlobDownloader() processor.ResourceStreamProcessor { + return &localOCIBlobDownloader{} +} + +func (d *localOCIBlobDownloader) Process(ctx context.Context, r io.Reader, w io.Writer) error { + cd, res, _, err := util.ReadArchive(tar.NewReader(r)) + if err != nil { + return fmt.Errorf("unable to read input archive: %w", err) + } + + if res.Access.GetType() != cdv2.LocalOCIBlobType { + return fmt.Errorf("unsupported access type: %+v", res.Access) + } + + tmpfile, err := ioutil.TempFile("", "") + if err != nil { + return fmt.Errorf("unable to create tempfile: %w", err) + } + defer tmpfile.Close() + + err = fetchLocalOCIBlob(ctx, cd, res, tmpfile) + if err != nil { + return fmt.Errorf("unable to fetch blob: %w", err) + } + + _, err = tmpfile.Seek(0, 0) + if err != nil { + return fmt.Errorf("unable to seek to beginning of tempfile: %w", err) + } + + err = util.WriteArchive(ctx, cd, res, tmpfile, tar.NewWriter(w)) + if err != nil { + return fmt.Errorf("unable to write output archive: %w", err) + } + + return nil +} + +func fetchLocalOCIBlob(ctx context.Context, cd *cdv2.ComponentDescriptor, res cdv2.Resource, w io.Writer) error { + ociClient, err := ociclient.NewClient( + logr.Discard(), + ) + if err != nil { + return fmt.Errorf("unable to create oci client: %w", err) + } + + repoctx := cdv2.OCIRegistryRepository{} + err = cd.GetEffectiveRepositoryContext().DecodeInto(&repoctx) + if err != nil { + return fmt.Errorf("unable to decode repository context: %w", err) + } + + resolver := cdoci.NewResolver(ociClient) + _, blobResolver, err := resolver.ResolveWithBlobResolver(ctx, &repoctx, cd.Name, cd.Version) + if err != nil { + return fmt.Errorf("unable to resolve component descriptor: %w", err) + } + + _, err = blobResolver.Resolve(ctx, res, w) + if err != nil { + return fmt.Errorf("unable to to resolve blob: %w", err) + } + + return nil +} diff --git a/pkg/transport/filter/component_filter.go b/pkg/transport/filter/component_filter.go new file mode 100644 index 00000000..e83d7072 --- /dev/null +++ b/pkg/transport/filter/component_filter.go @@ -0,0 +1,39 @@ +package filter + +import ( + "fmt" + "regexp" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +type componentFilter struct { + includeComponentNames []*regexp.Regexp +} + +func (f componentFilter) Matches(cd *cdv2.ComponentDescriptor, r cdv2.Resource) bool { + var matches bool + for _, icn := range f.includeComponentNames { + if matches = icn.MatchString(cd.Name); matches { + break + } + } + return matches +} + +func NewComponentFilter(includeComponentNames ...string) (Filter, error) { + icnRegexps := []*regexp.Regexp{} + for _, icn := range includeComponentNames { + icnRegexp, err := regexp.Compile(icn) + if err != nil { + return nil, fmt.Errorf("unable to parse regexp %s: %w", icn, err) + } + icnRegexps = append(icnRegexps, icnRegexp) + } + + filter := componentFilter{ + includeComponentNames: icnRegexps, + } + + return &filter, nil +} diff --git a/pkg/transport/filter/filter.go b/pkg/transport/filter/filter.go new file mode 100644 index 00000000..41f14c25 --- /dev/null +++ b/pkg/transport/filter/filter.go @@ -0,0 +1,9 @@ +package filter + +import ( + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +type Filter interface { + Matches(*cdv2.ComponentDescriptor, cdv2.Resource) bool +} diff --git a/pkg/transport/pipeline/pipeline.go b/pkg/transport/pipeline/pipeline.go new file mode 100644 index 00000000..838f0cd8 --- /dev/null +++ b/pkg/transport/pipeline/pipeline.go @@ -0,0 +1,123 @@ +package pipeline + +import ( + "context" + "os" + "sync" + "time" + + "archive/tar" + "fmt" + "io/ioutil" + + "github.com/gardener/component-cli/pkg/transport/download" + "github.com/gardener/component-cli/pkg/transport/processor" + "github.com/gardener/component-cli/pkg/transport/util" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +var TotalTime time.Duration = 0 +var mux = sync.Mutex{} + +type ResourceProcessingPipeline interface { + Process(context.Context, *cdv2.ComponentDescriptor, cdv2.Resource) (*cdv2.ComponentDescriptor, cdv2.Resource, error) +} + +type sequentialPipeline struct { + processors []processor.ResourceStreamProcessor +} + +func (p *sequentialPipeline) Process(ctx context.Context, cd *cdv2.ComponentDescriptor, res cdv2.Resource) (*cdv2.ComponentDescriptor, cdv2.Resource, error) { + infile, err := ioutil.TempFile("", "out") + if err != nil { + return nil, cdv2.Resource{}, fmt.Errorf("unable to create temporary infile: %w", err) + } + + err = util.WriteArchive(ctx, cd, res, nil, tar.NewWriter(infile)) + if err != nil { + return nil, cdv2.Resource{}, fmt.Errorf("unable to write: %w", err) + } + + start := time.Now() + + for _, proc := range p.processors { + outfile, err := p.process(ctx, infile, proc) + if err != nil { + return nil, cdv2.Resource{}, err + } + + infile = outfile + } + + end := time.Now() + delta := end.Sub(start) + mux.Lock() + TotalTime += delta + mux.Unlock() + + defer infile.Close() + + _, err = infile.Seek(0, 0) + if err != nil { + return nil, cdv2.Resource{}, err + } + + cd, res, _, err = util.ReadArchive(tar.NewReader(infile)) + if err != nil { + return nil, cdv2.Resource{}, fmt.Errorf("unable to read output data: %w", err) + } + + return cd, res, nil +} + +func (p *sequentialPipeline) process(ctx context.Context, infile *os.File, proc processor.ResourceStreamProcessor) (*os.File, error) { + defer infile.Close() + + _, err := infile.Seek(0, 0) + if err != nil { + return nil, fmt.Errorf("unable to seek to beginning of input file: %w", err) + } + + outfile, err := ioutil.TempFile("", "out") + if err != nil { + return nil, fmt.Errorf("unable to create temporary outfile: %w", err) + } + + inreader := infile + outwriter := outfile + + err = proc.Process(ctx, inreader, outwriter) + if err != nil { + return nil, fmt.Errorf("unable to process resource: %w", err) + } + + return outfile, nil +} + +func NewSequentialPipeline() (ResourceProcessingPipeline, error) { + procBins := []string{ + "/Users/i500806/dev/pipeman/bin/processor_1", + "/Users/i500806/dev/pipeman/bin/processor_2", + "/Users/i500806/dev/pipeman/bin/processor_3", + } + + procs := []processor.ResourceStreamProcessor{ + download.NewLocalOCIBlobDownloader(), + } + + for _, procBin := range procBins { + exec, err := processor.NewStdIOExecutable(procBin) + if err != nil { + return nil, err + } + procs = append(procs, exec) + } + + // procs = append(procs, upload.NewLocalOCIBlobUploader()) + + pip := sequentialPipeline{ + processors: procs, + } + + return &pip, nil +} diff --git a/pkg/transport/processor/processor.go b/pkg/transport/processor/processor.go new file mode 100644 index 00000000..746ab5b5 --- /dev/null +++ b/pkg/transport/processor/processor.go @@ -0,0 +1,10 @@ +package processor + +import ( + "context" + "io" +) + +type ResourceStreamProcessor interface { + Process(context.Context, io.Reader, io.Writer) error +} diff --git a/pkg/transport/processor/stdio_executable.go b/pkg/transport/processor/stdio_executable.go new file mode 100644 index 00000000..ef873eff --- /dev/null +++ b/pkg/transport/processor/stdio_executable.go @@ -0,0 +1,60 @@ +package processor + +import ( + "context" + "fmt" + "io" + "os" + "os/exec" +) + +type stdIOExecutable struct { + processor *exec.Cmd + stdin io.WriteCloser + stdout io.Reader +} + +func NewStdIOExecutable(bin string) (ResourceStreamProcessor, error) { + cmd := exec.Command(bin) + stdin, err := cmd.StdinPipe() + if err != nil { + return nil, err + } + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + cmd.Stderr = os.Stderr + + err = cmd.Start() + if err != nil { + return nil, fmt.Errorf("unable to start processor: %w", err) + } + + e := stdIOExecutable{ + processor: cmd, + stdin: stdin, + stdout: stdout, + } + + return &e, nil +} + +func (e *stdIOExecutable) Process(ctx context.Context, r io.Reader, w io.Writer) error { + _, err := io.Copy(e.stdin, r) + if err != nil { + return fmt.Errorf("unable to write input: %w", err) + } + + err = e.stdin.Close() + if err != nil { + return err + } + + _, err = io.Copy(w, e.stdout) + if err != nil { + return fmt.Errorf("unable to read output: %w", err) + } + + return nil +} diff --git a/pkg/transport/processor/uds_executable.go b/pkg/transport/processor/uds_executable.go new file mode 100644 index 00000000..665d9029 --- /dev/null +++ b/pkg/transport/processor/uds_executable.go @@ -0,0 +1,92 @@ +package processor + +import ( + "context" + "encoding/base64" + "fmt" + "io" + "net" + "os" + "os/exec" + "time" + + "github.com/gardener/component-cli/pkg/utils" +) + +type udsExecutable struct { + processor *exec.Cmd + addr string + conn net.Conn +} + +func NewUDSExecutable(bin string) (ResourceStreamProcessor, error) { + wd, err := os.Getwd() + if err != nil { + return nil, err + } + addr := fmt.Sprintf("%s/%s-%s.sock", wd, base64.StdEncoding.EncodeToString([]byte(bin)), utils.RandomString(5)) + + cmd := exec.Command(bin) + cmd.Args = append(cmd.Args, "--addr", addr) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + // exec.CommandContext() + + err = cmd.Start() + if err != nil { + return nil, fmt.Errorf("unable to start processor: %w", err) + } + + conn, err := tryConnect(addr) + if err != nil { + return nil, fmt.Errorf("unable to connect to processor: %w", err) + } + + e := udsExecutable{ + processor: cmd, + addr: addr, + conn: conn, + } + + return &e, nil +} + +func (e *udsExecutable) Process(ctx context.Context, r io.Reader, w io.Writer) error { + _, err := io.Copy(e.conn, r) + if err != nil { + return fmt.Errorf("unable to write input: %w", err) + } + + usock := e.conn.(*net.UnixConn) + err = usock.CloseWrite() + if err != nil { + return err + } + + _, err = io.Copy(w, e.conn) + if err != nil { + return fmt.Errorf("unable to read output: %w", err) + } + + return nil +} + +func tryConnect(addr string) (net.Conn, error) { + const ( + maxRetries = 5 + sleeptime = 500 * time.Millisecond + ) + + var conn net.Conn + var err error + for i := 0; i <= maxRetries; i++ { + conn, err = net.Dial("unix", addr) + if err == nil { + break + } + + time.Sleep(sleeptime) + } + + return conn, err +} diff --git a/pkg/transport/upload/local_oci_blob.go b/pkg/transport/upload/local_oci_blob.go new file mode 100644 index 00000000..50c24dea --- /dev/null +++ b/pkg/transport/upload/local_oci_blob.go @@ -0,0 +1,59 @@ +package upload + +import ( + "archive/tar" + "context" + "fmt" + "io" + + "github.com/gardener/component-cli/pkg/transport/processor" + "github.com/gardener/component-cli/pkg/transport/util" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +type localOCIBlobUploader struct { + targetCtx cdv2.OCIRegistryRepository +} + +func NewLocalOCIBlobUploader(targetCtx cdv2.OCIRegistryRepository) processor.ResourceStreamProcessor { + obj := localOCIBlobUploader{ + targetCtx: targetCtx, + } + return &obj +} + +func (d *localOCIBlobUploader) Process(ctx context.Context, r io.Reader, w io.Writer) error { + cd, res, blobreader, err := util.ReadArchive(tar.NewReader(r)) + if err != nil { + return fmt.Errorf("unable to read input archive: %w", err) + } + defer blobreader.Close() + + if res.Access.GetType() != cdv2.LocalOCIBlobType { + return fmt.Errorf("unsupported access type: %+v", res.Access) + } + + err = uploadLocalOCIBlob(ctx, cd, res, blobreader) + if err != nil { + return fmt.Errorf("unable to upload blob: %w", err) + } + + // TODO: blobreader stream will be empty here. fix somehow (TeeReader/TmpFile/...) + err = util.WriteArchive(ctx, cd, res, blobreader, tar.NewWriter(w)) + if err != nil { + return fmt.Errorf("unable to write output archive: %w", err) + } + + return nil +} + +func uploadLocalOCIBlob(ctx context.Context, cd *cdv2.ComponentDescriptor, res cdv2.Resource, r io.Reader) error { + // ociClient, err := ociclient.NewClient( + // logr.Discard(), + // ) + // if err != nil { + // return fmt.Errorf("unable to create oci client: %w", err) + // } + + return nil +} diff --git a/pkg/transport/util/archive.go b/pkg/transport/util/archive.go new file mode 100644 index 00000000..40c48978 --- /dev/null +++ b/pkg/transport/util/archive.go @@ -0,0 +1,163 @@ +package util + +import ( + "archive/tar" + "bytes" + "context" + "fmt" + "io" + "io/ioutil" + "time" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "sigs.k8s.io/yaml" +) + +const ( + ResourceFile = "resource.yaml" + ComponentDescriptorFile = "component-descriptor.yaml" + BlobFile = "blob" +) + +func WriteFile(fname string, content io.Reader, outArchive *tar.Writer) error { + tmpfile, err := ioutil.TempFile("", "tmp") + if err != nil { + return fmt.Errorf("unable to create tempfile: %w", err) + } + defer tmpfile.Close() + + _, err = io.Copy(tmpfile, content) + if err != nil { + return fmt.Errorf("unable to write content to tempfile: %w", err) + } + + _, err = tmpfile.Seek(0, 0) + if err != nil { + return fmt.Errorf("unable to seek to beginning of tempfile: %w", err) + } + + fstat, err := tmpfile.Stat() + if err != nil { + return fmt.Errorf("unable to get file stats: %w", err) + } + + header := tar.Header{ + Name: fname, + Size: fstat.Size(), + Mode: int64(fstat.Mode()), + ModTime: time.Now(), + } + + if err = outArchive.WriteHeader(&header); err != nil { + return fmt.Errorf("unable to write tar header: %w", err) + } + + _, err = io.Copy(outArchive, tmpfile) + if err != nil { + return fmt.Errorf("unable to write file to archive: %w", err) + } + + return nil +} + +func WriteArchive(ctx context.Context, cd *cdv2.ComponentDescriptor, res cdv2.Resource, resourceBlobReader io.Reader, outwriter *tar.Writer) error { + defer outwriter.Close() + + println("start writing data") + + marshaledCD, err := yaml.Marshal(cd) + if err != nil { + return fmt.Errorf("unable to marshal component descriptor: %w", err) + } + + println("writing component descriptor") + err = WriteFile(ComponentDescriptorFile, bytes.NewReader(marshaledCD), outwriter) + if err != nil { + return fmt.Errorf("unable to write component descriptor: %w", err) + } + + marshaledRes, err := yaml.Marshal(res) + if err != nil { + return fmt.Errorf("unable to marshal resource: %w", err) + } + + println("writing resource") + err = WriteFile(ResourceFile, bytes.NewReader(marshaledRes), outwriter) + if err != nil { + return fmt.Errorf("unable to write resource: %w", err) + } + + if resourceBlobReader != nil { + println("writing blob") + err = WriteFile(BlobFile, resourceBlobReader, outwriter) + if err != nil { + return fmt.Errorf("unable to write blob: %w", err) + } + } + + println("finished writing data") + + return nil +} + +func ReadArchive(r *tar.Reader) (*cdv2.ComponentDescriptor, cdv2.Resource, io.ReadCloser, error) { + var cd *cdv2.ComponentDescriptor + var res cdv2.Resource + + for { + header, err := r.Next() + if err != nil { + if err == io.EOF { + break + } + return nil, cdv2.Resource{}, nil, fmt.Errorf("%w", err) + } + + switch header.Name { + case ResourceFile: + res, err = ParseResource(r) + if err != nil { + return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read %s: %w", ResourceFile, err) + } + case ComponentDescriptorFile: + cd, err = ParseComponentDescriptor(r) + if err != nil { + return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read %s: %w", ComponentDescriptorFile, err) + } + } + } + + return cd, res, nil, nil +} + +func ParseResource(r *tar.Reader) (cdv2.Resource, error) { + buf := bytes.NewBuffer([]byte{}) + _, err := io.Copy(buf, r) + if err != nil { + return cdv2.Resource{}, fmt.Errorf("unable to read from stream: %w", err) + } + + var res cdv2.Resource + err = yaml.Unmarshal(buf.Bytes(), &res) + if err != nil { + return cdv2.Resource{}, fmt.Errorf("unable to unmarshal: %w", err) + } + + return res, nil +} + +func ParseComponentDescriptor(r *tar.Reader) (*cdv2.ComponentDescriptor, error) { + buf := bytes.NewBuffer([]byte{}) + _, err := io.Copy(buf, r) + if err != nil { + return nil, fmt.Errorf("unable to read from stream: %w", err) + } + + var cd cdv2.ComponentDescriptor + err = yaml.Unmarshal(buf.Bytes(), &cd) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal: %w", err) + } + + return &cd, nil +} From 3f75dc14cc9e629b72f24b9f7ca294dc1f7effbb Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 8 Sep 2021 11:55:02 +0200 Subject: [PATCH 02/94] reduces unix domain socket filepath length --- pkg/transport/pipeline/pipeline.go | 2 +- pkg/transport/processor/uds_executable.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/transport/pipeline/pipeline.go b/pkg/transport/pipeline/pipeline.go index 838f0cd8..89e9afd4 100644 --- a/pkg/transport/pipeline/pipeline.go +++ b/pkg/transport/pipeline/pipeline.go @@ -106,7 +106,7 @@ func NewSequentialPipeline() (ResourceProcessingPipeline, error) { } for _, procBin := range procBins { - exec, err := processor.NewStdIOExecutable(procBin) + exec, err := processor.NewUDSExecutable(procBin) if err != nil { return nil, err } diff --git a/pkg/transport/processor/uds_executable.go b/pkg/transport/processor/uds_executable.go index 665d9029..f2c6f603 100644 --- a/pkg/transport/processor/uds_executable.go +++ b/pkg/transport/processor/uds_executable.go @@ -2,7 +2,6 @@ package processor import ( "context" - "encoding/base64" "fmt" "io" "net" @@ -24,7 +23,7 @@ func NewUDSExecutable(bin string) (ResourceStreamProcessor, error) { if err != nil { return nil, err } - addr := fmt.Sprintf("%s/%s-%s.sock", wd, base64.StdEncoding.EncodeToString([]byte(bin)), utils.RandomString(5)) + addr := fmt.Sprintf("%s/%s.sock", wd, utils.RandomString(8)) cmd := exec.Command(bin) cmd.Args = append(cmd.Args, "--addr", addr) From b829dc02f9c511cbd5b327863471e8cbc9fc62f3 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Thu, 9 Sep 2021 15:24:46 +0200 Subject: [PATCH 03/94] working on down/upload of localOciBlob resources --- ociclient/client.go | 27 +++++++++ ociclient/types.go | 3 + pkg/commands/transport/transport.go | 16 ++++- pkg/transport/download/local_oci_blob.go | 27 ++++----- pkg/transport/pipeline/pipeline.go | 11 ++-- pkg/transport/upload/local_oci_blob.go | 74 ++++++++++++++++++++---- pkg/transport/upload/util.go | 17 ++++++ pkg/transport/util/archive.go | 26 +++++++-- pkg/utils/utils.go | 5 +- 9 files changed, 167 insertions(+), 39 deletions(-) create mode 100644 pkg/transport/upload/util.go diff --git a/ociclient/client.go b/ociclient/client.go index 66acc042..0732f15f 100644 --- a/ociclient/client.go +++ b/ociclient/client.go @@ -264,6 +264,33 @@ func (c *client) PushOCIArtifact(ctx context.Context, ref string, artifact *oci. } } +func (c *client) PushBlob(ctx context.Context, ref string, desc ocispecv1.Descriptor, options ...PushOption) error { + refspec, err := oci.ParseRef(ref) + if err != nil { + return fmt.Errorf("unable to parse ref: %w", err) + } + ref = refspec.String() + + opts := &PushOptions{} + opts.Store = c.cache + opts.ApplyOptions(options) + + resolver, err := c.getResolverForRef(ctx, ref, transport.PushScope) + if err != nil { + return err + } + pusher, err := resolver.Pusher(ctx, ref) + if err != nil { + return err + } + + if err := c.pushContent(ctx, opts.Store, pusher, desc); err != nil { + return err + } + + return nil +} + func (c *client) pushManifest(ctx context.Context, manifest *ocispecv1.Manifest, pusher remotes.Pusher, cache cache.Cache, opts *PushOptions) (ocispecv1.Descriptor, error) { // add dummy config if it is not set if manifest.Config.Size == 0 { diff --git a/ociclient/types.go b/ociclient/types.go index e7c0a3ea..8134ccd3 100644 --- a/ociclient/types.go +++ b/ociclient/types.go @@ -33,6 +33,9 @@ type Client interface { // PushOCIArtifact uploads the given OCIArtifact to the given ref. PushOCIArtifact(ctx context.Context, ref string, artifact *oci.Artifact, opts ...PushOption) error + + // PushBlob uploads the blob for the given ocispec Descriptor to the given ref + PushBlob(ctx context.Context, ref string, desc ocispecv1.Descriptor, opts ...PushOption) error } // ExtendedClient defines an oci client with extended functionality that may not work with all registries. diff --git a/pkg/commands/transport/transport.go b/pkg/commands/transport/transport.go index 64f8d2f0..ac1d6927 100644 --- a/pkg/commands/transport/transport.go +++ b/pkg/commands/transport/transport.go @@ -23,6 +23,11 @@ import ( "sigs.k8s.io/yaml" ) +const ( + parallelRuns = 1 + targetCtx = "o.ingress.js-ek.hubforplay.shoot.canary.k8s-hana.ondemand.com/js-transport-test" +) + type Options struct { // BaseUrl is the oci registry where the component is stored. BaseUrl string @@ -115,7 +120,6 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e return fmt.Errorf("unable to to fetch component descriptor %s: %w", ociRef, err) } - const parallelRuns = 1 cds := []*cdv2.ComponentDescriptor{} for i := 0; i < parallelRuns; i++ { cds = append(cds, cd) @@ -123,6 +127,14 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e wg := sync.WaitGroup{} + targetCtx := cdv2.OCIRegistryRepository{ + ObjectType: cdv2.ObjectType{ + Type: cdv2.OCIRegistryType, + }, + BaseURL: targetCtx, + ComponentNameMapping: cdv2.ComponentNameMapping(o.ComponentNameMapping), + } + for _, cd := range cds { for _, resource := range cd.Resources { resource := resource @@ -131,7 +143,7 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e go func() { defer wg.Done() - pip, err := pipeline.NewSequentialPipeline() + pip, err := pipeline.NewSequentialPipeline(ociClient, targetCtx) if err != nil { log.Error(err, "unable to build pipeline") } diff --git a/pkg/transport/download/local_oci_blob.go b/pkg/transport/download/local_oci_blob.go index 0114a431..9b72f19d 100644 --- a/pkg/transport/download/local_oci_blob.go +++ b/pkg/transport/download/local_oci_blob.go @@ -12,13 +12,17 @@ import ( "github.com/gardener/component-cli/pkg/transport/util" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" cdoci "github.com/gardener/component-spec/bindings-go/oci" - "github.com/go-logr/logr" ) -type localOCIBlobDownloader struct{} +type localOCIBlobDownloader struct { + client ociclient.Client +} -func NewLocalOCIBlobDownloader() processor.ResourceStreamProcessor { - return &localOCIBlobDownloader{} +func NewLocalOCIBlobDownloader(client ociclient.Client) processor.ResourceStreamProcessor { + obj := localOCIBlobDownloader{ + client: client, + } + return &obj } func (d *localOCIBlobDownloader) Process(ctx context.Context, r io.Reader, w io.Writer) error { @@ -37,7 +41,7 @@ func (d *localOCIBlobDownloader) Process(ctx context.Context, r io.Reader, w io. } defer tmpfile.Close() - err = fetchLocalOCIBlob(ctx, cd, res, tmpfile) + err = d.fetchLocalOCIBlob(ctx, cd, res, tmpfile) if err != nil { return fmt.Errorf("unable to fetch blob: %w", err) } @@ -55,21 +59,14 @@ func (d *localOCIBlobDownloader) Process(ctx context.Context, r io.Reader, w io. return nil } -func fetchLocalOCIBlob(ctx context.Context, cd *cdv2.ComponentDescriptor, res cdv2.Resource, w io.Writer) error { - ociClient, err := ociclient.NewClient( - logr.Discard(), - ) - if err != nil { - return fmt.Errorf("unable to create oci client: %w", err) - } - +func (d *localOCIBlobDownloader) fetchLocalOCIBlob(ctx context.Context, cd *cdv2.ComponentDescriptor, res cdv2.Resource, w io.Writer) error { repoctx := cdv2.OCIRegistryRepository{} - err = cd.GetEffectiveRepositoryContext().DecodeInto(&repoctx) + err := cd.GetEffectiveRepositoryContext().DecodeInto(&repoctx) if err != nil { return fmt.Errorf("unable to decode repository context: %w", err) } - resolver := cdoci.NewResolver(ociClient) + resolver := cdoci.NewResolver(d.client) _, blobResolver, err := resolver.ResolveWithBlobResolver(ctx, &repoctx, cd.Name, cd.Version) if err != nil { return fmt.Errorf("unable to resolve component descriptor: %w", err) diff --git a/pkg/transport/pipeline/pipeline.go b/pkg/transport/pipeline/pipeline.go index 89e9afd4..45bcbdb1 100644 --- a/pkg/transport/pipeline/pipeline.go +++ b/pkg/transport/pipeline/pipeline.go @@ -10,8 +10,10 @@ import ( "fmt" "io/ioutil" + "github.com/gardener/component-cli/ociclient" "github.com/gardener/component-cli/pkg/transport/download" "github.com/gardener/component-cli/pkg/transport/processor" + "github.com/gardener/component-cli/pkg/transport/upload" "github.com/gardener/component-cli/pkg/transport/util" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" ) @@ -62,10 +64,11 @@ func (p *sequentialPipeline) Process(ctx context.Context, cd *cdv2.ComponentDesc return nil, cdv2.Resource{}, err } - cd, res, _, err = util.ReadArchive(tar.NewReader(infile)) + cd, res, blobreader, err := util.ReadArchive(tar.NewReader(infile)) if err != nil { return nil, cdv2.Resource{}, fmt.Errorf("unable to read output data: %w", err) } + defer blobreader.Close() return cd, res, nil } @@ -94,7 +97,7 @@ func (p *sequentialPipeline) process(ctx context.Context, infile *os.File, proc return outfile, nil } -func NewSequentialPipeline() (ResourceProcessingPipeline, error) { +func NewSequentialPipeline(client ociclient.Client, targetCtx cdv2.OCIRegistryRepository) (ResourceProcessingPipeline, error) { procBins := []string{ "/Users/i500806/dev/pipeman/bin/processor_1", "/Users/i500806/dev/pipeman/bin/processor_2", @@ -102,7 +105,7 @@ func NewSequentialPipeline() (ResourceProcessingPipeline, error) { } procs := []processor.ResourceStreamProcessor{ - download.NewLocalOCIBlobDownloader(), + download.NewLocalOCIBlobDownloader(client), } for _, procBin := range procBins { @@ -113,7 +116,7 @@ func NewSequentialPipeline() (ResourceProcessingPipeline, error) { procs = append(procs, exec) } - // procs = append(procs, upload.NewLocalOCIBlobUploader()) + procs = append(procs, upload.NewLocalOCIBlobUploader(client, targetCtx)) pip := sequentialPipeline{ processors: procs, diff --git a/pkg/transport/upload/local_oci_blob.go b/pkg/transport/upload/local_oci_blob.go index 50c24dea..0e350701 100644 --- a/pkg/transport/upload/local_oci_blob.go +++ b/pkg/transport/upload/local_oci_blob.go @@ -5,19 +5,25 @@ import ( "context" "fmt" "io" + "io/ioutil" + "github.com/gardener/component-cli/ociclient" "github.com/gardener/component-cli/pkg/transport/processor" "github.com/gardener/component-cli/pkg/transport/util" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "github.com/opencontainers/go-digest" + ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" ) type localOCIBlobUploader struct { + client ociclient.Client targetCtx cdv2.OCIRegistryRepository } -func NewLocalOCIBlobUploader(targetCtx cdv2.OCIRegistryRepository) processor.ResourceStreamProcessor { +func NewLocalOCIBlobUploader(client ociclient.Client, targetCtx cdv2.OCIRegistryRepository) processor.ResourceStreamProcessor { obj := localOCIBlobUploader{ targetCtx: targetCtx, + client: client, } return &obj } @@ -33,13 +39,54 @@ func (d *localOCIBlobUploader) Process(ctx context.Context, r io.Reader, w io.Wr return fmt.Errorf("unsupported access type: %+v", res.Access) } - err = uploadLocalOCIBlob(ctx, cd, res, blobreader) + tmpfile, err := ioutil.TempFile("", "") + if err != nil { + return err + } + defer tmpfile.Close() + + _, err = io.Copy(tmpfile, blobreader) + if err != nil { + return err + } + + _, err = tmpfile.Seek(0, 0) + if err != nil { + return err + } + + fstat, err := tmpfile.Stat() + if err != nil { + return err + } + + dgst, err := digest.FromReader(tmpfile) + if err != nil{ + return err + } + + _, err = tmpfile.Seek(0, 0) + if err != nil { + return err + } + + desc := ocispecv1.Descriptor{ + Digest: dgst, + Size: fstat.Size(), + MediaType: res.Type, + } + + err = d.uploadLocalOCIBlob(ctx, cd, res, tmpfile, desc) if err != nil { return fmt.Errorf("unable to upload blob: %w", err) } - // TODO: blobreader stream will be empty here. fix somehow (TeeReader/TmpFile/...) - err = util.WriteArchive(ctx, cd, res, blobreader, tar.NewWriter(w)) + _, err = tmpfile.Seek(0, 0) + if err != nil { + return err + } + + err = util.WriteArchive(ctx, cd, res, tmpfile, tar.NewWriter(w)) if err != nil { return fmt.Errorf("unable to write output archive: %w", err) } @@ -47,13 +94,18 @@ func (d *localOCIBlobUploader) Process(ctx context.Context, r io.Reader, w io.Wr return nil } -func uploadLocalOCIBlob(ctx context.Context, cd *cdv2.ComponentDescriptor, res cdv2.Resource, r io.Reader) error { - // ociClient, err := ociclient.NewClient( - // logr.Discard(), - // ) - // if err != nil { - // return fmt.Errorf("unable to create oci client: %w", err) - // } +func (d *localOCIBlobUploader) uploadLocalOCIBlob(ctx context.Context, cd *cdv2.ComponentDescriptor, res cdv2.Resource, r io.Reader, desc ocispecv1.Descriptor) error { + targetRef := createUploadRef(d.targetCtx, cd.Name, cd.Version) + + store := ociclient.GenericStore(func(ctx context.Context, desc ocispecv1.Descriptor, writer io.Writer) error { + _, err := io.Copy(writer, r) + return err + }) + + err := d.client.PushBlob(ctx, targetRef, desc, ociclient.WithStore(store)) + if err != nil { + return fmt.Errorf("unable to push blob: %w", err) + } return nil } diff --git a/pkg/transport/upload/util.go b/pkg/transport/upload/util.go new file mode 100644 index 00000000..73a68a4b --- /dev/null +++ b/pkg/transport/upload/util.go @@ -0,0 +1,17 @@ +package upload + +import ( + "fmt" + "strings" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +func createUploadRef(repoCtx cdv2.OCIRegistryRepository, componentName string, componentVersion string) string { + uploadTag := componentVersion + if strings.Contains(componentVersion, ":") { + uploadTag = "latest" + } + + return fmt.Sprintf("%s/component-descriptors/%s:%s", repoCtx.BaseURL, componentName, uploadTag) +} \ No newline at end of file diff --git a/pkg/transport/util/archive.go b/pkg/transport/util/archive.go index 40c48978..fa77d55a 100644 --- a/pkg/transport/util/archive.go +++ b/pkg/transport/util/archive.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "io/ioutil" + "os" "time" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" @@ -60,7 +61,7 @@ func WriteFile(fname string, content io.Reader, outArchive *tar.Writer) error { return nil } -func WriteArchive(ctx context.Context, cd *cdv2.ComponentDescriptor, res cdv2.Resource, resourceBlobReader io.Reader, outwriter *tar.Writer) error { +func WriteArchive(ctx context.Context, cd *cdv2.ComponentDescriptor, res cdv2.Resource, blobReader io.Reader, outwriter *tar.Writer) error { defer outwriter.Close() println("start writing data") @@ -87,9 +88,9 @@ func WriteArchive(ctx context.Context, cd *cdv2.ComponentDescriptor, res cdv2.Re return fmt.Errorf("unable to write resource: %w", err) } - if resourceBlobReader != nil { + if blobReader != nil { println("writing blob") - err = WriteFile(BlobFile, resourceBlobReader, outwriter) + err = WriteFile(BlobFile, blobReader, outwriter) if err != nil { return fmt.Errorf("unable to write blob: %w", err) } @@ -103,6 +104,7 @@ func WriteArchive(ctx context.Context, cd *cdv2.ComponentDescriptor, res cdv2.Re func ReadArchive(r *tar.Reader) (*cdv2.ComponentDescriptor, cdv2.Resource, io.ReadCloser, error) { var cd *cdv2.ComponentDescriptor var res cdv2.Resource + var blobFile *os.File for { header, err := r.Next() @@ -124,10 +126,26 @@ func ReadArchive(r *tar.Reader) (*cdv2.ComponentDescriptor, cdv2.Resource, io.Re if err != nil { return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read %s: %w", ComponentDescriptorFile, err) } + case BlobFile: + blobFile, err = ioutil.TempFile("", "") + if err != nil { + return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to create tempfile: %w", err) + } + _, err = io.Copy(blobFile, r) + if err != nil { + return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read %s: %w", BlobFile, err) + } + } + } + + if blobFile != nil { + _, err := blobFile.Seek(0, 0) + if err != nil { + return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to seek to beginning of blobfile: %w", err) } } - return cd, res, nil, nil + return cd, res, blobFile, nil } func ParseResource(r *tar.Reader) (cdv2.Resource, error) { diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index e32b2fee..5f8b305d 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -15,12 +15,11 @@ import ( "path/filepath" "strings" + "github.com/gardener/component-cli/ociclient/cache" + "github.com/gardener/component-cli/pkg/commands/constants" "github.com/mandelsoft/vfs/pkg/vfs" "github.com/spf13/cobra" "sigs.k8s.io/yaml" - - "github.com/gardener/component-cli/ociclient/cache" - "github.com/gardener/component-cli/pkg/commands/constants" ) // PrintPrettyYaml prints the given objects as yaml if enabled. From f9269d0d14d3f420d27c3ee3213ef3b757a2c488 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Fri, 10 Sep 2021 16:06:40 +0200 Subject: [PATCH 04/94] refactoring --- pkg/commands/transport/transport.go | 98 ++++++++++++++----- pkg/transport/download/local_oci_blob.go | 4 +- .../{pipeline.go => sequential_pipeline.go} | 39 +------- .../stdio_executable.go | 2 +- pkg/transport/pipeline/types.go | 16 +++ .../{processor => pipeline}/uds_executable.go | 2 +- pkg/transport/processor/processor.go | 10 -- pkg/transport/upload/local_oci_blob.go | 4 +- pkg/transport/util/archive.go | 2 +- 9 files changed, 100 insertions(+), 77 deletions(-) rename pkg/transport/pipeline/{pipeline.go => sequential_pipeline.go} (62%) rename pkg/transport/{processor => pipeline}/stdio_executable.go (98%) create mode 100644 pkg/transport/pipeline/types.go rename pkg/transport/{processor => pipeline}/uds_executable.go (98%) delete mode 100644 pkg/transport/processor/processor.go diff --git a/pkg/commands/transport/transport.go b/pkg/commands/transport/transport.go index ac1d6927..932df09d 100644 --- a/pkg/commands/transport/transport.go +++ b/pkg/commands/transport/transport.go @@ -9,10 +9,13 @@ import ( "sync" "time" + "github.com/gardener/component-cli/ociclient" ociopts "github.com/gardener/component-cli/ociclient/options" "github.com/gardener/component-cli/pkg/commands/constants" "github.com/gardener/component-cli/pkg/logger" + "github.com/gardener/component-cli/pkg/transport/download" "github.com/gardener/component-cli/pkg/transport/pipeline" + "github.com/gardener/component-cli/pkg/transport/upload" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" cdoci "github.com/gardener/component-spec/bindings-go/oci" "github.com/go-logr/logr" @@ -25,7 +28,7 @@ import ( const ( parallelRuns = 1 - targetCtx = "o.ingress.js-ek.hubforplay.shoot.canary.k8s-hana.ondemand.com/js-transport-test" + targetCtxUrl = "o.ingress.js-ek.hubforplay.shoot.canary.k8s-hana.ondemand.com/js-transport-test" ) type Options struct { @@ -97,44 +100,25 @@ func (o *Options) Complete(args []string) error { } func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) error { - repoCtx := cdv2.OCIRegistryRepository{ - ObjectType: cdv2.ObjectType{ - Type: cdv2.OCIRegistryType, - }, - BaseURL: o.BaseUrl, - ComponentNameMapping: cdv2.ComponentNameMapping(o.ComponentNameMapping), - } - ociRef, err := cdoci.OCIRef(repoCtx, o.ComponentName, o.Version) - if err != nil { - return fmt.Errorf("invalid component reference: %w", err) - } - ociClient, _, err := o.OCIOptions.Build(log, fs) if err != nil { return fmt.Errorf("unable to build oci client: %s", err.Error()) } - cdresolver := cdoci.NewResolver(ociClient) - cd, err := cdresolver.Resolve(ctx, &repoCtx, o.ComponentName, o.Version) + cds, err := ResolveRecursive(ctx, ociClient, o.BaseUrl, o.ComponentName, o.Version, o.ComponentNameMapping) if err != nil { - return fmt.Errorf("unable to to fetch component descriptor %s: %w", ociRef, err) - } - - cds := []*cdv2.ComponentDescriptor{} - for i := 0; i < parallelRuns; i++ { - cds = append(cds, cd) + return fmt.Errorf("unable to resolve component: %w", err) } - wg := sync.WaitGroup{} - targetCtx := cdv2.OCIRegistryRepository{ ObjectType: cdv2.ObjectType{ Type: cdv2.OCIRegistryType, }, - BaseURL: targetCtx, + BaseURL: targetCtxUrl, ComponentNameMapping: cdv2.ComponentNameMapping(o.ComponentNameMapping), } + wg := sync.WaitGroup{} for _, cd := range cds { for _, resource := range cd.Resources { resource := resource @@ -143,9 +127,14 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e go func() { defer wg.Done() - pip, err := pipeline.NewSequentialPipeline(ociClient, targetCtx) + procs, err := createProcessors(ociClient,targetCtx) + if err != nil { + log.Error(err, "unable to create processors") + } + + pip, err := pipeline.NewSequentialPipeline(procs...) if err != nil { - log.Error(err, "unable to build pipeline") + log.Error(err, "unable to create pipeline") } processedCD, processedRes, err := pip.Process(ctx, cd, resource) @@ -176,3 +165,60 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e return nil } + +func ResolveRecursive(ctx context.Context, client ociclient.Client, baseUrl, componentName, componentVersion, componentNameMapping string) ([]*cdv2.ComponentDescriptor, error) { + repoCtx := cdv2.OCIRegistryRepository{ + ObjectType: cdv2.ObjectType{ + Type: cdv2.OCIRegistryType, + }, + BaseURL: baseUrl, + ComponentNameMapping: cdv2.ComponentNameMapping(componentNameMapping), + } + ociRef, err := cdoci.OCIRef(repoCtx, componentName, componentVersion) + if err != nil { + return nil, fmt.Errorf("invalid component reference: %w", err) + } + + cdresolver := cdoci.NewResolver(client) + cd, err := cdresolver.Resolve(ctx, &repoCtx, componentName, componentVersion) + if err != nil { + return nil, fmt.Errorf("unable to to fetch component descriptor %s: %w", ociRef, err) + } + + cds := []*cdv2.ComponentDescriptor{ + cd, + } + for _, ref := range cd.ComponentReferences { + cds2, err := ResolveRecursive(ctx, client, baseUrl, ref.ComponentName, ref.Version, componentNameMapping) + if err != nil { + return nil, fmt.Errorf("unable to resolve ref %+v: %w", ref, err) + } + cds = append(cds, cds2...) + } + + return cds, nil +} + +func createProcessors(client ociclient.Client, targetCtx cdv2.OCIRegistryRepository) ([]pipeline.ResourceStreamProcessor, error) { + procBins := []string{ + "/Users/i500806/dev/pipeman/bin/processor_1", + "/Users/i500806/dev/pipeman/bin/processor_2", + "/Users/i500806/dev/pipeman/bin/processor_3", + } + + procs := []pipeline.ResourceStreamProcessor{ + download.NewLocalOCIBlobDownloader(client), + } + + for _, procBin := range procBins { + exec, err := pipeline.NewUDSExecutable(procBin) + if err != nil { + return nil, err + } + procs = append(procs, exec) + } + + procs = append(procs, upload.NewLocalOCIBlobUploader(client, targetCtx)) + + return procs, nil +} diff --git a/pkg/transport/download/local_oci_blob.go b/pkg/transport/download/local_oci_blob.go index 9b72f19d..2bc001e0 100644 --- a/pkg/transport/download/local_oci_blob.go +++ b/pkg/transport/download/local_oci_blob.go @@ -8,7 +8,7 @@ import ( "io/ioutil" "github.com/gardener/component-cli/ociclient" - "github.com/gardener/component-cli/pkg/transport/processor" + "github.com/gardener/component-cli/pkg/transport/pipeline" "github.com/gardener/component-cli/pkg/transport/util" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" cdoci "github.com/gardener/component-spec/bindings-go/oci" @@ -18,7 +18,7 @@ type localOCIBlobDownloader struct { client ociclient.Client } -func NewLocalOCIBlobDownloader(client ociclient.Client) processor.ResourceStreamProcessor { +func NewLocalOCIBlobDownloader(client ociclient.Client) pipeline.ResourceStreamProcessor { obj := localOCIBlobDownloader{ client: client, } diff --git a/pkg/transport/pipeline/pipeline.go b/pkg/transport/pipeline/sequential_pipeline.go similarity index 62% rename from pkg/transport/pipeline/pipeline.go rename to pkg/transport/pipeline/sequential_pipeline.go index 45bcbdb1..25cd53a4 100644 --- a/pkg/transport/pipeline/pipeline.go +++ b/pkg/transport/pipeline/sequential_pipeline.go @@ -10,10 +10,6 @@ import ( "fmt" "io/ioutil" - "github.com/gardener/component-cli/ociclient" - "github.com/gardener/component-cli/pkg/transport/download" - "github.com/gardener/component-cli/pkg/transport/processor" - "github.com/gardener/component-cli/pkg/transport/upload" "github.com/gardener/component-cli/pkg/transport/util" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" ) @@ -21,12 +17,8 @@ import ( var TotalTime time.Duration = 0 var mux = sync.Mutex{} -type ResourceProcessingPipeline interface { - Process(context.Context, *cdv2.ComponentDescriptor, cdv2.Resource) (*cdv2.ComponentDescriptor, cdv2.Resource, error) -} - type sequentialPipeline struct { - processors []processor.ResourceStreamProcessor + processors []ResourceStreamProcessor } func (p *sequentialPipeline) Process(ctx context.Context, cd *cdv2.ComponentDescriptor, res cdv2.Resource) (*cdv2.ComponentDescriptor, cdv2.Resource, error) { @@ -73,7 +65,7 @@ func (p *sequentialPipeline) Process(ctx context.Context, cd *cdv2.ComponentDesc return cd, res, nil } -func (p *sequentialPipeline) process(ctx context.Context, infile *os.File, proc processor.ResourceStreamProcessor) (*os.File, error) { +func (p *sequentialPipeline) process(ctx context.Context, infile *os.File, proc ResourceStreamProcessor) (*os.File, error) { defer infile.Close() _, err := infile.Seek(0, 0) @@ -97,30 +89,9 @@ func (p *sequentialPipeline) process(ctx context.Context, infile *os.File, proc return outfile, nil } -func NewSequentialPipeline(client ociclient.Client, targetCtx cdv2.OCIRegistryRepository) (ResourceProcessingPipeline, error) { - procBins := []string{ - "/Users/i500806/dev/pipeman/bin/processor_1", - "/Users/i500806/dev/pipeman/bin/processor_2", - "/Users/i500806/dev/pipeman/bin/processor_3", - } - - procs := []processor.ResourceStreamProcessor{ - download.NewLocalOCIBlobDownloader(client), - } - - for _, procBin := range procBins { - exec, err := processor.NewUDSExecutable(procBin) - if err != nil { - return nil, err - } - procs = append(procs, exec) - } - - procs = append(procs, upload.NewLocalOCIBlobUploader(client, targetCtx)) - - pip := sequentialPipeline{ +func NewSequentialPipeline(procs ...ResourceStreamProcessor) (ResourceProcessingPipeline, error) { + p := sequentialPipeline{ processors: procs, } - - return &pip, nil + return &p, nil } diff --git a/pkg/transport/processor/stdio_executable.go b/pkg/transport/pipeline/stdio_executable.go similarity index 98% rename from pkg/transport/processor/stdio_executable.go rename to pkg/transport/pipeline/stdio_executable.go index ef873eff..ca1072a8 100644 --- a/pkg/transport/processor/stdio_executable.go +++ b/pkg/transport/pipeline/stdio_executable.go @@ -1,4 +1,4 @@ -package processor +package pipeline import ( "context" diff --git a/pkg/transport/pipeline/types.go b/pkg/transport/pipeline/types.go new file mode 100644 index 00000000..4fc4c4b5 --- /dev/null +++ b/pkg/transport/pipeline/types.go @@ -0,0 +1,16 @@ +package pipeline + +import ( + "context" + "io" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +type ResourceProcessingPipeline interface { + Process(context.Context, *cdv2.ComponentDescriptor, cdv2.Resource) (*cdv2.ComponentDescriptor, cdv2.Resource, error) +} + +type ResourceStreamProcessor interface { + Process(context.Context, io.Reader, io.Writer) error +} diff --git a/pkg/transport/processor/uds_executable.go b/pkg/transport/pipeline/uds_executable.go similarity index 98% rename from pkg/transport/processor/uds_executable.go rename to pkg/transport/pipeline/uds_executable.go index f2c6f603..6b7343a2 100644 --- a/pkg/transport/processor/uds_executable.go +++ b/pkg/transport/pipeline/uds_executable.go @@ -1,4 +1,4 @@ -package processor +package pipeline import ( "context" diff --git a/pkg/transport/processor/processor.go b/pkg/transport/processor/processor.go deleted file mode 100644 index 746ab5b5..00000000 --- a/pkg/transport/processor/processor.go +++ /dev/null @@ -1,10 +0,0 @@ -package processor - -import ( - "context" - "io" -) - -type ResourceStreamProcessor interface { - Process(context.Context, io.Reader, io.Writer) error -} diff --git a/pkg/transport/upload/local_oci_blob.go b/pkg/transport/upload/local_oci_blob.go index 0e350701..116bbcc2 100644 --- a/pkg/transport/upload/local_oci_blob.go +++ b/pkg/transport/upload/local_oci_blob.go @@ -8,7 +8,7 @@ import ( "io/ioutil" "github.com/gardener/component-cli/ociclient" - "github.com/gardener/component-cli/pkg/transport/processor" + "github.com/gardener/component-cli/pkg/transport/pipeline" "github.com/gardener/component-cli/pkg/transport/util" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" "github.com/opencontainers/go-digest" @@ -20,7 +20,7 @@ type localOCIBlobUploader struct { targetCtx cdv2.OCIRegistryRepository } -func NewLocalOCIBlobUploader(client ociclient.Client, targetCtx cdv2.OCIRegistryRepository) processor.ResourceStreamProcessor { +func NewLocalOCIBlobUploader(client ociclient.Client, targetCtx cdv2.OCIRegistryRepository) pipeline.ResourceStreamProcessor { obj := localOCIBlobUploader{ targetCtx: targetCtx, client: client, diff --git a/pkg/transport/util/archive.go b/pkg/transport/util/archive.go index fa77d55a..30fb273c 100644 --- a/pkg/transport/util/archive.go +++ b/pkg/transport/util/archive.go @@ -112,7 +112,7 @@ func ReadArchive(r *tar.Reader) (*cdv2.ComponentDescriptor, cdv2.Resource, io.Re if err == io.EOF { break } - return nil, cdv2.Resource{}, nil, fmt.Errorf("%w", err) + return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read header: %w", err) } switch header.Name { From cd1102b82184bd4aad15619a7fabe4b6bf1489a4 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 14 Sep 2021 14:33:28 +0200 Subject: [PATCH 05/94] refactored --- pkg/commands/transport/transport.go | 17 ++++----- pkg/transport/download/local_oci_blob.go | 4 +-- .../extension}/stdio_executable.go | 17 ++++++--- .../extension}/uds_executable.go | 35 +++++++++++++------ .../pipeline.go} | 18 +++++----- pkg/transport/{pipeline => process}/types.go | 4 +-- pkg/transport/upload/local_oci_blob.go | 4 +-- 7 files changed, 62 insertions(+), 37 deletions(-) rename pkg/transport/{pipeline => process/extension}/stdio_executable.go (62%) rename pkg/transport/{pipeline => process/extension}/uds_executable.go (61%) rename pkg/transport/{pipeline/sequential_pipeline.go => process/pipeline.go} (69%) rename pkg/transport/{pipeline => process}/types.go (63%) diff --git a/pkg/commands/transport/transport.go b/pkg/commands/transport/transport.go index 932df09d..2c668c1e 100644 --- a/pkg/commands/transport/transport.go +++ b/pkg/commands/transport/transport.go @@ -14,7 +14,8 @@ import ( "github.com/gardener/component-cli/pkg/commands/constants" "github.com/gardener/component-cli/pkg/logger" "github.com/gardener/component-cli/pkg/transport/download" - "github.com/gardener/component-cli/pkg/transport/pipeline" + "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/extension" "github.com/gardener/component-cli/pkg/transport/upload" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" cdoci "github.com/gardener/component-spec/bindings-go/oci" @@ -28,7 +29,7 @@ import ( const ( parallelRuns = 1 - targetCtxUrl = "o.ingress.js-ek.hubforplay.shoot.canary.k8s-hana.ondemand.com/js-transport-test" + targetCtxUrl = "eu.gcr.io/gardener-project/test/jschicktanz/target" ) type Options struct { @@ -132,12 +133,12 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e log.Error(err, "unable to create processors") } - pip, err := pipeline.NewSequentialPipeline(procs...) + pip, err := process.NewResourceProcessingPipeline(procs...) if err != nil { log.Error(err, "unable to create pipeline") } - processedCD, processedRes, err := pip.Process(ctx, cd, resource) + processedCD, processedRes, err := pip.Process(ctx, *cd, resource) if err != nil { log.Error(err, "unable to process resource") } @@ -160,7 +161,7 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e fmt.Println("waiting for goroutines to finish") wg.Wait() - fmt.Println("avg_duration =", pipeline.TotalTime/time.Millisecond/parallelRuns, "ms") + fmt.Println("avg_duration =", process.TotalTime/time.Millisecond/parallelRuns, "ms") fmt.Println("main finished") return nil @@ -199,19 +200,19 @@ func ResolveRecursive(ctx context.Context, client ociclient.Client, baseUrl, com return cds, nil } -func createProcessors(client ociclient.Client, targetCtx cdv2.OCIRegistryRepository) ([]pipeline.ResourceStreamProcessor, error) { +func createProcessors(client ociclient.Client, targetCtx cdv2.OCIRegistryRepository) ([]process.ResourceStreamProcessor, error) { procBins := []string{ "/Users/i500806/dev/pipeman/bin/processor_1", "/Users/i500806/dev/pipeman/bin/processor_2", "/Users/i500806/dev/pipeman/bin/processor_3", } - procs := []pipeline.ResourceStreamProcessor{ + procs := []process.ResourceStreamProcessor{ download.NewLocalOCIBlobDownloader(client), } for _, procBin := range procBins { - exec, err := pipeline.NewUDSExecutable(procBin) + exec, err := extension.NewStdIOExecutable(context.TODO(), procBin) if err != nil { return nil, err } diff --git a/pkg/transport/download/local_oci_blob.go b/pkg/transport/download/local_oci_blob.go index 2bc001e0..dd6655b5 100644 --- a/pkg/transport/download/local_oci_blob.go +++ b/pkg/transport/download/local_oci_blob.go @@ -8,7 +8,7 @@ import ( "io/ioutil" "github.com/gardener/component-cli/ociclient" - "github.com/gardener/component-cli/pkg/transport/pipeline" + "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/transport/util" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" cdoci "github.com/gardener/component-spec/bindings-go/oci" @@ -18,7 +18,7 @@ type localOCIBlobDownloader struct { client ociclient.Client } -func NewLocalOCIBlobDownloader(client ociclient.Client) pipeline.ResourceStreamProcessor { +func NewLocalOCIBlobDownloader(client ociclient.Client) process.ResourceStreamProcessor { obj := localOCIBlobDownloader{ client: client, } diff --git a/pkg/transport/pipeline/stdio_executable.go b/pkg/transport/process/extension/stdio_executable.go similarity index 62% rename from pkg/transport/pipeline/stdio_executable.go rename to pkg/transport/process/extension/stdio_executable.go index ca1072a8..9f7e956c 100644 --- a/pkg/transport/pipeline/stdio_executable.go +++ b/pkg/transport/process/extension/stdio_executable.go @@ -1,4 +1,4 @@ -package pipeline +package extension import ( "context" @@ -6,6 +6,8 @@ import ( "io" "os" "os/exec" + + "github.com/gardener/component-cli/pkg/transport/process" ) type stdIOExecutable struct { @@ -14,8 +16,10 @@ type stdIOExecutable struct { stdout io.Reader } -func NewStdIOExecutable(bin string) (ResourceStreamProcessor, error) { - cmd := exec.Command(bin) +// NewStdIOExecutable runs resource processor in the background. +// It communicates with this processor via stdin/stdout pipes. +func NewStdIOExecutable(ctx context.Context, bin string, args ...string) (process.ResourceStreamProcessor, error) { + cmd := exec.CommandContext(ctx, bin, args...) stdin, err := cmd.StdinPipe() if err != nil { return nil, err @@ -48,7 +52,7 @@ func (e *stdIOExecutable) Process(ctx context.Context, r io.Reader, w io.Writer) err = e.stdin.Close() if err != nil { - return err + return fmt.Errorf("unable to close input writer: %w", err) } _, err = io.Copy(w, e.stdout) @@ -56,5 +60,10 @@ func (e *stdIOExecutable) Process(ctx context.Context, r io.Reader, w io.Writer) return fmt.Errorf("unable to read output: %w", err) } + err = e.processor.Wait() + if err != nil { + return fmt.Errorf("unable to stop processor: %w", err) + } + return nil } diff --git a/pkg/transport/pipeline/uds_executable.go b/pkg/transport/process/extension/uds_executable.go similarity index 61% rename from pkg/transport/pipeline/uds_executable.go rename to pkg/transport/process/extension/uds_executable.go index 6b7343a2..fa4c7cf9 100644 --- a/pkg/transport/pipeline/uds_executable.go +++ b/pkg/transport/process/extension/uds_executable.go @@ -1,4 +1,4 @@ -package pipeline +package extension import ( "context" @@ -9,27 +9,37 @@ import ( "os/exec" "time" + "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/utils" ) +const serverAddressFlag = "--addr" + type udsExecutable struct { processor *exec.Cmd - addr string - conn net.Conn + addr string + conn net.Conn } -func NewUDSExecutable(bin string) (ResourceStreamProcessor, error) { +// NewUDSExecutable runs a resource processor in the background. +// It communicates with this processor via Unix Domain Sockets. +func NewUDSExecutable(ctx context.Context, bin string, args ...string) (process.ResourceStreamProcessor, error) { + for _, arg := range args { + if arg == serverAddressFlag { + return nil, fmt.Errorf("the flag %s is not allowed to be set manually", serverAddressFlag) + } + } + wd, err := os.Getwd() if err != nil { return nil, err } addr := fmt.Sprintf("%s/%s.sock", wd, utils.RandomString(8)) + args = append(args, "--addr", addr) - cmd := exec.Command(bin) - cmd.Args = append(cmd.Args, "--addr", addr) + cmd := exec.CommandContext(ctx, bin, args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - // exec.CommandContext() err = cmd.Start() if err != nil { @@ -43,8 +53,8 @@ func NewUDSExecutable(bin string) (ResourceStreamProcessor, error) { e := udsExecutable{ processor: cmd, - addr: addr, - conn: conn, + addr: addr, + conn: conn, } return &e, nil @@ -59,7 +69,7 @@ func (e *udsExecutable) Process(ctx context.Context, r io.Reader, w io.Writer) e usock := e.conn.(*net.UnixConn) err = usock.CloseWrite() if err != nil { - return err + return fmt.Errorf("unable to close input writer: %w", err) } _, err = io.Copy(w, e.conn) @@ -67,6 +77,11 @@ func (e *udsExecutable) Process(ctx context.Context, r io.Reader, w io.Writer) e return fmt.Errorf("unable to read output: %w", err) } + err = e.processor.Wait() + if err != nil { + return fmt.Errorf("unable to stop processor: %w", err) + } + return nil } diff --git a/pkg/transport/pipeline/sequential_pipeline.go b/pkg/transport/process/pipeline.go similarity index 69% rename from pkg/transport/pipeline/sequential_pipeline.go rename to pkg/transport/process/pipeline.go index 25cd53a4..2c8e7278 100644 --- a/pkg/transport/pipeline/sequential_pipeline.go +++ b/pkg/transport/process/pipeline.go @@ -1,4 +1,4 @@ -package pipeline +package process import ( "context" @@ -17,17 +17,17 @@ import ( var TotalTime time.Duration = 0 var mux = sync.Mutex{} -type sequentialPipeline struct { +type resourceProcessingPipelineImpl struct { processors []ResourceStreamProcessor } -func (p *sequentialPipeline) Process(ctx context.Context, cd *cdv2.ComponentDescriptor, res cdv2.Resource) (*cdv2.ComponentDescriptor, cdv2.Resource, error) { +func (p *resourceProcessingPipelineImpl) Process(ctx context.Context, cd cdv2.ComponentDescriptor, res cdv2.Resource) (*cdv2.ComponentDescriptor, cdv2.Resource, error) { infile, err := ioutil.TempFile("", "out") if err != nil { return nil, cdv2.Resource{}, fmt.Errorf("unable to create temporary infile: %w", err) } - err = util.WriteArchive(ctx, cd, res, nil, tar.NewWriter(infile)) + err = util.WriteArchive(ctx, &cd, res, nil, tar.NewWriter(infile)) if err != nil { return nil, cdv2.Resource{}, fmt.Errorf("unable to write: %w", err) } @@ -56,16 +56,16 @@ func (p *sequentialPipeline) Process(ctx context.Context, cd *cdv2.ComponentDesc return nil, cdv2.Resource{}, err } - cd, res, blobreader, err := util.ReadArchive(tar.NewReader(infile)) + processedCD, processedRes, blobreader, err := util.ReadArchive(tar.NewReader(infile)) if err != nil { return nil, cdv2.Resource{}, fmt.Errorf("unable to read output data: %w", err) } defer blobreader.Close() - return cd, res, nil + return processedCD, processedRes, nil } -func (p *sequentialPipeline) process(ctx context.Context, infile *os.File, proc ResourceStreamProcessor) (*os.File, error) { +func (p *resourceProcessingPipelineImpl) process(ctx context.Context, infile *os.File, proc ResourceStreamProcessor) (*os.File, error) { defer infile.Close() _, err := infile.Seek(0, 0) @@ -89,8 +89,8 @@ func (p *sequentialPipeline) process(ctx context.Context, infile *os.File, proc return outfile, nil } -func NewSequentialPipeline(procs ...ResourceStreamProcessor) (ResourceProcessingPipeline, error) { - p := sequentialPipeline{ +func NewResourceProcessingPipeline(procs ...ResourceStreamProcessor) (ResourceProcessingPipeline, error) { + p := resourceProcessingPipelineImpl{ processors: procs, } return &p, nil diff --git a/pkg/transport/pipeline/types.go b/pkg/transport/process/types.go similarity index 63% rename from pkg/transport/pipeline/types.go rename to pkg/transport/process/types.go index 4fc4c4b5..4faa38b9 100644 --- a/pkg/transport/pipeline/types.go +++ b/pkg/transport/process/types.go @@ -1,4 +1,4 @@ -package pipeline +package process import ( "context" @@ -8,7 +8,7 @@ import ( ) type ResourceProcessingPipeline interface { - Process(context.Context, *cdv2.ComponentDescriptor, cdv2.Resource) (*cdv2.ComponentDescriptor, cdv2.Resource, error) + Process(context.Context, cdv2.ComponentDescriptor, cdv2.Resource) (*cdv2.ComponentDescriptor, cdv2.Resource, error) } type ResourceStreamProcessor interface { diff --git a/pkg/transport/upload/local_oci_blob.go b/pkg/transport/upload/local_oci_blob.go index 116bbcc2..f5f6fd42 100644 --- a/pkg/transport/upload/local_oci_blob.go +++ b/pkg/transport/upload/local_oci_blob.go @@ -8,7 +8,7 @@ import ( "io/ioutil" "github.com/gardener/component-cli/ociclient" - "github.com/gardener/component-cli/pkg/transport/pipeline" + "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/transport/util" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" "github.com/opencontainers/go-digest" @@ -20,7 +20,7 @@ type localOCIBlobUploader struct { targetCtx cdv2.OCIRegistryRepository } -func NewLocalOCIBlobUploader(client ociclient.Client, targetCtx cdv2.OCIRegistryRepository) pipeline.ResourceStreamProcessor { +func NewLocalOCIBlobUploader(client ociclient.Client, targetCtx cdv2.OCIRegistryRepository) process.ResourceStreamProcessor { obj := localOCIBlobUploader{ targetCtx: targetCtx, client: client, From 2d6b4f1d2685d3902880cd970e071a627daffcca Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 14 Sep 2021 14:58:04 +0200 Subject: [PATCH 06/94] refactoring --- pkg/commands/transport/transport.go | 107 ++++++++++++------ .../{ => process}/download/local_oci_blob.go | 5 +- pkg/transport/process/download/oci_image.go | 73 ++++++++++++ pkg/transport/process/pipeline.go | 5 +- .../process/tar_archive_file_filter.go | 39 +++++++ .../{ => process}/upload/local_oci_blob.go | 5 +- pkg/transport/{ => process}/upload/util.go | 0 .../{util/archive.go => process/util.go} | 2 +- pkg/utils/utils.go | 39 +++++++ 9 files changed, 229 insertions(+), 46 deletions(-) rename pkg/transport/{ => process}/download/local_oci_blob.go (91%) create mode 100644 pkg/transport/process/download/oci_image.go create mode 100644 pkg/transport/process/process/tar_archive_file_filter.go rename pkg/transport/{ => process}/upload/local_oci_blob.go (92%) rename pkg/transport/{ => process}/upload/util.go (100%) rename pkg/transport/{util/archive.go => process/util.go} (99%) diff --git a/pkg/commands/transport/transport.go b/pkg/commands/transport/transport.go index 2c668c1e..90f16285 100644 --- a/pkg/commands/transport/transport.go +++ b/pkg/commands/transport/transport.go @@ -13,10 +13,10 @@ import ( ociopts "github.com/gardener/component-cli/ociclient/options" "github.com/gardener/component-cli/pkg/commands/constants" "github.com/gardener/component-cli/pkg/logger" - "github.com/gardener/component-cli/pkg/transport/download" "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/download" "github.com/gardener/component-cli/pkg/transport/process/extension" - "github.com/gardener/component-cli/pkg/transport/upload" + "github.com/gardener/component-cli/pkg/transport/process/upload" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" cdoci "github.com/gardener/component-spec/bindings-go/oci" "github.com/go-logr/logr" @@ -121,42 +121,19 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e wg := sync.WaitGroup{} for _, cd := range cds { - for _, resource := range cd.Resources { - resource := resource - - wg.Add(1) - go func() { - defer wg.Done() - - procs, err := createProcessors(ociClient,targetCtx) - if err != nil { - log.Error(err, "unable to create processors") - } - - pip, err := process.NewResourceProcessingPipeline(procs...) - if err != nil { - log.Error(err, "unable to create pipeline") - } - - processedCD, processedRes, err := pip.Process(ctx, *cd, resource) - if err != nil { - log.Error(err, "unable to process resource") - } - - mcd, err := yaml.Marshal(processedCD) - if err != nil { - log.Error(err, "unable to marshal cd") - } - - mres, err := yaml.Marshal(processedRes) - if err != nil { - log.Error(err, "unable to marshal res") + cd := cd + wg.Add(1) + go func() { + defer wg.Done() + processedResources, errs := handleResources(ctx, cd, targetCtx, log, ociClient) + if len(errs) > 0 { + for _, err := range errs { + log.Error(err, "") } + } - fmt.Println(string(mcd)) - fmt.Println(string(mres)) - }() - } + cd.Resources = processedResources + }() } fmt.Println("waiting for goroutines to finish") @@ -167,6 +144,64 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e return nil } +func handleResources(ctx context.Context, cd *cdv2.ComponentDescriptor, targetCtx cdv2.OCIRegistryRepository, log logr.Logger, ociClient ociclient.Client) ([]cdv2.Resource, []error) { + wg := sync.WaitGroup{} + errs := []error{} + mux := sync.Mutex{} + processedResources := []cdv2.Resource{} + + for _, resource := range cd.Resources { + resource := resource + + wg.Add(1) + go func() { + defer wg.Done() + + procs, err := createProcessors(ociClient, targetCtx) + if err != nil { + errs = append(errs, fmt.Errorf("unable to create processors: %w", err)) + return + } + + pip, err := process.NewResourceProcessingPipeline(procs...) + if err != nil { + errs = append(errs, fmt.Errorf("unable to create pipeline: %w", err)) + return + } + + // TODO: do we allow modifications of the component descriptor? + // If so, how do we merge the possibly different output of multiple resource pipelines? + processedCD, processedRes, err := pip.Process(ctx, *cd, resource) + if err != nil { + errs = append(errs, fmt.Errorf("unable to process resource: %w", err)) + return + } + + mux.Lock() + processedResources = append(processedResources, processedRes) + mux.Unlock() + + mcd, err := yaml.Marshal(processedCD) + if err != nil { + errs = append(errs, fmt.Errorf("unable to marshal cd: %w", err)) + return + } + + mres, err := yaml.Marshal(processedRes) + if err != nil { + errs = append(errs, fmt.Errorf("unable to marshal res: %w", err)) + return + } + + fmt.Println(string(mcd)) + fmt.Println(string(mres)) + }() + } + + wg.Wait() + return processedResources, errs +} + func ResolveRecursive(ctx context.Context, client ociclient.Client, baseUrl, componentName, componentVersion, componentNameMapping string) ([]*cdv2.ComponentDescriptor, error) { repoCtx := cdv2.OCIRegistryRepository{ ObjectType: cdv2.ObjectType{ diff --git a/pkg/transport/download/local_oci_blob.go b/pkg/transport/process/download/local_oci_blob.go similarity index 91% rename from pkg/transport/download/local_oci_blob.go rename to pkg/transport/process/download/local_oci_blob.go index dd6655b5..34c14b2f 100644 --- a/pkg/transport/download/local_oci_blob.go +++ b/pkg/transport/process/download/local_oci_blob.go @@ -9,7 +9,6 @@ import ( "github.com/gardener/component-cli/ociclient" "github.com/gardener/component-cli/pkg/transport/process" - "github.com/gardener/component-cli/pkg/transport/util" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" cdoci "github.com/gardener/component-spec/bindings-go/oci" ) @@ -26,7 +25,7 @@ func NewLocalOCIBlobDownloader(client ociclient.Client) process.ResourceStreamPr } func (d *localOCIBlobDownloader) Process(ctx context.Context, r io.Reader, w io.Writer) error { - cd, res, _, err := util.ReadArchive(tar.NewReader(r)) + cd, res, _, err := process.ReadArchive(tar.NewReader(r)) if err != nil { return fmt.Errorf("unable to read input archive: %w", err) } @@ -51,7 +50,7 @@ func (d *localOCIBlobDownloader) Process(ctx context.Context, r io.Reader, w io. return fmt.Errorf("unable to seek to beginning of tempfile: %w", err) } - err = util.WriteArchive(ctx, cd, res, tmpfile, tar.NewWriter(w)) + err = process.WriteArchive(ctx, cd, res, tmpfile, tar.NewWriter(w)) if err != nil { return fmt.Errorf("unable to write output archive: %w", err) } diff --git a/pkg/transport/process/download/oci_image.go b/pkg/transport/process/download/oci_image.go new file mode 100644 index 00000000..6cd1dde3 --- /dev/null +++ b/pkg/transport/process/download/oci_image.go @@ -0,0 +1,73 @@ +package download + +import ( + "archive/tar" + "context" + "fmt" + "io" + + "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/ociclient/oci" + "github.com/gardener/component-cli/pkg/transport/process" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +type ociImageDownloader struct { + client ociclient.Client +} + +func NewOCIImageDownloader(client ociclient.Client) process.ResourceStreamProcessor { + obj := ociImageDownloader{ + client: client, + } + return &obj +} + +func (d *ociImageDownloader) Process(ctx context.Context, r io.Reader, w io.Writer) error { + cd, res, _, err := process.ReadArchive(tar.NewReader(r)) + if err != nil { + return fmt.Errorf("unable to read input archive: %w", err) + } + + if res.Access.GetType() != cdv2.OCIRegistryType { + return fmt.Errorf("unsupported acces type: %+v", res.Access) + } + + if res.Type != cdv2.OCIImageType { + return fmt.Errorf("unsupported resource type: %s", res.Type) + } + + ociAccess := &cdv2.OCIRegistryAccess{} + if err := res.Access.DecodeInto(ociAccess); err != nil { + return fmt.Errorf("unable to decode resource access: %w", err) + } + + ociArtifact, err := d.client.GetOCIArtifact(ctx, ociAccess.ImageReference) + if err != nil { + return fmt.Errorf("unable to get oci artifact: %w", err) + } + + if ociArtifact.IsIndex() { + handleImageIndex() + } else { + handleImage() + } + + return nil +} + +func handleImageIndex(index *oci.Index) { + + for _, m := range index.Manifests { + + } + + artifact. +} + +func handleImage() { + err := process.WriteArchive(ctx, cd, res, tmpfile, tar.NewWriter(w)) + if err != nil { + return fmt.Errorf("unable to write output archive: %w", err) + } +} diff --git a/pkg/transport/process/pipeline.go b/pkg/transport/process/pipeline.go index 2c8e7278..4c0b6c57 100644 --- a/pkg/transport/process/pipeline.go +++ b/pkg/transport/process/pipeline.go @@ -10,7 +10,6 @@ import ( "fmt" "io/ioutil" - "github.com/gardener/component-cli/pkg/transport/util" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" ) @@ -27,7 +26,7 @@ func (p *resourceProcessingPipelineImpl) Process(ctx context.Context, cd cdv2.Co return nil, cdv2.Resource{}, fmt.Errorf("unable to create temporary infile: %w", err) } - err = util.WriteArchive(ctx, &cd, res, nil, tar.NewWriter(infile)) + err = WriteArchive(ctx, &cd, res, nil, tar.NewWriter(infile)) if err != nil { return nil, cdv2.Resource{}, fmt.Errorf("unable to write: %w", err) } @@ -56,7 +55,7 @@ func (p *resourceProcessingPipelineImpl) Process(ctx context.Context, cd cdv2.Co return nil, cdv2.Resource{}, err } - processedCD, processedRes, blobreader, err := util.ReadArchive(tar.NewReader(infile)) + processedCD, processedRes, blobreader, err := ReadArchive(tar.NewReader(infile)) if err != nil { return nil, cdv2.Resource{}, fmt.Errorf("unable to read output data: %w", err) } diff --git a/pkg/transport/process/process/tar_archive_file_filter.go b/pkg/transport/process/process/tar_archive_file_filter.go new file mode 100644 index 00000000..5a5a11d1 --- /dev/null +++ b/pkg/transport/process/process/tar_archive_file_filter.go @@ -0,0 +1,39 @@ +package builtin + +import ( + "archive/tar" + "context" + "fmt" + "io" + + "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/utils" +) + +type tarArchiveFileFilter struct { + removePatterns []string +} + +func (f *tarArchiveFileFilter) Process(ctx context.Context, r io.Reader, w io.Writer) error { + cd, res, blobreader, err := process.ReadArchive(tar.NewReader(r)) + if err != nil { + return fmt.Errorf("unable to read archive: %w", err) + } + + if err = utils.FilterTARArchive(blobreader, tar.NewWriter(w), f.removePatterns); err != nil { + return fmt.Errorf("unable to filter blob: %w", err) + } + + if err = process.WriteArchive(ctx, cd, res, nil, tar.NewWriter(w)); err != nil { + return fmt.Errorf("unable to write archive: %w", err) + } + + return nil +} + +func NewTarArchiveFileFilter(removePatterns []string) process.ResourceStreamProcessor { + obj := tarArchiveFileFilter{ + removePatterns: removePatterns, + } + return &obj +} diff --git a/pkg/transport/upload/local_oci_blob.go b/pkg/transport/process/upload/local_oci_blob.go similarity index 92% rename from pkg/transport/upload/local_oci_blob.go rename to pkg/transport/process/upload/local_oci_blob.go index f5f6fd42..42c27390 100644 --- a/pkg/transport/upload/local_oci_blob.go +++ b/pkg/transport/process/upload/local_oci_blob.go @@ -9,7 +9,6 @@ import ( "github.com/gardener/component-cli/ociclient" "github.com/gardener/component-cli/pkg/transport/process" - "github.com/gardener/component-cli/pkg/transport/util" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" "github.com/opencontainers/go-digest" ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -29,7 +28,7 @@ func NewLocalOCIBlobUploader(client ociclient.Client, targetCtx cdv2.OCIRegistry } func (d *localOCIBlobUploader) Process(ctx context.Context, r io.Reader, w io.Writer) error { - cd, res, blobreader, err := util.ReadArchive(tar.NewReader(r)) + cd, res, blobreader, err := process.ReadArchive(tar.NewReader(r)) if err != nil { return fmt.Errorf("unable to read input archive: %w", err) } @@ -86,7 +85,7 @@ func (d *localOCIBlobUploader) Process(ctx context.Context, r io.Reader, w io.Wr return err } - err = util.WriteArchive(ctx, cd, res, tmpfile, tar.NewWriter(w)) + err = process.WriteArchive(ctx, cd, res, tmpfile, tar.NewWriter(w)) if err != nil { return fmt.Errorf("unable to write output archive: %w", err) } diff --git a/pkg/transport/upload/util.go b/pkg/transport/process/upload/util.go similarity index 100% rename from pkg/transport/upload/util.go rename to pkg/transport/process/upload/util.go diff --git a/pkg/transport/util/archive.go b/pkg/transport/process/util.go similarity index 99% rename from pkg/transport/util/archive.go rename to pkg/transport/process/util.go index 30fb273c..51d57661 100644 --- a/pkg/transport/util/archive.go +++ b/pkg/transport/process/util.go @@ -1,4 +1,4 @@ -package util +package process import ( "archive/tar" diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 5f8b305d..d729d984 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -5,10 +5,12 @@ package utils import ( + "archive/tar" "bytes" "compress/gzip" "encoding/json" "fmt" + "io" "math/rand" "net/http" "os" @@ -167,3 +169,40 @@ func BytesString(bytes uint64, accuracy int) string { return fmt.Sprintf("%s %s", stringValue, unit) } + +func FilterTARArchive(r *tar.Reader, w *tar.Writer, removePatterns []string) error { + defer w.Close() + + NEXT_FILE: + for { + header, err := r.Next() + if err != nil { + if err == io.EOF { + break + } + return fmt.Errorf("unable to read header: %w", err) + } + + for _, removePattern := range removePatterns { + removeFile, err := filepath.Match(removePattern, header.Name) + if err != nil { + return fmt.Errorf("unable to match filename against pattern: %w", err) + } + + if removeFile { + continue NEXT_FILE + } + } + + if err := w.WriteHeader(header); err != nil { + return fmt.Errorf("unable to write header: %w", err) + } + + _, err = io.Copy(w, r) + if err != nil { + return fmt.Errorf("unable to write file: %w", err) + } + } + + return nil +} From 58a6494d5d5e175690bd7eea6690aa34127bcfac Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 14 Sep 2021 15:24:56 +0200 Subject: [PATCH 07/94] init --- .../process/extension/stdio_executable.go | 69 +++++++ .../process/extension/uds_executable.go | 106 ++++++++++ pkg/transport/process/pipeline.go | 82 ++++++++ pkg/transport/process/types.go | 16 ++ pkg/transport/process/util.go | 181 ++++++++++++++++++ 5 files changed, 454 insertions(+) create mode 100644 pkg/transport/process/extension/stdio_executable.go create mode 100644 pkg/transport/process/extension/uds_executable.go create mode 100644 pkg/transport/process/pipeline.go create mode 100644 pkg/transport/process/types.go create mode 100644 pkg/transport/process/util.go diff --git a/pkg/transport/process/extension/stdio_executable.go b/pkg/transport/process/extension/stdio_executable.go new file mode 100644 index 00000000..9f7e956c --- /dev/null +++ b/pkg/transport/process/extension/stdio_executable.go @@ -0,0 +1,69 @@ +package extension + +import ( + "context" + "fmt" + "io" + "os" + "os/exec" + + "github.com/gardener/component-cli/pkg/transport/process" +) + +type stdIOExecutable struct { + processor *exec.Cmd + stdin io.WriteCloser + stdout io.Reader +} + +// NewStdIOExecutable runs resource processor in the background. +// It communicates with this processor via stdin/stdout pipes. +func NewStdIOExecutable(ctx context.Context, bin string, args ...string) (process.ResourceStreamProcessor, error) { + cmd := exec.CommandContext(ctx, bin, args...) + stdin, err := cmd.StdinPipe() + if err != nil { + return nil, err + } + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + cmd.Stderr = os.Stderr + + err = cmd.Start() + if err != nil { + return nil, fmt.Errorf("unable to start processor: %w", err) + } + + e := stdIOExecutable{ + processor: cmd, + stdin: stdin, + stdout: stdout, + } + + return &e, nil +} + +func (e *stdIOExecutable) Process(ctx context.Context, r io.Reader, w io.Writer) error { + _, err := io.Copy(e.stdin, r) + if err != nil { + return fmt.Errorf("unable to write input: %w", err) + } + + err = e.stdin.Close() + if err != nil { + return fmt.Errorf("unable to close input writer: %w", err) + } + + _, err = io.Copy(w, e.stdout) + if err != nil { + return fmt.Errorf("unable to read output: %w", err) + } + + err = e.processor.Wait() + if err != nil { + return fmt.Errorf("unable to stop processor: %w", err) + } + + return nil +} diff --git a/pkg/transport/process/extension/uds_executable.go b/pkg/transport/process/extension/uds_executable.go new file mode 100644 index 00000000..fa4c7cf9 --- /dev/null +++ b/pkg/transport/process/extension/uds_executable.go @@ -0,0 +1,106 @@ +package extension + +import ( + "context" + "fmt" + "io" + "net" + "os" + "os/exec" + "time" + + "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/utils" +) + +const serverAddressFlag = "--addr" + +type udsExecutable struct { + processor *exec.Cmd + addr string + conn net.Conn +} + +// NewUDSExecutable runs a resource processor in the background. +// It communicates with this processor via Unix Domain Sockets. +func NewUDSExecutable(ctx context.Context, bin string, args ...string) (process.ResourceStreamProcessor, error) { + for _, arg := range args { + if arg == serverAddressFlag { + return nil, fmt.Errorf("the flag %s is not allowed to be set manually", serverAddressFlag) + } + } + + wd, err := os.Getwd() + if err != nil { + return nil, err + } + addr := fmt.Sprintf("%s/%s.sock", wd, utils.RandomString(8)) + args = append(args, "--addr", addr) + + cmd := exec.CommandContext(ctx, bin, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err = cmd.Start() + if err != nil { + return nil, fmt.Errorf("unable to start processor: %w", err) + } + + conn, err := tryConnect(addr) + if err != nil { + return nil, fmt.Errorf("unable to connect to processor: %w", err) + } + + e := udsExecutable{ + processor: cmd, + addr: addr, + conn: conn, + } + + return &e, nil +} + +func (e *udsExecutable) Process(ctx context.Context, r io.Reader, w io.Writer) error { + _, err := io.Copy(e.conn, r) + if err != nil { + return fmt.Errorf("unable to write input: %w", err) + } + + usock := e.conn.(*net.UnixConn) + err = usock.CloseWrite() + if err != nil { + return fmt.Errorf("unable to close input writer: %w", err) + } + + _, err = io.Copy(w, e.conn) + if err != nil { + return fmt.Errorf("unable to read output: %w", err) + } + + err = e.processor.Wait() + if err != nil { + return fmt.Errorf("unable to stop processor: %w", err) + } + + return nil +} + +func tryConnect(addr string) (net.Conn, error) { + const ( + maxRetries = 5 + sleeptime = 500 * time.Millisecond + ) + + var conn net.Conn + var err error + for i := 0; i <= maxRetries; i++ { + conn, err = net.Dial("unix", addr) + if err == nil { + break + } + + time.Sleep(sleeptime) + } + + return conn, err +} diff --git a/pkg/transport/process/pipeline.go b/pkg/transport/process/pipeline.go new file mode 100644 index 00000000..ad5b6864 --- /dev/null +++ b/pkg/transport/process/pipeline.go @@ -0,0 +1,82 @@ +package process + +import ( + "context" + "os" + + "archive/tar" + "fmt" + "io/ioutil" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +type resourceProcessingPipelineImpl struct { + processors []ResourceStreamProcessor +} + +func (p *resourceProcessingPipelineImpl) Process(ctx context.Context, cd cdv2.ComponentDescriptor, res cdv2.Resource) (*cdv2.ComponentDescriptor, cdv2.Resource, error) { + infile, err := ioutil.TempFile("", "out") + if err != nil { + return nil, cdv2.Resource{}, fmt.Errorf("unable to create temporary infile: %w", err) + } + + err = WriteArchive(ctx, &cd, res, nil, tar.NewWriter(infile)) + if err != nil { + return nil, cdv2.Resource{}, fmt.Errorf("unable to write: %w", err) + } + + for _, proc := range p.processors { + outfile, err := p.process(ctx, infile, proc) + if err != nil { + return nil, cdv2.Resource{}, err + } + + infile = outfile + } + defer infile.Close() + + _, err = infile.Seek(0, 0) + if err != nil { + return nil, cdv2.Resource{}, err + } + + processedCD, processedRes, blobreader, err := ReadArchive(tar.NewReader(infile)) + if err != nil { + return nil, cdv2.Resource{}, fmt.Errorf("unable to read output data: %w", err) + } + defer blobreader.Close() + + return processedCD, processedRes, nil +} + +func (p *resourceProcessingPipelineImpl) process(ctx context.Context, infile *os.File, proc ResourceStreamProcessor) (*os.File, error) { + defer infile.Close() + + _, err := infile.Seek(0, 0) + if err != nil { + return nil, fmt.Errorf("unable to seek to beginning of input file: %w", err) + } + + outfile, err := ioutil.TempFile("", "out") + if err != nil { + return nil, fmt.Errorf("unable to create temporary outfile: %w", err) + } + + inreader := infile + outwriter := outfile + + err = proc.Process(ctx, inreader, outwriter) + if err != nil { + return nil, fmt.Errorf("unable to process resource: %w", err) + } + + return outfile, nil +} + +func NewResourceProcessingPipeline(procs ...ResourceStreamProcessor) (ResourceProcessingPipeline, error) { + p := resourceProcessingPipelineImpl{ + processors: procs, + } + return &p, nil +} diff --git a/pkg/transport/process/types.go b/pkg/transport/process/types.go new file mode 100644 index 00000000..4faa38b9 --- /dev/null +++ b/pkg/transport/process/types.go @@ -0,0 +1,16 @@ +package process + +import ( + "context" + "io" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +type ResourceProcessingPipeline interface { + Process(context.Context, cdv2.ComponentDescriptor, cdv2.Resource) (*cdv2.ComponentDescriptor, cdv2.Resource, error) +} + +type ResourceStreamProcessor interface { + Process(context.Context, io.Reader, io.Writer) error +} diff --git a/pkg/transport/process/util.go b/pkg/transport/process/util.go new file mode 100644 index 00000000..e1984c40 --- /dev/null +++ b/pkg/transport/process/util.go @@ -0,0 +1,181 @@ +package process + +import ( + "archive/tar" + "bytes" + "context" + "fmt" + "io" + "io/ioutil" + "os" + "time" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "sigs.k8s.io/yaml" +) + +const ( + ResourceFile = "resource.yaml" + ComponentDescriptorFile = "component-descriptor.yaml" + BlobFile = "blob" +) + +func WriteFile(name string, contentReader io.Reader, outArchive *tar.Writer) error { + tmpfile, err := ioutil.TempFile("", "tmp") + if err != nil { + return fmt.Errorf("unable to create tempfile: %w", err) + } + defer tmpfile.Close() + + _, err = io.Copy(tmpfile, contentReader) + if err != nil { + return fmt.Errorf("unable to write content to tempfile: %w", err) + } + + _, err = tmpfile.Seek(0, 0) + if err != nil { + return fmt.Errorf("unable to seek to beginning of tempfile: %w", err) + } + + fstat, err := tmpfile.Stat() + if err != nil { + return fmt.Errorf("unable to get file stats: %w", err) + } + + header := tar.Header{ + Name: name, + Size: fstat.Size(), + Mode: int64(fstat.Mode()), + ModTime: time.Now(), + } + + if err = outArchive.WriteHeader(&header); err != nil { + return fmt.Errorf("unable to write tar header: %w", err) + } + + _, err = io.Copy(outArchive, tmpfile) + if err != nil { + return fmt.Errorf("unable to write file to archive: %w", err) + } + + return nil +} + +func WriteArchive(ctx context.Context, cd *cdv2.ComponentDescriptor, res cdv2.Resource, blobReader io.Reader, outwriter *tar.Writer) error { + defer outwriter.Close() + + println("start writing data") + + marshaledCD, err := yaml.Marshal(cd) + if err != nil { + return fmt.Errorf("unable to marshal component descriptor: %w", err) + } + + println("writing component descriptor") + err = WriteFile(ComponentDescriptorFile, bytes.NewReader(marshaledCD), outwriter) + if err != nil { + return fmt.Errorf("unable to write component descriptor: %w", err) + } + + marshaledRes, err := yaml.Marshal(res) + if err != nil { + return fmt.Errorf("unable to marshal resource: %w", err) + } + + println("writing resource") + err = WriteFile(ResourceFile, bytes.NewReader(marshaledRes), outwriter) + if err != nil { + return fmt.Errorf("unable to write resource: %w", err) + } + + if blobReader != nil { + println("writing blob") + err = WriteFile(BlobFile, blobReader, outwriter) + if err != nil { + return fmt.Errorf("unable to write blob: %w", err) + } + } + + println("finished writing data") + + return nil +} + +func ReadArchive(r *tar.Reader) (*cdv2.ComponentDescriptor, cdv2.Resource, io.ReadCloser, error) { + var cd *cdv2.ComponentDescriptor + var res cdv2.Resource + var blobFile *os.File + + for { + header, err := r.Next() + if err != nil { + if err == io.EOF { + break + } + return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read header: %w", err) + } + + switch header.Name { + case ResourceFile: + res, err = ParseResource(r) + if err != nil { + return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read %s: %w", ResourceFile, err) + } + case ComponentDescriptorFile: + cd, err = ParseComponentDescriptor(r) + if err != nil { + return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read %s: %w", ComponentDescriptorFile, err) + } + case BlobFile: + blobFile, err = ioutil.TempFile("", "") + if err != nil { + return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to create tempfile: %w", err) + } + _, err = io.Copy(blobFile, r) + if err != nil { + return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read %s: %w", BlobFile, err) + } + } + } + + if blobFile != nil { + _, err := blobFile.Seek(0, 0) + if err != nil { + return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to seek to beginning of blobfile: %w", err) + } + } + + return cd, res, blobFile, nil +} + +func ParseResource(r *tar.Reader) (cdv2.Resource, error) { + buf := bytes.NewBuffer([]byte{}) + _, err := io.Copy(buf, r) + if err != nil { + return cdv2.Resource{}, fmt.Errorf("unable to read from stream: %w", err) + } + + var res cdv2.Resource + err = yaml.Unmarshal(buf.Bytes(), &res) + if err != nil { + return cdv2.Resource{}, fmt.Errorf("unable to unmarshal: %w", err) + } + + return res, nil +} + +func ParseComponentDescriptor(r *tar.Reader) (*cdv2.ComponentDescriptor, error) { + buf := bytes.NewBuffer([]byte{}) + _, err := io.Copy(buf, r) + if err != nil { + return nil, fmt.Errorf("unable to read from stream: %w", err) + } + + var cd cdv2.ComponentDescriptor + err = yaml.Unmarshal(buf.Bytes(), &cd) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal: %w", err) + } + + return &cd, nil +} From 2813a7d6fdbbf706e25b992028c49ff57a28325b Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 14 Sep 2021 17:02:44 +0200 Subject: [PATCH 08/94] refactoring --- .../process/extension/stdio_executable.go | 2 +- .../process/extension/uds_executable.go | 3 +- pkg/transport/process/pipeline.go | 11 +- pkg/transport/process/types.go | 10 ++ pkg/transport/process/util.go | 132 +++++++++--------- 5 files changed, 83 insertions(+), 75 deletions(-) diff --git a/pkg/transport/process/extension/stdio_executable.go b/pkg/transport/process/extension/stdio_executable.go index 9f7e956c..5fafcc8b 100644 --- a/pkg/transport/process/extension/stdio_executable.go +++ b/pkg/transport/process/extension/stdio_executable.go @@ -16,7 +16,7 @@ type stdIOExecutable struct { stdout io.Reader } -// NewStdIOExecutable runs resource processor in the background. +// NewStdIOExecutable runs resource processor extension executable in the background. // It communicates with this processor via stdin/stdout pipes. func NewStdIOExecutable(ctx context.Context, bin string, args ...string) (process.ResourceStreamProcessor, error) { cmd := exec.CommandContext(ctx, bin, args...) diff --git a/pkg/transport/process/extension/uds_executable.go b/pkg/transport/process/extension/uds_executable.go index fa4c7cf9..b5e4d2c4 100644 --- a/pkg/transport/process/extension/uds_executable.go +++ b/pkg/transport/process/extension/uds_executable.go @@ -21,7 +21,7 @@ type udsExecutable struct { conn net.Conn } -// NewUDSExecutable runs a resource processor in the background. +// NewUDSExecutable runs a resource processor extension executable in the background. // It communicates with this processor via Unix Domain Sockets. func NewUDSExecutable(ctx context.Context, bin string, args ...string) (process.ResourceStreamProcessor, error) { for _, arg := range args { @@ -77,6 +77,7 @@ func (e *udsExecutable) Process(ctx context.Context, r io.Reader, w io.Writer) e return fmt.Errorf("unable to read output: %w", err) } + // extension servers must implement ordinary shutdown (!) err = e.processor.Wait() if err != nil { return fmt.Errorf("unable to stop processor: %w", err) diff --git a/pkg/transport/process/pipeline.go b/pkg/transport/process/pipeline.go index ad5b6864..98de0227 100644 --- a/pkg/transport/process/pipeline.go +++ b/pkg/transport/process/pipeline.go @@ -21,7 +21,7 @@ func (p *resourceProcessingPipelineImpl) Process(ctx context.Context, cd cdv2.Co return nil, cdv2.Resource{}, fmt.Errorf("unable to create temporary infile: %w", err) } - err = WriteArchive(ctx, &cd, res, nil, tar.NewWriter(infile)) + err = WriteTARArchive(ctx, cd, res, nil, tar.NewWriter(infile)) if err != nil { return nil, cdv2.Resource{}, fmt.Errorf("unable to write: %w", err) } @@ -41,7 +41,7 @@ func (p *resourceProcessingPipelineImpl) Process(ctx context.Context, cd cdv2.Co return nil, cdv2.Resource{}, err } - processedCD, processedRes, blobreader, err := ReadArchive(tar.NewReader(infile)) + processedCD, processedRes, blobreader, err := ReadTARArchive(tar.NewReader(infile)) if err != nil { return nil, cdv2.Resource{}, fmt.Errorf("unable to read output data: %w", err) } @@ -74,9 +74,10 @@ func (p *resourceProcessingPipelineImpl) process(ctx context.Context, infile *os return outfile, nil } -func NewResourceProcessingPipeline(procs ...ResourceStreamProcessor) (ResourceProcessingPipeline, error) { +// NewResourceProcessingPipeline returns a new ResourceProcessingPipeline +func NewResourceProcessingPipeline(processors ...ResourceStreamProcessor) ResourceProcessingPipeline { p := resourceProcessingPipelineImpl{ - processors: procs, + processors: processors, } - return &p, nil + return &p } diff --git a/pkg/transport/process/types.go b/pkg/transport/process/types.go index 4faa38b9..d0b1a998 100644 --- a/pkg/transport/process/types.go +++ b/pkg/transport/process/types.go @@ -7,10 +7,20 @@ import ( cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" ) +// ResourceProcessingPipeline describes a chain of multiple processors for processing a resource. +// Each processor receives its input from the preceding processor and writes the output for the +// subsequent processor. To work correctly, a pipeline must consist of 1 downloader, 0..n processors, +// and 1..n uploaders. type ResourceProcessingPipeline interface { + // Process executes all processors for a resource. + // Returns the component descriptor and resource of the last processor. Process(context.Context, cdv2.ComponentDescriptor, cdv2.Resource) (*cdv2.ComponentDescriptor, cdv2.Resource, error) } +// ResourceStreamProcessor describes an individual processor for processing a resource. +// A processor can upload, modify, or download a resource. type ResourceStreamProcessor interface { + // Process executes the processor for a resource. Input and Output streams must be TAR + // archives which contain the component descriptor, resource, and resource blob. Process(context.Context, io.Reader, io.Writer) error } diff --git a/pkg/transport/process/util.go b/pkg/transport/process/util.go index e1984c40..a07f3e63 100644 --- a/pkg/transport/process/util.go +++ b/pkg/transport/process/util.go @@ -15,96 +15,92 @@ import ( ) const ( - ResourceFile = "resource.yaml" - ComponentDescriptorFile = "component-descriptor.yaml" - BlobFile = "blob" + componentDescriptorFile = "component-descriptor.yaml" + resourceFile = "resource.yaml" + resourceBlobFile = "resource-blob" ) -func WriteFile(name string, contentReader io.Reader, outArchive *tar.Writer) error { - tmpfile, err := ioutil.TempFile("", "tmp") - if err != nil { - return fmt.Errorf("unable to create tempfile: %w", err) - } - defer tmpfile.Close() +// WriteTARArchive writes the component descriptor, resource and resource blob to a TAR archive +func WriteTARArchive(ctx context.Context, cd cdv2.ComponentDescriptor, res cdv2.Resource, resourceBlobReader io.Reader, outArchive *tar.Writer) error { + defer outArchive.Close() - _, err = io.Copy(tmpfile, contentReader) + marshaledCD, err := yaml.Marshal(cd) if err != nil { - return fmt.Errorf("unable to write content to tempfile: %w", err) + return fmt.Errorf("unable to marshal component descriptor: %w", err) } - _, err = tmpfile.Seek(0, 0) + err = writeFileToTARArchive(componentDescriptorFile, bytes.NewReader(marshaledCD), outArchive) if err != nil { - return fmt.Errorf("unable to seek to beginning of tempfile: %w", err) + return fmt.Errorf("unable to write %s: %w", componentDescriptorFile, err) } - fstat, err := tmpfile.Stat() + marshaledRes, err := yaml.Marshal(res) if err != nil { - return fmt.Errorf("unable to get file stats: %w", err) - } - - header := tar.Header{ - Name: name, - Size: fstat.Size(), - Mode: int64(fstat.Mode()), - ModTime: time.Now(), + return fmt.Errorf("unable to marshal resource: %w", err) } - if err = outArchive.WriteHeader(&header); err != nil { - return fmt.Errorf("unable to write tar header: %w", err) + err = writeFileToTARArchive(resourceFile, bytes.NewReader(marshaledRes), outArchive) + if err != nil { + return fmt.Errorf("unable to write %s: %w", resourceFile, err) } - _, err = io.Copy(outArchive, tmpfile) - if err != nil { - return fmt.Errorf("unable to write file to archive: %w", err) + if resourceBlobReader != nil { + err = writeFileToTARArchive(resourceBlobFile, resourceBlobReader, outArchive) + if err != nil { + return fmt.Errorf("unable to write %s: %w", resourceBlobFile, err) + } } return nil } -func WriteArchive(ctx context.Context, cd *cdv2.ComponentDescriptor, res cdv2.Resource, blobReader io.Reader, outwriter *tar.Writer) error { - defer outwriter.Close() - - println("start writing data") - - marshaledCD, err := yaml.Marshal(cd) +func writeFileToTARArchive(filename string, contentReader io.Reader, outArchive *tar.Writer) error { + tempfile, err := ioutil.TempFile("", "") if err != nil { - return fmt.Errorf("unable to marshal component descriptor: %w", err) + return fmt.Errorf("unable to create tempfile: %w", err) } + defer tempfile.Close() - println("writing component descriptor") - err = WriteFile(ComponentDescriptorFile, bytes.NewReader(marshaledCD), outwriter) + _, err = io.Copy(tempfile, contentReader) if err != nil { - return fmt.Errorf("unable to write component descriptor: %w", err) + return fmt.Errorf("unable to write content to file: %w", err) } - marshaledRes, err := yaml.Marshal(res) + _, err = tempfile.Seek(0, 0) if err != nil { - return fmt.Errorf("unable to marshal resource: %w", err) + return fmt.Errorf("unable to seek to beginning of file: %w", err) } - println("writing resource") - err = WriteFile(ResourceFile, bytes.NewReader(marshaledRes), outwriter) + fstat, err := tempfile.Stat() if err != nil { - return fmt.Errorf("unable to write resource: %w", err) + return fmt.Errorf("unable to get file info: %w", err) } - if blobReader != nil { - println("writing blob") - err = WriteFile(BlobFile, blobReader, outwriter) - if err != nil { - return fmt.Errorf("unable to write blob: %w", err) - } + header := tar.Header{ + Name: filename, + Size: fstat.Size(), + Mode: int64(fstat.Mode()), + ModTime: time.Now(), } - println("finished writing data") + if err = outArchive.WriteHeader(&header); err != nil { + return fmt.Errorf("unable to write tar header: %w", err) + } + + _, err = io.Copy(outArchive, tempfile) + if err != nil { + return fmt.Errorf("unable to write file to tar archive: %w", err) + } return nil } -func ReadArchive(r *tar.Reader) (*cdv2.ComponentDescriptor, cdv2.Resource, io.ReadCloser, error) { +// ReadTARArchive reads the component descriptor, resource and resource blob from a TAR archive. +// The resource blob reader can be nil. If a non-nil value is returned, it must be closed by the caller. +func ReadTARArchive(r *tar.Reader) (*cdv2.ComponentDescriptor, cdv2.Resource, io.ReadCloser, error) { var cd *cdv2.ComponentDescriptor var res cdv2.Resource - var blobFile *os.File + var f *os.File for { header, err := r.Next() @@ -112,43 +108,43 @@ func ReadArchive(r *tar.Reader) (*cdv2.ComponentDescriptor, cdv2.Resource, io.Re if err == io.EOF { break } - return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read header: %w", err) + return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read tar header: %w", err) } switch header.Name { - case ResourceFile: - res, err = ParseResource(r) + case resourceFile: + res, err = readResource(r) if err != nil { - return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read %s: %w", ResourceFile, err) + return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read %s: %w", resourceFile, err) } - case ComponentDescriptorFile: - cd, err = ParseComponentDescriptor(r) + case componentDescriptorFile: + cd, err = readComponentDescriptor(r) if err != nil { - return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read %s: %w", ComponentDescriptorFile, err) + return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read %s: %w", componentDescriptorFile, err) } - case BlobFile: - blobFile, err = ioutil.TempFile("", "") + case resourceBlobFile: + f, err = ioutil.TempFile("", "") if err != nil { return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to create tempfile: %w", err) } - _, err = io.Copy(blobFile, r) + _, err = io.Copy(f, r) if err != nil { - return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read %s: %w", BlobFile, err) + return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read %s: %w", resourceBlobFile, err) } } } - if blobFile != nil { - _, err := blobFile.Seek(0, 0) + if f != nil { + _, err := f.Seek(0, 0) if err != nil { - return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to seek to beginning of blobfile: %w", err) + return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to seek to beginning of file: %w", err) } } - return cd, res, blobFile, nil + return cd, res, f, nil } -func ParseResource(r *tar.Reader) (cdv2.Resource, error) { +func readResource(r *tar.Reader) (cdv2.Resource, error) { buf := bytes.NewBuffer([]byte{}) _, err := io.Copy(buf, r) if err != nil { @@ -164,7 +160,7 @@ func ParseResource(r *tar.Reader) (cdv2.Resource, error) { return res, nil } -func ParseComponentDescriptor(r *tar.Reader) (*cdv2.ComponentDescriptor, error) { +func readComponentDescriptor(r *tar.Reader) (*cdv2.ComponentDescriptor, error) { buf := bytes.NewBuffer([]byte{}) _, err := io.Copy(buf, r) if err != nil { From a5bdb52b4c14bb8b611085f19e2b9fede8074de8 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Thu, 16 Sep 2021 09:58:06 +0200 Subject: [PATCH 09/94] review feedback --- .../process/extension/stdio_executable.go | 26 +++++----- .../process/extension/uds_executable.go | 6 +-- pkg/transport/process/pipeline.go | 15 +++--- pkg/transport/process/util.go | 47 +++++++------------ 4 files changed, 36 insertions(+), 58 deletions(-) diff --git a/pkg/transport/process/extension/stdio_executable.go b/pkg/transport/process/extension/stdio_executable.go index 5fafcc8b..f487e91c 100644 --- a/pkg/transport/process/extension/stdio_executable.go +++ b/pkg/transport/process/extension/stdio_executable.go @@ -16,10 +16,11 @@ type stdIOExecutable struct { stdout io.Reader } -// NewStdIOExecutable runs resource processor extension executable in the background. +// NewStdIOExecutable runs a resource processor extension executable in the background. // It communicates with this processor via stdin/stdout pipes. -func NewStdIOExecutable(ctx context.Context, bin string, args ...string) (process.ResourceStreamProcessor, error) { +func NewStdIOExecutable(ctx context.Context, bin string, args []string, env []string) (process.ResourceStreamProcessor, error) { cmd := exec.CommandContext(ctx, bin, args...) + cmd.Env = env stdin, err := cmd.StdinPipe() if err != nil { return nil, err @@ -30,11 +31,6 @@ func NewStdIOExecutable(ctx context.Context, bin string, args ...string) (proces } cmd.Stderr = os.Stderr - err = cmd.Start() - if err != nil { - return nil, fmt.Errorf("unable to start processor: %w", err) - } - e := stdIOExecutable{ processor: cmd, stdin: stdin, @@ -45,23 +41,23 @@ func NewStdIOExecutable(ctx context.Context, bin string, args ...string) (proces } func (e *stdIOExecutable) Process(ctx context.Context, r io.Reader, w io.Writer) error { - _, err := io.Copy(e.stdin, r) - if err != nil { + if err := e.processor.Start(); err != nil { + return fmt.Errorf("unable to start processor: %w", err) + } + + if _, err := io.Copy(e.stdin, r); err != nil { return fmt.Errorf("unable to write input: %w", err) } - err = e.stdin.Close() - if err != nil { + if err := e.stdin.Close(); err != nil { return fmt.Errorf("unable to close input writer: %w", err) } - _, err = io.Copy(w, e.stdout) - if err != nil { + if _, err := io.Copy(w, e.stdout); err != nil { return fmt.Errorf("unable to read output: %w", err) } - err = e.processor.Wait() - if err != nil { + if err := e.processor.Wait(); err != nil { return fmt.Errorf("unable to stop processor: %w", err) } diff --git a/pkg/transport/process/extension/uds_executable.go b/pkg/transport/process/extension/uds_executable.go index b5e4d2c4..99cc410c 100644 --- a/pkg/transport/process/extension/uds_executable.go +++ b/pkg/transport/process/extension/uds_executable.go @@ -23,7 +23,7 @@ type udsExecutable struct { // NewUDSExecutable runs a resource processor extension executable in the background. // It communicates with this processor via Unix Domain Sockets. -func NewUDSExecutable(ctx context.Context, bin string, args ...string) (process.ResourceStreamProcessor, error) { +func NewUDSExecutable(ctx context.Context, bin string, args []string, env []string) (process.ResourceStreamProcessor, error) { for _, arg := range args { if arg == serverAddressFlag { return nil, fmt.Errorf("the flag %s is not allowed to be set manually", serverAddressFlag) @@ -38,11 +38,11 @@ func NewUDSExecutable(ctx context.Context, bin string, args ...string) (process. args = append(args, "--addr", addr) cmd := exec.CommandContext(ctx, bin, args...) + cmd.Env = env cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - err = cmd.Start() - if err != nil { + if err := cmd.Start(); err != nil { return nil, fmt.Errorf("unable to start processor: %w", err) } diff --git a/pkg/transport/process/pipeline.go b/pkg/transport/process/pipeline.go index 98de0227..26d49df7 100644 --- a/pkg/transport/process/pipeline.go +++ b/pkg/transport/process/pipeline.go @@ -2,6 +2,7 @@ package process import ( "context" + "io" "os" "archive/tar" @@ -16,13 +17,12 @@ type resourceProcessingPipelineImpl struct { } func (p *resourceProcessingPipelineImpl) Process(ctx context.Context, cd cdv2.ComponentDescriptor, res cdv2.Resource) (*cdv2.ComponentDescriptor, cdv2.Resource, error) { - infile, err := ioutil.TempFile("", "out") + infile, err := ioutil.TempFile("", "") if err != nil { return nil, cdv2.Resource{}, fmt.Errorf("unable to create temporary infile: %w", err) } - err = WriteTARArchive(ctx, cd, res, nil, tar.NewWriter(infile)) - if err != nil { + if err := WriteTARArchive(ctx, cd, res, nil, tar.NewWriter(infile)); err != nil { return nil, cdv2.Resource{}, fmt.Errorf("unable to write: %w", err) } @@ -36,8 +36,7 @@ func (p *resourceProcessingPipelineImpl) Process(ctx context.Context, cd cdv2.Co } defer infile.Close() - _, err = infile.Seek(0, 0) - if err != nil { + if _, err := infile.Seek(0, io.SeekStart); err != nil { return nil, cdv2.Resource{}, err } @@ -53,8 +52,7 @@ func (p *resourceProcessingPipelineImpl) Process(ctx context.Context, cd cdv2.Co func (p *resourceProcessingPipelineImpl) process(ctx context.Context, infile *os.File, proc ResourceStreamProcessor) (*os.File, error) { defer infile.Close() - _, err := infile.Seek(0, 0) - if err != nil { + if _, err := infile.Seek(0, io.SeekStart); err != nil { return nil, fmt.Errorf("unable to seek to beginning of input file: %w", err) } @@ -66,8 +64,7 @@ func (p *resourceProcessingPipelineImpl) process(ctx context.Context, infile *os inreader := infile outwriter := outfile - err = proc.Process(ctx, inreader, outwriter) - if err != nil { + if err := proc.Process(ctx, inreader, outwriter); err != nil { return nil, fmt.Errorf("unable to process resource: %w", err) } diff --git a/pkg/transport/process/util.go b/pkg/transport/process/util.go index a07f3e63..37760486 100644 --- a/pkg/transport/process/util.go +++ b/pkg/transport/process/util.go @@ -29,8 +29,7 @@ func WriteTARArchive(ctx context.Context, cd cdv2.ComponentDescriptor, res cdv2. return fmt.Errorf("unable to marshal component descriptor: %w", err) } - err = writeFileToTARArchive(componentDescriptorFile, bytes.NewReader(marshaledCD), outArchive) - if err != nil { + if err := writeFileToTARArchive(componentDescriptorFile, bytes.NewReader(marshaledCD), outArchive); err != nil { return fmt.Errorf("unable to write %s: %w", componentDescriptorFile, err) } @@ -39,14 +38,12 @@ func WriteTARArchive(ctx context.Context, cd cdv2.ComponentDescriptor, res cdv2. return fmt.Errorf("unable to marshal resource: %w", err) } - err = writeFileToTARArchive(resourceFile, bytes.NewReader(marshaledRes), outArchive) - if err != nil { + if err := writeFileToTARArchive(resourceFile, bytes.NewReader(marshaledRes), outArchive); err != nil { return fmt.Errorf("unable to write %s: %w", resourceFile, err) } if resourceBlobReader != nil { - err = writeFileToTARArchive(resourceBlobFile, resourceBlobReader, outArchive) - if err != nil { + if err := writeFileToTARArchive(resourceBlobFile, resourceBlobReader, outArchive); err != nil { return fmt.Errorf("unable to write %s: %w", resourceBlobFile, err) } } @@ -61,13 +58,11 @@ func writeFileToTARArchive(filename string, contentReader io.Reader, outArchive } defer tempfile.Close() - _, err = io.Copy(tempfile, contentReader) - if err != nil { + if _, err := io.Copy(tempfile, contentReader); err != nil { return fmt.Errorf("unable to write content to file: %w", err) } - _, err = tempfile.Seek(0, 0) - if err != nil { + if _, err := tempfile.Seek(0, io.SeekStart); err != nil { return fmt.Errorf("unable to seek to beginning of file: %w", err) } @@ -83,12 +78,11 @@ func writeFileToTARArchive(filename string, contentReader io.Reader, outArchive ModTime: time.Now(), } - if err = outArchive.WriteHeader(&header); err != nil { + if err := outArchive.WriteHeader(&header); err != nil { return fmt.Errorf("unable to write tar header: %w", err) } - _, err = io.Copy(outArchive, tempfile) - if err != nil { + if _, err := io.Copy(outArchive, tempfile); err != nil { return fmt.Errorf("unable to write file to tar archive: %w", err) } @@ -113,30 +107,25 @@ func ReadTARArchive(r *tar.Reader) (*cdv2.ComponentDescriptor, cdv2.Resource, io switch header.Name { case resourceFile: - res, err = readResource(r) - if err != nil { + if res, err = readResource(r); err != nil { return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read %s: %w", resourceFile, err) } case componentDescriptorFile: - cd, err = readComponentDescriptor(r) - if err != nil { + if cd, err = readComponentDescriptor(r); err != nil { return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read %s: %w", componentDescriptorFile, err) } case resourceBlobFile: - f, err = ioutil.TempFile("", "") - if err != nil { + if f, err = ioutil.TempFile("", ""); err != nil { return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to create tempfile: %w", err) } - _, err = io.Copy(f, r) - if err != nil { + if _, err := io.Copy(f, r); err != nil { return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read %s: %w", resourceBlobFile, err) } } } if f != nil { - _, err := f.Seek(0, 0) - if err != nil { + if _, err := f.Seek(0, io.SeekStart); err != nil { return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to seek to beginning of file: %w", err) } } @@ -146,14 +135,12 @@ func ReadTARArchive(r *tar.Reader) (*cdv2.ComponentDescriptor, cdv2.Resource, io func readResource(r *tar.Reader) (cdv2.Resource, error) { buf := bytes.NewBuffer([]byte{}) - _, err := io.Copy(buf, r) - if err != nil { + if _, err := io.Copy(buf, r); err != nil { return cdv2.Resource{}, fmt.Errorf("unable to read from stream: %w", err) } var res cdv2.Resource - err = yaml.Unmarshal(buf.Bytes(), &res) - if err != nil { + if err := yaml.Unmarshal(buf.Bytes(), &res); err != nil { return cdv2.Resource{}, fmt.Errorf("unable to unmarshal: %w", err) } @@ -162,14 +149,12 @@ func readResource(r *tar.Reader) (cdv2.Resource, error) { func readComponentDescriptor(r *tar.Reader) (*cdv2.ComponentDescriptor, error) { buf := bytes.NewBuffer([]byte{}) - _, err := io.Copy(buf, r) - if err != nil { + if _, err := io.Copy(buf, r); err != nil { return nil, fmt.Errorf("unable to read from stream: %w", err) } var cd cdv2.ComponentDescriptor - err = yaml.Unmarshal(buf.Bytes(), &cd) - if err != nil { + if err := yaml.Unmarshal(buf.Bytes(), &cd); err != nil { return nil, fmt.Errorf("unable to unmarshal: %w", err) } From 44752389cb5e13a20ac9e66beb9fada325c3cd1b Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Thu, 16 Sep 2021 09:59:18 +0200 Subject: [PATCH 10/94] run formatter --- pkg/transport/process/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/transport/process/types.go b/pkg/transport/process/types.go index d0b1a998..8379a93a 100644 --- a/pkg/transport/process/types.go +++ b/pkg/transport/process/types.go @@ -20,7 +20,7 @@ type ResourceProcessingPipeline interface { // ResourceStreamProcessor describes an individual processor for processing a resource. // A processor can upload, modify, or download a resource. type ResourceStreamProcessor interface { - // Process executes the processor for a resource. Input and Output streams must be TAR + // Process executes the processor for a resource. Input and Output streams must be TAR // archives which contain the component descriptor, resource, and resource blob. Process(context.Context, io.Reader, io.Writer) error } From 9d7a1d32adf5176cef7419b7607c7854addcbcf5 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Mon, 20 Sep 2021 09:38:32 +0200 Subject: [PATCH 11/94] renames extension pkg and adds tests --- hack/install-requirements.sh | 5 + .../extensions/extensions_suite_test.go | 106 ++++++++++++ .../stdio_executable.go | 5 +- .../uds_executable.go | 24 +-- pkg/transport/process/processors/test.go | 157 ++++++++++++++++++ 5 files changed, 286 insertions(+), 11 deletions(-) create mode 100644 pkg/transport/process/extensions/extensions_suite_test.go rename pkg/transport/process/{extension => extensions}/stdio_executable.go (90%) rename pkg/transport/process/{extension => extensions}/uds_executable.go (78%) create mode 100644 pkg/transport/process/processors/test.go diff --git a/hack/install-requirements.sh b/hack/install-requirements.sh index d219507b..19db5de2 100755 --- a/hack/install-requirements.sh +++ b/hack/install-requirements.sh @@ -36,3 +36,8 @@ $ export PATH=/usr/local/opt/gnu-tar/libexec/gnubin:\$PATH $ export PATH=/usr/local/opt/grep/libexec/gnubin:\$PATH EOM fi + + +echo "> Compile test processor binary" + +go build -o "${PROJECT_ROOT}/tmp/test/bin/processor" "${PROJECT_ROOT}/pkg/transport/process/processors" \ No newline at end of file diff --git a/pkg/transport/process/extensions/extensions_suite_test.go b/pkg/transport/process/extensions/extensions_suite_test.go new file mode 100644 index 00000000..f22cdd77 --- /dev/null +++ b/pkg/transport/process/extensions/extensions_suite_test.go @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package extensions_test + +import ( + "archive/tar" + "bytes" + "context" + "encoding/json" + "io" + "strings" + "testing" + + "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/extensions" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +const ( + defaultProcessorBinaryPath = "../../../../tmp/test/bin/processor" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "transport extensions Test Suite") +} + +var _ = Describe("transport extensions", func() { + + Context("stdio executable", func() { + It("should modify the processed resource correctly", func() { + args := []string{} + env := []string{} + processor, err := extensions.NewStdIOExecutable(context.TODO(), defaultProcessorBinaryPath, args, env) + Expect(err).ToNot(HaveOccurred()) + + testProcessor(processor) + }) + }) + + Context("uds executable", func() { + It("should modify the processed resource correctly", func() { + args := []string{} + env := []string{} + processor, err := extensions.NewUDSExecutable(context.TODO(), defaultProcessorBinaryPath, args, env) + Expect(err).ToNot(HaveOccurred()) + + testProcessor(processor) + }) + }) + +}) + +func testProcessor(processor process.ResourceStreamProcessor) { + const ( + processorName = "test-processor" + resourceData = "12345" + expectedResourceData = resourceData + "\n" + processorName + ) + + res := cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "my-res", + Version: "v0.1.0", + Type: "ociImage", + }, + } + + l := cdv2.Label{ + Name: "processor-name", + Value: json.RawMessage(`"` + processorName + `"`), + } + expectedRes := res + expectedRes.Labels = append(expectedRes.Labels, l) + + cd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + Resources: []cdv2.Resource{ + res, + }, + }, + } + + inputBuf := bytes.NewBuffer([]byte{}) + err := process.WriteTARArchive(cd, res, strings.NewReader(resourceData), tar.NewWriter(inputBuf)) + Expect(err).ToNot(HaveOccurred()) + + outputBuf := bytes.NewBuffer([]byte{}) + err = processor.Process(context.TODO(), inputBuf, outputBuf) + Expect(err).ToNot(HaveOccurred()) + + processedCD, processedRes, processedBlobReader, err := process.ReadTARArchive(tar.NewReader(outputBuf)) + Expect(err).ToNot(HaveOccurred()) + + Expect(*processedCD).To(Equal(cd)) + Expect(processedRes).To(Equal(expectedRes)) + + processedResourceDataBuf := bytes.NewBuffer([]byte{}) + _, err = io.Copy(processedResourceDataBuf, processedBlobReader) + Expect(err).ToNot(HaveOccurred()) + + Expect(processedResourceDataBuf.String()).To(Equal(expectedResourceData)) +} diff --git a/pkg/transport/process/extension/stdio_executable.go b/pkg/transport/process/extensions/stdio_executable.go similarity index 90% rename from pkg/transport/process/extension/stdio_executable.go rename to pkg/transport/process/extensions/stdio_executable.go index f487e91c..6596805e 100644 --- a/pkg/transport/process/extension/stdio_executable.go +++ b/pkg/transport/process/extensions/stdio_executable.go @@ -1,4 +1,7 @@ -package extension +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package extensions import ( "context" diff --git a/pkg/transport/process/extension/uds_executable.go b/pkg/transport/process/extensions/uds_executable.go similarity index 78% rename from pkg/transport/process/extension/uds_executable.go rename to pkg/transport/process/extensions/uds_executable.go index 99cc410c..6ac31004 100644 --- a/pkg/transport/process/extension/uds_executable.go +++ b/pkg/transport/process/extensions/uds_executable.go @@ -1,4 +1,7 @@ -package extension +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package extensions import ( "context" @@ -7,6 +10,7 @@ import ( "net" "os" "os/exec" + "syscall" "time" "github.com/gardener/component-cli/pkg/transport/process" @@ -61,26 +65,26 @@ func NewUDSExecutable(ctx context.Context, bin string, args []string, env []stri } func (e *udsExecutable) Process(ctx context.Context, r io.Reader, w io.Writer) error { - _, err := io.Copy(e.conn, r) - if err != nil { + if _, err := io.Copy(e.conn, r); err != nil { return fmt.Errorf("unable to write input: %w", err) } usock := e.conn.(*net.UnixConn) - err = usock.CloseWrite() - if err != nil { + if err := usock.CloseWrite(); err != nil { return fmt.Errorf("unable to close input writer: %w", err) } - _, err = io.Copy(w, e.conn) - if err != nil { + if _, err := io.Copy(w, e.conn); err != nil { return fmt.Errorf("unable to read output: %w", err) } + if err := e.processor.Process.Signal(syscall.SIGTERM); err != nil { + return fmt.Errorf("unable to send SIGTERM to processor: %w", err) + } + // extension servers must implement ordinary shutdown (!) - err = e.processor.Wait() - if err != nil { - return fmt.Errorf("unable to stop processor: %w", err) + if err := e.processor.Wait(); err != nil { + return fmt.Errorf("unable to wait for processor: %w", err) } return nil diff --git a/pkg/transport/process/processors/test.go b/pkg/transport/process/processors/test.go new file mode 100644 index 00000000..e7679067 --- /dev/null +++ b/pkg/transport/process/processors/test.go @@ -0,0 +1,157 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package main + +import ( + "archive/tar" + "bytes" + "encoding/json" + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "net" + "os" + "os/signal" + "strings" + "sync" + "syscall" + + "github.com/gardener/component-cli/pkg/transport/process" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +const processorName = "test-processor" + +type ProcessorHandlerFunc func(io.Reader, io.WriteCloser) + +type Server struct { + listener net.Listener + quit chan interface{} + wg sync.WaitGroup + handler ProcessorHandlerFunc +} + +func NewServer(addr string, h ProcessorHandlerFunc) (*Server, error) { + l, err := net.Listen("unix", addr) + if err != nil { + return nil, err + } + s := &Server{ + quit: make(chan interface{}), + listener: l, + handler: h, + } + return s, nil +} + +func (s *Server) Start() { + s.wg.Add(1) + go s.serve() +} + +func (s *Server) serve() { + defer s.wg.Done() + + for { + conn, err := s.listener.Accept() + if err != nil { + select { + case <-s.quit: + return + default: + log.Println("accept error", err) + } + } else { + s.wg.Add(1) + go func() { + defer s.wg.Done() + s.handler(conn, conn) + }() + } + } +} + +func (s *Server) Stop() { + close(s.quit) + if err := s.listener.Close(); err != nil { + println(err) + } + s.wg.Wait() +} + +func main() { + addr := flag.String("addr", "", "") + flag.Parse() + + if *addr == "" { + // if addr is not set, use stdin/stdout for communication + if err := ProcessorRoutine(os.Stdin, os.Stdout); err != nil { + log.Fatal(err) + } + return + } + + h := func(r io.Reader, w io.WriteCloser) { + ProcessorRoutine(r, w) + } + + srv, err := NewServer(*addr, h) + if err != nil { + log.Fatal(err) + } + + srv.Start() + + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + <-stop + + srv.Stop() +} + +func ProcessorRoutine(inputStream io.Reader, outputStream io.WriteCloser) error { + defer outputStream.Close() + + tmpfile, err := ioutil.TempFile("", "") + if err != nil { + return err + } + defer tmpfile.Close() + + if _, err := io.Copy(tmpfile, inputStream); err != nil { + return err + } + + if _, err := tmpfile.Seek(0, io.SeekStart); err != nil { + return err + } + + cd, res, resourceBlobReader, err := process.ReadTARArchive(tar.NewReader(tmpfile)) + if err != nil { + return err + } + if resourceBlobReader != nil { + defer resourceBlobReader.Close() + } + + buf := bytes.NewBuffer([]byte{}) + if _, err := io.Copy(buf, resourceBlobReader); err != nil { + return err + } + outputData := fmt.Sprintf("%s\n%s", buf.String(), processorName) + + l := cdv2.Label{ + Name: "processor-name", + Value: json.RawMessage(`"` + processorName + `"`), + } + res.Labels = append(res.Labels, l) + + if err := process.WriteTARArchive(*cd, res, strings.NewReader(outputData), tar.NewWriter(outputStream)); err != nil { + return err + } + + return nil +} From 38fd5ff9e8b17eab02a0398b520935cc81bf4cf3 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Mon, 20 Sep 2021 09:38:52 +0200 Subject: [PATCH 12/94] adds license headers --- pkg/transport/process/pipeline.go | 5 ++++- pkg/transport/process/types.go | 3 +++ pkg/transport/process/util.go | 6 ++++-- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/pkg/transport/process/pipeline.go b/pkg/transport/process/pipeline.go index 26d49df7..0ac32887 100644 --- a/pkg/transport/process/pipeline.go +++ b/pkg/transport/process/pipeline.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 package process import ( @@ -22,7 +25,7 @@ func (p *resourceProcessingPipelineImpl) Process(ctx context.Context, cd cdv2.Co return nil, cdv2.Resource{}, fmt.Errorf("unable to create temporary infile: %w", err) } - if err := WriteTARArchive(ctx, cd, res, nil, tar.NewWriter(infile)); err != nil { + if err := WriteTARArchive(cd, res, nil, tar.NewWriter(infile)); err != nil { return nil, cdv2.Resource{}, fmt.Errorf("unable to write: %w", err) } diff --git a/pkg/transport/process/types.go b/pkg/transport/process/types.go index 8379a93a..6e970285 100644 --- a/pkg/transport/process/types.go +++ b/pkg/transport/process/types.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 package process import ( diff --git a/pkg/transport/process/util.go b/pkg/transport/process/util.go index 37760486..85c91c81 100644 --- a/pkg/transport/process/util.go +++ b/pkg/transport/process/util.go @@ -1,9 +1,11 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 package process import ( "archive/tar" "bytes" - "context" "fmt" "io" "io/ioutil" @@ -21,7 +23,7 @@ const ( ) // WriteTARArchive writes the component descriptor, resource and resource blob to a TAR archive -func WriteTARArchive(ctx context.Context, cd cdv2.ComponentDescriptor, res cdv2.Resource, resourceBlobReader io.Reader, outArchive *tar.Writer) error { +func WriteTARArchive(cd cdv2.ComponentDescriptor, res cdv2.Resource, resourceBlobReader io.Reader, outArchive *tar.Writer) error { defer outArchive.Close() marshaledCD, err := yaml.Marshal(cd) From 8fe08355797df82788ab4c7d118075a6ad61dac0 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Mon, 20 Sep 2021 13:52:11 +0200 Subject: [PATCH 13/94] refactoring + adds test --- .../extensions/extensions_suite_test.go | 4 +- pkg/transport/process/pipeline.go | 4 +- pkg/transport/process/process_suite_test.go | 16 +++++ pkg/transport/process/processors/test.go | 8 ++- pkg/transport/process/util.go | 62 +++++++++++-------- pkg/transport/process/util_test.go | 56 +++++++++++++++++ 6 files changed, 118 insertions(+), 32 deletions(-) create mode 100644 pkg/transport/process/process_suite_test.go create mode 100644 pkg/transport/process/util_test.go diff --git a/pkg/transport/process/extensions/extensions_suite_test.go b/pkg/transport/process/extensions/extensions_suite_test.go index f22cdd77..4f88035b 100644 --- a/pkg/transport/process/extensions/extensions_suite_test.go +++ b/pkg/transport/process/extensions/extensions_suite_test.go @@ -85,14 +85,14 @@ func testProcessor(processor process.ResourceStreamProcessor) { } inputBuf := bytes.NewBuffer([]byte{}) - err := process.WriteTARArchive(cd, res, strings.NewReader(resourceData), tar.NewWriter(inputBuf)) + err := process.WriteProcessorMessage(cd, res, strings.NewReader(resourceData), tar.NewWriter(inputBuf)) Expect(err).ToNot(HaveOccurred()) outputBuf := bytes.NewBuffer([]byte{}) err = processor.Process(context.TODO(), inputBuf, outputBuf) Expect(err).ToNot(HaveOccurred()) - processedCD, processedRes, processedBlobReader, err := process.ReadTARArchive(tar.NewReader(outputBuf)) + processedCD, processedRes, processedBlobReader, err := process.ReadProcessorMessage(tar.NewReader(outputBuf)) Expect(err).ToNot(HaveOccurred()) Expect(*processedCD).To(Equal(cd)) diff --git a/pkg/transport/process/pipeline.go b/pkg/transport/process/pipeline.go index 0ac32887..71b15819 100644 --- a/pkg/transport/process/pipeline.go +++ b/pkg/transport/process/pipeline.go @@ -25,7 +25,7 @@ func (p *resourceProcessingPipelineImpl) Process(ctx context.Context, cd cdv2.Co return nil, cdv2.Resource{}, fmt.Errorf("unable to create temporary infile: %w", err) } - if err := WriteTARArchive(cd, res, nil, tar.NewWriter(infile)); err != nil { + if err := WriteProcessorMessage(cd, res, nil, tar.NewWriter(infile)); err != nil { return nil, cdv2.Resource{}, fmt.Errorf("unable to write: %w", err) } @@ -43,7 +43,7 @@ func (p *resourceProcessingPipelineImpl) Process(ctx context.Context, cd cdv2.Co return nil, cdv2.Resource{}, err } - processedCD, processedRes, blobreader, err := ReadTARArchive(tar.NewReader(infile)) + processedCD, processedRes, blobreader, err := ReadProcessorMessage(tar.NewReader(infile)) if err != nil { return nil, cdv2.Resource{}, fmt.Errorf("unable to read output data: %w", err) } diff --git a/pkg/transport/process/process_suite_test.go b/pkg/transport/process/process_suite_test.go new file mode 100644 index 00000000..0f1b48d0 --- /dev/null +++ b/pkg/transport/process/process_suite_test.go @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package process_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Transport Process Test Suite") +} \ No newline at end of file diff --git a/pkg/transport/process/processors/test.go b/pkg/transport/process/processors/test.go index e7679067..35215aa9 100644 --- a/pkg/transport/process/processors/test.go +++ b/pkg/transport/process/processors/test.go @@ -95,7 +95,9 @@ func main() { } h := func(r io.Reader, w io.WriteCloser) { - ProcessorRoutine(r, w) + if err := ProcessorRoutine(r, w); err != nil { + log.Fatal(err) + } } srv, err := NewServer(*addr, h) @@ -129,7 +131,7 @@ func ProcessorRoutine(inputStream io.Reader, outputStream io.WriteCloser) error return err } - cd, res, resourceBlobReader, err := process.ReadTARArchive(tar.NewReader(tmpfile)) + cd, res, resourceBlobReader, err := process.ReadProcessorMessage(tar.NewReader(tmpfile)) if err != nil { return err } @@ -149,7 +151,7 @@ func ProcessorRoutine(inputStream io.Reader, outputStream io.WriteCloser) error } res.Labels = append(res.Labels, l) - if err := process.WriteTARArchive(*cd, res, strings.NewReader(outputData), tar.NewWriter(outputStream)); err != nil { + if err := process.WriteProcessorMessage(*cd, res, strings.NewReader(outputData), tar.NewWriter(outputStream)); err != nil { return err } diff --git a/pkg/transport/process/util.go b/pkg/transport/process/util.go index 85c91c81..09af0cec 100644 --- a/pkg/transport/process/util.go +++ b/pkg/transport/process/util.go @@ -17,22 +17,30 @@ import ( ) const ( - componentDescriptorFile = "component-descriptor.yaml" - resourceFile = "resource.yaml" - resourceBlobFile = "resource-blob" + // ComponentDescriptorFile is the filename of the component descriptor in a processor message tar archive + ComponentDescriptorFile = "component-descriptor.yaml" + + // ResourceFile is the filename of the resource in a processor message tar archive + ResourceFile = "resource.yaml" + + // ResourceBlobFile is the filename of the resource blob in a processor message tar archive + ResourceBlobFile = "resource-blob" ) -// WriteTARArchive writes the component descriptor, resource and resource blob to a TAR archive -func WriteTARArchive(cd cdv2.ComponentDescriptor, res cdv2.Resource, resourceBlobReader io.Reader, outArchive *tar.Writer) error { - defer outArchive.Close() +// WriteProcessorMessage writes a component descriptor, resource and resource blob as a processor +// message (tar archive with fixed filenames for component descriptor, resource, and resource blob) +// which can be consumed by processors. +func WriteProcessorMessage(cd cdv2.ComponentDescriptor, res cdv2.Resource, resourceBlobReader io.Reader, w io.Writer) error { + tw := tar.NewWriter(w) + defer tw.Close() marshaledCD, err := yaml.Marshal(cd) if err != nil { return fmt.Errorf("unable to marshal component descriptor: %w", err) } - if err := writeFileToTARArchive(componentDescriptorFile, bytes.NewReader(marshaledCD), outArchive); err != nil { - return fmt.Errorf("unable to write %s: %w", componentDescriptorFile, err) + if err := writeFileToTARArchive(ComponentDescriptorFile, bytes.NewReader(marshaledCD), tw); err != nil { + return fmt.Errorf("unable to write %s: %w", ComponentDescriptorFile, err) } marshaledRes, err := yaml.Marshal(res) @@ -40,13 +48,13 @@ func WriteTARArchive(cd cdv2.ComponentDescriptor, res cdv2.Resource, resourceBlo return fmt.Errorf("unable to marshal resource: %w", err) } - if err := writeFileToTARArchive(resourceFile, bytes.NewReader(marshaledRes), outArchive); err != nil { - return fmt.Errorf("unable to write %s: %w", resourceFile, err) + if err := writeFileToTARArchive(ResourceFile, bytes.NewReader(marshaledRes), tw); err != nil { + return fmt.Errorf("unable to write %s: %w", ResourceFile, err) } if resourceBlobReader != nil { - if err := writeFileToTARArchive(resourceBlobFile, resourceBlobReader, outArchive); err != nil { - return fmt.Errorf("unable to write %s: %w", resourceBlobFile, err) + if err := writeFileToTARArchive(ResourceBlobFile, resourceBlobReader, tw); err != nil { + return fmt.Errorf("unable to write %s: %w", ResourceBlobFile, err) } } @@ -91,15 +99,19 @@ func writeFileToTARArchive(filename string, contentReader io.Reader, outArchive return nil } -// ReadTARArchive reads the component descriptor, resource and resource blob from a TAR archive. -// The resource blob reader can be nil. If a non-nil value is returned, it must be closed by the caller. -func ReadTARArchive(r *tar.Reader) (*cdv2.ComponentDescriptor, cdv2.Resource, io.ReadCloser, error) { +// ReadProcessorMessage reads the component descriptor, resource and resource blob from a processor message +// (tar archive with fixed filenames for component descriptor, resource, and resource blob) which is +// produced by processors. The resource blob reader can be nil. If a non-nil value is returned, it must +// be closed by the caller. +func ReadProcessorMessage(r io.Reader) (*cdv2.ComponentDescriptor, cdv2.Resource, io.ReadCloser, error) { + tr := tar.NewReader(r) + var cd *cdv2.ComponentDescriptor var res cdv2.Resource var f *os.File for { - header, err := r.Next() + header, err := tr.Next() if err != nil { if err == io.EOF { break @@ -108,20 +120,20 @@ func ReadTARArchive(r *tar.Reader) (*cdv2.ComponentDescriptor, cdv2.Resource, io } switch header.Name { - case resourceFile: - if res, err = readResource(r); err != nil { - return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read %s: %w", resourceFile, err) + case ResourceFile: + if res, err = readResource(tr); err != nil { + return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read %s: %w", ResourceFile, err) } - case componentDescriptorFile: - if cd, err = readComponentDescriptor(r); err != nil { - return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read %s: %w", componentDescriptorFile, err) + case ComponentDescriptorFile: + if cd, err = readComponentDescriptor(tr); err != nil { + return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read %s: %w", ComponentDescriptorFile, err) } - case resourceBlobFile: + case ResourceBlobFile: if f, err = ioutil.TempFile("", ""); err != nil { return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to create tempfile: %w", err) } - if _, err := io.Copy(f, r); err != nil { - return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read %s: %w", resourceBlobFile, err) + if _, err := io.Copy(f, tr); err != nil { + return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to read %s: %w", ResourceBlobFile, err) } } } diff --git a/pkg/transport/process/util_test.go b/pkg/transport/process/util_test.go new file mode 100644 index 00000000..b9b8dcda --- /dev/null +++ b/pkg/transport/process/util_test.go @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package process_test + +import ( + "bytes" + "io" + "strings" + + "github.com/gardener/component-cli/pkg/transport/process" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("utils", func() { + + Context("WriteProcessMessage & ReadProcessMessage", func() { + + It("should correctly write and read a process message", func() { + res := cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "my-res", + Version: "v0.1.0", + Type: "ociImage", + }, + } + resourceData := "test-data" + + cd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + Resources: []cdv2.Resource{ + res, + }, + }, + } + + processMsgBuf := bytes.NewBuffer([]byte{}) + err := process.WriteProcessorMessage(cd, res, strings.NewReader(resourceData), processMsgBuf) + Expect(err).ToNot(HaveOccurred()) + + actualCD, actualRes, resourceBlobReader, err := process.ReadProcessorMessage(processMsgBuf) + Expect(err).ToNot(HaveOccurred()) + + Expect(*actualCD).To(Equal(cd)) + Expect(actualRes).To(Equal(res)) + + resourceBlobBuf := bytes.NewBuffer([]byte{}) + _, err = io.Copy(resourceBlobBuf, resourceBlobReader) + Expect(err).ToNot(HaveOccurred()) + Expect(resourceBlobBuf.String()).To(Equal(resourceData)) + }) + + }) +}) From 5fbf6badf2aa83130e958fb701f2eb1a79ef35ab Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 21 Sep 2021 09:42:49 +0200 Subject: [PATCH 14/94] fix tests and adds check for test processor binary --- .../process/extensions/extensions_suite_test.go | 12 +++++++++--- pkg/transport/process/pipeline.go | 7 +++---- pkg/transport/process/processors/test.go | 5 ++--- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/pkg/transport/process/extensions/extensions_suite_test.go b/pkg/transport/process/extensions/extensions_suite_test.go index 4f88035b..abd54dcd 100644 --- a/pkg/transport/process/extensions/extensions_suite_test.go +++ b/pkg/transport/process/extensions/extensions_suite_test.go @@ -4,11 +4,11 @@ package extensions_test import ( - "archive/tar" "bytes" "context" "encoding/json" "io" + "os" "strings" "testing" @@ -28,6 +28,12 @@ func TestConfig(t *testing.T) { RunSpecs(t, "transport extensions Test Suite") } +var _ = BeforeSuite(func() { + info, err := os.Stat(defaultProcessorBinaryPath) + Expect(err).ToNot(HaveOccurred()) + Expect(info.IsDir()).To(BeFalse()) +}, 5) + var _ = Describe("transport extensions", func() { Context("stdio executable", func() { @@ -85,14 +91,14 @@ func testProcessor(processor process.ResourceStreamProcessor) { } inputBuf := bytes.NewBuffer([]byte{}) - err := process.WriteProcessorMessage(cd, res, strings.NewReader(resourceData), tar.NewWriter(inputBuf)) + err := process.WriteProcessorMessage(cd, res, strings.NewReader(resourceData), inputBuf) Expect(err).ToNot(HaveOccurred()) outputBuf := bytes.NewBuffer([]byte{}) err = processor.Process(context.TODO(), inputBuf, outputBuf) Expect(err).ToNot(HaveOccurred()) - processedCD, processedRes, processedBlobReader, err := process.ReadProcessorMessage(tar.NewReader(outputBuf)) + processedCD, processedRes, processedBlobReader, err := process.ReadProcessorMessage(outputBuf) Expect(err).ToNot(HaveOccurred()) Expect(*processedCD).To(Equal(cd)) diff --git a/pkg/transport/process/pipeline.go b/pkg/transport/process/pipeline.go index 71b15819..77941189 100644 --- a/pkg/transport/process/pipeline.go +++ b/pkg/transport/process/pipeline.go @@ -8,7 +8,6 @@ import ( "io" "os" - "archive/tar" "fmt" "io/ioutil" @@ -25,7 +24,7 @@ func (p *resourceProcessingPipelineImpl) Process(ctx context.Context, cd cdv2.Co return nil, cdv2.Resource{}, fmt.Errorf("unable to create temporary infile: %w", err) } - if err := WriteProcessorMessage(cd, res, nil, tar.NewWriter(infile)); err != nil { + if err := WriteProcessorMessage(cd, res, nil, infile); err != nil { return nil, cdv2.Resource{}, fmt.Errorf("unable to write: %w", err) } @@ -43,7 +42,7 @@ func (p *resourceProcessingPipelineImpl) Process(ctx context.Context, cd cdv2.Co return nil, cdv2.Resource{}, err } - processedCD, processedRes, blobreader, err := ReadProcessorMessage(tar.NewReader(infile)) + processedCD, processedRes, blobreader, err := ReadProcessorMessage(infile) if err != nil { return nil, cdv2.Resource{}, fmt.Errorf("unable to read output data: %w", err) } @@ -59,7 +58,7 @@ func (p *resourceProcessingPipelineImpl) process(ctx context.Context, infile *os return nil, fmt.Errorf("unable to seek to beginning of input file: %w", err) } - outfile, err := ioutil.TempFile("", "out") + outfile, err := ioutil.TempFile("", "") if err != nil { return nil, fmt.Errorf("unable to create temporary outfile: %w", err) } diff --git a/pkg/transport/process/processors/test.go b/pkg/transport/process/processors/test.go index 35215aa9..13c0f7a8 100644 --- a/pkg/transport/process/processors/test.go +++ b/pkg/transport/process/processors/test.go @@ -4,7 +4,6 @@ package main import ( - "archive/tar" "bytes" "encoding/json" "flag" @@ -131,7 +130,7 @@ func ProcessorRoutine(inputStream io.Reader, outputStream io.WriteCloser) error return err } - cd, res, resourceBlobReader, err := process.ReadProcessorMessage(tar.NewReader(tmpfile)) + cd, res, resourceBlobReader, err := process.ReadProcessorMessage(tmpfile) if err != nil { return err } @@ -151,7 +150,7 @@ func ProcessorRoutine(inputStream io.Reader, outputStream io.WriteCloser) error } res.Labels = append(res.Labels, l) - if err := process.WriteProcessorMessage(*cd, res, strings.NewReader(outputData), tar.NewWriter(outputStream)); err != nil { + if err := process.WriteProcessorMessage(*cd, res, strings.NewReader(outputData), outputStream); err != nil { return err } From 94da006ea1c2368816b27c426c7a2643894273a6 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 21 Sep 2021 09:43:51 +0200 Subject: [PATCH 15/94] formatting --- pkg/transport/process/extensions/extensions_suite_test.go | 5 +++-- pkg/transport/process/process_suite_test.go | 2 +- pkg/transport/process/processors/test.go | 3 ++- pkg/transport/process/util.go | 6 +++--- pkg/transport/process/util_test.go | 3 ++- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/pkg/transport/process/extensions/extensions_suite_test.go b/pkg/transport/process/extensions/extensions_suite_test.go index abd54dcd..fca9f68e 100644 --- a/pkg/transport/process/extensions/extensions_suite_test.go +++ b/pkg/transport/process/extensions/extensions_suite_test.go @@ -12,11 +12,12 @@ import ( "strings" "testing" - "github.com/gardener/component-cli/pkg/transport/process" - "github.com/gardener/component-cli/pkg/transport/process/extensions" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + + "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/extensions" ) const ( diff --git a/pkg/transport/process/process_suite_test.go b/pkg/transport/process/process_suite_test.go index 0f1b48d0..b891cba1 100644 --- a/pkg/transport/process/process_suite_test.go +++ b/pkg/transport/process/process_suite_test.go @@ -13,4 +13,4 @@ import ( func TestConfig(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Transport Process Test Suite") -} \ No newline at end of file +} diff --git a/pkg/transport/process/processors/test.go b/pkg/transport/process/processors/test.go index 13c0f7a8..2ecc0b9c 100644 --- a/pkg/transport/process/processors/test.go +++ b/pkg/transport/process/processors/test.go @@ -18,8 +18,9 @@ import ( "sync" "syscall" - "github.com/gardener/component-cli/pkg/transport/process" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + + "github.com/gardener/component-cli/pkg/transport/process" ) const processorName = "test-processor" diff --git a/pkg/transport/process/util.go b/pkg/transport/process/util.go index 09af0cec..880b6a6b 100644 --- a/pkg/transport/process/util.go +++ b/pkg/transport/process/util.go @@ -28,7 +28,7 @@ const ( ) // WriteProcessorMessage writes a component descriptor, resource and resource blob as a processor -// message (tar archive with fixed filenames for component descriptor, resource, and resource blob) +// message (tar archive with fixed filenames for component descriptor, resource, and resource blob) // which can be consumed by processors. func WriteProcessorMessage(cd cdv2.ComponentDescriptor, res cdv2.Resource, resourceBlobReader io.Reader, w io.Writer) error { tw := tar.NewWriter(w) @@ -100,8 +100,8 @@ func writeFileToTARArchive(filename string, contentReader io.Reader, outArchive } // ReadProcessorMessage reads the component descriptor, resource and resource blob from a processor message -// (tar archive with fixed filenames for component descriptor, resource, and resource blob) which is -// produced by processors. The resource blob reader can be nil. If a non-nil value is returned, it must +// (tar archive with fixed filenames for component descriptor, resource, and resource blob) which is +// produced by processors. The resource blob reader can be nil. If a non-nil value is returned, it must // be closed by the caller. func ReadProcessorMessage(r io.Reader) (*cdv2.ComponentDescriptor, cdv2.Resource, io.ReadCloser, error) { tr := tar.NewReader(r) diff --git a/pkg/transport/process/util_test.go b/pkg/transport/process/util_test.go index b9b8dcda..b10b596f 100644 --- a/pkg/transport/process/util_test.go +++ b/pkg/transport/process/util_test.go @@ -8,10 +8,11 @@ import ( "io" "strings" - "github.com/gardener/component-cli/pkg/transport/process" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + + "github.com/gardener/component-cli/pkg/transport/process" ) var _ = Describe("utils", func() { From 2c8504e4bfd5026ebc043a6cc0f0c16c5aea3b65 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 21 Sep 2021 09:46:21 +0200 Subject: [PATCH 16/94] improves before suite check --- pkg/transport/process/extensions/extensions_suite_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/transport/process/extensions/extensions_suite_test.go b/pkg/transport/process/extensions/extensions_suite_test.go index fca9f68e..5d21c01c 100644 --- a/pkg/transport/process/extensions/extensions_suite_test.go +++ b/pkg/transport/process/extensions/extensions_suite_test.go @@ -30,9 +30,8 @@ func TestConfig(t *testing.T) { } var _ = BeforeSuite(func() { - info, err := os.Stat(defaultProcessorBinaryPath) - Expect(err).ToNot(HaveOccurred()) - Expect(info.IsDir()).To(BeFalse()) + _, err := os.Stat(defaultProcessorBinaryPath) + Expect(err).ToNot(HaveOccurred(), "test processor doesn't exists. pls run make install-requirements.") }, 5) var _ = Describe("transport extensions", func() { From a878e86c0911e7bcfc0f8a92d6dd6ef9a6abe354 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 21 Sep 2021 16:05:11 +0200 Subject: [PATCH 17/94] refactoring + adds processor timeouts --- hack/install-requirements.sh | 5 +- .../extensions/extensions_suite_test.go | 66 +++++++++++++--- .../process/extensions/stdio_executable.go | 52 ++++++------ .../process/extensions/uds_executable.go | 63 ++++++++------- pkg/transport/process/pipeline.go | 6 ++ .../processors/{test.go => example/main.go} | 79 +++---------------- .../process/processors/sleep/main.go | 51 ++++++++++++ pkg/transport/process/processors/util.go | 68 ++++++++++++++++ pkg/transport/process/util_test.go | 2 +- 9 files changed, 257 insertions(+), 135 deletions(-) rename pkg/transport/process/processors/{test.go => example/main.go} (57%) create mode 100644 pkg/transport/process/processors/sleep/main.go create mode 100644 pkg/transport/process/processors/util.go diff --git a/hack/install-requirements.sh b/hack/install-requirements.sh index 19db5de2..0496b95d 100755 --- a/hack/install-requirements.sh +++ b/hack/install-requirements.sh @@ -38,6 +38,7 @@ EOM fi -echo "> Compile test processor binary" +echo "> Compile processor binaries for testing" -go build -o "${PROJECT_ROOT}/tmp/test/bin/processor" "${PROJECT_ROOT}/pkg/transport/process/processors" \ No newline at end of file +go build -o "${PROJECT_ROOT}/tmp/test/bin/example-processor" "${PROJECT_ROOT}/pkg/transport/process/processors/example" +go build -o "${PROJECT_ROOT}/tmp/test/bin/sleep-processor" "${PROJECT_ROOT}/pkg/transport/process/processors/sleep" \ No newline at end of file diff --git a/pkg/transport/process/extensions/extensions_suite_test.go b/pkg/transport/process/extensions/extensions_suite_test.go index 5d21c01c..c70458a3 100644 --- a/pkg/transport/process/extensions/extensions_suite_test.go +++ b/pkg/transport/process/extensions/extensions_suite_test.go @@ -7,10 +7,12 @@ import ( "bytes" "context" "encoding/json" + "fmt" "io" "os" "strings" "testing" + "time" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" . "github.com/onsi/ginkgo" @@ -21,7 +23,11 @@ import ( ) const ( - defaultProcessorBinaryPath = "../../../../tmp/test/bin/processor" + exampleProcessorBinaryPath = "../../../../tmp/test/bin/example-processor" + sleepProcessorBinaryPath = "../../../../tmp/test/bin/sleep-processor" + sleepTimeEnv = "SLEEP_TIME" + timeout = 2 * time.Second + sleepTime = 5 * time.Second ) func TestConfig(t *testing.T) { @@ -30,8 +36,11 @@ func TestConfig(t *testing.T) { } var _ = BeforeSuite(func() { - _, err := os.Stat(defaultProcessorBinaryPath) - Expect(err).ToNot(HaveOccurred(), "test processor doesn't exists. pls run make install-requirements.") + _, err := os.Stat(exampleProcessorBinaryPath) + Expect(err).ToNot(HaveOccurred(), exampleProcessorBinaryPath+" doesn't exists. pls run make install-requirements.") + + _, err = os.Stat(sleepProcessorBinaryPath) + Expect(err).ToNot(HaveOccurred(), sleepProcessorBinaryPath+" doesn't exists. pls run make install-requirements.") }, 5) var _ = Describe("transport extensions", func() { @@ -40,10 +49,21 @@ var _ = Describe("transport extensions", func() { It("should modify the processed resource correctly", func() { args := []string{} env := []string{} - processor, err := extensions.NewStdIOExecutable(context.TODO(), defaultProcessorBinaryPath, args, env) + processor, err := extensions.NewStdIOExecutable(exampleProcessorBinaryPath, args, env) Expect(err).ToNot(HaveOccurred()) - testProcessor(processor) + runExampleResourceTest(processor) + }) + + It("should exit with error when timeout is reached", func() { + args := []string{} + env := []string{ + fmt.Sprintf("%s=%s", sleepTimeEnv, sleepTime.String()), + } + processor, err := extensions.NewStdIOExecutable(sleepProcessorBinaryPath, args, env) + Expect(err).ToNot(HaveOccurred()) + + runTimeoutTest(processor) }) }) @@ -51,18 +71,46 @@ var _ = Describe("transport extensions", func() { It("should modify the processed resource correctly", func() { args := []string{} env := []string{} - processor, err := extensions.NewUDSExecutable(context.TODO(), defaultProcessorBinaryPath, args, env) + processor, err := extensions.NewUDSExecutable(exampleProcessorBinaryPath, args, env) Expect(err).ToNot(HaveOccurred()) - testProcessor(processor) + runExampleResourceTest(processor) + }) + + It("should raise an error when trying to set the server address env variable manually", func() { + args := []string{} + env := []string{ + extensions.ServerAddressEnv + "=/tmp/my-processor.sock", + } + _, err := extensions.NewUDSExecutable(exampleProcessorBinaryPath, args, env) + Expect(err).To(MatchError(fmt.Sprintf("the env variable %s is not allowed to be set manually", extensions.ServerAddressEnv))) + }) + + It("should exit with error when timeout is reached", func() { + args := []string{} + env := []string{ + fmt.Sprintf("%s=%s", sleepTimeEnv, sleepTime.String()), + } + processor, err := extensions.NewUDSExecutable(sleepProcessorBinaryPath, args, env) + Expect(err).ToNot(HaveOccurred()) + + runTimeoutTest(processor) }) }) }) -func testProcessor(processor process.ResourceStreamProcessor) { +func runTimeoutTest(processor process.ResourceStreamProcessor) { + ctx, cancelfunc := context.WithTimeout(context.TODO(), timeout) + defer cancelfunc() + + err := processor.Process(ctx, bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{})) + Expect(err).To(MatchError("unable to wait for processor: signal: killed")) +} + +func runExampleResourceTest(processor process.ResourceStreamProcessor) { const ( - processorName = "test-processor" + processorName = "example-processor" resourceData = "12345" expectedResourceData = resourceData + "\n" + processorName ) diff --git a/pkg/transport/process/extensions/stdio_executable.go b/pkg/transport/process/extensions/stdio_executable.go index 6596805e..5ae2b17f 100644 --- a/pkg/transport/process/extensions/stdio_executable.go +++ b/pkg/transport/process/extensions/stdio_executable.go @@ -14,54 +14,54 @@ import ( ) type stdIOExecutable struct { - processor *exec.Cmd - stdin io.WriteCloser - stdout io.Reader + bin string + args []string + env []string } -// NewStdIOExecutable runs a resource processor extension executable in the background. -// It communicates with this processor via stdin/stdout pipes. -func NewStdIOExecutable(ctx context.Context, bin string, args []string, env []string) (process.ResourceStreamProcessor, error) { - cmd := exec.CommandContext(ctx, bin, args...) - cmd.Env = env +// NewStdIOExecutable returns a resource processor extension which runs an executable. +// in the background. It communicates with this processor via stdin/stdout pipes. +func NewStdIOExecutable(bin string, args []string, env []string) (process.ResourceStreamProcessor, error) { + e := stdIOExecutable{ + bin: bin, + args: args, + env: env, + } + + return &e, nil +} + +func (e *stdIOExecutable) Process(ctx context.Context, r io.Reader, w io.Writer) error { + cmd := exec.CommandContext(ctx, e.bin, e.args...) + cmd.Env = e.env stdin, err := cmd.StdinPipe() if err != nil { - return nil, err + return fmt.Errorf("unable to get stdin pipe: %w", err) } stdout, err := cmd.StdoutPipe() if err != nil { - return nil, err + return fmt.Errorf("unable to get stdout pipe: %w", err) } cmd.Stderr = os.Stderr - e := stdIOExecutable{ - processor: cmd, - stdin: stdin, - stdout: stdout, - } - - return &e, nil -} - -func (e *stdIOExecutable) Process(ctx context.Context, r io.Reader, w io.Writer) error { - if err := e.processor.Start(); err != nil { + if err := cmd.Start(); err != nil { return fmt.Errorf("unable to start processor: %w", err) } - if _, err := io.Copy(e.stdin, r); err != nil { + if _, err := io.Copy(stdin, r); err != nil { return fmt.Errorf("unable to write input: %w", err) } - if err := e.stdin.Close(); err != nil { + if err := stdin.Close(); err != nil { return fmt.Errorf("unable to close input writer: %w", err) } - if _, err := io.Copy(w, e.stdout); err != nil { + if _, err := io.Copy(w, stdout); err != nil { return fmt.Errorf("unable to read output: %w", err) } - if err := e.processor.Wait(); err != nil { - return fmt.Errorf("unable to stop processor: %w", err) + if err := cmd.Wait(); err != nil { + return fmt.Errorf("unable to wait for processor: %w", err) } return nil diff --git a/pkg/transport/process/extensions/uds_executable.go b/pkg/transport/process/extensions/uds_executable.go index 6ac31004..eb0dcd97 100644 --- a/pkg/transport/process/extensions/uds_executable.go +++ b/pkg/transport/process/extensions/uds_executable.go @@ -10,6 +10,7 @@ import ( "net" "os" "os/exec" + "strings" "syscall" "time" @@ -17,20 +18,23 @@ import ( "github.com/gardener/component-cli/pkg/utils" ) -const serverAddressFlag = "--addr" +// ServerAddressEnv is the environment variable key which is used for propagating the +// address under which a processor server should start to a processor binary. +const ServerAddressEnv = "SERVER_ADDRESS" type udsExecutable struct { - processor *exec.Cmd - addr string - conn net.Conn + bin string + args []string + env []string + addr string } // NewUDSExecutable runs a resource processor extension executable in the background. // It communicates with this processor via Unix Domain Sockets. -func NewUDSExecutable(ctx context.Context, bin string, args []string, env []string) (process.ResourceStreamProcessor, error) { - for _, arg := range args { - if arg == serverAddressFlag { - return nil, fmt.Errorf("the flag %s is not allowed to be set manually", serverAddressFlag) +func NewUDSExecutable(bin string, args []string, env []string) (process.ResourceStreamProcessor, error) { + for _, e := range env { + if strings.HasPrefix(e, ServerAddressEnv+"=") { + return nil, fmt.Errorf("the env variable %s is not allowed to be set manually", ServerAddressEnv) } } @@ -39,51 +43,52 @@ func NewUDSExecutable(ctx context.Context, bin string, args []string, env []stri return nil, err } addr := fmt.Sprintf("%s/%s.sock", wd, utils.RandomString(8)) - args = append(args, "--addr", addr) + env = append(env, fmt.Sprintf("%s=%s", ServerAddressEnv, addr)) - cmd := exec.CommandContext(ctx, bin, args...) - cmd.Env = env + e := udsExecutable{ + bin: bin, + args: args, + env: env, + addr: addr, + } + + return &e, nil +} + +func (e *udsExecutable) Process(ctx context.Context, r io.Reader, w io.Writer) error { + cmd := exec.CommandContext(ctx, e.bin, e.args...) + cmd.Env = e.env cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { - return nil, fmt.Errorf("unable to start processor: %w", err) + return fmt.Errorf("unable to start processor: %w", err) } - conn, err := tryConnect(addr) + conn, err := tryConnect(e.addr) if err != nil { - return nil, fmt.Errorf("unable to connect to processor: %w", err) + return fmt.Errorf("unable to connect to processor: %w", err) } - e := udsExecutable{ - processor: cmd, - addr: addr, - conn: conn, - } - - return &e, nil -} - -func (e *udsExecutable) Process(ctx context.Context, r io.Reader, w io.Writer) error { - if _, err := io.Copy(e.conn, r); err != nil { + if _, err := io.Copy(conn, r); err != nil { return fmt.Errorf("unable to write input: %w", err) } - usock := e.conn.(*net.UnixConn) + usock := conn.(*net.UnixConn) if err := usock.CloseWrite(); err != nil { return fmt.Errorf("unable to close input writer: %w", err) } - if _, err := io.Copy(w, e.conn); err != nil { + if _, err := io.Copy(w, conn); err != nil { return fmt.Errorf("unable to read output: %w", err) } - if err := e.processor.Process.Signal(syscall.SIGTERM); err != nil { + if err := cmd.Process.Signal(syscall.SIGTERM); err != nil { return fmt.Errorf("unable to send SIGTERM to processor: %w", err) } // extension servers must implement ordinary shutdown (!) - if err := e.processor.Wait(); err != nil { + if err := cmd.Wait(); err != nil { return fmt.Errorf("unable to wait for processor: %w", err) } diff --git a/pkg/transport/process/pipeline.go b/pkg/transport/process/pipeline.go index 77941189..41dd01fe 100644 --- a/pkg/transport/process/pipeline.go +++ b/pkg/transport/process/pipeline.go @@ -7,6 +7,7 @@ import ( "context" "io" "os" + "time" "fmt" "io/ioutil" @@ -14,6 +15,8 @@ import ( cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" ) +const processorTimeout = 30 * time.Second + type resourceProcessingPipelineImpl struct { processors []ResourceStreamProcessor } @@ -66,6 +69,9 @@ func (p *resourceProcessingPipelineImpl) process(ctx context.Context, infile *os inreader := infile outwriter := outfile + ctx, cancelfunc := context.WithTimeout(ctx, processorTimeout) + defer cancelfunc() + if err := proc.Process(ctx, inreader, outwriter); err != nil { return nil, fmt.Errorf("unable to process resource: %w", err) } diff --git a/pkg/transport/process/processors/test.go b/pkg/transport/process/processors/example/main.go similarity index 57% rename from pkg/transport/process/processors/test.go rename to pkg/transport/process/processors/example/main.go index 2ecc0b9c..f5c66780 100644 --- a/pkg/transport/process/processors/test.go +++ b/pkg/transport/process/processors/example/main.go @@ -6,101 +6,44 @@ package main import ( "bytes" "encoding/json" - "flag" "fmt" "io" "io/ioutil" "log" - "net" "os" "os/signal" "strings" - "sync" "syscall" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/extensions" + "github.com/gardener/component-cli/pkg/transport/process/processors" ) -const processorName = "test-processor" - -type ProcessorHandlerFunc func(io.Reader, io.WriteCloser) - -type Server struct { - listener net.Listener - quit chan interface{} - wg sync.WaitGroup - handler ProcessorHandlerFunc -} - -func NewServer(addr string, h ProcessorHandlerFunc) (*Server, error) { - l, err := net.Listen("unix", addr) - if err != nil { - return nil, err - } - s := &Server{ - quit: make(chan interface{}), - listener: l, - handler: h, - } - return s, nil -} - -func (s *Server) Start() { - s.wg.Add(1) - go s.serve() -} - -func (s *Server) serve() { - defer s.wg.Done() - - for { - conn, err := s.listener.Accept() - if err != nil { - select { - case <-s.quit: - return - default: - log.Println("accept error", err) - } - } else { - s.wg.Add(1) - go func() { - defer s.wg.Done() - s.handler(conn, conn) - }() - } - } -} - -func (s *Server) Stop() { - close(s.quit) - if err := s.listener.Close(); err != nil { - println(err) - } - s.wg.Wait() -} +const processorName = "example-processor" +// a test processor which adds its name to the resource labels and the resource blob. +// the resource blob is expected to be plain text data. func main() { - addr := flag.String("addr", "", "") - flag.Parse() + addr := os.Getenv(extensions.ServerAddressEnv) - if *addr == "" { + if addr == "" { // if addr is not set, use stdin/stdout for communication - if err := ProcessorRoutine(os.Stdin, os.Stdout); err != nil { + if err := processorRoutine(os.Stdin, os.Stdout); err != nil { log.Fatal(err) } return } h := func(r io.Reader, w io.WriteCloser) { - if err := ProcessorRoutine(r, w); err != nil { + if err := processorRoutine(r, w); err != nil { log.Fatal(err) } } - srv, err := NewServer(*addr, h) + srv, err := processors.NewUDSServer(addr, h) if err != nil { log.Fatal(err) } @@ -114,7 +57,7 @@ func main() { srv.Stop() } -func ProcessorRoutine(inputStream io.Reader, outputStream io.WriteCloser) error { +func processorRoutine(inputStream io.Reader, outputStream io.WriteCloser) error { defer outputStream.Close() tmpfile, err := ioutil.TempFile("", "") diff --git a/pkg/transport/process/processors/sleep/main.go b/pkg/transport/process/processors/sleep/main.go new file mode 100644 index 00000000..477c8c0c --- /dev/null +++ b/pkg/transport/process/processors/sleep/main.go @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package main + +import ( + "io" + "log" + "os" + "os/signal" + "syscall" + "time" + + "github.com/gardener/component-cli/pkg/transport/process/extensions" + "github.com/gardener/component-cli/pkg/transport/process/processors" +) + +const sleepTimeEnv = "SLEEP_TIME" + +// a test processor which sleeps for a configurable duration and then exists with an error. +func main() { + sleepTime, err := time.ParseDuration(os.Getenv(sleepTimeEnv)) + if err != nil { + log.Fatal(err) + } + + addr := os.Getenv(extensions.ServerAddressEnv) + + if addr == "" { + time.Sleep(sleepTime) + log.Fatal("finished sleeping -> exit with error") + } + + h := func(r io.Reader, w io.WriteCloser) { + time.Sleep(sleepTime) + log.Fatal("finished sleeping -> exit with error") + } + + srv, err := processors.NewUDSServer(addr, h) + if err != nil { + log.Fatal(err) + } + + srv.Start() + + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + <-stop + + srv.Stop() +} diff --git a/pkg/transport/process/processors/util.go b/pkg/transport/process/processors/util.go new file mode 100644 index 00000000..8109721c --- /dev/null +++ b/pkg/transport/process/processors/util.go @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package processors + +import ( + "io" + "log" + "net" + "sync" +) + +type ProcessorHandlerFunc func(io.Reader, io.WriteCloser) + +type UDSServer struct { + listener net.Listener + quit chan interface{} + wg sync.WaitGroup + handler ProcessorHandlerFunc +} + +func NewUDSServer(addr string, h ProcessorHandlerFunc) (*UDSServer, error) { + l, err := net.Listen("unix", addr) + if err != nil { + return nil, err + } + s := &UDSServer{ + quit: make(chan interface{}), + listener: l, + handler: h, + } + return s, nil +} + +func (s *UDSServer) Start() { + s.wg.Add(1) + go s.serve() +} + +func (s *UDSServer) serve() { + defer s.wg.Done() + + for { + conn, err := s.listener.Accept() + if err != nil { + select { + case <-s.quit: + return + default: + log.Println("accept error", err) + } + } else { + s.wg.Add(1) + go func() { + defer s.wg.Done() + s.handler(conn, conn) + }() + } + } +} + +func (s *UDSServer) Stop() { + close(s.quit) + if err := s.listener.Close(); err != nil { + println(err) + } + s.wg.Wait() +} diff --git a/pkg/transport/process/util_test.go b/pkg/transport/process/util_test.go index b10b596f..91f5b569 100644 --- a/pkg/transport/process/util_test.go +++ b/pkg/transport/process/util_test.go @@ -15,7 +15,7 @@ import ( "github.com/gardener/component-cli/pkg/transport/process" ) -var _ = Describe("utils", func() { +var _ = Describe("util", func() { Context("WriteProcessMessage & ReadProcessMessage", func() { From acc06bedcf4594c0430b7c17e12fb0082f12e9bc Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 21 Sep 2021 16:22:09 +0200 Subject: [PATCH 18/94] refactoring + updates doc --- pkg/transport/process/extensions/extensions_suite_test.go | 3 ++- pkg/transport/process/types.go | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/transport/process/extensions/extensions_suite_test.go b/pkg/transport/process/extensions/extensions_suite_test.go index c70458a3..1b38dfff 100644 --- a/pkg/transport/process/extensions/extensions_suite_test.go +++ b/pkg/transport/process/extensions/extensions_suite_test.go @@ -26,7 +26,6 @@ const ( exampleProcessorBinaryPath = "../../../../tmp/test/bin/example-processor" sleepProcessorBinaryPath = "../../../../tmp/test/bin/sleep-processor" sleepTimeEnv = "SLEEP_TIME" - timeout = 2 * time.Second sleepTime = 5 * time.Second ) @@ -101,6 +100,8 @@ var _ = Describe("transport extensions", func() { }) func runTimeoutTest(processor process.ResourceStreamProcessor) { + const timeout = 2 * time.Second + ctx, cancelfunc := context.WithTimeout(context.TODO(), timeout) defer cancelfunc() diff --git a/pkg/transport/process/types.go b/pkg/transport/process/types.go index 6e970285..1004f940 100644 --- a/pkg/transport/process/types.go +++ b/pkg/transport/process/types.go @@ -23,7 +23,8 @@ type ResourceProcessingPipeline interface { // ResourceStreamProcessor describes an individual processor for processing a resource. // A processor can upload, modify, or download a resource. type ResourceStreamProcessor interface { - // Process executes the processor for a resource. Input and Output streams must be TAR - // archives which contain the component descriptor, resource, and resource blob. + // Process executes the processor for a resource. Input and Output streams must be + // compliant to a specific format ("processor message"). See also ./util.go for helper + // functions to read/write processor messages. Process(context.Context, io.Reader, io.Writer) error } From e41fd557fd78b8bc24246eae4dc52ab34fe1cfdf Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 22 Sep 2021 14:53:23 +0200 Subject: [PATCH 19/94] refactoring, adds pipeline tests, adds labelling processor --- pkg/transport/process/pipeline.go | 4 +- pkg/transport/process/pipeline_test.go | 63 +++++++++++++++++++ pkg/transport/process/processors/labelling.go | 44 +++++++++++++ .../process/processors/labelling_test.go | 4 ++ pkg/transport/process/types.go | 2 +- pkg/transport/process/util.go | 10 +-- pkg/transport/process/util_test.go | 4 +- 7 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 pkg/transport/process/pipeline_test.go create mode 100644 pkg/transport/process/processors/labelling.go create mode 100644 pkg/transport/process/processors/labelling_test.go diff --git a/pkg/transport/process/pipeline.go b/pkg/transport/process/pipeline.go index 41dd01fe..40f9ef35 100644 --- a/pkg/transport/process/pipeline.go +++ b/pkg/transport/process/pipeline.go @@ -49,7 +49,9 @@ func (p *resourceProcessingPipelineImpl) Process(ctx context.Context, cd cdv2.Co if err != nil { return nil, cdv2.Resource{}, fmt.Errorf("unable to read output data: %w", err) } - defer blobreader.Close() + if blobreader != nil { + defer blobreader.Close() + } return processedCD, processedRes, nil } diff --git a/pkg/transport/process/pipeline_test.go b/pkg/transport/process/pipeline_test.go new file mode 100644 index 00000000..c0a0f4c5 --- /dev/null +++ b/pkg/transport/process/pipeline_test.go @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package process_test + +import ( + "context" + "encoding/json" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/processors" +) + +var _ = Describe("pipeline", func() { + + Context("Process", func() { + + It("should correctly process resource", func() { + res := cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "my-res", + Version: "v0.1.0", + Type: "ociImage", + }, + } + + l1 := cdv2.Label{ + Name: "processor-0", + Value: json.RawMessage(`"true"`), + } + l2 := cdv2.Label{ + Name: "processor-1", + Value: json.RawMessage(`"true"`), + } + expectedRes := res + expectedRes.Labels = append(expectedRes.Labels, l1) + expectedRes.Labels = append(expectedRes.Labels, l2) + + cd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + Resources: []cdv2.Resource{ + res, + }, + }, + } + + p1 := processors.NewLabellingProcessor(l1) + p2 := processors.NewLabellingProcessor(l2) + pipeline := process.NewResourceProcessingPipeline(p1, p2) + + actualCD, actualRes, err := pipeline.Process(context.TODO(), cd, res) + Expect(err).ToNot(HaveOccurred()) + + Expect(*actualCD).To(Equal(cd)) + Expect(actualRes).To(Equal(expectedRes)) + }) + + }) +}) diff --git a/pkg/transport/process/processors/labelling.go b/pkg/transport/process/processors/labelling.go new file mode 100644 index 00000000..7cc17e39 --- /dev/null +++ b/pkg/transport/process/processors/labelling.go @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier +package processors + +import ( + "context" + "fmt" + "io" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + + "github.com/gardener/component-cli/pkg/transport/process" +) + +type labellingProcessor struct { + labels cdv2.Labels +} + +// NewLabellingProcessor returns a processor that appends one or more labels to a resource +func NewLabellingProcessor(labels ...cdv2.Label) process.ResourceStreamProcessor { + obj := labellingProcessor{ + labels: labels, + } + return &obj +} + +func (p *labellingProcessor) Process(ctx context.Context, r io.Reader, w io.Writer) error { + cd, res, resBlobReader, err := process.ReadProcessorMessage(r) + if err != nil { + return fmt.Errorf("unable to read processor message: %w", err) + } + if resBlobReader != nil { + defer resBlobReader.Close() + } + + res.Labels = append(res.Labels, p.labels...) + + if err := process.WriteProcessorMessage(*cd, res, resBlobReader, w); err != nil { + return fmt.Errorf("unable to write processor message: %w", err) + } + + return nil +} diff --git a/pkg/transport/process/processors/labelling_test.go b/pkg/transport/process/processors/labelling_test.go new file mode 100644 index 00000000..556ce187 --- /dev/null +++ b/pkg/transport/process/processors/labelling_test.go @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier +package processors_test diff --git a/pkg/transport/process/types.go b/pkg/transport/process/types.go index 1004f940..9889d07e 100644 --- a/pkg/transport/process/types.go +++ b/pkg/transport/process/types.go @@ -24,7 +24,7 @@ type ResourceProcessingPipeline interface { // A processor can upload, modify, or download a resource. type ResourceStreamProcessor interface { // Process executes the processor for a resource. Input and Output streams must be - // compliant to a specific format ("processor message"). See also ./util.go for helper + // compliant to a specific format ("processor message"). See also ./util.go for helper // functions to read/write processor messages. Process(context.Context, io.Reader, io.Writer) error } diff --git a/pkg/transport/process/util.go b/pkg/transport/process/util.go index 880b6a6b..c4a2268f 100644 --- a/pkg/transport/process/util.go +++ b/pkg/transport/process/util.go @@ -138,10 +138,12 @@ func ReadProcessorMessage(r io.Reader) (*cdv2.ComponentDescriptor, cdv2.Resource } } - if f != nil { - if _, err := f.Seek(0, io.SeekStart); err != nil { - return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to seek to beginning of file: %w", err) - } + if f == nil { + return cd, res, nil, nil + } + + if _, err := f.Seek(0, io.SeekStart); err != nil { + return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to seek to beginning of file: %w", err) } return cd, res, f, nil diff --git a/pkg/transport/process/util_test.go b/pkg/transport/process/util_test.go index 91f5b569..c668e5cb 100644 --- a/pkg/transport/process/util_test.go +++ b/pkg/transport/process/util_test.go @@ -17,9 +17,9 @@ import ( var _ = Describe("util", func() { - Context("WriteProcessMessage & ReadProcessMessage", func() { + Context("WriteProcessorMessage & ReadProcessorMessage", func() { - It("should correctly write and read a process message", func() { + It("should correctly write and read a processor message", func() { res := cdv2.Resource{ IdentityObjectMeta: cdv2.IdentityObjectMeta{ Name: "my-res", From d343d8c57b02f96203f6c3ba870fef8444e9d441 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 22 Sep 2021 15:28:41 +0200 Subject: [PATCH 20/94] wip --- .../process/download/local_oci_blob.go | 80 ------------------- pkg/transport/process/download/oci_image.go | 73 ----------------- .../stdio_executable.go | 0 .../uds_executable.go | 0 pkg/transport/process/pipeline_test.go | 63 +++++++++++++++ .../tar_archive_file_filter.go | 2 +- .../{upload => uploaders}/local_oci_blob.go | 0 .../process/{upload => uploaders}/util.go | 0 8 files changed, 64 insertions(+), 154 deletions(-) delete mode 100644 pkg/transport/process/download/local_oci_blob.go delete mode 100644 pkg/transport/process/download/oci_image.go rename pkg/transport/process/{extension => extensions}/stdio_executable.go (100%) rename pkg/transport/process/{extension => extensions}/uds_executable.go (100%) create mode 100644 pkg/transport/process/pipeline_test.go rename pkg/transport/process/{process => processors}/tar_archive_file_filter.go (98%) rename pkg/transport/process/{upload => uploaders}/local_oci_blob.go (100%) rename pkg/transport/process/{upload => uploaders}/util.go (100%) diff --git a/pkg/transport/process/download/local_oci_blob.go b/pkg/transport/process/download/local_oci_blob.go deleted file mode 100644 index 34c14b2f..00000000 --- a/pkg/transport/process/download/local_oci_blob.go +++ /dev/null @@ -1,80 +0,0 @@ -package download - -import ( - "archive/tar" - "context" - "fmt" - "io" - "io/ioutil" - - "github.com/gardener/component-cli/ociclient" - "github.com/gardener/component-cli/pkg/transport/process" - cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" - cdoci "github.com/gardener/component-spec/bindings-go/oci" -) - -type localOCIBlobDownloader struct { - client ociclient.Client -} - -func NewLocalOCIBlobDownloader(client ociclient.Client) process.ResourceStreamProcessor { - obj := localOCIBlobDownloader{ - client: client, - } - return &obj -} - -func (d *localOCIBlobDownloader) Process(ctx context.Context, r io.Reader, w io.Writer) error { - cd, res, _, err := process.ReadArchive(tar.NewReader(r)) - if err != nil { - return fmt.Errorf("unable to read input archive: %w", err) - } - - if res.Access.GetType() != cdv2.LocalOCIBlobType { - return fmt.Errorf("unsupported access type: %+v", res.Access) - } - - tmpfile, err := ioutil.TempFile("", "") - if err != nil { - return fmt.Errorf("unable to create tempfile: %w", err) - } - defer tmpfile.Close() - - err = d.fetchLocalOCIBlob(ctx, cd, res, tmpfile) - if err != nil { - return fmt.Errorf("unable to fetch blob: %w", err) - } - - _, err = tmpfile.Seek(0, 0) - if err != nil { - return fmt.Errorf("unable to seek to beginning of tempfile: %w", err) - } - - err = process.WriteArchive(ctx, cd, res, tmpfile, tar.NewWriter(w)) - if err != nil { - return fmt.Errorf("unable to write output archive: %w", err) - } - - return nil -} - -func (d *localOCIBlobDownloader) fetchLocalOCIBlob(ctx context.Context, cd *cdv2.ComponentDescriptor, res cdv2.Resource, w io.Writer) error { - repoctx := cdv2.OCIRegistryRepository{} - err := cd.GetEffectiveRepositoryContext().DecodeInto(&repoctx) - if err != nil { - return fmt.Errorf("unable to decode repository context: %w", err) - } - - resolver := cdoci.NewResolver(d.client) - _, blobResolver, err := resolver.ResolveWithBlobResolver(ctx, &repoctx, cd.Name, cd.Version) - if err != nil { - return fmt.Errorf("unable to resolve component descriptor: %w", err) - } - - _, err = blobResolver.Resolve(ctx, res, w) - if err != nil { - return fmt.Errorf("unable to to resolve blob: %w", err) - } - - return nil -} diff --git a/pkg/transport/process/download/oci_image.go b/pkg/transport/process/download/oci_image.go deleted file mode 100644 index 6cd1dde3..00000000 --- a/pkg/transport/process/download/oci_image.go +++ /dev/null @@ -1,73 +0,0 @@ -package download - -import ( - "archive/tar" - "context" - "fmt" - "io" - - "github.com/gardener/component-cli/ociclient" - "github.com/gardener/component-cli/ociclient/oci" - "github.com/gardener/component-cli/pkg/transport/process" - cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" -) - -type ociImageDownloader struct { - client ociclient.Client -} - -func NewOCIImageDownloader(client ociclient.Client) process.ResourceStreamProcessor { - obj := ociImageDownloader{ - client: client, - } - return &obj -} - -func (d *ociImageDownloader) Process(ctx context.Context, r io.Reader, w io.Writer) error { - cd, res, _, err := process.ReadArchive(tar.NewReader(r)) - if err != nil { - return fmt.Errorf("unable to read input archive: %w", err) - } - - if res.Access.GetType() != cdv2.OCIRegistryType { - return fmt.Errorf("unsupported acces type: %+v", res.Access) - } - - if res.Type != cdv2.OCIImageType { - return fmt.Errorf("unsupported resource type: %s", res.Type) - } - - ociAccess := &cdv2.OCIRegistryAccess{} - if err := res.Access.DecodeInto(ociAccess); err != nil { - return fmt.Errorf("unable to decode resource access: %w", err) - } - - ociArtifact, err := d.client.GetOCIArtifact(ctx, ociAccess.ImageReference) - if err != nil { - return fmt.Errorf("unable to get oci artifact: %w", err) - } - - if ociArtifact.IsIndex() { - handleImageIndex() - } else { - handleImage() - } - - return nil -} - -func handleImageIndex(index *oci.Index) { - - for _, m := range index.Manifests { - - } - - artifact. -} - -func handleImage() { - err := process.WriteArchive(ctx, cd, res, tmpfile, tar.NewWriter(w)) - if err != nil { - return fmt.Errorf("unable to write output archive: %w", err) - } -} diff --git a/pkg/transport/process/extension/stdio_executable.go b/pkg/transport/process/extensions/stdio_executable.go similarity index 100% rename from pkg/transport/process/extension/stdio_executable.go rename to pkg/transport/process/extensions/stdio_executable.go diff --git a/pkg/transport/process/extension/uds_executable.go b/pkg/transport/process/extensions/uds_executable.go similarity index 100% rename from pkg/transport/process/extension/uds_executable.go rename to pkg/transport/process/extensions/uds_executable.go diff --git a/pkg/transport/process/pipeline_test.go b/pkg/transport/process/pipeline_test.go new file mode 100644 index 00000000..c0a0f4c5 --- /dev/null +++ b/pkg/transport/process/pipeline_test.go @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package process_test + +import ( + "context" + "encoding/json" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/processors" +) + +var _ = Describe("pipeline", func() { + + Context("Process", func() { + + It("should correctly process resource", func() { + res := cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "my-res", + Version: "v0.1.0", + Type: "ociImage", + }, + } + + l1 := cdv2.Label{ + Name: "processor-0", + Value: json.RawMessage(`"true"`), + } + l2 := cdv2.Label{ + Name: "processor-1", + Value: json.RawMessage(`"true"`), + } + expectedRes := res + expectedRes.Labels = append(expectedRes.Labels, l1) + expectedRes.Labels = append(expectedRes.Labels, l2) + + cd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + Resources: []cdv2.Resource{ + res, + }, + }, + } + + p1 := processors.NewLabellingProcessor(l1) + p2 := processors.NewLabellingProcessor(l2) + pipeline := process.NewResourceProcessingPipeline(p1, p2) + + actualCD, actualRes, err := pipeline.Process(context.TODO(), cd, res) + Expect(err).ToNot(HaveOccurred()) + + Expect(*actualCD).To(Equal(cd)) + Expect(actualRes).To(Equal(expectedRes)) + }) + + }) +}) diff --git a/pkg/transport/process/process/tar_archive_file_filter.go b/pkg/transport/process/processors/tar_archive_file_filter.go similarity index 98% rename from pkg/transport/process/process/tar_archive_file_filter.go rename to pkg/transport/process/processors/tar_archive_file_filter.go index 5a5a11d1..b2ea2e85 100644 --- a/pkg/transport/process/process/tar_archive_file_filter.go +++ b/pkg/transport/process/processors/tar_archive_file_filter.go @@ -1,4 +1,4 @@ -package builtin +package processors import ( "archive/tar" diff --git a/pkg/transport/process/upload/local_oci_blob.go b/pkg/transport/process/uploaders/local_oci_blob.go similarity index 100% rename from pkg/transport/process/upload/local_oci_blob.go rename to pkg/transport/process/uploaders/local_oci_blob.go diff --git a/pkg/transport/process/upload/util.go b/pkg/transport/process/uploaders/util.go similarity index 100% rename from pkg/transport/process/upload/util.go rename to pkg/transport/process/uploaders/util.go From 93933fac3f67fb4a500dfb37568dab803915a21d Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 22 Sep 2021 15:42:20 +0200 Subject: [PATCH 21/94] restores deleted files and fixes compile errors --- pkg/commands/transport/transport.go | 16 ++-- .../process/downloaders/local_oci_blob.go | 82 +++++++++++++++++++ .../process/downloaders/oci_image.go | 76 +++++++++++++++++ .../processors/tar_archive_file_filter.go | 4 +- .../process/uploaders/local_oci_blob.go | 10 ++- pkg/transport/process/uploaders/util.go | 5 +- 6 files changed, 177 insertions(+), 16 deletions(-) create mode 100644 pkg/transport/process/downloaders/local_oci_blob.go create mode 100644 pkg/transport/process/downloaders/oci_image.go diff --git a/pkg/commands/transport/transport.go b/pkg/commands/transport/transport.go index ca165244..1cb44087 100644 --- a/pkg/commands/transport/transport.go +++ b/pkg/commands/transport/transport.go @@ -7,7 +7,6 @@ import ( "os" "path/filepath" "sync" - "time" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" cdoci "github.com/gardener/component-spec/bindings-go/oci" @@ -23,9 +22,9 @@ import ( "github.com/gardener/component-cli/pkg/commands/constants" "github.com/gardener/component-cli/pkg/logger" "github.com/gardener/component-cli/pkg/transport/process" - "github.com/gardener/component-cli/pkg/transport/process/download" - "github.com/gardener/component-cli/pkg/transport/process/extension" - "github.com/gardener/component-cli/pkg/transport/process/upload" + "github.com/gardener/component-cli/pkg/transport/process/downloaders" + "github.com/gardener/component-cli/pkg/transport/process/extensions" + "github.com/gardener/component-cli/pkg/transport/process/uploaders" ) const ( @@ -139,7 +138,6 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e fmt.Println("waiting for goroutines to finish") wg.Wait() - fmt.Println("avg_duration =", process.TotalTime/time.Millisecond/parallelRuns, "ms") fmt.Println("main finished") return nil @@ -164,7 +162,7 @@ func handleResources(ctx context.Context, cd *cdv2.ComponentDescriptor, targetCt return } - pip, err := process.NewResourceProcessingPipeline(procs...) + pip := process.NewResourceProcessingPipeline(procs...) if err != nil { errs = append(errs, fmt.Errorf("unable to create pipeline: %w", err)) return @@ -244,18 +242,18 @@ func createProcessors(client ociclient.Client, targetCtx cdv2.OCIRegistryReposit } procs := []process.ResourceStreamProcessor{ - download.NewLocalOCIBlobDownloader(client), + downloaders.NewLocalOCIBlobDownloader(client), } for _, procBin := range procBins { - exec, err := extension.NewStdIOExecutable(context.TODO(), procBin) + exec, err := extensions.NewStdIOExecutable(procBin, []string{}, []string{}) if err != nil { return nil, err } procs = append(procs, exec) } - procs = append(procs, upload.NewLocalOCIBlobUploader(client, targetCtx)) + procs = append(procs, uploaders.NewLocalOCIBlobUploader(client, targetCtx)) return procs, nil } diff --git a/pkg/transport/process/downloaders/local_oci_blob.go b/pkg/transport/process/downloaders/local_oci_blob.go new file mode 100644 index 00000000..0b3043e3 --- /dev/null +++ b/pkg/transport/process/downloaders/local_oci_blob.go @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier +package downloaders + +import ( + "context" + "fmt" + "io" + "io/ioutil" + + "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/pkg/transport/process" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + cdoci "github.com/gardener/component-spec/bindings-go/oci" +) + +type localOCIBlobDownloader struct { + client ociclient.Client +} + +func NewLocalOCIBlobDownloader(client ociclient.Client) process.ResourceStreamProcessor { + obj := localOCIBlobDownloader{ + client: client, + } + return &obj +} + +func (d *localOCIBlobDownloader) Process(ctx context.Context, r io.Reader, w io.Writer) error { + cd, res, _, err := process.ReadProcessorMessage(r) + if err != nil { + return fmt.Errorf("unable to read input archive: %w", err) + } + + if res.Access.GetType() != cdv2.LocalOCIBlobType { + return fmt.Errorf("unsupported access type: %+v", res.Access) + } + + tmpfile, err := ioutil.TempFile("", "") + if err != nil { + return fmt.Errorf("unable to create tempfile: %w", err) + } + defer tmpfile.Close() + + err = d.fetchLocalOCIBlob(ctx, cd, res, tmpfile) + if err != nil { + return fmt.Errorf("unable to fetch blob: %w", err) + } + + _, err = tmpfile.Seek(0, 0) + if err != nil { + return fmt.Errorf("unable to seek to beginning of tempfile: %w", err) + } + + err = process.WriteProcessorMessage(*cd, res, tmpfile, w) + if err != nil { + return fmt.Errorf("unable to write output archive: %w", err) + } + + return nil +} + +func (d *localOCIBlobDownloader) fetchLocalOCIBlob(ctx context.Context, cd *cdv2.ComponentDescriptor, res cdv2.Resource, w io.Writer) error { + repoctx := cdv2.OCIRegistryRepository{} + err := cd.GetEffectiveRepositoryContext().DecodeInto(&repoctx) + if err != nil { + return fmt.Errorf("unable to decode repository context: %w", err) + } + + resolver := cdoci.NewResolver(d.client) + _, blobResolver, err := resolver.ResolveWithBlobResolver(ctx, &repoctx, cd.Name, cd.Version) + if err != nil { + return fmt.Errorf("unable to resolve component descriptor: %w", err) + } + + _, err = blobResolver.Resolve(ctx, res, w) + if err != nil { + return fmt.Errorf("unable to to resolve blob: %w", err) + } + + return nil +} diff --git a/pkg/transport/process/downloaders/oci_image.go b/pkg/transport/process/downloaders/oci_image.go new file mode 100644 index 00000000..6c2e3dad --- /dev/null +++ b/pkg/transport/process/downloaders/oci_image.go @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier +package downloaders + +import ( + "archive/tar" + "context" + "fmt" + "io" + + "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/ociclient/oci" + "github.com/gardener/component-cli/pkg/transport/process" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +type ociImageDownloader struct { + client ociclient.Client +} + +func NewOCIImageDownloader(client ociclient.Client) process.ResourceStreamProcessor { + obj := ociImageDownloader{ + client: client, + } + return &obj +} + +func (d *ociImageDownloader) Process(ctx context.Context, r io.Reader, w io.Writer) error { + cd, res, _, err := process.ReadArchive(tar.NewReader(r)) + if err != nil { + return fmt.Errorf("unable to read input archive: %w", err) + } + + if res.Access.GetType() != cdv2.OCIRegistryType { + return fmt.Errorf("unsupported acces type: %+v", res.Access) + } + + if res.Type != cdv2.OCIImageType { + return fmt.Errorf("unsupported resource type: %s", res.Type) + } + + ociAccess := &cdv2.OCIRegistryAccess{} + if err := res.Access.DecodeInto(ociAccess); err != nil { + return fmt.Errorf("unable to decode resource access: %w", err) + } + + ociArtifact, err := d.client.GetOCIArtifact(ctx, ociAccess.ImageReference) + if err != nil { + return fmt.Errorf("unable to get oci artifact: %w", err) + } + + if ociArtifact.IsIndex() { + handleImageIndex() + } else { + handleImage() + } + + return nil +} + +func handleImageIndex(index *oci.Index) { + + for _, m := range index.Manifests { + + } + + artifact. +} + +func handleImage() { + err := process.WriteArchive(ctx, cd, res, tmpfile, tar.NewWriter(w)) + if err != nil { + return fmt.Errorf("unable to write output archive: %w", err) + } +} diff --git a/pkg/transport/process/processors/tar_archive_file_filter.go b/pkg/transport/process/processors/tar_archive_file_filter.go index b2ea2e85..d2b1d7ff 100644 --- a/pkg/transport/process/processors/tar_archive_file_filter.go +++ b/pkg/transport/process/processors/tar_archive_file_filter.go @@ -15,7 +15,7 @@ type tarArchiveFileFilter struct { } func (f *tarArchiveFileFilter) Process(ctx context.Context, r io.Reader, w io.Writer) error { - cd, res, blobreader, err := process.ReadArchive(tar.NewReader(r)) + cd, res, blobreader, err := process.ReadProcessorMessage(r) if err != nil { return fmt.Errorf("unable to read archive: %w", err) } @@ -24,7 +24,7 @@ func (f *tarArchiveFileFilter) Process(ctx context.Context, r io.Reader, w io.Wr return fmt.Errorf("unable to filter blob: %w", err) } - if err = process.WriteArchive(ctx, cd, res, nil, tar.NewWriter(w)); err != nil { + if err = process.WriteProcessorMessage(*cd, res, nil, w); err != nil { return fmt.Errorf("unable to write archive: %w", err) } diff --git a/pkg/transport/process/uploaders/local_oci_blob.go b/pkg/transport/process/uploaders/local_oci_blob.go index 189c0f6d..5c47e30a 100644 --- a/pkg/transport/process/uploaders/local_oci_blob.go +++ b/pkg/transport/process/uploaders/local_oci_blob.go @@ -1,7 +1,9 @@ -package upload +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier +package uploaders import ( - "archive/tar" "context" "fmt" "io" @@ -29,7 +31,7 @@ func NewLocalOCIBlobUploader(client ociclient.Client, targetCtx cdv2.OCIRegistry } func (d *localOCIBlobUploader) Process(ctx context.Context, r io.Reader, w io.Writer) error { - cd, res, blobreader, err := process.ReadArchive(tar.NewReader(r)) + cd, res, blobreader, err := process.ReadProcessorMessage(r) if err != nil { return fmt.Errorf("unable to read input archive: %w", err) } @@ -86,7 +88,7 @@ func (d *localOCIBlobUploader) Process(ctx context.Context, r io.Reader, w io.Wr return err } - err = process.WriteArchive(ctx, cd, res, tmpfile, tar.NewWriter(w)) + err = process.WriteProcessorMessage(*cd, res, tmpfile, w) if err != nil { return fmt.Errorf("unable to write output archive: %w", err) } diff --git a/pkg/transport/process/uploaders/util.go b/pkg/transport/process/uploaders/util.go index 625818a4..a216bf7d 100644 --- a/pkg/transport/process/uploaders/util.go +++ b/pkg/transport/process/uploaders/util.go @@ -1,4 +1,7 @@ -package upload +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier +package uploaders import ( "fmt" From a5a016ed44993b32a3aa6fbc4f183ff42c4ba4bf Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Mon, 27 Sep 2021 10:21:12 +0200 Subject: [PATCH 22/94] wip --- pkg/commands/transport/transport.go | 12 +- .../process/downloaders/local_oci_blob.go | 17 +-- .../process/downloaders/oci_image.go | 34 ++---- pkg/transport/process/serialize/oci_image.go | 113 ++++++++++++++++++ .../process/uploaders/local_oci_blob.go | 2 +- pkg/transport/process/util.go | 46 +------ pkg/utils/utils.go | 40 +++++++ 7 files changed, 180 insertions(+), 84 deletions(-) create mode 100644 pkg/transport/process/serialize/oci_image.go diff --git a/pkg/commands/transport/transport.go b/pkg/commands/transport/transport.go index 1cb44087..3ffebd27 100644 --- a/pkg/commands/transport/transport.go +++ b/pkg/commands/transport/transport.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 package transport import ( @@ -133,6 +136,7 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e } cd.Resources = processedResources + }() } @@ -172,7 +176,7 @@ func handleResources(ctx context.Context, cd *cdv2.ComponentDescriptor, targetCt // If so, how do we merge the possibly different output of multiple resource pipelines? processedCD, processedRes, err := pip.Process(ctx, *cd, resource) if err != nil { - errs = append(errs, fmt.Errorf("unable to process resource: %w", err)) + errs = append(errs, fmt.Errorf("unable to process resource %+v: %w", resource, err)) return } @@ -236,9 +240,9 @@ func ResolveRecursive(ctx context.Context, client ociclient.Client, baseUrl, com func createProcessors(client ociclient.Client, targetCtx cdv2.OCIRegistryRepository) ([]process.ResourceStreamProcessor, error) { procBins := []string{ - "/Users/i500806/dev/pipeman/bin/processor_1", - "/Users/i500806/dev/pipeman/bin/processor_2", - "/Users/i500806/dev/pipeman/bin/processor_3", + "../../../pipeman/bin/processor_1", + "../../../pipeman/bin/processor_2", + "../../../pipeman/bin/processor_3", } procs := []process.ResourceStreamProcessor{ diff --git a/pkg/transport/process/downloaders/local_oci_blob.go b/pkg/transport/process/downloaders/local_oci_blob.go index 0b3043e3..8c990a6c 100644 --- a/pkg/transport/process/downloaders/local_oci_blob.go +++ b/pkg/transport/process/downloaders/local_oci_blob.go @@ -42,19 +42,16 @@ func (d *localOCIBlobDownloader) Process(ctx context.Context, r io.Reader, w io. } defer tmpfile.Close() - err = d.fetchLocalOCIBlob(ctx, cd, res, tmpfile) - if err != nil { + if err := d.fetchLocalOCIBlob(ctx, cd, res, tmpfile); err != nil { return fmt.Errorf("unable to fetch blob: %w", err) } - _, err = tmpfile.Seek(0, 0) - if err != nil { + if _, err := tmpfile.Seek(0, 0); err != nil { return fmt.Errorf("unable to seek to beginning of tempfile: %w", err) } - err = process.WriteProcessorMessage(*cd, res, tmpfile, w) - if err != nil { - return fmt.Errorf("unable to write output archive: %w", err) + if err := process.WriteProcessorMessage(*cd, res, tmpfile, w); err != nil { + return fmt.Errorf("unable to write processor message: %w", err) } return nil @@ -62,8 +59,7 @@ func (d *localOCIBlobDownloader) Process(ctx context.Context, r io.Reader, w io. func (d *localOCIBlobDownloader) fetchLocalOCIBlob(ctx context.Context, cd *cdv2.ComponentDescriptor, res cdv2.Resource, w io.Writer) error { repoctx := cdv2.OCIRegistryRepository{} - err := cd.GetEffectiveRepositoryContext().DecodeInto(&repoctx) - if err != nil { + if err := cd.GetEffectiveRepositoryContext().DecodeInto(&repoctx); err != nil { return fmt.Errorf("unable to decode repository context: %w", err) } @@ -73,8 +69,7 @@ func (d *localOCIBlobDownloader) fetchLocalOCIBlob(ctx context.Context, cd *cdv2 return fmt.Errorf("unable to resolve component descriptor: %w", err) } - _, err = blobResolver.Resolve(ctx, res, w) - if err != nil { + if _, err := blobResolver.Resolve(ctx, res, w); err != nil { return fmt.Errorf("unable to to resolve blob: %w", err) } diff --git a/pkg/transport/process/downloaders/oci_image.go b/pkg/transport/process/downloaders/oci_image.go index 6c2e3dad..50f99e27 100644 --- a/pkg/transport/process/downloaders/oci_image.go +++ b/pkg/transport/process/downloaders/oci_image.go @@ -4,14 +4,13 @@ package downloaders import ( - "archive/tar" "context" "fmt" "io" "github.com/gardener/component-cli/ociclient" - "github.com/gardener/component-cli/ociclient/oci" "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/serialize" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" ) @@ -27,13 +26,13 @@ func NewOCIImageDownloader(client ociclient.Client) process.ResourceStreamProces } func (d *ociImageDownloader) Process(ctx context.Context, r io.Reader, w io.Writer) error { - cd, res, _, err := process.ReadArchive(tar.NewReader(r)) + cd, res, _, err := process.ReadProcessorMessage(r) if err != nil { return fmt.Errorf("unable to read input archive: %w", err) } if res.Access.GetType() != cdv2.OCIRegistryType { - return fmt.Errorf("unsupported acces type: %+v", res.Access) + return fmt.Errorf("unsupported access type: %+v", res.Access) } if res.Type != cdv2.OCIImageType { @@ -45,32 +44,15 @@ func (d *ociImageDownloader) Process(ctx context.Context, r io.Reader, w io.Writ return fmt.Errorf("unable to decode resource access: %w", err) } - ociArtifact, err := d.client.GetOCIArtifact(ctx, ociAccess.ImageReference) + blobReader, err := serialize.SerializeOCIArtifact(ctx, d.client, ociAccess.ImageReference) if err != nil { - return fmt.Errorf("unable to get oci artifact: %w", err) + return fmt.Errorf("unable to serialize oci artifact: %w", err) } + defer blobReader.Close() - if ociArtifact.IsIndex() { - handleImageIndex() - } else { - handleImage() + if err := process.WriteProcessorMessage(*cd, res, blobReader, w); err != nil { + return fmt.Errorf("unable to write processor message: %w", err) } return nil } - -func handleImageIndex(index *oci.Index) { - - for _, m := range index.Manifests { - - } - - artifact. -} - -func handleImage() { - err := process.WriteArchive(ctx, cd, res, tmpfile, tar.NewWriter(w)) - if err != nil { - return fmt.Errorf("unable to write output archive: %w", err) - } -} diff --git a/pkg/transport/process/serialize/oci_image.go b/pkg/transport/process/serialize/oci_image.go new file mode 100644 index 00000000..2ad3e1b1 --- /dev/null +++ b/pkg/transport/process/serialize/oci_image.go @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier +package serialize + +import ( + "archive/tar" + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "path" + + "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/ociclient/oci" + "github.com/gardener/component-cli/pkg/utils" +) + +func SerializeOCIArtifact(ctx context.Context, client ociclient.Client, ref string) (io.ReadCloser, error) { + ociArtifact, err := client.GetOCIArtifact(ctx, ref) + if err != nil { + return nil, fmt.Errorf("unable to get oci artifact: %w", err) + } + + tmpfile, err := ioutil.TempFile("", "") + if err != nil { + return nil, fmt.Errorf("unable to create tempfile: %w", err) + } + + if ociArtifact.IsIndex() { + if err := serializeImageIndex(ctx, client, ref, ociArtifact.GetIndex(), tmpfile); err != nil { + return nil, fmt.Errorf("unable to serialize image index: %w", err) + } + } else { + if err := serializeImage(ctx, client, ref, ociArtifact.GetManifest(), tar.NewWriter(tmpfile)); err != nil { + return nil, fmt.Errorf("unable to serialize image: %w", err) + } + } + + if _, err := tmpfile.Seek(0, io.SeekStart); err != nil { + return nil, fmt.Errorf("unable to seek to beginning of tempfile: %w", err) + } + + return tmpfile, nil +} + +func serializeImageIndex(ctx context.Context, client ociclient.Client, ref string, index *oci.Index, w io.Writer) error { + tw := tar.NewWriter(w) + defer tw.Close() + + for _, m := range index.Manifests { + if err := serializeImage(ctx, client, ref, m, tw); err != nil { + return fmt.Errorf("unable to serialize image: %w", err) + } + } + + return nil +} + +func serializeImage(ctx context.Context, client ociclient.Client, ref string, manifest *oci.Manifest, tw *tar.Writer) error { + imageFilesPrefix := manifest.Descriptor.Digest.Encoded() + + manifestFile := path.Join(imageFilesPrefix, "manifest.json") + manifestBytes, err := json.Marshal(manifest) + if err != nil { + return fmt.Errorf("unable to marshal manifest: %w", err) + } + + if err := utils.WriteFileToTARArchive(manifestFile, bytes.NewReader(manifestBytes), tw); err != nil { + return fmt.Errorf("unable to write manifest: %w", err) + } + + buf := bytes.NewBuffer([]byte{}) + if err := client.Fetch(ctx, ref, manifest.Data.Config, buf); err != nil { + return fmt.Errorf("unable to fetch config blob: %w", err) + } + + cfgFile := path.Join(imageFilesPrefix, "config.json") + cfgBytes, err := json.Marshal(buf.Bytes()) + if err != nil { + return fmt.Errorf("unable to marshal config: %w", err) + } + + if err := utils.WriteFileToTARArchive(cfgFile, bytes.NewReader(cfgBytes), tw); err != nil { + return fmt.Errorf("unable to write config: %w", err) + } + + layerFilesPrefix := path.Join(imageFilesPrefix, "layers") + for _, layer := range manifest.Data.Layers { + tmpfile, err := ioutil.TempFile("", "") + if err != nil { + return fmt.Errorf("unable to create tempfile: %w", err) + } + defer tmpfile.Close() + + if err := client.Fetch(ctx, ref, layer, tmpfile); err != nil { + return fmt.Errorf("unable to fetch layer blob: %w", err) + } + + if _, err := tmpfile.Seek(0, 0); err != nil { + return fmt.Errorf("unable to seek to beginning of tempfile: %w", err) + } + + layerFile := path.Join(layerFilesPrefix, layer.Digest.Encoded()) + if err := utils.WriteFileToTARArchive(layerFile, tmpfile, tw); err != nil { + return fmt.Errorf("unable to write layer: %w", err) + } + } + + return nil +} diff --git a/pkg/transport/process/uploaders/local_oci_blob.go b/pkg/transport/process/uploaders/local_oci_blob.go index 5c47e30a..0314c2c3 100644 --- a/pkg/transport/process/uploaders/local_oci_blob.go +++ b/pkg/transport/process/uploaders/local_oci_blob.go @@ -90,7 +90,7 @@ func (d *localOCIBlobUploader) Process(ctx context.Context, r io.Reader, w io.Wr err = process.WriteProcessorMessage(*cd, res, tmpfile, w) if err != nil { - return fmt.Errorf("unable to write output archive: %w", err) + return fmt.Errorf("unable to write processor message: %w", err) } return nil diff --git a/pkg/transport/process/util.go b/pkg/transport/process/util.go index c4a2268f..8cfd9f1d 100644 --- a/pkg/transport/process/util.go +++ b/pkg/transport/process/util.go @@ -10,8 +10,8 @@ import ( "io" "io/ioutil" "os" - "time" + "github.com/gardener/component-cli/pkg/utils" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" "sigs.k8s.io/yaml" ) @@ -39,7 +39,7 @@ func WriteProcessorMessage(cd cdv2.ComponentDescriptor, res cdv2.Resource, resou return fmt.Errorf("unable to marshal component descriptor: %w", err) } - if err := writeFileToTARArchive(ComponentDescriptorFile, bytes.NewReader(marshaledCD), tw); err != nil { + if err := utils.WriteFileToTARArchive(ComponentDescriptorFile, bytes.NewReader(marshaledCD), tw); err != nil { return fmt.Errorf("unable to write %s: %w", ComponentDescriptorFile, err) } @@ -48,12 +48,12 @@ func WriteProcessorMessage(cd cdv2.ComponentDescriptor, res cdv2.Resource, resou return fmt.Errorf("unable to marshal resource: %w", err) } - if err := writeFileToTARArchive(ResourceFile, bytes.NewReader(marshaledRes), tw); err != nil { + if err := utils.WriteFileToTARArchive(ResourceFile, bytes.NewReader(marshaledRes), tw); err != nil { return fmt.Errorf("unable to write %s: %w", ResourceFile, err) } if resourceBlobReader != nil { - if err := writeFileToTARArchive(ResourceBlobFile, resourceBlobReader, tw); err != nil { + if err := utils.WriteFileToTARArchive(ResourceBlobFile, resourceBlobReader, tw); err != nil { return fmt.Errorf("unable to write %s: %w", ResourceBlobFile, err) } } @@ -61,44 +61,6 @@ func WriteProcessorMessage(cd cdv2.ComponentDescriptor, res cdv2.Resource, resou return nil } -func writeFileToTARArchive(filename string, contentReader io.Reader, outArchive *tar.Writer) error { - tempfile, err := ioutil.TempFile("", "") - if err != nil { - return fmt.Errorf("unable to create tempfile: %w", err) - } - defer tempfile.Close() - - if _, err := io.Copy(tempfile, contentReader); err != nil { - return fmt.Errorf("unable to write content to file: %w", err) - } - - if _, err := tempfile.Seek(0, io.SeekStart); err != nil { - return fmt.Errorf("unable to seek to beginning of file: %w", err) - } - - fstat, err := tempfile.Stat() - if err != nil { - return fmt.Errorf("unable to get file info: %w", err) - } - - header := tar.Header{ - Name: filename, - Size: fstat.Size(), - Mode: int64(fstat.Mode()), - ModTime: time.Now(), - } - - if err := outArchive.WriteHeader(&header); err != nil { - return fmt.Errorf("unable to write tar header: %w", err) - } - - if _, err := io.Copy(outArchive, tempfile); err != nil { - return fmt.Errorf("unable to write file to tar archive: %w", err) - } - - return nil -} - // ReadProcessorMessage reads the component descriptor, resource and resource blob from a processor message // (tar archive with fixed filenames for component descriptor, resource, and resource blob) which is // produced by processors. The resource blob reader can be nil. If a non-nil value is returned, it must diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 76743d4c..2e97c986 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -11,11 +11,13 @@ import ( "encoding/json" "fmt" "io" + "io/ioutil" "math/rand" "net/http" "os" "path/filepath" "strings" + "time" "github.com/mandelsoft/vfs/pkg/vfs" "github.com/spf13/cobra" @@ -207,3 +209,41 @@ NEXT_FILE: return nil } + +func WriteFileToTARArchive(filename string, contentReader io.Reader, outArchive *tar.Writer) error { + tempfile, err := ioutil.TempFile("", "") + if err != nil { + return fmt.Errorf("unable to create tempfile: %w", err) + } + defer tempfile.Close() + + if _, err := io.Copy(tempfile, contentReader); err != nil { + return fmt.Errorf("unable to write content to file: %w", err) + } + + if _, err := tempfile.Seek(0, io.SeekStart); err != nil { + return fmt.Errorf("unable to seek to beginning of file: %w", err) + } + + fstat, err := tempfile.Stat() + if err != nil { + return fmt.Errorf("unable to get file info: %w", err) + } + + header := tar.Header{ + Name: filename, + Size: fstat.Size(), + Mode: int64(fstat.Mode()), + ModTime: time.Now(), + } + + if err := outArchive.WriteHeader(&header); err != nil { + return fmt.Errorf("unable to write tar header: %w", err) + } + + if _, err := io.Copy(outArchive, tempfile); err != nil { + return fmt.Errorf("unable to write file to tar archive: %w", err) + } + + return nil +} From dd28da330104d1a71d2eb2a184986cdb41511b61 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 5 Oct 2021 13:21:59 +0200 Subject: [PATCH 23/94] wip --- pkg/commands/transport/transport.go | 6 +- .../process/downloaders/oci_image.go | 12 +- .../process/processors/oci_image_filter.go | 125 ++++++++++++++++++ .../processors/tar_archive_file_filter.go | 39 ------ pkg/transport/process/serialize/oci_image.go | 120 +++++++++++------ pkg/transport/process/uploaders/oci_image.go | 73 ++++++++++ pkg/utils/utils.go | 12 +- 7 files changed, 300 insertions(+), 87 deletions(-) create mode 100644 pkg/transport/process/processors/oci_image_filter.go delete mode 100644 pkg/transport/process/processors/tar_archive_file_filter.go create mode 100644 pkg/transport/process/uploaders/oci_image.go diff --git a/pkg/commands/transport/transport.go b/pkg/commands/transport/transport.go index 3ffebd27..13101424 100644 --- a/pkg/commands/transport/transport.go +++ b/pkg/commands/transport/transport.go @@ -240,9 +240,9 @@ func ResolveRecursive(ctx context.Context, client ociclient.Client, baseUrl, com func createProcessors(client ociclient.Client, targetCtx cdv2.OCIRegistryRepository) ([]process.ResourceStreamProcessor, error) { procBins := []string{ - "../../../pipeman/bin/processor_1", - "../../../pipeman/bin/processor_2", - "../../../pipeman/bin/processor_3", + "../../../ctt-playground/bin/processor_1", + "../../../ctt-playground/bin/processor_2", + "../../../ctt-playground/bin/processor_3", } procs := []process.ResourceStreamProcessor{ diff --git a/pkg/transport/process/downloaders/oci_image.go b/pkg/transport/process/downloaders/oci_image.go index 50f99e27..dbead165 100644 --- a/pkg/transport/process/downloaders/oci_image.go +++ b/pkg/transport/process/downloaders/oci_image.go @@ -9,6 +9,7 @@ import ( "io" "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/transport/process/serialize" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" @@ -16,11 +17,13 @@ import ( type ociImageDownloader struct { client ociclient.Client + cache cache.Cache } -func NewOCIImageDownloader(client ociclient.Client) process.ResourceStreamProcessor { +func NewOCIImageDownloader(client ociclient.Client, cache cache.Cache) process.ResourceStreamProcessor { obj := ociImageDownloader{ client: client, + cache: cache, } return &obj } @@ -44,7 +47,12 @@ func (d *ociImageDownloader) Process(ctx context.Context, r io.Reader, w io.Writ return fmt.Errorf("unable to decode resource access: %w", err) } - blobReader, err := serialize.SerializeOCIArtifact(ctx, d.client, ociAccess.ImageReference) + ociArtifact, err := d.client.GetOCIArtifact(ctx, ociAccess.ImageReference) + if err != nil { + return fmt.Errorf("unable to get oci artifact: %w", err) + } + + blobReader, err := serialize.SerializeOCIArtifact(*ociArtifact, d.cache) if err != nil { return fmt.Errorf("unable to serialize oci artifact: %w", err) } diff --git a/pkg/transport/process/processors/oci_image_filter.go b/pkg/transport/process/processors/oci_image_filter.go new file mode 100644 index 00000000..5c144580 --- /dev/null +++ b/pkg/transport/process/processors/oci_image_filter.go @@ -0,0 +1,125 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package processors + +import ( + "archive/tar" + "compress/gzip" + "context" + "fmt" + "io" + "io/ioutil" + + "github.com/gardener/component-cli/ociclient/cache" + "github.com/gardener/component-cli/ociclient/oci" + "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/serialize" + "github.com/gardener/component-cli/pkg/utils" + "github.com/opencontainers/go-digest" + ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +type ociImageFilter struct { + removePatterns []string + cache cache.Cache +} + +func (f *ociImageFilter) Process(ctx context.Context, r io.Reader, w io.Writer) error { + cd, res, blobreader, err := process.ReadProcessorMessage(r) + if err != nil { + return fmt.Errorf("unable to read archive: %w", err) + } + defer blobreader.Close() + + ociArtifact, err := serialize.DeserializeOCIArtifact(blobreader, f.cache) + if err != nil { + return fmt.Errorf("unable to deserialize oci artifact: %w", err) + } + + if ociArtifact.IsIndex() { + filteredImgs := []*oci.Manifest{} + for _, m := range ociArtifact.GetIndex().Manifests { + filteredManifest, err := f.filterImage(*m) + if err != nil { + return fmt.Errorf("unable to filter image %+v: %w", m, err) + } + filteredImgs = append(filteredImgs, filteredManifest) + } + filteredIndex := &oci.Index{ + Manifests: filteredImgs, + Annotations: ociArtifact.GetIndex().Annotations, + } + if err := ociArtifact.SetIndex(filteredIndex); err != nil { + return fmt.Errorf("unable to set index: %w", err) + } + } else { + filteredImg, err := f.filterImage(*ociArtifact.GetManifest()) + if err != nil { + return fmt.Errorf("unable to filter image ") + } + if err := ociArtifact.SetManifest(filteredImg); err != nil { + return fmt.Errorf("unable to set manifest: %w", err) + } + } + + blobReader, err := serialize.SerializeOCIArtifact(*ociArtifact, f.cache) + if err != nil { + return fmt.Errorf("unable to serialice oci artifact: %w", err) + } + + if err = process.WriteProcessorMessage(*cd, res, blobReader, w); err != nil { + return fmt.Errorf("unable to write archive: %w", err) + } + + return nil +} + +func (f *ociImageFilter) filterImage(manifest oci.Manifest) (*oci.Manifest, error) { + filteredLayers := []ocispecv1.Descriptor{} + for _, layer := range manifest.Data.Layers { + layerBlobReader, err := f.cache.Get(layer) + if err != nil { + return nil, err + } + + tmpfile, err := ioutil.TempFile("", "") + if err != nil { + return nil, fmt.Errorf("unable to create tempfile: %w", err) + } + defer tmpfile.Close() + + if layer.MediaType == ocispecv1.MediaTypeImageLayerGzip { + layerBlobReader, err = gzip.NewReader(layerBlobReader) + if err != nil { + return nil, fmt.Errorf("unable to create gzip reader for layer: %w", err) + } + } + + if err = utils.FilterTARArchive(layerBlobReader, tar.NewWriter(tmpfile), f.removePatterns); err != nil { + return nil, fmt.Errorf("unable to filter blob: %w", err) + } + + blobDigest, err := digest.FromReader(tmpfile) + if err != nil { + return nil, fmt.Errorf("unable to calculate digest for layer %+v: %w", layer, err) + } + layer.Digest = blobDigest + + if _, err := tmpfile.Seek(0, io.SeekStart); err != nil { + return nil, fmt.Errorf("unable to reset input file: %s", err) + } + if err := f.cache.Add(layer, tmpfile); err != nil { + return nil, fmt.Errorf("unable to add filtered layer blob to cache: %w", err) + } + } + manifest.Data.Layers = filteredLayers + return &manifest, nil +} + +func NewOCIImageFilter(removePatterns []string) process.ResourceStreamProcessor { + obj := ociImageFilter{ + removePatterns: removePatterns, + } + return &obj +} diff --git a/pkg/transport/process/processors/tar_archive_file_filter.go b/pkg/transport/process/processors/tar_archive_file_filter.go deleted file mode 100644 index d2b1d7ff..00000000 --- a/pkg/transport/process/processors/tar_archive_file_filter.go +++ /dev/null @@ -1,39 +0,0 @@ -package processors - -import ( - "archive/tar" - "context" - "fmt" - "io" - - "github.com/gardener/component-cli/pkg/transport/process" - "github.com/gardener/component-cli/pkg/utils" -) - -type tarArchiveFileFilter struct { - removePatterns []string -} - -func (f *tarArchiveFileFilter) Process(ctx context.Context, r io.Reader, w io.Writer) error { - cd, res, blobreader, err := process.ReadProcessorMessage(r) - if err != nil { - return fmt.Errorf("unable to read archive: %w", err) - } - - if err = utils.FilterTARArchive(blobreader, tar.NewWriter(w), f.removePatterns); err != nil { - return fmt.Errorf("unable to filter blob: %w", err) - } - - if err = process.WriteProcessorMessage(*cd, res, nil, w); err != nil { - return fmt.Errorf("unable to write archive: %w", err) - } - - return nil -} - -func NewTarArchiveFileFilter(removePatterns []string) process.ResourceStreamProcessor { - obj := tarArchiveFileFilter{ - removePatterns: removePatterns, - } - return &obj -} diff --git a/pkg/transport/process/serialize/oci_image.go b/pkg/transport/process/serialize/oci_image.go index 2ad3e1b1..23b4f5f8 100644 --- a/pkg/transport/process/serialize/oci_image.go +++ b/pkg/transport/process/serialize/oci_image.go @@ -6,35 +6,36 @@ package serialize import ( "archive/tar" "bytes" - "context" "encoding/json" "fmt" "io" "io/ioutil" "path" - "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/ociclient/oci" "github.com/gardener/component-cli/pkg/utils" + "github.com/opencontainers/image-spec/specs-go" + ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" ) -func SerializeOCIArtifact(ctx context.Context, client ociclient.Client, ref string) (io.ReadCloser, error) { - ociArtifact, err := client.GetOCIArtifact(ctx, ref) - if err != nil { - return nil, fmt.Errorf("unable to get oci artifact: %w", err) - } +const ( + ManifestFile = "manifest.json" + BlobsDir = "blobs" +) +func SerializeOCIArtifact(ociArtifact oci.Artifact, cache cache.Cache) (io.ReadCloser, error) { tmpfile, err := ioutil.TempFile("", "") if err != nil { return nil, fmt.Errorf("unable to create tempfile: %w", err) } if ociArtifact.IsIndex() { - if err := serializeImageIndex(ctx, client, ref, ociArtifact.GetIndex(), tmpfile); err != nil { + if err := serializeImageIndex(cache, ociArtifact.GetIndex(), tmpfile); err != nil { return nil, fmt.Errorf("unable to serialize image index: %w", err) } } else { - if err := serializeImage(ctx, client, ref, ociArtifact.GetManifest(), tar.NewWriter(tmpfile)); err != nil { + if err := serializeImage(cache, ociArtifact.GetManifest(), ManifestFile, tar.NewWriter(tmpfile)); err != nil { return nil, fmt.Errorf("unable to serialize image: %w", err) } } @@ -46,23 +47,40 @@ func SerializeOCIArtifact(ctx context.Context, client ociclient.Client, ref stri return tmpfile, nil } -func serializeImageIndex(ctx context.Context, client ociclient.Client, ref string, index *oci.Index, w io.Writer) error { +func serializeImageIndex(cache cache.Cache, index *oci.Index, w io.Writer) error { tw := tar.NewWriter(w) defer tw.Close() + descs := []ocispecv1.Descriptor{} for _, m := range index.Manifests { - if err := serializeImage(ctx, client, ref, m, tw); err != nil { + manifestFile := path.Join(BlobsDir, m.Descriptor.Digest.Encoded()) + if err := serializeImage(cache, m, manifestFile, tw); err != nil { return fmt.Errorf("unable to serialize image: %w", err) } + descs = append(descs, m.Descriptor) + } + + i := ocispecv1.Index{ + Versioned: specs.Versioned{ + SchemaVersion: 2, + }, + Manifests: descs, + Annotations: index.Annotations, + } + + indexBytes, err := json.Marshal(i) + if err != nil { + return fmt.Errorf("unable to marshal index manifest: %w", err) + } + + if err := utils.WriteFileToTARArchive(ManifestFile, bytes.NewReader(indexBytes), tw); err != nil { + return fmt.Errorf("unable to write index manifest: %w", err) } return nil } -func serializeImage(ctx context.Context, client ociclient.Client, ref string, manifest *oci.Manifest, tw *tar.Writer) error { - imageFilesPrefix := manifest.Descriptor.Digest.Encoded() - - manifestFile := path.Join(imageFilesPrefix, "manifest.json") +func serializeImage(cache cache.Cache, manifest *oci.Manifest, manifestFile string, tw *tar.Writer) error { manifestBytes, err := json.Marshal(manifest) if err != nil { return fmt.Errorf("unable to marshal manifest: %w", err) @@ -72,42 +90,68 @@ func serializeImage(ctx context.Context, client ociclient.Client, ref string, ma return fmt.Errorf("unable to write manifest: %w", err) } - buf := bytes.NewBuffer([]byte{}) - if err := client.Fetch(ctx, ref, manifest.Data.Config, buf); err != nil { - return fmt.Errorf("unable to fetch config blob: %w", err) - } - - cfgFile := path.Join(imageFilesPrefix, "config.json") - cfgBytes, err := json.Marshal(buf.Bytes()) + configReader, err := cache.Get(manifest.Data.Config) if err != nil { - return fmt.Errorf("unable to marshal config: %w", err) + return fmt.Errorf("unable to get config blob from cache: %w", err) } + defer configReader.Close() - if err := utils.WriteFileToTARArchive(cfgFile, bytes.NewReader(cfgBytes), tw); err != nil { + cfgFile := path.Join(BlobsDir, manifest.Data.Config.Digest.Encoded()) + if err := utils.WriteFileToTARArchive(cfgFile, configReader, tw); err != nil { return fmt.Errorf("unable to write config: %w", err) } - layerFilesPrefix := path.Join(imageFilesPrefix, "layers") for _, layer := range manifest.Data.Layers { - tmpfile, err := ioutil.TempFile("", "") + layerReader, err := cache.Get(layer) if err != nil { - return fmt.Errorf("unable to create tempfile: %w", err) - } - defer tmpfile.Close() - - if err := client.Fetch(ctx, ref, layer, tmpfile); err != nil { - return fmt.Errorf("unable to fetch layer blob: %w", err) + return fmt.Errorf("unable to get layer blob from cache: %w", err) } + defer layerReader.Close() - if _, err := tmpfile.Seek(0, 0); err != nil { - return fmt.Errorf("unable to seek to beginning of tempfile: %w", err) - } - - layerFile := path.Join(layerFilesPrefix, layer.Digest.Encoded()) - if err := utils.WriteFileToTARArchive(layerFile, tmpfile, tw); err != nil { + layerFile := path.Join(BlobsDir, layer.Digest.Encoded()) + if err := utils.WriteFileToTARArchive(layerFile, layerReader, tw); err != nil { return fmt.Errorf("unable to write layer: %w", err) } } return nil } + +func DeserializeOCIArtifact(r io.Reader, cache cache.Cache) (*oci.Artifact, error) { + // tr := tar.NewReader(r) + + // for { + // header, err := tr.Next() + // if err != nil { + // if err == io.EOF { + // break + // } + // return nil, fmt.Errorf("unable to read tar header: %w", err) + // } + + // if header.Name == ManifestFile { + + // } else if strings.HasPrefix(header.Name, BlobsDir) { + // } else { + // return nil, fmt.Errorf() + // } + // } + + // if f == nil { + // return cd, res, nil, nil + // } + + // if _, err := f.Seek(0, io.SeekStart); err != nil { + // return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to seek to beginning of file: %w", err) + // } + + // return cd, res, f, nil + + // desc := ocispecv1.Descriptor{} + + // cache.Add(desc, layerReader) + + // ociArtifact := oci.Artifact{} + + return nil, nil +} diff --git a/pkg/transport/process/uploaders/oci_image.go b/pkg/transport/process/uploaders/oci_image.go new file mode 100644 index 00000000..563c3e62 --- /dev/null +++ b/pkg/transport/process/uploaders/oci_image.go @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier +package uploaders + +import ( + "context" + "fmt" + "io" + + "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/ociclient/cache" + "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/serialize" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +type ociImageUploader struct { + targetURL string + client ociclient.Client + cache cache.Cache +} + +func NewOCIImageUploader(targetURL string, client ociclient.Client, cache cache.Cache) process.ResourceStreamProcessor { + obj := ociImageUploader{ + targetURL: targetURL, + client: client, + cache: cache, + } + return &obj +} + +func (u *ociImageUploader) Process(ctx context.Context, r io.Reader, w io.Writer) error { + cd, res, resBlobReader, err := process.ReadProcessorMessage(r) + if err != nil { + return fmt.Errorf("unable to read input archive: %w", err) + } + defer resBlobReader.Close() + + ociArtifact, err := serialize.DeserializeOCIArtifact(resBlobReader, u.cache) + if err != nil { + return fmt.Errorf("unable to deserialize oci artifact: %w", err) + } + + if res.Access.GetType() != cdv2.OCIRegistryType { + return fmt.Errorf("unsupported access type: %+v", res.Access) + } + + if res.Type != cdv2.OCIImageType { + return fmt.Errorf("unsupported resource type: %s", res.Type) + } + + ociAccess := &cdv2.OCIRegistryAccess{} + if err := res.Access.DecodeInto(ociAccess); err != nil { + return fmt.Errorf("unable to decode resource access: %w", err) + } + + if err := u.client.PushOCIArtifact(ctx, targetRef, ociArtifact, ociclient.WithStore(u.cache)); err != nil { + return fmt.Errorf("unable to push oci artifact: %w", err) + } + + blobReader, err := serialize.SerializeOCIArtifact(*ociArtifact, u.cache) + if err != nil { + return fmt.Errorf("unable to serialize oci artifact: %w", err) + } + defer blobReader.Close() + + if err := process.WriteProcessorMessage(*cd, res, blobReader, w); err != nil { + return fmt.Errorf("unable to write processor message: %w", err) + } + + return nil +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 2e97c986..28903844 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -173,12 +173,14 @@ func BytesString(bytes uint64, accuracy int) string { return fmt.Sprintf("%s %s", stringValue, unit) } -func FilterTARArchive(r *tar.Reader, w *tar.Writer, removePatterns []string) error { - defer w.Close() +func FilterTARArchive(r io.Reader, w io.Writer, removePatterns []string) error { + tr := tar.NewReader(r) + tw := tar.NewWriter(w) + defer tw.Close() NEXT_FILE: for { - header, err := r.Next() + header, err := tr.Next() if err != nil { if err == io.EOF { break @@ -197,11 +199,11 @@ NEXT_FILE: } } - if err := w.WriteHeader(header); err != nil { + if err := tw.WriteHeader(header); err != nil { return fmt.Errorf("unable to write header: %w", err) } - _, err = io.Copy(w, r) + _, err = io.Copy(tw, tr) if err != nil { return fmt.Errorf("unable to write file: %w", err) } From 8acaba7394c684bbee69b3ca1f79e23080ecef05 Mon Sep 17 00:00:00 2001 From: enrico-kaack-comp Date: Wed, 6 Oct 2021 16:19:30 +0200 Subject: [PATCH 24/94] add config parser and pipeline compiler --- cmd/component-cli/app/app.go | 2 + pkg/transport/config/parse.go | 114 ++++++++++++ pkg/transport/config/pipeline.go | 223 +++++++++++++++++++++++ pkg/transport/config/types.go | 59 ++++++ pkg/transport/filter/component_filter.go | 26 +++ 5 files changed, 424 insertions(+) create mode 100644 pkg/transport/config/parse.go create mode 100644 pkg/transport/config/pipeline.go create mode 100644 pkg/transport/config/types.go diff --git a/cmd/component-cli/app/app.go b/cmd/component-cli/app/app.go index 9a6ec9cb..1d6f48f0 100644 --- a/cmd/component-cli/app/app.go +++ b/cmd/component-cli/app/app.go @@ -19,6 +19,7 @@ import ( "github.com/gardener/component-cli/pkg/commands/transport" "github.com/gardener/component-cli/pkg/logcontext" "github.com/gardener/component-cli/pkg/logger" + "github.com/gardener/component-cli/pkg/transport/config" "github.com/gardener/component-cli/pkg/version" "github.com/spf13/cobra" @@ -50,6 +51,7 @@ func NewComponentsCliCommand(ctx context.Context) *cobra.Command { cmd.AddCommand(oci.NewOCICommand(ctx)) cmd.AddCommand(cachecmd.NewCacheCommand(ctx)) cmd.AddCommand(transport.NewTransportCommand(ctx)) + cmd.AddCommand(config.NewConfigParseCommand(ctx)) return cmd } diff --git a/pkg/transport/config/parse.go b/pkg/transport/config/parse.go new file mode 100644 index 00000000..bc49662f --- /dev/null +++ b/pkg/transport/config/parse.go @@ -0,0 +1,114 @@ +package config + +import ( + "context" + "fmt" + "os" + + "github.com/go-logr/logr" + "github.com/mandelsoft/vfs/pkg/osfs" + "github.com/mandelsoft/vfs/pkg/vfs" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "sigs.k8s.io/yaml" + + "github.com/gardener/component-cli/pkg/logger" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +// ParsingOptions defines all options that are used +type ParsingOptions struct { + ConfigPath string +} + +func NewConfigParseCommand(ctx context.Context) *cobra.Command { + opts := &ParsingOptions{} + cmd := &cobra.Command{ + Use: "parse PATH_TO_PROCESSING_CFG", + Args: cobra.RangeArgs(1, 2), + Short: "Parses a processing config.", + Long: ` +`, + Run: func(cmd *cobra.Command, args []string) { + if err := opts.Complete(args); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + if err := opts.Run(ctx, logger.Log, osfs.New()); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + }, + } + opts.AddFlags(cmd.Flags()) + return cmd +} + +func (o *ParsingOptions) AddFlags(fs *pflag.FlagSet) { +} + +func (o *ParsingOptions) Complete(args []string) error { + if len(args) != 1 { + return fmt.Errorf("a path to a config file is required") + } + o.ConfigPath = args[0] + return nil +} + +func (o *ParsingOptions) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) error { + rawConfig, err := os.ReadFile(o.ConfigPath) + if err != nil { + return fmt.Errorf("failed reading config file %w", err) + } + + var config Config + err = yaml.Unmarshal(rawConfig, &config) + if err != nil { + return fmt.Errorf("failed parsing config %w", err) + } + + compiler, err := CompileFromConfig(&config) + if err != nil { + return fmt.Errorf("failed creating lookup table %w", err) + } + fmt.Println(compiler.lookup) + + cd := []cdv2.ComponentDescriptor{ + cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + ObjectMeta: cdv2.ObjectMeta{ + Name: "ComponentDescirptor1", + }, + Resources: []cdv2.Resource{ + cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "MyResource", + }, + }, + }, + }, + }, + cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + ObjectMeta: cdv2.ObjectMeta{ + Name: "ComponentDescirptor2", + }, + Resources: []cdv2.Resource{ + cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "MyResource", + }, + }, + }, + }, + }, + } + + pipeline, err := compiler.CreateResourcePipeline(cd) + if err != nil { + return fmt.Errorf("failed creating pipeline %w", err) + } + fmt.Println(pipeline) + return nil +} diff --git a/pkg/transport/config/pipeline.go b/pkg/transport/config/pipeline.go new file mode 100644 index 00000000..157b7634 --- /dev/null +++ b/pkg/transport/config/pipeline.go @@ -0,0 +1,223 @@ +package config + +import ( + "encoding/json" + "fmt" + + "github.com/gardener/component-cli/pkg/transport/filter" + "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/downloaders" + "github.com/gardener/component-cli/pkg/transport/process/uploaders" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +type ResourcePipeline struct { + Cd *cdv2.ComponentDescriptor + Resource *cdv2.Resource + Downloaders []ProcessorWithName + Processors []ProcessorWithName + Uploaders []ProcessorWithName +} + +type ProcessorWithName struct { + Processor process.ResourceStreamProcessor + Name string +} + +type ProcessorsLookup struct { + downloaders []struct { + ProcessorWithName + filters []filter.Filter + } + processors []ProcessorWithName + + uploaders []struct { + ProcessorWithName + filters []filter.Filter + } + + rules []struct { + name string + processors []string + filters []filter.Filter + } +} + +type ProcessingPipelineCompiler struct { + lookup ProcessorsLookup +} + +// Create a ProcessingPipelineCompiler on the base of a config +func CompileFromConfig(config *Config) (*ProcessingPipelineCompiler, error) { + var lookup ProcessorsLookup + + // downloader + for _, downlaoderDefinition := range config.Downloaders { + if downlaoderDefinition.Type == ExecutableProcessor { + fmt.Println("Not yet implemented") + } else { + downloader := createBuiltInProcessor(string(downlaoderDefinition.Type), downlaoderDefinition.Spec) + filters, err := createFilterList(downlaoderDefinition.Filters) + if err != nil { + return nil, fmt.Errorf("failed creating downloader %s: %w", downlaoderDefinition.Name, err) + } + lookup.downloaders = append(lookup.downloaders, struct { + ProcessorWithName + filters []filter.Filter + }{ProcessorWithName{downloader, downlaoderDefinition.Name}, filters}) + } + } + + // processors + for _, processorsDefinition := range config.Processors { + if processorsDefinition.Type == ExecutableProcessor { + fmt.Println("Not yet implemented") + } else { + processor := createBuiltInProcessor(string(processorsDefinition.Type), processorsDefinition.Spec) + lookup.processors = append(lookup.processors, struct { + Processor process.ResourceStreamProcessor + Name string + }{processor, processorsDefinition.Name}) + } + } + + // uploaders + for _, uploaderDefinition := range config.Uploaders { + if uploaderDefinition.Type == ExecutableProcessor { + fmt.Println("Not yet implemented") + } else { + uploader := createBuiltInProcessor(string(uploaderDefinition.Type), uploaderDefinition.Spec) + filters, err := createFilterList(uploaderDefinition.Filters) + if err != nil { + return nil, fmt.Errorf("failed creating downloader %s: %w", uploaderDefinition.Name, err) + } + lookup.uploaders = append(lookup.uploaders, struct { + ProcessorWithName + filters []filter.Filter + }{ProcessorWithName{uploader, uploaderDefinition.Name}, filters}) + } + } + + // rules + for _, rule := range config.Rules { + var ruleLookup struct { + name string + processors []string + filters []filter.Filter + } + ruleLookup.name = rule.Name + for _, processor := range rule.Processors { + ruleLookup.processors = append(ruleLookup.processors, processor.Name) + } + filters, err := createFilterList(rule.Filters) + if err != nil { + return nil, fmt.Errorf("failed creating rule %s: %w", rule.Name, err) + } + ruleLookup.filters = filters + lookup.rules = append(lookup.rules, ruleLookup) + } + + return &ProcessingPipelineCompiler{ + lookup: lookup, + }, nil +} + +func (c *ProcessingPipelineCompiler) CreateResourcePipeline(cds []cdv2.ComponentDescriptor) ([]ResourcePipeline, error) { + var pipelines []ResourcePipeline + + // loop through all resources + for _, cd := range cds { + for _, res := range cd.Resources { + var pipeline ResourcePipeline + pipeline.Cd = &cd + pipeline.Resource = &res + + // find matching downloader + for _, downloader := range c.lookup.downloaders { + matches := doesAllFilterMatch(downloader.filters, cd, res) + if matches { + pipeline.Downloaders = append(pipeline.Downloaders, ProcessorWithName{downloader.Processor, downloader.Name}) + } + } + + // find matching uploader + for _, uploader := range c.lookup.uploaders { + matches := doesAllFilterMatch(uploader.filters, cd, res) + if matches { + pipeline.Uploaders = append(pipeline.Uploaders, ProcessorWithName{uploader.Processor, uploader.Name}) + } + } + + // loop through all rules to find corresponding processors + for _, rule := range c.lookup.rules { + matches := doesAllFilterMatch(rule.filters, cd, res) + if matches { + for _, processorName := range rule.processors { + processorDefined, err := lookupProcessorByName(processorName, &c.lookup) + if err != nil { + return nil, fmt.Errorf("failed compiling rule %s: %w", rule.name, err) + } + pipeline.Processors = append(pipeline.Processors, ProcessorWithName{processorDefined.Processor, processorDefined.Name}) + } + } + } + pipelines = append(pipelines, pipeline) + } + } + + return pipelines, nil +} + +func doesAllFilterMatch(filters []filter.Filter, cd cdv2.ComponentDescriptor, res cdv2.Resource) bool { + for _, filter := range filters { + if !filter.Matches(&cd, res) { + return false + } + } + return true +} + +func createBuiltInProcessor(builtinType string, spec *json.RawMessage) process.ResourceStreamProcessor { + switch builtinType { + case "LocalOCIBlobDownloader": //TODO: make to constant + // TODO parse config into corresponding config structure + return downloaders.NewLocalOCIBlobDownloader(nil) // TODO: pass correct oci client + case "LocalOCIBlobUploader": + // TODO parse config into corresponding config structure + return uploaders.NewLocalOCIBlobUploader(nil, cdv2.OCIRegistryRepository{}) // TODO: pass correct oci client + } + return nil // TODO: change to error +} + +func createFilterList(filterDefinitions []FilterDefinition) ([]filter.Filter, error) { + var filters []filter.Filter + for _, f := range filterDefinitions { + filter, err := createFilter(f.Type, f.Args) + if err != nil { + return nil, fmt.Errorf("error creating filter list for type %s with args %s: %w", f.Type, string(*f.Args), err) + } + filters = append(filters, filter) + } + return filters, nil +} + +func createFilter(filterType string, args *json.RawMessage) (filter.Filter, error) { + switch filterType { + case "ComponentFilter": // TODO: make constant + filter, err := filter.CreateComponentFilterFromConfig(args) + if err != nil { + return nil, fmt.Errorf("can not create filter %s with provided args", filterType) + } + return filter, nil + } + return nil, fmt.Errorf("can not find filter %s", filterType) +} + +func lookupProcessorByName(name string, lookup *ProcessorsLookup) (*ProcessorWithName, error) { + for _, processor := range lookup.processors { + if processor.Name == name { + return &processor, nil + } + } + return nil, fmt.Errorf("can not find processor %s", name) +} diff --git a/pkg/transport/config/types.go b/pkg/transport/config/types.go new file mode 100644 index 00000000..a312c026 --- /dev/null +++ b/pkg/transport/config/types.go @@ -0,0 +1,59 @@ +package config + +import "encoding/json" + +type Config struct { + Meta string + Version string `json:"version"` + Uploaders []UploaderDefinition `json:"uploaders"` + Processors []ProcessorDefinition `json:"processors"` + Downloaders []DownloaderDefinition `json:"downloaders"` + Rules []Rule `json:"rules"` +} + +type ExtensionType string + +const ( + ExecutableProcessor ExtensionType = "executeable" +) + +type BaseProcessorDefinition struct { + Name string `json:"name"` + Type ExtensionType `json:"type"` + Spec *json.RawMessage `json:"spec"` +} + +type HookDefinition struct { + BaseProcessorDefinition +} + +type FilterDefinition struct { + Type string `json:"type"` + Args *json.RawMessage `json:"args"` +} + +type DownloaderDefinition struct { + BaseProcessorDefinition + Filters []FilterDefinition `json:"filters"` +} + +type UploaderDefinition struct { + BaseProcessorDefinition + Filters []FilterDefinition `json:"filters"` +} + +type ProcessorDefinition struct { + BaseProcessorDefinition +} + +type ProcessorReference struct { + Name string `json:"name"` + Type string `json:"type"` +} + +type Rule struct { + Name string + CopyByReference bool `json:"copyByReference"` + Filters []FilterDefinition `json:"filters"` + Processors []ProcessorReference `json:"processors"` +} diff --git a/pkg/transport/filter/component_filter.go b/pkg/transport/filter/component_filter.go index e83d7072..aaa10ecc 100644 --- a/pkg/transport/filter/component_filter.go +++ b/pkg/transport/filter/component_filter.go @@ -1,10 +1,12 @@ package filter import ( + "encoding/json" "fmt" "regexp" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "k8s.io/apimachinery/pkg/util/yaml" ) type componentFilter struct { @@ -21,6 +23,30 @@ func (f componentFilter) Matches(cd *cdv2.ComponentDescriptor, r cdv2.Resource) return matches } +func CreateComponentFilterFromConfig(rawConfig *json.RawMessage) (Filter, error) { + type RawConfigStruct struct { + IncludeComponentNames []string `json:"includeComponentNames"` + } + var config RawConfigStruct + err := yaml.Unmarshal(*rawConfig, &config) + if err != nil { + return nil, fmt.Errorf("unable to parse filter configuration for ComponentFilter %w", err) + } + icnRegexps := []*regexp.Regexp{} + for _, icn := range config.IncludeComponentNames { + icnRegexp, err := regexp.Compile(icn) + if err != nil { + return nil, fmt.Errorf("unable to parse regexp %s: %w", icn, err) + } + icnRegexps = append(icnRegexps, icnRegexp) + } + + filter := componentFilter{ + includeComponentNames: icnRegexps, + } + return &filter, nil +} + func NewComponentFilter(includeComponentNames ...string) (Filter, error) { icnRegexps := []*regexp.Regexp{} for _, icn := range includeComponentNames { From b8908cba118390482cd2ebce3f062e825481e670 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Fri, 8 Oct 2021 14:50:45 +0200 Subject: [PATCH 25/94] wip --- cmd/component-cli/app/app.go | 1 + pkg/commands/componentarchive/remote/copy.go | 32 +--- .../process/downloaders/oci_image.go | 29 ++++ pkg/transport/process/pipeline.go | 2 +- pkg/transport/process/serialize/oci_image.go | 144 ++++++++++++++---- pkg/transport/process/uploaders/oci_image.go | 24 ++- pkg/utils/utils.go | 29 ++++ 7 files changed, 189 insertions(+), 72 deletions(-) diff --git a/cmd/component-cli/app/app.go b/cmd/component-cli/app/app.go index 1d6f48f0..ce052f58 100644 --- a/cmd/component-cli/app/app.go +++ b/cmd/component-cli/app/app.go @@ -52,6 +52,7 @@ func NewComponentsCliCommand(ctx context.Context) *cobra.Command { cmd.AddCommand(cachecmd.NewCacheCommand(ctx)) cmd.AddCommand(transport.NewTransportCommand(ctx)) cmd.AddCommand(config.NewConfigParseCommand(ctx)) + cmd.AddCommand(transport.NewTestCommand(ctx)) return cmd } diff --git a/pkg/commands/componentarchive/remote/copy.go b/pkg/commands/componentarchive/remote/copy.go index 0ea6ee7f..a09ab171 100644 --- a/pkg/commands/componentarchive/remote/copy.go +++ b/pkg/commands/componentarchive/remote/copy.go @@ -9,7 +9,6 @@ import ( "errors" "fmt" "io" - "net/url" "os" "path" "strings" @@ -25,8 +24,6 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" - "github.com/gardener/component-cli/ociclient/oci" - "github.com/gardener/component-cli/ociclient" "github.com/gardener/component-cli/ociclient/cache" @@ -277,7 +274,7 @@ func (c *Copier) Copy(ctx context.Context, name, version string) error { } // mangle the target artifact name to keep the original image ref somehow readable. - target, err := targetOCIArtifactRef(c.TargetArtifactRepository, ociRegistryAcc.ImageReference, c.KeepSourceRepository) + target, err := utils.TargetOCIArtifactRef(c.TargetArtifactRepository, ociRegistryAcc.ImageReference, c.KeepSourceRepository) if err != nil { return fmt.Errorf("unable to create target oci artifact reference for resource %s: %w", res.Name, err) } @@ -312,7 +309,7 @@ func (c *Copier) Copy(ctx context.Context, name, version string) error { } src := path.Join(c.SourceArtifactRepository, relOCIRegistryAcc.Reference) - target, err := targetOCIArtifactRef(c.TargetArtifactRepository, src, c.KeepSourceRepository) + target, err := utils.TargetOCIArtifactRef(c.TargetArtifactRepository, src, c.KeepSourceRepository) if err != nil { return fmt.Errorf("unable to create target oci artifact reference for resource %s: %w", res.Name, err) } @@ -377,28 +374,3 @@ func (c *Copier) Copy(ctx context.Context, name, version string) error { return nil } - -func targetOCIArtifactRef(targetRepo, ref string, keepOrigHost bool) (string, error) { - if !strings.Contains(targetRepo, "://") { - // add dummy protocol to correctly parse the url - targetRepo = "http://" + targetRepo - } - t, err := url.Parse(targetRepo) - if err != nil { - return "", err - } - parsedRef, err := oci.ParseRef(ref) - if err != nil { - return "", err - } - - if !keepOrigHost { - parsedRef.Host = t.Host - parsedRef.Repository = path.Join(t.Path, parsedRef.Repository) - return parsedRef.String(), nil - } - replacedRef := strings.NewReplacer(".", "_", ":", "_").Replace(parsedRef.Name()) - parsedRef.Repository = path.Join(t.Path, replacedRef) - parsedRef.Host = t.Host - return parsedRef.String(), nil -} diff --git a/pkg/transport/process/downloaders/oci_image.go b/pkg/transport/process/downloaders/oci_image.go index dbead165..a288c0d4 100644 --- a/pkg/transport/process/downloaders/oci_image.go +++ b/pkg/transport/process/downloaders/oci_image.go @@ -4,6 +4,7 @@ package downloaders import ( + "bytes" "context" "fmt" "io" @@ -13,6 +14,7 @@ import ( "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/transport/process/serialize" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" ) type ociImageDownloader struct { @@ -52,6 +54,19 @@ func (d *ociImageDownloader) Process(ctx context.Context, r io.Reader, w io.Writ return fmt.Errorf("unable to get oci artifact: %w", err) } + // fetch config blobs which adds them to the client cache + if ociArtifact.IsManifest() { + if err := d.fetchConfigAndLayerBlobs(ctx, ociAccess.ImageReference, ociArtifact.GetManifest().Data); err != nil { + return err + } + } else if ociArtifact.IsIndex() { + for _, m := range ociArtifact.GetIndex().Manifests { + if err := d.fetchConfigAndLayerBlobs(ctx, ociAccess.ImageReference, m.Data); err != nil { + return err + } + } + } + blobReader, err := serialize.SerializeOCIArtifact(*ociArtifact, d.cache) if err != nil { return fmt.Errorf("unable to serialize oci artifact: %w", err) @@ -64,3 +79,17 @@ func (d *ociImageDownloader) Process(ctx context.Context, r io.Reader, w io.Writ return nil } + +func (d *ociImageDownloader) fetchConfigAndLayerBlobs(ctx context.Context, ref string, manifest *ocispecv1.Manifest) error { + buf := bytes.NewBuffer([]byte{}) + if err := d.client.Fetch(ctx, ref, manifest.Config, buf); err != nil { + return fmt.Errorf("unable to fetch config blob: %w", err) + } + for _, l := range manifest.Layers { + buf := bytes.NewBuffer([]byte{}) + if err := d.client.Fetch(ctx, ref, l, buf); err != nil { + return fmt.Errorf("unable to fetch config blob: %w", err) + } + } + return nil +} diff --git a/pkg/transport/process/pipeline.go b/pkg/transport/process/pipeline.go index 1a5a6e4e..52eaeddb 100644 --- a/pkg/transport/process/pipeline.go +++ b/pkg/transport/process/pipeline.go @@ -41,7 +41,7 @@ func (p *resourceProcessingPipelineImpl) Process(ctx context.Context, cd cdv2.Co defer infile.Close() if _, err := infile.Seek(0, io.SeekStart); err != nil { - return nil, cdv2.Resource{}, err + return nil, cdv2.Resource{}, fmt.Errorf("unable to seek to beginning of file: %w", err) } processedCD, processedRes, blobreader, err := ReadProcessorMessage(infile) diff --git a/pkg/transport/process/serialize/oci_image.go b/pkg/transport/process/serialize/oci_image.go index 23b4f5f8..e884bf89 100644 --- a/pkg/transport/process/serialize/oci_image.go +++ b/pkg/transport/process/serialize/oci_image.go @@ -11,16 +11,19 @@ import ( "io" "io/ioutil" "path" + "strings" "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/ociclient/oci" "github.com/gardener/component-cli/pkg/utils" + "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/specs-go" ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" ) const ( ManifestFile = "manifest.json" + IndexFile = "index.json" BlobsDir = "blobs" ) @@ -51,20 +54,20 @@ func serializeImageIndex(cache cache.Cache, index *oci.Index, w io.Writer) error tw := tar.NewWriter(w) defer tw.Close() - descs := []ocispecv1.Descriptor{} + manifestDescs := []ocispecv1.Descriptor{} for _, m := range index.Manifests { manifestFile := path.Join(BlobsDir, m.Descriptor.Digest.Encoded()) if err := serializeImage(cache, m, manifestFile, tw); err != nil { return fmt.Errorf("unable to serialize image: %w", err) } - descs = append(descs, m.Descriptor) + manifestDescs = append(manifestDescs, m.Descriptor) } i := ocispecv1.Index{ Versioned: specs.Versioned{ SchemaVersion: 2, }, - Manifests: descs, + Manifests: manifestDescs, Annotations: index.Annotations, } @@ -73,7 +76,7 @@ func serializeImageIndex(cache cache.Cache, index *oci.Index, w io.Writer) error return fmt.Errorf("unable to marshal index manifest: %w", err) } - if err := utils.WriteFileToTARArchive(ManifestFile, bytes.NewReader(indexBytes), tw); err != nil { + if err := utils.WriteFileToTARArchive(IndexFile, bytes.NewReader(indexBytes), tw); err != nil { return fmt.Errorf("unable to write index manifest: %w", err) } @@ -81,7 +84,7 @@ func serializeImageIndex(cache cache.Cache, index *oci.Index, w io.Writer) error } func serializeImage(cache cache.Cache, manifest *oci.Manifest, manifestFile string, tw *tar.Writer) error { - manifestBytes, err := json.Marshal(manifest) + manifestBytes, err := json.Marshal(manifest.Data) if err != nil { return fmt.Errorf("unable to marshal manifest: %w", err) } @@ -118,40 +121,115 @@ func serializeImage(cache cache.Cache, manifest *oci.Manifest, manifestFile stri } func DeserializeOCIArtifact(r io.Reader, cache cache.Cache) (*oci.Artifact, error) { - // tr := tar.NewReader(r) + tr := tar.NewReader(r) - // for { - // header, err := tr.Next() - // if err != nil { - // if err == io.EOF { - // break - // } - // return nil, fmt.Errorf("unable to read tar header: %w", err) - // } + buf := bytes.NewBuffer([]byte{}) + isImageIndex := false - // if header.Name == ManifestFile { - - // } else if strings.HasPrefix(header.Name, BlobsDir) { - // } else { - // return nil, fmt.Errorf() - // } - // } - - // if f == nil { - // return cd, res, nil, nil - // } + for { + header, err := tr.Next() + if err != nil { + if err == io.EOF { + break + } + return nil, fmt.Errorf("unable to read tar header: %w", err) + } - // if _, err := f.Seek(0, io.SeekStart); err != nil { - // return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to seek to beginning of file: %w", err) - // } + if header.Name == ManifestFile { + if _, err := io.Copy(buf, tr); err != nil { + return nil, fmt.Errorf("unable to copy %s to buffer: %w", ManifestFile, err) + } + } else if header.Name == IndexFile { + if _, err := io.Copy(buf, tr); err != nil { + return nil, fmt.Errorf("unable to copy %s to buffer: %w", IndexFile, err) + } + isImageIndex = true + } else if strings.HasPrefix(header.Name, BlobsDir) { + tmpfile, err := ioutil.TempFile("", "") + if err != nil { + return nil, fmt.Errorf("") + } + + if _, err := io.Copy(tmpfile, tr); err != nil { + return nil, fmt.Errorf("") + } + + dgst, err := digest.FromReader(tmpfile) + if err != nil { + return nil, fmt.Errorf("unable to calculate digest for blobfile %s: %w", header.Name, err) + } + + if _, err := tmpfile.Seek(0, io.SeekStart); err != nil { + return nil, fmt.Errorf("unable to seek to beginning of file: %w", err) + } + + desc := ocispecv1.Descriptor{ + Digest: dgst, + } + if err := cache.Add(desc, tmpfile); err != nil { + return nil, fmt.Errorf("unable to write blob %+v to cache: %w", desc, err) + } + } else { + return nil, fmt.Errorf("unknown file") + } + } - // return cd, res, f, nil + var ociArtifact *oci.Artifact + var err error + if isImageIndex { + var index ocispecv1.Index + if err := json.Unmarshal(buf.Bytes(), &index); err != nil { + return nil, fmt.Errorf("unable to unmarshal image index: %w", err) + } - // desc := ocispecv1.Descriptor{} + manifests := []*oci.Manifest{} + for _, m := range index.Manifests { + blobreader, err := cache.Get(m) + if err != nil { + return nil, fmt.Errorf("unable to get manifest blob: %w", err) + } + defer blobreader.Close() + + buf := bytes.NewBuffer([]byte{}) + if _, err := io.Copy(buf, tr); err != nil { + return nil, fmt.Errorf("unable to copy %s to buffer: %w", ManifestFile, err) + } + + var manifest ocispecv1.Manifest + if err := json.Unmarshal(buf.Bytes(), &manifest); err != nil { + return nil, fmt.Errorf("unable to unmarshal %s: %w", ManifestFile, err) + } + + m := oci.Manifest{ + Descriptor: m, + Data: &manifest, + } + manifests = append(manifests, &m) + } - // cache.Add(desc, layerReader) + i := oci.Index{ + Manifests: manifests, + Annotations: index.Annotations, + } + if ociArtifact, err = oci.NewIndexArtifact(&i); err != nil { + return nil, fmt.Errorf("unable to create oci artifact: %w", err) + } + } else { + var manifest ocispecv1.Manifest + if err := json.Unmarshal(buf.Bytes(), &manifest); err != nil { + return nil, fmt.Errorf("unable to unmarshal manifest: %w", err) + } - // ociArtifact := oci.Artifact{} + m := oci.Manifest{ + Descriptor: ocispecv1.Descriptor{ + Digest: digest.FromBytes(buf.Bytes()), + }, + Data: &manifest, + } + if ociArtifact, err = oci.NewManifestArtifact(&m); err != nil { + return nil, fmt.Errorf("unable to create oci artifact: %w", err) + } + } - return nil, nil + return ociArtifact, nil } diff --git a/pkg/transport/process/uploaders/oci_image.go b/pkg/transport/process/uploaders/oci_image.go index 563c3e62..2e166198 100644 --- a/pkg/transport/process/uploaders/oci_image.go +++ b/pkg/transport/process/uploaders/oci_image.go @@ -12,20 +12,23 @@ import ( "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/transport/process/serialize" + "github.com/gardener/component-cli/pkg/utils" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" ) type ociImageUploader struct { - targetURL string - client ociclient.Client - cache cache.Cache + client ociclient.Client + cache cache.Cache + targetRepo string + keepSourceRepo bool } -func NewOCIImageUploader(targetURL string, client ociclient.Client, cache cache.Cache) process.ResourceStreamProcessor { +func NewOCIImageUploader(client ociclient.Client, cache cache.Cache, targetRepo string, keepSourceRepo bool) process.ResourceStreamProcessor { obj := ociImageUploader{ - targetURL: targetURL, - client: client, - cache: cache, + client: client, + cache: cache, + targetRepo: targetRepo, + keepSourceRepo: keepSourceRepo, } return &obj } @@ -55,7 +58,12 @@ func (u *ociImageUploader) Process(ctx context.Context, r io.Reader, w io.Writer return fmt.Errorf("unable to decode resource access: %w", err) } - if err := u.client.PushOCIArtifact(ctx, targetRef, ociArtifact, ociclient.WithStore(u.cache)); err != nil { + target, err := utils.TargetOCIArtifactRef(u.targetRepo, ociAccess.ImageReference, u.keepSourceRepo) + if err != nil { + return fmt.Errorf("unable to create target oci artifact reference: %w", err) + } + + if err := u.client.PushOCIArtifact(ctx, target, ociArtifact, ociclient.WithStore(u.cache)); err != nil { return fmt.Errorf("unable to push oci artifact: %w", err) } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 28903844..9175cd0f 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -14,7 +14,9 @@ import ( "io/ioutil" "math/rand" "net/http" + "net/url" "os" + "path" "path/filepath" "strings" "time" @@ -24,6 +26,7 @@ import ( "sigs.k8s.io/yaml" "github.com/gardener/component-cli/ociclient/cache" + "github.com/gardener/component-cli/ociclient/oci" "github.com/gardener/component-cli/pkg/commands/constants" ) @@ -249,3 +252,29 @@ func WriteFileToTARArchive(filename string, contentReader io.Reader, outArchive return nil } + +// TargetOCIArtifactRef calculates the target reference for +func TargetOCIArtifactRef(targetRepo, ref string, keepOrigHost bool) (string, error) { + if !strings.Contains(targetRepo, "://") { + // add dummy protocol to correctly parse the url + targetRepo = "http://" + targetRepo + } + t, err := url.Parse(targetRepo) + if err != nil { + return "", err + } + parsedRef, err := oci.ParseRef(ref) + if err != nil { + return "", err + } + + if !keepOrigHost { + parsedRef.Host = t.Host + parsedRef.Repository = path.Join(t.Path, parsedRef.Repository) + return parsedRef.String(), nil + } + replacedRef := strings.NewReplacer(".", "_", ":", "_").Replace(parsedRef.Name()) + parsedRef.Repository = path.Join(t.Path, replacedRef) + parsedRef.Host = t.Host + return parsedRef.String(), nil +} From b59ae47bab0367938af7f39a0dff0475fa8142ef Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 12 Oct 2021 09:14:46 +0200 Subject: [PATCH 26/94] wip --- .../process/processors/oci_image_filter.go | 90 +++++++++++++++++-- pkg/transport/process/serialize/oci_image.go | 2 +- pkg/utils/utils.go | 2 +- 3 files changed, 84 insertions(+), 10 deletions(-) diff --git a/pkg/transport/process/processors/oci_image_filter.go b/pkg/transport/process/processors/oci_image_filter.go index 5c144580..7cff4963 100644 --- a/pkg/transport/process/processors/oci_image_filter.go +++ b/pkg/transport/process/processors/oci_image_filter.go @@ -4,13 +4,17 @@ package processors import ( - "archive/tar" + "bytes" "compress/gzip" "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" "fmt" "io" "io/ioutil" + "github.com/containerd/containerd/images" "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/ociclient/oci" "github.com/gardener/component-cli/pkg/transport/process" @@ -21,8 +25,8 @@ import ( ) type ociImageFilter struct { - removePatterns []string cache cache.Cache + removePatterns []string } func (f *ociImageFilter) Process(ctx context.Context, r io.Reader, w io.Writer) error { @@ -76,6 +80,8 @@ func (f *ociImageFilter) Process(ctx context.Context, r io.Reader, w io.Writer) } func (f *ociImageFilter) filterImage(manifest oci.Manifest) (*oci.Manifest, error) { + diffIDs := []digest.Digest{} + digestMappings := map[digest.Digest]digest.Digest{} filteredLayers := []ocispecv1.Descriptor{} for _, layer := range manifest.Data.Layers { layerBlobReader, err := f.cache.Get(layer) @@ -88,37 +94,105 @@ func (f *ociImageFilter) filterImage(manifest oci.Manifest) (*oci.Manifest, erro return nil, fmt.Errorf("unable to create tempfile: %w", err) } defer tmpfile.Close() + var layerBlobWriter io.Writer = tmpfile - if layer.MediaType == ocispecv1.MediaTypeImageLayerGzip { + if layer.MediaType == ocispecv1.MediaTypeImageLayerGzip || layer.MediaType == images.MediaTypeDockerSchema2LayerGzip { layerBlobReader, err = gzip.NewReader(layerBlobReader) if err != nil { return nil, fmt.Errorf("unable to create gzip reader for layer: %w", err) } + layerBlobWriter = gzip.NewWriter(layerBlobWriter) } - if err = utils.FilterTARArchive(layerBlobReader, tar.NewWriter(tmpfile), f.removePatterns); err != nil { + uncompressedHasher := sha256.New() + mw := io.MultiWriter(layerBlobWriter, uncompressedHasher) + + if err = utils.FilterTARArchive(layerBlobReader, mw, f.removePatterns); err != nil { return nil, fmt.Errorf("unable to filter blob: %w", err) } - blobDigest, err := digest.FromReader(tmpfile) + if _, err := tmpfile.Seek(0, io.SeekStart); err != nil { + return nil, fmt.Errorf("unable to reset input file: %s", err) + } + + filteredDigest, err := digest.FromReader(tmpfile) if err != nil { return nil, fmt.Errorf("unable to calculate digest for layer %+v: %w", layer, err) } - layer.Digest = blobDigest + + digestMappings[layer.Digest] = filteredDigest + diffIDs = append(diffIDs, digest.NewDigestFromEncoded(digest.SHA256, hex.EncodeToString(uncompressedHasher.Sum(nil)))) + fstat, err := tmpfile.Stat() + if err != nil { + return nil, fmt.Errorf("unable to get file stat: %w", err) + } + + desc := ocispecv1.Descriptor{ + MediaType: layer.MediaType, + Digest: filteredDigest, + Size: fstat.Size(), + URLs: layer.URLs, + Platform: layer.Platform, + Annotations: layer.Annotations, + } + filteredLayers = append(filteredLayers, desc) if _, err := tmpfile.Seek(0, io.SeekStart); err != nil { return nil, fmt.Errorf("unable to reset input file: %s", err) } - if err := f.cache.Add(layer, tmpfile); err != nil { + if err := f.cache.Add(desc, tmpfile); err != nil { return nil, fmt.Errorf("unable to add filtered layer blob to cache: %w", err) } } manifest.Data.Layers = filteredLayers + + cfgBlob, err := f.cache.Get(manifest.Data.Config) + if err != nil { + return nil, fmt.Errorf("unable to get config blob from cache: %w", err) + } + + data, err := io.ReadAll(cfgBlob) + if err != nil { + return nil, fmt.Errorf("unable to read config blob: %w", err) + } + + var config map[string]*json.RawMessage + if err := json.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("unable to unmarshal config: %w", err) + } + + rootfs := ocispecv1.RootFS{ + Type: "layers", + DiffIDs: diffIDs, + } + rootfsRaw, err := utils.RawJSON(rootfs) + if err != nil { + return nil, fmt.Errorf("unable to convert rootfs to JSON: %w", err) + } + config["rootfs"] = rootfsRaw + + marshaledConfig, err := json.Marshal(config) + if err != nil { + return nil, fmt.Errorf("unable to marshal config: %w", err) + } + + configDesc := ocispecv1.Descriptor{ + MediaType: ocispecv1.MediaTypeImageConfig, + Digest: digest.FromBytes(marshaledConfig), + Size: int64(len(marshaledConfig)), + } + manifest.Data.Config = configDesc + + if err := f.cache.Add(configDesc, io.NopCloser(bytes.NewReader(marshaledConfig))); err != nil { + return nil, fmt.Errorf("unable to add filtered layer blob to cache: %w", err) + } + return &manifest, nil } -func NewOCIImageFilter(removePatterns []string) process.ResourceStreamProcessor { +func NewOCIImageFilter(cache cache.Cache, removePatterns []string) process.ResourceStreamProcessor { obj := ociImageFilter{ + cache: cache, removePatterns: removePatterns, } return &obj diff --git a/pkg/transport/process/serialize/oci_image.go b/pkg/transport/process/serialize/oci_image.go index e884bf89..ddccadb3 100644 --- a/pkg/transport/process/serialize/oci_image.go +++ b/pkg/transport/process/serialize/oci_image.go @@ -191,7 +191,7 @@ func DeserializeOCIArtifact(r io.Reader, cache cache.Cache) (*oci.Artifact, erro defer blobreader.Close() buf := bytes.NewBuffer([]byte{}) - if _, err := io.Copy(buf, tr); err != nil { + if _, err := io.Copy(buf, blobreader); err != nil { return nil, fmt.Errorf("unable to copy %s to buffer: %w", ManifestFile, err) } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 9175cd0f..9b6f7c05 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -253,7 +253,7 @@ func WriteFileToTARArchive(filename string, contentReader io.Reader, outArchive return nil } -// TargetOCIArtifactRef calculates the target reference for +// TargetOCIArtifactRef calculates the target reference for func TargetOCIArtifactRef(targetRepo, ref string, keepOrigHost bool) (string, error) { if !strings.Contains(targetRepo, "://") { // add dummy protocol to correctly parse the url From 3f84f8633b26059aa8dca97b9caf1d6b93f6a549 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 12 Oct 2021 15:34:07 +0200 Subject: [PATCH 27/94] wip --- pkg/transport/process/processors/oci_image_filter.go | 4 ++-- pkg/utils/utils.go | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/transport/process/processors/oci_image_filter.go b/pkg/transport/process/processors/oci_image_filter.go index 7cff4963..e1d3bb05 100644 --- a/pkg/transport/process/processors/oci_image_filter.go +++ b/pkg/transport/process/processors/oci_image_filter.go @@ -60,7 +60,7 @@ func (f *ociImageFilter) Process(ctx context.Context, r io.Reader, w io.Writer) } else { filteredImg, err := f.filterImage(*ociArtifact.GetManifest()) if err != nil { - return fmt.Errorf("unable to filter image ") + return fmt.Errorf("unable to filter image: %w", err) } if err := ociArtifact.SetManifest(filteredImg); err != nil { return fmt.Errorf("unable to set manifest: %w", err) @@ -101,7 +101,6 @@ func (f *ociImageFilter) filterImage(manifest oci.Manifest) (*oci.Manifest, erro if err != nil { return nil, fmt.Errorf("unable to create gzip reader for layer: %w", err) } - layerBlobWriter = gzip.NewWriter(layerBlobWriter) } uncompressedHasher := sha256.New() @@ -122,6 +121,7 @@ func (f *ociImageFilter) filterImage(manifest oci.Manifest) (*oci.Manifest, erro digestMappings[layer.Digest] = filteredDigest diffIDs = append(diffIDs, digest.NewDigestFromEncoded(digest.SHA256, hex.EncodeToString(uncompressedHasher.Sum(nil)))) + fstat, err := tmpfile.Stat() if err != nil { return nil, fmt.Errorf("unable to get file stat: %w", err) diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 9b6f7c05..c2202e6d 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -206,8 +206,7 @@ NEXT_FILE: return fmt.Errorf("unable to write header: %w", err) } - _, err = io.Copy(tw, tr) - if err != nil { + if _, err = io.Copy(tw, tr); err != nil { return fmt.Errorf("unable to write file: %w", err) } } From 45818cc16c4dd073b828bc21f8a6bfb8d56f638e Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 13 Oct 2021 11:00:41 +0200 Subject: [PATCH 28/94] should fix oci image filter --- .../process/processors/oci_image_filter.go | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pkg/transport/process/processors/oci_image_filter.go b/pkg/transport/process/processors/oci_image_filter.go index e1d3bb05..9d560bb4 100644 --- a/pkg/transport/process/processors/oci_image_filter.go +++ b/pkg/transport/process/processors/oci_image_filter.go @@ -48,6 +48,16 @@ func (f *ociImageFilter) Process(ctx context.Context, r io.Reader, w io.Writer) if err != nil { return fmt.Errorf("unable to filter image %+v: %w", m, err) } + + manifestBytes, err := json.Marshal(filteredManifest.Data) + if err != nil { + return fmt.Errorf("unable to marshal manifest: ") + } + + if err := f.cache.Add(filteredManifest.Descriptor, io.NopCloser(bytes.NewReader(manifestBytes))); err != nil { + return fmt.Errorf("unable to add filtered manifest to cache: %w", err) + } + filteredImgs = append(filteredImgs, filteredManifest) } filteredIndex := &oci.Index{ @@ -101,6 +111,9 @@ func (f *ociImageFilter) filterImage(manifest oci.Manifest) (*oci.Manifest, erro if err != nil { return nil, fmt.Errorf("unable to create gzip reader for layer: %w", err) } + gzipw := gzip.NewWriter(layerBlobWriter) + defer gzipw.Close() + layerBlobWriter = gzipw } uncompressedHasher := sha256.New() @@ -187,6 +200,14 @@ func (f *ociImageFilter) filterImage(manifest oci.Manifest) (*oci.Manifest, erro return nil, fmt.Errorf("unable to add filtered layer blob to cache: %w", err) } + manifestBytes, err := json.Marshal(manifest.Data) + if err != nil { + return nil, fmt.Errorf("unable to marshal manifest: %w", err) + } + + manifest.Descriptor.Size = int64(len(manifestBytes)) + manifest.Descriptor.Digest = digest.FromBytes(manifestBytes) + return &manifest, nil } From e62058bd76e75efd54f339fc7ba82a98d85ba9ba Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 13 Oct 2021 14:57:35 +0200 Subject: [PATCH 29/94] refactoring and godoc --- .../process/extensions/uds_executable.go | 4 +- .../process/processors/example/main.go | 3 +- .../process/processors/sleep/main.go | 6 +- pkg/transport/process/processors/util.go | 68 ------------------- pkg/transport/process/util.go | 66 ++++++++++++++++++ 5 files changed, 72 insertions(+), 75 deletions(-) delete mode 100644 pkg/transport/process/processors/util.go diff --git a/pkg/transport/process/extensions/uds_executable.go b/pkg/transport/process/extensions/uds_executable.go index eb0dcd97..be078160 100644 --- a/pkg/transport/process/extensions/uds_executable.go +++ b/pkg/transport/process/extensions/uds_executable.go @@ -18,8 +18,8 @@ import ( "github.com/gardener/component-cli/pkg/utils" ) -// ServerAddressEnv is the environment variable key which is used for propagating the -// address under which a processor server should start to a processor binary. +// ServerAddressEnv is the environment variable key which is used to store the +// address under which a resource processor server should start. const ServerAddressEnv = "SERVER_ADDRESS" type udsExecutable struct { diff --git a/pkg/transport/process/processors/example/main.go b/pkg/transport/process/processors/example/main.go index f5c66780..cc581f3a 100644 --- a/pkg/transport/process/processors/example/main.go +++ b/pkg/transport/process/processors/example/main.go @@ -19,7 +19,6 @@ import ( "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/transport/process/extensions" - "github.com/gardener/component-cli/pkg/transport/process/processors" ) const processorName = "example-processor" @@ -43,7 +42,7 @@ func main() { } } - srv, err := processors.NewUDSServer(addr, h) + srv, err := process.NewUDSServer(addr, h) if err != nil { log.Fatal(err) } diff --git a/pkg/transport/process/processors/sleep/main.go b/pkg/transport/process/processors/sleep/main.go index 477c8c0c..ad8b4e28 100644 --- a/pkg/transport/process/processors/sleep/main.go +++ b/pkg/transport/process/processors/sleep/main.go @@ -11,13 +11,13 @@ import ( "syscall" "time" + "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/transport/process/extensions" - "github.com/gardener/component-cli/pkg/transport/process/processors" ) const sleepTimeEnv = "SLEEP_TIME" -// a test processor which sleeps for a configurable duration and then exists with an error. +// a test processor which sleeps for a configurable duration and then exits with an error. func main() { sleepTime, err := time.ParseDuration(os.Getenv(sleepTimeEnv)) if err != nil { @@ -36,7 +36,7 @@ func main() { log.Fatal("finished sleeping -> exit with error") } - srv, err := processors.NewUDSServer(addr, h) + srv, err := process.NewUDSServer(addr, h) if err != nil { log.Fatal(err) } diff --git a/pkg/transport/process/processors/util.go b/pkg/transport/process/processors/util.go deleted file mode 100644 index 8109721c..00000000 --- a/pkg/transport/process/processors/util.go +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. -// -// SPDX-License-Identifier: Apache-2.0 -package processors - -import ( - "io" - "log" - "net" - "sync" -) - -type ProcessorHandlerFunc func(io.Reader, io.WriteCloser) - -type UDSServer struct { - listener net.Listener - quit chan interface{} - wg sync.WaitGroup - handler ProcessorHandlerFunc -} - -func NewUDSServer(addr string, h ProcessorHandlerFunc) (*UDSServer, error) { - l, err := net.Listen("unix", addr) - if err != nil { - return nil, err - } - s := &UDSServer{ - quit: make(chan interface{}), - listener: l, - handler: h, - } - return s, nil -} - -func (s *UDSServer) Start() { - s.wg.Add(1) - go s.serve() -} - -func (s *UDSServer) serve() { - defer s.wg.Done() - - for { - conn, err := s.listener.Accept() - if err != nil { - select { - case <-s.quit: - return - default: - log.Println("accept error", err) - } - } else { - s.wg.Add(1) - go func() { - defer s.wg.Done() - s.handler(conn, conn) - }() - } - } -} - -func (s *UDSServer) Stop() { - close(s.quit) - if err := s.listener.Close(); err != nil { - println(err) - } - s.wg.Wait() -} diff --git a/pkg/transport/process/util.go b/pkg/transport/process/util.go index c4a2268f..5019d077 100644 --- a/pkg/transport/process/util.go +++ b/pkg/transport/process/util.go @@ -9,7 +9,10 @@ import ( "fmt" "io" "io/ioutil" + "log" + "net" "os" + "sync" "time" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" @@ -176,3 +179,66 @@ func readComponentDescriptor(r *tar.Reader) (*cdv2.ComponentDescriptor, error) { return &cd, nil } + +// HandlerFunc defines the interface of a function that should be served by a UDS server +type HandlerFunc func(io.Reader, io.WriteCloser) + +// UDSServer implements a Unix Domain Socket server +type UDSServer struct { + listener net.Listener + quit chan interface{} + wg sync.WaitGroup + handler HandlerFunc +} + +// NewUDSServer returns a new UDS server. +// The parameters define the server address and the handler func it serves +func NewUDSServer(addr string, handler HandlerFunc) (*UDSServer, error) { + l, err := net.Listen("unix", addr) + if err != nil { + return nil, err + } + s := &UDSServer{ + quit: make(chan interface{}), + listener: l, + handler: handler, + } + return s, nil +} + +// Start starts the server goroutine +func (s *UDSServer) Start() { + s.wg.Add(1) + go s.serve() +} + +func (s *UDSServer) serve() { + defer s.wg.Done() + + for { + conn, err := s.listener.Accept() + if err != nil { + select { + case <-s.quit: + return + default: + log.Println("accept error", err) + } + } else { + s.wg.Add(1) + go func() { + defer s.wg.Done() + s.handler(conn, conn) + }() + } + } +} + +// Stop stops the server goroutine +func (s *UDSServer) Stop() { + close(s.quit) + if err := s.listener.Close(); err != nil { + println(err) + } + s.wg.Wait() +} From 8b18f1e5dad879d3828d901111383a6ec0d2723c Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Thu, 14 Oct 2021 13:04:03 +0200 Subject: [PATCH 30/94] refactors and improves config parsing --- pkg/transport/config/downloader_factory.go | 34 +++ pkg/transport/config/filter_factory.go | 30 +++ pkg/transport/config/parse.go | 114 --------- pkg/transport/config/pipeline.go | 273 +++++++++++---------- pkg/transport/config/processor_factory.go | 27 ++ pkg/transport/config/types.go | 5 +- pkg/transport/config/uploader_factory.go | 31 +++ pkg/transport/config/util.go | 27 ++ pkg/transport/filter/component_filter.go | 3 + pkg/transport/filter/filter.go | 3 + 10 files changed, 308 insertions(+), 239 deletions(-) create mode 100644 pkg/transport/config/downloader_factory.go create mode 100644 pkg/transport/config/filter_factory.go delete mode 100644 pkg/transport/config/parse.go create mode 100644 pkg/transport/config/processor_factory.go create mode 100644 pkg/transport/config/uploader_factory.go create mode 100644 pkg/transport/config/util.go diff --git a/pkg/transport/config/downloader_factory.go b/pkg/transport/config/downloader_factory.go new file mode 100644 index 00000000..ba045006 --- /dev/null +++ b/pkg/transport/config/downloader_factory.go @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package config + +import ( + "encoding/json" + "fmt" + + "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/downloaders" +) + +func NewDownloaderFactory(client ociclient.Client) *DownloaderFactory { + return &DownloaderFactory{ + client: client, + } +} + +type DownloaderFactory struct { + client ociclient.Client +} + +func (f *DownloaderFactory) Create(typ string, spec *json.RawMessage) (process.ResourceStreamProcessor, error) { + switch typ { + case "localOCIBlob": + return downloaders.NewLocalOCIBlobDownloader(f.client), nil + case "executable": + return createExecutable(spec) + default: + return nil, fmt.Errorf("unable to create downloader: unknown type %s", typ) + } +} diff --git a/pkg/transport/config/filter_factory.go b/pkg/transport/config/filter_factory.go new file mode 100644 index 00000000..01c70540 --- /dev/null +++ b/pkg/transport/config/filter_factory.go @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package config + +import ( + "encoding/json" + "fmt" + + "github.com/gardener/component-cli/pkg/transport/filter" +) + +func NewFilterFactory() *FilterFactory { + return &FilterFactory{} +} + +type FilterFactory struct{} + +func (f *FilterFactory) Create(typ string, spec *json.RawMessage) (filter.Filter, error) { + switch typ { + case "ComponentFilter": + filter, err := filter.CreateComponentFilterFromConfig(spec) + if err != nil { + return nil, fmt.Errorf("can not create filter %s with provided args", typ) + } + return filter, nil + default: + return nil, fmt.Errorf("unable to create downloader: unknown type %s", typ) + } +} diff --git a/pkg/transport/config/parse.go b/pkg/transport/config/parse.go deleted file mode 100644 index bc49662f..00000000 --- a/pkg/transport/config/parse.go +++ /dev/null @@ -1,114 +0,0 @@ -package config - -import ( - "context" - "fmt" - "os" - - "github.com/go-logr/logr" - "github.com/mandelsoft/vfs/pkg/osfs" - "github.com/mandelsoft/vfs/pkg/vfs" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "sigs.k8s.io/yaml" - - "github.com/gardener/component-cli/pkg/logger" - cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" -) - -// ParsingOptions defines all options that are used -type ParsingOptions struct { - ConfigPath string -} - -func NewConfigParseCommand(ctx context.Context) *cobra.Command { - opts := &ParsingOptions{} - cmd := &cobra.Command{ - Use: "parse PATH_TO_PROCESSING_CFG", - Args: cobra.RangeArgs(1, 2), - Short: "Parses a processing config.", - Long: ` -`, - Run: func(cmd *cobra.Command, args []string) { - if err := opts.Complete(args); err != nil { - fmt.Println(err.Error()) - os.Exit(1) - } - - if err := opts.Run(ctx, logger.Log, osfs.New()); err != nil { - fmt.Println(err.Error()) - os.Exit(1) - } - }, - } - opts.AddFlags(cmd.Flags()) - return cmd -} - -func (o *ParsingOptions) AddFlags(fs *pflag.FlagSet) { -} - -func (o *ParsingOptions) Complete(args []string) error { - if len(args) != 1 { - return fmt.Errorf("a path to a config file is required") - } - o.ConfigPath = args[0] - return nil -} - -func (o *ParsingOptions) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) error { - rawConfig, err := os.ReadFile(o.ConfigPath) - if err != nil { - return fmt.Errorf("failed reading config file %w", err) - } - - var config Config - err = yaml.Unmarshal(rawConfig, &config) - if err != nil { - return fmt.Errorf("failed parsing config %w", err) - } - - compiler, err := CompileFromConfig(&config) - if err != nil { - return fmt.Errorf("failed creating lookup table %w", err) - } - fmt.Println(compiler.lookup) - - cd := []cdv2.ComponentDescriptor{ - cdv2.ComponentDescriptor{ - ComponentSpec: cdv2.ComponentSpec{ - ObjectMeta: cdv2.ObjectMeta{ - Name: "ComponentDescirptor1", - }, - Resources: []cdv2.Resource{ - cdv2.Resource{ - IdentityObjectMeta: cdv2.IdentityObjectMeta{ - Name: "MyResource", - }, - }, - }, - }, - }, - cdv2.ComponentDescriptor{ - ComponentSpec: cdv2.ComponentSpec{ - ObjectMeta: cdv2.ObjectMeta{ - Name: "ComponentDescirptor2", - }, - Resources: []cdv2.Resource{ - cdv2.Resource{ - IdentityObjectMeta: cdv2.IdentityObjectMeta{ - Name: "MyResource", - }, - }, - }, - }, - }, - } - - pipeline, err := compiler.CreateResourcePipeline(cd) - if err != nil { - return fmt.Errorf("failed creating pipeline %w", err) - } - fmt.Println(pipeline) - return nil -} diff --git a/pkg/transport/config/pipeline.go b/pkg/transport/config/pipeline.go index 157b7634..bd5db784 100644 --- a/pkg/transport/config/pipeline.go +++ b/pkg/transport/config/pipeline.go @@ -1,14 +1,17 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 package config import ( "encoding/json" "fmt" + "os" "github.com/gardener/component-cli/pkg/transport/filter" "github.com/gardener/component-cli/pkg/transport/process" - "github.com/gardener/component-cli/pkg/transport/process/downloaders" - "github.com/gardener/component-cli/pkg/transport/process/uploaders" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "sigs.k8s.io/yaml" ) type ResourcePipeline struct { @@ -24,148 +27,194 @@ type ProcessorWithName struct { Name string } +type DD struct { + name string + typ ExtensionType + spec *json.RawMessage + filters []filter.Filter +} + +type PD struct { + name string + typ ExtensionType + spec *json.RawMessage +} + +type UD struct { + name string + typ ExtensionType + spec *json.RawMessage + filters []filter.Filter +} + +type RD struct { + name string + processors []string + filters []filter.Filter +} + type ProcessorsLookup struct { - downloaders []struct { - ProcessorWithName - filters []filter.Filter + downloaders []DD + processors []PD + uploaders []UD + rules []RD +} + +func NewPipelineCompiler(transportCfgPath string, df *DownloaderFactory, pf *ProcessorFactory, uf *UploaderFactory) (*ProcessingPipelineCompiler, error) { + transportCfgYaml, err := os.ReadFile(transportCfgPath) + if err != nil { + return nil, fmt.Errorf("unable to read transport config file: %w", err) } - processors []ProcessorWithName - uploaders []struct { - ProcessorWithName - filters []filter.Filter + var transportCfg transportConfig + err = yaml.Unmarshal(transportCfgYaml, &transportCfg) + if err != nil { + return nil, fmt.Errorf("unable to parse transport config file: %w", err) } - rules []struct { - name string - processors []string - filters []filter.Filter + compiler, err := compileFromConfig(&transportCfg) + if err != nil { + return nil, fmt.Errorf("failed creating lookup table %w", err) } + + c := ProcessingPipelineCompiler{ + lookup: compiler, + downloaderFactory: df, + processorFactory: pf, + uploaderFactory: uf, + } + + return &c, nil } type ProcessingPipelineCompiler struct { - lookup ProcessorsLookup + lookup *ProcessorsLookup + uploaderFactory *UploaderFactory + downloaderFactory *DownloaderFactory + processorFactory *ProcessorFactory } // Create a ProcessingPipelineCompiler on the base of a config -func CompileFromConfig(config *Config) (*ProcessingPipelineCompiler, error) { +func compileFromConfig(config *transportConfig) (*ProcessorsLookup, error) { var lookup ProcessorsLookup + ff := NewFilterFactory() // downloader - for _, downlaoderDefinition := range config.Downloaders { - if downlaoderDefinition.Type == ExecutableProcessor { - fmt.Println("Not yet implemented") - } else { - downloader := createBuiltInProcessor(string(downlaoderDefinition.Type), downlaoderDefinition.Spec) - filters, err := createFilterList(downlaoderDefinition.Filters) - if err != nil { - return nil, fmt.Errorf("failed creating downloader %s: %w", downlaoderDefinition.Name, err) - } - lookup.downloaders = append(lookup.downloaders, struct { - ProcessorWithName - filters []filter.Filter - }{ProcessorWithName{downloader, downlaoderDefinition.Name}, filters}) + for _, downloaderDefinition := range config.Downloaders { + filters, err := createFilterList(downloaderDefinition.Filters, ff) + if err != nil { + return nil, fmt.Errorf("failed creating downloader %s: %w", downloaderDefinition.Name, err) } + lookup.downloaders = append(lookup.downloaders, DD{ + name: downloaderDefinition.Name, + typ: downloaderDefinition.Type, + spec: downloaderDefinition.Spec, + filters: filters, + }) } // processors for _, processorsDefinition := range config.Processors { - if processorsDefinition.Type == ExecutableProcessor { - fmt.Println("Not yet implemented") - } else { - processor := createBuiltInProcessor(string(processorsDefinition.Type), processorsDefinition.Spec) - lookup.processors = append(lookup.processors, struct { - Processor process.ResourceStreamProcessor - Name string - }{processor, processorsDefinition.Name}) - } + lookup.processors = append(lookup.processors, PD{ + name: processorsDefinition.Name, + typ: processorsDefinition.Type, + spec: processorsDefinition.Spec, + }) } // uploaders for _, uploaderDefinition := range config.Uploaders { - if uploaderDefinition.Type == ExecutableProcessor { - fmt.Println("Not yet implemented") - } else { - uploader := createBuiltInProcessor(string(uploaderDefinition.Type), uploaderDefinition.Spec) - filters, err := createFilterList(uploaderDefinition.Filters) - if err != nil { - return nil, fmt.Errorf("failed creating downloader %s: %w", uploaderDefinition.Name, err) - } - lookup.uploaders = append(lookup.uploaders, struct { - ProcessorWithName - filters []filter.Filter - }{ProcessorWithName{uploader, uploaderDefinition.Name}, filters}) + filters, err := createFilterList(uploaderDefinition.Filters, ff) + if err != nil { + return nil, fmt.Errorf("failed creating downloader %s: %w", uploaderDefinition.Name, err) } + lookup.uploaders = append(lookup.uploaders, UD{ + name: uploaderDefinition.Name, + typ: uploaderDefinition.Type, + spec: uploaderDefinition.Spec, + filters: filters, + }) } // rules for _, rule := range config.Rules { - var ruleLookup struct { - name string - processors []string - filters []filter.Filter - } - ruleLookup.name = rule.Name + processors := []string{} for _, processor := range rule.Processors { - ruleLookup.processors = append(ruleLookup.processors, processor.Name) + processors = append(processors, processor.Name) } - filters, err := createFilterList(rule.Filters) + filters, err := createFilterList(rule.Filters, ff) if err != nil { return nil, fmt.Errorf("failed creating rule %s: %w", rule.Name, err) } - ruleLookup.filters = filters + ruleLookup := RD{ + name: rule.Name, + processors: processors, + filters: filters, + } lookup.rules = append(lookup.rules, ruleLookup) } - return &ProcessingPipelineCompiler{ - lookup: lookup, - }, nil + return &lookup, nil } -func (c *ProcessingPipelineCompiler) CreateResourcePipeline(cds []cdv2.ComponentDescriptor) ([]ResourcePipeline, error) { - var pipelines []ResourcePipeline - - // loop through all resources - for _, cd := range cds { - for _, res := range cd.Resources { - var pipeline ResourcePipeline - pipeline.Cd = &cd - pipeline.Resource = &res - - // find matching downloader - for _, downloader := range c.lookup.downloaders { - matches := doesAllFilterMatch(downloader.filters, cd, res) - if matches { - pipeline.Downloaders = append(pipeline.Downloaders, ProcessorWithName{downloader.Processor, downloader.Name}) - } +func (c *ProcessingPipelineCompiler) CreateResourcePipeline(cd cdv2.ComponentDescriptor, res cdv2.Resource) (*ResourcePipeline, error) { + pipeline := ResourcePipeline{ + Cd: &cd, + Resource: &res, + } + + // find matching downloader + for _, downloader := range c.lookup.downloaders { + matches := doesAllFilterMatch(downloader.filters, cd, res) + if matches { + dl, err := c.downloaderFactory.Create(string(downloader.typ), downloader.spec) + if err != nil { + return nil, err } + pipeline.Downloaders = append(pipeline.Downloaders, ProcessorWithName{ + Name: downloader.name, + Processor: dl, + }) + } + } - // find matching uploader - for _, uploader := range c.lookup.uploaders { - matches := doesAllFilterMatch(uploader.filters, cd, res) - if matches { - pipeline.Uploaders = append(pipeline.Uploaders, ProcessorWithName{uploader.Processor, uploader.Name}) - } + // find matching uploader + for _, uploader := range c.lookup.uploaders { + matches := doesAllFilterMatch(uploader.filters, cd, res) + if matches { + ul, err := c.downloaderFactory.Create(string(uploader.typ), uploader.spec) + if err != nil { + return nil, err } + pipeline.Uploaders = append(pipeline.Uploaders, ProcessorWithName{ + Name: uploader.name, + Processor: ul, + }) + } + } - // loop through all rules to find corresponding processors - for _, rule := range c.lookup.rules { - matches := doesAllFilterMatch(rule.filters, cd, res) - if matches { - for _, processorName := range rule.processors { - processorDefined, err := lookupProcessorByName(processorName, &c.lookup) - if err != nil { - return nil, fmt.Errorf("failed compiling rule %s: %w", rule.name, err) - } - pipeline.Processors = append(pipeline.Processors, ProcessorWithName{processorDefined.Processor, processorDefined.Name}) - } + // loop through all rules to find corresponding processors + for _, rule := range c.lookup.rules { + matches := doesAllFilterMatch(rule.filters, cd, res) + if matches { + for _, processorName := range rule.processors { + processorDefined, err := lookupProcessorByName(processorName, c.lookup) + if err != nil { + return nil, fmt.Errorf("failed compiling rule %s: %w", rule.name, err) + } + p, err := c.processorFactory.Create(string(processorDefined.typ), processorDefined.spec) + if err != nil { + return nil, err } + pipeline.Processors = append(pipeline.Processors, ProcessorWithName{ + Name: processorDefined.name, + Processor: p, + }) } - pipelines = append(pipelines, pipeline) } } - return pipelines, nil + return &pipeline, nil } func doesAllFilterMatch(filters []filter.Filter, cd cdv2.ComponentDescriptor, res cdv2.Resource) bool { @@ -177,22 +226,10 @@ func doesAllFilterMatch(filters []filter.Filter, cd cdv2.ComponentDescriptor, re return true } -func createBuiltInProcessor(builtinType string, spec *json.RawMessage) process.ResourceStreamProcessor { - switch builtinType { - case "LocalOCIBlobDownloader": //TODO: make to constant - // TODO parse config into corresponding config structure - return downloaders.NewLocalOCIBlobDownloader(nil) // TODO: pass correct oci client - case "LocalOCIBlobUploader": - // TODO parse config into corresponding config structure - return uploaders.NewLocalOCIBlobUploader(nil, cdv2.OCIRegistryRepository{}) // TODO: pass correct oci client - } - return nil // TODO: change to error -} - -func createFilterList(filterDefinitions []FilterDefinition) ([]filter.Filter, error) { +func createFilterList(filterDefinitions []FilterDefinition, ff *FilterFactory) ([]filter.Filter, error) { var filters []filter.Filter for _, f := range filterDefinitions { - filter, err := createFilter(f.Type, f.Args) + filter, err := ff.Create(f.Type, f.Args) if err != nil { return nil, fmt.Errorf("error creating filter list for type %s with args %s: %w", f.Type, string(*f.Args), err) } @@ -201,21 +238,9 @@ func createFilterList(filterDefinitions []FilterDefinition) ([]filter.Filter, er return filters, nil } -func createFilter(filterType string, args *json.RawMessage) (filter.Filter, error) { - switch filterType { - case "ComponentFilter": // TODO: make constant - filter, err := filter.CreateComponentFilterFromConfig(args) - if err != nil { - return nil, fmt.Errorf("can not create filter %s with provided args", filterType) - } - return filter, nil - } - return nil, fmt.Errorf("can not find filter %s", filterType) -} - -func lookupProcessorByName(name string, lookup *ProcessorsLookup) (*ProcessorWithName, error) { +func lookupProcessorByName(name string, lookup *ProcessorsLookup) (*PD, error) { for _, processor := range lookup.processors { - if processor.Name == name { + if processor.name == name { return &processor, nil } } diff --git a/pkg/transport/config/processor_factory.go b/pkg/transport/config/processor_factory.go new file mode 100644 index 00000000..348c072c --- /dev/null +++ b/pkg/transport/config/processor_factory.go @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package config + +import ( + "encoding/json" + "fmt" + + "github.com/gardener/component-cli/pkg/transport/process" +) + +func NewProcessorFactory() *ProcessorFactory{ + return &ProcessorFactory{} +} + +type ProcessorFactory struct { +} + +func (f *ProcessorFactory) Create(typ string, spec *json.RawMessage) (process.ResourceStreamProcessor, error) { + switch typ { + case "executable": + return createExecutable(spec) + default: + return nil, fmt.Errorf("unable to create processor: unknown type %s", typ) + } +} \ No newline at end of file diff --git a/pkg/transport/config/types.go b/pkg/transport/config/types.go index a312c026..72b1ee59 100644 --- a/pkg/transport/config/types.go +++ b/pkg/transport/config/types.go @@ -1,8 +1,11 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 package config import "encoding/json" -type Config struct { +type transportConfig struct { Meta string Version string `json:"version"` Uploaders []UploaderDefinition `json:"uploaders"` diff --git a/pkg/transport/config/uploader_factory.go b/pkg/transport/config/uploader_factory.go new file mode 100644 index 00000000..e84cb009 --- /dev/null +++ b/pkg/transport/config/uploader_factory.go @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package config + +import ( + "encoding/json" + "fmt" + + "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/pkg/transport/process" +) + +func NewUploaderFactory(client ociclient.Client) *UploaderFactory { + return &UploaderFactory{ + client: client, + } +} + +type UploaderFactory struct { + client ociclient.Client +} + +func (f *UploaderFactory) Create(typ string, spec *json.RawMessage) (process.ResourceStreamProcessor, error) { + switch typ { + case "executable": + return createExecutable(spec) + default: + return nil, fmt.Errorf("unable to create uploader: unknown type %s", typ) + } +} diff --git a/pkg/transport/config/util.go b/pkg/transport/config/util.go new file mode 100644 index 00000000..ec5a24db --- /dev/null +++ b/pkg/transport/config/util.go @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package config + +import ( + "encoding/json" + "fmt" + + "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/extensions" + "sigs.k8s.io/yaml" +) + +type executableSpec struct { + Bin string + Args []string + Env []string +} + +func createExecutable(spec *json.RawMessage) (process.ResourceStreamProcessor, error) { + var specstr executableSpec + if err := yaml.Unmarshal(*spec, &specstr); err != nil { + return nil, fmt.Errorf("unable to parse downloader spec: %w", err) + } + return extensions.NewUDSExecutable(specstr.Bin, specstr.Args, specstr.Env) +} diff --git a/pkg/transport/filter/component_filter.go b/pkg/transport/filter/component_filter.go index aaa10ecc..1ba5b09e 100644 --- a/pkg/transport/filter/component_filter.go +++ b/pkg/transport/filter/component_filter.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 package filter import ( diff --git a/pkg/transport/filter/filter.go b/pkg/transport/filter/filter.go index 41f14c25..745d2376 100644 --- a/pkg/transport/filter/filter.go +++ b/pkg/transport/filter/filter.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 package filter import ( From 3c2a1ef2d1afe12ac94ed6bd1a4f3528cb930509 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Fri, 15 Oct 2021 14:38:38 +0200 Subject: [PATCH 31/94] implements object factories --- pkg/transport/config/downloader_factory.go | 25 ++++++++++++-- pkg/transport/config/filter_factory.go | 23 +++++++++---- pkg/transport/config/processor_factory.go | 2 +- pkg/transport/config/uploader_factory.go | 35 +++++++++++++++++--- pkg/transport/config/util.go | 21 ++++++------ pkg/transport/filter/component_filter.go | 26 --------------- pkg/transport/process/uploaders/oci_image.go | 8 ++--- 7 files changed, 87 insertions(+), 53 deletions(-) diff --git a/pkg/transport/config/downloader_factory.go b/pkg/transport/config/downloader_factory.go index ba045006..963a20e1 100644 --- a/pkg/transport/config/downloader_factory.go +++ b/pkg/transport/config/downloader_factory.go @@ -8,27 +8,48 @@ import ( "fmt" "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/transport/process/downloaders" + "sigs.k8s.io/yaml" ) -func NewDownloaderFactory(client ociclient.Client) *DownloaderFactory { +func NewDownloaderFactory(client ociclient.Client, ocicache cache.Cache) *DownloaderFactory { return &DownloaderFactory{ client: client, + cache: ocicache, } } type DownloaderFactory struct { client ociclient.Client + cache cache.Cache } func (f *DownloaderFactory) Create(typ string, spec *json.RawMessage) (process.ResourceStreamProcessor, error) { switch typ { case "localOCIBlob": return downloaders.NewLocalOCIBlobDownloader(f.client), nil + case "ociImage": + return f.createOCIImageDownloader(spec) case "executable": return createExecutable(spec) default: - return nil, fmt.Errorf("unable to create downloader: unknown type %s", typ) + return nil, fmt.Errorf("unknown downloader type %s", typ) } } + +func (f *DownloaderFactory) createOCIImageDownloader(rawSpec *json.RawMessage) (process.ResourceStreamProcessor, error) { + type downloaderSpec struct { + BaseUrl string `json:"baseUrl"` + KeepSourceRepo bool `json:"keepSourceRepo"` + } + + var spec downloaderSpec + err := yaml.Unmarshal(*rawSpec, &spec) + if err != nil { + return nil, fmt.Errorf("unable to parse spec: %w", err) + } + + return downloaders.NewOCIImageDownloader(f.client, f.cache), nil +} diff --git a/pkg/transport/config/filter_factory.go b/pkg/transport/config/filter_factory.go index 01c70540..b6b29893 100644 --- a/pkg/transport/config/filter_factory.go +++ b/pkg/transport/config/filter_factory.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/gardener/component-cli/pkg/transport/filter" + "sigs.k8s.io/yaml" ) func NewFilterFactory() *FilterFactory { @@ -19,12 +20,22 @@ type FilterFactory struct{} func (f *FilterFactory) Create(typ string, spec *json.RawMessage) (filter.Filter, error) { switch typ { case "ComponentFilter": - filter, err := filter.CreateComponentFilterFromConfig(spec) - if err != nil { - return nil, fmt.Errorf("can not create filter %s with provided args", typ) - } - return filter, nil + return f.createComponentFilter(spec) default: - return nil, fmt.Errorf("unable to create downloader: unknown type %s", typ) + return nil, fmt.Errorf("unknown filter type %s", typ) } } + +func (f *FilterFactory) createComponentFilter(rawSpec *json.RawMessage) (filter.Filter, error) { + type filterSpec struct { + IncludeComponentNames []string `json:"includeComponentNames"` + } + + var spec filterSpec + err := yaml.Unmarshal(*rawSpec, &spec) + if err != nil { + return nil, fmt.Errorf("unable to parse spec: %w", err) + } + + return filter.NewComponentFilter(spec.IncludeComponentNames...) +} diff --git a/pkg/transport/config/processor_factory.go b/pkg/transport/config/processor_factory.go index 348c072c..6bfd3f31 100644 --- a/pkg/transport/config/processor_factory.go +++ b/pkg/transport/config/processor_factory.go @@ -22,6 +22,6 @@ func (f *ProcessorFactory) Create(typ string, spec *json.RawMessage) (process.Re case "executable": return createExecutable(spec) default: - return nil, fmt.Errorf("unable to create processor: unknown type %s", typ) + return nil, fmt.Errorf("unknown processor type %s", typ) } } \ No newline at end of file diff --git a/pkg/transport/config/uploader_factory.go b/pkg/transport/config/uploader_factory.go index e84cb009..1e2a3a28 100644 --- a/pkg/transport/config/uploader_factory.go +++ b/pkg/transport/config/uploader_factory.go @@ -8,24 +8,51 @@ import ( "fmt" "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/uploaders" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "sigs.k8s.io/yaml" ) -func NewUploaderFactory(client ociclient.Client) *UploaderFactory { +func NewUploaderFactory(client ociclient.Client, ocicache cache.Cache, targetCtx cdv2.OCIRegistryRepository) *UploaderFactory { return &UploaderFactory{ - client: client, + client: client, + cache: ocicache, + targetCtx: targetCtx, } } type UploaderFactory struct { - client ociclient.Client + client ociclient.Client + cache cache.Cache + targetCtx cdv2.OCIRegistryRepository } func (f *UploaderFactory) Create(typ string, spec *json.RawMessage) (process.ResourceStreamProcessor, error) { switch typ { + case "localOCIBlob": + return uploaders.NewLocalOCIBlobUploader(f.client, f.targetCtx), nil + case "ociImage": + return f.createOCIImageUploader(spec) case "executable": return createExecutable(spec) default: - return nil, fmt.Errorf("unable to create uploader: unknown type %s", typ) + return nil, fmt.Errorf("unknown uploader type %s", typ) } } + +func (f *UploaderFactory) createOCIImageUploader(rawSpec *json.RawMessage) (process.ResourceStreamProcessor, error) { + type uploaderSpec struct { + BaseUrl string `json:"baseUrl"` + KeepSourceRepo bool `json:"keepSourceRepo"` + } + + var spec uploaderSpec + err := yaml.Unmarshal(*rawSpec, &spec) + if err != nil { + return nil, fmt.Errorf("unable to parse spec: %w", err) + } + + return uploaders.NewOCIImageUploader(f.client, f.cache, spec.BaseUrl, spec.KeepSourceRepo), nil +} diff --git a/pkg/transport/config/util.go b/pkg/transport/config/util.go index ec5a24db..559911f1 100644 --- a/pkg/transport/config/util.go +++ b/pkg/transport/config/util.go @@ -12,16 +12,17 @@ import ( "sigs.k8s.io/yaml" ) -type executableSpec struct { - Bin string - Args []string - Env []string -} +func createExecutable(rawSpec *json.RawMessage) (process.ResourceStreamProcessor, error) { + type executableSpec struct { + Bin string + Args []string + Env []string + } -func createExecutable(spec *json.RawMessage) (process.ResourceStreamProcessor, error) { - var specstr executableSpec - if err := yaml.Unmarshal(*spec, &specstr); err != nil { - return nil, fmt.Errorf("unable to parse downloader spec: %w", err) + var spec executableSpec + if err := yaml.Unmarshal(*rawSpec, &spec); err != nil { + return nil, fmt.Errorf("unable to parse spec: %w", err) } - return extensions.NewUDSExecutable(specstr.Bin, specstr.Args, specstr.Env) + + return extensions.NewUDSExecutable(spec.Bin, spec.Args, spec.Env) } diff --git a/pkg/transport/filter/component_filter.go b/pkg/transport/filter/component_filter.go index 1ba5b09e..f1034a3f 100644 --- a/pkg/transport/filter/component_filter.go +++ b/pkg/transport/filter/component_filter.go @@ -4,12 +4,10 @@ package filter import ( - "encoding/json" "fmt" "regexp" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" - "k8s.io/apimachinery/pkg/util/yaml" ) type componentFilter struct { @@ -26,30 +24,6 @@ func (f componentFilter) Matches(cd *cdv2.ComponentDescriptor, r cdv2.Resource) return matches } -func CreateComponentFilterFromConfig(rawConfig *json.RawMessage) (Filter, error) { - type RawConfigStruct struct { - IncludeComponentNames []string `json:"includeComponentNames"` - } - var config RawConfigStruct - err := yaml.Unmarshal(*rawConfig, &config) - if err != nil { - return nil, fmt.Errorf("unable to parse filter configuration for ComponentFilter %w", err) - } - icnRegexps := []*regexp.Regexp{} - for _, icn := range config.IncludeComponentNames { - icnRegexp, err := regexp.Compile(icn) - if err != nil { - return nil, fmt.Errorf("unable to parse regexp %s: %w", icn, err) - } - icnRegexps = append(icnRegexps, icnRegexp) - } - - filter := componentFilter{ - includeComponentNames: icnRegexps, - } - return &filter, nil -} - func NewComponentFilter(includeComponentNames ...string) (Filter, error) { icnRegexps := []*regexp.Regexp{} for _, icn := range includeComponentNames { diff --git a/pkg/transport/process/uploaders/oci_image.go b/pkg/transport/process/uploaders/oci_image.go index 2e166198..8be0c39a 100644 --- a/pkg/transport/process/uploaders/oci_image.go +++ b/pkg/transport/process/uploaders/oci_image.go @@ -19,15 +19,15 @@ import ( type ociImageUploader struct { client ociclient.Client cache cache.Cache - targetRepo string + baseUrl string keepSourceRepo bool } -func NewOCIImageUploader(client ociclient.Client, cache cache.Cache, targetRepo string, keepSourceRepo bool) process.ResourceStreamProcessor { +func NewOCIImageUploader(client ociclient.Client, cache cache.Cache, baseUrl string, keepSourceRepo bool) process.ResourceStreamProcessor { obj := ociImageUploader{ client: client, cache: cache, - targetRepo: targetRepo, + baseUrl: baseUrl, keepSourceRepo: keepSourceRepo, } return &obj @@ -58,7 +58,7 @@ func (u *ociImageUploader) Process(ctx context.Context, r io.Reader, w io.Writer return fmt.Errorf("unable to decode resource access: %w", err) } - target, err := utils.TargetOCIArtifactRef(u.targetRepo, ociAccess.ImageReference, u.keepSourceRepo) + target, err := utils.TargetOCIArtifactRef(u.baseUrl, ociAccess.ImageReference, u.keepSourceRepo) if err != nil { return fmt.Errorf("unable to create target oci artifact reference: %w", err) } From 555ca810497aaa1861e1ae3ca09a1214bdbf6215 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Fri, 15 Oct 2021 15:53:43 +0200 Subject: [PATCH 32/94] implements processor factory --- pkg/transport/config/processor_factory.go | 45 +++++++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/pkg/transport/config/processor_factory.go b/pkg/transport/config/processor_factory.go index 6bfd3f31..08ed1f62 100644 --- a/pkg/transport/config/processor_factory.go +++ b/pkg/transport/config/processor_factory.go @@ -7,21 +7,60 @@ import ( "encoding/json" "fmt" + "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/processors" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "sigs.k8s.io/yaml" ) -func NewProcessorFactory() *ProcessorFactory{ - return &ProcessorFactory{} +func NewProcessorFactory(ociCache cache.Cache) *ProcessorFactory { + return &ProcessorFactory{ + cache: ociCache, + } } type ProcessorFactory struct { + cache cache.Cache } func (f *ProcessorFactory) Create(typ string, spec *json.RawMessage) (process.ResourceStreamProcessor, error) { switch typ { + case "label": + return f.createLabellingProcessor(spec) + case "ociImageFilter": + return f.createOCIImageFilter(spec) case "executable": return createExecutable(spec) default: return nil, fmt.Errorf("unknown processor type %s", typ) } -} \ No newline at end of file +} + +func (f *ProcessorFactory) createLabellingProcessor(rawSpec *json.RawMessage) (process.ResourceStreamProcessor, error) { + type processorSpec struct { + Labels cdv2.Labels `json:"labels"` + } + + var spec processorSpec + err := yaml.Unmarshal(*rawSpec, &spec) + if err != nil { + return nil, fmt.Errorf("unable to parse spec: %w", err) + } + + return processors.NewLabellingProcessor(spec.Labels...), nil +} + +func (f *ProcessorFactory) createOCIImageFilter(rawSpec *json.RawMessage) (process.ResourceStreamProcessor, error) { + type processorSpec struct { + RemovePatterns []string `json:"removePatterns"` + } + + var spec processorSpec + err := yaml.Unmarshal(*rawSpec, &spec) + if err != nil { + return nil, fmt.Errorf("unable to parse spec: %w", err) + } + + return processors.NewOCIImageFilter(f.cache, spec.RemovePatterns), nil +} From f21513afd448f75d71fea06239b67a02e68e5c2c Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Mon, 18 Oct 2021 10:17:58 +0200 Subject: [PATCH 33/94] fix compile errors + refactoring --- pkg/commands/transport/transport.go | 126 +++++++++++----------------- pkg/transport/config/pipeline.go | 62 ++++++++++---- 2 files changed, 94 insertions(+), 94 deletions(-) diff --git a/pkg/commands/transport/transport.go b/pkg/commands/transport/transport.go index 13101424..b6f39c73 100644 --- a/pkg/commands/transport/transport.go +++ b/pkg/commands/transport/transport.go @@ -21,29 +21,30 @@ import ( "sigs.k8s.io/yaml" "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/ociclient/cache" ociopts "github.com/gardener/component-cli/ociclient/options" "github.com/gardener/component-cli/pkg/commands/constants" "github.com/gardener/component-cli/pkg/logger" - "github.com/gardener/component-cli/pkg/transport/process" - "github.com/gardener/component-cli/pkg/transport/process/downloaders" - "github.com/gardener/component-cli/pkg/transport/process/extensions" - "github.com/gardener/component-cli/pkg/transport/process/uploaders" + "github.com/gardener/component-cli/pkg/transport/config" ) const ( parallelRuns = 1 - targetCtxUrl = "eu.gcr.io/gardener-project/test/jschicktanz/target" ) type Options struct { - // BaseUrl is the oci registry where the component is stored. - BaseUrl string + SourceRepository string + TargetRepository string + // ComponentName is the unique name of the component in the registry. ComponentName string // Version is the component Version in the oci registry. Version string - ComponentNameMapping string + // TransportCfgPath is the path to the transport config file + TransportCfgPath string + // RepoCtxOverrideCfgPath is the path to the repo context override config file + RepoCtxOverrideCfgPath string // OCIOptions contains all oci client related options. OCIOptions ociopts.Options @@ -71,13 +72,15 @@ func NewTransportCommand(ctx context.Context) *cobra.Command { } func (o *Options) AddFlags(fs *pflag.FlagSet) { - fs.StringVar(&o.ComponentNameMapping, "component-name-mapping", string(cdv2.OCIRegistryURLPathMapping), "[OPTIONAL] repository context name mapping") + fs.StringVar(&o.SourceRepository, "from", "", "source repository base url.") + fs.StringVar(&o.TargetRepository, "to", "", "target repository where the components are copied to.") + fs.StringVar(&o.TransportCfgPath, "transport-cfg", "", "path to the transport config file") + fs.StringVar(&o.RepoCtxOverrideCfgPath, "repo-ctx-override-cfg", "", "path to the repo context override config file") o.OCIOptions.AddFlags(fs) } func (o *Options) Complete(args []string) error { - // todo: validate args - o.BaseUrl = args[0] + o.SourceRepository = args[0] o.ComponentName = args[1] o.Version = args[2] @@ -90,7 +93,7 @@ func (o *Options) Complete(args []string) error { return fmt.Errorf("unable to create cache directory %s: %w", o.OCIOptions.CacheDir, err) } - if len(o.BaseUrl) == 0 { + if len(o.SourceRepository) == 0 { return errors.New("the base url must be defined") } if len(o.ComponentName) == 0 { @@ -100,6 +103,10 @@ func (o *Options) Complete(args []string) error { return errors.New("a component's Version must be defined") } + if len(o.TransportCfgPath) == 0 { + return errors.New("a path to a transport config file must be defined") + } + return nil } @@ -109,17 +116,29 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e return fmt.Errorf("unable to build oci client: %s", err.Error()) } - cds, err := ResolveRecursive(ctx, ociClient, o.BaseUrl, o.ComponentName, o.Version, o.ComponentNameMapping) + ociCache, err := cache.NewCache(log, cache.WithBasePath(o.OCIOptions.CacheDir)) + if err != nil { + return fmt.Errorf("unable to build cache: %w", err) + } + + if err := cache.InjectCacheInto(ociClient, ociCache); err != nil { + return fmt.Errorf("unable to inject cache into oci client: %w", err) + } + + sourceCtx := cdv2.NewOCIRegistryRepository(o.SourceRepository, "") + cds, err := ResolveRecursive(ctx, ociClient, *sourceCtx, o.ComponentName, o.Version) if err != nil { return fmt.Errorf("unable to resolve component: %w", err) } - targetCtx := cdv2.OCIRegistryRepository{ - ObjectType: cdv2.ObjectType{ - Type: cdv2.OCIRegistryType, - }, - BaseURL: targetCtxUrl, - ComponentNameMapping: cdv2.ComponentNameMapping(o.ComponentNameMapping), + targetCtx := cdv2.NewOCIRegistryRepository(o.TargetRepository, "") + + df := config.NewDownloaderFactory(ociClient, ociCache) + pf := config.NewProcessorFactory(ociCache) + uf := config.NewUploaderFactory(ociClient, ociCache, *targetCtx) + pjf, err := config.NewProcessingJobFactory(o.TransportCfgPath, df, pf, uf) + if err != nil { + return err } wg := sync.WaitGroup{} @@ -128,7 +147,7 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e wg.Add(1) go func() { defer wg.Done() - processedResources, errs := handleResources(ctx, cd, targetCtx, log, ociClient) + processedResources, errs := handleResources(ctx, cd, *targetCtx, log, pjf) if len(errs) > 0 { for _, err := range errs { log.Error(err, "") @@ -147,7 +166,7 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e return nil } -func handleResources(ctx context.Context, cd *cdv2.ComponentDescriptor, targetCtx cdv2.OCIRegistryRepository, log logr.Logger, ociClient ociclient.Client) ([]cdv2.Resource, []error) { +func handleResources(ctx context.Context, cd *cdv2.ComponentDescriptor, targetCtx cdv2.OCIRegistryRepository, log logr.Logger, processingJobFactory *config.ProcessingPipelineCompiler) ([]cdv2.Resource, []error) { wg := sync.WaitGroup{} errs := []error{} mux := sync.Mutex{} @@ -160,43 +179,27 @@ func handleResources(ctx context.Context, cd *cdv2.ComponentDescriptor, targetCt go func() { defer wg.Done() - procs, err := createProcessors(ociClient, targetCtx) - if err != nil { - errs = append(errs, fmt.Errorf("unable to create processors: %w", err)) - return - } - - pip := process.NewResourceProcessingPipeline(procs...) + job, err := processingJobFactory.Create(*cd, resource) if err != nil { - errs = append(errs, fmt.Errorf("unable to create pipeline: %w", err)) + errs = append(errs, fmt.Errorf("unable to create processing job: %w", err)) return } - // TODO: do we allow modifications of the component descriptor? - // If so, how do we merge the possibly different output of multiple resource pipelines? - processedCD, processedRes, err := pip.Process(ctx, *cd, resource) - if err != nil { + if err = job.Process(ctx); err != nil { errs = append(errs, fmt.Errorf("unable to process resource %+v: %w", resource, err)) return } mux.Lock() - processedResources = append(processedResources, processedRes) + processedResources = append(processedResources, *job.ProcessedResource) mux.Unlock() - mcd, err := yaml.Marshal(processedCD) - if err != nil { - errs = append(errs, fmt.Errorf("unable to marshal cd: %w", err)) - return - } - - mres, err := yaml.Marshal(processedRes) + mres, err := yaml.Marshal(*job.ProcessedResource) if err != nil { errs = append(errs, fmt.Errorf("unable to marshal res: %w", err)) return } - fmt.Println(string(mcd)) fmt.Println(string(mres)) }() } @@ -205,21 +208,14 @@ func handleResources(ctx context.Context, cd *cdv2.ComponentDescriptor, targetCt return processedResources, errs } -func ResolveRecursive(ctx context.Context, client ociclient.Client, baseUrl, componentName, componentVersion, componentNameMapping string) ([]*cdv2.ComponentDescriptor, error) { - repoCtx := cdv2.OCIRegistryRepository{ - ObjectType: cdv2.ObjectType{ - Type: cdv2.OCIRegistryType, - }, - BaseURL: baseUrl, - ComponentNameMapping: cdv2.ComponentNameMapping(componentNameMapping), - } - ociRef, err := cdoci.OCIRef(repoCtx, componentName, componentVersion) +func ResolveRecursive(ctx context.Context, client ociclient.Client, repo cdv2.OCIRegistryRepository, componentName, componentVersion string) ([]*cdv2.ComponentDescriptor, error) { + ociRef, err := cdoci.OCIRef(repo, componentName, componentVersion) if err != nil { return nil, fmt.Errorf("invalid component reference: %w", err) } cdresolver := cdoci.NewResolver(client) - cd, err := cdresolver.Resolve(ctx, &repoCtx, componentName, componentVersion) + cd, err := cdresolver.Resolve(ctx, &repo, componentName, componentVersion) if err != nil { return nil, fmt.Errorf("unable to to fetch component descriptor %s: %w", ociRef, err) } @@ -228,7 +224,7 @@ func ResolveRecursive(ctx context.Context, client ociclient.Client, baseUrl, com cd, } for _, ref := range cd.ComponentReferences { - cds2, err := ResolveRecursive(ctx, client, baseUrl, ref.ComponentName, ref.Version, componentNameMapping) + cds2, err := ResolveRecursive(ctx, client, repo, ref.ComponentName, ref.Version) if err != nil { return nil, fmt.Errorf("unable to resolve ref %+v: %w", ref, err) } @@ -237,27 +233,3 @@ func ResolveRecursive(ctx context.Context, client ociclient.Client, baseUrl, com return cds, nil } - -func createProcessors(client ociclient.Client, targetCtx cdv2.OCIRegistryRepository) ([]process.ResourceStreamProcessor, error) { - procBins := []string{ - "../../../ctt-playground/bin/processor_1", - "../../../ctt-playground/bin/processor_2", - "../../../ctt-playground/bin/processor_3", - } - - procs := []process.ResourceStreamProcessor{ - downloaders.NewLocalOCIBlobDownloader(client), - } - - for _, procBin := range procBins { - exec, err := extensions.NewStdIOExecutable(procBin, []string{}, []string{}) - if err != nil { - return nil, err - } - procs = append(procs, exec) - } - - procs = append(procs, uploaders.NewLocalOCIBlobUploader(client, targetCtx)) - - return procs, nil -} diff --git a/pkg/transport/config/pipeline.go b/pkg/transport/config/pipeline.go index bd5db784..16d9bd2e 100644 --- a/pkg/transport/config/pipeline.go +++ b/pkg/transport/config/pipeline.go @@ -4,6 +4,7 @@ package config import ( + "context" "encoding/json" "fmt" "os" @@ -14,12 +15,13 @@ import ( "sigs.k8s.io/yaml" ) -type ResourcePipeline struct { - Cd *cdv2.ComponentDescriptor - Resource *cdv2.Resource - Downloaders []ProcessorWithName - Processors []ProcessorWithName - Uploaders []ProcessorWithName +type ProcessingJob struct { + ComponentDescriptor *cdv2.ComponentDescriptor + Resource *cdv2.Resource + Downloaders []ProcessorWithName + Processors []ProcessorWithName + Uploaders []ProcessorWithName + ProcessedResource *cdv2.Resource } type ProcessorWithName struct { @@ -60,7 +62,7 @@ type ProcessorsLookup struct { rules []RD } -func NewPipelineCompiler(transportCfgPath string, df *DownloaderFactory, pf *ProcessorFactory, uf *UploaderFactory) (*ProcessingPipelineCompiler, error) { +func NewProcessingJobFactory(transportCfgPath string, df *DownloaderFactory, pf *ProcessorFactory, uf *UploaderFactory) (*ProcessingJobFactory, error) { transportCfgYaml, err := os.ReadFile(transportCfgPath) if err != nil { return nil, fmt.Errorf("unable to read transport config file: %w", err) @@ -77,7 +79,7 @@ func NewPipelineCompiler(transportCfgPath string, df *DownloaderFactory, pf *Pro return nil, fmt.Errorf("failed creating lookup table %w", err) } - c := ProcessingPipelineCompiler{ + c := ProcessingJobFactory{ lookup: compiler, downloaderFactory: df, processorFactory: pf, @@ -87,7 +89,7 @@ func NewPipelineCompiler(transportCfgPath string, df *DownloaderFactory, pf *Pro return &c, nil } -type ProcessingPipelineCompiler struct { +type ProcessingJobFactory struct { lookup *ProcessorsLookup uploaderFactory *UploaderFactory downloaderFactory *DownloaderFactory @@ -157,10 +159,10 @@ func compileFromConfig(config *transportConfig) (*ProcessorsLookup, error) { return &lookup, nil } -func (c *ProcessingPipelineCompiler) CreateResourcePipeline(cd cdv2.ComponentDescriptor, res cdv2.Resource) (*ResourcePipeline, error) { - pipeline := ResourcePipeline{ - Cd: &cd, - Resource: &res, +func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Resource) (*ProcessingJob, error) { + job := ProcessingJob{ + ComponentDescriptor: &cd, + Resource: &res, } // find matching downloader @@ -171,7 +173,7 @@ func (c *ProcessingPipelineCompiler) CreateResourcePipeline(cd cdv2.ComponentDes if err != nil { return nil, err } - pipeline.Downloaders = append(pipeline.Downloaders, ProcessorWithName{ + job.Downloaders = append(job.Downloaders, ProcessorWithName{ Name: downloader.name, Processor: dl, }) @@ -186,7 +188,7 @@ func (c *ProcessingPipelineCompiler) CreateResourcePipeline(cd cdv2.ComponentDes if err != nil { return nil, err } - pipeline.Uploaders = append(pipeline.Uploaders, ProcessorWithName{ + job.Uploaders = append(job.Uploaders, ProcessorWithName{ Name: uploader.name, Processor: ul, }) @@ -206,7 +208,7 @@ func (c *ProcessingPipelineCompiler) CreateResourcePipeline(cd cdv2.ComponentDes if err != nil { return nil, err } - pipeline.Processors = append(pipeline.Processors, ProcessorWithName{ + job.Processors = append(job.Processors, ProcessorWithName{ Name: processorDefined.name, Processor: p, }) @@ -214,7 +216,7 @@ func (c *ProcessingPipelineCompiler) CreateResourcePipeline(cd cdv2.ComponentDes } } - return &pipeline, nil + return &job, nil } func doesAllFilterMatch(filters []filter.Filter, cd cdv2.ComponentDescriptor, res cdv2.Resource) bool { @@ -246,3 +248,29 @@ func lookupProcessorByName(name string, lookup *ProcessorsLookup) (*PD, error) { } return nil, fmt.Errorf("can not find processor %s", name) } + +func (j *ProcessingJob) Process(ctx context.Context) error { + processors := []process.ResourceStreamProcessor{} + + for _, d := range j.Downloaders { + processors = append(processors, d.Processor) + } + + for _, p := range j.Processors { + processors = append(processors, p.Processor) + } + + for _, u := range j.Uploaders { + processors = append(processors, u.Processor) + } + + p := process.NewResourceProcessingPipeline(processors...) + _, processedResource, err := p.Process(ctx, *j.ComponentDescriptor, *j.Resource) + if err != nil { + return err + } + + j.ProcessedResource = &processedResource + + return nil +} From 784570744d7e780380b4698310ff34c12c2680b1 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Mon, 18 Oct 2021 14:04:39 +0200 Subject: [PATCH 34/94] adds upload of patched component descriptors --- pkg/commands/transport/transport.go | 31 ++++++++++++++++++----------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/pkg/commands/transport/transport.go b/pkg/commands/transport/transport.go index b6f39c73..7f4a5e6f 100644 --- a/pkg/commands/transport/transport.go +++ b/pkg/commands/transport/transport.go @@ -12,13 +12,13 @@ import ( "sync" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "github.com/gardener/component-spec/bindings-go/ctf" cdoci "github.com/gardener/component-spec/bindings-go/oci" "github.com/go-logr/logr" "github.com/mandelsoft/vfs/pkg/osfs" "github.com/mandelsoft/vfs/pkg/vfs" "github.com/spf13/cobra" "github.com/spf13/pflag" - "sigs.k8s.io/yaml" "github.com/gardener/component-cli/ociclient" "github.com/gardener/component-cli/ociclient/cache" @@ -151,22 +151,37 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e if len(errs) > 0 { for _, err := range errs { log.Error(err, "") + return } } cd.Resources = processedResources + manifest, err := cdoci.NewManifestBuilder(ociCache, ctf.NewComponentArchive(cd, nil)).Build(ctx) + if err != nil { + log.Error(err, "unable to build oci artifact for component acrchive") + return + } + + ociRef, err := cdoci.OCIRef(*targetCtx, o.ComponentName, o.Version) + if err != nil { + log.Error(err, "unable to build component reference") + return + } + + if err := ociClient.PushManifest(ctx, ociRef, manifest); err != nil { + log.Error(err, "unable to push manifest") + return + } }() } - fmt.Println("waiting for goroutines to finish") wg.Wait() - fmt.Println("main finished") return nil } -func handleResources(ctx context.Context, cd *cdv2.ComponentDescriptor, targetCtx cdv2.OCIRegistryRepository, log logr.Logger, processingJobFactory *config.ProcessingPipelineCompiler) ([]cdv2.Resource, []error) { +func handleResources(ctx context.Context, cd *cdv2.ComponentDescriptor, targetCtx cdv2.OCIRegistryRepository, log logr.Logger, processingJobFactory *config.ProcessingJobFactory) ([]cdv2.Resource, []error) { wg := sync.WaitGroup{} errs := []error{} mux := sync.Mutex{} @@ -193,14 +208,6 @@ func handleResources(ctx context.Context, cd *cdv2.ComponentDescriptor, targetCt mux.Lock() processedResources = append(processedResources, *job.ProcessedResource) mux.Unlock() - - mres, err := yaml.Marshal(*job.ProcessedResource) - if err != nil { - errs = append(errs, fmt.Errorf("unable to marshal res: %w", err)) - return - } - - fmt.Println(string(mres)) }() } From 5d1142bd26819c0d4e7f2fa063b6d4984ef7058e Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 19 Oct 2021 10:59:11 +0200 Subject: [PATCH 35/94] implement additional filters --- pkg/transport/config/filter_factory.go | 32 ++++++++++++++++++++ pkg/transport/filter/acess_type_filter.go | 29 ++++++++++++++++++ pkg/transport/filter/resource_type_filter.go | 29 ++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 pkg/transport/filter/acess_type_filter.go create mode 100644 pkg/transport/filter/resource_type_filter.go diff --git a/pkg/transport/config/filter_factory.go b/pkg/transport/config/filter_factory.go index b6b29893..c90cae8d 100644 --- a/pkg/transport/config/filter_factory.go +++ b/pkg/transport/config/filter_factory.go @@ -21,6 +21,10 @@ func (f *FilterFactory) Create(typ string, spec *json.RawMessage) (filter.Filter switch typ { case "ComponentFilter": return f.createComponentFilter(spec) + case "ResourceTypeFilter": + return f.createResourceTypeFilter(spec) + case "AccessTypeFilter": + return f.createAccessTypeFilter(spec) default: return nil, fmt.Errorf("unknown filter type %s", typ) } @@ -39,3 +43,31 @@ func (f *FilterFactory) createComponentFilter(rawSpec *json.RawMessage) (filter. return filter.NewComponentFilter(spec.IncludeComponentNames...) } + +func (f *FilterFactory) createResourceTypeFilter(rawSpec *json.RawMessage) (filter.Filter, error) { + type filterSpec struct { + IncludeResourceTypes []string `json:"includeResourceTypes"` + } + + var spec filterSpec + err := yaml.Unmarshal(*rawSpec, &spec) + if err != nil { + return nil, fmt.Errorf("unable to parse spec: %w", err) + } + + return filter.NewComponentFilter(spec.IncludeResourceTypes...) +} + +func (f *FilterFactory) createAccessTypeFilter(rawSpec *json.RawMessage) (filter.Filter, error) { + type filterSpec struct { + IncludeAccessTypes []string `json:"includeAccessTypes"` + } + + var spec filterSpec + err := yaml.Unmarshal(*rawSpec, &spec) + if err != nil { + return nil, fmt.Errorf("unable to parse spec: %w", err) + } + + return filter.NewAccessTypeFilter(spec.IncludeAccessTypes...) +} diff --git a/pkg/transport/filter/acess_type_filter.go b/pkg/transport/filter/acess_type_filter.go new file mode 100644 index 00000000..050d0aeb --- /dev/null +++ b/pkg/transport/filter/acess_type_filter.go @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package filter + +import ( + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +type accessTypeFilter struct { + includeAccessTypes []string +} + +func (f accessTypeFilter) Matches(cd *cdv2.ComponentDescriptor, r cdv2.Resource) bool { + for _, accessType := range f.includeAccessTypes { + if r.Access.Type == accessType { + return true + } + } + return false +} + +func NewAccessTypeFilter(includeAccessTypes ...string) (Filter, error) { + filter := accessTypeFilter{ + includeAccessTypes: includeAccessTypes, + } + + return &filter, nil +} diff --git a/pkg/transport/filter/resource_type_filter.go b/pkg/transport/filter/resource_type_filter.go new file mode 100644 index 00000000..77f1c8dc --- /dev/null +++ b/pkg/transport/filter/resource_type_filter.go @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package filter + +import ( + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +type resourceTypeFilter struct { + includeResourceTypes []string +} + +func (f resourceTypeFilter) Matches(cd *cdv2.ComponentDescriptor, r cdv2.Resource) bool { + for _, resourceType := range f.includeResourceTypes { + if r.Type == resourceType { + return true + } + } + return false +} + +func NewResourceTypeFilter(includeResourceTypes ...string) (Filter, error) { + filter := resourceTypeFilter{ + includeResourceTypes: includeResourceTypes, + } + + return &filter, nil +} From 5c3a5c2d665985ea6e7105e1bfc522d17a17cf7a Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 19 Oct 2021 12:18:47 +0200 Subject: [PATCH 36/94] makes transport cmd run through --- cmd/component-cli/app/app.go | 2 -- pkg/commands/transport/transport.go | 11 +++++----- pkg/transport/config/downloader_factory.go | 22 +++---------------- pkg/transport/config/filter_factory.go | 2 +- pkg/transport/config/pipeline.go | 15 +++++-------- pkg/transport/config/types.go | 9 +++++--- pkg/transport/config/uploader_factory.go | 4 ++-- ...s_type_filter.go => access_type_filter.go} | 0 8 files changed, 23 insertions(+), 42 deletions(-) rename pkg/transport/filter/{acess_type_filter.go => access_type_filter.go} (100%) diff --git a/cmd/component-cli/app/app.go b/cmd/component-cli/app/app.go index ce052f58..f0efad73 100644 --- a/cmd/component-cli/app/app.go +++ b/cmd/component-cli/app/app.go @@ -19,7 +19,6 @@ import ( "github.com/gardener/component-cli/pkg/commands/transport" "github.com/gardener/component-cli/pkg/logcontext" "github.com/gardener/component-cli/pkg/logger" - "github.com/gardener/component-cli/pkg/transport/config" "github.com/gardener/component-cli/pkg/version" "github.com/spf13/cobra" @@ -51,7 +50,6 @@ func NewComponentsCliCommand(ctx context.Context) *cobra.Command { cmd.AddCommand(oci.NewOCICommand(ctx)) cmd.AddCommand(cachecmd.NewCacheCommand(ctx)) cmd.AddCommand(transport.NewTransportCommand(ctx)) - cmd.AddCommand(config.NewConfigParseCommand(ctx)) cmd.AddCommand(transport.NewTestCommand(ctx)) return cmd diff --git a/pkg/commands/transport/transport.go b/pkg/commands/transport/transport.go index 7f4a5e6f..9de0b714 100644 --- a/pkg/commands/transport/transport.go +++ b/pkg/commands/transport/transport.go @@ -80,9 +80,8 @@ func (o *Options) AddFlags(fs *pflag.FlagSet) { } func (o *Options) Complete(args []string) error { - o.SourceRepository = args[0] - o.ComponentName = args[1] - o.Version = args[2] + o.ComponentName = args[0] + o.Version = args[1] cliHomeDir, err := constants.CliHomeDir() if err != nil { @@ -93,9 +92,6 @@ func (o *Options) Complete(args []string) error { return fmt.Errorf("unable to create cache directory %s: %w", o.OCIOptions.CacheDir, err) } - if len(o.SourceRepository) == 0 { - return errors.New("the base url must be defined") - } if len(o.ComponentName) == 0 { return errors.New("a component name must be defined") } @@ -103,6 +99,9 @@ func (o *Options) Complete(args []string) error { return errors.New("a component's Version must be defined") } + if len(o.SourceRepository) == 0 { + return errors.New("the base url must be defined") + } if len(o.TransportCfgPath) == 0 { return errors.New("a path to a transport config file must be defined") } diff --git a/pkg/transport/config/downloader_factory.go b/pkg/transport/config/downloader_factory.go index 963a20e1..22c5f36c 100644 --- a/pkg/transport/config/downloader_factory.go +++ b/pkg/transport/config/downloader_factory.go @@ -11,7 +11,6 @@ import ( "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/transport/process/downloaders" - "sigs.k8s.io/yaml" ) func NewDownloaderFactory(client ociclient.Client, ocicache cache.Cache) *DownloaderFactory { @@ -28,28 +27,13 @@ type DownloaderFactory struct { func (f *DownloaderFactory) Create(typ string, spec *json.RawMessage) (process.ResourceStreamProcessor, error) { switch typ { - case "localOCIBlob": + case "localOciBlobDL": return downloaders.NewLocalOCIBlobDownloader(f.client), nil - case "ociImage": - return f.createOCIImageDownloader(spec) + case "ociImageDL": + return downloaders.NewOCIImageDownloader(f.client, f.cache), nil case "executable": return createExecutable(spec) default: return nil, fmt.Errorf("unknown downloader type %s", typ) } } - -func (f *DownloaderFactory) createOCIImageDownloader(rawSpec *json.RawMessage) (process.ResourceStreamProcessor, error) { - type downloaderSpec struct { - BaseUrl string `json:"baseUrl"` - KeepSourceRepo bool `json:"keepSourceRepo"` - } - - var spec downloaderSpec - err := yaml.Unmarshal(*rawSpec, &spec) - if err != nil { - return nil, fmt.Errorf("unable to parse spec: %w", err) - } - - return downloaders.NewOCIImageDownloader(f.client, f.cache), nil -} diff --git a/pkg/transport/config/filter_factory.go b/pkg/transport/config/filter_factory.go index c90cae8d..a4f18cbc 100644 --- a/pkg/transport/config/filter_factory.go +++ b/pkg/transport/config/filter_factory.go @@ -55,7 +55,7 @@ func (f *FilterFactory) createResourceTypeFilter(rawSpec *json.RawMessage) (filt return nil, fmt.Errorf("unable to parse spec: %w", err) } - return filter.NewComponentFilter(spec.IncludeResourceTypes...) + return filter.NewResourceTypeFilter(spec.IncludeResourceTypes...) } func (f *FilterFactory) createAccessTypeFilter(rawSpec *json.RawMessage) (filter.Filter, error) { diff --git a/pkg/transport/config/pipeline.go b/pkg/transport/config/pipeline.go index 16d9bd2e..18290a0d 100644 --- a/pkg/transport/config/pipeline.go +++ b/pkg/transport/config/pipeline.go @@ -167,8 +167,7 @@ func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Reso // find matching downloader for _, downloader := range c.lookup.downloaders { - matches := doesAllFilterMatch(downloader.filters, cd, res) - if matches { + if doesAllFilterMatch(downloader.filters, cd, res) { dl, err := c.downloaderFactory.Create(string(downloader.typ), downloader.spec) if err != nil { return nil, err @@ -182,9 +181,8 @@ func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Reso // find matching uploader for _, uploader := range c.lookup.uploaders { - matches := doesAllFilterMatch(uploader.filters, cd, res) - if matches { - ul, err := c.downloaderFactory.Create(string(uploader.typ), uploader.spec) + if doesAllFilterMatch(uploader.filters, cd, res) { + ul, err := c.uploaderFactory.Create(string(uploader.typ), uploader.spec) if err != nil { return nil, err } @@ -197,8 +195,7 @@ func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Reso // loop through all rules to find corresponding processors for _, rule := range c.lookup.rules { - matches := doesAllFilterMatch(rule.filters, cd, res) - if matches { + if doesAllFilterMatch(rule.filters, cd, res) { for _, processorName := range rule.processors { processorDefined, err := lookupProcessorByName(processorName, c.lookup) if err != nil { @@ -231,9 +228,9 @@ func doesAllFilterMatch(filters []filter.Filter, cd cdv2.ComponentDescriptor, re func createFilterList(filterDefinitions []FilterDefinition, ff *FilterFactory) ([]filter.Filter, error) { var filters []filter.Filter for _, f := range filterDefinitions { - filter, err := ff.Create(f.Type, f.Args) + filter, err := ff.Create(f.Type, f.Spec) if err != nil { - return nil, fmt.Errorf("error creating filter list for type %s with args %s: %w", f.Type, string(*f.Args), err) + return nil, fmt.Errorf("error creating filter list for type %s with args %s: %w", f.Type, string(*f.Spec), err) } filters = append(filters, filter) } diff --git a/pkg/transport/config/types.go b/pkg/transport/config/types.go index 72b1ee59..0fcf751f 100644 --- a/pkg/transport/config/types.go +++ b/pkg/transport/config/types.go @@ -5,9 +5,12 @@ package config import "encoding/json" +type meta struct { + Version string `json:"version"` +} + type transportConfig struct { - Meta string - Version string `json:"version"` + Meta meta `json:"meta"` Uploaders []UploaderDefinition `json:"uploaders"` Processors []ProcessorDefinition `json:"processors"` Downloaders []DownloaderDefinition `json:"downloaders"` @@ -32,7 +35,7 @@ type HookDefinition struct { type FilterDefinition struct { Type string `json:"type"` - Args *json.RawMessage `json:"args"` + Spec *json.RawMessage `json:"spec"` } type DownloaderDefinition struct { diff --git a/pkg/transport/config/uploader_factory.go b/pkg/transport/config/uploader_factory.go index 1e2a3a28..4f0aa2b2 100644 --- a/pkg/transport/config/uploader_factory.go +++ b/pkg/transport/config/uploader_factory.go @@ -31,9 +31,9 @@ type UploaderFactory struct { func (f *UploaderFactory) Create(typ string, spec *json.RawMessage) (process.ResourceStreamProcessor, error) { switch typ { - case "localOCIBlob": + case "localOciBlobUL": return uploaders.NewLocalOCIBlobUploader(f.client, f.targetCtx), nil - case "ociImage": + case "ociImageUL": return f.createOCIImageUploader(spec) case "executable": return createExecutable(spec) diff --git a/pkg/transport/filter/acess_type_filter.go b/pkg/transport/filter/access_type_filter.go similarity index 100% rename from pkg/transport/filter/acess_type_filter.go rename to pkg/transport/filter/access_type_filter.go From 21ce176a0152b9d4b56edc07a2c747fc664e4344 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 20 Oct 2021 15:26:55 +0200 Subject: [PATCH 37/94] fixes filtering of gzip compressed layers --- .../process/processors/oci_image_filter.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pkg/transport/process/processors/oci_image_filter.go b/pkg/transport/process/processors/oci_image_filter.go index 9d560bb4..f1a52802 100644 --- a/pkg/transport/process/processors/oci_image_filter.go +++ b/pkg/transport/process/processors/oci_image_filter.go @@ -104,9 +104,11 @@ func (f *ociImageFilter) filterImage(manifest oci.Manifest) (*oci.Manifest, erro return nil, fmt.Errorf("unable to create tempfile: %w", err) } defer tmpfile.Close() - var layerBlobWriter io.Writer = tmpfile + var layerBlobWriter io.WriteCloser = tmpfile - if layer.MediaType == ocispecv1.MediaTypeImageLayerGzip || layer.MediaType == images.MediaTypeDockerSchema2LayerGzip { + isGzipCompressedLayer := layer.MediaType == ocispecv1.MediaTypeImageLayerGzip || layer.MediaType == images.MediaTypeDockerSchema2LayerGzip + + if isGzipCompressedLayer { layerBlobReader, err = gzip.NewReader(layerBlobReader) if err != nil { return nil, fmt.Errorf("unable to create gzip reader for layer: %w", err) @@ -123,6 +125,13 @@ func (f *ociImageFilter) filterImage(manifest oci.Manifest) (*oci.Manifest, erro return nil, fmt.Errorf("unable to filter blob: %w", err) } + if isGzipCompressedLayer { + // close gzip writer (flushes any unwritten data and writes gzip footer) + if err := layerBlobWriter.Close(); err != nil { + return nil, fmt.Errorf("unable to close layer writer: %w", err) + } + } + if _, err := tmpfile.Seek(0, io.SeekStart); err != nil { return nil, fmt.Errorf("unable to reset input file: %s", err) } From 48955f9010e595015514ab00b9df13d8f7bc44fb Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 20 Oct 2021 15:27:10 +0200 Subject: [PATCH 38/94] increase processor timeout --- pkg/transport/process/pipeline.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/transport/process/pipeline.go b/pkg/transport/process/pipeline.go index 52eaeddb..7587c23a 100644 --- a/pkg/transport/process/pipeline.go +++ b/pkg/transport/process/pipeline.go @@ -14,7 +14,7 @@ import ( cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" ) -const processorTimeout = 30 * time.Second +const processorTimeout = 60 * time.Second type resourceProcessingPipelineImpl struct { processors []ResourceStreamProcessor From 4ca458d702ed5a845dc1ccb69c0f4e89b07e0a6a Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 20 Oct 2021 16:40:55 +0200 Subject: [PATCH 39/94] refactoring --- pkg/transport/config/downloader_factory.go | 11 ++++++++--- pkg/transport/config/filter_factory.go | 18 ++++++++++++------ pkg/transport/config/{pipeline.go => job.go} | 0 pkg/transport/config/processor_factory.go | 17 +++++++++++------ pkg/transport/config/uploader_factory.go | 11 ++++++++--- pkg/transport/config/util.go | 6 +++++- ...nent_filter.go => component_name_filter.go} | 8 ++++---- pkg/transport/filter/{filter.go => types.go} | 1 + .../{labelling.go => resource_labeler.go} | 10 +++++----- ...elling_test.go => resource_labeler_test.go} | 0 10 files changed, 54 insertions(+), 28 deletions(-) rename pkg/transport/config/{pipeline.go => job.go} (100%) rename pkg/transport/filter/{component_filter.go => component_name_filter.go} (76%) rename pkg/transport/filter/{filter.go => types.go} (73%) rename pkg/transport/process/processors/{labelling.go => resource_labeler.go} (70%) rename pkg/transport/process/processors/{labelling_test.go => resource_labeler_test.go} (100%) diff --git a/pkg/transport/config/downloader_factory.go b/pkg/transport/config/downloader_factory.go index 22c5f36c..5104851a 100644 --- a/pkg/transport/config/downloader_factory.go +++ b/pkg/transport/config/downloader_factory.go @@ -13,6 +13,11 @@ import ( "github.com/gardener/component-cli/pkg/transport/process/downloaders" ) +const ( + LocalOCIBlobDownloaderType = "LocalOciBlobDownloader" + OCIImageDownloaderType = "OciImageDownloader" +) + func NewDownloaderFactory(client ociclient.Client, ocicache cache.Cache) *DownloaderFactory { return &DownloaderFactory{ client: client, @@ -27,11 +32,11 @@ type DownloaderFactory struct { func (f *DownloaderFactory) Create(typ string, spec *json.RawMessage) (process.ResourceStreamProcessor, error) { switch typ { - case "localOciBlobDL": + case LocalOCIBlobDownloaderType: return downloaders.NewLocalOCIBlobDownloader(f.client), nil - case "ociImageDL": + case OCIImageDownloaderType: return downloaders.NewOCIImageDownloader(f.client, f.cache), nil - case "executable": + case ExecutableType: return createExecutable(spec) default: return nil, fmt.Errorf("unknown downloader type %s", typ) diff --git a/pkg/transport/config/filter_factory.go b/pkg/transport/config/filter_factory.go index a4f18cbc..aa4053c0 100644 --- a/pkg/transport/config/filter_factory.go +++ b/pkg/transport/config/filter_factory.go @@ -11,6 +11,12 @@ import ( "sigs.k8s.io/yaml" ) +const ( + ComponentNameFilterType = "ComponentNameFilter" + ResourceTypeFilterType = "ResourceTypeFilter" + AccessTypeFilterType = "AccessTypeFilter" +) + func NewFilterFactory() *FilterFactory { return &FilterFactory{} } @@ -19,18 +25,18 @@ type FilterFactory struct{} func (f *FilterFactory) Create(typ string, spec *json.RawMessage) (filter.Filter, error) { switch typ { - case "ComponentFilter": - return f.createComponentFilter(spec) - case "ResourceTypeFilter": + case ComponentNameFilterType: + return f.createComponentNameFilter(spec) + case ResourceTypeFilterType: return f.createResourceTypeFilter(spec) - case "AccessTypeFilter": + case AccessTypeFilterType: return f.createAccessTypeFilter(spec) default: return nil, fmt.Errorf("unknown filter type %s", typ) } } -func (f *FilterFactory) createComponentFilter(rawSpec *json.RawMessage) (filter.Filter, error) { +func (f *FilterFactory) createComponentNameFilter(rawSpec *json.RawMessage) (filter.Filter, error) { type filterSpec struct { IncludeComponentNames []string `json:"includeComponentNames"` } @@ -41,7 +47,7 @@ func (f *FilterFactory) createComponentFilter(rawSpec *json.RawMessage) (filter. return nil, fmt.Errorf("unable to parse spec: %w", err) } - return filter.NewComponentFilter(spec.IncludeComponentNames...) + return filter.NewComponentNameFilter(spec.IncludeComponentNames...) } func (f *FilterFactory) createResourceTypeFilter(rawSpec *json.RawMessage) (filter.Filter, error) { diff --git a/pkg/transport/config/pipeline.go b/pkg/transport/config/job.go similarity index 100% rename from pkg/transport/config/pipeline.go rename to pkg/transport/config/job.go diff --git a/pkg/transport/config/processor_factory.go b/pkg/transport/config/processor_factory.go index 08ed1f62..80850e2d 100644 --- a/pkg/transport/config/processor_factory.go +++ b/pkg/transport/config/processor_factory.go @@ -14,6 +14,11 @@ import ( "sigs.k8s.io/yaml" ) +const ( + ResourceLabelerProcessorType = "ResourceLabeler" + OCIImageFilterProcessorType = "OciImageFilter" +) + func NewProcessorFactory(ociCache cache.Cache) *ProcessorFactory { return &ProcessorFactory{ cache: ociCache, @@ -26,18 +31,18 @@ type ProcessorFactory struct { func (f *ProcessorFactory) Create(typ string, spec *json.RawMessage) (process.ResourceStreamProcessor, error) { switch typ { - case "label": - return f.createLabellingProcessor(spec) - case "ociImageFilter": + case ResourceLabelerProcessorType: + return f.createResourceLabeler(spec) + case OCIImageFilterProcessorType: return f.createOCIImageFilter(spec) - case "executable": + case ExecutableType: return createExecutable(spec) default: return nil, fmt.Errorf("unknown processor type %s", typ) } } -func (f *ProcessorFactory) createLabellingProcessor(rawSpec *json.RawMessage) (process.ResourceStreamProcessor, error) { +func (f *ProcessorFactory) createResourceLabeler(rawSpec *json.RawMessage) (process.ResourceStreamProcessor, error) { type processorSpec struct { Labels cdv2.Labels `json:"labels"` } @@ -48,7 +53,7 @@ func (f *ProcessorFactory) createLabellingProcessor(rawSpec *json.RawMessage) (p return nil, fmt.Errorf("unable to parse spec: %w", err) } - return processors.NewLabellingProcessor(spec.Labels...), nil + return processors.NewResourceLabeler(spec.Labels...), nil } func (f *ProcessorFactory) createOCIImageFilter(rawSpec *json.RawMessage) (process.ResourceStreamProcessor, error) { diff --git a/pkg/transport/config/uploader_factory.go b/pkg/transport/config/uploader_factory.go index 4f0aa2b2..0b2d6c58 100644 --- a/pkg/transport/config/uploader_factory.go +++ b/pkg/transport/config/uploader_factory.go @@ -15,6 +15,11 @@ import ( "sigs.k8s.io/yaml" ) +const ( + LocalOCIBlobUploaderType = "localOciBlobUL" + OCIImageUploaderType = "ociImageUL" +) + func NewUploaderFactory(client ociclient.Client, ocicache cache.Cache, targetCtx cdv2.OCIRegistryRepository) *UploaderFactory { return &UploaderFactory{ client: client, @@ -31,11 +36,11 @@ type UploaderFactory struct { func (f *UploaderFactory) Create(typ string, spec *json.RawMessage) (process.ResourceStreamProcessor, error) { switch typ { - case "localOciBlobUL": + case LocalOCIBlobUploaderType: return uploaders.NewLocalOCIBlobUploader(f.client, f.targetCtx), nil - case "ociImageUL": + case OCIImageUploaderType: return f.createOCIImageUploader(spec) - case "executable": + case ExecutableType: return createExecutable(spec) default: return nil, fmt.Errorf("unknown uploader type %s", typ) diff --git a/pkg/transport/config/util.go b/pkg/transport/config/util.go index 559911f1..5f217f0d 100644 --- a/pkg/transport/config/util.go +++ b/pkg/transport/config/util.go @@ -12,6 +12,10 @@ import ( "sigs.k8s.io/yaml" ) +const ( + ExecutableType = "executable" +) + func createExecutable(rawSpec *json.RawMessage) (process.ResourceStreamProcessor, error) { type executableSpec struct { Bin string @@ -23,6 +27,6 @@ func createExecutable(rawSpec *json.RawMessage) (process.ResourceStreamProcessor if err := yaml.Unmarshal(*rawSpec, &spec); err != nil { return nil, fmt.Errorf("unable to parse spec: %w", err) } - + return extensions.NewUDSExecutable(spec.Bin, spec.Args, spec.Env) } diff --git a/pkg/transport/filter/component_filter.go b/pkg/transport/filter/component_name_filter.go similarity index 76% rename from pkg/transport/filter/component_filter.go rename to pkg/transport/filter/component_name_filter.go index f1034a3f..bc8bd200 100644 --- a/pkg/transport/filter/component_filter.go +++ b/pkg/transport/filter/component_name_filter.go @@ -10,11 +10,11 @@ import ( cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" ) -type componentFilter struct { +type componentNameFilter struct { includeComponentNames []*regexp.Regexp } -func (f componentFilter) Matches(cd *cdv2.ComponentDescriptor, r cdv2.Resource) bool { +func (f componentNameFilter) Matches(cd *cdv2.ComponentDescriptor, r cdv2.Resource) bool { var matches bool for _, icn := range f.includeComponentNames { if matches = icn.MatchString(cd.Name); matches { @@ -24,7 +24,7 @@ func (f componentFilter) Matches(cd *cdv2.ComponentDescriptor, r cdv2.Resource) return matches } -func NewComponentFilter(includeComponentNames ...string) (Filter, error) { +func NewComponentNameFilter(includeComponentNames ...string) (Filter, error) { icnRegexps := []*regexp.Regexp{} for _, icn := range includeComponentNames { icnRegexp, err := regexp.Compile(icn) @@ -34,7 +34,7 @@ func NewComponentFilter(includeComponentNames ...string) (Filter, error) { icnRegexps = append(icnRegexps, icnRegexp) } - filter := componentFilter{ + filter := componentNameFilter{ includeComponentNames: icnRegexps, } diff --git a/pkg/transport/filter/filter.go b/pkg/transport/filter/types.go similarity index 73% rename from pkg/transport/filter/filter.go rename to pkg/transport/filter/types.go index 745d2376..97468703 100644 --- a/pkg/transport/filter/filter.go +++ b/pkg/transport/filter/types.go @@ -8,5 +8,6 @@ import ( ) type Filter interface { + // Matches receives a component descriptor and a resource and returns whether they match the filter criteria Matches(*cdv2.ComponentDescriptor, cdv2.Resource) bool } diff --git a/pkg/transport/process/processors/labelling.go b/pkg/transport/process/processors/resource_labeler.go similarity index 70% rename from pkg/transport/process/processors/labelling.go rename to pkg/transport/process/processors/resource_labeler.go index 7cc17e39..6b24b052 100644 --- a/pkg/transport/process/processors/labelling.go +++ b/pkg/transport/process/processors/resource_labeler.go @@ -13,19 +13,19 @@ import ( "github.com/gardener/component-cli/pkg/transport/process" ) -type labellingProcessor struct { +type resourceLabeler struct { labels cdv2.Labels } -// NewLabellingProcessor returns a processor that appends one or more labels to a resource -func NewLabellingProcessor(labels ...cdv2.Label) process.ResourceStreamProcessor { - obj := labellingProcessor{ +// NewResourceLabeler returns a processor that appends one or more labels to a resource +func NewResourceLabeler(labels ...cdv2.Label) process.ResourceStreamProcessor { + obj := resourceLabeler{ labels: labels, } return &obj } -func (p *labellingProcessor) Process(ctx context.Context, r io.Reader, w io.Writer) error { +func (p *resourceLabeler) Process(ctx context.Context, r io.Reader, w io.Writer) error { cd, res, resBlobReader, err := process.ReadProcessorMessage(r) if err != nil { return fmt.Errorf("unable to read processor message: %w", err) diff --git a/pkg/transport/process/processors/labelling_test.go b/pkg/transport/process/processors/resource_labeler_test.go similarity index 100% rename from pkg/transport/process/processors/labelling_test.go rename to pkg/transport/process/processors/resource_labeler_test.go From e183dc3775db0891a38024a7fc86d3d83512ac07 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 20 Oct 2021 18:06:44 +0200 Subject: [PATCH 40/94] refactoring --- .../config/{job.go => processing_job.go} | 164 +++++++++--------- pkg/transport/config/transport_config.go | 54 ++++++ pkg/transport/config/types.go | 65 ------- pkg/transport/config/uploader_factory.go | 4 +- pkg/transport/process/pipeline_test.go | 4 +- 5 files changed, 140 insertions(+), 151 deletions(-) rename pkg/transport/config/{job.go => processing_job.go} (51%) create mode 100644 pkg/transport/config/transport_config.go delete mode 100644 pkg/transport/config/types.go diff --git a/pkg/transport/config/job.go b/pkg/transport/config/processing_job.go similarity index 51% rename from pkg/transport/config/job.go rename to pkg/transport/config/processing_job.go index 18290a0d..ae61ff3d 100644 --- a/pkg/transport/config/job.go +++ b/pkg/transport/config/processing_job.go @@ -18,48 +18,48 @@ import ( type ProcessingJob struct { ComponentDescriptor *cdv2.ComponentDescriptor Resource *cdv2.Resource - Downloaders []ProcessorWithName - Processors []ProcessorWithName - Uploaders []ProcessorWithName + Downloaders []namedResourceStreamProcessor + Processors []namedResourceStreamProcessor + Uploaders []namedResourceStreamProcessor ProcessedResource *cdv2.Resource } -type ProcessorWithName struct { +type namedResourceStreamProcessor struct { Processor process.ResourceStreamProcessor Name string } -type DD struct { - name string - typ ExtensionType - spec *json.RawMessage - filters []filter.Filter +type parsedDownloaderDefinition struct { + Name string + Type string + Spec *json.RawMessage + Filters []filter.Filter } -type PD struct { - name string - typ ExtensionType - spec *json.RawMessage +type parsedProcessorDefinition struct { + Name string + Type string + Spec *json.RawMessage } -type UD struct { - name string - typ ExtensionType - spec *json.RawMessage - filters []filter.Filter +type parsedUploaderDefinition struct { + Name string + Type string + Spec *json.RawMessage + Filters []filter.Filter } -type RD struct { - name string - processors []string - filters []filter.Filter +type parsedRuleDefinition struct { + Name string + Processors []string + Filters []filter.Filter } -type ProcessorsLookup struct { - downloaders []DD - processors []PD - uploaders []UD - rules []RD +type parsedTransportConfig struct { + Downloaders []parsedDownloaderDefinition + Processors []parsedProcessorDefinition + Uploaders []parsedUploaderDefinition + Rules []parsedRuleDefinition } func NewProcessingJobFactory(transportCfgPath string, df *DownloaderFactory, pf *ProcessorFactory, uf *UploaderFactory) (*ProcessingJobFactory, error) { @@ -74,13 +74,13 @@ func NewProcessingJobFactory(transportCfgPath string, df *DownloaderFactory, pf return nil, fmt.Errorf("unable to parse transport config file: %w", err) } - compiler, err := compileFromConfig(&transportCfg) + parsedTransportConfig, err := parseTransportConfig(&transportCfg) if err != nil { return nil, fmt.Errorf("failed creating lookup table %w", err) } c := ProcessingJobFactory{ - lookup: compiler, + parsedConfig: parsedTransportConfig, downloaderFactory: df, processorFactory: pf, uploaderFactory: uf, @@ -90,37 +90,37 @@ func NewProcessingJobFactory(transportCfgPath string, df *DownloaderFactory, pf } type ProcessingJobFactory struct { - lookup *ProcessorsLookup + parsedConfig *parsedTransportConfig uploaderFactory *UploaderFactory downloaderFactory *DownloaderFactory processorFactory *ProcessorFactory } -// Create a ProcessingPipelineCompiler on the base of a config -func compileFromConfig(config *transportConfig) (*ProcessorsLookup, error) { - var lookup ProcessorsLookup +// Create a ProcessorsLookup on the base of a config +func parseTransportConfig(config *transportConfig) (*parsedTransportConfig, error) { + var parsedConfig parsedTransportConfig ff := NewFilterFactory() - // downloader + // downloaders for _, downloaderDefinition := range config.Downloaders { filters, err := createFilterList(downloaderDefinition.Filters, ff) if err != nil { - return nil, fmt.Errorf("failed creating downloader %s: %w", downloaderDefinition.Name, err) + return nil, fmt.Errorf("unable to create downloader %s: %w", downloaderDefinition.Name, err) } - lookup.downloaders = append(lookup.downloaders, DD{ - name: downloaderDefinition.Name, - typ: downloaderDefinition.Type, - spec: downloaderDefinition.Spec, - filters: filters, + parsedConfig.Downloaders = append(parsedConfig.Downloaders, parsedDownloaderDefinition{ + Name: downloaderDefinition.Name, + Type: downloaderDefinition.Type, + Spec: downloaderDefinition.Spec, + Filters: filters, }) } // processors for _, processorsDefinition := range config.Processors { - lookup.processors = append(lookup.processors, PD{ - name: processorsDefinition.Name, - typ: processorsDefinition.Type, - spec: processorsDefinition.Spec, + parsedConfig.Processors = append(parsedConfig.Processors, parsedProcessorDefinition{ + Name: processorsDefinition.Name, + Type: processorsDefinition.Type, + Spec: processorsDefinition.Spec, }) } @@ -128,13 +128,13 @@ func compileFromConfig(config *transportConfig) (*ProcessorsLookup, error) { for _, uploaderDefinition := range config.Uploaders { filters, err := createFilterList(uploaderDefinition.Filters, ff) if err != nil { - return nil, fmt.Errorf("failed creating downloader %s: %w", uploaderDefinition.Name, err) + return nil, fmt.Errorf("unable to create uploader %s: %w", uploaderDefinition.Name, err) } - lookup.uploaders = append(lookup.uploaders, UD{ - name: uploaderDefinition.Name, - typ: uploaderDefinition.Type, - spec: uploaderDefinition.Spec, - filters: filters, + parsedConfig.Uploaders = append(parsedConfig.Uploaders, parsedUploaderDefinition{ + Name: uploaderDefinition.Name, + Type: uploaderDefinition.Type, + Spec: uploaderDefinition.Spec, + Filters: filters, }) } @@ -146,17 +146,17 @@ func compileFromConfig(config *transportConfig) (*ProcessorsLookup, error) { } filters, err := createFilterList(rule.Filters, ff) if err != nil { - return nil, fmt.Errorf("failed creating rule %s: %w", rule.Name, err) + return nil, fmt.Errorf("unable to create rule %s: %w", rule.Name, err) } - ruleLookup := RD{ - name: rule.Name, - processors: processors, - filters: filters, + ruleLookup := parsedRuleDefinition{ + Name: rule.Name, + Processors: processors, + Filters: filters, } - lookup.rules = append(lookup.rules, ruleLookup) + parsedConfig.Rules = append(parsedConfig.Rules, ruleLookup) } - return &lookup, nil + return &parsedConfig, nil } func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Resource) (*ProcessingJob, error) { @@ -166,47 +166,47 @@ func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Reso } // find matching downloader - for _, downloader := range c.lookup.downloaders { - if doesAllFilterMatch(downloader.filters, cd, res) { - dl, err := c.downloaderFactory.Create(string(downloader.typ), downloader.spec) + for _, downloader := range c.parsedConfig.Downloaders { + if areAllFiltersMatching(downloader.Filters, cd, res) { + dl, err := c.downloaderFactory.Create(string(downloader.Type), downloader.Spec) if err != nil { return nil, err } - job.Downloaders = append(job.Downloaders, ProcessorWithName{ - Name: downloader.name, + job.Downloaders = append(job.Downloaders, namedResourceStreamProcessor{ + Name: downloader.Name, Processor: dl, }) } } // find matching uploader - for _, uploader := range c.lookup.uploaders { - if doesAllFilterMatch(uploader.filters, cd, res) { - ul, err := c.uploaderFactory.Create(string(uploader.typ), uploader.spec) + for _, uploader := range c.parsedConfig.Uploaders { + if areAllFiltersMatching(uploader.Filters, cd, res) { + ul, err := c.uploaderFactory.Create(string(uploader.Type), uploader.Spec) if err != nil { return nil, err } - job.Uploaders = append(job.Uploaders, ProcessorWithName{ - Name: uploader.name, + job.Uploaders = append(job.Uploaders, namedResourceStreamProcessor{ + Name: uploader.Name, Processor: ul, }) } } - // loop through all rules to find corresponding processors - for _, rule := range c.lookup.rules { - if doesAllFilterMatch(rule.filters, cd, res) { - for _, processorName := range rule.processors { - processorDefined, err := lookupProcessorByName(processorName, c.lookup) + // find matching processing rules + for _, rule := range c.parsedConfig.Rules { + if areAllFiltersMatching(rule.Filters, cd, res) { + for _, processorName := range rule.Processors { + processorDefined, err := findProcessorByName(processorName, c.parsedConfig) if err != nil { - return nil, fmt.Errorf("failed compiling rule %s: %w", rule.name, err) + return nil, fmt.Errorf("failed compiling rule %s: %w", rule.Name, err) } - p, err := c.processorFactory.Create(string(processorDefined.typ), processorDefined.spec) + p, err := c.processorFactory.Create(string(processorDefined.Type), processorDefined.Spec) if err != nil { return nil, err } - job.Processors = append(job.Processors, ProcessorWithName{ - Name: processorDefined.name, + job.Processors = append(job.Processors, namedResourceStreamProcessor{ + Name: processorDefined.Name, Processor: p, }) } @@ -216,7 +216,7 @@ func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Reso return &job, nil } -func doesAllFilterMatch(filters []filter.Filter, cd cdv2.ComponentDescriptor, res cdv2.Resource) bool { +func areAllFiltersMatching(filters []filter.Filter, cd cdv2.ComponentDescriptor, res cdv2.Resource) bool { for _, filter := range filters { if !filter.Matches(&cd, res) { return false @@ -225,7 +225,7 @@ func doesAllFilterMatch(filters []filter.Filter, cd cdv2.ComponentDescriptor, re return true } -func createFilterList(filterDefinitions []FilterDefinition, ff *FilterFactory) ([]filter.Filter, error) { +func createFilterList(filterDefinitions []filterDefinition, ff *FilterFactory) ([]filter.Filter, error) { var filters []filter.Filter for _, f := range filterDefinitions { filter, err := ff.Create(f.Type, f.Spec) @@ -237,13 +237,13 @@ func createFilterList(filterDefinitions []FilterDefinition, ff *FilterFactory) ( return filters, nil } -func lookupProcessorByName(name string, lookup *ProcessorsLookup) (*PD, error) { - for _, processor := range lookup.processors { - if processor.name == name { +func findProcessorByName(name string, lookup *parsedTransportConfig) (*parsedProcessorDefinition, error) { + for _, processor := range lookup.Processors { + if processor.Name == name { return &processor, nil } } - return nil, fmt.Errorf("can not find processor %s", name) + return nil, fmt.Errorf("unable to find processor %s", name) } func (j *ProcessingJob) Process(ctx context.Context) error { diff --git a/pkg/transport/config/transport_config.go b/pkg/transport/config/transport_config.go new file mode 100644 index 00000000..ee08a613 --- /dev/null +++ b/pkg/transport/config/transport_config.go @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package config + +import "encoding/json" + +type meta struct { + Version string `json:"version"` +} + +type transportConfig struct { + Meta meta `json:"meta"` + Uploaders []uploaderDefinition `json:"uploaders"` + Processors []processorDefinition `json:"processors"` + Downloaders []downloaderDefinition `json:"downloaders"` + Rules []rule `json:"rules"` +} + +type baseProcessorDefinition struct { + Name string `json:"name"` + Type string `json:"type"` + Spec *json.RawMessage `json:"spec"` +} + +type filterDefinition struct { + Type string `json:"type"` + Spec *json.RawMessage `json:"spec"` +} + +type downloaderDefinition struct { + baseProcessorDefinition + Filters []filterDefinition `json:"filters"` +} + +type uploaderDefinition struct { + baseProcessorDefinition + Filters []filterDefinition `json:"filters"` +} + +type processorDefinition struct { + baseProcessorDefinition +} + +type processorReference struct { + Name string `json:"name"` + Type string `json:"type"` +} + +type rule struct { + Name string + Filters []filterDefinition `json:"filters"` + Processors []processorReference `json:"processors"` +} diff --git a/pkg/transport/config/types.go b/pkg/transport/config/types.go deleted file mode 100644 index 0fcf751f..00000000 --- a/pkg/transport/config/types.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. -// -// SPDX-License-Identifier: Apache-2.0 -package config - -import "encoding/json" - -type meta struct { - Version string `json:"version"` -} - -type transportConfig struct { - Meta meta `json:"meta"` - Uploaders []UploaderDefinition `json:"uploaders"` - Processors []ProcessorDefinition `json:"processors"` - Downloaders []DownloaderDefinition `json:"downloaders"` - Rules []Rule `json:"rules"` -} - -type ExtensionType string - -const ( - ExecutableProcessor ExtensionType = "executeable" -) - -type BaseProcessorDefinition struct { - Name string `json:"name"` - Type ExtensionType `json:"type"` - Spec *json.RawMessage `json:"spec"` -} - -type HookDefinition struct { - BaseProcessorDefinition -} - -type FilterDefinition struct { - Type string `json:"type"` - Spec *json.RawMessage `json:"spec"` -} - -type DownloaderDefinition struct { - BaseProcessorDefinition - Filters []FilterDefinition `json:"filters"` -} - -type UploaderDefinition struct { - BaseProcessorDefinition - Filters []FilterDefinition `json:"filters"` -} - -type ProcessorDefinition struct { - BaseProcessorDefinition -} - -type ProcessorReference struct { - Name string `json:"name"` - Type string `json:"type"` -} - -type Rule struct { - Name string - CopyByReference bool `json:"copyByReference"` - Filters []FilterDefinition `json:"filters"` - Processors []ProcessorReference `json:"processors"` -} diff --git a/pkg/transport/config/uploader_factory.go b/pkg/transport/config/uploader_factory.go index 0b2d6c58..d67817d0 100644 --- a/pkg/transport/config/uploader_factory.go +++ b/pkg/transport/config/uploader_factory.go @@ -16,8 +16,8 @@ import ( ) const ( - LocalOCIBlobUploaderType = "localOciBlobUL" - OCIImageUploaderType = "ociImageUL" + LocalOCIBlobUploaderType = "LocalOciBlobUploader" + OCIImageUploaderType = "OciImageUploader" ) func NewUploaderFactory(client ociclient.Client, ocicache cache.Cache, targetCtx cdv2.OCIRegistryRepository) *UploaderFactory { diff --git a/pkg/transport/process/pipeline_test.go b/pkg/transport/process/pipeline_test.go index c0a0f4c5..baaff70a 100644 --- a/pkg/transport/process/pipeline_test.go +++ b/pkg/transport/process/pipeline_test.go @@ -48,8 +48,8 @@ var _ = Describe("pipeline", func() { }, } - p1 := processors.NewLabellingProcessor(l1) - p2 := processors.NewLabellingProcessor(l2) + p1 := processors.NewResourceLabeler(l1) + p2 := processors.NewResourceLabeler(l2) pipeline := process.NewResourceProcessingPipeline(p1, p2) actualCD, actualRes, err := pipeline.Process(context.TODO(), cd, res) From 75f5ba5d9ac619eda91bd6fd711efc13da874725 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Thu, 21 Oct 2021 10:05:16 +0200 Subject: [PATCH 41/94] refactors filter package and adds tests --- pkg/transport/config/filter_factory.go | 16 +- pkg/transport/config/processing_job.go | 30 ++-- .../{filter => filters}/access_type_filter.go | 10 +- .../component_name_filter.go | 8 +- pkg/transport/filters/filters_suite_test.go | 170 ++++++++++++++++++ .../resource_type_filter.go | 10 +- pkg/transport/{filter => filters}/types.go | 6 +- 7 files changed, 218 insertions(+), 32 deletions(-) rename pkg/transport/{filter => filters}/access_type_filter.go (72%) rename pkg/transport/{filter => filters}/component_name_filter.go (80%) create mode 100644 pkg/transport/filters/filters_suite_test.go rename pkg/transport/{filter => filters}/resource_type_filter.go (72%) rename pkg/transport/{filter => filters}/types.go (56%) diff --git a/pkg/transport/config/filter_factory.go b/pkg/transport/config/filter_factory.go index aa4053c0..1f64324a 100644 --- a/pkg/transport/config/filter_factory.go +++ b/pkg/transport/config/filter_factory.go @@ -7,7 +7,7 @@ import ( "encoding/json" "fmt" - "github.com/gardener/component-cli/pkg/transport/filter" + "github.com/gardener/component-cli/pkg/transport/filters" "sigs.k8s.io/yaml" ) @@ -23,7 +23,7 @@ func NewFilterFactory() *FilterFactory { type FilterFactory struct{} -func (f *FilterFactory) Create(typ string, spec *json.RawMessage) (filter.Filter, error) { +func (f *FilterFactory) Create(typ string, spec *json.RawMessage) (filters.Filter, error) { switch typ { case ComponentNameFilterType: return f.createComponentNameFilter(spec) @@ -36,7 +36,7 @@ func (f *FilterFactory) Create(typ string, spec *json.RawMessage) (filter.Filter } } -func (f *FilterFactory) createComponentNameFilter(rawSpec *json.RawMessage) (filter.Filter, error) { +func (f *FilterFactory) createComponentNameFilter(rawSpec *json.RawMessage) (filters.Filter, error) { type filterSpec struct { IncludeComponentNames []string `json:"includeComponentNames"` } @@ -47,10 +47,10 @@ func (f *FilterFactory) createComponentNameFilter(rawSpec *json.RawMessage) (fil return nil, fmt.Errorf("unable to parse spec: %w", err) } - return filter.NewComponentNameFilter(spec.IncludeComponentNames...) + return filters.NewComponentNameFilter(spec.IncludeComponentNames...) } -func (f *FilterFactory) createResourceTypeFilter(rawSpec *json.RawMessage) (filter.Filter, error) { +func (f *FilterFactory) createResourceTypeFilter(rawSpec *json.RawMessage) (filters.Filter, error) { type filterSpec struct { IncludeResourceTypes []string `json:"includeResourceTypes"` } @@ -61,10 +61,10 @@ func (f *FilterFactory) createResourceTypeFilter(rawSpec *json.RawMessage) (filt return nil, fmt.Errorf("unable to parse spec: %w", err) } - return filter.NewResourceTypeFilter(spec.IncludeResourceTypes...) + return filters.NewResourceTypeFilter(spec.IncludeResourceTypes...) } -func (f *FilterFactory) createAccessTypeFilter(rawSpec *json.RawMessage) (filter.Filter, error) { +func (f *FilterFactory) createAccessTypeFilter(rawSpec *json.RawMessage) (filters.Filter, error) { type filterSpec struct { IncludeAccessTypes []string `json:"includeAccessTypes"` } @@ -75,5 +75,5 @@ func (f *FilterFactory) createAccessTypeFilter(rawSpec *json.RawMessage) (filter return nil, fmt.Errorf("unable to parse spec: %w", err) } - return filter.NewAccessTypeFilter(spec.IncludeAccessTypes...) + return filters.NewAccessTypeFilter(spec.IncludeAccessTypes...) } diff --git a/pkg/transport/config/processing_job.go b/pkg/transport/config/processing_job.go index ae61ff3d..60af6dcd 100644 --- a/pkg/transport/config/processing_job.go +++ b/pkg/transport/config/processing_job.go @@ -9,7 +9,7 @@ import ( "fmt" "os" - "github.com/gardener/component-cli/pkg/transport/filter" + "github.com/gardener/component-cli/pkg/transport/filters" "github.com/gardener/component-cli/pkg/transport/process" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" "sigs.k8s.io/yaml" @@ -18,13 +18,13 @@ import ( type ProcessingJob struct { ComponentDescriptor *cdv2.ComponentDescriptor Resource *cdv2.Resource - Downloaders []namedResourceStreamProcessor - Processors []namedResourceStreamProcessor - Uploaders []namedResourceStreamProcessor + Downloaders []NamedResourceStreamProcessor + Processors []NamedResourceStreamProcessor + Uploaders []NamedResourceStreamProcessor ProcessedResource *cdv2.Resource } -type namedResourceStreamProcessor struct { +type NamedResourceStreamProcessor struct { Processor process.ResourceStreamProcessor Name string } @@ -33,7 +33,7 @@ type parsedDownloaderDefinition struct { Name string Type string Spec *json.RawMessage - Filters []filter.Filter + Filters []filters.Filter } type parsedProcessorDefinition struct { @@ -46,13 +46,13 @@ type parsedUploaderDefinition struct { Name string Type string Spec *json.RawMessage - Filters []filter.Filter + Filters []filters.Filter } type parsedRuleDefinition struct { Name string Processors []string - Filters []filter.Filter + Filters []filters.Filter } type parsedTransportConfig struct { @@ -172,7 +172,7 @@ func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Reso if err != nil { return nil, err } - job.Downloaders = append(job.Downloaders, namedResourceStreamProcessor{ + job.Downloaders = append(job.Downloaders, NamedResourceStreamProcessor{ Name: downloader.Name, Processor: dl, }) @@ -186,7 +186,7 @@ func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Reso if err != nil { return nil, err } - job.Uploaders = append(job.Uploaders, namedResourceStreamProcessor{ + job.Uploaders = append(job.Uploaders, NamedResourceStreamProcessor{ Name: uploader.Name, Processor: ul, }) @@ -205,7 +205,7 @@ func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Reso if err != nil { return nil, err } - job.Processors = append(job.Processors, namedResourceStreamProcessor{ + job.Processors = append(job.Processors, NamedResourceStreamProcessor{ Name: processorDefined.Name, Processor: p, }) @@ -216,17 +216,17 @@ func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Reso return &job, nil } -func areAllFiltersMatching(filters []filter.Filter, cd cdv2.ComponentDescriptor, res cdv2.Resource) bool { +func areAllFiltersMatching(filters []filters.Filter, cd cdv2.ComponentDescriptor, res cdv2.Resource) bool { for _, filter := range filters { - if !filter.Matches(&cd, res) { + if !filter.Matches(cd, res) { return false } } return true } -func createFilterList(filterDefinitions []filterDefinition, ff *FilterFactory) ([]filter.Filter, error) { - var filters []filter.Filter +func createFilterList(filterDefinitions []filterDefinition, ff *FilterFactory) ([]filters.Filter, error) { + var filters []filters.Filter for _, f := range filterDefinitions { filter, err := ff.Create(f.Type, f.Spec) if err != nil { diff --git a/pkg/transport/filter/access_type_filter.go b/pkg/transport/filters/access_type_filter.go similarity index 72% rename from pkg/transport/filter/access_type_filter.go rename to pkg/transport/filters/access_type_filter.go index 050d0aeb..27de3123 100644 --- a/pkg/transport/filter/access_type_filter.go +++ b/pkg/transport/filters/access_type_filter.go @@ -1,9 +1,11 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // // SPDX-License-Identifier: Apache-2.0 -package filter +package filters import ( + "fmt" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" ) @@ -11,7 +13,7 @@ type accessTypeFilter struct { includeAccessTypes []string } -func (f accessTypeFilter) Matches(cd *cdv2.ComponentDescriptor, r cdv2.Resource) bool { +func (f accessTypeFilter) Matches(cd cdv2.ComponentDescriptor, r cdv2.Resource) bool { for _, accessType := range f.includeAccessTypes { if r.Access.Type == accessType { return true @@ -21,6 +23,10 @@ func (f accessTypeFilter) Matches(cd *cdv2.ComponentDescriptor, r cdv2.Resource) } func NewAccessTypeFilter(includeAccessTypes ...string) (Filter, error) { + if len(includeAccessTypes) == 0 { + return nil, fmt.Errorf("includeAccessTypes must not be empty") + } + filter := accessTypeFilter{ includeAccessTypes: includeAccessTypes, } diff --git a/pkg/transport/filter/component_name_filter.go b/pkg/transport/filters/component_name_filter.go similarity index 80% rename from pkg/transport/filter/component_name_filter.go rename to pkg/transport/filters/component_name_filter.go index bc8bd200..bd840515 100644 --- a/pkg/transport/filter/component_name_filter.go +++ b/pkg/transport/filters/component_name_filter.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // // SPDX-License-Identifier: Apache-2.0 -package filter +package filters import ( "fmt" @@ -14,7 +14,7 @@ type componentNameFilter struct { includeComponentNames []*regexp.Regexp } -func (f componentNameFilter) Matches(cd *cdv2.ComponentDescriptor, r cdv2.Resource) bool { +func (f componentNameFilter) Matches(cd cdv2.ComponentDescriptor, r cdv2.Resource) bool { var matches bool for _, icn := range f.includeComponentNames { if matches = icn.MatchString(cd.Name); matches { @@ -25,6 +25,10 @@ func (f componentNameFilter) Matches(cd *cdv2.ComponentDescriptor, r cdv2.Resour } func NewComponentNameFilter(includeComponentNames ...string) (Filter, error) { + if len(includeComponentNames) == 0 { + return nil, fmt.Errorf("includeComponentNames must not be empty") + } + icnRegexps := []*regexp.Regexp{} for _, icn := range includeComponentNames { icnRegexp, err := regexp.Compile(icn) diff --git a/pkg/transport/filters/filters_suite_test.go b/pkg/transport/filters/filters_suite_test.go new file mode 100644 index 00000000..b391dc06 --- /dev/null +++ b/pkg/transport/filters/filters_suite_test.go @@ -0,0 +1,170 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package filters_test + +import ( + "testing" + + filter "github.com/gardener/component-cli/pkg/transport/filters" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Filters Test Suite") +} + +var _ = Describe("filters", func() { + + Context("accessTypeFilter", func() { + + It("should match if access type is in include list", func() { + cd := cdv2.ComponentDescriptor{} + res := cdv2.Resource{ + Access: cdv2.NewEmptyUnstructured(cdv2.OCIRegistryType), + } + + f, err := filter.NewAccessTypeFilter(cdv2.OCIRegistryType) + Expect(err).ToNot(HaveOccurred()) + + actualMatch := f.Matches(cd, res) + Expect(actualMatch).To(Equal(true)) + }) + + It("should not match if access type is not in include list", func() { + cd := cdv2.ComponentDescriptor{} + res := cdv2.Resource{ + Access: cdv2.NewEmptyUnstructured(cdv2.OCIRegistryType), + } + + f, err := filter.NewAccessTypeFilter(cdv2.LocalOCIBlobType) + Expect(err).ToNot(HaveOccurred()) + + actualMatch := f.Matches(cd, res) + Expect(actualMatch).To(Equal(false)) + + }) + + It("should return error upon creation if include list is empty", func() { + includeAccessTypes := []string{} + _, err := filter.NewAccessTypeFilter(includeAccessTypes...) + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError("includeAccessTypes must not be empty")) + }) + + }) + + Context("resourceTypeFilter", func() { + + It("should match if resource type is in include list", func() { + cd := cdv2.ComponentDescriptor{} + res := cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "my-res", + Version: "v0.1.0", + Type: cdv2.OCIImageType, + }, + } + + f, err := filter.NewResourceTypeFilter(cdv2.OCIImageType) + Expect(err).ToNot(HaveOccurred()) + + actualMatch := f.Matches(cd, res) + Expect(actualMatch).To(Equal(true)) + + }) + + It("should not match if resource type is not in include list", func() { + cd := cdv2.ComponentDescriptor{} + res := cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "my-res", + Version: "v0.1.0", + Type: "helm", + }, + } + + f, err := filter.NewResourceTypeFilter(cdv2.OCIImageType) + Expect(err).ToNot(HaveOccurred()) + + actualMatch := f.Matches(cd, res) + Expect(actualMatch).To(Equal(false)) + + }) + + It("should return error upon creation if include list is empty", func() { + includeResourceTypes := []string{} + _, err := filter.NewResourceTypeFilter(includeResourceTypes...) + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError("includeResourceTypes must not be empty")) + }) + + }) + + Context("componentNameFilter", func() { + + It("should match if component name is in include list", func() { + cd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + ObjectMeta: cdv2.ObjectMeta{ + Name: "github.com/test/my-component", + }, + }, + } + res := cdv2.Resource{} + + f1, err := filter.NewComponentNameFilter("github.com/test/my-component") + Expect(err).ToNot(HaveOccurred()) + + match1 := f1.Matches(cd, res) + Expect(match1).To(Equal(true)) + + f2, err := filter.NewComponentNameFilter("github.com/test/*") + Expect(err).ToNot(HaveOccurred()) + + match2 := f2.Matches(cd, res) + Expect(match2).To(Equal(true)) + }) + + It("should not match if component name is not in include list", func() { + cd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + ObjectMeta: cdv2.ObjectMeta{ + Name: "github.com/test/my-component", + }, + }, + } + res := cdv2.Resource{} + + f1, err := filter.NewComponentNameFilter("github.com/test/my-other-component") + Expect(err).ToNot(HaveOccurred()) + + match1 := f1.Matches(cd, res) + Expect(match1).To(Equal(false)) + + f2, err := filter.NewComponentNameFilter("github.com/test-2/*") + Expect(err).ToNot(HaveOccurred()) + + match2 := f2.Matches(cd, res) + Expect(match2).To(Equal(false)) + }) + + It("should return error upon creation if include list is empty", func() { + includeComponentNames := []string{} + _, err := filter.NewComponentNameFilter(includeComponentNames...) + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError("includeComponentNames must not be empty")) + }) + + It("should return error upon creation if regexp is invalid", func() { + _, err := filter.NewComponentNameFilter("github.com/\\") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("error parsing regexp")) + }) + + }) + +}) diff --git a/pkg/transport/filter/resource_type_filter.go b/pkg/transport/filters/resource_type_filter.go similarity index 72% rename from pkg/transport/filter/resource_type_filter.go rename to pkg/transport/filters/resource_type_filter.go index 77f1c8dc..4635e274 100644 --- a/pkg/transport/filter/resource_type_filter.go +++ b/pkg/transport/filters/resource_type_filter.go @@ -1,9 +1,11 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // // SPDX-License-Identifier: Apache-2.0 -package filter +package filters import ( + "fmt" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" ) @@ -11,7 +13,7 @@ type resourceTypeFilter struct { includeResourceTypes []string } -func (f resourceTypeFilter) Matches(cd *cdv2.ComponentDescriptor, r cdv2.Resource) bool { +func (f resourceTypeFilter) Matches(cd cdv2.ComponentDescriptor, r cdv2.Resource) bool { for _, resourceType := range f.includeResourceTypes { if r.Type == resourceType { return true @@ -21,6 +23,10 @@ func (f resourceTypeFilter) Matches(cd *cdv2.ComponentDescriptor, r cdv2.Resourc } func NewResourceTypeFilter(includeResourceTypes ...string) (Filter, error) { + if len(includeResourceTypes) == 0 { + return nil, fmt.Errorf("includeResourceTypes must not be empty") + } + filter := resourceTypeFilter{ includeResourceTypes: includeResourceTypes, } diff --git a/pkg/transport/filter/types.go b/pkg/transport/filters/types.go similarity index 56% rename from pkg/transport/filter/types.go rename to pkg/transport/filters/types.go index 97468703..d29d2e4b 100644 --- a/pkg/transport/filter/types.go +++ b/pkg/transport/filters/types.go @@ -1,13 +1,13 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // // SPDX-License-Identifier: Apache-2.0 -package filter +package filters import ( cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" ) type Filter interface { - // Matches receives a component descriptor and a resource and returns whether they match the filter criteria - Matches(*cdv2.ComponentDescriptor, cdv2.Resource) bool + // Matches matches a component descriptor and a resource against the filter + Matches(cdv2.ComponentDescriptor, cdv2.Resource) bool } From f6611fb524340db97a28cbcbe38ff46521a511d5 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Thu, 21 Oct 2021 10:08:11 +0200 Subject: [PATCH 42/94] renames accessTypeFilter --- pkg/transport/config/filter_factory.go | 4 ++-- ...cess_type_filter.go => resource_access_type_filter.go} | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) rename pkg/transport/filters/{access_type_filter.go => resource_access_type_filter.go} (69%) diff --git a/pkg/transport/config/filter_factory.go b/pkg/transport/config/filter_factory.go index 1f64324a..9e644755 100644 --- a/pkg/transport/config/filter_factory.go +++ b/pkg/transport/config/filter_factory.go @@ -14,7 +14,7 @@ import ( const ( ComponentNameFilterType = "ComponentNameFilter" ResourceTypeFilterType = "ResourceTypeFilter" - AccessTypeFilterType = "AccessTypeFilter" + AccessTypeFilterType = "ResourceAccessTypeFilter" ) func NewFilterFactory() *FilterFactory { @@ -75,5 +75,5 @@ func (f *FilterFactory) createAccessTypeFilter(rawSpec *json.RawMessage) (filter return nil, fmt.Errorf("unable to parse spec: %w", err) } - return filters.NewAccessTypeFilter(spec.IncludeAccessTypes...) + return filters.NewResourceAccessTypeFilter(spec.IncludeAccessTypes...) } diff --git a/pkg/transport/filters/access_type_filter.go b/pkg/transport/filters/resource_access_type_filter.go similarity index 69% rename from pkg/transport/filters/access_type_filter.go rename to pkg/transport/filters/resource_access_type_filter.go index 27de3123..b3de065f 100644 --- a/pkg/transport/filters/access_type_filter.go +++ b/pkg/transport/filters/resource_access_type_filter.go @@ -9,11 +9,11 @@ import ( cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" ) -type accessTypeFilter struct { +type resourceAccessTypeFilter struct { includeAccessTypes []string } -func (f accessTypeFilter) Matches(cd cdv2.ComponentDescriptor, r cdv2.Resource) bool { +func (f resourceAccessTypeFilter) Matches(cd cdv2.ComponentDescriptor, r cdv2.Resource) bool { for _, accessType := range f.includeAccessTypes { if r.Access.Type == accessType { return true @@ -22,12 +22,12 @@ func (f accessTypeFilter) Matches(cd cdv2.ComponentDescriptor, r cdv2.Resource) return false } -func NewAccessTypeFilter(includeAccessTypes ...string) (Filter, error) { +func NewResourceAccessTypeFilter(includeAccessTypes ...string) (Filter, error) { if len(includeAccessTypes) == 0 { return nil, fmt.Errorf("includeAccessTypes must not be empty") } - filter := accessTypeFilter{ + filter := resourceAccessTypeFilter{ includeAccessTypes: includeAccessTypes, } From 6e247b6d74936955929789df9e78b741ea42874a Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Thu, 21 Oct 2021 10:10:40 +0200 Subject: [PATCH 43/94] format and lint --- pkg/commands/transport/transport.go | 4 ---- pkg/transport/config/filter_factory.go | 3 ++- pkg/transport/config/processing_job.go | 5 +++-- pkg/transport/config/processor_factory.go | 5 +++-- pkg/transport/config/uploader_factory.go | 5 +++-- pkg/transport/config/util.go | 3 ++- pkg/transport/filters/filters_suite_test.go | 9 +++++---- pkg/transport/process/downloaders/local_oci_blob.go | 5 +++-- pkg/transport/process/downloaders/oci_image.go | 5 +++-- pkg/transport/process/processors/oci_image_filter.go | 5 +++-- pkg/transport/process/serialize/oci_image.go | 7 ++++--- pkg/transport/process/uploaders/oci_image.go | 3 ++- pkg/transport/process/util.go | 3 ++- 13 files changed, 35 insertions(+), 27 deletions(-) diff --git a/pkg/commands/transport/transport.go b/pkg/commands/transport/transport.go index 9de0b714..e375fe79 100644 --- a/pkg/commands/transport/transport.go +++ b/pkg/commands/transport/transport.go @@ -28,10 +28,6 @@ import ( "github.com/gardener/component-cli/pkg/transport/config" ) -const ( - parallelRuns = 1 -) - type Options struct { SourceRepository string TargetRepository string diff --git a/pkg/transport/config/filter_factory.go b/pkg/transport/config/filter_factory.go index 9e644755..dd2199a1 100644 --- a/pkg/transport/config/filter_factory.go +++ b/pkg/transport/config/filter_factory.go @@ -7,8 +7,9 @@ import ( "encoding/json" "fmt" - "github.com/gardener/component-cli/pkg/transport/filters" "sigs.k8s.io/yaml" + + "github.com/gardener/component-cli/pkg/transport/filters" ) const ( diff --git a/pkg/transport/config/processing_job.go b/pkg/transport/config/processing_job.go index 60af6dcd..a0e46197 100644 --- a/pkg/transport/config/processing_job.go +++ b/pkg/transport/config/processing_job.go @@ -9,10 +9,11 @@ import ( "fmt" "os" - "github.com/gardener/component-cli/pkg/transport/filters" - "github.com/gardener/component-cli/pkg/transport/process" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" "sigs.k8s.io/yaml" + + "github.com/gardener/component-cli/pkg/transport/filters" + "github.com/gardener/component-cli/pkg/transport/process" ) type ProcessingJob struct { diff --git a/pkg/transport/config/processor_factory.go b/pkg/transport/config/processor_factory.go index 80850e2d..2681aea1 100644 --- a/pkg/transport/config/processor_factory.go +++ b/pkg/transport/config/processor_factory.go @@ -7,11 +7,12 @@ import ( "encoding/json" "fmt" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "sigs.k8s.io/yaml" + "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/transport/process/processors" - cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" - "sigs.k8s.io/yaml" ) const ( diff --git a/pkg/transport/config/uploader_factory.go b/pkg/transport/config/uploader_factory.go index d67817d0..437b1531 100644 --- a/pkg/transport/config/uploader_factory.go +++ b/pkg/transport/config/uploader_factory.go @@ -7,12 +7,13 @@ import ( "encoding/json" "fmt" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "sigs.k8s.io/yaml" + "github.com/gardener/component-cli/ociclient" "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/transport/process/uploaders" - cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" - "sigs.k8s.io/yaml" ) const ( diff --git a/pkg/transport/config/util.go b/pkg/transport/config/util.go index 5f217f0d..7e5957ae 100644 --- a/pkg/transport/config/util.go +++ b/pkg/transport/config/util.go @@ -7,9 +7,10 @@ import ( "encoding/json" "fmt" + "sigs.k8s.io/yaml" + "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/transport/process/extensions" - "sigs.k8s.io/yaml" ) const ( diff --git a/pkg/transport/filters/filters_suite_test.go b/pkg/transport/filters/filters_suite_test.go index b391dc06..7ca64aa4 100644 --- a/pkg/transport/filters/filters_suite_test.go +++ b/pkg/transport/filters/filters_suite_test.go @@ -6,10 +6,11 @@ package filters_test import ( "testing" - filter "github.com/gardener/component-cli/pkg/transport/filters" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + + filter "github.com/gardener/component-cli/pkg/transport/filters" ) func TestConfig(t *testing.T) { @@ -27,7 +28,7 @@ var _ = Describe("filters", func() { Access: cdv2.NewEmptyUnstructured(cdv2.OCIRegistryType), } - f, err := filter.NewAccessTypeFilter(cdv2.OCIRegistryType) + f, err := filter.NewResourceAccessTypeFilter(cdv2.OCIRegistryType) Expect(err).ToNot(HaveOccurred()) actualMatch := f.Matches(cd, res) @@ -40,7 +41,7 @@ var _ = Describe("filters", func() { Access: cdv2.NewEmptyUnstructured(cdv2.OCIRegistryType), } - f, err := filter.NewAccessTypeFilter(cdv2.LocalOCIBlobType) + f, err := filter.NewResourceAccessTypeFilter(cdv2.LocalOCIBlobType) Expect(err).ToNot(HaveOccurred()) actualMatch := f.Matches(cd, res) @@ -50,7 +51,7 @@ var _ = Describe("filters", func() { It("should return error upon creation if include list is empty", func() { includeAccessTypes := []string{} - _, err := filter.NewAccessTypeFilter(includeAccessTypes...) + _, err := filter.NewResourceAccessTypeFilter(includeAccessTypes...) Expect(err).To(HaveOccurred()) Expect(err).To(MatchError("includeAccessTypes must not be empty")) }) diff --git a/pkg/transport/process/downloaders/local_oci_blob.go b/pkg/transport/process/downloaders/local_oci_blob.go index 8c990a6c..b12b6090 100644 --- a/pkg/transport/process/downloaders/local_oci_blob.go +++ b/pkg/transport/process/downloaders/local_oci_blob.go @@ -9,10 +9,11 @@ import ( "io" "io/ioutil" - "github.com/gardener/component-cli/ociclient" - "github.com/gardener/component-cli/pkg/transport/process" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" cdoci "github.com/gardener/component-spec/bindings-go/oci" + + "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/pkg/transport/process" ) type localOCIBlobDownloader struct { diff --git a/pkg/transport/process/downloaders/oci_image.go b/pkg/transport/process/downloaders/oci_image.go index a288c0d4..773f6a27 100644 --- a/pkg/transport/process/downloaders/oci_image.go +++ b/pkg/transport/process/downloaders/oci_image.go @@ -9,12 +9,13 @@ import ( "fmt" "io" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/gardener/component-cli/ociclient" "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/transport/process/serialize" - cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" - ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" ) type ociImageDownloader struct { diff --git a/pkg/transport/process/processors/oci_image_filter.go b/pkg/transport/process/processors/oci_image_filter.go index f1a52802..348d9ad7 100644 --- a/pkg/transport/process/processors/oci_image_filter.go +++ b/pkg/transport/process/processors/oci_image_filter.go @@ -15,13 +15,14 @@ import ( "io/ioutil" "github.com/containerd/containerd/images" + "github.com/opencontainers/go-digest" + ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/ociclient/oci" "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/transport/process/serialize" "github.com/gardener/component-cli/pkg/utils" - "github.com/opencontainers/go-digest" - ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" ) type ociImageFilter struct { diff --git a/pkg/transport/process/serialize/oci_image.go b/pkg/transport/process/serialize/oci_image.go index ddccadb3..d89e39f2 100644 --- a/pkg/transport/process/serialize/oci_image.go +++ b/pkg/transport/process/serialize/oci_image.go @@ -13,12 +13,13 @@ import ( "path" "strings" - "github.com/gardener/component-cli/ociclient/cache" - "github.com/gardener/component-cli/ociclient/oci" - "github.com/gardener/component-cli/pkg/utils" "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/specs-go" ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/gardener/component-cli/ociclient/cache" + "github.com/gardener/component-cli/ociclient/oci" + "github.com/gardener/component-cli/pkg/utils" ) const ( diff --git a/pkg/transport/process/uploaders/oci_image.go b/pkg/transport/process/uploaders/oci_image.go index 8be0c39a..93a6b774 100644 --- a/pkg/transport/process/uploaders/oci_image.go +++ b/pkg/transport/process/uploaders/oci_image.go @@ -8,12 +8,13 @@ import ( "fmt" "io" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "github.com/gardener/component-cli/ociclient" "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/transport/process/serialize" "github.com/gardener/component-cli/pkg/utils" - cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" ) type ociImageUploader struct { diff --git a/pkg/transport/process/util.go b/pkg/transport/process/util.go index 8cfd9f1d..c32f5003 100644 --- a/pkg/transport/process/util.go +++ b/pkg/transport/process/util.go @@ -11,9 +11,10 @@ import ( "io/ioutil" "os" - "github.com/gardener/component-cli/pkg/utils" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" "sigs.k8s.io/yaml" + + "github.com/gardener/component-cli/pkg/utils" ) const ( From d2538a849cc99114ef8205d2e705d78585a76963 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Thu, 21 Oct 2021 11:52:11 +0200 Subject: [PATCH 44/94] refactors filters package --- pkg/transport/filters/component_name_filter.go | 8 ++++---- pkg/transport/filters/filters_suite_test.go | 5 +---- pkg/transport/filters/resource_access_type_filter.go | 1 + pkg/transport/filters/resource_type_filter.go | 1 + pkg/transport/filters/types.go | 1 + 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/transport/filters/component_name_filter.go b/pkg/transport/filters/component_name_filter.go index bd840515..bee9c24c 100644 --- a/pkg/transport/filters/component_name_filter.go +++ b/pkg/transport/filters/component_name_filter.go @@ -15,15 +15,15 @@ type componentNameFilter struct { } func (f componentNameFilter) Matches(cd cdv2.ComponentDescriptor, r cdv2.Resource) bool { - var matches bool for _, icn := range f.includeComponentNames { - if matches = icn.MatchString(cd.Name); matches { - break + if icn.MatchString(cd.Name) { + return true } } - return matches + return false } +// NewComponentNameFilter creates a new componentNameFilter func NewComponentNameFilter(includeComponentNames ...string) (Filter, error) { if len(includeComponentNames) == 0 { return nil, fmt.Errorf("includeComponentNames must not be empty") diff --git a/pkg/transport/filters/filters_suite_test.go b/pkg/transport/filters/filters_suite_test.go index 7ca64aa4..949c24ca 100644 --- a/pkg/transport/filters/filters_suite_test.go +++ b/pkg/transport/filters/filters_suite_test.go @@ -20,7 +20,7 @@ func TestConfig(t *testing.T) { var _ = Describe("filters", func() { - Context("accessTypeFilter", func() { + Context("resourceAccessTypeFilter", func() { It("should match if access type is in include list", func() { cd := cdv2.ComponentDescriptor{} @@ -46,7 +46,6 @@ var _ = Describe("filters", func() { actualMatch := f.Matches(cd, res) Expect(actualMatch).To(Equal(false)) - }) It("should return error upon creation if include list is empty", func() { @@ -75,7 +74,6 @@ var _ = Describe("filters", func() { actualMatch := f.Matches(cd, res) Expect(actualMatch).To(Equal(true)) - }) It("should not match if resource type is not in include list", func() { @@ -93,7 +91,6 @@ var _ = Describe("filters", func() { actualMatch := f.Matches(cd, res) Expect(actualMatch).To(Equal(false)) - }) It("should return error upon creation if include list is empty", func() { diff --git a/pkg/transport/filters/resource_access_type_filter.go b/pkg/transport/filters/resource_access_type_filter.go index b3de065f..4e6d54ea 100644 --- a/pkg/transport/filters/resource_access_type_filter.go +++ b/pkg/transport/filters/resource_access_type_filter.go @@ -22,6 +22,7 @@ func (f resourceAccessTypeFilter) Matches(cd cdv2.ComponentDescriptor, r cdv2.Re return false } +// NewResourceAccessTypeFilter creates a new resourceAccessTypeFilter func NewResourceAccessTypeFilter(includeAccessTypes ...string) (Filter, error) { if len(includeAccessTypes) == 0 { return nil, fmt.Errorf("includeAccessTypes must not be empty") diff --git a/pkg/transport/filters/resource_type_filter.go b/pkg/transport/filters/resource_type_filter.go index 4635e274..787973e8 100644 --- a/pkg/transport/filters/resource_type_filter.go +++ b/pkg/transport/filters/resource_type_filter.go @@ -22,6 +22,7 @@ func (f resourceTypeFilter) Matches(cd cdv2.ComponentDescriptor, r cdv2.Resource return false } +// NewResourceTypeFilter creates a new resourceTypeFilter func NewResourceTypeFilter(includeResourceTypes ...string) (Filter, error) { if len(includeResourceTypes) == 0 { return nil, fmt.Errorf("includeResourceTypes must not be empty") diff --git a/pkg/transport/filters/types.go b/pkg/transport/filters/types.go index d29d2e4b..c06b0ae2 100644 --- a/pkg/transport/filters/types.go +++ b/pkg/transport/filters/types.go @@ -7,6 +7,7 @@ import ( cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" ) +// Filter type Filter interface { // Matches matches a component descriptor and a resource against the filter Matches(cdv2.ComponentDescriptor, cdv2.Resource) bool From c7c2cc3dae908a6b8554e7f75ba21ad3618b3fb5 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Thu, 21 Oct 2021 12:50:09 +0200 Subject: [PATCH 45/94] adds filters package --- .../filters/component_name_filter.go | 46 +++++ pkg/transport/filters/filters_suite_test.go | 168 ++++++++++++++++++ .../filters/resource_access_type_filter.go | 36 ++++ pkg/transport/filters/resource_type_filter.go | 36 ++++ pkg/transport/filters/types.go | 14 ++ 5 files changed, 300 insertions(+) create mode 100644 pkg/transport/filters/component_name_filter.go create mode 100644 pkg/transport/filters/filters_suite_test.go create mode 100644 pkg/transport/filters/resource_access_type_filter.go create mode 100644 pkg/transport/filters/resource_type_filter.go create mode 100644 pkg/transport/filters/types.go diff --git a/pkg/transport/filters/component_name_filter.go b/pkg/transport/filters/component_name_filter.go new file mode 100644 index 00000000..bee9c24c --- /dev/null +++ b/pkg/transport/filters/component_name_filter.go @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package filters + +import ( + "fmt" + "regexp" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +type componentNameFilter struct { + includeComponentNames []*regexp.Regexp +} + +func (f componentNameFilter) Matches(cd cdv2.ComponentDescriptor, r cdv2.Resource) bool { + for _, icn := range f.includeComponentNames { + if icn.MatchString(cd.Name) { + return true + } + } + return false +} + +// NewComponentNameFilter creates a new componentNameFilter +func NewComponentNameFilter(includeComponentNames ...string) (Filter, error) { + if len(includeComponentNames) == 0 { + return nil, fmt.Errorf("includeComponentNames must not be empty") + } + + icnRegexps := []*regexp.Regexp{} + for _, icn := range includeComponentNames { + icnRegexp, err := regexp.Compile(icn) + if err != nil { + return nil, fmt.Errorf("unable to parse regexp %s: %w", icn, err) + } + icnRegexps = append(icnRegexps, icnRegexp) + } + + filter := componentNameFilter{ + includeComponentNames: icnRegexps, + } + + return &filter, nil +} diff --git a/pkg/transport/filters/filters_suite_test.go b/pkg/transport/filters/filters_suite_test.go new file mode 100644 index 00000000..949c24ca --- /dev/null +++ b/pkg/transport/filters/filters_suite_test.go @@ -0,0 +1,168 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package filters_test + +import ( + "testing" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + filter "github.com/gardener/component-cli/pkg/transport/filters" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Filters Test Suite") +} + +var _ = Describe("filters", func() { + + Context("resourceAccessTypeFilter", func() { + + It("should match if access type is in include list", func() { + cd := cdv2.ComponentDescriptor{} + res := cdv2.Resource{ + Access: cdv2.NewEmptyUnstructured(cdv2.OCIRegistryType), + } + + f, err := filter.NewResourceAccessTypeFilter(cdv2.OCIRegistryType) + Expect(err).ToNot(HaveOccurred()) + + actualMatch := f.Matches(cd, res) + Expect(actualMatch).To(Equal(true)) + }) + + It("should not match if access type is not in include list", func() { + cd := cdv2.ComponentDescriptor{} + res := cdv2.Resource{ + Access: cdv2.NewEmptyUnstructured(cdv2.OCIRegistryType), + } + + f, err := filter.NewResourceAccessTypeFilter(cdv2.LocalOCIBlobType) + Expect(err).ToNot(HaveOccurred()) + + actualMatch := f.Matches(cd, res) + Expect(actualMatch).To(Equal(false)) + }) + + It("should return error upon creation if include list is empty", func() { + includeAccessTypes := []string{} + _, err := filter.NewResourceAccessTypeFilter(includeAccessTypes...) + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError("includeAccessTypes must not be empty")) + }) + + }) + + Context("resourceTypeFilter", func() { + + It("should match if resource type is in include list", func() { + cd := cdv2.ComponentDescriptor{} + res := cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "my-res", + Version: "v0.1.0", + Type: cdv2.OCIImageType, + }, + } + + f, err := filter.NewResourceTypeFilter(cdv2.OCIImageType) + Expect(err).ToNot(HaveOccurred()) + + actualMatch := f.Matches(cd, res) + Expect(actualMatch).To(Equal(true)) + }) + + It("should not match if resource type is not in include list", func() { + cd := cdv2.ComponentDescriptor{} + res := cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "my-res", + Version: "v0.1.0", + Type: "helm", + }, + } + + f, err := filter.NewResourceTypeFilter(cdv2.OCIImageType) + Expect(err).ToNot(HaveOccurred()) + + actualMatch := f.Matches(cd, res) + Expect(actualMatch).To(Equal(false)) + }) + + It("should return error upon creation if include list is empty", func() { + includeResourceTypes := []string{} + _, err := filter.NewResourceTypeFilter(includeResourceTypes...) + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError("includeResourceTypes must not be empty")) + }) + + }) + + Context("componentNameFilter", func() { + + It("should match if component name is in include list", func() { + cd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + ObjectMeta: cdv2.ObjectMeta{ + Name: "github.com/test/my-component", + }, + }, + } + res := cdv2.Resource{} + + f1, err := filter.NewComponentNameFilter("github.com/test/my-component") + Expect(err).ToNot(HaveOccurred()) + + match1 := f1.Matches(cd, res) + Expect(match1).To(Equal(true)) + + f2, err := filter.NewComponentNameFilter("github.com/test/*") + Expect(err).ToNot(HaveOccurred()) + + match2 := f2.Matches(cd, res) + Expect(match2).To(Equal(true)) + }) + + It("should not match if component name is not in include list", func() { + cd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + ObjectMeta: cdv2.ObjectMeta{ + Name: "github.com/test/my-component", + }, + }, + } + res := cdv2.Resource{} + + f1, err := filter.NewComponentNameFilter("github.com/test/my-other-component") + Expect(err).ToNot(HaveOccurred()) + + match1 := f1.Matches(cd, res) + Expect(match1).To(Equal(false)) + + f2, err := filter.NewComponentNameFilter("github.com/test-2/*") + Expect(err).ToNot(HaveOccurred()) + + match2 := f2.Matches(cd, res) + Expect(match2).To(Equal(false)) + }) + + It("should return error upon creation if include list is empty", func() { + includeComponentNames := []string{} + _, err := filter.NewComponentNameFilter(includeComponentNames...) + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError("includeComponentNames must not be empty")) + }) + + It("should return error upon creation if regexp is invalid", func() { + _, err := filter.NewComponentNameFilter("github.com/\\") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("error parsing regexp")) + }) + + }) + +}) diff --git a/pkg/transport/filters/resource_access_type_filter.go b/pkg/transport/filters/resource_access_type_filter.go new file mode 100644 index 00000000..4e6d54ea --- /dev/null +++ b/pkg/transport/filters/resource_access_type_filter.go @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package filters + +import ( + "fmt" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +type resourceAccessTypeFilter struct { + includeAccessTypes []string +} + +func (f resourceAccessTypeFilter) Matches(cd cdv2.ComponentDescriptor, r cdv2.Resource) bool { + for _, accessType := range f.includeAccessTypes { + if r.Access.Type == accessType { + return true + } + } + return false +} + +// NewResourceAccessTypeFilter creates a new resourceAccessTypeFilter +func NewResourceAccessTypeFilter(includeAccessTypes ...string) (Filter, error) { + if len(includeAccessTypes) == 0 { + return nil, fmt.Errorf("includeAccessTypes must not be empty") + } + + filter := resourceAccessTypeFilter{ + includeAccessTypes: includeAccessTypes, + } + + return &filter, nil +} diff --git a/pkg/transport/filters/resource_type_filter.go b/pkg/transport/filters/resource_type_filter.go new file mode 100644 index 00000000..787973e8 --- /dev/null +++ b/pkg/transport/filters/resource_type_filter.go @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package filters + +import ( + "fmt" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +type resourceTypeFilter struct { + includeResourceTypes []string +} + +func (f resourceTypeFilter) Matches(cd cdv2.ComponentDescriptor, r cdv2.Resource) bool { + for _, resourceType := range f.includeResourceTypes { + if r.Type == resourceType { + return true + } + } + return false +} + +// NewResourceTypeFilter creates a new resourceTypeFilter +func NewResourceTypeFilter(includeResourceTypes ...string) (Filter, error) { + if len(includeResourceTypes) == 0 { + return nil, fmt.Errorf("includeResourceTypes must not be empty") + } + + filter := resourceTypeFilter{ + includeResourceTypes: includeResourceTypes, + } + + return &filter, nil +} diff --git a/pkg/transport/filters/types.go b/pkg/transport/filters/types.go new file mode 100644 index 00000000..f6866890 --- /dev/null +++ b/pkg/transport/filters/types.go @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package filters + +import ( + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" +) + +// Filter defines the interface for matching component resources with downloaders, processing rules, and uploaders +type Filter interface { + // Matches matches a component descriptor and a resource against the filter + Matches(cdv2.ComponentDescriptor, cdv2.Resource) bool +} From 02d0bb20dbb8f09a20587b44e7c22ff102ce7330 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 26 Oct 2021 10:15:09 +0200 Subject: [PATCH 46/94] refactoring + added tests --- pkg/commands/transport/transport.go | 13 +- pkg/transport/config/downloader_factory.go | 8 +- pkg/transport/config/processing_job.go | 23 +-- pkg/transport/config/transport_config.go | 38 ++--- pkg/transport/config/uploader_factory.go | 10 +- .../downloaders/downloaders_suite_test.go | 136 ++++++++++++++++++ .../process/downloaders/local_oci_blob.go | 14 +- .../{oci_image.go => oci_artifact.go} | 32 +++-- .../process/downloaders/oci_artifact_test.go | 4 + .../process/uploaders/local_oci_blob.go | 13 +- .../{oci_image.go => oci_artifact.go} | 21 ++- 11 files changed, 240 insertions(+), 72 deletions(-) create mode 100644 pkg/transport/process/downloaders/downloaders_suite_test.go rename pkg/transport/process/downloaders/{oci_image.go => oci_artifact.go} (72%) create mode 100644 pkg/transport/process/downloaders/oci_artifact_test.go rename pkg/transport/process/uploaders/{oci_image.go => oci_artifact.go} (82%) diff --git a/pkg/commands/transport/transport.go b/pkg/commands/transport/transport.go index e375fe79..ac8db3c2 100644 --- a/pkg/commands/transport/transport.go +++ b/pkg/commands/transport/transport.go @@ -19,6 +19,7 @@ import ( "github.com/mandelsoft/vfs/pkg/vfs" "github.com/spf13/cobra" "github.com/spf13/pflag" + "gopkg.in/yaml.v2" "github.com/gardener/component-cli/ociclient" "github.com/gardener/component-cli/ociclient/cache" @@ -128,10 +129,20 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e targetCtx := cdv2.NewOCIRegistryRepository(o.TargetRepository, "") + transportCfgYaml, err := os.ReadFile(o.TransportCfgPath) + if err != nil { + return fmt.Errorf("unable to read transport config file: %w", err) + } + + var transportCfg config.TransportConfig + if err := yaml.Unmarshal(transportCfgYaml, &transportCfg); err != nil { + return fmt.Errorf("unable to parse transport config file: %w", err) + } + df := config.NewDownloaderFactory(ociClient, ociCache) pf := config.NewProcessorFactory(ociCache) uf := config.NewUploaderFactory(ociClient, ociCache, *targetCtx) - pjf, err := config.NewProcessingJobFactory(o.TransportCfgPath, df, pf, uf) + pjf, err := config.NewProcessingJobFactory(transportCfg, df, pf, uf) if err != nil { return err } diff --git a/pkg/transport/config/downloader_factory.go b/pkg/transport/config/downloader_factory.go index 5104851a..a23ce592 100644 --- a/pkg/transport/config/downloader_factory.go +++ b/pkg/transport/config/downloader_factory.go @@ -15,7 +15,7 @@ import ( const ( LocalOCIBlobDownloaderType = "LocalOciBlobDownloader" - OCIImageDownloaderType = "OciImageDownloader" + OCIArtifactDownloaderType = "OciArtifactDownloader" ) func NewDownloaderFactory(client ociclient.Client, ocicache cache.Cache) *DownloaderFactory { @@ -33,9 +33,9 @@ type DownloaderFactory struct { func (f *DownloaderFactory) Create(typ string, spec *json.RawMessage) (process.ResourceStreamProcessor, error) { switch typ { case LocalOCIBlobDownloaderType: - return downloaders.NewLocalOCIBlobDownloader(f.client), nil - case OCIImageDownloaderType: - return downloaders.NewOCIImageDownloader(f.client, f.cache), nil + return downloaders.NewLocalOCIBlobDownloader(f.client) + case OCIArtifactDownloaderType: + return downloaders.NewOCIArtifactDownloader(f.client, f.cache) case ExecutableType: return createExecutable(spec) default: diff --git a/pkg/transport/config/processing_job.go b/pkg/transport/config/processing_job.go index a0e46197..8288f456 100644 --- a/pkg/transport/config/processing_job.go +++ b/pkg/transport/config/processing_job.go @@ -7,10 +7,8 @@ import ( "context" "encoding/json" "fmt" - "os" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" - "sigs.k8s.io/yaml" "github.com/gardener/component-cli/pkg/transport/filters" "github.com/gardener/component-cli/pkg/transport/process" @@ -63,18 +61,7 @@ type parsedTransportConfig struct { Rules []parsedRuleDefinition } -func NewProcessingJobFactory(transportCfgPath string, df *DownloaderFactory, pf *ProcessorFactory, uf *UploaderFactory) (*ProcessingJobFactory, error) { - transportCfgYaml, err := os.ReadFile(transportCfgPath) - if err != nil { - return nil, fmt.Errorf("unable to read transport config file: %w", err) - } - - var transportCfg transportConfig - err = yaml.Unmarshal(transportCfgYaml, &transportCfg) - if err != nil { - return nil, fmt.Errorf("unable to parse transport config file: %w", err) - } - +func NewProcessingJobFactory(transportCfg TransportConfig, df *DownloaderFactory, pf *ProcessorFactory, uf *UploaderFactory) (*ProcessingJobFactory, error) { parsedTransportConfig, err := parseTransportConfig(&transportCfg) if err != nil { return nil, fmt.Errorf("failed creating lookup table %w", err) @@ -98,7 +85,7 @@ type ProcessingJobFactory struct { } // Create a ProcessorsLookup on the base of a config -func parseTransportConfig(config *transportConfig) (*parsedTransportConfig, error) { +func parseTransportConfig(config *TransportConfig) (*parsedTransportConfig, error) { var parsedConfig parsedTransportConfig ff := NewFilterFactory() @@ -149,12 +136,12 @@ func parseTransportConfig(config *transportConfig) (*parsedTransportConfig, erro if err != nil { return nil, fmt.Errorf("unable to create rule %s: %w", rule.Name, err) } - ruleLookup := parsedRuleDefinition{ + rule := parsedRuleDefinition{ Name: rule.Name, Processors: processors, Filters: filters, } - parsedConfig.Rules = append(parsedConfig.Rules, ruleLookup) + parsedConfig.Rules = append(parsedConfig.Rules, rule) } return &parsedConfig, nil @@ -226,7 +213,7 @@ func areAllFiltersMatching(filters []filters.Filter, cd cdv2.ComponentDescriptor return true } -func createFilterList(filterDefinitions []filterDefinition, ff *FilterFactory) ([]filters.Filter, error) { +func createFilterList(filterDefinitions []FilterDefinition, ff *FilterFactory) ([]filters.Filter, error) { var filters []filters.Filter for _, f := range filterDefinitions { filter, err := ff.Create(f.Type, f.Spec) diff --git a/pkg/transport/config/transport_config.go b/pkg/transport/config/transport_config.go index ee08a613..26aac7a0 100644 --- a/pkg/transport/config/transport_config.go +++ b/pkg/transport/config/transport_config.go @@ -9,46 +9,46 @@ type meta struct { Version string `json:"version"` } -type transportConfig struct { +type TransportConfig struct { Meta meta `json:"meta"` - Uploaders []uploaderDefinition `json:"uploaders"` - Processors []processorDefinition `json:"processors"` - Downloaders []downloaderDefinition `json:"downloaders"` - Rules []rule `json:"rules"` + Uploaders []UploaderDefinition `json:"uploaders"` + Processors []ProcessorDefinition `json:"processors"` + Downloaders []DownloaderDefinition `json:"downloaders"` + Rules []Rule `json:"rules"` } -type baseProcessorDefinition struct { +type BaseProcessorDefinition struct { Name string `json:"name"` Type string `json:"type"` Spec *json.RawMessage `json:"spec"` } -type filterDefinition struct { +type FilterDefinition struct { Type string `json:"type"` Spec *json.RawMessage `json:"spec"` } -type downloaderDefinition struct { - baseProcessorDefinition - Filters []filterDefinition `json:"filters"` +type DownloaderDefinition struct { + BaseProcessorDefinition + Filters []FilterDefinition `json:"filters"` } -type uploaderDefinition struct { - baseProcessorDefinition - Filters []filterDefinition `json:"filters"` +type UploaderDefinition struct { + BaseProcessorDefinition + Filters []FilterDefinition `json:"filters"` } -type processorDefinition struct { - baseProcessorDefinition +type ProcessorDefinition struct { + BaseProcessorDefinition } -type processorReference struct { +type ProcessorReference struct { Name string `json:"name"` Type string `json:"type"` } -type rule struct { +type Rule struct { Name string - Filters []filterDefinition `json:"filters"` - Processors []processorReference `json:"processors"` + Filters []FilterDefinition `json:"filters"` + Processors []ProcessorReference `json:"processors"` } diff --git a/pkg/transport/config/uploader_factory.go b/pkg/transport/config/uploader_factory.go index 437b1531..ab6ac464 100644 --- a/pkg/transport/config/uploader_factory.go +++ b/pkg/transport/config/uploader_factory.go @@ -18,7 +18,7 @@ import ( const ( LocalOCIBlobUploaderType = "LocalOciBlobUploader" - OCIImageUploaderType = "OciImageUploader" + OCIImageUploaderType = "OciArtifactUploader" ) func NewUploaderFactory(client ociclient.Client, ocicache cache.Cache, targetCtx cdv2.OCIRegistryRepository) *UploaderFactory { @@ -38,9 +38,9 @@ type UploaderFactory struct { func (f *UploaderFactory) Create(typ string, spec *json.RawMessage) (process.ResourceStreamProcessor, error) { switch typ { case LocalOCIBlobUploaderType: - return uploaders.NewLocalOCIBlobUploader(f.client, f.targetCtx), nil + return uploaders.NewLocalOCIBlobUploader(f.client, f.targetCtx) case OCIImageUploaderType: - return f.createOCIImageUploader(spec) + return f.createOCIArtifactUploader(spec) case ExecutableType: return createExecutable(spec) default: @@ -48,7 +48,7 @@ func (f *UploaderFactory) Create(typ string, spec *json.RawMessage) (process.Res } } -func (f *UploaderFactory) createOCIImageUploader(rawSpec *json.RawMessage) (process.ResourceStreamProcessor, error) { +func (f *UploaderFactory) createOCIArtifactUploader(rawSpec *json.RawMessage) (process.ResourceStreamProcessor, error) { type uploaderSpec struct { BaseUrl string `json:"baseUrl"` KeepSourceRepo bool `json:"keepSourceRepo"` @@ -60,5 +60,5 @@ func (f *UploaderFactory) createOCIImageUploader(rawSpec *json.RawMessage) (proc return nil, fmt.Errorf("unable to parse spec: %w", err) } - return uploaders.NewOCIImageUploader(f.client, f.cache, spec.BaseUrl, spec.KeepSourceRepo), nil + return uploaders.NewOCIImageUploader(f.client, f.cache, spec.BaseUrl, spec.KeepSourceRepo) } diff --git a/pkg/transport/process/downloaders/downloaders_suite_test.go b/pkg/transport/process/downloaders/downloaders_suite_test.go new file mode 100644 index 00000000..9cff190d --- /dev/null +++ b/pkg/transport/process/downloaders/downloaders_suite_test.go @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package downloaders_test + +import ( + "context" + "os" + "path/filepath" + "testing" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "github.com/gardener/component-spec/bindings-go/ctf" + cdoci "github.com/gardener/component-spec/bindings-go/oci" + "github.com/go-logr/logr" + "github.com/mandelsoft/vfs/pkg/memoryfs" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/opencontainers/go-digest" + + "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/ociclient/cache" + "github.com/gardener/component-cli/ociclient/credentials" + "github.com/gardener/component-cli/ociclient/test/envtest" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Downloaders Test Suite") +} + +var ( + testenv *envtest.Environment + client ociclient.Client + ocicache cache.Cache + keyring *credentials.GeneralOciKeyring + testComponent cdv2.ComponentDescriptor + localOciBlobResData = []byte("Hello World") + localOciBlobResIndex = 0 +) + +var _ = BeforeSuite(func() { + testenv = envtest.New(envtest.Options{ + RegistryBinaryPath: filepath.Join("../../../../", envtest.DefaultRegistryBinaryPath), + Stdout: GinkgoWriter, + Stderr: GinkgoWriter, + }) + Expect(testenv.Start(context.Background())).To(Succeed()) + + keyring = credentials.New() + Expect(keyring.AddAuthConfig(testenv.Addr, credentials.AuthConfig{ + Username: testenv.BasicAuth.Username, + Password: testenv.BasicAuth.Password, + })).To(Succeed()) + ocicache = cache.NewInMemoryCache() + var err error + client, err = ociclient.NewClient(logr.Discard(), ociclient.WithKeyring(keyring), ociclient.WithCache(ocicache)) + Expect(err).ToNot(HaveOccurred()) + + uploadTestComponent() +}, 60) + +var _ = AfterSuite(func() { + Expect(testenv.Close()).To(Succeed()) +}) + +func uploadTestComponent() { + dgst := digest.FromBytes(localOciBlobResData) + + fs := memoryfs.New() + Expect(fs.Mkdir(ctf.BlobsDirectoryName, os.ModePerm)).To(Succeed()) + + blobfile, err := fs.Create(ctf.BlobPath(dgst.String())) + Expect(err).ToNot(HaveOccurred()) + + _, err = blobfile.Write(localOciBlobResData) + Expect(err).ToNot(HaveOccurred()) + + Expect(blobfile.Close()).To(Succeed()) + + ctx := context.TODO() + + localOciBlobAcc, err := cdv2.NewUnstructured( + cdv2.NewLocalFilesystemBlobAccess( + dgst.String(), + "text/plain", + ), + ) + Expect(err).ToNot(HaveOccurred()) + + localOciBlobRes := cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "local-oci-blob-res", + Version: "0.1.0", + Type: "plain-text", + }, + Relation: cdv2.LocalRelation, + Access: &localOciBlobAcc, + } + + ociRepo := cdv2.NewOCIRegistryRepository(testenv.Addr+"/test/downloaders", "") + repoCtx, err := cdv2.NewUnstructured( + ociRepo, + ) + Expect(err).ToNot(HaveOccurred()) + + localCd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + ObjectMeta: cdv2.ObjectMeta{ + Name: "github.com/component-cli/test-component", + Version: "0.1.0", + }, + Provider: cdv2.InternalProvider, + RepositoryContexts: []*cdv2.UnstructuredTypedObject{ + &repoCtx, + }, + Resources: []cdv2.Resource{ + localOciBlobRes, + }, + }, + } + + manifest, err := cdoci.NewManifestBuilder(ocicache, ctf.NewComponentArchive(&localCd, fs)).Build(ctx) + Expect(err).ToNot(HaveOccurred()) + + ociRef, err := cdoci.OCIRef(*ociRepo, localCd.Name, localCd.Version) + Expect(err).ToNot(HaveOccurred()) + + Expect(client.PushManifest(ctx, ociRef, manifest)).To(Succeed()) + + cdresolver := cdoci.NewResolver(client) + actualCd, err := cdresolver.Resolve(ctx, ociRepo, localCd.Name, localCd.Version) + Expect(err).ToNot(HaveOccurred()) + + testComponent = *actualCd +} diff --git a/pkg/transport/process/downloaders/local_oci_blob.go b/pkg/transport/process/downloaders/local_oci_blob.go index b12b6090..35f17917 100644 --- a/pkg/transport/process/downloaders/local_oci_blob.go +++ b/pkg/transport/process/downloaders/local_oci_blob.go @@ -5,6 +5,7 @@ package downloaders import ( "context" + "errors" "fmt" "io" "io/ioutil" @@ -20,21 +21,26 @@ type localOCIBlobDownloader struct { client ociclient.Client } -func NewLocalOCIBlobDownloader(client ociclient.Client) process.ResourceStreamProcessor { +// NewLocalOCIBlobDownloader creates a new localOCIBlobDownloader +func NewLocalOCIBlobDownloader(client ociclient.Client) (process.ResourceStreamProcessor, error) { + if client == nil { + return nil, errors.New("client must not be nil") + } + obj := localOCIBlobDownloader{ client: client, } - return &obj + return &obj, nil } func (d *localOCIBlobDownloader) Process(ctx context.Context, r io.Reader, w io.Writer) error { cd, res, _, err := process.ReadProcessorMessage(r) if err != nil { - return fmt.Errorf("unable to read input archive: %w", err) + return fmt.Errorf("unable to read processor message: %w", err) } if res.Access.GetType() != cdv2.LocalOCIBlobType { - return fmt.Errorf("unsupported access type: %+v", res.Access) + return fmt.Errorf("unsupported access type: %s", res.Access.Type) } tmpfile, err := ioutil.TempFile("", "") diff --git a/pkg/transport/process/downloaders/oci_image.go b/pkg/transport/process/downloaders/oci_artifact.go similarity index 72% rename from pkg/transport/process/downloaders/oci_image.go rename to pkg/transport/process/downloaders/oci_artifact.go index 773f6a27..5a27602e 100644 --- a/pkg/transport/process/downloaders/oci_image.go +++ b/pkg/transport/process/downloaders/oci_artifact.go @@ -6,6 +6,7 @@ package downloaders import ( "bytes" "context" + "errors" "fmt" "io" @@ -18,31 +19,36 @@ import ( "github.com/gardener/component-cli/pkg/transport/process/serialize" ) -type ociImageDownloader struct { +type ociArtifactDownloader struct { client ociclient.Client cache cache.Cache } -func NewOCIImageDownloader(client ociclient.Client, cache cache.Cache) process.ResourceStreamProcessor { - obj := ociImageDownloader{ +// NewOCIArtifactDownloader creates a new ociArtifactDownloader +func NewOCIArtifactDownloader(client ociclient.Client, cache cache.Cache) (process.ResourceStreamProcessor, error) { + if client == nil { + return nil, errors.New("client must not be nil") + } + + if cache == nil { + return nil, errors.New("cache must not be nil") + } + + obj := ociArtifactDownloader{ client: client, cache: cache, } - return &obj + return &obj, nil } -func (d *ociImageDownloader) Process(ctx context.Context, r io.Reader, w io.Writer) error { +func (d *ociArtifactDownloader) Process(ctx context.Context, r io.Reader, w io.Writer) error { cd, res, _, err := process.ReadProcessorMessage(r) if err != nil { - return fmt.Errorf("unable to read input archive: %w", err) + return fmt.Errorf("unable to read processor message: %w", err) } if res.Access.GetType() != cdv2.OCIRegistryType { - return fmt.Errorf("unsupported access type: %+v", res.Access) - } - - if res.Type != cdv2.OCIImageType { - return fmt.Errorf("unsupported resource type: %s", res.Type) + return fmt.Errorf("unsupported access type: %s", res.Access.Type) } ociAccess := &cdv2.OCIRegistryAccess{} @@ -81,7 +87,7 @@ func (d *ociImageDownloader) Process(ctx context.Context, r io.Reader, w io.Writ return nil } -func (d *ociImageDownloader) fetchConfigAndLayerBlobs(ctx context.Context, ref string, manifest *ocispecv1.Manifest) error { +func (d *ociArtifactDownloader) fetchConfigAndLayerBlobs(ctx context.Context, ref string, manifest *ocispecv1.Manifest) error { buf := bytes.NewBuffer([]byte{}) if err := d.client.Fetch(ctx, ref, manifest.Config, buf); err != nil { return fmt.Errorf("unable to fetch config blob: %w", err) @@ -89,7 +95,7 @@ func (d *ociImageDownloader) fetchConfigAndLayerBlobs(ctx context.Context, ref s for _, l := range manifest.Layers { buf := bytes.NewBuffer([]byte{}) if err := d.client.Fetch(ctx, ref, l, buf); err != nil { - return fmt.Errorf("unable to fetch config blob: %w", err) + return fmt.Errorf("unable to fetch layer blob: %w", err) } } return nil diff --git a/pkg/transport/process/downloaders/oci_artifact_test.go b/pkg/transport/process/downloaders/oci_artifact_test.go new file mode 100644 index 00000000..899e2159 --- /dev/null +++ b/pkg/transport/process/downloaders/oci_artifact_test.go @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package downloaders_test diff --git a/pkg/transport/process/uploaders/local_oci_blob.go b/pkg/transport/process/uploaders/local_oci_blob.go index 0314c2c3..5446de31 100644 --- a/pkg/transport/process/uploaders/local_oci_blob.go +++ b/pkg/transport/process/uploaders/local_oci_blob.go @@ -5,6 +5,7 @@ package uploaders import ( "context" + "errors" "fmt" "io" "io/ioutil" @@ -22,23 +23,27 @@ type localOCIBlobUploader struct { targetCtx cdv2.OCIRegistryRepository } -func NewLocalOCIBlobUploader(client ociclient.Client, targetCtx cdv2.OCIRegistryRepository) process.ResourceStreamProcessor { +func NewLocalOCIBlobUploader(client ociclient.Client, targetCtx cdv2.OCIRegistryRepository) (process.ResourceStreamProcessor, error) { + if client == nil { + return nil, errors.New("client must not be nil") + } + obj := localOCIBlobUploader{ targetCtx: targetCtx, client: client, } - return &obj + return &obj, nil } func (d *localOCIBlobUploader) Process(ctx context.Context, r io.Reader, w io.Writer) error { cd, res, blobreader, err := process.ReadProcessorMessage(r) if err != nil { - return fmt.Errorf("unable to read input archive: %w", err) + return fmt.Errorf("unable to read processor message: %w", err) } defer blobreader.Close() if res.Access.GetType() != cdv2.LocalOCIBlobType { - return fmt.Errorf("unsupported access type: %+v", res.Access) + return fmt.Errorf("unsupported access type: %s", res.Access.Type) } tmpfile, err := ioutil.TempFile("", "") diff --git a/pkg/transport/process/uploaders/oci_image.go b/pkg/transport/process/uploaders/oci_artifact.go similarity index 82% rename from pkg/transport/process/uploaders/oci_image.go rename to pkg/transport/process/uploaders/oci_artifact.go index 93a6b774..a88fca27 100644 --- a/pkg/transport/process/uploaders/oci_image.go +++ b/pkg/transport/process/uploaders/oci_artifact.go @@ -5,6 +5,7 @@ package uploaders import ( "context" + "errors" "fmt" "io" @@ -24,20 +25,32 @@ type ociImageUploader struct { keepSourceRepo bool } -func NewOCIImageUploader(client ociclient.Client, cache cache.Cache, baseUrl string, keepSourceRepo bool) process.ResourceStreamProcessor { +func NewOCIImageUploader(client ociclient.Client, cache cache.Cache, baseUrl string, keepSourceRepo bool) (process.ResourceStreamProcessor, error) { + if client == nil { + return nil, errors.New("client must not be nil") + } + + if cache == nil { + return nil, errors.New("cache must not be nil") + } + + if baseUrl == "" { + return nil, errors.New("baseUrl must not be empty") + } + obj := ociImageUploader{ client: client, cache: cache, baseUrl: baseUrl, keepSourceRepo: keepSourceRepo, } - return &obj + return &obj, nil } func (u *ociImageUploader) Process(ctx context.Context, r io.Reader, w io.Writer) error { cd, res, resBlobReader, err := process.ReadProcessorMessage(r) if err != nil { - return fmt.Errorf("unable to read input archive: %w", err) + return fmt.Errorf("unable to read processor message: %w", err) } defer resBlobReader.Close() @@ -47,7 +60,7 @@ func (u *ociImageUploader) Process(ctx context.Context, r io.Reader, w io.Writer } if res.Access.GetType() != cdv2.OCIRegistryType { - return fmt.Errorf("unsupported access type: %+v", res.Access) + return fmt.Errorf("unsupported access type: %s", res.Access.Type) } if res.Type != cdv2.OCIImageType { From 54b89b997382a93a27750a8bdd8c07f8c9ef2b6b Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 26 Oct 2021 10:16:07 +0200 Subject: [PATCH 47/94] adds tests for local oci blob downloader --- .../downloaders/local_oci_blob_test.go | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 pkg/transport/process/downloaders/local_oci_blob_test.go diff --git a/pkg/transport/process/downloaders/local_oci_blob_test.go b/pkg/transport/process/downloaders/local_oci_blob_test.go new file mode 100644 index 00000000..0854cab1 --- /dev/null +++ b/pkg/transport/process/downloaders/local_oci_blob_test.go @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package downloaders_test + +import ( + "bytes" + "context" + "io" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/downloaders" +) + +var _ = Describe("localOciBlob", func() { + + Context("Process", func() { + + It("should download and stream resource", func() { + localOciBlobRes := testComponent.Resources[localOciBlobResIndex] + + inProcessorMsg := bytes.NewBuffer([]byte{}) + err := process.WriteProcessorMessage(testComponent, localOciBlobRes, nil, inProcessorMsg) + Expect(err).ToNot(HaveOccurred()) + + d, err := downloaders.NewLocalOCIBlobDownloader(client) + Expect(err).ToNot(HaveOccurred()) + + outProcessorMsg := bytes.NewBuffer([]byte{}) + err = d.Process(context.TODO(), inProcessorMsg, outProcessorMsg) + Expect(err).ToNot(HaveOccurred()) + + actualCd, actualRes, resBlobReader, err := process.ReadProcessorMessage(outProcessorMsg) + Expect(err).ToNot(HaveOccurred()) + defer resBlobReader.Close() + + Expect(*actualCd).To(Equal(testComponent)) + Expect(actualRes).To(Equal(localOciBlobRes)) + + resBlob := bytes.NewBuffer([]byte{}) + _, err = io.Copy(resBlob, resBlobReader) + Expect(err).ToNot(HaveOccurred()) + Expect(resBlob.Bytes()).To(Equal(localOciBlobResData)) + }) + + It("should return error if called with resource of invalid type", func() { + cd := cdv2.ComponentDescriptor{} + + access, err := cdv2.NewUnstructured( + cdv2.NewOCIRegistryAccess("example-registry.com/test/image:1.0.0"), + ) + Expect(err).ToNot(HaveOccurred()) + + res := cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "my-res", + Version: "0.1.0", + Type: "helm", + }, + Access: &access, + } + + d, err := downloaders.NewLocalOCIBlobDownloader(client) + Expect(err).ToNot(HaveOccurred()) + + b1 := bytes.NewBuffer([]byte{}) + err = process.WriteProcessorMessage(cd, res, nil, b1) + Expect(err).ToNot(HaveOccurred()) + + b2 := bytes.NewBuffer([]byte{}) + err = d.Process(context.TODO(), b1, b2) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unsupported access type")) + }) + + }) + +}) From 6304559089b47ee96fc8312e1e82ca36c460814b Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 26 Oct 2021 14:43:12 +0200 Subject: [PATCH 48/94] refactoring + adds test for oci artifact downloader --- cmd/component-cli/app/app.go | 1 - ociclient/client_test.go | 57 +++------------- pkg/commands/transport/transport.go | 2 +- pkg/testutils/oci.go | 67 +++++++++++++++++++ .../downloaders/downloaders_suite_test.go | 52 +++++++++++--- .../downloaders/local_oci_blob_test.go | 25 ++----- .../process/downloaders/oci_artifact_test.go | 62 +++++++++++++++++ pkg/transport/process/serialize/oci_image.go | 4 +- 8 files changed, 188 insertions(+), 82 deletions(-) create mode 100644 pkg/testutils/oci.go diff --git a/cmd/component-cli/app/app.go b/cmd/component-cli/app/app.go index f0efad73..9a6ec9cb 100644 --- a/cmd/component-cli/app/app.go +++ b/cmd/component-cli/app/app.go @@ -50,7 +50,6 @@ func NewComponentsCliCommand(ctx context.Context) *cobra.Command { cmd.AddCommand(oci.NewOCICommand(ctx)) cmd.AddCommand(cachecmd.NewCacheCommand(ctx)) cmd.AddCommand(transport.NewTransportCommand(ctx)) - cmd.AddCommand(transport.NewTestCommand(ctx)) return cmd } diff --git a/ociclient/client_test.go b/ociclient/client_test.go index 40b201c7..24e4e0d8 100644 --- a/ociclient/client_test.go +++ b/ociclient/client_test.go @@ -9,7 +9,6 @@ import ( "context" "encoding/json" "fmt" - "io" "net/http" "net/http/httptest" "net/url" @@ -23,6 +22,7 @@ import ( "github.com/gardener/component-cli/ociclient/credentials" "github.com/gardener/component-cli/ociclient/oci" + "github.com/gardener/component-cli/pkg/testutils" "github.com/gardener/component-cli/ociclient" ) @@ -36,14 +36,14 @@ var _ = Describe("client", func() { defer ctx.Done() ref := testenv.Addr + "/test/artifact:v0.0.1" - manifest := uploadTestManifest(ctx, ref) + manifest, _ := testutils.UploadTestManifest(ctx, client, ref) res, err := client.GetManifest(ctx, ref) Expect(err).ToNot(HaveOccurred()) Expect(res.Config).To(Equal(manifest.Config)) Expect(res.Layers).To(Equal(manifest.Layers)) - compareManifestToTestManifest(ctx, ref, res) + testutils.CompareManifestToTestManifest(ctx, client, ref, res) }, 20) It("should push and pull an oci image index", func() { @@ -93,7 +93,7 @@ var _ = Describe("client", func() { ref := testenv.Addr + "/image-index/3/img:v0.0.1" manifest1Ref := testenv.Addr + "/image-index/1/img-platform-1:v0.0.1" - manifest := uploadTestManifest(ctx, manifest1Ref) + manifest, _ := testutils.UploadTestManifest(ctx, client, manifest1Ref) index := oci.Index{ Manifests: []*oci.Manifest{ { @@ -124,7 +124,7 @@ var _ = Describe("client", func() { defer ctx.Done() ref := testenv.Addr + "/test/artifact:v0.0.1" - manifest := uploadTestManifest(ctx, ref) + manifest, _ := testutils.UploadTestManifest(ctx, client, ref) newRef := testenv.Addr + "/new/artifact:v0.0.1" Expect(ociclient.Copy(ctx, client, ref, newRef)).To(Succeed()) @@ -161,7 +161,7 @@ var _ = Describe("client", func() { compareImageIndices(actualArtifact.GetIndex(), index) for _, manifest := range actualArtifact.GetIndex().Manifests { - compareManifestToTestManifest(ctx, newRef, manifest.Data) + testutils.CompareManifestToTestManifest(ctx, client, newRef, manifest.Data) } }, 20) @@ -285,47 +285,6 @@ var _ = Describe("client", func() { }) -func uploadTestManifest(ctx context.Context, ref string) *ocispecv1.Manifest { - data := []byte("test") - layerData := []byte("test-config") - manifest := &ocispecv1.Manifest{ - Config: ocispecv1.Descriptor{ - MediaType: "text/plain", - Digest: digest.FromBytes(data), - Size: int64(len(data)), - }, - Layers: []ocispecv1.Descriptor{ - { - MediaType: "text/plain", - Digest: digest.FromBytes(layerData), - Size: int64(len(layerData)), - }, - }, - } - store := ociclient.GenericStore(func(ctx context.Context, desc ocispecv1.Descriptor, writer io.Writer) error { - switch desc.Digest.String() { - case manifest.Config.Digest.String(): - _, err := writer.Write(data) - return err - default: - _, err := writer.Write(layerData) - return err - } - }) - Expect(client.PushManifest(ctx, ref, manifest, ociclient.WithStore(store))).To(Succeed()) - return manifest -} - -func compareManifestToTestManifest(ctx context.Context, ref string, manifest *ocispecv1.Manifest) { - var configBlob bytes.Buffer - Expect(client.Fetch(ctx, ref, manifest.Config, &configBlob)).To(Succeed()) - Expect(configBlob.String()).To(Equal("test")) - - var layerBlob bytes.Buffer - Expect(client.Fetch(ctx, ref, manifest.Layers[0], &layerBlob)).To(Succeed()) - Expect(layerBlob.String()).To(Equal("test-config")) -} - func uploadTestIndex(ctx context.Context, indexRef string) *oci.Index { splitted := strings.Split(indexRef, ":") indexRepo := strings.Join(splitted[0:len(splitted)-1], ":") @@ -333,8 +292,8 @@ func uploadTestIndex(ctx context.Context, indexRef string) *oci.Index { manifest1Ref := fmt.Sprintf("%s-platform-1:%s", indexRepo, tag) manifest2Ref := fmt.Sprintf("%s-platform-2:%s", indexRepo, tag) - manifest1 := uploadTestManifest(ctx, manifest1Ref) - manifest2 := uploadTestManifest(ctx, manifest2Ref) + manifest1, _ := testutils.UploadTestManifest(ctx, client, manifest1Ref) + manifest2, _ := testutils.UploadTestManifest(ctx, client, manifest2Ref) index := oci.Index{ Manifests: []*oci.Manifest{ { diff --git a/pkg/commands/transport/transport.go b/pkg/commands/transport/transport.go index ac8db3c2..e8806e8e 100644 --- a/pkg/commands/transport/transport.go +++ b/pkg/commands/transport/transport.go @@ -19,7 +19,7 @@ import ( "github.com/mandelsoft/vfs/pkg/vfs" "github.com/spf13/cobra" "github.com/spf13/pflag" - "gopkg.in/yaml.v2" + "sigs.k8s.io/yaml" "github.com/gardener/component-cli/ociclient" "github.com/gardener/component-cli/ociclient/cache" diff --git a/pkg/testutils/oci.go b/pkg/testutils/oci.go new file mode 100644 index 00000000..b66b5d61 --- /dev/null +++ b/pkg/testutils/oci.go @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package testutils + +import ( + "bytes" + "context" + "encoding/json" + "io" + + "github.com/gardener/component-cli/ociclient" + . "github.com/onsi/gomega" + "github.com/opencontainers/go-digest" + ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +func UploadTestManifest(ctx context.Context, client ociclient.Client, ref string) (*ocispecv1.Manifest, ocispecv1.Descriptor) { + data := []byte("test") + layerData := []byte("test-config") + manifest := &ocispecv1.Manifest{ + Config: ocispecv1.Descriptor{ + MediaType: "text/plain", + Digest: digest.FromBytes(data), + Size: int64(len(data)), + }, + Layers: []ocispecv1.Descriptor{ + { + MediaType: "text/plain", + Digest: digest.FromBytes(layerData), + Size: int64(len(layerData)), + }, + }, + } + store := ociclient.GenericStore(func(ctx context.Context, desc ocispecv1.Descriptor, writer io.Writer) error { + switch desc.Digest.String() { + case manifest.Config.Digest.String(): + _, err := writer.Write(data) + return err + default: + _, err := writer.Write(layerData) + return err + } + }) + Expect(client.PushManifest(ctx, ref, manifest, ociclient.WithStore(store))).To(Succeed()) + + manifestBytes, err := json.Marshal(manifest) + Expect(err).ToNot(HaveOccurred()) + + desc := ocispecv1.Descriptor{ + MediaType: ocispecv1.MediaTypeImageManifest, + Digest: digest.FromBytes(manifestBytes), + Size: int64(len(manifestBytes)), + } + + return manifest, desc +} + +func CompareManifestToTestManifest(ctx context.Context, client ociclient.Client, ref string, manifest *ocispecv1.Manifest) { + var configBlob bytes.Buffer + Expect(client.Fetch(ctx, ref, manifest.Config, &configBlob)).To(Succeed()) + Expect(configBlob.String()).To(Equal("test")) + + var layerBlob bytes.Buffer + Expect(client.Fetch(ctx, ref, manifest.Layers[0], &layerBlob)).To(Succeed()) + Expect(layerBlob.String()).To(Equal("test-config")) +} diff --git a/pkg/transport/process/downloaders/downloaders_suite_test.go b/pkg/transport/process/downloaders/downloaders_suite_test.go index 9cff190d..4d5a0650 100644 --- a/pkg/transport/process/downloaders/downloaders_suite_test.go +++ b/pkg/transport/process/downloaders/downloaders_suite_test.go @@ -21,7 +21,9 @@ import ( "github.com/gardener/component-cli/ociclient" "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/ociclient/credentials" + "github.com/gardener/component-cli/ociclient/oci" "github.com/gardener/component-cli/ociclient/test/envtest" + "github.com/gardener/component-cli/pkg/testutils" ) func TestConfig(t *testing.T) { @@ -31,12 +33,14 @@ func TestConfig(t *testing.T) { var ( testenv *envtest.Environment - client ociclient.Client - ocicache cache.Cache + ociClient ociclient.Client + ociCache cache.Cache keyring *credentials.GeneralOciKeyring testComponent cdv2.ComponentDescriptor - localOciBlobResData = []byte("Hello World") localOciBlobResIndex = 0 + localOciBlobData = []byte("Hello World") + ociArtifactResIndex = 1 + expectedOciArtifact oci.Artifact ) var _ = BeforeSuite(func() { @@ -52,9 +56,9 @@ var _ = BeforeSuite(func() { Username: testenv.BasicAuth.Username, Password: testenv.BasicAuth.Password, })).To(Succeed()) - ocicache = cache.NewInMemoryCache() + ociCache = cache.NewInMemoryCache() var err error - client, err = ociclient.NewClient(logr.Discard(), ociclient.WithKeyring(keyring), ociclient.WithCache(ocicache)) + ociClient, err = ociclient.NewClient(logr.Discard(), ociclient.WithKeyring(keyring), ociclient.WithCache(ociCache)) Expect(err).ToNot(HaveOccurred()) uploadTestComponent() @@ -65,7 +69,7 @@ var _ = AfterSuite(func() { }) func uploadTestComponent() { - dgst := digest.FromBytes(localOciBlobResData) + dgst := digest.FromBytes(localOciBlobData) fs := memoryfs.New() Expect(fs.Mkdir(ctf.BlobsDirectoryName, os.ModePerm)).To(Succeed()) @@ -73,7 +77,7 @@ func uploadTestComponent() { blobfile, err := fs.Create(ctf.BlobPath(dgst.String())) Expect(err).ToNot(HaveOccurred()) - _, err = blobfile.Write(localOciBlobResData) + _, err = blobfile.Write(localOciBlobData) Expect(err).ToNot(HaveOccurred()) Expect(blobfile.Close()).To(Succeed()) @@ -98,6 +102,33 @@ func uploadTestComponent() { Access: &localOciBlobAcc, } + ociArtifactRef := testenv.Addr + "/test/downloaders/image:0.1.0" + + m, d := testutils.UploadTestManifest(ctx, ociClient, ociArtifactRef) + a, err := oci.NewManifestArtifact(&oci.Manifest{ + Descriptor: d, + Data: m, + }) + Expect(err).ToNot(HaveOccurred()) + expectedOciArtifact = *a + + ociArtifactAcc, err := cdv2.NewUnstructured( + cdv2.NewOCIRegistryAccess( + ociArtifactRef, + ), + ) + Expect(err).ToNot(HaveOccurred()) + + ociArtifactRes := cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "oci-artifact-res", + Version: "0.1.0", + Type: cdv2.OCIImageType, + }, + Relation: cdv2.LocalRelation, + Access: &ociArtifactAcc, + } + ociRepo := cdv2.NewOCIRegistryRepository(testenv.Addr+"/test/downloaders", "") repoCtx, err := cdv2.NewUnstructured( ociRepo, @@ -116,19 +147,20 @@ func uploadTestComponent() { }, Resources: []cdv2.Resource{ localOciBlobRes, + ociArtifactRes, }, }, } - manifest, err := cdoci.NewManifestBuilder(ocicache, ctf.NewComponentArchive(&localCd, fs)).Build(ctx) + manifest, err := cdoci.NewManifestBuilder(ociCache, ctf.NewComponentArchive(&localCd, fs)).Build(ctx) Expect(err).ToNot(HaveOccurred()) ociRef, err := cdoci.OCIRef(*ociRepo, localCd.Name, localCd.Version) Expect(err).ToNot(HaveOccurred()) - Expect(client.PushManifest(ctx, ociRef, manifest)).To(Succeed()) + Expect(ociClient.PushManifest(ctx, ociRef, manifest)).To(Succeed()) - cdresolver := cdoci.NewResolver(client) + cdresolver := cdoci.NewResolver(ociClient) actualCd, err := cdresolver.Resolve(ctx, ociRepo, localCd.Name, localCd.Version) Expect(err).ToNot(HaveOccurred()) diff --git a/pkg/transport/process/downloaders/local_oci_blob_test.go b/pkg/transport/process/downloaders/local_oci_blob_test.go index 0854cab1..35d1076f 100644 --- a/pkg/transport/process/downloaders/local_oci_blob_test.go +++ b/pkg/transport/process/downloaders/local_oci_blob_test.go @@ -8,7 +8,6 @@ import ( "context" "io" - cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -27,7 +26,7 @@ var _ = Describe("localOciBlob", func() { err := process.WriteProcessorMessage(testComponent, localOciBlobRes, nil, inProcessorMsg) Expect(err).ToNot(HaveOccurred()) - d, err := downloaders.NewLocalOCIBlobDownloader(client) + d, err := downloaders.NewLocalOCIBlobDownloader(ociClient) Expect(err).ToNot(HaveOccurred()) outProcessorMsg := bytes.NewBuffer([]byte{}) @@ -44,31 +43,17 @@ var _ = Describe("localOciBlob", func() { resBlob := bytes.NewBuffer([]byte{}) _, err = io.Copy(resBlob, resBlobReader) Expect(err).ToNot(HaveOccurred()) - Expect(resBlob.Bytes()).To(Equal(localOciBlobResData)) + Expect(resBlob.Bytes()).To(Equal(localOciBlobData)) }) It("should return error if called with resource of invalid type", func() { - cd := cdv2.ComponentDescriptor{} + ociArtifactRes := testComponent.Resources[ociArtifactResIndex] - access, err := cdv2.NewUnstructured( - cdv2.NewOCIRegistryAccess("example-registry.com/test/image:1.0.0"), - ) - Expect(err).ToNot(HaveOccurred()) - - res := cdv2.Resource{ - IdentityObjectMeta: cdv2.IdentityObjectMeta{ - Name: "my-res", - Version: "0.1.0", - Type: "helm", - }, - Access: &access, - } - - d, err := downloaders.NewLocalOCIBlobDownloader(client) + d, err := downloaders.NewLocalOCIBlobDownloader(ociClient) Expect(err).ToNot(HaveOccurred()) b1 := bytes.NewBuffer([]byte{}) - err = process.WriteProcessorMessage(cd, res, nil, b1) + err = process.WriteProcessorMessage(testComponent, ociArtifactRes, nil, b1) Expect(err).ToNot(HaveOccurred()) b2 := bytes.NewBuffer([]byte{}) diff --git a/pkg/transport/process/downloaders/oci_artifact_test.go b/pkg/transport/process/downloaders/oci_artifact_test.go index 899e2159..764a426f 100644 --- a/pkg/transport/process/downloaders/oci_artifact_test.go +++ b/pkg/transport/process/downloaders/oci_artifact_test.go @@ -2,3 +2,65 @@ // // SPDX-License-Identifier: Apache-2.0 package downloaders_test + +import ( + "bytes" + "context" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/downloaders" + "github.com/gardener/component-cli/pkg/transport/process/serialize" +) + +var _ = Describe("ociArtifact", func() { + + Context("Process", func() { + + It("should download and stream resource", func() { + ociArtifactRes := testComponent.Resources[ociArtifactResIndex] + + inProcessorMsg := bytes.NewBuffer([]byte{}) + err := process.WriteProcessorMessage(testComponent, ociArtifactRes, nil, inProcessorMsg) + Expect(err).ToNot(HaveOccurred()) + + d, err := downloaders.NewOCIArtifactDownloader(ociClient, ociCache) + Expect(err).ToNot(HaveOccurred()) + + outProcessorMsg := bytes.NewBuffer([]byte{}) + err = d.Process(context.TODO(), inProcessorMsg, outProcessorMsg) + Expect(err).ToNot(HaveOccurred()) + + actualCd, actualRes, resBlobReader, err := process.ReadProcessorMessage(outProcessorMsg) + Expect(err).ToNot(HaveOccurred()) + defer resBlobReader.Close() + + Expect(*actualCd).To(Equal(testComponent)) + Expect(actualRes).To(Equal(ociArtifactRes)) + + actualOciArtifact, err := serialize.DeserializeOCIArtifact(resBlobReader, ociCache) + Expect(err).ToNot(HaveOccurred()) + Expect(*actualOciArtifact).To(Equal(expectedOciArtifact)) + }) + + It("should return error if called with resource of invalid type", func() { + localOciBlobRes := testComponent.Resources[localOciBlobResIndex] + + d, err := downloaders.NewOCIArtifactDownloader(ociClient, ociCache) + Expect(err).ToNot(HaveOccurred()) + + b1 := bytes.NewBuffer([]byte{}) + err = process.WriteProcessorMessage(testComponent, localOciBlobRes, nil, b1) + Expect(err).ToNot(HaveOccurred()) + + b2 := bytes.NewBuffer([]byte{}) + err = d.Process(context.TODO(), b1, b2) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unsupported access type")) + }) + + }) + +}) diff --git a/pkg/transport/process/serialize/oci_image.go b/pkg/transport/process/serialize/oci_image.go index d89e39f2..45378e9f 100644 --- a/pkg/transport/process/serialize/oci_image.go +++ b/pkg/transport/process/serialize/oci_image.go @@ -223,7 +223,9 @@ func DeserializeOCIArtifact(r io.Reader, cache cache.Cache) (*oci.Artifact, erro m := oci.Manifest{ Descriptor: ocispecv1.Descriptor{ - Digest: digest.FromBytes(buf.Bytes()), + MediaType: ocispecv1.MediaTypeImageManifest, + Digest: digest.FromBytes(buf.Bytes()), + Size: int64(buf.Len()), }, Data: &manifest, } From 4953cc3a3101e4f0186fecc0dff0875214570b7d Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Thu, 28 Oct 2021 09:46:23 +0200 Subject: [PATCH 49/94] refactoring + adds tests --- ociclient/client.go | 25 ++- ociclient/client_test.go | 92 ++-------- pkg/testutils/oci.go | 95 ++++++++++- .../downloaders/downloaders_suite_test.go | 158 ++++++++++++------ .../downloaders/local_oci_blob_test.go | 2 +- .../process/downloaders/oci_artifact.go | 1 - .../process/downloaders/oci_artifact_test.go | 37 +++- 7 files changed, 254 insertions(+), 156 deletions(-) diff --git a/ociclient/client.go b/ociclient/client.go index 80572d51..7370495d 100644 --- a/ociclient/client.go +++ b/ociclient/client.go @@ -252,7 +252,7 @@ func (c *client) PushOCIArtifact(ctx context.Context, ref string, artifact *oci. _, err := c.pushManifest(ctx, artifact.GetManifest().Data, pusher, tempCache, opts) return err } else if artifact.IsIndex() { - return c.pushImageIndex(ctx, artifact, pusher, tempCache, opts) + return c.pushImageIndex(ctx, artifact.GetIndex(), pusher, tempCache, opts) } else { // execution of this code should never happen // the oci artifact should always be of type manifest or index @@ -303,36 +303,35 @@ func (c *client) pushManifest(ctx context.Context, manifest *ocispecv1.Manifest, if err := cache.Add(dummyDesc, ioutil.NopCloser(bytes.NewBuffer(dummyConfig))); err != nil { return ocispecv1.Descriptor{}, fmt.Errorf("unable to add dummy config to cache: %w", err) } - if err := c.pushContent(ctx, cache, pusher, manifest.Config); err != nil { - return ocispecv1.Descriptor{}, err + return ocispecv1.Descriptor{}, fmt.Errorf("unable to push dummy config: %w", err) } } else { if err := c.pushContent(ctx, opts.Store, pusher, manifest.Config); err != nil { - return ocispecv1.Descriptor{}, err + return ocispecv1.Descriptor{}, fmt.Errorf("unable to push config: %w", err) } } // last upload all layers for _, layer := range manifest.Layers { if err := c.pushContent(ctx, opts.Store, pusher, layer); err != nil { - return ocispecv1.Descriptor{}, err + return ocispecv1.Descriptor{}, fmt.Errorf("unable to push layer: %w", err) } } - desc, err := createDescriptorFromManifest(cache, manifest) + manifestDesc, err := createDescriptorFromManifest(cache, manifest) if err != nil { - return ocispecv1.Descriptor{}, err + return ocispecv1.Descriptor{}, fmt.Errorf("unable to create manifest descriptor: %w", err) } - if err := c.pushContent(ctx, cache, pusher, desc); err != nil { - return ocispecv1.Descriptor{}, err + if err := c.pushContent(ctx, cache, pusher, manifestDesc); err != nil { + return ocispecv1.Descriptor{}, fmt.Errorf("unable to push manifest: %w", err) } - return desc, nil + return manifestDesc, nil } -func (c *client) pushImageIndex(ctx context.Context, ociArtifact *oci.Artifact, pusher remotes.Pusher, cache cache.Cache, opts *PushOptions) error { +func (c *client) pushImageIndex(ctx context.Context, indexArtifact *oci.Index, pusher remotes.Pusher, cache cache.Cache, opts *PushOptions) error { manifestDescs := []ocispecv1.Descriptor{} - for _, manifest := range ociArtifact.GetIndex().Manifests { + for _, manifest := range indexArtifact.Manifests { mdesc, err := c.pushManifest(ctx, manifest.Data, pusher, cache, opts) if err != nil { return fmt.Errorf("unable to upload manifest: %w", err) @@ -347,7 +346,7 @@ func (c *client) pushImageIndex(ctx context.Context, ociArtifact *oci.Artifact, SchemaVersion: 2, }, Manifests: manifestDescs, - Annotations: ociArtifact.GetIndex().Annotations, + Annotations: indexArtifact.Annotations, } idesc, err := createDescriptorFromIndex(cache, &index) diff --git a/ociclient/client_test.go b/ociclient/client_test.go index 24e4e0d8..1bbb8c99 100644 --- a/ociclient/client_test.go +++ b/ociclient/client_test.go @@ -7,18 +7,14 @@ package ociclient_test import ( "bytes" "context" - "encoding/json" "fmt" "net/http" "net/http/httptest" "net/url" - "strings" "github.com/go-logr/logr" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - "github.com/opencontainers/go-digest" - ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/gardener/component-cli/ociclient/credentials" "github.com/gardener/component-cli/ociclient/oci" @@ -36,13 +32,15 @@ var _ = Describe("client", func() { defer ctx.Done() ref := testenv.Addr + "/test/artifact:v0.0.1" - manifest, _ := testutils.UploadTestManifest(ctx, client, ref) + manifest, _, err := testutils.UploadTestManifest(ctx, client, ref) + Expect(err).ToNot(HaveOccurred()) res, err := client.GetManifest(ctx, ref) Expect(err).ToNot(HaveOccurred()) Expect(res.Config).To(Equal(manifest.Config)) Expect(res.Layers).To(Equal(manifest.Layers)) + // TODO: oci image index test only working because cache is filled in this function with config/layer blobs. should be fixed testutils.CompareManifestToTestManifest(ctx, client, ref, res) }, 20) @@ -51,14 +49,15 @@ var _ = Describe("client", func() { defer ctx.Done() indexRef := testenv.Addr + "/image-index/1/img:v0.0.1" - index := uploadTestIndex(ctx, indexRef) + index, err := testutils.UploadTestIndex(ctx, client, indexRef) + Expect(err).ToNot(HaveOccurred()) actualArtifact, err := client.GetOCIArtifact(ctx, indexRef) Expect(err).ToNot(HaveOccurred()) Expect(actualArtifact.IsManifest()).To(BeFalse()) Expect(actualArtifact.IsIndex()).To(BeTrue()) - compareImageIndices(actualArtifact.GetIndex(), index) + testutils.CompareImageIndices(actualArtifact.GetIndex(), index) }, 20) It("should push and pull an empty oci image index", func() { @@ -84,7 +83,7 @@ var _ = Describe("client", func() { Expect(actualArtifact.IsManifest()).To(BeFalse()) Expect(actualArtifact.IsIndex()).To(BeTrue()) - compareImageIndices(actualArtifact.GetIndex(), &index) + testutils.CompareImageIndices(actualArtifact.GetIndex(), &index) }, 20) It("should push and pull an oci image index with only 1 manifest and no platform information", func() { @@ -93,7 +92,9 @@ var _ = Describe("client", func() { ref := testenv.Addr + "/image-index/3/img:v0.0.1" manifest1Ref := testenv.Addr + "/image-index/1/img-platform-1:v0.0.1" - manifest, _ := testutils.UploadTestManifest(ctx, client, manifest1Ref) + manifest, _, err := testutils.UploadTestManifest(ctx, client, manifest1Ref) + Expect(err).ToNot(HaveOccurred()) + index := oci.Index{ Manifests: []*oci.Manifest{ { @@ -116,7 +117,7 @@ var _ = Describe("client", func() { Expect(actualArtifact.IsManifest()).To(BeFalse()) Expect(actualArtifact.IsIndex()).To(BeTrue()) - compareImageIndices(actualArtifact.GetIndex(), &index) + testutils.CompareImageIndices(actualArtifact.GetIndex(), &index) }, 20) It("should copy an oci artifact", func() { @@ -124,7 +125,8 @@ var _ = Describe("client", func() { defer ctx.Done() ref := testenv.Addr + "/test/artifact:v0.0.1" - manifest, _ := testutils.UploadTestManifest(ctx, client, ref) + manifest, _, err := testutils.UploadTestManifest(ctx, client, ref) + Expect(err).ToNot(HaveOccurred()) newRef := testenv.Addr + "/new/artifact:v0.0.1" Expect(ociclient.Copy(ctx, client, ref, newRef)).To(Succeed()) @@ -140,7 +142,7 @@ var _ = Describe("client", func() { var layerBlob bytes.Buffer Expect(client.Fetch(ctx, ref, res.Layers[0], &layerBlob)).To(Succeed()) - Expect(layerBlob.String()).To(Equal("test-config")) + Expect(layerBlob.String()).To(Equal("layer-data")) }, 20) It("should copy an oci image index", func() { @@ -148,7 +150,8 @@ var _ = Describe("client", func() { defer ctx.Done() ref := testenv.Addr + "/copy/image-index/src/img:v0.0.1" - index := uploadTestIndex(ctx, ref) + index, err := testutils.UploadTestIndex(ctx, client, ref) + Expect(err).ToNot(HaveOccurred()) newRef := testenv.Addr + "/copy/image-index/tgt/img:v0.0.1" Expect(ociclient.Copy(ctx, client, ref, newRef)).To(Succeed()) @@ -158,7 +161,7 @@ var _ = Describe("client", func() { Expect(actualArtifact.IsManifest()).To(BeFalse()) Expect(actualArtifact.IsIndex()).To(BeTrue()) - compareImageIndices(actualArtifact.GetIndex(), index) + testutils.CompareImageIndices(actualArtifact.GetIndex(), index) for _, manifest := range actualArtifact.GetIndex().Manifests { testutils.CompareManifestToTestManifest(ctx, client, newRef, manifest.Data) @@ -284,64 +287,3 @@ var _ = Describe("client", func() { }) }) - -func uploadTestIndex(ctx context.Context, indexRef string) *oci.Index { - splitted := strings.Split(indexRef, ":") - indexRepo := strings.Join(splitted[0:len(splitted)-1], ":") - tag := splitted[len(splitted)-1] - - manifest1Ref := fmt.Sprintf("%s-platform-1:%s", indexRepo, tag) - manifest2Ref := fmt.Sprintf("%s-platform-2:%s", indexRepo, tag) - manifest1, _ := testutils.UploadTestManifest(ctx, client, manifest1Ref) - manifest2, _ := testutils.UploadTestManifest(ctx, client, manifest2Ref) - index := oci.Index{ - Manifests: []*oci.Manifest{ - { - Descriptor: ocispecv1.Descriptor{ - Platform: &ocispecv1.Platform{ - Architecture: "amd64", - OS: "linux", - }, - }, - Data: manifest1, - }, - { - Descriptor: ocispecv1.Descriptor{ - Platform: &ocispecv1.Platform{ - Architecture: "amd64", - OS: "windows", - }, - }, - Data: manifest2, - }, - }, - Annotations: map[string]string{ - "test": "test", - }, - } - - tmp, err := oci.NewIndexArtifact(&index) - Expect(err).ToNot(HaveOccurred()) - - Expect(client.PushOCIArtifact(ctx, indexRef, tmp)).To(Succeed()) - return &index -} - -func compareImageIndices(actualIndex *oci.Index, expectedIndex *oci.Index) { - Expect(actualIndex.Annotations).To(Equal(expectedIndex.Annotations)) - Expect(len(actualIndex.Manifests)).To(Equal(len(expectedIndex.Manifests))) - - for i := 0; i < len(actualIndex.Manifests); i++ { - actualManifest := actualIndex.Manifests[i] - expectedManifest := expectedIndex.Manifests[i] - - expectedManifestBytes, err := json.Marshal(expectedManifest.Data) - Expect(err).ToNot(HaveOccurred()) - - Expect(actualManifest.Descriptor.MediaType).To(Equal(ocispecv1.MediaTypeImageManifest)) - Expect(actualManifest.Descriptor.Digest).To(Equal(digest.FromBytes(expectedManifestBytes))) - Expect(actualManifest.Descriptor.Size).To(Equal(int64(len(expectedManifestBytes)))) - Expect(actualManifest.Descriptor.Platform).To(Equal(expectedManifest.Descriptor.Platform)) - Expect(actualManifest.Data).To(Equal(expectedManifest.Data)) - } -} diff --git a/pkg/testutils/oci.go b/pkg/testutils/oci.go index b66b5d61..2f1d7b2d 100644 --- a/pkg/testutils/oci.go +++ b/pkg/testutils/oci.go @@ -7,17 +7,20 @@ import ( "bytes" "context" "encoding/json" + "fmt" "io" + "strings" "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/ociclient/oci" . "github.com/onsi/gomega" "github.com/opencontainers/go-digest" ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" ) -func UploadTestManifest(ctx context.Context, client ociclient.Client, ref string) (*ocispecv1.Manifest, ocispecv1.Descriptor) { +func UploadTestManifest(ctx context.Context, client ociclient.Client, ref string) (*ocispecv1.Manifest, ocispecv1.Descriptor, error) { data := []byte("test") - layerData := []byte("test-config") + layerData := []byte("layer-data") manifest := &ocispecv1.Manifest{ Config: ocispecv1.Descriptor{ MediaType: "text/plain", @@ -42,10 +45,15 @@ func UploadTestManifest(ctx context.Context, client ociclient.Client, ref string return err } }) - Expect(client.PushManifest(ctx, ref, manifest, ociclient.WithStore(store))).To(Succeed()) + + if err := client.PushManifest(ctx, ref, manifest, ociclient.WithStore(store)); err != nil { + return nil, ocispecv1.Descriptor{}, err + } manifestBytes, err := json.Marshal(manifest) - Expect(err).ToNot(HaveOccurred()) + if err != nil { + return nil, ocispecv1.Descriptor{}, err + } desc := ocispecv1.Descriptor{ MediaType: ocispecv1.MediaTypeImageManifest, @@ -53,7 +61,7 @@ func UploadTestManifest(ctx context.Context, client ociclient.Client, ref string Size: int64(len(manifestBytes)), } - return manifest, desc + return manifest, desc, nil } func CompareManifestToTestManifest(ctx context.Context, client ociclient.Client, ref string, manifest *ocispecv1.Manifest) { @@ -63,5 +71,80 @@ func CompareManifestToTestManifest(ctx context.Context, client ociclient.Client, var layerBlob bytes.Buffer Expect(client.Fetch(ctx, ref, manifest.Layers[0], &layerBlob)).To(Succeed()) - Expect(layerBlob.String()).To(Equal("test-config")) + Expect(layerBlob.String()).To(Equal("layer-data")) +} + +func UploadTestIndex(ctx context.Context, client ociclient.Client, indexRef string) (*oci.Index, error) { + splitted := strings.Split(indexRef, ":") + indexRepo := strings.Join(splitted[0:len(splitted)-1], ":") + tag := splitted[len(splitted)-1] + + manifest1Ref := fmt.Sprintf("%s-platform-1:%s", indexRepo, tag) + manifest2Ref := fmt.Sprintf("%s-platform-2:%s", indexRepo, tag) + + manifest1, _, err := UploadTestManifest(ctx, client, manifest1Ref) + if err != nil { + return nil, err + } + + manifest2, _, err := UploadTestManifest(ctx, client, manifest2Ref) + if err != nil { + return nil, err + } + + index := oci.Index{ + Manifests: []*oci.Manifest{ + { + Descriptor: ocispecv1.Descriptor{ + Platform: &ocispecv1.Platform{ + Architecture: "amd64", + OS: "linux", + }, + }, + Data: manifest1, + }, + { + Descriptor: ocispecv1.Descriptor{ + Platform: &ocispecv1.Platform{ + Architecture: "amd64", + OS: "windows", + }, + }, + Data: manifest2, + }, + }, + Annotations: map[string]string{ + "test": "test", + }, + } + + ociArtifact, err := oci.NewIndexArtifact(&index) + if err != nil { + return nil, err + } + + if err := client.PushOCIArtifact(ctx, indexRef, ociArtifact); err != nil { + return nil, err + } + + return &index, nil +} + +func CompareImageIndices(actualIndex *oci.Index, expectedIndex *oci.Index) { + Expect(actualIndex.Annotations).To(Equal(expectedIndex.Annotations)) + Expect(len(actualIndex.Manifests)).To(Equal(len(expectedIndex.Manifests))) + + for i := 0; i < len(actualIndex.Manifests); i++ { + actualManifest := actualIndex.Manifests[i] + expectedManifest := expectedIndex.Manifests[i] + + expectedManifestBytes, err := json.Marshal(expectedManifest.Data) + Expect(err).ToNot(HaveOccurred()) + + Expect(actualManifest.Descriptor.MediaType).To(Equal(ocispecv1.MediaTypeImageManifest)) + Expect(actualManifest.Descriptor.Digest).To(Equal(digest.FromBytes(expectedManifestBytes))) + Expect(actualManifest.Descriptor.Size).To(Equal(int64(len(expectedManifestBytes)))) + Expect(actualManifest.Descriptor.Platform).To(Equal(expectedManifest.Descriptor.Platform)) + Expect(actualManifest.Data).To(Equal(expectedManifest.Data)) + } } diff --git a/pkg/transport/process/downloaders/downloaders_suite_test.go b/pkg/transport/process/downloaders/downloaders_suite_test.go index 4d5a0650..b1a767e1 100644 --- a/pkg/transport/process/downloaders/downloaders_suite_test.go +++ b/pkg/transport/process/downloaders/downloaders_suite_test.go @@ -14,6 +14,7 @@ import ( cdoci "github.com/gardener/component-spec/bindings-go/oci" "github.com/go-logr/logr" "github.com/mandelsoft/vfs/pkg/memoryfs" + "github.com/mandelsoft/vfs/pkg/vfs" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/opencontainers/go-digest" @@ -32,15 +33,19 @@ func TestConfig(t *testing.T) { } var ( - testenv *envtest.Environment - ociClient ociclient.Client - ociCache cache.Cache - keyring *credentials.GeneralOciKeyring - testComponent cdv2.ComponentDescriptor - localOciBlobResIndex = 0 - localOciBlobData = []byte("Hello World") - ociArtifactResIndex = 1 - expectedOciArtifact oci.Artifact + testenv *envtest.Environment + ociClient ociclient.Client + ociCache cache.Cache + keyring *credentials.GeneralOciKeyring + testComponent cdv2.ComponentDescriptor + localOciBlobResIndex = 0 + localOciBlobData = []byte("Hello World") + imageRef string + imageResIndex = 1 + expectedImageManifest oci.Manifest + imageIndexRef string + imageIndexResIndex = 2 + expectedImageIndex oci.Index ) var _ = BeforeSuite(func() { @@ -69,11 +74,56 @@ var _ = AfterSuite(func() { }) func uploadTestComponent() { - dgst := digest.FromBytes(localOciBlobData) - + ctx := context.TODO() fs := memoryfs.New() + + localOciBlobRes := createLocalOciBlobRes(fs) + imageRes := createImageRes(ctx) + imageIndexRes := createImageIndexRes(ctx) + + ociRepo := cdv2.NewOCIRegistryRepository(testenv.Addr+"/test/downloaders", "") + repoCtx, err := cdv2.NewUnstructured( + ociRepo, + ) + Expect(err).ToNot(HaveOccurred()) + + localCd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + ObjectMeta: cdv2.ObjectMeta{ + Name: "github.com/component-cli/test-component", + Version: "0.1.0", + }, + Provider: cdv2.InternalProvider, + RepositoryContexts: []*cdv2.UnstructuredTypedObject{ + &repoCtx, + }, + Resources: []cdv2.Resource{ + localOciBlobRes, + imageRes, + imageIndexRes, + }, + }, + } + + manifest, err := cdoci.NewManifestBuilder(ociCache, ctf.NewComponentArchive(&localCd, fs)).Build(ctx) + Expect(err).ToNot(HaveOccurred()) + + ociRef, err := cdoci.OCIRef(*ociRepo, localCd.Name, localCd.Version) + Expect(err).ToNot(HaveOccurred()) + + Expect(ociClient.PushManifest(ctx, ociRef, manifest)).To(Succeed()) + + cdresolver := cdoci.NewResolver(ociClient) + actualCd, err := cdresolver.Resolve(ctx, ociRepo, localCd.Name, localCd.Version) + Expect(err).ToNot(HaveOccurred()) + + testComponent = *actualCd +} + +func createLocalOciBlobRes(fs vfs.FileSystem) cdv2.Resource { Expect(fs.Mkdir(ctf.BlobsDirectoryName, os.ModePerm)).To(Succeed()) + dgst := digest.FromBytes(localOciBlobData) blobfile, err := fs.Create(ctf.BlobPath(dgst.String())) Expect(err).ToNot(HaveOccurred()) @@ -82,8 +132,6 @@ func uploadTestComponent() { Expect(blobfile.Close()).To(Succeed()) - ctx := context.TODO() - localOciBlobAcc, err := cdv2.NewUnstructured( cdv2.NewLocalFilesystemBlobAccess( dgst.String(), @@ -94,7 +142,7 @@ func uploadTestComponent() { localOciBlobRes := cdv2.Resource{ IdentityObjectMeta: cdv2.IdentityObjectMeta{ - Name: "local-oci-blob-res", + Name: "local-oci-blob", Version: "0.1.0", Type: "plain-text", }, @@ -102,67 +150,67 @@ func uploadTestComponent() { Access: &localOciBlobAcc, } - ociArtifactRef := testenv.Addr + "/test/downloaders/image:0.1.0" + return localOciBlobRes +} - m, d := testutils.UploadTestManifest(ctx, ociClient, ociArtifactRef) - a, err := oci.NewManifestArtifact(&oci.Manifest{ - Descriptor: d, - Data: m, - }) +func createImageRes(ctx context.Context) cdv2.Resource { + imageRef = testenv.Addr + "/test/downloaders/image:0.1.0" + + manifest, desc, err := testutils.UploadTestManifest(ctx, ociClient, imageRef) Expect(err).ToNot(HaveOccurred()) - expectedOciArtifact = *a - ociArtifactAcc, err := cdv2.NewUnstructured( + // TODO: currently needed to fill the cache. remove from test, also from ociclient unit test + testutils.CompareManifestToTestManifest(context.TODO(), ociClient, imageRef, manifest) + + expectedImageManifest = oci.Manifest{ + Descriptor: desc, + Data: manifest, + } + + acc, err := cdv2.NewUnstructured( cdv2.NewOCIRegistryAccess( - ociArtifactRef, + imageRef, ), ) Expect(err).ToNot(HaveOccurred()) - ociArtifactRes := cdv2.Resource{ + res := cdv2.Resource{ IdentityObjectMeta: cdv2.IdentityObjectMeta{ - Name: "oci-artifact-res", + Name: "image", Version: "0.1.0", Type: cdv2.OCIImageType, }, Relation: cdv2.LocalRelation, - Access: &ociArtifactAcc, + Access: &acc, } - ociRepo := cdv2.NewOCIRegistryRepository(testenv.Addr+"/test/downloaders", "") - repoCtx, err := cdv2.NewUnstructured( - ociRepo, - ) - Expect(err).ToNot(HaveOccurred()) - - localCd := cdv2.ComponentDescriptor{ - ComponentSpec: cdv2.ComponentSpec{ - ObjectMeta: cdv2.ObjectMeta{ - Name: "github.com/component-cli/test-component", - Version: "0.1.0", - }, - Provider: cdv2.InternalProvider, - RepositoryContexts: []*cdv2.UnstructuredTypedObject{ - &repoCtx, - }, - Resources: []cdv2.Resource{ - localOciBlobRes, - ociArtifactRes, - }, - }, - } + return res +} - manifest, err := cdoci.NewManifestBuilder(ociCache, ctf.NewComponentArchive(&localCd, fs)).Build(ctx) - Expect(err).ToNot(HaveOccurred()) +func createImageIndexRes(ctx context.Context) cdv2.Resource { + imageIndexRef = testenv.Addr + "/test/downloaders/image-index:0.1.0" - ociRef, err := cdoci.OCIRef(*ociRepo, localCd.Name, localCd.Version) + i, err := testutils.UploadTestIndex(ctx, ociClient, imageIndexRef) Expect(err).ToNot(HaveOccurred()) - Expect(ociClient.PushManifest(ctx, ociRef, manifest)).To(Succeed()) + expectedImageIndex = *i - cdresolver := cdoci.NewResolver(ociClient) - actualCd, err := cdresolver.Resolve(ctx, ociRepo, localCd.Name, localCd.Version) + acc, err := cdv2.NewUnstructured( + cdv2.NewOCIRegistryAccess( + imageIndexRef, + ), + ) Expect(err).ToNot(HaveOccurred()) - testComponent = *actualCd + res := cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "image-index", + Version: "0.1.0", + Type: cdv2.OCIImageType, + }, + Relation: cdv2.LocalRelation, + Access: &acc, + } + + return res } diff --git a/pkg/transport/process/downloaders/local_oci_blob_test.go b/pkg/transport/process/downloaders/local_oci_blob_test.go index 35d1076f..f60c95a3 100644 --- a/pkg/transport/process/downloaders/local_oci_blob_test.go +++ b/pkg/transport/process/downloaders/local_oci_blob_test.go @@ -47,7 +47,7 @@ var _ = Describe("localOciBlob", func() { }) It("should return error if called with resource of invalid type", func() { - ociArtifactRes := testComponent.Resources[ociArtifactResIndex] + ociArtifactRes := testComponent.Resources[imageResIndex] d, err := downloaders.NewLocalOCIBlobDownloader(ociClient) Expect(err).ToNot(HaveOccurred()) diff --git a/pkg/transport/process/downloaders/oci_artifact.go b/pkg/transport/process/downloaders/oci_artifact.go index 5a27602e..d661faa0 100644 --- a/pkg/transport/process/downloaders/oci_artifact.go +++ b/pkg/transport/process/downloaders/oci_artifact.go @@ -61,7 +61,6 @@ func (d *ociArtifactDownloader) Process(ctx context.Context, r io.Reader, w io.W return fmt.Errorf("unable to get oci artifact: %w", err) } - // fetch config blobs which adds them to the client cache if ociArtifact.IsManifest() { if err := d.fetchConfigAndLayerBlobs(ctx, ociAccess.ImageReference, ociArtifact.GetManifest().Data); err != nil { return err diff --git a/pkg/transport/process/downloaders/oci_artifact_test.go b/pkg/transport/process/downloaders/oci_artifact_test.go index 764a426f..2232f692 100644 --- a/pkg/transport/process/downloaders/oci_artifact_test.go +++ b/pkg/transport/process/downloaders/oci_artifact_test.go @@ -10,6 +10,7 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "github.com/gardener/component-cli/pkg/testutils" "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/transport/process/downloaders" "github.com/gardener/component-cli/pkg/transport/process/serialize" @@ -19,11 +20,11 @@ var _ = Describe("ociArtifact", func() { Context("Process", func() { - It("should download and stream resource", func() { - ociArtifactRes := testComponent.Resources[ociArtifactResIndex] + It("should download and stream oci image", func() { + ociImageRes := testComponent.Resources[imageResIndex] inProcessorMsg := bytes.NewBuffer([]byte{}) - err := process.WriteProcessorMessage(testComponent, ociArtifactRes, nil, inProcessorMsg) + err := process.WriteProcessorMessage(testComponent, ociImageRes, nil, inProcessorMsg) Expect(err).ToNot(HaveOccurred()) d, err := downloaders.NewOCIArtifactDownloader(ociClient, ociCache) @@ -38,11 +39,37 @@ var _ = Describe("ociArtifact", func() { defer resBlobReader.Close() Expect(*actualCd).To(Equal(testComponent)) - Expect(actualRes).To(Equal(ociArtifactRes)) + Expect(actualRes).To(Equal(ociImageRes)) actualOciArtifact, err := serialize.DeserializeOCIArtifact(resBlobReader, ociCache) Expect(err).ToNot(HaveOccurred()) - Expect(*actualOciArtifact).To(Equal(expectedOciArtifact)) + Expect(*actualOciArtifact.GetManifest()).To(Equal(expectedImageManifest)) + }) + + It("should download and stream oci image index", func() { + ociImageIndexRes := testComponent.Resources[imageIndexResIndex] + + inProcessorMsg := bytes.NewBuffer([]byte{}) + err := process.WriteProcessorMessage(testComponent, ociImageIndexRes, nil, inProcessorMsg) + Expect(err).ToNot(HaveOccurred()) + + d, err := downloaders.NewOCIArtifactDownloader(ociClient, ociCache) + Expect(err).ToNot(HaveOccurred()) + + outProcessorMsg := bytes.NewBuffer([]byte{}) + err = d.Process(context.TODO(), inProcessorMsg, outProcessorMsg) + Expect(err).ToNot(HaveOccurred()) + + actualCd, actualRes, resBlobReader, err := process.ReadProcessorMessage(outProcessorMsg) + Expect(err).ToNot(HaveOccurred()) + defer resBlobReader.Close() + + Expect(*actualCd).To(Equal(testComponent)) + Expect(actualRes).To(Equal(ociImageIndexRes)) + + actualOciArtifact, err := serialize.DeserializeOCIArtifact(resBlobReader, ociCache) + Expect(err).ToNot(HaveOccurred()) + testutils.CompareImageIndices(actualOciArtifact.GetIndex(), &expectedImageIndex) }) It("should return error if called with resource of invalid type", func() { From 00cb5682d070b6499850871378b750b2ac661dc3 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Thu, 28 Oct 2021 15:46:24 +0200 Subject: [PATCH 50/94] wip --- pkg/testutils/oci.go | 5 +++-- pkg/transport/process/downloaders/oci_artifact.go | 3 +-- pkg/transport/process/downloaders/oci_artifact_test.go | 6 +++--- .../{serialize/oci_image.go => oci_image_serialization.go} | 3 ++- pkg/transport/process/processors/oci_image_filter.go | 5 ++--- 5 files changed, 11 insertions(+), 11 deletions(-) rename pkg/transport/process/{serialize/oci_image.go => oci_image_serialization.go} (99%) diff --git a/pkg/testutils/oci.go b/pkg/testutils/oci.go index 2f1d7b2d..b5d0c8ce 100644 --- a/pkg/testutils/oci.go +++ b/pkg/testutils/oci.go @@ -11,11 +11,12 @@ import ( "io" "strings" - "github.com/gardener/component-cli/ociclient" - "github.com/gardener/component-cli/ociclient/oci" . "github.com/onsi/gomega" "github.com/opencontainers/go-digest" ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/ociclient/oci" ) func UploadTestManifest(ctx context.Context, client ociclient.Client, ref string) (*ocispecv1.Manifest, ocispecv1.Descriptor, error) { diff --git a/pkg/transport/process/downloaders/oci_artifact.go b/pkg/transport/process/downloaders/oci_artifact.go index d661faa0..52fd76a1 100644 --- a/pkg/transport/process/downloaders/oci_artifact.go +++ b/pkg/transport/process/downloaders/oci_artifact.go @@ -16,7 +16,6 @@ import ( "github.com/gardener/component-cli/ociclient" "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/pkg/transport/process" - "github.com/gardener/component-cli/pkg/transport/process/serialize" ) type ociArtifactDownloader struct { @@ -73,7 +72,7 @@ func (d *ociArtifactDownloader) Process(ctx context.Context, r io.Reader, w io.W } } - blobReader, err := serialize.SerializeOCIArtifact(*ociArtifact, d.cache) + blobReader, err := process.SerializeOCIArtifact(*ociArtifact, d.cache) if err != nil { return fmt.Errorf("unable to serialize oci artifact: %w", err) } diff --git a/pkg/transport/process/downloaders/oci_artifact_test.go b/pkg/transport/process/downloaders/oci_artifact_test.go index 2232f692..f3282c09 100644 --- a/pkg/transport/process/downloaders/oci_artifact_test.go +++ b/pkg/transport/process/downloaders/oci_artifact_test.go @@ -13,7 +13,6 @@ import ( "github.com/gardener/component-cli/pkg/testutils" "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/transport/process/downloaders" - "github.com/gardener/component-cli/pkg/transport/process/serialize" ) var _ = Describe("ociArtifact", func() { @@ -41,9 +40,10 @@ var _ = Describe("ociArtifact", func() { Expect(*actualCd).To(Equal(testComponent)) Expect(actualRes).To(Equal(ociImageRes)) - actualOciArtifact, err := serialize.DeserializeOCIArtifact(resBlobReader, ociCache) + actualOciArtifact, err := process.DeserializeOCIArtifact(resBlobReader, ociCache) Expect(err).ToNot(HaveOccurred()) Expect(*actualOciArtifact.GetManifest()).To(Equal(expectedImageManifest)) + testutils.CompareManifestToTestManifest(context.TODO(), ociClient, imageRef, expectedImageManifest.Data) }) It("should download and stream oci image index", func() { @@ -67,7 +67,7 @@ var _ = Describe("ociArtifact", func() { Expect(*actualCd).To(Equal(testComponent)) Expect(actualRes).To(Equal(ociImageIndexRes)) - actualOciArtifact, err := serialize.DeserializeOCIArtifact(resBlobReader, ociCache) + actualOciArtifact, err := process.DeserializeOCIArtifact(resBlobReader, ociCache) Expect(err).ToNot(HaveOccurred()) testutils.CompareImageIndices(actualOciArtifact.GetIndex(), &expectedImageIndex) }) diff --git a/pkg/transport/process/serialize/oci_image.go b/pkg/transport/process/oci_image_serialization.go similarity index 99% rename from pkg/transport/process/serialize/oci_image.go rename to pkg/transport/process/oci_image_serialization.go index 45378e9f..0fbd1e20 100644 --- a/pkg/transport/process/serialize/oci_image.go +++ b/pkg/transport/process/oci_image_serialization.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // // SPDX-License-Identifier -package serialize +package process import ( "archive/tar" @@ -28,6 +28,7 @@ const ( BlobsDir = "blobs" ) +// func SerializeOCIArtifact(ociArtifact oci.Artifact, cache cache.Cache) (io.ReadCloser, error) { tmpfile, err := ioutil.TempFile("", "") if err != nil { diff --git a/pkg/transport/process/processors/oci_image_filter.go b/pkg/transport/process/processors/oci_image_filter.go index 348d9ad7..ae07e9c6 100644 --- a/pkg/transport/process/processors/oci_image_filter.go +++ b/pkg/transport/process/processors/oci_image_filter.go @@ -21,7 +21,6 @@ import ( "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/ociclient/oci" "github.com/gardener/component-cli/pkg/transport/process" - "github.com/gardener/component-cli/pkg/transport/process/serialize" "github.com/gardener/component-cli/pkg/utils" ) @@ -37,7 +36,7 @@ func (f *ociImageFilter) Process(ctx context.Context, r io.Reader, w io.Writer) } defer blobreader.Close() - ociArtifact, err := serialize.DeserializeOCIArtifact(blobreader, f.cache) + ociArtifact, err := process.DeserializeOCIArtifact(blobreader, f.cache) if err != nil { return fmt.Errorf("unable to deserialize oci artifact: %w", err) } @@ -78,7 +77,7 @@ func (f *ociImageFilter) Process(ctx context.Context, r io.Reader, w io.Writer) } } - blobReader, err := serialize.SerializeOCIArtifact(*ociArtifact, f.cache) + blobReader, err := process.SerializeOCIArtifact(*ociArtifact, f.cache) if err != nil { return fmt.Errorf("unable to serialice oci artifact: %w", err) } From 06815ffe0dc69333f77eb72a38a959d63e3cc0cb Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Fri, 29 Oct 2021 16:11:49 +0200 Subject: [PATCH 51/94] adds first test for oci image serialization --- ociclient/client.go | 19 +- pkg/testutils/oci.go | 8 +- ...ation.go => oci_artifact_serialization.go} | 61 ++++-- .../oci_artifact_serialization_test.go | 200 ++++++++++++++++++ 4 files changed, 259 insertions(+), 29 deletions(-) rename pkg/transport/process/{oci_image_serialization.go => oci_artifact_serialization.go} (71%) create mode 100644 pkg/transport/process/oci_artifact_serialization_test.go diff --git a/ociclient/client.go b/ociclient/client.go index 7370495d..8db66d99 100644 --- a/ociclient/client.go +++ b/ociclient/client.go @@ -318,10 +318,21 @@ func (c *client) pushManifest(ctx context.Context, manifest *ocispecv1.Manifest, } } - manifestDesc, err := createDescriptorFromManifest(cache, manifest) + manifestDesc, err := CreateDescriptorFromManifest(manifest) if err != nil { return ocispecv1.Descriptor{}, fmt.Errorf("unable to create manifest descriptor: %w", err) } + + manifestBytes, err := json.Marshal(manifest) + if err != nil { + return ocispecv1.Descriptor{}, fmt.Errorf("unable to marshal manifest: %w", err) + } + + manifestBuf := bytes.NewBuffer(manifestBytes) + if err := cache.Add(manifestDesc, ioutil.NopCloser(manifestBuf)); err != nil { + return ocispecv1.Descriptor{}, fmt.Errorf("unable to add manifest to cache: %w", err) + } + if err := c.pushContent(ctx, cache, pusher, manifestDesc); err != nil { return ocispecv1.Descriptor{}, fmt.Errorf("unable to push manifest: %w", err) } @@ -727,7 +738,7 @@ func doRequestWithPaging(ctx context.Context, u *url.URL, pFunc pagingFunc) erro } } -func createDescriptorFromManifest(cache cache.Cache, manifest *ocispecv1.Manifest) (ocispecv1.Descriptor, error) { +func CreateDescriptorFromManifest(manifest *ocispecv1.Manifest) (ocispecv1.Descriptor, error) { if manifest.SchemaVersion == 0 { manifest.SchemaVersion = 2 } @@ -741,10 +752,6 @@ func createDescriptorFromManifest(cache cache.Cache, manifest *ocispecv1.Manifes Size: int64(len(manifestBytes)), } - manifestBuf := bytes.NewBuffer(manifestBytes) - if err := cache.Add(manifestDescriptor, ioutil.NopCloser(manifestBuf)); err != nil { - return ocispecv1.Descriptor{}, err - } return manifestDescriptor, nil } diff --git a/pkg/testutils/oci.go b/pkg/testutils/oci.go index b5d0c8ce..dc1262ae 100644 --- a/pkg/testutils/oci.go +++ b/pkg/testutils/oci.go @@ -20,13 +20,13 @@ import ( ) func UploadTestManifest(ctx context.Context, client ociclient.Client, ref string) (*ocispecv1.Manifest, ocispecv1.Descriptor, error) { - data := []byte("test") + configData := []byte("test") layerData := []byte("layer-data") manifest := &ocispecv1.Manifest{ Config: ocispecv1.Descriptor{ MediaType: "text/plain", - Digest: digest.FromBytes(data), - Size: int64(len(data)), + Digest: digest.FromBytes(configData), + Size: int64(len(configData)), }, Layers: []ocispecv1.Descriptor{ { @@ -39,7 +39,7 @@ func UploadTestManifest(ctx context.Context, client ociclient.Client, ref string store := ociclient.GenericStore(func(ctx context.Context, desc ocispecv1.Descriptor, writer io.Writer) error { switch desc.Digest.String() { case manifest.Config.Digest.String(): - _, err := writer.Write(data) + _, err := writer.Write(configData) return err default: _, err := writer.Write(layerData) diff --git a/pkg/transport/process/oci_image_serialization.go b/pkg/transport/process/oci_artifact_serialization.go similarity index 71% rename from pkg/transport/process/oci_image_serialization.go rename to pkg/transport/process/oci_artifact_serialization.go index 0fbd1e20..4658a57a 100644 --- a/pkg/transport/process/oci_image_serialization.go +++ b/pkg/transport/process/oci_artifact_serialization.go @@ -17,18 +17,28 @@ import ( "github.com/opencontainers/image-spec/specs-go" ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/gardener/component-cli/ociclient" "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/ociclient/oci" "github.com/gardener/component-cli/pkg/utils" ) const ( + // ManifestFile is the name of the manifest file of a serialized oci artifact ManifestFile = "manifest.json" - IndexFile = "index.json" - BlobsDir = "blobs" + + // IndexFile is the name of the image index file of a serialized oci artifact + IndexFile = "index.json" + + // BlobsDir is the name of the blobs directory of a serialized oci artifact + BlobsDir = "blobs" ) -// +// SerializeOCIArtifact serializes an oci artifact into a TAR archive. the TAR archive contains +// the manifest.json (if the oci artifact is of type manifest) or index.json (if the oci artifact +// is a docker image list / oci image index) and a single directory which contains all blobs. +// the cache instance is used for reading config and layer blobs. returns a reader for the TAR +// archive which must be closed by the caller. func SerializeOCIArtifact(ociArtifact oci.Artifact, cache cache.Cache) (io.ReadCloser, error) { tmpfile, err := ioutil.TempFile("", "") if err != nil { @@ -58,11 +68,19 @@ func serializeImageIndex(cache cache.Cache, index *oci.Index, w io.Writer) error manifestDescs := []ocispecv1.Descriptor{} for _, m := range index.Manifests { - manifestFile := path.Join(BlobsDir, m.Descriptor.Digest.Encoded()) + mDesc, err := ociclient.CreateDescriptorFromManifest(m.Data) + if err != nil { + return fmt.Errorf("unable to create manifest descriptor: %w", err) + } + mDesc.Annotations = m.Descriptor.Annotations + mDesc.Platform = m.Descriptor.Platform + mDesc.URLs = m.Descriptor.URLs + + manifestFile := path.Join(BlobsDir, mDesc.Digest.Encoded()) if err := serializeImage(cache, m, manifestFile, tw); err != nil { return fmt.Errorf("unable to serialize image: %w", err) } - manifestDescs = append(manifestDescs, m.Descriptor) + manifestDescs = append(manifestDescs, mDesc) } i := ocispecv1.Index{ @@ -75,11 +93,11 @@ func serializeImageIndex(cache cache.Cache, index *oci.Index, w io.Writer) error indexBytes, err := json.Marshal(i) if err != nil { - return fmt.Errorf("unable to marshal index manifest: %w", err) + return fmt.Errorf("unable to marshal image index: %w", err) } if err := utils.WriteFileToTARArchive(IndexFile, bytes.NewReader(indexBytes), tw); err != nil { - return fmt.Errorf("unable to write index manifest: %w", err) + return fmt.Errorf("unable to write image index: %w", err) } return nil @@ -122,6 +140,10 @@ func serializeImage(cache cache.Cache, manifest *oci.Manifest, manifestFile stri return nil } +// DeserializeOCIArtifact deserializes an oci artifact from a TAR archive. the TAR archive must contain +// the manifest.json (if the oci artifact is of type manifest) or index.json (if the oci artifact +// is a docker image list / oci image index) and a single directory which contains all blobs. +// all blobs from the blobs directory are additionally stored in the cache instance. func DeserializeOCIArtifact(r io.Reader, cache cache.Cache) (*oci.Artifact, error) { tr := tar.NewReader(r) @@ -149,30 +171,31 @@ func DeserializeOCIArtifact(r io.Reader, cache cache.Cache) (*oci.Artifact, erro } else if strings.HasPrefix(header.Name, BlobsDir) { tmpfile, err := ioutil.TempFile("", "") if err != nil { - return nil, fmt.Errorf("") + return nil, fmt.Errorf("unable to create tempfile: %w", err) } if _, err := io.Copy(tmpfile, tr); err != nil { - return nil, fmt.Errorf("") + return nil, fmt.Errorf("unable to copy file content to tempfile: %w", err) } - dgst, err := digest.FromReader(tmpfile) - if err != nil { - return nil, fmt.Errorf("unable to calculate digest for blobfile %s: %w", header.Name, err) + splittedFilename := strings.Split(header.Name, "/") + if len(splittedFilename) != 2 { + return nil, fmt.Errorf("unable to process file: invalid filename %s must follow schema blobs/", header.Name) } - if _, err := tmpfile.Seek(0, io.SeekStart); err != nil { - return nil, fmt.Errorf("unable to seek to beginning of file: %w", err) + desc := ocispecv1.Descriptor{ + Digest: digest.NewDigestFromEncoded(digest.SHA256, splittedFilename[1]), } - desc := ocispecv1.Descriptor{ - Digest: dgst, + if _, err := tmpfile.Seek(0, io.SeekStart); err != nil { + return nil, fmt.Errorf("unable to seek to beginning of tempfile: %w", err) } + if err := cache.Add(desc, tmpfile); err != nil { return nil, fmt.Errorf("unable to write blob %+v to cache: %w", desc, err) } } else { - return nil, fmt.Errorf("unknown file") + return nil, fmt.Errorf("unknown file %s", header.Name) } } @@ -194,12 +217,12 @@ func DeserializeOCIArtifact(r io.Reader, cache cache.Cache) (*oci.Artifact, erro buf := bytes.NewBuffer([]byte{}) if _, err := io.Copy(buf, blobreader); err != nil { - return nil, fmt.Errorf("unable to copy %s to buffer: %w", ManifestFile, err) + return nil, fmt.Errorf("unable to copy manifest to buffer: %w", err) } var manifest ocispecv1.Manifest if err := json.Unmarshal(buf.Bytes(), &manifest); err != nil { - return nil, fmt.Errorf("unable to unmarshal %s: %w", ManifestFile, err) + return nil, fmt.Errorf("unable to unmarshal manifest: %w", err) } m := oci.Manifest{ diff --git a/pkg/transport/process/oci_artifact_serialization_test.go b/pkg/transport/process/oci_artifact_serialization_test.go new file mode 100644 index 00000000..95499931 --- /dev/null +++ b/pkg/transport/process/oci_artifact_serialization_test.go @@ -0,0 +1,200 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package process_test + +import ( + "bytes" + "encoding/json" + "io" + + "github.com/gardener/component-cli/ociclient/cache" + "github.com/gardener/component-cli/ociclient/oci" + "github.com/gardener/component-cli/pkg/transport/process" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/opencontainers/go-digest" + ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +var _ = Describe("oci artifact serialization", func() { + + Context("serialize and deserialize oci artifact", func() { + + It("should correctly serialize and deserialize image", func() { + configData := []byte("config-data") + layerData := []byte("layer-data") + m, _, err := createManifest(configData, layerData) + Expect(err).ToNot(HaveOccurred()) + + expectedOciArtifact, err := oci.NewManifestArtifact( + &oci.Manifest{ + Data: m, + }, + ) + Expect(err).ToNot(HaveOccurred()) + + serializeCache := cache.NewInMemoryCache() + Expect(serializeCache.Add(m.Config, io.NopCloser(bytes.NewReader(configData)))).To(Succeed()) + Expect(serializeCache.Add(m.Layers[0], io.NopCloser(bytes.NewReader(layerData)))).To(Succeed()) + + serializedReader, err := process.SerializeOCIArtifact(*expectedOciArtifact, serializeCache) + Expect(err).ToNot(HaveOccurred()) + + deserializeCache := cache.NewInMemoryCache() + actualOciArtifact, err := process.DeserializeOCIArtifact(serializedReader, deserializeCache) + Expect(err).ToNot(HaveOccurred()) + Expect(actualOciArtifact.GetManifest().Data).To(Equal(expectedOciArtifact.GetManifest().Data)) + + actualConfigReader, err := deserializeCache.Get(actualOciArtifact.GetManifest().Data.Config) + Expect(err).ToNot(HaveOccurred()) + actualConfigBuf := bytes.NewBuffer([]byte{}) + _, err = io.Copy(actualConfigBuf, actualConfigReader) + Expect(err).ToNot(HaveOccurred()) + Expect(actualConfigBuf.Bytes()).To(Equal(configData)) + + actualLayerReader, err := deserializeCache.Get(actualOciArtifact.GetManifest().Data.Layers[0]) + Expect(err).ToNot(HaveOccurred()) + actualLayerBuf := bytes.NewBuffer([]byte{}) + _, err = io.Copy(actualLayerBuf, actualLayerReader) + Expect(err).ToNot(HaveOccurred()) + Expect(actualLayerBuf.Bytes()).To(Equal(layerData)) + }) + + It("should correctly serialize and deserialize image index", func() { + configData1 := []byte("config-data-1") + layerData1 := []byte("layer-data-1") + configData2 := []byte("config-data-2") + layerData2 := []byte("layer-data-2") + + m1, m1Desc, err := createManifest(configData1, layerData1) + Expect(err).ToNot(HaveOccurred()) + m2, m2Desc, err := createManifest(configData2, layerData2) + Expect(err).ToNot(HaveOccurred()) + + m1Bytes, err := json.Marshal(m1) + Expect(err).ToNot(HaveOccurred()) + + m2Bytes, err := json.Marshal(m2) + Expect(err).ToNot(HaveOccurred()) + + expectedOciArtifact, err := oci.NewIndexArtifact( + &oci.Index{ + Manifests: []*oci.Manifest{ + { + Data: m1, + }, + { + Data: m2, + }, + }, + Annotations: map[string]string{ + "testkey": "testval", + }, + }, + ) + Expect(err).ToNot(HaveOccurred()) + + serializeCache := cache.NewInMemoryCache() + Expect(serializeCache.Add(m1Desc, io.NopCloser(bytes.NewReader(m1Bytes)))).To(Succeed()) + Expect(serializeCache.Add(m1.Config, io.NopCloser(bytes.NewReader(configData1)))).To(Succeed()) + Expect(serializeCache.Add(m1.Layers[0], io.NopCloser(bytes.NewReader(layerData1)))).To(Succeed()) + Expect(serializeCache.Add(m2Desc, io.NopCloser(bytes.NewReader(m2Bytes)))).To(Succeed()) + Expect(serializeCache.Add(m2.Config, io.NopCloser(bytes.NewReader(configData2)))).To(Succeed()) + Expect(serializeCache.Add(m2.Layers[0], io.NopCloser(bytes.NewReader(layerData2)))).To(Succeed()) + + serializedReader, err := process.SerializeOCIArtifact(*expectedOciArtifact, serializeCache) + Expect(err).ToNot(HaveOccurred()) + + deserializeCache := cache.NewInMemoryCache() + actualOciArtifact, err := process.DeserializeOCIArtifact(serializedReader, deserializeCache) + Expect(err).ToNot(HaveOccurred()) + + // check image index and manifests + Expect(actualOciArtifact.GetIndex().Annotations).To(Equal(expectedOciArtifact.GetIndex().Annotations)) + Expect(actualOciArtifact.GetIndex().Manifests[0].Data).To(Equal(m1)) + Expect(actualOciArtifact.GetIndex().Manifests[1].Data).To(Equal(m2)) + + // check first manifest config and layer + actualConfigReader, err := deserializeCache.Get(actualOciArtifact.GetIndex().Manifests[0].Data.Config) + Expect(err).ToNot(HaveOccurred()) + actualConfigBuf := bytes.NewBuffer([]byte{}) + _, err = io.Copy(actualConfigBuf, actualConfigReader) + Expect(err).ToNot(HaveOccurred()) + Expect(actualConfigBuf.Bytes()).To(Equal(configData1)) + + actualLayerReader, err := deserializeCache.Get(actualOciArtifact.GetIndex().Manifests[0].Data.Layers[0]) + Expect(err).ToNot(HaveOccurred()) + actualLayerBuf := bytes.NewBuffer([]byte{}) + _, err = io.Copy(actualLayerBuf, actualLayerReader) + Expect(err).ToNot(HaveOccurred()) + Expect(actualLayerBuf.Bytes()).To(Equal(layerData1)) + + // check second manifest config and layer + actualConfigReader, err = deserializeCache.Get(actualOciArtifact.GetIndex().Manifests[1].Data.Config) + Expect(err).ToNot(HaveOccurred()) + actualConfigBuf = bytes.NewBuffer([]byte{}) + _, err = io.Copy(actualConfigBuf, actualConfigReader) + Expect(err).ToNot(HaveOccurred()) + Expect(actualConfigBuf.Bytes()).To(Equal(configData2)) + + actualLayerReader, err = deserializeCache.Get(actualOciArtifact.GetIndex().Manifests[1].Data.Layers[0]) + Expect(err).ToNot(HaveOccurred()) + actualLayerBuf = bytes.NewBuffer([]byte{}) + _, err = io.Copy(actualLayerBuf, actualLayerReader) + Expect(err).ToNot(HaveOccurred()) + Expect(actualLayerBuf.Bytes()).To(Equal(layerData2)) + + }) + + }) + + Context("serialize oci artifact", func() { + + It("should raise error if ....", func() { + + }) + + }) + + Context("deserialize oci artifact", func() { + + It("should raise error if ....", func() { + + }) + + }) + +}) + +func createManifest(configData []byte, layerData []byte) (*ocispecv1.Manifest, ocispecv1.Descriptor, error) { + configDesc := ocispecv1.Descriptor{ + MediaType: "text/plain", + Digest: digest.FromBytes(configData), + Size: int64(len(configData)), + } + + layerDesc := ocispecv1.Descriptor{ + MediaType: "text/plain", + Digest: digest.FromBytes(layerData), + Size: int64(len(layerData)), + } + + m := ocispecv1.Manifest{ + Config: configDesc, + Layers: []ocispecv1.Descriptor{ + layerDesc, + }, + } + + mBytes, err := json.Marshal(m) + if err != nil { + return nil, ocispecv1.Descriptor{}, err + } + + d := ocispecv1.Descriptor{ + Digest: digest.FromBytes(mBytes), + } + + return &m, d, nil +} From 162707f4719c8c0abf8a054c4b2355dd6c07d2b7 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 2 Nov 2021 16:02:10 +0100 Subject: [PATCH 52/94] improves tests of oci artifact serialization --- pkg/testutils/oci.go | 32 +++++++ .../process/oci_artifact_serialization.go | 52 +++++++---- .../oci_artifact_serialization_test.go | 88 ++++++++++--------- 3 files changed, 111 insertions(+), 61 deletions(-) diff --git a/pkg/testutils/oci.go b/pkg/testutils/oci.go index dc1262ae..e1850e2f 100644 --- a/pkg/testutils/oci.go +++ b/pkg/testutils/oci.go @@ -149,3 +149,35 @@ func CompareImageIndices(actualIndex *oci.Index, expectedIndex *oci.Index) { Expect(actualManifest.Data).To(Equal(expectedManifest.Data)) } } + +func CreateManifest(configData []byte, layerData []byte) (*ocispecv1.Manifest, ocispecv1.Descriptor, error) { + configDesc := ocispecv1.Descriptor{ + MediaType: "text/plain", + Digest: digest.FromBytes(configData), + Size: int64(len(configData)), + } + + layerDesc := ocispecv1.Descriptor{ + MediaType: "text/plain", + Digest: digest.FromBytes(layerData), + Size: int64(len(layerData)), + } + + m := ocispecv1.Manifest{ + Config: configDesc, + Layers: []ocispecv1.Descriptor{ + layerDesc, + }, + } + + mBytes, err := json.Marshal(m) + if err != nil { + return nil, ocispecv1.Descriptor{}, err + } + + d := ocispecv1.Descriptor{ + Digest: digest.FromBytes(mBytes), + } + + return &m, d, nil +} diff --git a/pkg/transport/process/oci_artifact_serialization.go b/pkg/transport/process/oci_artifact_serialization.go index 4658a57a..b100028e 100644 --- a/pkg/transport/process/oci_artifact_serialization.go +++ b/pkg/transport/process/oci_artifact_serialization.go @@ -7,6 +7,7 @@ import ( "archive/tar" "bytes" "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -40,6 +41,10 @@ const ( // the cache instance is used for reading config and layer blobs. returns a reader for the TAR // archive which must be closed by the caller. func SerializeOCIArtifact(ociArtifact oci.Artifact, cache cache.Cache) (io.ReadCloser, error) { + if cache == nil { + return nil, errors.New("cache must not be nil") + } + tmpfile, err := ioutil.TempFile("", "") if err != nil { return nil, fmt.Errorf("unable to create tempfile: %w", err) @@ -50,9 +55,13 @@ func SerializeOCIArtifact(ociArtifact oci.Artifact, cache cache.Cache) (io.ReadC return nil, fmt.Errorf("unable to serialize image index: %w", err) } } else { - if err := serializeImage(cache, ociArtifact.GetManifest(), ManifestFile, tar.NewWriter(tmpfile)); err != nil { + tw := tar.NewWriter(tmpfile) + if err := serializeImage(cache, ociArtifact.GetManifest(), ManifestFile, tw); err != nil { return nil, fmt.Errorf("unable to serialize image: %w", err) } + if err := tw.Close(); err != nil { + return nil, fmt.Errorf("unable to close tar writer: %w", err) + } } if _, err := tmpfile.Seek(0, io.SeekStart); err != nil { @@ -68,19 +77,19 @@ func serializeImageIndex(cache cache.Cache, index *oci.Index, w io.Writer) error manifestDescs := []ocispecv1.Descriptor{} for _, m := range index.Manifests { - mDesc, err := ociclient.CreateDescriptorFromManifest(m.Data) + manifestDesc, err := ociclient.CreateDescriptorFromManifest(m.Data) if err != nil { return fmt.Errorf("unable to create manifest descriptor: %w", err) } - mDesc.Annotations = m.Descriptor.Annotations - mDesc.Platform = m.Descriptor.Platform - mDesc.URLs = m.Descriptor.URLs + manifestDesc.Annotations = m.Descriptor.Annotations + manifestDesc.Platform = m.Descriptor.Platform + manifestDesc.URLs = m.Descriptor.URLs - manifestFile := path.Join(BlobsDir, mDesc.Digest.Encoded()) + manifestFile := path.Join(BlobsDir, manifestDesc.Digest.Encoded()) if err := serializeImage(cache, m, manifestFile, tw); err != nil { return fmt.Errorf("unable to serialize image: %w", err) } - manifestDescs = append(manifestDescs, mDesc) + manifestDescs = append(manifestDescs, manifestDesc) } i := ocispecv1.Index{ @@ -140,13 +149,20 @@ func serializeImage(cache cache.Cache, manifest *oci.Manifest, manifestFile stri return nil } -// DeserializeOCIArtifact deserializes an oci artifact from a TAR archive. the TAR archive must contain -// the manifest.json (if the oci artifact is of type manifest) or index.json (if the oci artifact -// is a docker image list / oci image index) and a single directory which contains all blobs. -// all blobs from the blobs directory are additionally stored in the cache instance. -func DeserializeOCIArtifact(r io.Reader, cache cache.Cache) (*oci.Artifact, error) { - tr := tar.NewReader(r) +// DeserializeOCIArtifact deserializes an oci artifact from a TAR archive. the TAR archive must +// contain a manifest.json (if the oci artifact is of type manifest) or index.json (if the oci artifact +// artifact is a docker image list / oci image index) and a single directory which contains all blobs. +// all blobs from the blobs directory are stored in the cache instance during deserialization. +func DeserializeOCIArtifact(reader io.Reader, cache cache.Cache) (*oci.Artifact, error) { + if reader == nil { + return nil, errors.New("reader must not be nil") + } + + if cache == nil { + return nil, errors.New("cache must not be nil") + } + tr := tar.NewReader(reader) buf := bytes.NewBuffer([]byte{}) isImageIndex := false @@ -175,7 +191,7 @@ func DeserializeOCIArtifact(r io.Reader, cache cache.Cache) (*oci.Artifact, erro } if _, err := io.Copy(tmpfile, tr); err != nil { - return nil, fmt.Errorf("unable to copy file content to tempfile: %w", err) + return nil, fmt.Errorf("unable to copy %s to tempfile: %w", header.Name, err) } splittedFilename := strings.Split(header.Name, "/") @@ -208,10 +224,10 @@ func DeserializeOCIArtifact(r io.Reader, cache cache.Cache) (*oci.Artifact, erro } manifests := []*oci.Manifest{} - for _, m := range index.Manifests { - blobreader, err := cache.Get(m) + for _, manifestDesc := range index.Manifests { + blobreader, err := cache.Get(manifestDesc) if err != nil { - return nil, fmt.Errorf("unable to get manifest blob: %w", err) + return nil, fmt.Errorf("unable to get manifest blob from cache: %w", err) } defer blobreader.Close() @@ -226,7 +242,7 @@ func DeserializeOCIArtifact(r io.Reader, cache cache.Cache) (*oci.Artifact, erro } m := oci.Manifest{ - Descriptor: m, + Descriptor: manifestDesc, Data: &manifest, } manifests = append(manifests, &m) diff --git a/pkg/transport/process/oci_artifact_serialization_test.go b/pkg/transport/process/oci_artifact_serialization_test.go index 95499931..5f5de0ee 100644 --- a/pkg/transport/process/oci_artifact_serialization_test.go +++ b/pkg/transport/process/oci_artifact_serialization_test.go @@ -4,17 +4,20 @@ package process_test import ( + "archive/tar" "bytes" "encoding/json" "io" + "os" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/ociclient/oci" + "github.com/gardener/component-cli/pkg/testutils" "github.com/gardener/component-cli/pkg/transport/process" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "github.com/opencontainers/go-digest" - ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" ) var _ = Describe("oci artifact serialization", func() { @@ -24,7 +27,7 @@ var _ = Describe("oci artifact serialization", func() { It("should correctly serialize and deserialize image", func() { configData := []byte("config-data") layerData := []byte("layer-data") - m, _, err := createManifest(configData, layerData) + m, _, err := testutils.CreateManifest(configData, layerData) Expect(err).ToNot(HaveOccurred()) expectedOciArtifact, err := oci.NewManifestArtifact( @@ -67,9 +70,9 @@ var _ = Describe("oci artifact serialization", func() { configData2 := []byte("config-data-2") layerData2 := []byte("layer-data-2") - m1, m1Desc, err := createManifest(configData1, layerData1) + m1, m1Desc, err := testutils.CreateManifest(configData1, layerData1) Expect(err).ToNot(HaveOccurred()) - m2, m2Desc, err := createManifest(configData2, layerData2) + m2, m2Desc, err := testutils.CreateManifest(configData2, layerData2) Expect(err).ToNot(HaveOccurred()) m1Bytes, err := json.Marshal(m1) @@ -144,57 +147,56 @@ var _ = Describe("oci artifact serialization", func() { _, err = io.Copy(actualLayerBuf, actualLayerReader) Expect(err).ToNot(HaveOccurred()) Expect(actualLayerBuf.Bytes()).To(Equal(layerData2)) - }) }) Context("serialize oci artifact", func() { - It("should raise error if ....", func() { - + It("should raise error if cache is nil", func() { + _, err := process.SerializeOCIArtifact(oci.Artifact{}, nil) + Expect(err).To(MatchError("cache must not be nil")) }) }) Context("deserialize oci artifact", func() { - It("should raise error if ....", func() { + It("should raise error if reader is nil", func() { + _, err := process.DeserializeOCIArtifact(nil, cache.NewInMemoryCache()) + Expect(err).To(MatchError("reader must not be nil")) + }) + It("should raise error if cache is nil", func() { + buf := bytes.NewBuffer([]byte{}) + _, err := process.DeserializeOCIArtifact(buf, nil) + Expect(err).To(MatchError("cache must not be nil")) + }) + + It("should raise error if tar archive contains unknown file", func() { + fileName := "invalid-filename" + fileContent := []byte("file-content") + + buf := bytes.NewBuffer([]byte{}) + tw := tar.NewWriter(buf) + fileHeader := tar.Header{ + Name: fileName, + Size: int64(len(fileContent)), + Mode: int64(os.ModePerm), + ModTime: time.Now(), + } + + Expect(tw.WriteHeader(&fileHeader)).To(Succeed()) + + _, err := io.Copy(tw, bytes.NewReader(fileContent)) + Expect(err).ToNot(HaveOccurred()) + + Expect(tw.Close()).To(Succeed()) + + _, err = process.DeserializeOCIArtifact(buf, cache.NewInMemoryCache()) + Expect(err).To(MatchError("unknown file " + fileName)) }) }) }) - -func createManifest(configData []byte, layerData []byte) (*ocispecv1.Manifest, ocispecv1.Descriptor, error) { - configDesc := ocispecv1.Descriptor{ - MediaType: "text/plain", - Digest: digest.FromBytes(configData), - Size: int64(len(configData)), - } - - layerDesc := ocispecv1.Descriptor{ - MediaType: "text/plain", - Digest: digest.FromBytes(layerData), - Size: int64(len(layerData)), - } - - m := ocispecv1.Manifest{ - Config: configDesc, - Layers: []ocispecv1.Descriptor{ - layerDesc, - }, - } - - mBytes, err := json.Marshal(m) - if err != nil { - return nil, ocispecv1.Descriptor{}, err - } - - d := ocispecv1.Descriptor{ - Digest: digest.FromBytes(mBytes), - } - - return &m, d, nil -} From db92cd5c595e0ec10f32b36dcfb09eb4291ce7da Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 2 Nov 2021 16:02:42 +0100 Subject: [PATCH 53/94] improves uploaders package --- .../process/uploaders/local_oci_blob_test.go | 4 ++++ pkg/transport/process/uploaders/oci_artifact.go | 11 +++++------ pkg/transport/process/uploaders/oci_artifact_test.go | 4 ++++ .../process/uploaders/uploaders_suite_test.go | 4 ++++ 4 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 pkg/transport/process/uploaders/local_oci_blob_test.go create mode 100644 pkg/transport/process/uploaders/oci_artifact_test.go create mode 100644 pkg/transport/process/uploaders/uploaders_suite_test.go diff --git a/pkg/transport/process/uploaders/local_oci_blob_test.go b/pkg/transport/process/uploaders/local_oci_blob_test.go new file mode 100644 index 00000000..99c7a6e5 --- /dev/null +++ b/pkg/transport/process/uploaders/local_oci_blob_test.go @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package uploaders_test diff --git a/pkg/transport/process/uploaders/oci_artifact.go b/pkg/transport/process/uploaders/oci_artifact.go index a88fca27..1fd9f1a6 100644 --- a/pkg/transport/process/uploaders/oci_artifact.go +++ b/pkg/transport/process/uploaders/oci_artifact.go @@ -14,11 +14,10 @@ import ( "github.com/gardener/component-cli/ociclient" "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/pkg/transport/process" - "github.com/gardener/component-cli/pkg/transport/process/serialize" "github.com/gardener/component-cli/pkg/utils" ) -type ociImageUploader struct { +type ociArtifactUploader struct { client ociclient.Client cache cache.Cache baseUrl string @@ -38,7 +37,7 @@ func NewOCIImageUploader(client ociclient.Client, cache cache.Cache, baseUrl str return nil, errors.New("baseUrl must not be empty") } - obj := ociImageUploader{ + obj := ociArtifactUploader{ client: client, cache: cache, baseUrl: baseUrl, @@ -47,14 +46,14 @@ func NewOCIImageUploader(client ociclient.Client, cache cache.Cache, baseUrl str return &obj, nil } -func (u *ociImageUploader) Process(ctx context.Context, r io.Reader, w io.Writer) error { +func (u *ociArtifactUploader) Process(ctx context.Context, r io.Reader, w io.Writer) error { cd, res, resBlobReader, err := process.ReadProcessorMessage(r) if err != nil { return fmt.Errorf("unable to read processor message: %w", err) } defer resBlobReader.Close() - ociArtifact, err := serialize.DeserializeOCIArtifact(resBlobReader, u.cache) + ociArtifact, err := process.DeserializeOCIArtifact(resBlobReader, u.cache) if err != nil { return fmt.Errorf("unable to deserialize oci artifact: %w", err) } @@ -81,7 +80,7 @@ func (u *ociImageUploader) Process(ctx context.Context, r io.Reader, w io.Writer return fmt.Errorf("unable to push oci artifact: %w", err) } - blobReader, err := serialize.SerializeOCIArtifact(*ociArtifact, u.cache) + blobReader, err := process.SerializeOCIArtifact(*ociArtifact, u.cache) if err != nil { return fmt.Errorf("unable to serialize oci artifact: %w", err) } diff --git a/pkg/transport/process/uploaders/oci_artifact_test.go b/pkg/transport/process/uploaders/oci_artifact_test.go new file mode 100644 index 00000000..99c7a6e5 --- /dev/null +++ b/pkg/transport/process/uploaders/oci_artifact_test.go @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package uploaders_test diff --git a/pkg/transport/process/uploaders/uploaders_suite_test.go b/pkg/transport/process/uploaders/uploaders_suite_test.go new file mode 100644 index 00000000..99c7a6e5 --- /dev/null +++ b/pkg/transport/process/uploaders/uploaders_suite_test.go @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package uploaders_test From 3b4a5dc51878793aa5bf6fb21a9a1b865f7f9d9d Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 2 Nov 2021 16:02:58 +0100 Subject: [PATCH 54/94] renames file --- pkg/transport/process/processors/{util.go => uds_server.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pkg/transport/process/processors/{util.go => uds_server.go} (100%) diff --git a/pkg/transport/process/processors/util.go b/pkg/transport/process/processors/uds_server.go similarity index 100% rename from pkg/transport/process/processors/util.go rename to pkg/transport/process/processors/uds_server.go From 5b1ab2b78bbafb1b819154526b750eda68ca5a1d Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 2 Nov 2021 16:31:52 +0100 Subject: [PATCH 55/94] moves function to utils package and adds tests --- pkg/transport/process/util.go | 47 ++-------------- pkg/transport/process/util_test.go | 1 + pkg/utils/utils.go | 52 ++++++++++++++++++ pkg/utils/utils_suite_test.go | 16 ++++++ pkg/utils/utils_test.go | 86 ++++++++++++++++++++++++++++++ 5 files changed, 160 insertions(+), 42 deletions(-) create mode 100644 pkg/utils/utils_suite_test.go create mode 100644 pkg/utils/utils_test.go diff --git a/pkg/transport/process/util.go b/pkg/transport/process/util.go index 5019d077..a991c89a 100644 --- a/pkg/transport/process/util.go +++ b/pkg/transport/process/util.go @@ -13,10 +13,11 @@ import ( "net" "os" "sync" - "time" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" "sigs.k8s.io/yaml" + + "github.com/gardener/component-cli/pkg/utils" ) const ( @@ -42,7 +43,7 @@ func WriteProcessorMessage(cd cdv2.ComponentDescriptor, res cdv2.Resource, resou return fmt.Errorf("unable to marshal component descriptor: %w", err) } - if err := writeFileToTARArchive(ComponentDescriptorFile, bytes.NewReader(marshaledCD), tw); err != nil { + if err := utils.WriteFileToTARArchive(ComponentDescriptorFile, bytes.NewReader(marshaledCD), tw); err != nil { return fmt.Errorf("unable to write %s: %w", ComponentDescriptorFile, err) } @@ -51,12 +52,12 @@ func WriteProcessorMessage(cd cdv2.ComponentDescriptor, res cdv2.Resource, resou return fmt.Errorf("unable to marshal resource: %w", err) } - if err := writeFileToTARArchive(ResourceFile, bytes.NewReader(marshaledRes), tw); err != nil { + if err := utils.WriteFileToTARArchive(ResourceFile, bytes.NewReader(marshaledRes), tw); err != nil { return fmt.Errorf("unable to write %s: %w", ResourceFile, err) } if resourceBlobReader != nil { - if err := writeFileToTARArchive(ResourceBlobFile, resourceBlobReader, tw); err != nil { + if err := utils.WriteFileToTARArchive(ResourceBlobFile, resourceBlobReader, tw); err != nil { return fmt.Errorf("unable to write %s: %w", ResourceBlobFile, err) } } @@ -64,44 +65,6 @@ func WriteProcessorMessage(cd cdv2.ComponentDescriptor, res cdv2.Resource, resou return nil } -func writeFileToTARArchive(filename string, contentReader io.Reader, outArchive *tar.Writer) error { - tempfile, err := ioutil.TempFile("", "") - if err != nil { - return fmt.Errorf("unable to create tempfile: %w", err) - } - defer tempfile.Close() - - if _, err := io.Copy(tempfile, contentReader); err != nil { - return fmt.Errorf("unable to write content to file: %w", err) - } - - if _, err := tempfile.Seek(0, io.SeekStart); err != nil { - return fmt.Errorf("unable to seek to beginning of file: %w", err) - } - - fstat, err := tempfile.Stat() - if err != nil { - return fmt.Errorf("unable to get file info: %w", err) - } - - header := tar.Header{ - Name: filename, - Size: fstat.Size(), - Mode: int64(fstat.Mode()), - ModTime: time.Now(), - } - - if err := outArchive.WriteHeader(&header); err != nil { - return fmt.Errorf("unable to write tar header: %w", err) - } - - if _, err := io.Copy(outArchive, tempfile); err != nil { - return fmt.Errorf("unable to write file to tar archive: %w", err) - } - - return nil -} - // ReadProcessorMessage reads the component descriptor, resource and resource blob from a processor message // (tar archive with fixed filenames for component descriptor, resource, and resource blob) which is // produced by processors. The resource blob reader can be nil. If a non-nil value is returned, it must diff --git a/pkg/transport/process/util_test.go b/pkg/transport/process/util_test.go index c668e5cb..cd3c40b3 100644 --- a/pkg/transport/process/util_test.go +++ b/pkg/transport/process/util_test.go @@ -54,4 +54,5 @@ var _ = Describe("util", func() { }) }) + }) diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index e32b2fee..e136a934 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -5,15 +5,20 @@ package utils import ( + "archive/tar" "bytes" "compress/gzip" "encoding/json" + "errors" "fmt" + "io" + "io/ioutil" "math/rand" "net/http" "os" "path/filepath" "strings" + "time" "github.com/mandelsoft/vfs/pkg/vfs" "github.com/spf13/cobra" @@ -168,3 +173,50 @@ func BytesString(bytes uint64, accuracy int) string { return fmt.Sprintf("%s %s", stringValue, unit) } + +// WriteFileToTARArchive writes a new file with name=filename and content=contentReader to archiveWriter +func WriteFileToTARArchive(filename string, contentReader io.Reader, archiveWriter *tar.Writer) error { + if filename == "" { + return errors.New("filename must not be empty") + } + + if contentReader == nil { + return errors.New("contentReader must not be nil") + } + + if archiveWriter == nil { + return errors.New("archiveWriter must not be nil") + } + + tempfile, err := ioutil.TempFile("", "") + if err != nil { + return fmt.Errorf("unable to create tempfile: %w", err) + } + defer tempfile.Close() + + fsize, err := io.Copy(tempfile, contentReader) + if err != nil { + return fmt.Errorf("unable to copy content to tempfile: %w", err) + } + + if _, err := tempfile.Seek(0, io.SeekStart); err != nil { + return fmt.Errorf("unable to seek to beginning of tempfile: %w", err) + } + + header := tar.Header{ + Name: filename, + Size: int64(fsize), + Mode: 0600, + ModTime: time.Now(), + } + + if err := archiveWriter.WriteHeader(&header); err != nil { + return fmt.Errorf("unable to write tar header: %w", err) + } + + if _, err := io.Copy(archiveWriter, tempfile); err != nil { + return fmt.Errorf("unable to write file to tar archive: %w", err) + } + + return nil +} diff --git a/pkg/utils/utils_suite_test.go b/pkg/utils/utils_suite_test.go new file mode 100644 index 00000000..03afdd59 --- /dev/null +++ b/pkg/utils/utils_suite_test.go @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package utils_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Utils Test Suite") +} diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go new file mode 100644 index 00000000..1fcc428f --- /dev/null +++ b/pkg/utils/utils_test.go @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package utils_test + +import ( + "archive/tar" + "bytes" + "io" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/gardener/component-cli/pkg/utils" +) + +var _ = Describe("utils", func() { + + Context("WriteFileToTARArchive", func() { + + It("should write file", func() { + fname := "testfile" + content := []byte("testcontent") + + archiveBuf := bytes.NewBuffer([]byte{}) + tw := tar.NewWriter(archiveBuf) + + Expect(utils.WriteFileToTARArchive(fname, bytes.NewReader(content), tw)).To(Succeed()) + Expect(tw.Close()).To(Succeed()) + + tr := tar.NewReader(archiveBuf) + fheader, err := tr.Next() + Expect(err).ToNot(HaveOccurred()) + Expect(fheader.Name).To(Equal(fname)) + + actualContentBuf := bytes.NewBuffer([]byte{}) + _, err = io.Copy(actualContentBuf, tr) + Expect(err).ToNot(HaveOccurred()) + Expect(actualContentBuf.Bytes()).To(Equal(content)) + + _, err = tr.Next() + Expect(err).To(Equal(io.EOF)) + }) + + It("should write empty file", func() { + fname := "testfile" + + archiveBuf := bytes.NewBuffer([]byte{}) + tw := tar.NewWriter(archiveBuf) + + Expect(utils.WriteFileToTARArchive(fname, bytes.NewReader([]byte{}), tw)).To(Succeed()) + Expect(tw.Close()).To(Succeed()) + + tr := tar.NewReader(archiveBuf) + fheader, err := tr.Next() + Expect(err).ToNot(HaveOccurred()) + Expect(fheader.Name).To(Equal(fname)) + + actualContentBuf := bytes.NewBuffer([]byte{}) + contentLenght, err := io.Copy(actualContentBuf, tr) + Expect(err).ToNot(HaveOccurred()) + Expect(contentLenght).To(Equal(int64(0))) + + _, err = tr.Next() + Expect(err).To(Equal(io.EOF)) + }) + + It("should return error if filename is empty", func() { + tw := tar.NewWriter(bytes.NewBuffer([]byte{})) + contentReader := bytes.NewReader([]byte{}) + Expect(utils.WriteFileToTARArchive("", contentReader, tw)).To(MatchError("filename must not be empty")) + }) + + It("should return error if contentReader is nil", func() { + tw := tar.NewWriter(bytes.NewBuffer([]byte{})) + Expect(utils.WriteFileToTARArchive("testfile", nil, tw)).To(MatchError("contentReader must not be nil")) + }) + + It("should return error if outArchive is nil", func() { + contentReader := bytes.NewReader([]byte{}) + Expect(utils.WriteFileToTARArchive("testfile", contentReader, nil)).To(MatchError("archiveWriter must not be nil")) + }) + + }) + +}) From e5fc684103e610b78ecd19b9fd98a56e8b95c81b Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 3 Nov 2021 08:58:49 +0100 Subject: [PATCH 56/94] renames labeling processor and adds test --- pkg/transport/process/pipeline_test.go | 4 +- .../process/processors/labelling_test.go | 4 - .../processors/processors_suite_test.go | 16 ++++ .../{labelling.go => resource_labeler.go} | 12 +-- .../processors/resource_labeler_test.go | 76 +++++++++++++++++++ 5 files changed, 100 insertions(+), 12 deletions(-) delete mode 100644 pkg/transport/process/processors/labelling_test.go create mode 100644 pkg/transport/process/processors/processors_suite_test.go rename pkg/transport/process/processors/{labelling.go => resource_labeler.go} (68%) create mode 100644 pkg/transport/process/processors/resource_labeler_test.go diff --git a/pkg/transport/process/pipeline_test.go b/pkg/transport/process/pipeline_test.go index c0a0f4c5..baaff70a 100644 --- a/pkg/transport/process/pipeline_test.go +++ b/pkg/transport/process/pipeline_test.go @@ -48,8 +48,8 @@ var _ = Describe("pipeline", func() { }, } - p1 := processors.NewLabellingProcessor(l1) - p2 := processors.NewLabellingProcessor(l2) + p1 := processors.NewResourceLabeler(l1) + p2 := processors.NewResourceLabeler(l2) pipeline := process.NewResourceProcessingPipeline(p1, p2) actualCD, actualRes, err := pipeline.Process(context.TODO(), cd, res) diff --git a/pkg/transport/process/processors/labelling_test.go b/pkg/transport/process/processors/labelling_test.go deleted file mode 100644 index 556ce187..00000000 --- a/pkg/transport/process/processors/labelling_test.go +++ /dev/null @@ -1,4 +0,0 @@ -// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. -// -// SPDX-License-Identifier -package processors_test diff --git a/pkg/transport/process/processors/processors_suite_test.go b/pkg/transport/process/processors/processors_suite_test.go new file mode 100644 index 00000000..b4add5bd --- /dev/null +++ b/pkg/transport/process/processors/processors_suite_test.go @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package processors_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Processors Test Suite") +} diff --git a/pkg/transport/process/processors/labelling.go b/pkg/transport/process/processors/resource_labeler.go similarity index 68% rename from pkg/transport/process/processors/labelling.go rename to pkg/transport/process/processors/resource_labeler.go index 7cc17e39..fba440cd 100644 --- a/pkg/transport/process/processors/labelling.go +++ b/pkg/transport/process/processors/resource_labeler.go @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // -// SPDX-License-Identifier +// SPDX-License-Identifier: Apache-2.0 package processors import ( @@ -13,19 +13,19 @@ import ( "github.com/gardener/component-cli/pkg/transport/process" ) -type labellingProcessor struct { +type resourceLabeler struct { labels cdv2.Labels } -// NewLabellingProcessor returns a processor that appends one or more labels to a resource -func NewLabellingProcessor(labels ...cdv2.Label) process.ResourceStreamProcessor { - obj := labellingProcessor{ +// NewResourceLabeler returns a processor that appends one or more labels to a resource +func NewResourceLabeler(labels ...cdv2.Label) process.ResourceStreamProcessor { + obj := resourceLabeler{ labels: labels, } return &obj } -func (p *labellingProcessor) Process(ctx context.Context, r io.Reader, w io.Writer) error { +func (p *resourceLabeler) Process(ctx context.Context, r io.Reader, w io.Writer) error { cd, res, resBlobReader, err := process.ReadProcessorMessage(r) if err != nil { return fmt.Errorf("unable to read processor message: %w", err) diff --git a/pkg/transport/process/processors/resource_labeler_test.go b/pkg/transport/process/processors/resource_labeler_test.go new file mode 100644 index 00000000..d1ea99eb --- /dev/null +++ b/pkg/transport/process/processors/resource_labeler_test.go @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package processors_test + +import ( + "bytes" + "context" + "encoding/json" + "io" + + "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/processors" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("resourceLabeler", func() { + + Context("Process", func() { + + It("should correctly add labels", func() { + res := cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "my-res", + Version: "v0.1.0", + Type: "ociImage", + }, + } + + l1 := cdv2.Label{ + Name: "first-label", + Value: json.RawMessage(`"true"`), + } + l2 := cdv2.Label{ + Name: "second-label", + Value: json.RawMessage(`"true"`), + } + + resBytes := []byte("resource-blob") + + expectedRes := res + expectedRes.Labels = append(expectedRes.Labels, l1) + expectedRes.Labels = append(expectedRes.Labels, l2) + + cd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + Resources: []cdv2.Resource{ + res, + }, + }, + } + + inBuf := bytes.NewBuffer([]byte{}) + Expect(process.WriteProcessorMessage(cd, res, bytes.NewReader(resBytes), inBuf)).To(Succeed()) + + outbuf := bytes.NewBuffer([]byte{}) + + p1 := processors.NewResourceLabeler(l1, l2) + Expect(p1.Process(context.TODO(), inBuf, outbuf)).To(Succeed()) + + actualCD, actualRes, actualResBlobReader, err := process.ReadProcessorMessage(outbuf) + Expect(err).ToNot(HaveOccurred()) + + Expect(*actualCD).To(Equal(cd)) + Expect(actualRes).To(Equal(expectedRes)) + + actualResBlobBuf := bytes.NewBuffer([]byte{}) + _, err =io.Copy(actualResBlobBuf, actualResBlobReader) + Expect(err).ToNot(HaveOccurred()) + Expect(actualResBlobBuf.Bytes()).To(Equal(resBytes)) + }) + + }) +}) From e1d99f4de978866230fe55685fa8bba45f0a8253 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 3 Nov 2021 09:16:15 +0100 Subject: [PATCH 57/94] refactoring --- .../extensions/extensions_suite_test.go | 5 +- pkg/transport/process/pipeline.go | 6 +- .../process/processors/example/main.go | 8 +- .../process/processors/resource_labeler.go | 5 +- .../processors/resource_labeler_test.go | 11 +-- .../process/processors/sleep/main.go | 4 +- pkg/transport/process/utils/uds_server.go | 74 +++++++++++++++++++ pkg/transport/process/{ => utils}/util.go | 68 +---------------- .../process/{ => utils}/util_test.go | 8 +- 9 files changed, 101 insertions(+), 88 deletions(-) create mode 100644 pkg/transport/process/utils/uds_server.go rename pkg/transport/process/{ => utils}/util.go (77%) rename pkg/transport/process/{ => utils}/util_test.go (81%) diff --git a/pkg/transport/process/extensions/extensions_suite_test.go b/pkg/transport/process/extensions/extensions_suite_test.go index 1b38dfff..28df2cd5 100644 --- a/pkg/transport/process/extensions/extensions_suite_test.go +++ b/pkg/transport/process/extensions/extensions_suite_test.go @@ -20,6 +20,7 @@ import ( "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/transport/process/extensions" + "github.com/gardener/component-cli/pkg/transport/process/utils" ) const ( @@ -140,14 +141,14 @@ func runExampleResourceTest(processor process.ResourceStreamProcessor) { } inputBuf := bytes.NewBuffer([]byte{}) - err := process.WriteProcessorMessage(cd, res, strings.NewReader(resourceData), inputBuf) + err := utils.WriteProcessorMessage(cd, res, strings.NewReader(resourceData), inputBuf) Expect(err).ToNot(HaveOccurred()) outputBuf := bytes.NewBuffer([]byte{}) err = processor.Process(context.TODO(), inputBuf, outputBuf) Expect(err).ToNot(HaveOccurred()) - processedCD, processedRes, processedBlobReader, err := process.ReadProcessorMessage(outputBuf) + processedCD, processedRes, processedBlobReader, err := utils.ReadProcessorMessage(outputBuf) Expect(err).ToNot(HaveOccurred()) Expect(*processedCD).To(Equal(cd)) diff --git a/pkg/transport/process/pipeline.go b/pkg/transport/process/pipeline.go index 40f9ef35..fe9da4db 100644 --- a/pkg/transport/process/pipeline.go +++ b/pkg/transport/process/pipeline.go @@ -13,6 +13,8 @@ import ( "io/ioutil" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + + "github.com/gardener/component-cli/pkg/transport/process/utils" ) const processorTimeout = 30 * time.Second @@ -27,7 +29,7 @@ func (p *resourceProcessingPipelineImpl) Process(ctx context.Context, cd cdv2.Co return nil, cdv2.Resource{}, fmt.Errorf("unable to create temporary infile: %w", err) } - if err := WriteProcessorMessage(cd, res, nil, infile); err != nil { + if err := utils.WriteProcessorMessage(cd, res, nil, infile); err != nil { return nil, cdv2.Resource{}, fmt.Errorf("unable to write: %w", err) } @@ -45,7 +47,7 @@ func (p *resourceProcessingPipelineImpl) Process(ctx context.Context, cd cdv2.Co return nil, cdv2.Resource{}, err } - processedCD, processedRes, blobreader, err := ReadProcessorMessage(infile) + processedCD, processedRes, blobreader, err := utils.ReadProcessorMessage(infile) if err != nil { return nil, cdv2.Resource{}, fmt.Errorf("unable to read output data: %w", err) } diff --git a/pkg/transport/process/processors/example/main.go b/pkg/transport/process/processors/example/main.go index cc581f3a..2bb26490 100644 --- a/pkg/transport/process/processors/example/main.go +++ b/pkg/transport/process/processors/example/main.go @@ -17,8 +17,8 @@ import ( cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" - "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/transport/process/extensions" + "github.com/gardener/component-cli/pkg/transport/process/utils" ) const processorName = "example-processor" @@ -42,7 +42,7 @@ func main() { } } - srv, err := process.NewUDSServer(addr, h) + srv, err := utils.NewUDSServer(addr, h) if err != nil { log.Fatal(err) } @@ -73,7 +73,7 @@ func processorRoutine(inputStream io.Reader, outputStream io.WriteCloser) error return err } - cd, res, resourceBlobReader, err := process.ReadProcessorMessage(tmpfile) + cd, res, resourceBlobReader, err := utils.ReadProcessorMessage(tmpfile) if err != nil { return err } @@ -93,7 +93,7 @@ func processorRoutine(inputStream io.Reader, outputStream io.WriteCloser) error } res.Labels = append(res.Labels, l) - if err := process.WriteProcessorMessage(*cd, res, strings.NewReader(outputData), outputStream); err != nil { + if err := utils.WriteProcessorMessage(*cd, res, strings.NewReader(outputData), outputStream); err != nil { return err } diff --git a/pkg/transport/process/processors/resource_labeler.go b/pkg/transport/process/processors/resource_labeler.go index fba440cd..22c3d480 100644 --- a/pkg/transport/process/processors/resource_labeler.go +++ b/pkg/transport/process/processors/resource_labeler.go @@ -11,6 +11,7 @@ import ( cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/utils" ) type resourceLabeler struct { @@ -26,7 +27,7 @@ func NewResourceLabeler(labels ...cdv2.Label) process.ResourceStreamProcessor { } func (p *resourceLabeler) Process(ctx context.Context, r io.Reader, w io.Writer) error { - cd, res, resBlobReader, err := process.ReadProcessorMessage(r) + cd, res, resBlobReader, err := utils.ReadProcessorMessage(r) if err != nil { return fmt.Errorf("unable to read processor message: %w", err) } @@ -36,7 +37,7 @@ func (p *resourceLabeler) Process(ctx context.Context, r io.Reader, w io.Writer) res.Labels = append(res.Labels, p.labels...) - if err := process.WriteProcessorMessage(*cd, res, resBlobReader, w); err != nil { + if err := utils.WriteProcessorMessage(*cd, res, resBlobReader, w); err != nil { return fmt.Errorf("unable to write processor message: %w", err) } diff --git a/pkg/transport/process/processors/resource_labeler_test.go b/pkg/transport/process/processors/resource_labeler_test.go index d1ea99eb..263d7a39 100644 --- a/pkg/transport/process/processors/resource_labeler_test.go +++ b/pkg/transport/process/processors/resource_labeler_test.go @@ -9,11 +9,12 @@ import ( "encoding/json" "io" - "github.com/gardener/component-cli/pkg/transport/process" - "github.com/gardener/component-cli/pkg/transport/process/processors" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + + "github.com/gardener/component-cli/pkg/transport/process/processors" + "github.com/gardener/component-cli/pkg/transport/process/utils" ) var _ = Describe("resourceLabeler", func() { @@ -53,21 +54,21 @@ var _ = Describe("resourceLabeler", func() { } inBuf := bytes.NewBuffer([]byte{}) - Expect(process.WriteProcessorMessage(cd, res, bytes.NewReader(resBytes), inBuf)).To(Succeed()) + Expect(utils.WriteProcessorMessage(cd, res, bytes.NewReader(resBytes), inBuf)).To(Succeed()) outbuf := bytes.NewBuffer([]byte{}) p1 := processors.NewResourceLabeler(l1, l2) Expect(p1.Process(context.TODO(), inBuf, outbuf)).To(Succeed()) - actualCD, actualRes, actualResBlobReader, err := process.ReadProcessorMessage(outbuf) + actualCD, actualRes, actualResBlobReader, err := utils.ReadProcessorMessage(outbuf) Expect(err).ToNot(HaveOccurred()) Expect(*actualCD).To(Equal(cd)) Expect(actualRes).To(Equal(expectedRes)) actualResBlobBuf := bytes.NewBuffer([]byte{}) - _, err =io.Copy(actualResBlobBuf, actualResBlobReader) + _, err = io.Copy(actualResBlobBuf, actualResBlobReader) Expect(err).ToNot(HaveOccurred()) Expect(actualResBlobBuf.Bytes()).To(Equal(resBytes)) }) diff --git a/pkg/transport/process/processors/sleep/main.go b/pkg/transport/process/processors/sleep/main.go index ad8b4e28..7be914c7 100644 --- a/pkg/transport/process/processors/sleep/main.go +++ b/pkg/transport/process/processors/sleep/main.go @@ -11,8 +11,8 @@ import ( "syscall" "time" - "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/transport/process/extensions" + "github.com/gardener/component-cli/pkg/transport/process/utils" ) const sleepTimeEnv = "SLEEP_TIME" @@ -36,7 +36,7 @@ func main() { log.Fatal("finished sleeping -> exit with error") } - srv, err := process.NewUDSServer(addr, h) + srv, err := utils.NewUDSServer(addr, h) if err != nil { log.Fatal(err) } diff --git a/pkg/transport/process/utils/uds_server.go b/pkg/transport/process/utils/uds_server.go new file mode 100644 index 00000000..4f88c865 --- /dev/null +++ b/pkg/transport/process/utils/uds_server.go @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package utils + +import ( + "io" + "log" + "net" + "sync" +) + +// HandlerFunc defines the interface of a function that should be served by a UDS server +type HandlerFunc func(io.Reader, io.WriteCloser) + +// UDSServer implements a Unix Domain Socket server +type UDSServer struct { + listener net.Listener + quit chan interface{} + wg sync.WaitGroup + handler HandlerFunc +} + +// NewUDSServer returns a new UDS server. +// The parameters define the server address and the handler func it serves +func NewUDSServer(addr string, handler HandlerFunc) (*UDSServer, error) { + l, err := net.Listen("unix", addr) + if err != nil { + return nil, err + } + s := &UDSServer{ + quit: make(chan interface{}), + listener: l, + handler: handler, + } + return s, nil +} + +// Start starts the server goroutine +func (s *UDSServer) Start() { + s.wg.Add(1) + go s.serve() +} + +func (s *UDSServer) serve() { + defer s.wg.Done() + + for { + conn, err := s.listener.Accept() + if err != nil { + select { + case <-s.quit: + return + default: + log.Println("accept error", err) + } + } else { + s.wg.Add(1) + go func() { + defer s.wg.Done() + s.handler(conn, conn) + }() + } + } +} + +// Stop stops the server goroutine +func (s *UDSServer) Stop() { + close(s.quit) + if err := s.listener.Close(); err != nil { + println(err) + } + s.wg.Wait() +} diff --git a/pkg/transport/process/util.go b/pkg/transport/process/utils/util.go similarity index 77% rename from pkg/transport/process/util.go rename to pkg/transport/process/utils/util.go index a991c89a..2f52cb7b 100644 --- a/pkg/transport/process/util.go +++ b/pkg/transport/process/utils/util.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // // SPDX-License-Identifier: Apache-2.0 -package process +package utils import ( "archive/tar" @@ -9,10 +9,7 @@ import ( "fmt" "io" "io/ioutil" - "log" - "net" "os" - "sync" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" "sigs.k8s.io/yaml" @@ -142,66 +139,3 @@ func readComponentDescriptor(r *tar.Reader) (*cdv2.ComponentDescriptor, error) { return &cd, nil } - -// HandlerFunc defines the interface of a function that should be served by a UDS server -type HandlerFunc func(io.Reader, io.WriteCloser) - -// UDSServer implements a Unix Domain Socket server -type UDSServer struct { - listener net.Listener - quit chan interface{} - wg sync.WaitGroup - handler HandlerFunc -} - -// NewUDSServer returns a new UDS server. -// The parameters define the server address and the handler func it serves -func NewUDSServer(addr string, handler HandlerFunc) (*UDSServer, error) { - l, err := net.Listen("unix", addr) - if err != nil { - return nil, err - } - s := &UDSServer{ - quit: make(chan interface{}), - listener: l, - handler: handler, - } - return s, nil -} - -// Start starts the server goroutine -func (s *UDSServer) Start() { - s.wg.Add(1) - go s.serve() -} - -func (s *UDSServer) serve() { - defer s.wg.Done() - - for { - conn, err := s.listener.Accept() - if err != nil { - select { - case <-s.quit: - return - default: - log.Println("accept error", err) - } - } else { - s.wg.Add(1) - go func() { - defer s.wg.Done() - s.handler(conn, conn) - }() - } - } -} - -// Stop stops the server goroutine -func (s *UDSServer) Stop() { - close(s.quit) - if err := s.listener.Close(); err != nil { - println(err) - } - s.wg.Wait() -} diff --git a/pkg/transport/process/util_test.go b/pkg/transport/process/utils/util_test.go similarity index 81% rename from pkg/transport/process/util_test.go rename to pkg/transport/process/utils/util_test.go index cd3c40b3..a99292e9 100644 --- a/pkg/transport/process/util_test.go +++ b/pkg/transport/process/utils/util_test.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // // SPDX-License-Identifier: Apache-2.0 -package process_test +package utils_test import ( "bytes" @@ -12,7 +12,7 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/utils" ) var _ = Describe("util", func() { @@ -38,10 +38,10 @@ var _ = Describe("util", func() { } processMsgBuf := bytes.NewBuffer([]byte{}) - err := process.WriteProcessorMessage(cd, res, strings.NewReader(resourceData), processMsgBuf) + err := utils.WriteProcessorMessage(cd, res, strings.NewReader(resourceData), processMsgBuf) Expect(err).ToNot(HaveOccurred()) - actualCD, actualRes, resourceBlobReader, err := process.ReadProcessorMessage(processMsgBuf) + actualCD, actualRes, resourceBlobReader, err := utils.ReadProcessorMessage(processMsgBuf) Expect(err).ToNot(HaveOccurred()) Expect(*actualCD).To(Equal(cd)) From 779cd8ce959ca0ff9e91bb4ca6d4666ff726c0cb Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 3 Nov 2021 09:48:53 +0100 Subject: [PATCH 58/94] refactoring --- .../utils/{util.go => processor_message.go} | 0 .../{util_test.go => processor_message_test.go} | 0 pkg/transport/process/utils/utils_suite_test.go | 16 ++++++++++++++++ 3 files changed, 16 insertions(+) rename pkg/transport/process/utils/{util.go => processor_message.go} (100%) rename pkg/transport/process/utils/{util_test.go => processor_message_test.go} (100%) create mode 100644 pkg/transport/process/utils/utils_suite_test.go diff --git a/pkg/transport/process/utils/util.go b/pkg/transport/process/utils/processor_message.go similarity index 100% rename from pkg/transport/process/utils/util.go rename to pkg/transport/process/utils/processor_message.go diff --git a/pkg/transport/process/utils/util_test.go b/pkg/transport/process/utils/processor_message_test.go similarity index 100% rename from pkg/transport/process/utils/util_test.go rename to pkg/transport/process/utils/processor_message_test.go diff --git a/pkg/transport/process/utils/utils_suite_test.go b/pkg/transport/process/utils/utils_suite_test.go new file mode 100644 index 00000000..03afdd59 --- /dev/null +++ b/pkg/transport/process/utils/utils_suite_test.go @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package utils_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Utils Test Suite") +} From 526e68fc8636d1a09484c494e4e978801ae6814b Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 3 Nov 2021 10:04:17 +0100 Subject: [PATCH 59/94] use map[string]string for passing env variables --- .../extensions/extensions_suite_test.go | 28 +++++++++++++------ .../process/extensions/stdio_executable.go | 9 ++++-- .../process/extensions/uds_executable.go | 18 ++++++------ 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/pkg/transport/process/extensions/extensions_suite_test.go b/pkg/transport/process/extensions/extensions_suite_test.go index 28df2cd5..9ea2ce7b 100644 --- a/pkg/transport/process/extensions/extensions_suite_test.go +++ b/pkg/transport/process/extensions/extensions_suite_test.go @@ -46,9 +46,15 @@ var _ = BeforeSuite(func() { var _ = Describe("transport extensions", func() { Context("stdio executable", func() { + It("should create processor successfully if env is nil", func() { + args := []string{} + _, err := extensions.NewStdIOExecutable(exampleProcessorBinaryPath, args, nil) + Expect(err).ToNot(HaveOccurred()) + }) + It("should modify the processed resource correctly", func() { args := []string{} - env := []string{} + env := map[string]string{} processor, err := extensions.NewStdIOExecutable(exampleProcessorBinaryPath, args, env) Expect(err).ToNot(HaveOccurred()) @@ -57,8 +63,8 @@ var _ = Describe("transport extensions", func() { It("should exit with error when timeout is reached", func() { args := []string{} - env := []string{ - fmt.Sprintf("%s=%s", sleepTimeEnv, sleepTime.String()), + env := map[string]string{ + sleepTimeEnv: sleepTime.String(), } processor, err := extensions.NewStdIOExecutable(sleepProcessorBinaryPath, args, env) Expect(err).ToNot(HaveOccurred()) @@ -68,9 +74,15 @@ var _ = Describe("transport extensions", func() { }) Context("uds executable", func() { + It("should create processor successfully if env is nil", func() { + args := []string{} + _, err := extensions.NewUDSExecutable(exampleProcessorBinaryPath, args, nil) + Expect(err).ToNot(HaveOccurred()) + }) + It("should modify the processed resource correctly", func() { args := []string{} - env := []string{} + env := map[string]string{} processor, err := extensions.NewUDSExecutable(exampleProcessorBinaryPath, args, env) Expect(err).ToNot(HaveOccurred()) @@ -79,8 +91,8 @@ var _ = Describe("transport extensions", func() { It("should raise an error when trying to set the server address env variable manually", func() { args := []string{} - env := []string{ - extensions.ServerAddressEnv + "=/tmp/my-processor.sock", + env := map[string]string{ + extensions.ServerAddressEnv: "/tmp/my-processor.sock", } _, err := extensions.NewUDSExecutable(exampleProcessorBinaryPath, args, env) Expect(err).To(MatchError(fmt.Sprintf("the env variable %s is not allowed to be set manually", extensions.ServerAddressEnv))) @@ -88,8 +100,8 @@ var _ = Describe("transport extensions", func() { It("should exit with error when timeout is reached", func() { args := []string{} - env := []string{ - fmt.Sprintf("%s=%s", sleepTimeEnv, sleepTime.String()), + env := map[string]string{ + sleepTimeEnv: sleepTime.String(), } processor, err := extensions.NewUDSExecutable(sleepProcessorBinaryPath, args, env) Expect(err).ToNot(HaveOccurred()) diff --git a/pkg/transport/process/extensions/stdio_executable.go b/pkg/transport/process/extensions/stdio_executable.go index 5ae2b17f..a85a8283 100644 --- a/pkg/transport/process/extensions/stdio_executable.go +++ b/pkg/transport/process/extensions/stdio_executable.go @@ -21,11 +21,16 @@ type stdIOExecutable struct { // NewStdIOExecutable returns a resource processor extension which runs an executable. // in the background. It communicates with this processor via stdin/stdout pipes. -func NewStdIOExecutable(bin string, args []string, env []string) (process.ResourceStreamProcessor, error) { +func NewStdIOExecutable(bin string, args []string, env map[string]string) (process.ResourceStreamProcessor, error) { + parsedEnv := []string{} + for k, v := range env { + parsedEnv = append(parsedEnv, fmt.Sprintf("%s=%s", k, v)) + } + e := stdIOExecutable{ bin: bin, args: args, - env: env, + env: parsedEnv, } return &e, nil diff --git a/pkg/transport/process/extensions/uds_executable.go b/pkg/transport/process/extensions/uds_executable.go index be078160..6c14148d 100644 --- a/pkg/transport/process/extensions/uds_executable.go +++ b/pkg/transport/process/extensions/uds_executable.go @@ -10,7 +10,6 @@ import ( "net" "os" "os/exec" - "strings" "syscall" "time" @@ -31,11 +30,14 @@ type udsExecutable struct { // NewUDSExecutable runs a resource processor extension executable in the background. // It communicates with this processor via Unix Domain Sockets. -func NewUDSExecutable(bin string, args []string, env []string) (process.ResourceStreamProcessor, error) { - for _, e := range env { - if strings.HasPrefix(e, ServerAddressEnv+"=") { - return nil, fmt.Errorf("the env variable %s is not allowed to be set manually", ServerAddressEnv) - } +func NewUDSExecutable(bin string, args []string, env map[string]string) (process.ResourceStreamProcessor, error) { + if _, ok := env[ServerAddressEnv]; ok { + return nil, fmt.Errorf("the env variable %s is not allowed to be set manually", ServerAddressEnv) + } + + parsedEnv := []string{} + for k, v := range env { + parsedEnv = append(parsedEnv, fmt.Sprintf("%s=%s", k, v)) } wd, err := os.Getwd() @@ -43,12 +45,12 @@ func NewUDSExecutable(bin string, args []string, env []string) (process.Resource return nil, err } addr := fmt.Sprintf("%s/%s.sock", wd, utils.RandomString(8)) - env = append(env, fmt.Sprintf("%s=%s", ServerAddressEnv, addr)) + parsedEnv = append(parsedEnv, fmt.Sprintf("%s=%s", ServerAddressEnv, addr)) e := udsExecutable{ bin: bin, args: args, - env: env, + env: parsedEnv, addr: addr, } From 9fbcdc7d2f4278a691a10ca0b4ff39d0e1c655e2 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 3 Nov 2021 10:40:19 +0100 Subject: [PATCH 60/94] refactoring + changes doc --- pkg/transport/process/pipeline.go | 4 ++-- pkg/transport/process/types.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/transport/process/pipeline.go b/pkg/transport/process/pipeline.go index fe9da4db..77861dac 100644 --- a/pkg/transport/process/pipeline.go +++ b/pkg/transport/process/pipeline.go @@ -34,7 +34,7 @@ func (p *resourceProcessingPipelineImpl) Process(ctx context.Context, cd cdv2.Co } for _, proc := range p.processors { - outfile, err := p.process(ctx, infile, proc) + outfile, err := p.runProcessor(ctx, infile, proc) if err != nil { return nil, cdv2.Resource{}, err } @@ -58,7 +58,7 @@ func (p *resourceProcessingPipelineImpl) Process(ctx context.Context, cd cdv2.Co return processedCD, processedRes, nil } -func (p *resourceProcessingPipelineImpl) process(ctx context.Context, infile *os.File, proc ResourceStreamProcessor) (*os.File, error) { +func (p *resourceProcessingPipelineImpl) runProcessor(ctx context.Context, infile *os.File, proc ResourceStreamProcessor) (*os.File, error) { defer infile.Close() if _, err := infile.Seek(0, io.SeekStart); err != nil { diff --git a/pkg/transport/process/types.go b/pkg/transport/process/types.go index 9889d07e..d8b69eb3 100644 --- a/pkg/transport/process/types.go +++ b/pkg/transport/process/types.go @@ -24,7 +24,7 @@ type ResourceProcessingPipeline interface { // A processor can upload, modify, or download a resource. type ResourceStreamProcessor interface { // Process executes the processor for a resource. Input and Output streams must be - // compliant to a specific format ("processor message"). See also ./util.go for helper - // functions to read/write processor messages. + // compliant to a specific format ("processor message"). See also ./utils/processor_message.go + // which describes the format and provides helper functions to read/write processor messages. Process(context.Context, io.Reader, io.Writer) error } From 5325cbc9f4eee61ba148a62f952d8204fb153a07 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 3 Nov 2021 12:55:25 +0100 Subject: [PATCH 61/94] moves oci artifact serialization to process/utils package --- .../process/downloaders/oci_artifact.go | 2 +- .../process/downloaders/oci_artifact_test.go | 5 ++--- .../process/processors/oci_image_filter.go | 4 ++-- .../process/uploaders/oci_artifact.go | 4 ++-- .../{ => utils}/oci_artifact_serialization.go | 4 ++-- .../oci_artifact_serialization_test.go | 20 +++++++++---------- 6 files changed, 19 insertions(+), 20 deletions(-) rename pkg/transport/process/{ => utils}/oci_artifact_serialization.go (99%) rename pkg/transport/process/{ => utils}/oci_artifact_serialization_test.go (90%) diff --git a/pkg/transport/process/downloaders/oci_artifact.go b/pkg/transport/process/downloaders/oci_artifact.go index 4369feaf..a3db7a76 100644 --- a/pkg/transport/process/downloaders/oci_artifact.go +++ b/pkg/transport/process/downloaders/oci_artifact.go @@ -73,7 +73,7 @@ func (d *ociArtifactDownloader) Process(ctx context.Context, r io.Reader, w io.W } } - blobReader, err := process.SerializeOCIArtifact(*ociArtifact, d.cache) + blobReader, err := utils.SerializeOCIArtifact(*ociArtifact, d.cache) if err != nil { return fmt.Errorf("unable to serialize oci artifact: %w", err) } diff --git a/pkg/transport/process/downloaders/oci_artifact_test.go b/pkg/transport/process/downloaders/oci_artifact_test.go index 2c9b5338..6579edd6 100644 --- a/pkg/transport/process/downloaders/oci_artifact_test.go +++ b/pkg/transport/process/downloaders/oci_artifact_test.go @@ -11,7 +11,6 @@ import ( . "github.com/onsi/gomega" "github.com/gardener/component-cli/pkg/testutils" - "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/transport/process/downloaders" "github.com/gardener/component-cli/pkg/transport/process/utils" ) @@ -41,7 +40,7 @@ var _ = Describe("ociArtifact", func() { Expect(*actualCd).To(Equal(testComponent)) Expect(actualRes).To(Equal(ociImageRes)) - actualOciArtifact, err := process.DeserializeOCIArtifact(resBlobReader, ociCache) + actualOciArtifact, err := utils.DeserializeOCIArtifact(resBlobReader, ociCache) Expect(err).ToNot(HaveOccurred()) Expect(*actualOciArtifact.GetManifest()).To(Equal(expectedImageManifest)) testutils.CompareManifestToTestManifest(context.TODO(), ociClient, imageRef, expectedImageManifest.Data) @@ -68,7 +67,7 @@ var _ = Describe("ociArtifact", func() { Expect(*actualCd).To(Equal(testComponent)) Expect(actualRes).To(Equal(ociImageIndexRes)) - actualOciArtifact, err := process.DeserializeOCIArtifact(resBlobReader, ociCache) + actualOciArtifact, err := utils.DeserializeOCIArtifact(resBlobReader, ociCache) Expect(err).ToNot(HaveOccurred()) testutils.CompareImageIndices(actualOciArtifact.GetIndex(), &expectedImageIndex) }) diff --git a/pkg/transport/process/processors/oci_image_filter.go b/pkg/transport/process/processors/oci_image_filter.go index c7a10f67..e7a8e51b 100644 --- a/pkg/transport/process/processors/oci_image_filter.go +++ b/pkg/transport/process/processors/oci_image_filter.go @@ -37,7 +37,7 @@ func (f *ociImageFilter) Process(ctx context.Context, r io.Reader, w io.Writer) } defer blobreader.Close() - ociArtifact, err := process.DeserializeOCIArtifact(blobreader, f.cache) + ociArtifact, err := processutils.DeserializeOCIArtifact(blobreader, f.cache) if err != nil { return fmt.Errorf("unable to deserialize oci artifact: %w", err) } @@ -78,7 +78,7 @@ func (f *ociImageFilter) Process(ctx context.Context, r io.Reader, w io.Writer) } } - blobReader, err := process.SerializeOCIArtifact(*ociArtifact, f.cache) + blobReader, err := processutils.SerializeOCIArtifact(*ociArtifact, f.cache) if err != nil { return fmt.Errorf("unable to serialice oci artifact: %w", err) } diff --git a/pkg/transport/process/uploaders/oci_artifact.go b/pkg/transport/process/uploaders/oci_artifact.go index 2348676f..b7e2be7b 100644 --- a/pkg/transport/process/uploaders/oci_artifact.go +++ b/pkg/transport/process/uploaders/oci_artifact.go @@ -54,7 +54,7 @@ func (u *ociArtifactUploader) Process(ctx context.Context, r io.Reader, w io.Wri } defer resBlobReader.Close() - ociArtifact, err := process.DeserializeOCIArtifact(resBlobReader, u.cache) + ociArtifact, err := processutils.DeserializeOCIArtifact(resBlobReader, u.cache) if err != nil { return fmt.Errorf("unable to deserialize oci artifact: %w", err) } @@ -81,7 +81,7 @@ func (u *ociArtifactUploader) Process(ctx context.Context, r io.Reader, w io.Wri return fmt.Errorf("unable to push oci artifact: %w", err) } - blobReader, err := process.SerializeOCIArtifact(*ociArtifact, u.cache) + blobReader, err := processutils.SerializeOCIArtifact(*ociArtifact, u.cache) if err != nil { return fmt.Errorf("unable to serialize oci artifact: %w", err) } diff --git a/pkg/transport/process/oci_artifact_serialization.go b/pkg/transport/process/utils/oci_artifact_serialization.go similarity index 99% rename from pkg/transport/process/oci_artifact_serialization.go rename to pkg/transport/process/utils/oci_artifact_serialization.go index b100028e..40f8b9bd 100644 --- a/pkg/transport/process/oci_artifact_serialization.go +++ b/pkg/transport/process/utils/oci_artifact_serialization.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // -// SPDX-License-Identifier -package process +// SPDX-License-Identifier: Apache-2.0 +package utils import ( "archive/tar" diff --git a/pkg/transport/process/oci_artifact_serialization_test.go b/pkg/transport/process/utils/oci_artifact_serialization_test.go similarity index 90% rename from pkg/transport/process/oci_artifact_serialization_test.go rename to pkg/transport/process/utils/oci_artifact_serialization_test.go index 5f5de0ee..d6e6ddff 100644 --- a/pkg/transport/process/oci_artifact_serialization_test.go +++ b/pkg/transport/process/utils/oci_artifact_serialization_test.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // // SPDX-License-Identifier: Apache-2.0 -package process_test +package utils_test import ( "archive/tar" @@ -17,7 +17,7 @@ import ( "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/ociclient/oci" "github.com/gardener/component-cli/pkg/testutils" - "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/utils" ) var _ = Describe("oci artifact serialization", func() { @@ -41,11 +41,11 @@ var _ = Describe("oci artifact serialization", func() { Expect(serializeCache.Add(m.Config, io.NopCloser(bytes.NewReader(configData)))).To(Succeed()) Expect(serializeCache.Add(m.Layers[0], io.NopCloser(bytes.NewReader(layerData)))).To(Succeed()) - serializedReader, err := process.SerializeOCIArtifact(*expectedOciArtifact, serializeCache) + serializedReader, err := utils.SerializeOCIArtifact(*expectedOciArtifact, serializeCache) Expect(err).ToNot(HaveOccurred()) deserializeCache := cache.NewInMemoryCache() - actualOciArtifact, err := process.DeserializeOCIArtifact(serializedReader, deserializeCache) + actualOciArtifact, err := utils.DeserializeOCIArtifact(serializedReader, deserializeCache) Expect(err).ToNot(HaveOccurred()) Expect(actualOciArtifact.GetManifest().Data).To(Equal(expectedOciArtifact.GetManifest().Data)) @@ -106,11 +106,11 @@ var _ = Describe("oci artifact serialization", func() { Expect(serializeCache.Add(m2.Config, io.NopCloser(bytes.NewReader(configData2)))).To(Succeed()) Expect(serializeCache.Add(m2.Layers[0], io.NopCloser(bytes.NewReader(layerData2)))).To(Succeed()) - serializedReader, err := process.SerializeOCIArtifact(*expectedOciArtifact, serializeCache) + serializedReader, err := utils.SerializeOCIArtifact(*expectedOciArtifact, serializeCache) Expect(err).ToNot(HaveOccurred()) deserializeCache := cache.NewInMemoryCache() - actualOciArtifact, err := process.DeserializeOCIArtifact(serializedReader, deserializeCache) + actualOciArtifact, err := utils.DeserializeOCIArtifact(serializedReader, deserializeCache) Expect(err).ToNot(HaveOccurred()) // check image index and manifests @@ -154,7 +154,7 @@ var _ = Describe("oci artifact serialization", func() { Context("serialize oci artifact", func() { It("should raise error if cache is nil", func() { - _, err := process.SerializeOCIArtifact(oci.Artifact{}, nil) + _, err := utils.SerializeOCIArtifact(oci.Artifact{}, nil) Expect(err).To(MatchError("cache must not be nil")) }) @@ -163,13 +163,13 @@ var _ = Describe("oci artifact serialization", func() { Context("deserialize oci artifact", func() { It("should raise error if reader is nil", func() { - _, err := process.DeserializeOCIArtifact(nil, cache.NewInMemoryCache()) + _, err := utils.DeserializeOCIArtifact(nil, cache.NewInMemoryCache()) Expect(err).To(MatchError("reader must not be nil")) }) It("should raise error if cache is nil", func() { buf := bytes.NewBuffer([]byte{}) - _, err := process.DeserializeOCIArtifact(buf, nil) + _, err := utils.DeserializeOCIArtifact(buf, nil) Expect(err).To(MatchError("cache must not be nil")) }) @@ -193,7 +193,7 @@ var _ = Describe("oci artifact serialization", func() { Expect(tw.Close()).To(Succeed()) - _, err = process.DeserializeOCIArtifact(buf, cache.NewInMemoryCache()) + _, err = utils.DeserializeOCIArtifact(buf, cache.NewInMemoryCache()) Expect(err).To(MatchError("unknown file " + fileName)) }) From 2b2710ca9d3851d40e5cf258b5a8fbdcfe161df9 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 3 Nov 2021 13:47:05 +0100 Subject: [PATCH 62/94] updates license headers --- pkg/transport/process/downloaders/local_oci_blob.go | 2 +- pkg/transport/process/downloaders/oci_artifact.go | 2 +- pkg/transport/process/uploaders/local_oci_blob.go | 2 +- pkg/transport/process/uploaders/oci_artifact.go | 2 +- pkg/transport/process/uploaders/util.go | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/transport/process/downloaders/local_oci_blob.go b/pkg/transport/process/downloaders/local_oci_blob.go index ea5f7b33..421361c9 100644 --- a/pkg/transport/process/downloaders/local_oci_blob.go +++ b/pkg/transport/process/downloaders/local_oci_blob.go @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // -// SPDX-License-Identifier +// SPDX-License-Identifier: Apache-2.0 package downloaders import ( diff --git a/pkg/transport/process/downloaders/oci_artifact.go b/pkg/transport/process/downloaders/oci_artifact.go index a3db7a76..202e8e8e 100644 --- a/pkg/transport/process/downloaders/oci_artifact.go +++ b/pkg/transport/process/downloaders/oci_artifact.go @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // -// SPDX-License-Identifier +// SPDX-License-Identifier: Apache-2.0 package downloaders import ( diff --git a/pkg/transport/process/uploaders/local_oci_blob.go b/pkg/transport/process/uploaders/local_oci_blob.go index 54ea2f45..9aa771e3 100644 --- a/pkg/transport/process/uploaders/local_oci_blob.go +++ b/pkg/transport/process/uploaders/local_oci_blob.go @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // -// SPDX-License-Identifier +// SPDX-License-Identifier: Apache-2.0 package uploaders import ( diff --git a/pkg/transport/process/uploaders/oci_artifact.go b/pkg/transport/process/uploaders/oci_artifact.go index b7e2be7b..242c8f9e 100644 --- a/pkg/transport/process/uploaders/oci_artifact.go +++ b/pkg/transport/process/uploaders/oci_artifact.go @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // -// SPDX-License-Identifier +// SPDX-License-Identifier: Apache-2.0 package uploaders import ( diff --git a/pkg/transport/process/uploaders/util.go b/pkg/transport/process/uploaders/util.go index a216bf7d..e28ebaec 100644 --- a/pkg/transport/process/uploaders/util.go +++ b/pkg/transport/process/uploaders/util.go @@ -1,6 +1,6 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // -// SPDX-License-Identifier +// SPDX-License-Identifier: Apache-2.0 package uploaders import ( From 5385d57d355afebd0b87cefc768a0b3a608207bd Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Thu, 4 Nov 2021 09:33:57 +0100 Subject: [PATCH 63/94] adds tests for utils --- pkg/utils/utils.go | 32 +++++++++------ pkg/utils/utils_test.go | 90 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 104 insertions(+), 18 deletions(-) diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index de66ef37..23f5a280 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -177,9 +177,17 @@ func BytesString(bytes uint64, accuracy int) string { return fmt.Sprintf("%s %s", stringValue, unit) } -func FilterTARArchive(r io.Reader, w io.Writer, removePatterns []string) error { - tr := tar.NewReader(r) - tw := tar.NewWriter(w) +func FilterTARArchive(inputReader io.Reader, outputWriter io.Writer, removePatterns []string) error { + if inputReader == nil { + return errors.New("inputReader must not be nil") + } + + if outputWriter == nil { + return errors.New("outputWriter must not be nil") + } + + tr := tar.NewReader(inputReader) + tw := tar.NewWriter(outputWriter) defer tw.Close() NEXT_FILE: @@ -215,18 +223,18 @@ NEXT_FILE: return nil } -// WriteFileToTARArchive writes a new file with name=filename and content=contentReader to archiveWriter -func WriteFileToTARArchive(filename string, contentReader io.Reader, archiveWriter *tar.Writer) error { +// WriteFileToTARArchive writes a new file with name=filename and content=inputReader to outputWriter +func WriteFileToTARArchive(filename string, inputReader io.Reader, outputWriter *tar.Writer) error { if filename == "" { return errors.New("filename must not be empty") } - if contentReader == nil { - return errors.New("contentReader must not be nil") + if inputReader == nil { + return errors.New("inputReader must not be nil") } - if archiveWriter == nil { - return errors.New("archiveWriter must not be nil") + if outputWriter == nil { + return errors.New("outputWriter must not be nil") } tempfile, err := ioutil.TempFile("", "") @@ -235,7 +243,7 @@ func WriteFileToTARArchive(filename string, contentReader io.Reader, archiveWrit } defer tempfile.Close() - fsize, err := io.Copy(tempfile, contentReader) + fsize, err := io.Copy(tempfile, inputReader) if err != nil { return fmt.Errorf("unable to copy content to tempfile: %w", err) } @@ -251,11 +259,11 @@ func WriteFileToTARArchive(filename string, contentReader io.Reader, archiveWrit ModTime: time.Now(), } - if err := archiveWriter.WriteHeader(&header); err != nil { + if err := outputWriter.WriteHeader(&header); err != nil { return fmt.Errorf("unable to write tar header: %w", err) } - if _, err := io.Copy(archiveWriter, tempfile); err != nil { + if _, err := io.Copy(outputWriter, tempfile); err != nil { return fmt.Errorf("unable to write file to tar archive: %w", err) } diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index 1fcc428f..3a26e43e 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -7,6 +7,7 @@ import ( "archive/tar" "bytes" "io" + "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -67,20 +68,97 @@ var _ = Describe("utils", func() { It("should return error if filename is empty", func() { tw := tar.NewWriter(bytes.NewBuffer([]byte{})) - contentReader := bytes.NewReader([]byte{}) - Expect(utils.WriteFileToTARArchive("", contentReader, tw)).To(MatchError("filename must not be empty")) + inputReader := bytes.NewReader([]byte{}) + Expect(utils.WriteFileToTARArchive("", inputReader, tw)).To(MatchError("filename must not be empty")) }) - It("should return error if contentReader is nil", func() { + It("should return error if inputReader is nil", func() { tw := tar.NewWriter(bytes.NewBuffer([]byte{})) - Expect(utils.WriteFileToTARArchive("testfile", nil, tw)).To(MatchError("contentReader must not be nil")) + Expect(utils.WriteFileToTARArchive("testfile", nil, tw)).To(MatchError("inputReader must not be nil")) }) It("should return error if outArchive is nil", func() { - contentReader := bytes.NewReader([]byte{}) - Expect(utils.WriteFileToTARArchive("testfile", contentReader, nil)).To(MatchError("archiveWriter must not be nil")) + inputReader := bytes.NewReader([]byte{}) + Expect(utils.WriteFileToTARArchive("testfile", inputReader, nil)).To(MatchError("outputWriter must not be nil")) + }) + + }) + + Context("FilterTARArchive", func() { + + It("should filter archive", func() { + removePatterns := []string{ + "second/*", + } + + inputFiles := map[string][]byte{ + "first/testfile": []byte("some-content"), + "second/testfile": []byte("more-content"), + "second/testfile-2": []byte("other-content"), + } + + expectedFiles := map[string][]byte{ + "first/testfile": []byte("some-content"), + } + + inBuf := bytes.NewBuffer([]byte{}) + tw := tar.NewWriter(inBuf) + + for filename, content := range inputFiles { + h := tar.Header{ + Name: filename, + Size: int64(len(content)), + Mode: 0600, + ModTime: time.Now(), + } + + Expect(tw.WriteHeader(&h)).To(Succeed()) + _, err := tw.Write(content) + Expect(err).ToNot(HaveOccurred()) + } + + outBuf := bytes.NewBuffer([]byte{}) + Expect(utils.FilterTARArchive(inBuf, outBuf, removePatterns)).To(Succeed()) + + CheckTarArchive(outBuf, expectedFiles) + }) + + It("should return error if inputReader is nil", func() { + outWriter := bytes.NewBuffer([]byte{}) + Expect(utils.FilterTARArchive(nil, outWriter, []string{})).To(MatchError("inputReader must not be nil")) + }) + + It("should return error if outputWriter is nil", func() { + inputReader := bytes.NewReader([]byte{}) + Expect(utils.FilterTARArchive(inputReader, nil, []string{})).To(MatchError("outputWriter must not be nil")) }) }) }) + +func CheckTarArchive(r io.Reader, expectedFiles map[string][]byte) { + tr := tar.NewReader(r) + + for { + header, err := tr.Next() + if err != nil { + if err == io.EOF { + break + } + Expect(err).ToNot(HaveOccurred()) + } + + actualContentBuf := bytes.NewBuffer([]byte{}) + _, err = io.Copy(actualContentBuf, tr) + Expect(err).ToNot(HaveOccurred()) + + expectedContent, ok := expectedFiles[header.Name] + Expect(ok).To(BeTrue()) + Expect(actualContentBuf.Bytes()).To(Equal(expectedContent)) + + delete(expectedFiles, header.Name) + } + + Expect(expectedFiles).To(BeEmpty()) +} From 7c694d18da7bb2679468829437f715f2dcb2a1cc Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 9 Nov 2021 16:40:38 +0100 Subject: [PATCH 64/94] adds tests for filtering --- pkg/testutils/oci.go | 38 ++--- pkg/testutils/tar.go | 60 ++++++++ pkg/transport/config/processor_factory.go | 2 +- .../process/processors/oci_image_filter.go | 131 ++++++++++-------- .../processors/oci_image_filter_test.go | 102 ++++++++++++++ .../processors/resource_labeler_test.go | 11 +- .../utils/oci_artifact_serialization_test.go | 33 +++-- pkg/utils/utils_test.go | 48 +------ 8 files changed, 285 insertions(+), 140 deletions(-) create mode 100644 pkg/testutils/tar.go create mode 100644 pkg/transport/process/processors/oci_image_filter_test.go diff --git a/pkg/testutils/oci.go b/pkg/testutils/oci.go index e1850e2f..f887c3d7 100644 --- a/pkg/testutils/oci.go +++ b/pkg/testutils/oci.go @@ -16,6 +16,7 @@ import ( ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/ociclient/oci" ) @@ -150,34 +151,39 @@ func CompareImageIndices(actualIndex *oci.Index, expectedIndex *oci.Index) { } } -func CreateManifest(configData []byte, layerData []byte) (*ocispecv1.Manifest, ocispecv1.Descriptor, error) { +func CreateManifest(configData []byte, layersData [][]byte, ocicache cache.Cache) (*ocispecv1.Manifest, ocispecv1.Descriptor) { configDesc := ocispecv1.Descriptor{ MediaType: "text/plain", Digest: digest.FromBytes(configData), Size: int64(len(configData)), } + Expect(ocicache.Add(configDesc, io.NopCloser(bytes.NewReader(configData)))).To(Succeed()) - layerDesc := ocispecv1.Descriptor{ - MediaType: "text/plain", - Digest: digest.FromBytes(layerData), - Size: int64(len(layerData)), + layerDescs := []ocispecv1.Descriptor{} + for _, layerData := range layersData { + layerDesc := ocispecv1.Descriptor{ + MediaType: "text/plain", + Digest: digest.FromBytes(layerData), + Size: int64(len(layerData)), + } + layerDescs = append(layerDescs, layerDesc) + Expect(ocicache.Add(layerDesc, io.NopCloser(bytes.NewReader(layerData)))).To(Succeed()) } - m := ocispecv1.Manifest{ + manifest := ocispecv1.Manifest{ Config: configDesc, - Layers: []ocispecv1.Descriptor{ - layerDesc, - }, + Layers: layerDescs, } - mBytes, err := json.Marshal(m) - if err != nil { - return nil, ocispecv1.Descriptor{}, err - } + manifestBytes, err := json.Marshal(manifest) + Expect(err).ToNot(HaveOccurred()) - d := ocispecv1.Descriptor{ - Digest: digest.FromBytes(mBytes), + manifestDesc := ocispecv1.Descriptor{ + MediaType: ocispecv1.MediaTypeImageManifest, + Digest: digest.FromBytes(manifestBytes), + Size: int64(len(manifestBytes)), } + Expect(ocicache.Add(manifestDesc, io.NopCloser(bytes.NewReader(manifestBytes)))).To(Succeed()) - return &m, d, nil + return &manifest, manifestDesc } diff --git a/pkg/testutils/tar.go b/pkg/testutils/tar.go new file mode 100644 index 00000000..2726c59a --- /dev/null +++ b/pkg/testutils/tar.go @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package testutils + +import ( + "archive/tar" + "bytes" + "io" + "time" + + . "github.com/onsi/gomega" +) + +func CreateTARArchive(files map[string][]byte) *bytes.Buffer { + buf := bytes.NewBuffer([]byte{}) + tw := tar.NewWriter(buf) + defer tw.Close() + + for filename, content := range files { + h := tar.Header{ + Name: filename, + Size: int64(len(content)), + Mode: 0600, + ModTime: time.Now(), + } + + Expect(tw.WriteHeader(&h)).To(Succeed()) + _, err := tw.Write(content) + Expect(err).ToNot(HaveOccurred()) + } + + return buf +} + +func CheckTARArchive(r io.Reader, expectedFiles map[string][]byte) { + tr := tar.NewReader(r) + + for { + header, err := tr.Next() + if err != nil { + if err == io.EOF { + break + } + Expect(err).ToNot(HaveOccurred()) + } + + actualContentBuf := bytes.NewBuffer([]byte{}) + _, err = io.Copy(actualContentBuf, tr) + Expect(err).ToNot(HaveOccurred()) + + expectedContent, ok := expectedFiles[header.Name] + Expect(ok).To(BeTrue()) + Expect(actualContentBuf.Bytes()).To(Equal(expectedContent)) + + delete(expectedFiles, header.Name) + } + + Expect(expectedFiles).To(BeEmpty()) +} diff --git a/pkg/transport/config/processor_factory.go b/pkg/transport/config/processor_factory.go index 2681aea1..c8cf06b2 100644 --- a/pkg/transport/config/processor_factory.go +++ b/pkg/transport/config/processor_factory.go @@ -68,5 +68,5 @@ func (f *ProcessorFactory) createOCIImageFilter(rawSpec *json.RawMessage) (proce return nil, fmt.Errorf("unable to parse spec: %w", err) } - return processors.NewOCIImageFilter(f.cache, spec.RemovePatterns), nil + return processors.NewOCIImageFilter(f.cache, spec.RemovePatterns) } diff --git a/pkg/transport/process/processors/oci_image_filter.go b/pkg/transport/process/processors/oci_image_filter.go index e7a8e51b..e6fb6be6 100644 --- a/pkg/transport/process/processors/oci_image_filter.go +++ b/pkg/transport/process/processors/oci_image_filter.go @@ -10,6 +10,7 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -43,33 +44,15 @@ func (f *ociImageFilter) Process(ctx context.Context, r io.Reader, w io.Writer) } if ociArtifact.IsIndex() { - filteredImgs := []*oci.Manifest{} - for _, m := range ociArtifact.GetIndex().Manifests { - filteredManifest, err := f.filterImage(*m) - if err != nil { - return fmt.Errorf("unable to filter image %+v: %w", m, err) - } - - manifestBytes, err := json.Marshal(filteredManifest.Data) - if err != nil { - return fmt.Errorf("unable to marshal manifest: ") - } - - if err := f.cache.Add(filteredManifest.Descriptor, io.NopCloser(bytes.NewReader(manifestBytes))); err != nil { - return fmt.Errorf("unable to add filtered manifest to cache: %w", err) - } - - filteredImgs = append(filteredImgs, filteredManifest) - } - filteredIndex := &oci.Index{ - Manifests: filteredImgs, - Annotations: ociArtifact.GetIndex().Annotations, + filteredIndex, err := f.FilterImageIndex(*ociArtifact.GetIndex()) + if err != nil { + return fmt.Errorf("unable to filter image index: %w", err) } if err := ociArtifact.SetIndex(filteredIndex); err != nil { return fmt.Errorf("unable to set index: %w", err) } } else { - filteredImg, err := f.filterImage(*ociArtifact.GetManifest()) + filteredImg, err := f.FilterImage(*ociArtifact.GetManifest()) if err != nil { return fmt.Errorf("unable to filter image: %w", err) } @@ -90,10 +73,39 @@ func (f *ociImageFilter) Process(ctx context.Context, r io.Reader, w io.Writer) return nil } -func (f *ociImageFilter) filterImage(manifest oci.Manifest) (*oci.Manifest, error) { +func (f *ociImageFilter) FilterImageIndex(inputIndex oci.Index) (*oci.Index, error) { + filteredImgs := []*oci.Manifest{} + for _, m := range inputIndex.Manifests { + filteredManifest, err := f.FilterImage(*m) + if err != nil { + return nil, fmt.Errorf("unable to filter image %+v: %w", m, err) + } + + manifestBytes, err := json.Marshal(filteredManifest.Data) + if err != nil { + return nil, fmt.Errorf("unable to marshal manifest: ") + } + + if err := f.cache.Add(filteredManifest.Descriptor, io.NopCloser(bytes.NewReader(manifestBytes))); err != nil { + return nil, fmt.Errorf("unable to add filtered manifest to cache: %w", err) + } + + filteredImgs = append(filteredImgs, filteredManifest) + } + + filteredIndex := oci.Index{ + Manifests: filteredImgs, + Annotations: inputIndex.Annotations, + } + + return &filteredIndex, nil +} + +func (f *ociImageFilter) FilterImage(manifest oci.Manifest) (*oci.Manifest, error) { diffIDs := []digest.Digest{} - digestMappings := map[digest.Digest]digest.Digest{} + unfilteredToFilteredDigestMappings := map[digest.Digest]digest.Digest{} filteredLayers := []ocispecv1.Descriptor{} + for _, layer := range manifest.Data.Layers { layerBlobReader, err := f.cache.Get(layer) if err != nil { @@ -108,8 +120,8 @@ func (f *ociImageFilter) filterImage(manifest oci.Manifest) (*oci.Manifest, erro var layerBlobWriter io.WriteCloser = tmpfile isGzipCompressedLayer := layer.MediaType == ocispecv1.MediaTypeImageLayerGzip || layer.MediaType == images.MediaTypeDockerSchema2LayerGzip - if isGzipCompressedLayer { + // TODO: detect correct compression and apply to reader and writer layerBlobReader, err = gzip.NewReader(layerBlobReader) if err != nil { return nil, fmt.Errorf("unable to create gzip reader for layer: %w", err) @@ -123,7 +135,7 @@ func (f *ociImageFilter) filterImage(manifest oci.Manifest) (*oci.Manifest, erro mw := io.MultiWriter(layerBlobWriter, uncompressedHasher) if err = utils.FilterTARArchive(layerBlobReader, mw, f.removePatterns); err != nil { - return nil, fmt.Errorf("unable to filter blob: %w", err) + return nil, fmt.Errorf("unable to filter layer blob: %w", err) } if isGzipCompressedLayer { @@ -142,7 +154,7 @@ func (f *ociImageFilter) filterImage(manifest oci.Manifest) (*oci.Manifest, erro return nil, fmt.Errorf("unable to calculate digest for layer %+v: %w", layer, err) } - digestMappings[layer.Digest] = filteredDigest + unfilteredToFilteredDigestMappings[layer.Digest] = filteredDigest diffIDs = append(diffIDs, digest.NewDigestFromEncoded(digest.SHA256, hex.EncodeToString(uncompressedHasher.Sum(nil)))) fstat, err := tmpfile.Stat() @@ -167,6 +179,7 @@ func (f *ociImageFilter) filterImage(manifest oci.Manifest) (*oci.Manifest, erro return nil, fmt.Errorf("unable to add filtered layer blob to cache: %w", err) } } + manifest.Data.Layers = filteredLayers cfgBlob, err := f.cache.Get(manifest.Data.Config) @@ -174,39 +187,37 @@ func (f *ociImageFilter) filterImage(manifest oci.Manifest) (*oci.Manifest, erro return nil, fmt.Errorf("unable to get config blob from cache: %w", err) } - data, err := io.ReadAll(cfgBlob) + cfgData, err := io.ReadAll(cfgBlob) if err != nil { return nil, fmt.Errorf("unable to read config blob: %w", err) } - var config map[string]*json.RawMessage - if err := json.Unmarshal(data, &config); err != nil { - return nil, fmt.Errorf("unable to unmarshal config: %w", err) - } - - rootfs := ocispecv1.RootFS{ - Type: "layers", - DiffIDs: diffIDs, - } - rootfsRaw, err := utils.RawJSON(rootfs) - if err != nil { - return nil, fmt.Errorf("unable to convert rootfs to JSON: %w", err) - } - config["rootfs"] = rootfsRaw - - marshaledConfig, err := json.Marshal(config) - if err != nil { - return nil, fmt.Errorf("unable to marshal config: %w", err) - } - - configDesc := ocispecv1.Descriptor{ - MediaType: ocispecv1.MediaTypeImageConfig, - Digest: digest.FromBytes(marshaledConfig), - Size: int64(len(marshaledConfig)), - } - manifest.Data.Config = configDesc - - if err := f.cache.Add(configDesc, io.NopCloser(bytes.NewReader(marshaledConfig))); err != nil { + // TODO: check which modifications on config should be performed + // var config map[string]*json.RawMessage + // if err := json.Unmarshal(data, &config); err != nil { + // return nil, fmt.Errorf("unable to unmarshal config: %w", err) + // } + // rootfs := ocispecv1.RootFS{ + // Type: "layers", + // DiffIDs: diffIDs, + // } + // rootfsRaw, err := utils.RawJSON(rootfs) + // if err != nil { + // return nil, fmt.Errorf("unable to convert rootfs to JSON: %w", err) + // } + // config["rootfs"] = rootfsRaw + // marshaledConfig, err := json.Marshal(cfgData) + // if err != nil { + // return nil, fmt.Errorf("unable to marshal config: %w", err) + // } + // configDesc := ocispecv1.Descriptor{ + // MediaType: ocispecv1.MediaTypeImageConfig, + // Digest: digest.FromBytes(marshaledConfig), + // Size: int64(len(marshaledConfig)), + // } + // manifest.Data.Config = configDesc + + if err := f.cache.Add(manifest.Data.Config, io.NopCloser(bytes.NewReader(cfgData))); err != nil { return nil, fmt.Errorf("unable to add filtered layer blob to cache: %w", err) } @@ -221,10 +232,14 @@ func (f *ociImageFilter) filterImage(manifest oci.Manifest) (*oci.Manifest, erro return &manifest, nil } -func NewOCIImageFilter(cache cache.Cache, removePatterns []string) process.ResourceStreamProcessor { +func NewOCIImageFilter(cache cache.Cache, removePatterns []string) (process.ResourceStreamProcessor, error) { + if cache == nil { + return nil, errors.New("cache must not be nil") + } + obj := ociImageFilter{ cache: cache, removePatterns: removePatterns, } - return &obj + return &obj, nil } diff --git a/pkg/transport/process/processors/oci_image_filter_test.go b/pkg/transport/process/processors/oci_image_filter_test.go new file mode 100644 index 00000000..12768dad --- /dev/null +++ b/pkg/transport/process/processors/oci_image_filter_test.go @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package processors_test + +import ( + "bytes" + "context" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/gardener/component-cli/ociclient/cache" + "github.com/gardener/component-cli/ociclient/oci" + "github.com/gardener/component-cli/pkg/testutils" + "github.com/gardener/component-cli/pkg/transport/process/processors" + processutils "github.com/gardener/component-cli/pkg/transport/process/utils" +) + +var _ = Describe("ociImageFilter", func() { + + Context("Process", func() { + + It("should filter files from oci image", func() { + expectedRes := cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "my-res", + Version: "v0.1.0", + Type: "ociImage", + }, + } + + l1Files := map[string][]byte{ + "test": []byte("test-content"), + } + + config := []byte("{}") + + layers := [][]byte{ + testutils.CreateTARArchive(l1Files).Bytes(), + } + + cd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + Resources: []cdv2.Resource{ + expectedRes, + }, + }, + } + + removePatterns := []string{ + "", + } + + ociCache := cache.NewInMemoryCache() + + manifestData, manifestDesc := testutils.CreateManifest(config, layers, ociCache) + + m := oci.Manifest{ + Descriptor: manifestDesc, + Data: manifestData, + } + + ociArtifact, err := oci.NewManifestArtifact(&m) + Expect(err).ToNot(HaveOccurred()) + + r1, err := processutils.SerializeOCIArtifact(*ociArtifact, ociCache) + Expect(err).ToNot(HaveOccurred()) + defer r1.Close() + + inBuf := bytes.NewBuffer([]byte{}) + Expect(processutils.WriteProcessorMessage(cd, expectedRes, r1, inBuf)).To(Succeed()) + + outbuf := bytes.NewBuffer([]byte{}) + proc, err := processors.NewOCIImageFilter(ociCache, removePatterns) + Expect(err).ToNot(HaveOccurred()) + Expect(proc.Process(context.TODO(), inBuf, outbuf)).To(Succeed()) + + actualCD, actualRes, actualResBlobReader, err := processutils.ReadProcessorMessage(outbuf) + Expect(err).ToNot(HaveOccurred()) + + Expect(*actualCD).To(Equal(cd)) + Expect(actualRes).To(Equal(expectedRes)) + + newCache := cache.NewInMemoryCache() + actualOciArtifact, err := processutils.DeserializeOCIArtifact(actualResBlobReader, newCache) + Expect(err).ToNot(HaveOccurred()) + Expect(actualOciArtifact).To(Equal(ociArtifact)) + }) + + It("should filter files from oci image index", func() { + + }) + + It("should return error if cache is nil", func() { + _, err := processors.NewOCIImageFilter(nil, []string{}) + Expect(err).To(MatchError("cache must not be nil")) + }) + + }) +}) diff --git a/pkg/transport/process/processors/resource_labeler_test.go b/pkg/transport/process/processors/resource_labeler_test.go index 263d7a39..232f315b 100644 --- a/pkg/transport/process/processors/resource_labeler_test.go +++ b/pkg/transport/process/processors/resource_labeler_test.go @@ -45,7 +45,7 @@ var _ = Describe("resourceLabeler", func() { expectedRes.Labels = append(expectedRes.Labels, l1) expectedRes.Labels = append(expectedRes.Labels, l2) - cd := cdv2.ComponentDescriptor{ + expectedCd := cdv2.ComponentDescriptor{ ComponentSpec: cdv2.ComponentSpec{ Resources: []cdv2.Resource{ res, @@ -54,17 +54,16 @@ var _ = Describe("resourceLabeler", func() { } inBuf := bytes.NewBuffer([]byte{}) - Expect(utils.WriteProcessorMessage(cd, res, bytes.NewReader(resBytes), inBuf)).To(Succeed()) + Expect(utils.WriteProcessorMessage(expectedCd, res, bytes.NewReader(resBytes), inBuf)).To(Succeed()) outbuf := bytes.NewBuffer([]byte{}) - - p1 := processors.NewResourceLabeler(l1, l2) - Expect(p1.Process(context.TODO(), inBuf, outbuf)).To(Succeed()) + proc := processors.NewResourceLabeler(l1, l2) + Expect(proc.Process(context.TODO(), inBuf, outbuf)).To(Succeed()) actualCD, actualRes, actualResBlobReader, err := utils.ReadProcessorMessage(outbuf) Expect(err).ToNot(HaveOccurred()) - Expect(*actualCD).To(Equal(cd)) + Expect(*actualCD).To(Equal(expectedCd)) Expect(actualRes).To(Equal(expectedRes)) actualResBlobBuf := bytes.NewBuffer([]byte{}) diff --git a/pkg/transport/process/utils/oci_artifact_serialization_test.go b/pkg/transport/process/utils/oci_artifact_serialization_test.go index d6e6ddff..db117cee 100644 --- a/pkg/transport/process/utils/oci_artifact_serialization_test.go +++ b/pkg/transport/process/utils/oci_artifact_serialization_test.go @@ -26,9 +26,10 @@ var _ = Describe("oci artifact serialization", func() { It("should correctly serialize and deserialize image", func() { configData := []byte("config-data") - layerData := []byte("layer-data") - m, _, err := testutils.CreateManifest(configData, layerData) - Expect(err).ToNot(HaveOccurred()) + layers := [][]byte{ + []byte("layer-data"), + } + m, _ := testutils.CreateManifest(configData, layers, cache.NewInMemoryCache()) expectedOciArtifact, err := oci.NewManifestArtifact( &oci.Manifest{ @@ -39,7 +40,7 @@ var _ = Describe("oci artifact serialization", func() { serializeCache := cache.NewInMemoryCache() Expect(serializeCache.Add(m.Config, io.NopCloser(bytes.NewReader(configData)))).To(Succeed()) - Expect(serializeCache.Add(m.Layers[0], io.NopCloser(bytes.NewReader(layerData)))).To(Succeed()) + Expect(serializeCache.Add(m.Layers[0], io.NopCloser(bytes.NewReader(layers[0])))).To(Succeed()) serializedReader, err := utils.SerializeOCIArtifact(*expectedOciArtifact, serializeCache) Expect(err).ToNot(HaveOccurred()) @@ -61,19 +62,21 @@ var _ = Describe("oci artifact serialization", func() { actualLayerBuf := bytes.NewBuffer([]byte{}) _, err = io.Copy(actualLayerBuf, actualLayerReader) Expect(err).ToNot(HaveOccurred()) - Expect(actualLayerBuf.Bytes()).To(Equal(layerData)) + Expect(actualLayerBuf.Bytes()).To(Equal(layers[0])) }) It("should correctly serialize and deserialize image index", func() { configData1 := []byte("config-data-1") - layerData1 := []byte("layer-data-1") + layers1 := [][]byte{ + []byte("layer-data-1"), + } configData2 := []byte("config-data-2") - layerData2 := []byte("layer-data-2") + layers2 := [][]byte{ + []byte("layer-data-2"), + } - m1, m1Desc, err := testutils.CreateManifest(configData1, layerData1) - Expect(err).ToNot(HaveOccurred()) - m2, m2Desc, err := testutils.CreateManifest(configData2, layerData2) - Expect(err).ToNot(HaveOccurred()) + m1, m1Desc := testutils.CreateManifest(configData1, layers1, cache.NewInMemoryCache()) + m2, m2Desc := testutils.CreateManifest(configData2, layers2, cache.NewInMemoryCache()) m1Bytes, err := json.Marshal(m1) Expect(err).ToNot(HaveOccurred()) @@ -101,10 +104,10 @@ var _ = Describe("oci artifact serialization", func() { serializeCache := cache.NewInMemoryCache() Expect(serializeCache.Add(m1Desc, io.NopCloser(bytes.NewReader(m1Bytes)))).To(Succeed()) Expect(serializeCache.Add(m1.Config, io.NopCloser(bytes.NewReader(configData1)))).To(Succeed()) - Expect(serializeCache.Add(m1.Layers[0], io.NopCloser(bytes.NewReader(layerData1)))).To(Succeed()) + Expect(serializeCache.Add(m1.Layers[0], io.NopCloser(bytes.NewReader(layers1[0])))).To(Succeed()) Expect(serializeCache.Add(m2Desc, io.NopCloser(bytes.NewReader(m2Bytes)))).To(Succeed()) Expect(serializeCache.Add(m2.Config, io.NopCloser(bytes.NewReader(configData2)))).To(Succeed()) - Expect(serializeCache.Add(m2.Layers[0], io.NopCloser(bytes.NewReader(layerData2)))).To(Succeed()) + Expect(serializeCache.Add(m2.Layers[0], io.NopCloser(bytes.NewReader(layers2[0])))).To(Succeed()) serializedReader, err := utils.SerializeOCIArtifact(*expectedOciArtifact, serializeCache) Expect(err).ToNot(HaveOccurred()) @@ -131,7 +134,7 @@ var _ = Describe("oci artifact serialization", func() { actualLayerBuf := bytes.NewBuffer([]byte{}) _, err = io.Copy(actualLayerBuf, actualLayerReader) Expect(err).ToNot(HaveOccurred()) - Expect(actualLayerBuf.Bytes()).To(Equal(layerData1)) + Expect(actualLayerBuf.Bytes()).To(Equal(layers1[0])) // check second manifest config and layer actualConfigReader, err = deserializeCache.Get(actualOciArtifact.GetIndex().Manifests[1].Data.Config) @@ -146,7 +149,7 @@ var _ = Describe("oci artifact serialization", func() { actualLayerBuf = bytes.NewBuffer([]byte{}) _, err = io.Copy(actualLayerBuf, actualLayerReader) Expect(err).ToNot(HaveOccurred()) - Expect(actualLayerBuf.Bytes()).To(Equal(layerData2)) + Expect(actualLayerBuf.Bytes()).To(Equal(layers2[0])) }) }) diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go index 3a26e43e..eb8d0129 100644 --- a/pkg/utils/utils_test.go +++ b/pkg/utils/utils_test.go @@ -7,11 +7,11 @@ import ( "archive/tar" "bytes" "io" - "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "github.com/gardener/component-cli/pkg/testutils" "github.com/gardener/component-cli/pkg/utils" ) @@ -101,26 +101,12 @@ var _ = Describe("utils", func() { "first/testfile": []byte("some-content"), } - inBuf := bytes.NewBuffer([]byte{}) - tw := tar.NewWriter(inBuf) - - for filename, content := range inputFiles { - h := tar.Header{ - Name: filename, - Size: int64(len(content)), - Mode: 0600, - ModTime: time.Now(), - } - - Expect(tw.WriteHeader(&h)).To(Succeed()) - _, err := tw.Write(content) - Expect(err).ToNot(HaveOccurred()) - } + archive := testutils.CreateTARArchive(inputFiles) outBuf := bytes.NewBuffer([]byte{}) - Expect(utils.FilterTARArchive(inBuf, outBuf, removePatterns)).To(Succeed()) + Expect(utils.FilterTARArchive(archive, outBuf, removePatterns)).To(Succeed()) - CheckTarArchive(outBuf, expectedFiles) + testutils.CheckTARArchive(outBuf, expectedFiles) }) It("should return error if inputReader is nil", func() { @@ -136,29 +122,3 @@ var _ = Describe("utils", func() { }) }) - -func CheckTarArchive(r io.Reader, expectedFiles map[string][]byte) { - tr := tar.NewReader(r) - - for { - header, err := tr.Next() - if err != nil { - if err == io.EOF { - break - } - Expect(err).ToNot(HaveOccurred()) - } - - actualContentBuf := bytes.NewBuffer([]byte{}) - _, err = io.Copy(actualContentBuf, tr) - Expect(err).ToNot(HaveOccurred()) - - expectedContent, ok := expectedFiles[header.Name] - Expect(ok).To(BeTrue()) - Expect(actualContentBuf.Bytes()).To(Equal(expectedContent)) - - delete(expectedFiles, header.Name) - } - - Expect(expectedFiles).To(BeEmpty()) -} From 9630a31cc8d2401468a2925c6d48ba54e6452869 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 10 Nov 2021 15:09:34 +0100 Subject: [PATCH 65/94] wip --- pkg/testutils/oci.go | 14 ++++- pkg/testutils/tar.go | 5 +- .../process/processors/oci_image_filter.go | 10 ++-- .../processors/oci_image_filter_test.go | 59 +++++++++++++------ .../utils/oci_artifact_serialization_test.go | 6 +- 5 files changed, 62 insertions(+), 32 deletions(-) diff --git a/pkg/testutils/oci.go b/pkg/testutils/oci.go index f887c3d7..4506c600 100644 --- a/pkg/testutils/oci.go +++ b/pkg/testutils/oci.go @@ -157,7 +157,10 @@ func CreateManifest(configData []byte, layersData [][]byte, ocicache cache.Cache Digest: digest.FromBytes(configData), Size: int64(len(configData)), } - Expect(ocicache.Add(configDesc, io.NopCloser(bytes.NewReader(configData)))).To(Succeed()) + + if ocicache != nil { + Expect(ocicache.Add(configDesc, io.NopCloser(bytes.NewReader(configData)))).To(Succeed()) + } layerDescs := []ocispecv1.Descriptor{} for _, layerData := range layersData { @@ -167,7 +170,10 @@ func CreateManifest(configData []byte, layersData [][]byte, ocicache cache.Cache Size: int64(len(layerData)), } layerDescs = append(layerDescs, layerDesc) - Expect(ocicache.Add(layerDesc, io.NopCloser(bytes.NewReader(layerData)))).To(Succeed()) + + if ocicache != nil { + Expect(ocicache.Add(layerDesc, io.NopCloser(bytes.NewReader(layerData)))).To(Succeed()) + } } manifest := ocispecv1.Manifest{ @@ -183,7 +189,9 @@ func CreateManifest(configData []byte, layersData [][]byte, ocicache cache.Cache Digest: digest.FromBytes(manifestBytes), Size: int64(len(manifestBytes)), } - Expect(ocicache.Add(manifestDesc, io.NopCloser(bytes.NewReader(manifestBytes)))).To(Succeed()) + if ocicache != nil { + Expect(ocicache.Add(manifestDesc, io.NopCloser(bytes.NewReader(manifestBytes)))).To(Succeed()) + } return &manifest, manifestDesc } diff --git a/pkg/testutils/tar.go b/pkg/testutils/tar.go index 2726c59a..78fc49eb 100644 --- a/pkg/testutils/tar.go +++ b/pkg/testutils/tar.go @@ -6,6 +6,7 @@ package testutils import ( "archive/tar" "bytes" + "fmt" "io" "time" @@ -50,11 +51,11 @@ func CheckTARArchive(r io.Reader, expectedFiles map[string][]byte) { Expect(err).ToNot(HaveOccurred()) expectedContent, ok := expectedFiles[header.Name] - Expect(ok).To(BeTrue()) + Expect(ok).To(BeTrue(), fmt.Sprintf("file \"%s\" is not included in expected files", header.Name)) Expect(actualContentBuf.Bytes()).To(Equal(expectedContent)) delete(expectedFiles, header.Name) } - Expect(expectedFiles).To(BeEmpty()) + Expect(expectedFiles).To(BeEmpty(), fmt.Sprintf("unable to find all expected files in TAR archive. missing files = %+v", expectedFiles)) } diff --git a/pkg/transport/process/processors/oci_image_filter.go b/pkg/transport/process/processors/oci_image_filter.go index e6fb6be6..dfb382e0 100644 --- a/pkg/transport/process/processors/oci_image_filter.go +++ b/pkg/transport/process/processors/oci_image_filter.go @@ -44,7 +44,7 @@ func (f *ociImageFilter) Process(ctx context.Context, r io.Reader, w io.Writer) } if ociArtifact.IsIndex() { - filteredIndex, err := f.FilterImageIndex(*ociArtifact.GetIndex()) + filteredIndex, err := f.filterImageIndex(*ociArtifact.GetIndex()) if err != nil { return fmt.Errorf("unable to filter image index: %w", err) } @@ -52,7 +52,7 @@ func (f *ociImageFilter) Process(ctx context.Context, r io.Reader, w io.Writer) return fmt.Errorf("unable to set index: %w", err) } } else { - filteredImg, err := f.FilterImage(*ociArtifact.GetManifest()) + filteredImg, err := f.filterImage(*ociArtifact.GetManifest()) if err != nil { return fmt.Errorf("unable to filter image: %w", err) } @@ -73,10 +73,10 @@ func (f *ociImageFilter) Process(ctx context.Context, r io.Reader, w io.Writer) return nil } -func (f *ociImageFilter) FilterImageIndex(inputIndex oci.Index) (*oci.Index, error) { +func (f *ociImageFilter) filterImageIndex(inputIndex oci.Index) (*oci.Index, error) { filteredImgs := []*oci.Manifest{} for _, m := range inputIndex.Manifests { - filteredManifest, err := f.FilterImage(*m) + filteredManifest, err := f.filterImage(*m) if err != nil { return nil, fmt.Errorf("unable to filter image %+v: %w", m, err) } @@ -101,7 +101,7 @@ func (f *ociImageFilter) FilterImageIndex(inputIndex oci.Index) (*oci.Index, err return &filteredIndex, nil } -func (f *ociImageFilter) FilterImage(manifest oci.Manifest) (*oci.Manifest, error) { +func (f *ociImageFilter) filterImage(manifest oci.Manifest) (*oci.Manifest, error) { diffIDs := []digest.Digest{} unfilteredToFilteredDigestMappings := map[digest.Digest]digest.Digest{} filteredLayers := []ocispecv1.Descriptor{} diff --git a/pkg/transport/process/processors/oci_image_filter_test.go b/pkg/transport/process/processors/oci_image_filter_test.go index 12768dad..a624aacf 100644 --- a/pkg/transport/process/processors/oci_image_filter_test.go +++ b/pkg/transport/process/processors/oci_image_filter_test.go @@ -30,33 +30,50 @@ var _ = Describe("ociImageFilter", func() { Type: "ociImage", }, } + expectedCd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + Resources: []cdv2.Resource{ + expectedRes, + }, + }, + } - l1Files := map[string][]byte{ - "test": []byte("test-content"), + removePatterns := []string{ + "filter-this/*", } - config := []byte("{}") + l1Files := map[string][]byte{ + "test": []byte("test-content"), + "filter-this/file1": []byte("file1-content"), + "filter-this/file2": []byte("file2-content"), + } + // TODO: add gzipped layer layers := [][]byte{ testutils.CreateTARArchive(l1Files).Bytes(), } - cd := cdv2.ComponentDescriptor{ - ComponentSpec: cdv2.ComponentSpec{ - Resources: []cdv2.Resource{ - expectedRes, - }, - }, + expectedL1Files := map[string][]byte{ + "test": []byte("test-content"), } - removePatterns := []string{ - "", + expectedLayers := [][]byte{ + testutils.CreateTARArchive(expectedL1Files).Bytes(), } - ociCache := cache.NewInMemoryCache() + configData := []byte("{}") - manifestData, manifestDesc := testutils.CreateManifest(config, layers, ociCache) + expectedManifestData, expectedManifestDesc := testutils.CreateManifest(configData, expectedLayers, nil) + em := oci.Manifest{ + Descriptor: expectedManifestDesc, + Data: expectedManifestData, + } + expectedOciArtifact, err := oci.NewManifestArtifact(&em) + Expect(err).ToNot(HaveOccurred()) + + ociCache := cache.NewInMemoryCache() + manifestData, manifestDesc := testutils.CreateManifest(configData, layers, ociCache) m := oci.Manifest{ Descriptor: manifestDesc, Data: manifestData, @@ -70,7 +87,7 @@ var _ = Describe("ociImageFilter", func() { defer r1.Close() inBuf := bytes.NewBuffer([]byte{}) - Expect(processutils.WriteProcessorMessage(cd, expectedRes, r1, inBuf)).To(Succeed()) + Expect(processutils.WriteProcessorMessage(expectedCd, expectedRes, r1, inBuf)).To(Succeed()) outbuf := bytes.NewBuffer([]byte{}) proc, err := processors.NewOCIImageFilter(ociCache, removePatterns) @@ -80,16 +97,20 @@ var _ = Describe("ociImageFilter", func() { actualCD, actualRes, actualResBlobReader, err := processutils.ReadProcessorMessage(outbuf) Expect(err).ToNot(HaveOccurred()) - Expect(*actualCD).To(Equal(cd)) + Expect(*actualCD).To(Equal(expectedCd)) Expect(actualRes).To(Equal(expectedRes)) - newCache := cache.NewInMemoryCache() - actualOciArtifact, err := processutils.DeserializeOCIArtifact(actualResBlobReader, newCache) + deserializeCache := cache.NewInMemoryCache() + actualOciArtifact, err := processutils.DeserializeOCIArtifact(actualResBlobReader, deserializeCache) + Expect(err).ToNot(HaveOccurred()) + Expect(actualOciArtifact).To(Equal(expectedOciArtifact)) + + r, err := deserializeCache.Get(actualOciArtifact.GetManifest().Data.Layers[0]) Expect(err).ToNot(HaveOccurred()) - Expect(actualOciArtifact).To(Equal(ociArtifact)) + testutils.CheckTARArchive(r, expectedL1Files) }) - It("should filter files from oci image index", func() { + It("should filter files from all images of an oci image index", func() { }) diff --git a/pkg/transport/process/utils/oci_artifact_serialization_test.go b/pkg/transport/process/utils/oci_artifact_serialization_test.go index db117cee..fa8058b1 100644 --- a/pkg/transport/process/utils/oci_artifact_serialization_test.go +++ b/pkg/transport/process/utils/oci_artifact_serialization_test.go @@ -29,7 +29,7 @@ var _ = Describe("oci artifact serialization", func() { layers := [][]byte{ []byte("layer-data"), } - m, _ := testutils.CreateManifest(configData, layers, cache.NewInMemoryCache()) + m, _ := testutils.CreateManifest(configData, layers, nil) expectedOciArtifact, err := oci.NewManifestArtifact( &oci.Manifest{ @@ -75,8 +75,8 @@ var _ = Describe("oci artifact serialization", func() { []byte("layer-data-2"), } - m1, m1Desc := testutils.CreateManifest(configData1, layers1, cache.NewInMemoryCache()) - m2, m2Desc := testutils.CreateManifest(configData2, layers2, cache.NewInMemoryCache()) + m1, m1Desc := testutils.CreateManifest(configData1, layers1, nil) + m2, m2Desc := testutils.CreateManifest(configData2, layers2, nil) m1Bytes, err := json.Marshal(m1) Expect(err).ToNot(HaveOccurred()) From 2b9b508a03da234dd9a5a361cd6ca2589d64545a Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Thu, 11 Nov 2021 12:20:55 +0100 Subject: [PATCH 66/94] adds test for image index in oci image filter --- pkg/testutils/oci.go | 4 + pkg/testutils/tar.go | 9 +- .../processors/oci_image_filter_test.go | 146 ++++++++++++++++++ 3 files changed, 157 insertions(+), 2 deletions(-) diff --git a/pkg/testutils/oci.go b/pkg/testutils/oci.go index 4506c600..29d4d76b 100644 --- a/pkg/testutils/oci.go +++ b/pkg/testutils/oci.go @@ -13,6 +13,7 @@ import ( . "github.com/onsi/gomega" "github.com/opencontainers/go-digest" + "github.com/opencontainers/image-spec/specs-go" ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/gardener/component-cli/ociclient" @@ -177,6 +178,9 @@ func CreateManifest(configData []byte, layersData [][]byte, ocicache cache.Cache } manifest := ocispecv1.Manifest{ + Versioned: specs.Versioned{ + SchemaVersion: 2, + }, Config: configDesc, Layers: layerDescs, } diff --git a/pkg/testutils/tar.go b/pkg/testutils/tar.go index 78fc49eb..80b8b358 100644 --- a/pkg/testutils/tar.go +++ b/pkg/testutils/tar.go @@ -37,6 +37,11 @@ func CreateTARArchive(files map[string][]byte) *bytes.Buffer { func CheckTARArchive(r io.Reader, expectedFiles map[string][]byte) { tr := tar.NewReader(r) + expectedFilesCopy := map[string][]byte{} + for key, value := range expectedFiles { + expectedFilesCopy[key] = value + } + for { header, err := tr.Next() if err != nil { @@ -54,8 +59,8 @@ func CheckTARArchive(r io.Reader, expectedFiles map[string][]byte) { Expect(ok).To(BeTrue(), fmt.Sprintf("file \"%s\" is not included in expected files", header.Name)) Expect(actualContentBuf.Bytes()).To(Equal(expectedContent)) - delete(expectedFiles, header.Name) + delete(expectedFilesCopy, header.Name) } - Expect(expectedFiles).To(BeEmpty(), fmt.Sprintf("unable to find all expected files in TAR archive. missing files = %+v", expectedFiles)) + Expect(expectedFilesCopy).To(BeEmpty(), fmt.Sprintf("unable to find all expected files in TAR archive. missing files = %+v", expectedFiles)) } diff --git a/pkg/transport/process/processors/oci_image_filter_test.go b/pkg/transport/process/processors/oci_image_filter_test.go index a624aacf..c7b004fe 100644 --- a/pkg/transport/process/processors/oci_image_filter_test.go +++ b/pkg/transport/process/processors/oci_image_filter_test.go @@ -10,6 +10,7 @@ import ( cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/ociclient/oci" @@ -111,7 +112,152 @@ var _ = Describe("ociImageFilter", func() { }) It("should filter files from all images of an oci image index", func() { + expectedRes := cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "my-res", + Version: "v0.1.0", + Type: "ociImage", + }, + } + expectedCd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + Resources: []cdv2.Resource{ + expectedRes, + }, + }, + } + removePatterns := []string{ + "filter-this/*", + } + + l1Files := map[string][]byte{ + "test": []byte("test-content"), + "filter-this/file1": []byte("file1-content"), + "filter-this/file2": []byte("file2-content"), + } + + // TODO: add gzipped layer + layers := [][]byte{ + testutils.CreateTARArchive(l1Files).Bytes(), + } + + expectedL1Files := map[string][]byte{ + "test": []byte("test-content"), + } + + expectedLayers := [][]byte{ + testutils.CreateTARArchive(expectedL1Files).Bytes(), + } + + configData := []byte("{}") + + expectedManifestData, expectedManifestDesc := testutils.CreateManifest(configData, expectedLayers, nil) + ei := oci.Index{ + Manifests: []*oci.Manifest{ + { + Descriptor: ocispecv1.Descriptor{ + MediaType: expectedManifestDesc.MediaType, + Digest: expectedManifestDesc.Digest, + Size: expectedManifestDesc.Size, + Platform: &ocispecv1.Platform{ + Architecture: "amd64", + OS: "linux", + }, + }, + Data: expectedManifestData, + }, + { + Descriptor: ocispecv1.Descriptor{ + MediaType: expectedManifestDesc.MediaType, + Digest: expectedManifestDesc.Digest, + Size: expectedManifestDesc.Size, + Platform: &ocispecv1.Platform{ + Architecture: "amd64", + OS: "windows", + }, + }, + Data: expectedManifestData, + }, + }, + Annotations: map[string]string{ + "test": "test", + }, + } + expectedOciArtifact, err := oci.NewIndexArtifact(&ei) + Expect(err).ToNot(HaveOccurred()) + + ociCache := cache.NewInMemoryCache() + + manifestData, manifestDesc := testutils.CreateManifest(configData, layers, ociCache) + + index := oci.Index{ + Manifests: []*oci.Manifest{ + { + Descriptor: ocispecv1.Descriptor{ + MediaType: manifestDesc.MediaType, + Digest: manifestDesc.Digest, + Size: manifestDesc.Size, + Platform: &ocispecv1.Platform{ + Architecture: "amd64", + OS: "linux", + }, + }, + Data: manifestData, + }, + { + Descriptor: ocispecv1.Descriptor{ + MediaType: manifestDesc.MediaType, + Digest: manifestDesc.Digest, + Size: manifestDesc.Size, + Platform: &ocispecv1.Platform{ + Architecture: "amd64", + OS: "windows", + }, + }, + Data: manifestData, + }, + }, + Annotations: map[string]string{ + "test": "test", + }, + } + + ociArtifact, err := oci.NewIndexArtifact(&index) + Expect(err).ToNot(HaveOccurred()) + + r1, err := processutils.SerializeOCIArtifact(*ociArtifact, ociCache) + Expect(err).ToNot(HaveOccurred()) + defer r1.Close() + + inBuf := bytes.NewBuffer([]byte{}) + Expect(processutils.WriteProcessorMessage(expectedCd, expectedRes, r1, inBuf)).To(Succeed()) + + outbuf := bytes.NewBuffer([]byte{}) + proc, err := processors.NewOCIImageFilter(ociCache, removePatterns) + Expect(err).ToNot(HaveOccurred()) + Expect(proc.Process(context.TODO(), inBuf, outbuf)).To(Succeed()) + + actualCD, actualRes, actualResBlobReader, err := processutils.ReadProcessorMessage(outbuf) + Expect(err).ToNot(HaveOccurred()) + + Expect(*actualCD).To(Equal(expectedCd)) + Expect(actualRes).To(Equal(expectedRes)) + + deserializeCache := cache.NewInMemoryCache() + actualOciArtifact, err := processutils.DeserializeOCIArtifact(actualResBlobReader, deserializeCache) + Expect(err).ToNot(HaveOccurred()) + Expect(actualOciArtifact).To(Equal(expectedOciArtifact)) + + firstMan := actualOciArtifact.GetIndex().Manifests[0] + fr, err := deserializeCache.Get(firstMan.Data.Layers[0]) + Expect(err).ToNot(HaveOccurred()) + testutils.CheckTARArchive(fr, expectedL1Files) + + secondMan := actualOciArtifact.GetIndex().Manifests[1] + sr, err := deserializeCache.Get(secondMan.Data.Layers[0]) + Expect(err).ToNot(HaveOccurred()) + testutils.CheckTARArchive(sr, expectedL1Files) }) It("should return error if cache is nil", func() { From bc7da1d390a27d4f6ac08758caf4bb2aa9624fae Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Thu, 11 Nov 2021 14:05:21 +0100 Subject: [PATCH 67/94] comment out unnecessary code --- pkg/transport/process/processors/oci_image_filter.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pkg/transport/process/processors/oci_image_filter.go b/pkg/transport/process/processors/oci_image_filter.go index dfb382e0..bf6a44f4 100644 --- a/pkg/transport/process/processors/oci_image_filter.go +++ b/pkg/transport/process/processors/oci_image_filter.go @@ -8,7 +8,6 @@ import ( "compress/gzip" "context" "crypto/sha256" - "encoding/hex" "encoding/json" "errors" "fmt" @@ -102,8 +101,8 @@ func (f *ociImageFilter) filterImageIndex(inputIndex oci.Index) (*oci.Index, err } func (f *ociImageFilter) filterImage(manifest oci.Manifest) (*oci.Manifest, error) { - diffIDs := []digest.Digest{} - unfilteredToFilteredDigestMappings := map[digest.Digest]digest.Digest{} + // diffIDs := []digest.Digest{} + // unfilteredToFilteredDigestMappings := map[digest.Digest]digest.Digest{} filteredLayers := []ocispecv1.Descriptor{} for _, layer := range manifest.Data.Layers { @@ -154,8 +153,8 @@ func (f *ociImageFilter) filterImage(manifest oci.Manifest) (*oci.Manifest, erro return nil, fmt.Errorf("unable to calculate digest for layer %+v: %w", layer, err) } - unfilteredToFilteredDigestMappings[layer.Digest] = filteredDigest - diffIDs = append(diffIDs, digest.NewDigestFromEncoded(digest.SHA256, hex.EncodeToString(uncompressedHasher.Sum(nil)))) + // unfilteredToFilteredDigestMappings[layer.Digest] = filteredDigest + // diffIDs = append(diffIDs, digest.NewDigestFromEncoded(digest.SHA256, hex.EncodeToString(uncompressedHasher.Sum(nil)))) fstat, err := tmpfile.Stat() if err != nil { From a6e85fbd8044cca204829dbbf926ae9fbe774b5e Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Thu, 11 Nov 2021 17:04:55 +0100 Subject: [PATCH 68/94] adds tests for config package --- pkg/transport/config/config_suite_test.go | 48 +++++++++++ pkg/transport/config/processing_job.go | 2 +- pkg/transport/config/processing_job_test.go | 92 +++++++++++++++++++++ pkg/transport/config/testdata/transport.cfg | 91 ++++++++++++++++++++ pkg/transport/config/util.go | 2 +- 5 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 pkg/transport/config/config_suite_test.go create mode 100644 pkg/transport/config/processing_job_test.go create mode 100644 pkg/transport/config/testdata/transport.cfg diff --git a/pkg/transport/config/config_suite_test.go b/pkg/transport/config/config_suite_test.go new file mode 100644 index 00000000..8375aed9 --- /dev/null +++ b/pkg/transport/config/config_suite_test.go @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package config_test + +import ( + "os" + "testing" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "github.com/go-logr/logr" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "sigs.k8s.io/yaml" + + "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/ociclient/cache" + "github.com/gardener/component-cli/pkg/transport/config" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Config Test Suite") +} + +var ( + factory *config.ProcessingJobFactory +) + +var _ = BeforeSuite(func() { + transportCfgYaml, err := os.ReadFile("./testdata/transport.cfg") + Expect(err).ToNot(HaveOccurred()) + + var transportCfg config.TransportConfig + Expect(yaml.Unmarshal(transportCfgYaml, &transportCfg)).To(Succeed()) + + client, err := ociclient.NewClient(logr.Discard()) + Expect(err).ToNot(HaveOccurred()) + ocicache := cache.NewInMemoryCache() + targetCtx := cdv2.NewOCIRegistryRepository("", "") + + df := config.NewDownloaderFactory(client, ocicache) + pf := config.NewProcessorFactory(ocicache) + uf := config.NewUploaderFactory(client, ocicache, *targetCtx) + + factory, err = config.NewProcessingJobFactory(transportCfg, df, pf, uf) + Expect(err).ToNot(HaveOccurred()) +}, 5) diff --git a/pkg/transport/config/processing_job.go b/pkg/transport/config/processing_job.go index 8288f456..54f4311f 100644 --- a/pkg/transport/config/processing_job.go +++ b/pkg/transport/config/processing_job.go @@ -64,7 +64,7 @@ type parsedTransportConfig struct { func NewProcessingJobFactory(transportCfg TransportConfig, df *DownloaderFactory, pf *ProcessorFactory, uf *UploaderFactory) (*ProcessingJobFactory, error) { parsedTransportConfig, err := parseTransportConfig(&transportCfg) if err != nil { - return nil, fmt.Errorf("failed creating lookup table %w", err) + return nil, fmt.Errorf("unable to parse transport config: %w", err) } c := ProcessingJobFactory{ diff --git a/pkg/transport/config/processing_job_test.go b/pkg/transport/config/processing_job_test.go new file mode 100644 index 00000000..40f6a1f1 --- /dev/null +++ b/pkg/transport/config/processing_job_test.go @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package config_test + +import ( + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("processing job", func() { + + Context("processing job factory", func() { + + It("should create processing job", func() { + cd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + ObjectMeta: cdv2.ObjectMeta{ + Name: "github.com/my-component", + Version: "0.1.0", + }, + }, + } + acc, err := cdv2.NewUnstructured(cdv2.NewOCIRegistryAccess("test.com")) + Expect(err).ToNot(HaveOccurred()) + res := cdv2.Resource{ + Access: &acc, + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "my-res", + Version: "0.1.0", + Type: cdv2.OCIImageType, + }, + } + + job, err := factory.Create(cd, res) + Expect(err).ToNot(HaveOccurred()) + + Expect(*job.ComponentDescriptor).To(Equal(cd)) + Expect(*job.Resource).To(Equal(res)) + + Expect(len(job.Downloaders)).To(Equal(1)) + Expect(job.Downloaders[0].Name).To(Equal("oci-artifact-dl")) + + Expect(len(job.Processors)).To(Equal(3)) + Expect(job.Processors[0].Name).To(Equal("my-filter")) + Expect(job.Processors[1].Name).To(Equal("my-labeler")) + Expect(job.Processors[2].Name).To(Equal("my-extension")) + + Expect(len(job.Uploaders)).To(Equal(1)) + Expect(job.Uploaders[0].Name).To(Equal("oci-artifact-ul")) + }) + + It("should create processing job", func() { + cd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + ObjectMeta: cdv2.ObjectMeta{ + Name: "github.com/my-component", + Version: "0.1.0", + }, + }, + } + acc, err := cdv2.NewUnstructured(cdv2.NewLocalOCIBlobAccess("sha256:123")) + Expect(err).ToNot(HaveOccurred()) + res := cdv2.Resource{ + Access: &acc, + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "my-res", + Version: "0.1.0", + Type: "helm", + }, + } + + job, err := factory.Create(cd, res) + Expect(err).ToNot(HaveOccurred()) + + Expect(*job.ComponentDescriptor).To(Equal(cd)) + Expect(*job.Resource).To(Equal(res)) + + Expect(len(job.Downloaders)).To(Equal(1)) + Expect(job.Downloaders[0].Name).To(Equal("local-oci-blob-dl")) + + Expect(len(job.Processors)).To(Equal(1)) + Expect(job.Processors[0].Name).To(Equal("my-labeler")) + + Expect(len(job.Uploaders)).To(Equal(1)) + Expect(job.Uploaders[0].Name).To(Equal("local-oci-blob-ul")) + }) + + }) + +}) diff --git a/pkg/transport/config/testdata/transport.cfg b/pkg/transport/config/testdata/transport.cfg new file mode 100644 index 00000000..2ab8db25 --- /dev/null +++ b/pkg/transport/config/testdata/transport.cfg @@ -0,0 +1,91 @@ +meta: + version: v1 + +processors: +- name: 'my-filter' + type: 'OciImageFilter' + spec: + removePatterns: + - 'bin/*' +- name: 'my-labeler' + type: 'ResourceLabeler' + spec: + labels: + - name: 'label-name' + value: 'label-value' +- name: 'my-extension' + type: 'Executable' + spec: + bin: '/path/to/processor' + args: + - '-arg1=test' + env: + key1: val1 + +downloaders: +- name: 'oci-artifact-dl' + type: 'OciArtifactDownloader' + filters: + - type: 'ResourceTypeFilter' + spec: + includeResourceTypes: + - 'ociImage' +- name: 'local-oci-blob-dl' + type: 'LocalOciBlobDownloader' + filters: + - type: 'ResourceAccessTypeFilter' + spec: + includeAccessTypes: + - 'localOciBlob' + +uploaders: +- name: 'oci-artifact-ul' + type: 'OciArtifactUploader' + spec: + baseUrl: 'o.ingress.js-ek.hubforplay.shoot.canary.k8s-hana.ondemand.com/js-transport-0' + keepSourceRepo: false + filters: + - type: 'ResourceTypeFilter' + spec: + includeResourceTypes: + - 'ociImage' +- name: 'local-oci-blob-ul' + type: 'LocalOciBlobUploader' + filters: + - type: 'ResourceAccessTypeFilter' + spec: + includeAccessTypes: + - 'localOciBlob' + +rules: +- name: 'generic-image-filtering' + processors: + - name: 'my-filter' + type: 'processor' + filters: + - type: 'ResourceTypeFilter' + spec: + includeResourceTypes: + - 'ociImage' +- name: 'my-component-labeling' + processors: + - name: 'my-labeler' + type: 'processor' + filters: + - type: 'ComponentNameFilter' + spec: + includeComponentNames: + - 'github.com/my-component' +- name: 'my-component-special-image-processing' + processors: + - name: 'my-extension' + type: 'processor' + filters: + - type: 'ComponentNameFilter' + spec: + includeComponentNames: + - 'github.com/my-component' + - type: 'ResourceTypeFilter' + spec: + includeResourceTypes: + - 'ociImage' \ No newline at end of file diff --git a/pkg/transport/config/util.go b/pkg/transport/config/util.go index a11aa625..a4605950 100644 --- a/pkg/transport/config/util.go +++ b/pkg/transport/config/util.go @@ -14,7 +14,7 @@ import ( ) const ( - ExecutableType = "executable" + ExecutableType = "Executable" ) func createExecutable(rawSpec *json.RawMessage) (process.ResourceStreamProcessor, error) { From a2a335abcbc2d08b4605a959035e90bd4e006d62 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Fri, 12 Nov 2021 11:42:00 +0100 Subject: [PATCH 69/94] refactoring and docs added --- pkg/transport/config/config_suite_test.go | 2 +- pkg/transport/config/downloader_factory.go | 14 ++++++++---- pkg/transport/config/filter_factory.go | 20 ++++++++++++----- pkg/transport/config/processing_job.go | 6 ++++- pkg/transport/config/processing_job_test.go | 2 +- pkg/transport/config/processor_factory.go | 22 ++++++++++++------- pkg/transport/config/testdata/transport.cfg | 8 +++---- pkg/transport/config/uploader_factory.go | 18 ++++++++++----- ...image_filter.go => oci_artifact_filter.go} | 13 ++++++----- ...er_test.go => oci_artifact_filter_test.go} | 8 +++---- .../process/uploaders/oci_artifact.go | 2 +- 11 files changed, 73 insertions(+), 42 deletions(-) rename pkg/transport/process/processors/{oci_image_filter.go => oci_artifact_filter.go} (93%) rename pkg/transport/process/processors/{oci_image_filter_test.go => oci_artifact_filter_test.go} (96%) diff --git a/pkg/transport/config/config_suite_test.go b/pkg/transport/config/config_suite_test.go index 8375aed9..34c9b29b 100644 --- a/pkg/transport/config/config_suite_test.go +++ b/pkg/transport/config/config_suite_test.go @@ -37,7 +37,7 @@ var _ = BeforeSuite(func() { client, err := ociclient.NewClient(logr.Discard()) Expect(err).ToNot(HaveOccurred()) ocicache := cache.NewInMemoryCache() - targetCtx := cdv2.NewOCIRegistryRepository("", "") + targetCtx := cdv2.NewOCIRegistryRepository("my-target-registry.com/test", "") df := config.NewDownloaderFactory(client, ocicache) pf := config.NewProcessorFactory(ocicache) diff --git a/pkg/transport/config/downloader_factory.go b/pkg/transport/config/downloader_factory.go index a23ce592..cd0c57b5 100644 --- a/pkg/transport/config/downloader_factory.go +++ b/pkg/transport/config/downloader_factory.go @@ -14,10 +14,14 @@ import ( ) const ( + // LocalOCIBlobDownloaderType defines the type of a local oci blob downloader LocalOCIBlobDownloaderType = "LocalOciBlobDownloader" - OCIArtifactDownloaderType = "OciArtifactDownloader" + + // OCIArtifactDownloaderType defines the type of an oci artifact downloader + OCIArtifactDownloaderType = "OciArtifactDownloader" ) +// NewDownloaderFactory creates a new downloader factory func NewDownloaderFactory(client ociclient.Client, ocicache cache.Cache) *DownloaderFactory { return &DownloaderFactory{ client: client, @@ -25,13 +29,15 @@ func NewDownloaderFactory(client ociclient.Client, ocicache cache.Cache) *Downlo } } +// DownloaderFactory defines a helper struct for creating downloaders type DownloaderFactory struct { client ociclient.Client cache cache.Cache } -func (f *DownloaderFactory) Create(typ string, spec *json.RawMessage) (process.ResourceStreamProcessor, error) { - switch typ { +// Create creates a new downloader defined by a type and a spec +func (f *DownloaderFactory) Create(downloaderType string, spec *json.RawMessage) (process.ResourceStreamProcessor, error) { + switch downloaderType { case LocalOCIBlobDownloaderType: return downloaders.NewLocalOCIBlobDownloader(f.client) case OCIArtifactDownloaderType: @@ -39,6 +45,6 @@ func (f *DownloaderFactory) Create(typ string, spec *json.RawMessage) (process.R case ExecutableType: return createExecutable(spec) default: - return nil, fmt.Errorf("unknown downloader type %s", typ) + return nil, fmt.Errorf("unknown downloader type %s", downloaderType) } } diff --git a/pkg/transport/config/filter_factory.go b/pkg/transport/config/filter_factory.go index dd2199a1..27748673 100644 --- a/pkg/transport/config/filter_factory.go +++ b/pkg/transport/config/filter_factory.go @@ -13,27 +13,35 @@ import ( ) const ( + // ComponentNameFilterType defines the type of a component name filter ComponentNameFilterType = "ComponentNameFilter" - ResourceTypeFilterType = "ResourceTypeFilter" - AccessTypeFilterType = "ResourceAccessTypeFilter" + + // ResourceTypeFilterType defines the type of a resource type filter + ResourceTypeFilterType = "ResourceTypeFilter" + + // ResourceAccessTypeFilterType defines the type of a resource access filter + ResourceAccessTypeFilterType = "ResourceAccessTypeFilter" ) +// NewFilterFactory creates a new filter factory func NewFilterFactory() *FilterFactory { return &FilterFactory{} } +// FilterFactory defines a helper struct for creating filters type FilterFactory struct{} -func (f *FilterFactory) Create(typ string, spec *json.RawMessage) (filters.Filter, error) { - switch typ { +// Create creates a new filter defined by a type and a spec +func (f *FilterFactory) Create(filterType string, spec *json.RawMessage) (filters.Filter, error) { + switch filterType { case ComponentNameFilterType: return f.createComponentNameFilter(spec) case ResourceTypeFilterType: return f.createResourceTypeFilter(spec) - case AccessTypeFilterType: + case ResourceAccessTypeFilterType: return f.createAccessTypeFilter(spec) default: - return nil, fmt.Errorf("unknown filter type %s", typ) + return nil, fmt.Errorf("unknown filter type %s", filterType) } } diff --git a/pkg/transport/config/processing_job.go b/pkg/transport/config/processing_job.go index 54f4311f..eea04c0c 100644 --- a/pkg/transport/config/processing_job.go +++ b/pkg/transport/config/processing_job.go @@ -14,6 +14,7 @@ import ( "github.com/gardener/component-cli/pkg/transport/process" ) +// ProcessingJob defines a type which contains all data for processing a single resource type ProcessingJob struct { ComponentDescriptor *cdv2.ComponentDescriptor Resource *cdv2.Resource @@ -61,6 +62,7 @@ type parsedTransportConfig struct { Rules []parsedRuleDefinition } +// NewProcessingJobFactory creates a new processing job factory func NewProcessingJobFactory(transportCfg TransportConfig, df *DownloaderFactory, pf *ProcessorFactory, uf *UploaderFactory) (*ProcessingJobFactory, error) { parsedTransportConfig, err := parseTransportConfig(&transportCfg) if err != nil { @@ -77,6 +79,7 @@ func NewProcessingJobFactory(transportCfg TransportConfig, df *DownloaderFactory return &c, nil } +// ProcessingJobFactory defines a helper struct for creating processing jobs type ProcessingJobFactory struct { parsedConfig *parsedTransportConfig uploaderFactory *UploaderFactory @@ -84,7 +87,6 @@ type ProcessingJobFactory struct { processorFactory *ProcessorFactory } -// Create a ProcessorsLookup on the base of a config func parseTransportConfig(config *TransportConfig) (*parsedTransportConfig, error) { var parsedConfig parsedTransportConfig ff := NewFilterFactory() @@ -147,6 +149,7 @@ func parseTransportConfig(config *TransportConfig) (*parsedTransportConfig, erro return &parsedConfig, nil } +// Create creates a new processing job for a resource func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Resource) (*ProcessingJob, error) { job := ProcessingJob{ ComponentDescriptor: &cd, @@ -234,6 +237,7 @@ func findProcessorByName(name string, lookup *parsedTransportConfig) (*parsedPro return nil, fmt.Errorf("unable to find processor %s", name) } +// Process processes the resource func (j *ProcessingJob) Process(ctx context.Context) error { processors := []process.ResourceStreamProcessor{} diff --git a/pkg/transport/config/processing_job_test.go b/pkg/transport/config/processing_job_test.go index 40f6a1f1..90a4121c 100644 --- a/pkg/transport/config/processing_job_test.go +++ b/pkg/transport/config/processing_job_test.go @@ -43,7 +43,7 @@ var _ = Describe("processing job", func() { Expect(job.Downloaders[0].Name).To(Equal("oci-artifact-dl")) Expect(len(job.Processors)).To(Equal(3)) - Expect(job.Processors[0].Name).To(Equal("my-filter")) + Expect(job.Processors[0].Name).To(Equal("my-oci-filter")) Expect(job.Processors[1].Name).To(Equal("my-labeler")) Expect(job.Processors[2].Name).To(Equal("my-extension")) diff --git a/pkg/transport/config/processor_factory.go b/pkg/transport/config/processor_factory.go index c8cf06b2..bc70b0ef 100644 --- a/pkg/transport/config/processor_factory.go +++ b/pkg/transport/config/processor_factory.go @@ -16,30 +16,36 @@ import ( ) const ( + // ResourceLabelerProcessorType defines the type of a resource labeler ResourceLabelerProcessorType = "ResourceLabeler" - OCIImageFilterProcessorType = "OciImageFilter" + + // OCIArtifactFilterProcessorType defines the type of an oci artifact filter + OCIArtifactFilterProcessorType = "OciArtifactFilter" ) +// NewProcessorFactory creates a new processor factory func NewProcessorFactory(ociCache cache.Cache) *ProcessorFactory { return &ProcessorFactory{ cache: ociCache, } } +// ProcessorFactory defines a helper struct for creating processors type ProcessorFactory struct { cache cache.Cache } -func (f *ProcessorFactory) Create(typ string, spec *json.RawMessage) (process.ResourceStreamProcessor, error) { - switch typ { +// Create creates a new processor defined by a type and a spec +func (f *ProcessorFactory) Create(processorType string, spec *json.RawMessage) (process.ResourceStreamProcessor, error) { + switch processorType { case ResourceLabelerProcessorType: return f.createResourceLabeler(spec) - case OCIImageFilterProcessorType: - return f.createOCIImageFilter(spec) + case OCIArtifactFilterProcessorType: + return f.createOCIArtifactFilter(spec) case ExecutableType: return createExecutable(spec) default: - return nil, fmt.Errorf("unknown processor type %s", typ) + return nil, fmt.Errorf("unknown processor type %s", processorType) } } @@ -57,7 +63,7 @@ func (f *ProcessorFactory) createResourceLabeler(rawSpec *json.RawMessage) (proc return processors.NewResourceLabeler(spec.Labels...), nil } -func (f *ProcessorFactory) createOCIImageFilter(rawSpec *json.RawMessage) (process.ResourceStreamProcessor, error) { +func (f *ProcessorFactory) createOCIArtifactFilter(rawSpec *json.RawMessage) (process.ResourceStreamProcessor, error) { type processorSpec struct { RemovePatterns []string `json:"removePatterns"` } @@ -68,5 +74,5 @@ func (f *ProcessorFactory) createOCIImageFilter(rawSpec *json.RawMessage) (proce return nil, fmt.Errorf("unable to parse spec: %w", err) } - return processors.NewOCIImageFilter(f.cache, spec.RemovePatterns) + return processors.NewOCIArtifactFilter(f.cache, spec.RemovePatterns) } diff --git a/pkg/transport/config/testdata/transport.cfg b/pkg/transport/config/testdata/transport.cfg index 2ab8db25..45ae9b5c 100644 --- a/pkg/transport/config/testdata/transport.cfg +++ b/pkg/transport/config/testdata/transport.cfg @@ -2,8 +2,8 @@ meta: version: v1 processors: -- name: 'my-filter' - type: 'OciImageFilter' +- name: 'my-oci-filter' + type: 'OciArtifactFilter' spec: removePatterns: - 'bin/*' @@ -42,7 +42,7 @@ uploaders: - name: 'oci-artifact-ul' type: 'OciArtifactUploader' spec: - baseUrl: 'o.ingress.js-ek.hubforplay.shoot.canary.k8s-hana.ondemand.com/js-transport-0' + baseUrl: 'my-target-registry.com/test' keepSourceRepo: false filters: - type: 'ResourceTypeFilter' @@ -60,7 +60,7 @@ uploaders: rules: - name: 'generic-image-filtering' processors: - - name: 'my-filter' + - name: 'my-oci-filter' type: 'processor' filters: - type: 'ResourceTypeFilter' diff --git a/pkg/transport/config/uploader_factory.go b/pkg/transport/config/uploader_factory.go index ab6ac464..e46fcb3e 100644 --- a/pkg/transport/config/uploader_factory.go +++ b/pkg/transport/config/uploader_factory.go @@ -17,10 +17,14 @@ import ( ) const ( + // LocalOCIBlobUploaderType defines the type of a local oci blob uploader LocalOCIBlobUploaderType = "LocalOciBlobUploader" - OCIImageUploaderType = "OciArtifactUploader" + + // OCIArtifactUploaderType defines the type of an oci artifact uploader + OCIArtifactUploaderType = "OciArtifactUploader" ) +// NewUploaderFactory creates a new uploader factory func NewUploaderFactory(client ociclient.Client, ocicache cache.Cache, targetCtx cdv2.OCIRegistryRepository) *UploaderFactory { return &UploaderFactory{ client: client, @@ -29,22 +33,24 @@ func NewUploaderFactory(client ociclient.Client, ocicache cache.Cache, targetCtx } } +// UploaderFactory defines a helper struct for creating uploaders type UploaderFactory struct { client ociclient.Client cache cache.Cache targetCtx cdv2.OCIRegistryRepository } -func (f *UploaderFactory) Create(typ string, spec *json.RawMessage) (process.ResourceStreamProcessor, error) { - switch typ { +// Create creates a new uploader defined by a type and a spec +func (f *UploaderFactory) Create(uploaderType string, spec *json.RawMessage) (process.ResourceStreamProcessor, error) { + switch uploaderType { case LocalOCIBlobUploaderType: return uploaders.NewLocalOCIBlobUploader(f.client, f.targetCtx) - case OCIImageUploaderType: + case OCIArtifactUploaderType: return f.createOCIArtifactUploader(spec) case ExecutableType: return createExecutable(spec) default: - return nil, fmt.Errorf("unknown uploader type %s", typ) + return nil, fmt.Errorf("unknown uploader type %s", uploaderType) } } @@ -60,5 +66,5 @@ func (f *UploaderFactory) createOCIArtifactUploader(rawSpec *json.RawMessage) (p return nil, fmt.Errorf("unable to parse spec: %w", err) } - return uploaders.NewOCIImageUploader(f.client, f.cache, spec.BaseUrl, spec.KeepSourceRepo) + return uploaders.NewOCIArtifactUploader(f.client, f.cache, spec.BaseUrl, spec.KeepSourceRepo) } diff --git a/pkg/transport/process/processors/oci_image_filter.go b/pkg/transport/process/processors/oci_artifact_filter.go similarity index 93% rename from pkg/transport/process/processors/oci_image_filter.go rename to pkg/transport/process/processors/oci_artifact_filter.go index bf6a44f4..c5e6af70 100644 --- a/pkg/transport/process/processors/oci_image_filter.go +++ b/pkg/transport/process/processors/oci_artifact_filter.go @@ -25,12 +25,12 @@ import ( "github.com/gardener/component-cli/pkg/utils" ) -type ociImageFilter struct { +type ociArtifactFilter struct { cache cache.Cache removePatterns []string } -func (f *ociImageFilter) Process(ctx context.Context, r io.Reader, w io.Writer) error { +func (f *ociArtifactFilter) Process(ctx context.Context, r io.Reader, w io.Writer) error { cd, res, blobreader, err := processutils.ReadProcessorMessage(r) if err != nil { return fmt.Errorf("unable to read archive: %w", err) @@ -72,7 +72,7 @@ func (f *ociImageFilter) Process(ctx context.Context, r io.Reader, w io.Writer) return nil } -func (f *ociImageFilter) filterImageIndex(inputIndex oci.Index) (*oci.Index, error) { +func (f *ociArtifactFilter) filterImageIndex(inputIndex oci.Index) (*oci.Index, error) { filteredImgs := []*oci.Manifest{} for _, m := range inputIndex.Manifests { filteredManifest, err := f.filterImage(*m) @@ -100,7 +100,7 @@ func (f *ociImageFilter) filterImageIndex(inputIndex oci.Index) (*oci.Index, err return &filteredIndex, nil } -func (f *ociImageFilter) filterImage(manifest oci.Manifest) (*oci.Manifest, error) { +func (f *ociArtifactFilter) filterImage(manifest oci.Manifest) (*oci.Manifest, error) { // diffIDs := []digest.Digest{} // unfilteredToFilteredDigestMappings := map[digest.Digest]digest.Digest{} filteredLayers := []ocispecv1.Descriptor{} @@ -231,12 +231,13 @@ func (f *ociImageFilter) filterImage(manifest oci.Manifest) (*oci.Manifest, erro return &manifest, nil } -func NewOCIImageFilter(cache cache.Cache, removePatterns []string) (process.ResourceStreamProcessor, error) { +// NewOCIArtifactFilter returns a processor that filters files from oci artifact layers +func NewOCIArtifactFilter(cache cache.Cache, removePatterns []string) (process.ResourceStreamProcessor, error) { if cache == nil { return nil, errors.New("cache must not be nil") } - obj := ociImageFilter{ + obj := ociArtifactFilter{ cache: cache, removePatterns: removePatterns, } diff --git a/pkg/transport/process/processors/oci_image_filter_test.go b/pkg/transport/process/processors/oci_artifact_filter_test.go similarity index 96% rename from pkg/transport/process/processors/oci_image_filter_test.go rename to pkg/transport/process/processors/oci_artifact_filter_test.go index c7b004fe..c3cfed05 100644 --- a/pkg/transport/process/processors/oci_image_filter_test.go +++ b/pkg/transport/process/processors/oci_artifact_filter_test.go @@ -19,7 +19,7 @@ import ( processutils "github.com/gardener/component-cli/pkg/transport/process/utils" ) -var _ = Describe("ociImageFilter", func() { +var _ = Describe("ociArtifactFilter", func() { Context("Process", func() { @@ -91,7 +91,7 @@ var _ = Describe("ociImageFilter", func() { Expect(processutils.WriteProcessorMessage(expectedCd, expectedRes, r1, inBuf)).To(Succeed()) outbuf := bytes.NewBuffer([]byte{}) - proc, err := processors.NewOCIImageFilter(ociCache, removePatterns) + proc, err := processors.NewOCIArtifactFilter(ociCache, removePatterns) Expect(err).ToNot(HaveOccurred()) Expect(proc.Process(context.TODO(), inBuf, outbuf)).To(Succeed()) @@ -234,7 +234,7 @@ var _ = Describe("ociImageFilter", func() { Expect(processutils.WriteProcessorMessage(expectedCd, expectedRes, r1, inBuf)).To(Succeed()) outbuf := bytes.NewBuffer([]byte{}) - proc, err := processors.NewOCIImageFilter(ociCache, removePatterns) + proc, err := processors.NewOCIArtifactFilter(ociCache, removePatterns) Expect(err).ToNot(HaveOccurred()) Expect(proc.Process(context.TODO(), inBuf, outbuf)).To(Succeed()) @@ -261,7 +261,7 @@ var _ = Describe("ociImageFilter", func() { }) It("should return error if cache is nil", func() { - _, err := processors.NewOCIImageFilter(nil, []string{}) + _, err := processors.NewOCIArtifactFilter(nil, []string{}) Expect(err).To(MatchError("cache must not be nil")) }) diff --git a/pkg/transport/process/uploaders/oci_artifact.go b/pkg/transport/process/uploaders/oci_artifact.go index 242c8f9e..02c3c4e0 100644 --- a/pkg/transport/process/uploaders/oci_artifact.go +++ b/pkg/transport/process/uploaders/oci_artifact.go @@ -25,7 +25,7 @@ type ociArtifactUploader struct { keepSourceRepo bool } -func NewOCIImageUploader(client ociclient.Client, cache cache.Cache, baseUrl string, keepSourceRepo bool) (process.ResourceStreamProcessor, error) { +func NewOCIArtifactUploader(client ociclient.Client, cache cache.Cache, baseUrl string, keepSourceRepo bool) (process.ResourceStreamProcessor, error) { if client == nil { return nil, errors.New("client must not be nil") } From 648abf75d73be766a243d5a0f7cf4028bf881abd Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Fri, 19 Nov 2021 15:46:19 +0100 Subject: [PATCH 70/94] wip --- .../downloaders/local_oci_blob_test.go | 2 +- .../process/processors/oci_artifact_filter.go | 3 + .../processors/oci_artifact_filter_test.go | 5 + .../process/uploaders/local_oci_blob.go | 57 +++++---- .../process/uploaders/local_oci_blob_test.go | 109 ++++++++++++++++++ .../process/uploaders/oci_artifact.go | 3 + .../process/uploaders/uploaders_suite_test.go | 53 +++++++++ pkg/transport/process/uploaders/util.go | 20 ---- pkg/utils/utils.go | 12 ++ 9 files changed, 212 insertions(+), 52 deletions(-) delete mode 100644 pkg/transport/process/uploaders/util.go diff --git a/pkg/transport/process/downloaders/local_oci_blob_test.go b/pkg/transport/process/downloaders/local_oci_blob_test.go index 9aec230e..e67bbe57 100644 --- a/pkg/transport/process/downloaders/local_oci_blob_test.go +++ b/pkg/transport/process/downloaders/local_oci_blob_test.go @@ -46,7 +46,7 @@ var _ = Describe("localOciBlob", func() { Expect(resBlob.Bytes()).To(Equal(localOciBlobData)) }) - It("should return error if called with resource of invalid type", func() { + It("should return error if called with resource of invalid access type", func() { ociArtifactRes := testComponent.Resources[imageResIndex] d, err := downloaders.NewLocalOCIBlobDownloader(ociClient) diff --git a/pkg/transport/process/processors/oci_artifact_filter.go b/pkg/transport/process/processors/oci_artifact_filter.go index c5e6af70..5f2da990 100644 --- a/pkg/transport/process/processors/oci_artifact_filter.go +++ b/pkg/transport/process/processors/oci_artifact_filter.go @@ -35,6 +35,9 @@ func (f *ociArtifactFilter) Process(ctx context.Context, r io.Reader, w io.Write if err != nil { return fmt.Errorf("unable to read archive: %w", err) } + if blobreader == nil { + return errors.New("resource blob must not be nil") + } defer blobreader.Close() ociArtifact, err := processutils.DeserializeOCIArtifact(blobreader, f.cache) diff --git a/pkg/transport/process/processors/oci_artifact_filter_test.go b/pkg/transport/process/processors/oci_artifact_filter_test.go index c3cfed05..5abb5fea 100644 --- a/pkg/transport/process/processors/oci_artifact_filter_test.go +++ b/pkg/transport/process/processors/oci_artifact_filter_test.go @@ -265,5 +265,10 @@ var _ = Describe("ociArtifactFilter", func() { Expect(err).To(MatchError("cache must not be nil")) }) + It("should return error if resource blob reader is nil", func() { + _, err := processors.NewOCIArtifactFilter(nil, []string{}) + Expect(err).To(MatchError("cache must not be nil")) + }) + }) }) diff --git a/pkg/transport/process/uploaders/local_oci_blob.go b/pkg/transport/process/uploaders/local_oci_blob.go index 9aa771e3..c60f66d1 100644 --- a/pkg/transport/process/uploaders/local_oci_blob.go +++ b/pkg/transport/process/uploaders/local_oci_blob.go @@ -16,7 +16,8 @@ import ( "github.com/gardener/component-cli/ociclient" "github.com/gardener/component-cli/pkg/transport/process" - "github.com/gardener/component-cli/pkg/transport/process/utils" + processutils "github.com/gardener/component-cli/pkg/transport/process/utils" + "github.com/gardener/component-cli/pkg/utils" ) type localOCIBlobUploader struct { @@ -37,65 +38,60 @@ func NewLocalOCIBlobUploader(client ociclient.Client, targetCtx cdv2.OCIRegistry } func (d *localOCIBlobUploader) Process(ctx context.Context, r io.Reader, w io.Writer) error { - cd, res, blobreader, err := utils.ReadProcessorMessage(r) + cd, res, blobreader, err := processutils.ReadProcessorMessage(r) if err != nil { return fmt.Errorf("unable to read processor message: %w", err) } - defer blobreader.Close() - - if res.Access.GetType() != cdv2.LocalOCIBlobType { - return fmt.Errorf("unsupported access type: %s", res.Access.Type) + if blobreader == nil { + return errors.New("resource blob must not be nil") } + defer blobreader.Close() tmpfile, err := ioutil.TempFile("", "") if err != nil { - return err + return fmt.Errorf("unable to create tempfile: %w", err) } defer tmpfile.Close() - _, err = io.Copy(tmpfile, blobreader) - if err != nil { - return err - } - - _, err = tmpfile.Seek(0, 0) + size, err := io.Copy(tmpfile, blobreader) if err != nil { - return err + return fmt.Errorf("unable to copy resource blob: %w", err) } - fstat, err := tmpfile.Stat() - if err != nil { - return err + if _, err := tmpfile.Seek(0, 0); err != nil { + return fmt.Errorf("unable to seek to beginning of tempfile: %w", err) } dgst, err := digest.FromReader(tmpfile) if err != nil { - return err + return fmt.Errorf("unable to calculate digest: %w", err) } - _, err = tmpfile.Seek(0, 0) - if err != nil { - return err + if _, err := tmpfile.Seek(0, 0); err != nil { + return fmt.Errorf("unable to seek to beginning of tempfile: %w", err) } desc := ocispecv1.Descriptor{ Digest: dgst, - Size: fstat.Size(), + Size: int64(size), MediaType: res.Type, } - err = d.uploadLocalOCIBlob(ctx, cd, res, tmpfile, desc) - if err != nil { + if err := d.uploadLocalOCIBlob(ctx, cd, res, tmpfile, desc); err != nil { return fmt.Errorf("unable to upload blob: %w", err) } - _, err = tmpfile.Seek(0, 0) + acc, err := cdv2.NewUnstructured(cdv2.NewLocalOCIBlobAccess(dgst.String())) if err != nil { - return err + return fmt.Errorf("unable to create access object: %w", err) } + res.Access = &acc - err = utils.WriteProcessorMessage(*cd, res, tmpfile, w) - if err != nil { + if _, err := tmpfile.Seek(0, 0); err != nil { + return fmt.Errorf("unable to seek to beginning of tempfile: %w", err) + } + + if err := processutils.WriteProcessorMessage(*cd, res, tmpfile, w); err != nil { return fmt.Errorf("unable to write processor message: %w", err) } @@ -103,15 +99,14 @@ func (d *localOCIBlobUploader) Process(ctx context.Context, r io.Reader, w io.Wr } func (d *localOCIBlobUploader) uploadLocalOCIBlob(ctx context.Context, cd *cdv2.ComponentDescriptor, res cdv2.Resource, r io.Reader, desc ocispecv1.Descriptor) error { - targetRef := createUploadRef(d.targetCtx, cd.Name, cd.Version) + targetRef := utils.CalculateBlobUploadRef(d.targetCtx, cd.Name, cd.Version) store := ociclient.GenericStore(func(ctx context.Context, desc ocispecv1.Descriptor, writer io.Writer) error { _, err := io.Copy(writer, r) return err }) - err := d.client.PushBlob(ctx, targetRef, desc, ociclient.WithStore(store)) - if err != nil { + if err := d.client.PushBlob(ctx, targetRef, desc, ociclient.WithStore(store)); err != nil { return fmt.Errorf("unable to push blob: %w", err) } diff --git a/pkg/transport/process/uploaders/local_oci_blob_test.go b/pkg/transport/process/uploaders/local_oci_blob_test.go index 99c7a6e5..c3b71279 100644 --- a/pkg/transport/process/uploaders/local_oci_blob_test.go +++ b/pkg/transport/process/uploaders/local_oci_blob_test.go @@ -2,3 +2,112 @@ // // SPDX-License-Identifier: Apache-2.0 package uploaders_test + +import ( + "bytes" + "context" + "io" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/opencontainers/go-digest" + ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" + + "github.com/gardener/component-cli/pkg/transport/process/uploaders" + processutils "github.com/gardener/component-cli/pkg/transport/process/utils" + "github.com/gardener/component-cli/pkg/utils" +) + +var _ = Describe("localOciBlob", func() { + + Context("Process", func() { + + It("should upload and stream resource", func() { + resBytes := []byte("Hello World") + expectedDigest := digest.FromBytes(resBytes) + res := cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "my-res", + Version: "0.1.0", + Type: "plain-text", + }, + } + cd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + ObjectMeta: cdv2.ObjectMeta{ + Name: "github.com/component-cli/test-component", + Version: "0.1.0", + }, + Resources: []cdv2.Resource{ + res, + }, + }, + } + + inProcessorMsg := bytes.NewBuffer([]byte{}) + err := processutils.WriteProcessorMessage(cd, res, bytes.NewReader(resBytes), inProcessorMsg) + Expect(err).ToNot(HaveOccurred()) + + u, err := uploaders.NewLocalOCIBlobUploader(ociClient, *targetCtx) + Expect(err).ToNot(HaveOccurred()) + + outProcessorMsg := bytes.NewBuffer([]byte{}) + err = u.Process(context.TODO(), inProcessorMsg, outProcessorMsg) + Expect(err).ToNot(HaveOccurred()) + + actualCd, actualRes, resBlobReader, err := processutils.ReadProcessorMessage(outProcessorMsg) + Expect(err).ToNot(HaveOccurred()) + defer resBlobReader.Close() + + Expect(*actualCd).To(Equal(cd)) + Expect(actualRes.Name).To(Equal(res.Name)) + Expect(actualRes.Version).To(Equal(res.Version)) + Expect(actualRes.Type).To(Equal(res.Type)) + + acc := cdv2.LocalOCIBlobAccess{} + Expect(actualRes.Access.DecodeInto(&acc)).To(Succeed()) + Expect(acc.Digest).To(Equal(string(expectedDigest))) + + resBlob := bytes.NewBuffer([]byte{}) + _, err = io.Copy(resBlob, resBlobReader) + Expect(err).ToNot(HaveOccurred()) + Expect(resBlob.Bytes()).To(Equal(resBytes)) + + desc := ocispecv1.Descriptor{ + Digest: expectedDigest, + Size: int64(len(resBytes)), + } + buf := bytes.NewBuffer([]byte{}) + Expect(ociClient.Fetch(context.TODO(), utils.CalculateBlobUploadRef(*targetCtx, cd.Name, cd.Version), desc, buf)).To(Succeed()) + Expect(buf.Bytes()).To(Equal(resBytes)) + }) + + It("should return error if resource blob is nil", func() { + acc, err := cdv2.NewUnstructured(cdv2.NewLocalOCIBlobAccess("sha256:123")) + Expect(err).ToNot(HaveOccurred()) + res := cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "my-res", + Version: "0.1.0", + Type: "plain-text", + }, + Access: &acc, + } + cd := cdv2.ComponentDescriptor{} + + u, err := uploaders.NewLocalOCIBlobUploader(ociClient, *targetCtx) + Expect(err).ToNot(HaveOccurred()) + + b1 := bytes.NewBuffer([]byte{}) + err = processutils.WriteProcessorMessage(cd, res, nil, b1) + Expect(err).ToNot(HaveOccurred()) + + b2 := bytes.NewBuffer([]byte{}) + err = u.Process(context.TODO(), b1, b2) + Expect(err).To(MatchError("resource blob must not be nil")) + }) + + }) + +}) diff --git a/pkg/transport/process/uploaders/oci_artifact.go b/pkg/transport/process/uploaders/oci_artifact.go index 02c3c4e0..24db431b 100644 --- a/pkg/transport/process/uploaders/oci_artifact.go +++ b/pkg/transport/process/uploaders/oci_artifact.go @@ -52,6 +52,9 @@ func (u *ociArtifactUploader) Process(ctx context.Context, r io.Reader, w io.Wri if err != nil { return fmt.Errorf("unable to read processor message: %w", err) } + if resBlobReader == nil { + return errors.New("resource blob must not be nil") + } defer resBlobReader.Close() ociArtifact, err := processutils.DeserializeOCIArtifact(resBlobReader, u.cache) diff --git a/pkg/transport/process/uploaders/uploaders_suite_test.go b/pkg/transport/process/uploaders/uploaders_suite_test.go index 99c7a6e5..18a23bb8 100644 --- a/pkg/transport/process/uploaders/uploaders_suite_test.go +++ b/pkg/transport/process/uploaders/uploaders_suite_test.go @@ -2,3 +2,56 @@ // // SPDX-License-Identifier: Apache-2.0 package uploaders_test + +import ( + "context" + "path/filepath" + "testing" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "github.com/go-logr/logr" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/ociclient/cache" + "github.com/gardener/component-cli/ociclient/credentials" + "github.com/gardener/component-cli/ociclient/test/envtest" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Uploaders Test Suite") +} + +var ( + testenv *envtest.Environment + ociClient ociclient.Client + ociCache cache.Cache + keyring *credentials.GeneralOciKeyring + targetCtx *cdv2.OCIRegistryRepository +) + +var _ = BeforeSuite(func() { + testenv = envtest.New(envtest.Options{ + RegistryBinaryPath: filepath.Join("../../../../", envtest.DefaultRegistryBinaryPath), + Stdout: GinkgoWriter, + Stderr: GinkgoWriter, + }) + Expect(testenv.Start(context.Background())).To(Succeed()) + targetCtx = cdv2.NewOCIRegistryRepository(testenv.Addr+"/test", "") + + keyring = credentials.New() + Expect(keyring.AddAuthConfig(testenv.Addr, credentials.AuthConfig{ + Username: testenv.BasicAuth.Username, + Password: testenv.BasicAuth.Password, + })).To(Succeed()) + ociCache = cache.NewInMemoryCache() + var err error + ociClient, err = ociclient.NewClient(logr.Discard(), ociclient.WithKeyring(keyring), ociclient.WithCache(ociCache)) + Expect(err).ToNot(HaveOccurred()) +}, 60) + +var _ = AfterSuite(func() { + Expect(testenv.Close()).To(Succeed()) +}) diff --git a/pkg/transport/process/uploaders/util.go b/pkg/transport/process/uploaders/util.go deleted file mode 100644 index e28ebaec..00000000 --- a/pkg/transport/process/uploaders/util.go +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. -// -// SPDX-License-Identifier: Apache-2.0 -package uploaders - -import ( - "fmt" - "strings" - - cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" -) - -func createUploadRef(repoCtx cdv2.OCIRegistryRepository, componentName string, componentVersion string) string { - uploadTag := componentVersion - if strings.Contains(componentVersion, ":") { - uploadTag = "latest" - } - - return fmt.Sprintf("%s/component-descriptors/%s:%s", repoCtx.BaseURL, componentName, uploadTag) -} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 23f5a280..be5488c5 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -22,6 +22,7 @@ import ( "strings" "time" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" "github.com/mandelsoft/vfs/pkg/vfs" "github.com/spf13/cobra" "sigs.k8s.io/yaml" @@ -295,3 +296,14 @@ func TargetOCIArtifactRef(targetRepo, ref string, keepOrigHost bool) (string, er parsedRef.Host = t.Host return parsedRef.String(), nil } + +// CalculateBlobUploadRef calculates the OCI reference where blobs for a component should be uploaded +func CalculateBlobUploadRef(repoCtx cdv2.OCIRegistryRepository, componentName string, componentVersion string) string { + uploadTag := componentVersion + if strings.Contains(componentVersion, ":") { + // if componentVersion is a digest, use "latest" tag for upload ref + uploadTag = "latest" + } + + return fmt.Sprintf("%s/component-descriptors/%s:%s", repoCtx.BaseURL, componentName, uploadTag) +} From e5b943bb4d28581563927cebdcee59ec73a343bc Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Mon, 22 Nov 2021 13:17:53 +0100 Subject: [PATCH 71/94] wip --- .../process/uploaders/local_oci_blob.go | 2 +- .../process/uploaders/local_oci_blob_test.go | 3 +- .../process/uploaders/oci_artifact.go | 20 +-- .../process/uploaders/oci_artifact_test.go | 153 ++++++++++++++++++ 4 files changed, 166 insertions(+), 12 deletions(-) diff --git a/pkg/transport/process/uploaders/local_oci_blob.go b/pkg/transport/process/uploaders/local_oci_blob.go index c60f66d1..95f8417d 100644 --- a/pkg/transport/process/uploaders/local_oci_blob.go +++ b/pkg/transport/process/uploaders/local_oci_blob.go @@ -83,7 +83,7 @@ func (d *localOCIBlobUploader) Process(ctx context.Context, r io.Reader, w io.Wr acc, err := cdv2.NewUnstructured(cdv2.NewLocalOCIBlobAccess(dgst.String())) if err != nil { - return fmt.Errorf("unable to create access object: %w", err) + return fmt.Errorf("unable to create resource access object: %w", err) } res.Access = &acc diff --git a/pkg/transport/process/uploaders/local_oci_blob_test.go b/pkg/transport/process/uploaders/local_oci_blob_test.go index c3b71279..2ad8c75b 100644 --- a/pkg/transport/process/uploaders/local_oci_blob_test.go +++ b/pkg/transport/process/uploaders/local_oci_blob_test.go @@ -46,8 +46,7 @@ var _ = Describe("localOciBlob", func() { } inProcessorMsg := bytes.NewBuffer([]byte{}) - err := processutils.WriteProcessorMessage(cd, res, bytes.NewReader(resBytes), inProcessorMsg) - Expect(err).ToNot(HaveOccurred()) + Expect(processutils.WriteProcessorMessage(cd, res, bytes.NewReader(resBytes), inProcessorMsg)).To(Succeed()) u, err := uploaders.NewLocalOCIBlobUploader(ociClient, *targetCtx) Expect(err).ToNot(HaveOccurred()) diff --git a/pkg/transport/process/uploaders/oci_artifact.go b/pkg/transport/process/uploaders/oci_artifact.go index 24db431b..8a8d67a8 100644 --- a/pkg/transport/process/uploaders/oci_artifact.go +++ b/pkg/transport/process/uploaders/oci_artifact.go @@ -57,29 +57,31 @@ func (u *ociArtifactUploader) Process(ctx context.Context, r io.Reader, w io.Wri } defer resBlobReader.Close() - ociArtifact, err := processutils.DeserializeOCIArtifact(resBlobReader, u.cache) - if err != nil { - return fmt.Errorf("unable to deserialize oci artifact: %w", err) - } - if res.Access.GetType() != cdv2.OCIRegistryType { return fmt.Errorf("unsupported access type: %s", res.Access.Type) } - if res.Type != cdv2.OCIImageType { - return fmt.Errorf("unsupported resource type: %s", res.Type) - } - ociAccess := &cdv2.OCIRegistryAccess{} if err := res.Access.DecodeInto(ociAccess); err != nil { return fmt.Errorf("unable to decode resource access: %w", err) } + ociArtifact, err := processutils.DeserializeOCIArtifact(resBlobReader, u.cache) + if err != nil { + return fmt.Errorf("unable to deserialize oci artifact: %w", err) + } + target, err := utils.TargetOCIArtifactRef(u.baseUrl, ociAccess.ImageReference, u.keepSourceRepo) if err != nil { return fmt.Errorf("unable to create target oci artifact reference: %w", err) } + acc, err := cdv2.NewUnstructured(cdv2.NewOCIRegistryAccess(target)) + if err != nil { + return fmt.Errorf("unable to create resource access object: %w", err) + } + res.Access = &acc + if err := u.client.PushOCIArtifact(ctx, target, ociArtifact, ociclient.WithStore(u.cache)); err != nil { return fmt.Errorf("unable to push oci artifact: %w", err) } diff --git a/pkg/transport/process/uploaders/oci_artifact_test.go b/pkg/transport/process/uploaders/oci_artifact_test.go index 99c7a6e5..99337321 100644 --- a/pkg/transport/process/uploaders/oci_artifact_test.go +++ b/pkg/transport/process/uploaders/oci_artifact_test.go @@ -2,3 +2,156 @@ // // SPDX-License-Identifier: Apache-2.0 package uploaders_test + +import ( + "bytes" + "context" + "encoding/json" + "io" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + + "github.com/gardener/component-cli/ociclient/cache" + "github.com/gardener/component-cli/ociclient/oci" + "github.com/gardener/component-cli/pkg/testutils" + "github.com/gardener/component-cli/pkg/transport/process/uploaders" + "github.com/gardener/component-cli/pkg/transport/process/utils" +) + +var _ = Describe("ociArtifact", func() { + + Context("Process", func() { + + It("should upload and stream oci image", func() { + acc, err := cdv2.NewUnstructured(cdv2.NewOCIRegistryAccess("my-registry.com/image:0.1.0")) + Expect(err).ToNot(HaveOccurred()) + res := cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "my-res", + Version: "0.1.0", + Type: "plain-text", + }, + } + cd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + ObjectMeta: cdv2.ObjectMeta{ + Name: "github.com/component-cli/test-component", + Version: "0.1.0", + }, + Resources: []cdv2.Resource{ + res, + }, + }, + } + res.Access = &acc + expectedImageRef := targetCtx.BaseURL + "/image:0.1.0" + configData := []byte("config-data") + layers := [][]byte{ + []byte("layer-data"), + } + m, _ := testutils.CreateManifest(configData, layers, nil) + + expectedOciArtifact, err := oci.NewManifestArtifact( + &oci.Manifest{ + Data: m, + }, + ) + Expect(err).ToNot(HaveOccurred()) + + serializeCache := cache.NewInMemoryCache() + Expect(serializeCache.Add(m.Config, io.NopCloser(bytes.NewReader(configData)))).To(Succeed()) + Expect(serializeCache.Add(m.Layers[0], io.NopCloser(bytes.NewReader(layers[0])))).To(Succeed()) + + serializedReader, err := utils.SerializeOCIArtifact(*expectedOciArtifact, serializeCache) + Expect(err).ToNot(HaveOccurred()) + + inProcessorMsg := bytes.NewBuffer([]byte{}) + Expect(utils.WriteProcessorMessage(cd, res, serializedReader, inProcessorMsg)).To(Succeed()) + Expect(err).ToNot(HaveOccurred()) + + d, err := uploaders.NewOCIArtifactUploader(ociClient, serializeCache, targetCtx.BaseURL, false) + Expect(err).ToNot(HaveOccurred()) + + outProcessorMsg := bytes.NewBuffer([]byte{}) + err = d.Process(context.TODO(), inProcessorMsg, outProcessorMsg) + Expect(err).ToNot(HaveOccurred()) + + actualCd, actualRes, resBlobReader, err := utils.ReadProcessorMessage(outProcessorMsg) + Expect(err).ToNot(HaveOccurred()) + defer resBlobReader.Close() + + Expect(*actualCd).To(Equal(cd)) + Expect(actualRes.Name).To(Equal(res.Name)) + Expect(actualRes.Version).To(Equal(res.Version)) + Expect(actualRes.Type).To(Equal(res.Type)) + + ociAcc := cdv2.OCIRegistryAccess{} + Expect(actualRes.Access.DecodeInto(&ociAcc)).To(Succeed()) + Expect(ociAcc.ImageReference).To(Equal(expectedImageRef)) + + actualOciArtifact, err := utils.DeserializeOCIArtifact(resBlobReader, cache.NewInMemoryCache()) + Expect(err).ToNot(HaveOccurred()) + Expect(actualOciArtifact.GetManifest().Data).To(Equal(m)) + + buf := bytes.NewBuffer([]byte{}) + Expect(ociClient.Fetch(context.TODO(), expectedImageRef, actualOciArtifact.GetManifest().Descriptor, buf)).To(Succeed()) + am := ocispecv1.Manifest{} + Expect(json.Unmarshal(buf.Bytes(), &am)).To(Succeed()) + Expect(am).To(Equal(*m)) + + buf = bytes.NewBuffer([]byte{}) + Expect(ociClient.Fetch(context.TODO(), expectedImageRef, am.Config, buf)).To(Succeed()) + Expect(buf.Bytes()).To(Equal(configData)) + + buf = bytes.NewBuffer([]byte{}) + Expect(ociClient.Fetch(context.TODO(), expectedImageRef, am.Layers[0], buf)).To(Succeed()) + Expect(buf.Bytes()).To(Equal(layers[0])) + }) + + It("should upload and stream oci image index", func() { + + }) + + It("should return error for invalid access type", func() { + acc, err := cdv2.NewUnstructured(cdv2.NewLocalOCIBlobAccess("sha256:123")) + Expect(err).ToNot(HaveOccurred()) + res := cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "my-res", + Version: "0.1.0", + Type: "plain-text", + }, + Access: &acc, + } + cd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + ObjectMeta: cdv2.ObjectMeta{ + Name: "github.com/component-cli/test-component", + Version: "0.1.0", + }, + Resources: []cdv2.Resource{ + res, + }, + }, + } + + u, err := uploaders.NewOCIArtifactUploader(ociClient, ociCache, targetCtx.BaseURL, false) + Expect(err).ToNot(HaveOccurred()) + + b1 := bytes.NewBuffer([]byte{}) + err = utils.WriteProcessorMessage(cd, res, bytes.NewReader([]byte("Hello World")), b1) + Expect(err).ToNot(HaveOccurred()) + + b2 := bytes.NewBuffer([]byte{}) + err = u.Process(context.TODO(), b1, b2) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unsupported access type")) + }) + + }) + +}) From 7f0e10c204b59cfd51bdf81d97929f1c903e306a Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Mon, 22 Nov 2021 15:41:24 +0100 Subject: [PATCH 72/94] adds testcase for oci artifact uploader --- .../process/uploaders/oci_artifact_test.go | 136 ++++++++++++++++++ .../utils/oci_artifact_serialization_test.go | 10 ++ 2 files changed, 146 insertions(+) diff --git a/pkg/transport/process/uploaders/oci_artifact_test.go b/pkg/transport/process/uploaders/oci_artifact_test.go index 99337321..b9c3b00d 100644 --- a/pkg/transport/process/uploaders/oci_artifact_test.go +++ b/pkg/transport/process/uploaders/oci_artifact_test.go @@ -113,7 +113,143 @@ var _ = Describe("ociArtifact", func() { }) It("should upload and stream oci image index", func() { + acc, err := cdv2.NewUnstructured(cdv2.NewOCIRegistryAccess("my-registry.com/image:0.1.0")) + Expect(err).ToNot(HaveOccurred()) + res := cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "my-res", + Version: "0.1.0", + Type: "plain-text", + }, + } + cd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + ObjectMeta: cdv2.ObjectMeta{ + Name: "github.com/component-cli/test-component", + Version: "0.1.0", + }, + Resources: []cdv2.Resource{ + res, + }, + }, + } + res.Access = &acc + expectedImageRef := targetCtx.BaseURL + "/image:0.1.0" + + configData1 := []byte("config-data-1") + layers1 := [][]byte{ + []byte("layer-data-1"), + } + configData2 := []byte("config-data-2") + layers2 := [][]byte{ + []byte("layer-data-2"), + } + + m1, m1Desc := testutils.CreateManifest(configData1, layers1, nil) + m1Desc.Platform = &ocispecv1.Platform{ + Architecture: "amd64", + OS: "linux", + } + + m2, m2Desc := testutils.CreateManifest(configData2, layers2, nil) + m2Desc.Platform = &ocispecv1.Platform{ + Architecture: "amd64", + OS: "windows", + } + + m1Bytes, err := json.Marshal(m1) + Expect(err).ToNot(HaveOccurred()) + + m2Bytes, err := json.Marshal(m2) + Expect(err).ToNot(HaveOccurred()) + + expectedOciArtifact, err := oci.NewIndexArtifact( + &oci.Index{ + Manifests: []*oci.Manifest{ + { + Data: m1, + }, + { + Data: m2, + }, + }, + Annotations: map[string]string{ + "testkey": "testval", + }, + }, + ) + Expect(err).ToNot(HaveOccurred()) + + serializeCache := cache.NewInMemoryCache() + Expect(serializeCache.Add(m1Desc, io.NopCloser(bytes.NewReader(m1Bytes)))).To(Succeed()) + Expect(serializeCache.Add(m1.Config, io.NopCloser(bytes.NewReader(configData1)))).To(Succeed()) + Expect(serializeCache.Add(m1.Layers[0], io.NopCloser(bytes.NewReader(layers1[0])))).To(Succeed()) + Expect(serializeCache.Add(m2Desc, io.NopCloser(bytes.NewReader(m2Bytes)))).To(Succeed()) + Expect(serializeCache.Add(m2.Config, io.NopCloser(bytes.NewReader(configData2)))).To(Succeed()) + Expect(serializeCache.Add(m2.Layers[0], io.NopCloser(bytes.NewReader(layers2[0])))).To(Succeed()) + + serializedReader, err := utils.SerializeOCIArtifact(*expectedOciArtifact, serializeCache) + Expect(err).ToNot(HaveOccurred()) + + inProcessorMsg := bytes.NewBuffer([]byte{}) + Expect(utils.WriteProcessorMessage(cd, res, serializedReader, inProcessorMsg)).To(Succeed()) + Expect(err).ToNot(HaveOccurred()) + + d, err := uploaders.NewOCIArtifactUploader(ociClient, serializeCache, targetCtx.BaseURL, false) + Expect(err).ToNot(HaveOccurred()) + + outProcessorMsg := bytes.NewBuffer([]byte{}) + err = d.Process(context.TODO(), inProcessorMsg, outProcessorMsg) + Expect(err).ToNot(HaveOccurred()) + + actualCd, actualRes, resBlobReader, err := utils.ReadProcessorMessage(outProcessorMsg) + Expect(err).ToNot(HaveOccurred()) + defer resBlobReader.Close() + + Expect(*actualCd).To(Equal(cd)) + Expect(actualRes.Name).To(Equal(res.Name)) + Expect(actualRes.Version).To(Equal(res.Version)) + Expect(actualRes.Type).To(Equal(res.Type)) + + ociAcc := cdv2.OCIRegistryAccess{} + Expect(actualRes.Access.DecodeInto(&ociAcc)).To(Succeed()) + Expect(ociAcc.ImageReference).To(Equal(expectedImageRef)) + actualOciArtifact, err := utils.DeserializeOCIArtifact(resBlobReader, cache.NewInMemoryCache()) + Expect(err).ToNot(HaveOccurred()) + + // check image index and manifests + Expect(actualOciArtifact.GetIndex().Annotations).To(Equal(expectedOciArtifact.GetIndex().Annotations)) + Expect(actualOciArtifact.GetIndex().Manifests[0].Data).To(Equal(m1)) + Expect(actualOciArtifact.GetIndex().Manifests[1].Data).To(Equal(m2)) + + buf := bytes.NewBuffer([]byte{}) + Expect(ociClient.Fetch(context.TODO(), expectedImageRef, actualOciArtifact.GetIndex().Manifests[0].Descriptor, buf)).To(Succeed()) + am := ocispecv1.Manifest{} + Expect(json.Unmarshal(buf.Bytes(), &am)).To(Succeed()) + Expect(am).To(Equal(*m1)) + + buf = bytes.NewBuffer([]byte{}) + Expect(ociClient.Fetch(context.TODO(), expectedImageRef, am.Config, buf)).To(Succeed()) + Expect(buf.Bytes()).To(Equal(configData1)) + + buf = bytes.NewBuffer([]byte{}) + Expect(ociClient.Fetch(context.TODO(), expectedImageRef, am.Layers[0], buf)).To(Succeed()) + Expect(buf.Bytes()).To(Equal(layers1[0])) + + buf = bytes.NewBuffer([]byte{}) + Expect(ociClient.Fetch(context.TODO(), expectedImageRef, actualOciArtifact.GetIndex().Manifests[1].Descriptor, buf)).To(Succeed()) + am = ocispecv1.Manifest{} + Expect(json.Unmarshal(buf.Bytes(), &am)).To(Succeed()) + Expect(am).To(Equal(*m2)) + + buf = bytes.NewBuffer([]byte{}) + Expect(ociClient.Fetch(context.TODO(), expectedImageRef, am.Config, buf)).To(Succeed()) + Expect(buf.Bytes()).To(Equal(configData2)) + + buf = bytes.NewBuffer([]byte{}) + Expect(ociClient.Fetch(context.TODO(), expectedImageRef, am.Layers[0], buf)).To(Succeed()) + Expect(buf.Bytes()).To(Equal(layers2[0])) }) It("should return error for invalid access type", func() { diff --git a/pkg/transport/process/utils/oci_artifact_serialization_test.go b/pkg/transport/process/utils/oci_artifact_serialization_test.go index fa8058b1..a65be7ae 100644 --- a/pkg/transport/process/utils/oci_artifact_serialization_test.go +++ b/pkg/transport/process/utils/oci_artifact_serialization_test.go @@ -13,6 +13,7 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/ociclient/oci" @@ -76,7 +77,16 @@ var _ = Describe("oci artifact serialization", func() { } m1, m1Desc := testutils.CreateManifest(configData1, layers1, nil) + m1Desc.Platform = &ocispecv1.Platform{ + Architecture: "amd64", + OS: "linux", + } + m2, m2Desc := testutils.CreateManifest(configData2, layers2, nil) + m2Desc.Platform = &ocispecv1.Platform{ + Architecture: "amd64", + OS: "windows", + } m1Bytes, err := json.Marshal(m1) Expect(err).ToNot(HaveOccurred()) From 7ea523dc0a697729aee424ed4d1738d4e618389e Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 24 Nov 2021 14:32:26 +0100 Subject: [PATCH 73/94] refactors oci uploader tests --- pkg/testutils/oci.go | 18 ++++ .../process/uploaders/oci_artifact_test.go | 82 +++++++------------ 2 files changed, 48 insertions(+), 52 deletions(-) diff --git a/pkg/testutils/oci.go b/pkg/testutils/oci.go index 29d4d76b..4933f454 100644 --- a/pkg/testutils/oci.go +++ b/pkg/testutils/oci.go @@ -199,3 +199,21 @@ func CreateManifest(configData []byte, layersData [][]byte, ocicache cache.Cache return &manifest, manifestDesc } + +func CompareRemoteManifest(client ociclient.Client, ref string, expectedManifest oci.Manifest, expectedCfgBytes []byte, expectedLayers [][]byte) { + buf := bytes.NewBuffer([]byte{}) + Expect(client.Fetch(context.TODO(), ref, expectedManifest.Descriptor, buf)).To(Succeed()) + manifestFromRemote := ocispecv1.Manifest{} + Expect(json.Unmarshal(buf.Bytes(), &manifestFromRemote)).To(Succeed()) + Expect(manifestFromRemote).To(Equal(*expectedManifest.Data)) + + buf = bytes.NewBuffer([]byte{}) + Expect(client.Fetch(context.TODO(), ref, manifestFromRemote.Config, buf)).To(Succeed()) + Expect(buf.Bytes()).To(Equal(expectedCfgBytes)) + + for i, layerDesc := range manifestFromRemote.Layers { + buf = bytes.NewBuffer([]byte{}) + Expect(client.Fetch(context.TODO(), ref, layerDesc, buf)).To(Succeed()) + Expect(buf.Bytes()).To(Equal(expectedLayers[i])) + } +} diff --git a/pkg/transport/process/uploaders/oci_artifact_test.go b/pkg/transport/process/uploaders/oci_artifact_test.go index b9c3b00d..7c74f416 100644 --- a/pkg/transport/process/uploaders/oci_artifact_test.go +++ b/pkg/transport/process/uploaders/oci_artifact_test.go @@ -53,11 +53,12 @@ var _ = Describe("ociArtifact", func() { layers := [][]byte{ []byte("layer-data"), } - m, _ := testutils.CreateManifest(configData, layers, nil) + m, mdesc := testutils.CreateManifest(configData, layers, nil) expectedOciArtifact, err := oci.NewManifestArtifact( &oci.Manifest{ - Data: m, + Descriptor: mdesc, + Data: m, }, ) Expect(err).ToNot(HaveOccurred()) @@ -95,21 +96,14 @@ var _ = Describe("ociArtifact", func() { actualOciArtifact, err := utils.DeserializeOCIArtifact(resBlobReader, cache.NewInMemoryCache()) Expect(err).ToNot(HaveOccurred()) - Expect(actualOciArtifact.GetManifest().Data).To(Equal(m)) - - buf := bytes.NewBuffer([]byte{}) - Expect(ociClient.Fetch(context.TODO(), expectedImageRef, actualOciArtifact.GetManifest().Descriptor, buf)).To(Succeed()) - am := ocispecv1.Manifest{} - Expect(json.Unmarshal(buf.Bytes(), &am)).To(Succeed()) - Expect(am).To(Equal(*m)) - - buf = bytes.NewBuffer([]byte{}) - Expect(ociClient.Fetch(context.TODO(), expectedImageRef, am.Config, buf)).To(Succeed()) - Expect(buf.Bytes()).To(Equal(configData)) - - buf = bytes.NewBuffer([]byte{}) - Expect(ociClient.Fetch(context.TODO(), expectedImageRef, am.Layers[0], buf)).To(Succeed()) - Expect(buf.Bytes()).To(Equal(layers[0])) + Expect(actualOciArtifact).To(Equal(expectedOciArtifact)) + testutils.CompareRemoteManifest( + ociClient, + expectedImageRef, + *expectedOciArtifact.GetManifest(), + configData, + layers, + ) }) It("should upload and stream oci image index", func() { @@ -167,10 +161,12 @@ var _ = Describe("ociArtifact", func() { &oci.Index{ Manifests: []*oci.Manifest{ { - Data: m1, + Descriptor: m1Desc, + Data: m1, }, { - Data: m2, + Descriptor: m2Desc, + Data: m2, }, }, Annotations: map[string]string{ @@ -217,39 +213,21 @@ var _ = Describe("ociArtifact", func() { actualOciArtifact, err := utils.DeserializeOCIArtifact(resBlobReader, cache.NewInMemoryCache()) Expect(err).ToNot(HaveOccurred()) - - // check image index and manifests - Expect(actualOciArtifact.GetIndex().Annotations).To(Equal(expectedOciArtifact.GetIndex().Annotations)) - Expect(actualOciArtifact.GetIndex().Manifests[0].Data).To(Equal(m1)) - Expect(actualOciArtifact.GetIndex().Manifests[1].Data).To(Equal(m2)) - - buf := bytes.NewBuffer([]byte{}) - Expect(ociClient.Fetch(context.TODO(), expectedImageRef, actualOciArtifact.GetIndex().Manifests[0].Descriptor, buf)).To(Succeed()) - am := ocispecv1.Manifest{} - Expect(json.Unmarshal(buf.Bytes(), &am)).To(Succeed()) - Expect(am).To(Equal(*m1)) - - buf = bytes.NewBuffer([]byte{}) - Expect(ociClient.Fetch(context.TODO(), expectedImageRef, am.Config, buf)).To(Succeed()) - Expect(buf.Bytes()).To(Equal(configData1)) - - buf = bytes.NewBuffer([]byte{}) - Expect(ociClient.Fetch(context.TODO(), expectedImageRef, am.Layers[0], buf)).To(Succeed()) - Expect(buf.Bytes()).To(Equal(layers1[0])) - - buf = bytes.NewBuffer([]byte{}) - Expect(ociClient.Fetch(context.TODO(), expectedImageRef, actualOciArtifact.GetIndex().Manifests[1].Descriptor, buf)).To(Succeed()) - am = ocispecv1.Manifest{} - Expect(json.Unmarshal(buf.Bytes(), &am)).To(Succeed()) - Expect(am).To(Equal(*m2)) - - buf = bytes.NewBuffer([]byte{}) - Expect(ociClient.Fetch(context.TODO(), expectedImageRef, am.Config, buf)).To(Succeed()) - Expect(buf.Bytes()).To(Equal(configData2)) - - buf = bytes.NewBuffer([]byte{}) - Expect(ociClient.Fetch(context.TODO(), expectedImageRef, am.Layers[0], buf)).To(Succeed()) - Expect(buf.Bytes()).To(Equal(layers2[0])) + Expect(actualOciArtifact).To(Equal(expectedOciArtifact)) + testutils.CompareRemoteManifest( + ociClient, + expectedImageRef, + *expectedOciArtifact.GetIndex().Manifests[0], + configData1, + layers1, + ) + testutils.CompareRemoteManifest( + ociClient, + expectedImageRef, + *expectedOciArtifact.GetIndex().Manifests[1], + configData2, + layers2, + ) }) It("should return error for invalid access type", func() { From 9b24bd442bbac493a234ed63c36c17da9fa257a6 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 24 Nov 2021 15:32:27 +0100 Subject: [PATCH 74/94] refactors test coding --- ociclient/client_test.go | 13 ++--- pkg/testutils/oci.go | 52 ++++++------------- pkg/testutils/tar.go | 10 ++-- .../process/downloaders/oci_artifact_test.go | 2 +- 4 files changed, 29 insertions(+), 48 deletions(-) diff --git a/ociclient/client_test.go b/ociclient/client_test.go index 1bbb8c99..a63894fe 100644 --- a/ociclient/client_test.go +++ b/ociclient/client_test.go @@ -57,7 +57,7 @@ var _ = Describe("client", func() { Expect(actualArtifact.IsManifest()).To(BeFalse()) Expect(actualArtifact.IsIndex()).To(BeTrue()) - testutils.CompareImageIndices(actualArtifact.GetIndex(), index) + Expect(actualArtifact.GetIndex()).To(Equal(index)) }, 20) It("should push and pull an empty oci image index", func() { @@ -83,7 +83,7 @@ var _ = Describe("client", func() { Expect(actualArtifact.IsManifest()).To(BeFalse()) Expect(actualArtifact.IsIndex()).To(BeTrue()) - testutils.CompareImageIndices(actualArtifact.GetIndex(), &index) + Expect(actualArtifact.GetIndex()).To(Equal(&index)) }, 20) It("should push and pull an oci image index with only 1 manifest and no platform information", func() { @@ -92,13 +92,14 @@ var _ = Describe("client", func() { ref := testenv.Addr + "/image-index/3/img:v0.0.1" manifest1Ref := testenv.Addr + "/image-index/1/img-platform-1:v0.0.1" - manifest, _, err := testutils.UploadTestManifest(ctx, client, manifest1Ref) + manifest, mdesc, err := testutils.UploadTestManifest(ctx, client, manifest1Ref) Expect(err).ToNot(HaveOccurred()) index := oci.Index{ Manifests: []*oci.Manifest{ { - Data: manifest, + Descriptor: mdesc, + Data: manifest, }, }, Annotations: map[string]string{ @@ -117,7 +118,7 @@ var _ = Describe("client", func() { Expect(actualArtifact.IsManifest()).To(BeFalse()) Expect(actualArtifact.IsIndex()).To(BeTrue()) - testutils.CompareImageIndices(actualArtifact.GetIndex(), &index) + Expect(actualArtifact.GetIndex()).To(Equal(&index)) }, 20) It("should copy an oci artifact", func() { @@ -161,7 +162,7 @@ var _ = Describe("client", func() { Expect(actualArtifact.IsManifest()).To(BeFalse()) Expect(actualArtifact.IsIndex()).To(BeTrue()) - testutils.CompareImageIndices(actualArtifact.GetIndex(), index) + Expect(actualArtifact.GetIndex()).To(Equal(index)) for _, manifest := range actualArtifact.GetIndex().Manifests { testutils.CompareManifestToTestManifest(ctx, client, newRef, manifest.Data) diff --git a/pkg/testutils/oci.go b/pkg/testutils/oci.go index 4933f454..195e15e5 100644 --- a/pkg/testutils/oci.go +++ b/pkg/testutils/oci.go @@ -85,35 +85,33 @@ func UploadTestIndex(ctx context.Context, client ociclient.Client, indexRef stri manifest1Ref := fmt.Sprintf("%s-platform-1:%s", indexRepo, tag) manifest2Ref := fmt.Sprintf("%s-platform-2:%s", indexRepo, tag) - manifest1, _, err := UploadTestManifest(ctx, client, manifest1Ref) + manifest1, mdesc1, err := UploadTestManifest(ctx, client, manifest1Ref) if err != nil { return nil, err } + mdesc1.Platform = &ocispecv1.Platform{ + Architecture: "amd64", + OS: "linux", + } - manifest2, _, err := UploadTestManifest(ctx, client, manifest2Ref) + manifest2, mdesc2, err := UploadTestManifest(ctx, client, manifest2Ref) if err != nil { return nil, err } + mdesc2.Platform = &ocispecv1.Platform{ + Architecture: "amd64", + OS: "windows", + } index := oci.Index{ Manifests: []*oci.Manifest{ { - Descriptor: ocispecv1.Descriptor{ - Platform: &ocispecv1.Platform{ - Architecture: "amd64", - OS: "linux", - }, - }, - Data: manifest1, + Descriptor: mdesc1, + Data: manifest1, }, { - Descriptor: ocispecv1.Descriptor{ - Platform: &ocispecv1.Platform{ - Architecture: "amd64", - OS: "windows", - }, - }, - Data: manifest2, + Descriptor: mdesc2, + Data: manifest2, }, }, Annotations: map[string]string{ @@ -133,32 +131,13 @@ func UploadTestIndex(ctx context.Context, client ociclient.Client, indexRef stri return &index, nil } -func CompareImageIndices(actualIndex *oci.Index, expectedIndex *oci.Index) { - Expect(actualIndex.Annotations).To(Equal(expectedIndex.Annotations)) - Expect(len(actualIndex.Manifests)).To(Equal(len(expectedIndex.Manifests))) - - for i := 0; i < len(actualIndex.Manifests); i++ { - actualManifest := actualIndex.Manifests[i] - expectedManifest := expectedIndex.Manifests[i] - - expectedManifestBytes, err := json.Marshal(expectedManifest.Data) - Expect(err).ToNot(HaveOccurred()) - - Expect(actualManifest.Descriptor.MediaType).To(Equal(ocispecv1.MediaTypeImageManifest)) - Expect(actualManifest.Descriptor.Digest).To(Equal(digest.FromBytes(expectedManifestBytes))) - Expect(actualManifest.Descriptor.Size).To(Equal(int64(len(expectedManifestBytes)))) - Expect(actualManifest.Descriptor.Platform).To(Equal(expectedManifest.Descriptor.Platform)) - Expect(actualManifest.Data).To(Equal(expectedManifest.Data)) - } -} - +// CreateManifest creates an oci manifest. if ocicache is set, all blobs are added to cache func CreateManifest(configData []byte, layersData [][]byte, ocicache cache.Cache) (*ocispecv1.Manifest, ocispecv1.Descriptor) { configDesc := ocispecv1.Descriptor{ MediaType: "text/plain", Digest: digest.FromBytes(configData), Size: int64(len(configData)), } - if ocicache != nil { Expect(ocicache.Add(configDesc, io.NopCloser(bytes.NewReader(configData)))).To(Succeed()) } @@ -171,7 +150,6 @@ func CreateManifest(configData []byte, layersData [][]byte, ocicache cache.Cache Size: int64(len(layerData)), } layerDescs = append(layerDescs, layerDesc) - if ocicache != nil { Expect(ocicache.Add(layerDesc, io.NopCloser(bytes.NewReader(layerData)))).To(Succeed()) } diff --git a/pkg/testutils/tar.go b/pkg/testutils/tar.go index 80b8b358..927dadec 100644 --- a/pkg/testutils/tar.go +++ b/pkg/testutils/tar.go @@ -13,6 +13,7 @@ import ( . "github.com/onsi/gomega" ) +// CreateTARArchive creates a TAR archive which contains the defined set of files func CreateTARArchive(files map[string][]byte) *bytes.Buffer { buf := bytes.NewBuffer([]byte{}) tw := tar.NewWriter(buf) @@ -34,8 +35,9 @@ func CreateTARArchive(files map[string][]byte) *bytes.Buffer { return buf } -func CheckTARArchive(r io.Reader, expectedFiles map[string][]byte) { - tr := tar.NewReader(r) +// CheckTARArchive checks that a TAR archive contains exactly a defined set of files +func CheckTARArchive(archiveReader io.Reader, expectedFiles map[string][]byte) { + tr := tar.NewReader(archiveReader) expectedFilesCopy := map[string][]byte{} for key, value := range expectedFiles { @@ -55,12 +57,12 @@ func CheckTARArchive(r io.Reader, expectedFiles map[string][]byte) { _, err = io.Copy(actualContentBuf, tr) Expect(err).ToNot(HaveOccurred()) - expectedContent, ok := expectedFiles[header.Name] + expectedContent, ok := expectedFilesCopy[header.Name] Expect(ok).To(BeTrue(), fmt.Sprintf("file \"%s\" is not included in expected files", header.Name)) Expect(actualContentBuf.Bytes()).To(Equal(expectedContent)) delete(expectedFilesCopy, header.Name) } - Expect(expectedFilesCopy).To(BeEmpty(), fmt.Sprintf("unable to find all expected files in TAR archive. missing files = %+v", expectedFiles)) + Expect(expectedFilesCopy).To(BeEmpty(), fmt.Sprintf("unable to find all expected files in TAR archive. missing files = %+v", expectedFilesCopy)) } diff --git a/pkg/transport/process/downloaders/oci_artifact_test.go b/pkg/transport/process/downloaders/oci_artifact_test.go index 6579edd6..6db192b2 100644 --- a/pkg/transport/process/downloaders/oci_artifact_test.go +++ b/pkg/transport/process/downloaders/oci_artifact_test.go @@ -69,7 +69,7 @@ var _ = Describe("ociArtifact", func() { actualOciArtifact, err := utils.DeserializeOCIArtifact(resBlobReader, ociCache) Expect(err).ToNot(HaveOccurred()) - testutils.CompareImageIndices(actualOciArtifact.GetIndex(), &expectedImageIndex) + Expect(actualOciArtifact.GetIndex()).To(Equal(&expectedImageIndex)) }) It("should return error if called with resource of invalid type", func() { From 2690218ecf1ca8ca28d12274a1a16b590f29d059 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 24 Nov 2021 16:13:39 +0100 Subject: [PATCH 75/94] refactors test coding --- ociclient/client_test.go | 30 +++++++++++++++---- pkg/testutils/oci.go | 14 ++------- pkg/testutils/tar.go | 4 +-- .../downloaders/downloaders_suite_test.go | 14 ++++++++- .../process/downloaders/oci_artifact_test.go | 10 ++++++- 5 files changed, 52 insertions(+), 20 deletions(-) diff --git a/ociclient/client_test.go b/ociclient/client_test.go index a63894fe..8642b37d 100644 --- a/ociclient/client_test.go +++ b/ociclient/client_test.go @@ -32,7 +32,7 @@ var _ = Describe("client", func() { defer ctx.Done() ref := testenv.Addr + "/test/artifact:v0.0.1" - manifest, _, err := testutils.UploadTestManifest(ctx, client, ref) + manifest, mdesc, err := testutils.UploadTestManifest(ctx, client, ref) Expect(err).ToNot(HaveOccurred()) res, err := client.GetManifest(ctx, ref) @@ -41,7 +41,19 @@ var _ = Describe("client", func() { Expect(res.Layers).To(Equal(manifest.Layers)) // TODO: oci image index test only working because cache is filled in this function with config/layer blobs. should be fixed - testutils.CompareManifestToTestManifest(ctx, client, ref, res) + expectedManifest := oci.Manifest{ + Descriptor: mdesc, + Data: manifest, + } + testutils.CompareRemoteManifest( + client, + ref, + expectedManifest, + []byte("config-data"), + [][]byte{ + []byte("layer-data"), + }, + ) }, 20) It("should push and pull an oci image index", func() { @@ -139,7 +151,7 @@ var _ = Describe("client", func() { var configBlob bytes.Buffer Expect(client.Fetch(ctx, ref, res.Config, &configBlob)).To(Succeed()) - Expect(configBlob.String()).To(Equal("test")) + Expect(configBlob.String()).To(Equal("config-data")) var layerBlob bytes.Buffer Expect(client.Fetch(ctx, ref, res.Layers[0], &layerBlob)).To(Succeed()) @@ -164,8 +176,16 @@ var _ = Describe("client", func() { Expect(actualArtifact.IsIndex()).To(BeTrue()) Expect(actualArtifact.GetIndex()).To(Equal(index)) - for _, manifest := range actualArtifact.GetIndex().Manifests { - testutils.CompareManifestToTestManifest(ctx, client, newRef, manifest.Data) + for i := range actualArtifact.GetIndex().Manifests { + testutils.CompareRemoteManifest( + client, + ref, + *index.Manifests[i], + []byte("config-data"), + [][]byte{ + []byte("layer-data"), + }, + ) } }, 20) diff --git a/pkg/testutils/oci.go b/pkg/testutils/oci.go index 195e15e5..b65953aa 100644 --- a/pkg/testutils/oci.go +++ b/pkg/testutils/oci.go @@ -21,8 +21,9 @@ import ( "github.com/gardener/component-cli/ociclient/oci" ) +// UploadTestManifest uploads an oci image manifest to a registry func UploadTestManifest(ctx context.Context, client ociclient.Client, ref string) (*ocispecv1.Manifest, ocispecv1.Descriptor, error) { - configData := []byte("test") + configData := []byte("config-data") layerData := []byte("layer-data") manifest := &ocispecv1.Manifest{ Config: ocispecv1.Descriptor{ @@ -67,16 +68,7 @@ func UploadTestManifest(ctx context.Context, client ociclient.Client, ref string return manifest, desc, nil } -func CompareManifestToTestManifest(ctx context.Context, client ociclient.Client, ref string, manifest *ocispecv1.Manifest) { - var configBlob bytes.Buffer - Expect(client.Fetch(ctx, ref, manifest.Config, &configBlob)).To(Succeed()) - Expect(configBlob.String()).To(Equal("test")) - - var layerBlob bytes.Buffer - Expect(client.Fetch(ctx, ref, manifest.Layers[0], &layerBlob)).To(Succeed()) - Expect(layerBlob.String()).To(Equal("layer-data")) -} - +// UploadTestIndex uploads an oci image index to a registry func UploadTestIndex(ctx context.Context, client ociclient.Client, indexRef string) (*oci.Index, error) { splitted := strings.Split(indexRef, ":") indexRepo := strings.Join(splitted[0:len(splitted)-1], ":") diff --git a/pkg/testutils/tar.go b/pkg/testutils/tar.go index 927dadec..dbcc4a4b 100644 --- a/pkg/testutils/tar.go +++ b/pkg/testutils/tar.go @@ -13,7 +13,7 @@ import ( . "github.com/onsi/gomega" ) -// CreateTARArchive creates a TAR archive which contains the defined set of files +// CreateTARArchive creates a TAR archive which contains a defined set of files func CreateTARArchive(files map[string][]byte) *bytes.Buffer { buf := bytes.NewBuffer([]byte{}) tw := tar.NewWriter(buf) @@ -35,7 +35,7 @@ func CreateTARArchive(files map[string][]byte) *bytes.Buffer { return buf } -// CheckTARArchive checks that a TAR archive contains exactly a defined set of files +// CheckTARArchive checks that a TAR archive contains a defined set of files func CheckTARArchive(archiveReader io.Reader, expectedFiles map[string][]byte) { tr := tar.NewReader(archiveReader) diff --git a/pkg/transport/process/downloaders/downloaders_suite_test.go b/pkg/transport/process/downloaders/downloaders_suite_test.go index b1a767e1..74266f32 100644 --- a/pkg/transport/process/downloaders/downloaders_suite_test.go +++ b/pkg/transport/process/downloaders/downloaders_suite_test.go @@ -160,7 +160,19 @@ func createImageRes(ctx context.Context) cdv2.Resource { Expect(err).ToNot(HaveOccurred()) // TODO: currently needed to fill the cache. remove from test, also from ociclient unit test - testutils.CompareManifestToTestManifest(context.TODO(), ociClient, imageRef, manifest) + m := oci.Manifest{ + Descriptor: desc, + Data: manifest, + } + testutils.CompareRemoteManifest( + ociClient, + imageRef, + m, + []byte("config-data"), + [][]byte{ + []byte("layer-data"), + }, + ) expectedImageManifest = oci.Manifest{ Descriptor: desc, diff --git a/pkg/transport/process/downloaders/oci_artifact_test.go b/pkg/transport/process/downloaders/oci_artifact_test.go index 6db192b2..789e3f5d 100644 --- a/pkg/transport/process/downloaders/oci_artifact_test.go +++ b/pkg/transport/process/downloaders/oci_artifact_test.go @@ -43,7 +43,15 @@ var _ = Describe("ociArtifact", func() { actualOciArtifact, err := utils.DeserializeOCIArtifact(resBlobReader, ociCache) Expect(err).ToNot(HaveOccurred()) Expect(*actualOciArtifact.GetManifest()).To(Equal(expectedImageManifest)) - testutils.CompareManifestToTestManifest(context.TODO(), ociClient, imageRef, expectedImageManifest.Data) + testutils.CompareRemoteManifest( + ociClient, + imageRef, + expectedImageManifest, + []byte("config-data"), + [][]byte{ + []byte("layer-data"), + }, + ) }) It("should download and stream oci image index", func() { From a29600058b816717f8a93424aa98c3b691eb89b1 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Mon, 29 Nov 2021 11:58:56 +0100 Subject: [PATCH 76/94] renames uds to unix domain socket --- .../extensions/extensions_suite_test.go | 10 +++++----- ...ble.go => unix_domain_socket_executable.go} | 10 +++++----- .../process/processors/example/main.go | 2 +- pkg/transport/process/processors/sleep/main.go | 2 +- ..._server.go => unix_domain_socket_server.go} | 18 +++++++++--------- 5 files changed, 21 insertions(+), 21 deletions(-) rename pkg/transport/process/extensions/{uds_executable.go => unix_domain_socket_executable.go} (86%) rename pkg/transport/process/utils/{uds_server.go => unix_domain_socket_server.go} (70%) diff --git a/pkg/transport/process/extensions/extensions_suite_test.go b/pkg/transport/process/extensions/extensions_suite_test.go index 9ea2ce7b..68f3cda4 100644 --- a/pkg/transport/process/extensions/extensions_suite_test.go +++ b/pkg/transport/process/extensions/extensions_suite_test.go @@ -73,17 +73,17 @@ var _ = Describe("transport extensions", func() { }) }) - Context("uds executable", func() { + Context("unix domain socket executable", func() { It("should create processor successfully if env is nil", func() { args := []string{} - _, err := extensions.NewUDSExecutable(exampleProcessorBinaryPath, args, nil) + _, err := extensions.NewUnixDomainSocketExecutable(exampleProcessorBinaryPath, args, nil) Expect(err).ToNot(HaveOccurred()) }) It("should modify the processed resource correctly", func() { args := []string{} env := map[string]string{} - processor, err := extensions.NewUDSExecutable(exampleProcessorBinaryPath, args, env) + processor, err := extensions.NewUnixDomainSocketExecutable(exampleProcessorBinaryPath, args, env) Expect(err).ToNot(HaveOccurred()) runExampleResourceTest(processor) @@ -94,7 +94,7 @@ var _ = Describe("transport extensions", func() { env := map[string]string{ extensions.ServerAddressEnv: "/tmp/my-processor.sock", } - _, err := extensions.NewUDSExecutable(exampleProcessorBinaryPath, args, env) + _, err := extensions.NewUnixDomainSocketExecutable(exampleProcessorBinaryPath, args, env) Expect(err).To(MatchError(fmt.Sprintf("the env variable %s is not allowed to be set manually", extensions.ServerAddressEnv))) }) @@ -103,7 +103,7 @@ var _ = Describe("transport extensions", func() { env := map[string]string{ sleepTimeEnv: sleepTime.String(), } - processor, err := extensions.NewUDSExecutable(sleepProcessorBinaryPath, args, env) + processor, err := extensions.NewUnixDomainSocketExecutable(sleepProcessorBinaryPath, args, env) Expect(err).ToNot(HaveOccurred()) runTimeoutTest(processor) diff --git a/pkg/transport/process/extensions/uds_executable.go b/pkg/transport/process/extensions/unix_domain_socket_executable.go similarity index 86% rename from pkg/transport/process/extensions/uds_executable.go rename to pkg/transport/process/extensions/unix_domain_socket_executable.go index 6c14148d..63446099 100644 --- a/pkg/transport/process/extensions/uds_executable.go +++ b/pkg/transport/process/extensions/unix_domain_socket_executable.go @@ -21,16 +21,16 @@ import ( // address under which a resource processor server should start. const ServerAddressEnv = "SERVER_ADDRESS" -type udsExecutable struct { +type unixDomainSocketExecutable struct { bin string args []string env []string addr string } -// NewUDSExecutable runs a resource processor extension executable in the background. +// NewUnixDomainSocketExecutable runs a resource processor extension executable in the background. // It communicates with this processor via Unix Domain Sockets. -func NewUDSExecutable(bin string, args []string, env map[string]string) (process.ResourceStreamProcessor, error) { +func NewUnixDomainSocketExecutable(bin string, args []string, env map[string]string) (process.ResourceStreamProcessor, error) { if _, ok := env[ServerAddressEnv]; ok { return nil, fmt.Errorf("the env variable %s is not allowed to be set manually", ServerAddressEnv) } @@ -47,7 +47,7 @@ func NewUDSExecutable(bin string, args []string, env map[string]string) (process addr := fmt.Sprintf("%s/%s.sock", wd, utils.RandomString(8)) parsedEnv = append(parsedEnv, fmt.Sprintf("%s=%s", ServerAddressEnv, addr)) - e := udsExecutable{ + e := unixDomainSocketExecutable{ bin: bin, args: args, env: parsedEnv, @@ -57,7 +57,7 @@ func NewUDSExecutable(bin string, args []string, env map[string]string) (process return &e, nil } -func (e *udsExecutable) Process(ctx context.Context, r io.Reader, w io.Writer) error { +func (e *unixDomainSocketExecutable) Process(ctx context.Context, r io.Reader, w io.Writer) error { cmd := exec.CommandContext(ctx, e.bin, e.args...) cmd.Env = e.env cmd.Stdout = os.Stdout diff --git a/pkg/transport/process/processors/example/main.go b/pkg/transport/process/processors/example/main.go index 2bb26490..de1c758f 100644 --- a/pkg/transport/process/processors/example/main.go +++ b/pkg/transport/process/processors/example/main.go @@ -42,7 +42,7 @@ func main() { } } - srv, err := utils.NewUDSServer(addr, h) + srv, err := utils.NewUnixDomainSocketServer(addr, h) if err != nil { log.Fatal(err) } diff --git a/pkg/transport/process/processors/sleep/main.go b/pkg/transport/process/processors/sleep/main.go index 7be914c7..0ed1a801 100644 --- a/pkg/transport/process/processors/sleep/main.go +++ b/pkg/transport/process/processors/sleep/main.go @@ -36,7 +36,7 @@ func main() { log.Fatal("finished sleeping -> exit with error") } - srv, err := utils.NewUDSServer(addr, h) + srv, err := utils.NewUnixDomainSocketServer(addr, h) if err != nil { log.Fatal(err) } diff --git a/pkg/transport/process/utils/uds_server.go b/pkg/transport/process/utils/unix_domain_socket_server.go similarity index 70% rename from pkg/transport/process/utils/uds_server.go rename to pkg/transport/process/utils/unix_domain_socket_server.go index 4f88c865..5d10edfe 100644 --- a/pkg/transport/process/utils/uds_server.go +++ b/pkg/transport/process/utils/unix_domain_socket_server.go @@ -10,25 +10,25 @@ import ( "sync" ) -// HandlerFunc defines the interface of a function that should be served by a UDS server +// HandlerFunc defines the interface of a function that should be served by a Unix Domain Socket server type HandlerFunc func(io.Reader, io.WriteCloser) -// UDSServer implements a Unix Domain Socket server -type UDSServer struct { +// UnixDomainSocketServer implements a Unix Domain Socket server +type UnixDomainSocketServer struct { listener net.Listener quit chan interface{} wg sync.WaitGroup handler HandlerFunc } -// NewUDSServer returns a new UDS server. +// NewUnixDomainSocketServer returns a new Unix Domain Socket server. // The parameters define the server address and the handler func it serves -func NewUDSServer(addr string, handler HandlerFunc) (*UDSServer, error) { +func NewUnixDomainSocketServer(addr string, handler HandlerFunc) (*UnixDomainSocketServer, error) { l, err := net.Listen("unix", addr) if err != nil { return nil, err } - s := &UDSServer{ + s := &UnixDomainSocketServer{ quit: make(chan interface{}), listener: l, handler: handler, @@ -37,12 +37,12 @@ func NewUDSServer(addr string, handler HandlerFunc) (*UDSServer, error) { } // Start starts the server goroutine -func (s *UDSServer) Start() { +func (s *UnixDomainSocketServer) Start() { s.wg.Add(1) go s.serve() } -func (s *UDSServer) serve() { +func (s *UnixDomainSocketServer) serve() { defer s.wg.Done() for { @@ -65,7 +65,7 @@ func (s *UDSServer) serve() { } // Stop stops the server goroutine -func (s *UDSServer) Stop() { +func (s *UnixDomainSocketServer) Stop() { close(s.quit) if err := s.listener.Close(); err != nil { println(err) From 9dc8e97d5a279a41321fd91765028779ce5091e2 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Mon, 29 Nov 2021 16:11:22 +0100 Subject: [PATCH 77/94] review feedback --- .../process/extensions/extensions_suite_test.go | 4 ++-- .../process/extensions/stdio_executable.go | 4 ++-- .../extensions/unix_domain_socket_executable.go | 14 +++++++------- pkg/transport/process/pipeline.go | 2 +- pkg/transport/process/processors/example/main.go | 9 ++++++++- pkg/transport/process/processors/sleep/main.go | 2 +- pkg/transport/process/utils/processor_message.go | 2 +- 7 files changed, 22 insertions(+), 15 deletions(-) diff --git a/pkg/transport/process/extensions/extensions_suite_test.go b/pkg/transport/process/extensions/extensions_suite_test.go index 68f3cda4..0d904f4c 100644 --- a/pkg/transport/process/extensions/extensions_suite_test.go +++ b/pkg/transport/process/extensions/extensions_suite_test.go @@ -92,10 +92,10 @@ var _ = Describe("transport extensions", func() { It("should raise an error when trying to set the server address env variable manually", func() { args := []string{} env := map[string]string{ - extensions.ServerAddressEnv: "/tmp/my-processor.sock", + extensions.ProcessorServerAddressEnv: "/tmp/my-processor.sock", } _, err := extensions.NewUnixDomainSocketExecutable(exampleProcessorBinaryPath, args, env) - Expect(err).To(MatchError(fmt.Sprintf("the env variable %s is not allowed to be set manually", extensions.ServerAddressEnv))) + Expect(err).To(MatchError(fmt.Sprintf("the env variable %s is not allowed to be set manually", extensions.ProcessorServerAddressEnv))) }) It("should exit with error when timeout is reached", func() { diff --git a/pkg/transport/process/extensions/stdio_executable.go b/pkg/transport/process/extensions/stdio_executable.go index a85a8283..18ef96c2 100644 --- a/pkg/transport/process/extensions/stdio_executable.go +++ b/pkg/transport/process/extensions/stdio_executable.go @@ -19,8 +19,8 @@ type stdIOExecutable struct { env []string } -// NewStdIOExecutable returns a resource processor extension which runs an executable. -// in the background. It communicates with this processor via stdin/stdout pipes. +// NewStdIOExecutable returns a resource processor extension which runs an executable in the +// background when calling Process(). It communicates with this processor via stdin/stdout pipes. func NewStdIOExecutable(bin string, args []string, env map[string]string) (process.ResourceStreamProcessor, error) { parsedEnv := []string{} for k, v := range env { diff --git a/pkg/transport/process/extensions/unix_domain_socket_executable.go b/pkg/transport/process/extensions/unix_domain_socket_executable.go index 63446099..5a1241a4 100644 --- a/pkg/transport/process/extensions/unix_domain_socket_executable.go +++ b/pkg/transport/process/extensions/unix_domain_socket_executable.go @@ -17,9 +17,9 @@ import ( "github.com/gardener/component-cli/pkg/utils" ) -// ServerAddressEnv is the environment variable key which is used to store the +// ProcessorServerAddressEnv is the environment variable key which is used to store the // address under which a resource processor server should start. -const ServerAddressEnv = "SERVER_ADDRESS" +const ProcessorServerAddressEnv = "PROCESSOR_SERVER_ADDRESS" type unixDomainSocketExecutable struct { bin string @@ -28,11 +28,11 @@ type unixDomainSocketExecutable struct { addr string } -// NewUnixDomainSocketExecutable runs a resource processor extension executable in the background. -// It communicates with this processor via Unix Domain Sockets. +// NewUnixDomainSocketExecutable returns a resource processor extension which runs an executable in the +// background when calling Process(). It communicates with this processor via Unix Domain Sockets. func NewUnixDomainSocketExecutable(bin string, args []string, env map[string]string) (process.ResourceStreamProcessor, error) { - if _, ok := env[ServerAddressEnv]; ok { - return nil, fmt.Errorf("the env variable %s is not allowed to be set manually", ServerAddressEnv) + if _, ok := env[ProcessorServerAddressEnv]; ok { + return nil, fmt.Errorf("the env variable %s is not allowed to be set manually", ProcessorServerAddressEnv) } parsedEnv := []string{} @@ -45,7 +45,7 @@ func NewUnixDomainSocketExecutable(bin string, args []string, env map[string]str return nil, err } addr := fmt.Sprintf("%s/%s.sock", wd, utils.RandomString(8)) - parsedEnv = append(parsedEnv, fmt.Sprintf("%s=%s", ServerAddressEnv, addr)) + parsedEnv = append(parsedEnv, fmt.Sprintf("%s=%s", ProcessorServerAddressEnv, addr)) e := unixDomainSocketExecutable{ bin: bin, diff --git a/pkg/transport/process/pipeline.go b/pkg/transport/process/pipeline.go index 77861dac..98caafba 100644 --- a/pkg/transport/process/pipeline.go +++ b/pkg/transport/process/pipeline.go @@ -44,7 +44,7 @@ func (p *resourceProcessingPipelineImpl) Process(ctx context.Context, cd cdv2.Co defer infile.Close() if _, err := infile.Seek(0, io.SeekStart); err != nil { - return nil, cdv2.Resource{}, err + return nil, cdv2.Resource{}, fmt.Errorf("unable to seek to beginning of input file: %w", err) } processedCD, processedRes, blobreader, err := utils.ReadProcessorMessage(infile) diff --git a/pkg/transport/process/processors/example/main.go b/pkg/transport/process/processors/example/main.go index de1c758f..c2b0dcab 100644 --- a/pkg/transport/process/processors/example/main.go +++ b/pkg/transport/process/processors/example/main.go @@ -26,7 +26,8 @@ const processorName = "example-processor" // a test processor which adds its name to the resource labels and the resource blob. // the resource blob is expected to be plain text data. func main() { - addr := os.Getenv(extensions.ServerAddressEnv) + // read the address under which the unix domain socket server should start + addr := os.Getenv(extensions.ProcessorServerAddressEnv) if addr == "" { // if addr is not set, use stdin/stdout for communication @@ -35,6 +36,7 @@ func main() { } return } + // if addr is set, use unix domain sockets for communication h := func(r io.Reader, w io.WriteCloser) { if err := processorRoutine(r, w); err != nil { @@ -65,6 +67,7 @@ func processorRoutine(inputStream io.Reader, outputStream io.WriteCloser) error } defer tmpfile.Close() + // read the input stream if _, err := io.Copy(tmpfile, inputStream); err != nil { return err } @@ -73,6 +76,7 @@ func processorRoutine(inputStream io.Reader, outputStream io.WriteCloser) error return err } + // split up the input stream into component descriptor, resource, and resource blob cd, res, resourceBlobReader, err := utils.ReadProcessorMessage(tmpfile) if err != nil { return err @@ -81,18 +85,21 @@ func processorRoutine(inputStream io.Reader, outputStream io.WriteCloser) error defer resourceBlobReader.Close() } + // modify resource blob buf := bytes.NewBuffer([]byte{}) if _, err := io.Copy(buf, resourceBlobReader); err != nil { return err } outputData := fmt.Sprintf("%s\n%s", buf.String(), processorName) + // modify resource yaml l := cdv2.Label{ Name: "processor-name", Value: json.RawMessage(`"` + processorName + `"`), } res.Labels = append(res.Labels, l) + // write modified output to output stream if err := utils.WriteProcessorMessage(*cd, res, strings.NewReader(outputData), outputStream); err != nil { return err } diff --git a/pkg/transport/process/processors/sleep/main.go b/pkg/transport/process/processors/sleep/main.go index 0ed1a801..54aefa35 100644 --- a/pkg/transport/process/processors/sleep/main.go +++ b/pkg/transport/process/processors/sleep/main.go @@ -24,7 +24,7 @@ func main() { log.Fatal(err) } - addr := os.Getenv(extensions.ServerAddressEnv) + addr := os.Getenv(extensions.ProcessorServerAddressEnv) if addr == "" { time.Sleep(sleepTime) diff --git a/pkg/transport/process/utils/processor_message.go b/pkg/transport/process/utils/processor_message.go index 2f52cb7b..38500ee3 100644 --- a/pkg/transport/process/utils/processor_message.go +++ b/pkg/transport/process/utils/processor_message.go @@ -106,7 +106,7 @@ func ReadProcessorMessage(r io.Reader) (*cdv2.ComponentDescriptor, cdv2.Resource } if _, err := f.Seek(0, io.SeekStart); err != nil { - return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to seek to beginning of file: %w", err) + return nil, cdv2.Resource{}, nil, fmt.Errorf("unable to seek to beginning of resource blob file: %w", err) } return cd, res, f, nil From 6d0d6d2d53d0ec9d86035e38193db5cd25da64d8 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Mon, 29 Nov 2021 16:59:41 +0100 Subject: [PATCH 78/94] remove unix domain socket file after processor finished --- .../process/extensions/unix_domain_socket_executable.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/transport/process/extensions/unix_domain_socket_executable.go b/pkg/transport/process/extensions/unix_domain_socket_executable.go index 5a1241a4..9e6f3864 100644 --- a/pkg/transport/process/extensions/unix_domain_socket_executable.go +++ b/pkg/transport/process/extensions/unix_domain_socket_executable.go @@ -94,6 +94,15 @@ func (e *unixDomainSocketExecutable) Process(ctx context.Context, r io.Reader, w return fmt.Errorf("unable to wait for processor: %w", err) } + // remove socket file if server hasn't already cleaned up + if _, err := os.Stat(e.addr); err == nil { + if err := os.Remove(e.addr); err != nil { + return fmt.Errorf("unable to remove %s: %w", e.addr, err) + } + } else if !os.IsNotExist(err) { + return fmt.Errorf("unable to get file stats for %s: %w", e.addr, err) + } + return nil } From 38acc5bb7b1e69348c2c077991536fecb283a31f Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 30 Nov 2021 09:52:47 +0100 Subject: [PATCH 79/94] fix compile error --- pkg/transport/config/util.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/transport/config/util.go b/pkg/transport/config/util.go index a4605950..45cf9ebb 100644 --- a/pkg/transport/config/util.go +++ b/pkg/transport/config/util.go @@ -29,5 +29,5 @@ func createExecutable(rawSpec *json.RawMessage) (process.ResourceStreamProcessor return nil, fmt.Errorf("unable to parse spec: %w", err) } - return extensions.NewUDSExecutable(spec.Bin, spec.Args, spec.Env) + return extensions.NewUnixDomainSocketExecutable(spec.Bin, spec.Args, spec.Env) } From 4f49aa6d6f37df665023bc19944123882ffa22ce Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 30 Nov 2021 09:53:14 +0100 Subject: [PATCH 80/94] increase processor timeout --- pkg/transport/process/pipeline.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/transport/process/pipeline.go b/pkg/transport/process/pipeline.go index 5a2329a2..5d92e910 100644 --- a/pkg/transport/process/pipeline.go +++ b/pkg/transport/process/pipeline.go @@ -16,7 +16,7 @@ import ( "github.com/gardener/component-cli/pkg/transport/process/utils" ) -const processorTimeout = 60 * time.Second +const processorTimeout = 600 * time.Second type resourceProcessingPipelineImpl struct { processors []ResourceStreamProcessor From 6dea353085d0efa7efeed79df8deab3992c9aa8f Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 1 Dec 2021 15:54:47 +0100 Subject: [PATCH 81/94] wip --- pkg/commands/transport/transport.go | 60 +++++++------ pkg/transport/config/config_suite_test.go | 9 +- pkg/transport/config/filter_factory.go | 39 +++------ pkg/transport/config/processing_job.go | 31 ++++--- pkg/transport/config/testdata/transport.cfg | 4 +- .../filters/component_name_filter.go | 10 ++- pkg/transport/filters/filters_suite_test.go | 83 ++++++++++++++---- .../filters/resource_access_type_filter.go | 18 ++-- pkg/transport/filters/resource_type_filter.go | 10 ++- pkg/utils/repo_ctx_override.go | 84 +++++++++++++++++++ 10 files changed, 246 insertions(+), 102 deletions(-) create mode 100644 pkg/utils/repo_ctx_override.go diff --git a/pkg/commands/transport/transport.go b/pkg/commands/transport/transport.go index e8806e8e..90eff928 100644 --- a/pkg/commands/transport/transport.go +++ b/pkg/commands/transport/transport.go @@ -19,14 +19,14 @@ import ( "github.com/mandelsoft/vfs/pkg/vfs" "github.com/spf13/cobra" "github.com/spf13/pflag" - "sigs.k8s.io/yaml" "github.com/gardener/component-cli/ociclient" "github.com/gardener/component-cli/ociclient/cache" ociopts "github.com/gardener/component-cli/ociclient/options" "github.com/gardener/component-cli/pkg/commands/constants" "github.com/gardener/component-cli/pkg/logger" - "github.com/gardener/component-cli/pkg/transport/config" + transport_config "github.com/gardener/component-cli/pkg/transport/config" + "github.com/gardener/component-cli/pkg/utils" ) type Options struct { @@ -40,7 +40,7 @@ type Options struct { // TransportCfgPath is the path to the transport config file TransportCfgPath string - // RepoCtxOverrideCfgPath is the path to the repo context override config file + // RepoCtxOverrideCfgPath is the path to the repository context override config file RepoCtxOverrideCfgPath string // OCIOptions contains all oci client related options. @@ -69,10 +69,10 @@ func NewTransportCommand(ctx context.Context) *cobra.Command { } func (o *Options) AddFlags(fs *pflag.FlagSet) { - fs.StringVar(&o.SourceRepository, "from", "", "source repository base url.") - fs.StringVar(&o.TargetRepository, "to", "", "target repository where the components are copied to.") + fs.StringVar(&o.SourceRepository, "from", "", "source repository base url") + fs.StringVar(&o.TargetRepository, "to", "", "target repository where the components are copied to") fs.StringVar(&o.TransportCfgPath, "transport-cfg", "", "path to the transport config file") - fs.StringVar(&o.RepoCtxOverrideCfgPath, "repo-ctx-override-cfg", "", "path to the repo context override config file") + fs.StringVar(&o.RepoCtxOverrideCfgPath, "repo-ctx-override-cfg", "", "path to the repository context override config file") o.OCIOptions.AddFlags(fs) } @@ -121,28 +121,28 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e return fmt.Errorf("unable to inject cache into oci client: %w", err) } - sourceCtx := cdv2.NewOCIRegistryRepository(o.SourceRepository, "") - cds, err := ResolveRecursive(ctx, ociClient, *sourceCtx, o.ComponentName, o.Version) + repoCtxOverrideCfg, err := utils.ParseRepositoryContextConfig(o.RepoCtxOverrideCfgPath) if err != nil { - return fmt.Errorf("unable to resolve component: %w", err) + return fmt.Errorf("unable to parse repository context override config file: %w", err) } - targetCtx := cdv2.NewOCIRegistryRepository(o.TargetRepository, "") - - transportCfgYaml, err := os.ReadFile(o.TransportCfgPath) + transportCfg, err := transport_config.ParseConfig(o.TransportCfgPath) if err != nil { - return fmt.Errorf("unable to read transport config file: %w", err) + return fmt.Errorf("unable to parse transport config file: %w", err) } - var transportCfg config.TransportConfig - if err := yaml.Unmarshal(transportCfgYaml, &transportCfg); err != nil { - return fmt.Errorf("unable to parse transport config file: %w", err) + sourceCtx := cdv2.NewOCIRegistryRepository(o.SourceRepository, "") + targetCtx := cdv2.NewOCIRegistryRepository(o.TargetRepository, "") + + cds, err := ResolveRecursive(ctx, ociClient, *sourceCtx, o.ComponentName, o.Version, *repoCtxOverrideCfg) + if err != nil { + return fmt.Errorf("unable to resolve component: %w", err) } - df := config.NewDownloaderFactory(ociClient, ociCache) - pf := config.NewProcessorFactory(ociCache) - uf := config.NewUploaderFactory(ociClient, ociCache, *targetCtx) - pjf, err := config.NewProcessingJobFactory(transportCfg, df, pf, uf) + df := transport_config.NewDownloaderFactory(ociClient, ociCache) + pf := transport_config.NewProcessorFactory(ociCache) + uf := transport_config.NewUploaderFactory(ociClient, ociCache, *targetCtx) + pjf, err := transport_config.NewProcessingJobFactory(*transportCfg, df, pf, uf) if err != nil { return err } @@ -187,7 +187,7 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e return nil } -func handleResources(ctx context.Context, cd *cdv2.ComponentDescriptor, targetCtx cdv2.OCIRegistryRepository, log logr.Logger, processingJobFactory *config.ProcessingJobFactory) ([]cdv2.Resource, []error) { +func handleResources(ctx context.Context, cd *cdv2.ComponentDescriptor, targetCtx cdv2.OCIRegistryRepository, log logr.Logger, processingJobFactory *transport_config.ProcessingJobFactory) ([]cdv2.Resource, []error) { wg := sync.WaitGroup{} errs := []error{} mux := sync.Mutex{} @@ -221,14 +221,24 @@ func handleResources(ctx context.Context, cd *cdv2.ComponentDescriptor, targetCt return processedResources, errs } -func ResolveRecursive(ctx context.Context, client ociclient.Client, repo cdv2.OCIRegistryRepository, componentName, componentVersion string) ([]*cdv2.ComponentDescriptor, error) { - ociRef, err := cdoci.OCIRef(repo, componentName, componentVersion) +func ResolveRecursive( + ctx context.Context, + client ociclient.Client, + defaultRepo cdv2.OCIRegistryRepository, + componentName, + componentVersion string, + repoCtxOverrideCfg utils.RepositoryContextOverride, +) ([]*cdv2.ComponentDescriptor, error) { + + repoCtx := repoCtxOverrideCfg.GetRepositoryContext(componentName, defaultRepo) + + ociRef, err := cdoci.OCIRef(*repoCtx, componentName, componentVersion) if err != nil { return nil, fmt.Errorf("invalid component reference: %w", err) } cdresolver := cdoci.NewResolver(client) - cd, err := cdresolver.Resolve(ctx, &repo, componentName, componentVersion) + cd, err := cdresolver.Resolve(ctx, repoCtx, componentName, componentVersion) if err != nil { return nil, fmt.Errorf("unable to to fetch component descriptor %s: %w", ociRef, err) } @@ -237,7 +247,7 @@ func ResolveRecursive(ctx context.Context, client ociclient.Client, repo cdv2.OC cd, } for _, ref := range cd.ComponentReferences { - cds2, err := ResolveRecursive(ctx, client, repo, ref.ComponentName, ref.Version) + cds2, err := ResolveRecursive(ctx, client, defaultRepo, ref.ComponentName, ref.Version, repoCtxOverrideCfg) if err != nil { return nil, fmt.Errorf("unable to resolve ref %+v: %w", ref, err) } diff --git a/pkg/transport/config/config_suite_test.go b/pkg/transport/config/config_suite_test.go index 34c9b29b..b3024e86 100644 --- a/pkg/transport/config/config_suite_test.go +++ b/pkg/transport/config/config_suite_test.go @@ -4,14 +4,12 @@ package config_test import ( - "os" "testing" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" "github.com/go-logr/logr" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - "sigs.k8s.io/yaml" "github.com/gardener/component-cli/ociclient" "github.com/gardener/component-cli/ociclient/cache" @@ -28,12 +26,9 @@ var ( ) var _ = BeforeSuite(func() { - transportCfgYaml, err := os.ReadFile("./testdata/transport.cfg") + transportCfg, err := config.ParseConfig("./testdata/transport.cfg") Expect(err).ToNot(HaveOccurred()) - var transportCfg config.TransportConfig - Expect(yaml.Unmarshal(transportCfgYaml, &transportCfg)).To(Succeed()) - client, err := ociclient.NewClient(logr.Discard()) Expect(err).ToNot(HaveOccurred()) ocicache := cache.NewInMemoryCache() @@ -43,6 +38,6 @@ var _ = BeforeSuite(func() { pf := config.NewProcessorFactory(ocicache) uf := config.NewUploaderFactory(client, ocicache, *targetCtx) - factory, err = config.NewProcessingJobFactory(transportCfg, df, pf, uf) + factory, err = config.NewProcessingJobFactory(*transportCfg, df, pf, uf) Expect(err).ToNot(HaveOccurred()) }, 5) diff --git a/pkg/transport/config/filter_factory.go b/pkg/transport/config/filter_factory.go index 27748673..1646d2d2 100644 --- a/pkg/transport/config/filter_factory.go +++ b/pkg/transport/config/filter_factory.go @@ -19,8 +19,8 @@ const ( // ResourceTypeFilterType defines the type of a resource type filter ResourceTypeFilterType = "ResourceTypeFilter" - // ResourceAccessTypeFilterType defines the type of a resource access filter - ResourceAccessTypeFilterType = "ResourceAccessTypeFilter" + // AccessTypeFilterType defines the type of a access type filter + AccessTypeFilterType = "AccessTypeFilter" ) // NewFilterFactory creates a new filter factory @@ -38,7 +38,7 @@ func (f *FilterFactory) Create(filterType string, spec *json.RawMessage) (filter return f.createComponentNameFilter(spec) case ResourceTypeFilterType: return f.createResourceTypeFilter(spec) - case ResourceAccessTypeFilterType: + case AccessTypeFilterType: return f.createAccessTypeFilter(spec) default: return nil, fmt.Errorf("unknown filter type %s", filterType) @@ -46,43 +46,28 @@ func (f *FilterFactory) Create(filterType string, spec *json.RawMessage) (filter } func (f *FilterFactory) createComponentNameFilter(rawSpec *json.RawMessage) (filters.Filter, error) { - type filterSpec struct { - IncludeComponentNames []string `json:"includeComponentNames"` - } - - var spec filterSpec - err := yaml.Unmarshal(*rawSpec, &spec) - if err != nil { + var spec filters.ComponentNameFilterSpec + if err := yaml.Unmarshal(*rawSpec, &spec); err != nil { return nil, fmt.Errorf("unable to parse spec: %w", err) } - return filters.NewComponentNameFilter(spec.IncludeComponentNames...) + return filters.NewComponentNameFilter(spec) } func (f *FilterFactory) createResourceTypeFilter(rawSpec *json.RawMessage) (filters.Filter, error) { - type filterSpec struct { - IncludeResourceTypes []string `json:"includeResourceTypes"` - } - - var spec filterSpec - err := yaml.Unmarshal(*rawSpec, &spec) - if err != nil { + var spec filters.ResourceTypeFilterSpec + if err := yaml.Unmarshal(*rawSpec, &spec); err != nil { return nil, fmt.Errorf("unable to parse spec: %w", err) } - return filters.NewResourceTypeFilter(spec.IncludeResourceTypes...) + return filters.NewResourceTypeFilter(spec) } func (f *FilterFactory) createAccessTypeFilter(rawSpec *json.RawMessage) (filters.Filter, error) { - type filterSpec struct { - IncludeAccessTypes []string `json:"includeAccessTypes"` - } - - var spec filterSpec - err := yaml.Unmarshal(*rawSpec, &spec) - if err != nil { + var spec filters.AccessTypeFilterSpec + if err := yaml.Unmarshal(*rawSpec, &spec); err != nil { return nil, fmt.Errorf("unable to parse spec: %w", err) } - return filters.NewResourceAccessTypeFilter(spec.IncludeAccessTypes...) + return filters.NewAccessTypeFilter(spec) } diff --git a/pkg/transport/config/processing_job.go b/pkg/transport/config/processing_job.go index eea04c0c..d7f8c808 100644 --- a/pkg/transport/config/processing_job.go +++ b/pkg/transport/config/processing_job.go @@ -7,8 +7,10 @@ import ( "context" "encoding/json" "fmt" + "os" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "sigs.k8s.io/yaml" "github.com/gardener/component-cli/pkg/transport/filters" "github.com/gardener/component-cli/pkg/transport/process" @@ -55,7 +57,7 @@ type parsedRuleDefinition struct { Filters []filters.Filter } -type parsedTransportConfig struct { +type ParsedTransportConfig struct { Downloaders []parsedDownloaderDefinition Processors []parsedProcessorDefinition Uploaders []parsedUploaderDefinition @@ -63,14 +65,9 @@ type parsedTransportConfig struct { } // NewProcessingJobFactory creates a new processing job factory -func NewProcessingJobFactory(transportCfg TransportConfig, df *DownloaderFactory, pf *ProcessorFactory, uf *UploaderFactory) (*ProcessingJobFactory, error) { - parsedTransportConfig, err := parseTransportConfig(&transportCfg) - if err != nil { - return nil, fmt.Errorf("unable to parse transport config: %w", err) - } - +func NewProcessingJobFactory(transportCfg ParsedTransportConfig, df *DownloaderFactory, pf *ProcessorFactory, uf *UploaderFactory) (*ProcessingJobFactory, error) { c := ProcessingJobFactory{ - parsedConfig: parsedTransportConfig, + parsedConfig: &transportCfg, downloaderFactory: df, processorFactory: pf, uploaderFactory: uf, @@ -81,14 +78,24 @@ func NewProcessingJobFactory(transportCfg TransportConfig, df *DownloaderFactory // ProcessingJobFactory defines a helper struct for creating processing jobs type ProcessingJobFactory struct { - parsedConfig *parsedTransportConfig + parsedConfig *ParsedTransportConfig uploaderFactory *UploaderFactory downloaderFactory *DownloaderFactory processorFactory *ProcessorFactory } -func parseTransportConfig(config *TransportConfig) (*parsedTransportConfig, error) { - var parsedConfig parsedTransportConfig +func ParseConfig(configFilePath string) (*ParsedTransportConfig, error) { + transportCfgYaml, err := os.ReadFile(configFilePath) + if err != nil { + return nil, fmt.Errorf("unable to read transport config file: %w", err) + } + + var config TransportConfig + if err := yaml.Unmarshal(transportCfgYaml, &config); err != nil { + return nil, fmt.Errorf("unable to parse transport config file: %w", err) + } + + var parsedConfig ParsedTransportConfig ff := NewFilterFactory() // downloaders @@ -228,7 +235,7 @@ func createFilterList(filterDefinitions []FilterDefinition, ff *FilterFactory) ( return filters, nil } -func findProcessorByName(name string, lookup *parsedTransportConfig) (*parsedProcessorDefinition, error) { +func findProcessorByName(name string, lookup *ParsedTransportConfig) (*parsedProcessorDefinition, error) { for _, processor := range lookup.Processors { if processor.Name == name { return &processor, nil diff --git a/pkg/transport/config/testdata/transport.cfg b/pkg/transport/config/testdata/transport.cfg index 45ae9b5c..8b311f17 100644 --- a/pkg/transport/config/testdata/transport.cfg +++ b/pkg/transport/config/testdata/transport.cfg @@ -33,7 +33,7 @@ downloaders: - name: 'local-oci-blob-dl' type: 'LocalOciBlobDownloader' filters: - - type: 'ResourceAccessTypeFilter' + - type: 'AccessTypeFilter' spec: includeAccessTypes: - 'localOciBlob' @@ -52,7 +52,7 @@ uploaders: - name: 'local-oci-blob-ul' type: 'LocalOciBlobUploader' filters: - - type: 'ResourceAccessTypeFilter' + - type: 'AccessTypeFilter' spec: includeAccessTypes: - 'localOciBlob' diff --git a/pkg/transport/filters/component_name_filter.go b/pkg/transport/filters/component_name_filter.go index bee9c24c..6f6f237d 100644 --- a/pkg/transport/filters/component_name_filter.go +++ b/pkg/transport/filters/component_name_filter.go @@ -10,6 +10,10 @@ import ( cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" ) +type ComponentNameFilterSpec struct { + IncludeComponentNames []string +} + type componentNameFilter struct { includeComponentNames []*regexp.Regexp } @@ -24,13 +28,13 @@ func (f componentNameFilter) Matches(cd cdv2.ComponentDescriptor, r cdv2.Resourc } // NewComponentNameFilter creates a new componentNameFilter -func NewComponentNameFilter(includeComponentNames ...string) (Filter, error) { - if len(includeComponentNames) == 0 { +func NewComponentNameFilter(spec ComponentNameFilterSpec) (Filter, error) { + if len(spec.IncludeComponentNames) == 0 { return nil, fmt.Errorf("includeComponentNames must not be empty") } icnRegexps := []*regexp.Regexp{} - for _, icn := range includeComponentNames { + for _, icn := range spec.IncludeComponentNames { icnRegexp, err := regexp.Compile(icn) if err != nil { return nil, fmt.Errorf("unable to parse regexp %s: %w", icn, err) diff --git a/pkg/transport/filters/filters_suite_test.go b/pkg/transport/filters/filters_suite_test.go index 949c24ca..e9f08c7d 100644 --- a/pkg/transport/filters/filters_suite_test.go +++ b/pkg/transport/filters/filters_suite_test.go @@ -20,15 +20,20 @@ func TestConfig(t *testing.T) { var _ = Describe("filters", func() { - Context("resourceAccessTypeFilter", func() { + Context("accessTypeFilter", func() { It("should match if access type is in include list", func() { cd := cdv2.ComponentDescriptor{} res := cdv2.Resource{ Access: cdv2.NewEmptyUnstructured(cdv2.OCIRegistryType), } + spec := filter.AccessTypeFilterSpec{ + IncludeAccessTypes: []string{ + cdv2.OCIRegistryType, + }, + } - f, err := filter.NewResourceAccessTypeFilter(cdv2.OCIRegistryType) + f, err := filter.NewAccessTypeFilter(spec) Expect(err).ToNot(HaveOccurred()) actualMatch := f.Matches(cd, res) @@ -40,8 +45,13 @@ var _ = Describe("filters", func() { res := cdv2.Resource{ Access: cdv2.NewEmptyUnstructured(cdv2.OCIRegistryType), } + spec := filter.AccessTypeFilterSpec{ + IncludeAccessTypes: []string{ + cdv2.LocalOCIBlobType, + }, + } - f, err := filter.NewResourceAccessTypeFilter(cdv2.LocalOCIBlobType) + f, err := filter.NewAccessTypeFilter(spec) Expect(err).ToNot(HaveOccurred()) actualMatch := f.Matches(cd, res) @@ -49,8 +59,10 @@ var _ = Describe("filters", func() { }) It("should return error upon creation if include list is empty", func() { - includeAccessTypes := []string{} - _, err := filter.NewResourceAccessTypeFilter(includeAccessTypes...) + spec := filter.AccessTypeFilterSpec{ + IncludeAccessTypes: []string{}, + } + _, err := filter.NewAccessTypeFilter(spec) Expect(err).To(HaveOccurred()) Expect(err).To(MatchError("includeAccessTypes must not be empty")) }) @@ -68,8 +80,13 @@ var _ = Describe("filters", func() { Type: cdv2.OCIImageType, }, } + spec := filter.ResourceTypeFilterSpec{ + IncludeResourceTypes: []string{ + cdv2.OCIImageType, + }, + } - f, err := filter.NewResourceTypeFilter(cdv2.OCIImageType) + f, err := filter.NewResourceTypeFilter(spec) Expect(err).ToNot(HaveOccurred()) actualMatch := f.Matches(cd, res) @@ -85,8 +102,13 @@ var _ = Describe("filters", func() { Type: "helm", }, } + spec := filter.ResourceTypeFilterSpec{ + IncludeResourceTypes: []string{ + cdv2.OCIImageType, + }, + } - f, err := filter.NewResourceTypeFilter(cdv2.OCIImageType) + f, err := filter.NewResourceTypeFilter(spec) Expect(err).ToNot(HaveOccurred()) actualMatch := f.Matches(cd, res) @@ -94,8 +116,10 @@ var _ = Describe("filters", func() { }) It("should return error upon creation if include list is empty", func() { - includeResourceTypes := []string{} - _, err := filter.NewResourceTypeFilter(includeResourceTypes...) + spec := filter.ResourceTypeFilterSpec{ + IncludeResourceTypes: []string{}, + } + _, err := filter.NewResourceTypeFilter(spec) Expect(err).To(HaveOccurred()) Expect(err).To(MatchError("includeResourceTypes must not be empty")) }) @@ -113,14 +137,24 @@ var _ = Describe("filters", func() { }, } res := cdv2.Resource{} + spec := filter.ComponentNameFilterSpec{ + IncludeComponentNames: []string{ + "github.com/test/my-component", + }, + } - f1, err := filter.NewComponentNameFilter("github.com/test/my-component") + f1, err := filter.NewComponentNameFilter(spec) Expect(err).ToNot(HaveOccurred()) match1 := f1.Matches(cd, res) Expect(match1).To(Equal(true)) - f2, err := filter.NewComponentNameFilter("github.com/test/*") + spec = filter.ComponentNameFilterSpec{ + IncludeComponentNames: []string{ + "github.com/test/*", + }, + } + f2, err := filter.NewComponentNameFilter(spec) Expect(err).ToNot(HaveOccurred()) match2 := f2.Matches(cd, res) @@ -136,14 +170,24 @@ var _ = Describe("filters", func() { }, } res := cdv2.Resource{} + spec := filter.ComponentNameFilterSpec{ + IncludeComponentNames: []string{ + "github.com/test/my-other-component", + }, + } - f1, err := filter.NewComponentNameFilter("github.com/test/my-other-component") + f1, err := filter.NewComponentNameFilter(spec) Expect(err).ToNot(HaveOccurred()) match1 := f1.Matches(cd, res) Expect(match1).To(Equal(false)) - f2, err := filter.NewComponentNameFilter("github.com/test-2/*") + spec = filter.ComponentNameFilterSpec{ + IncludeComponentNames: []string{ + "github.com/test-2/*", + }, + } + f2, err := filter.NewComponentNameFilter(spec) Expect(err).ToNot(HaveOccurred()) match2 := f2.Matches(cd, res) @@ -151,14 +195,21 @@ var _ = Describe("filters", func() { }) It("should return error upon creation if include list is empty", func() { - includeComponentNames := []string{} - _, err := filter.NewComponentNameFilter(includeComponentNames...) + spec := filter.ComponentNameFilterSpec{ + IncludeComponentNames: []string{}, + } + _, err := filter.NewComponentNameFilter(spec) Expect(err).To(HaveOccurred()) Expect(err).To(MatchError("includeComponentNames must not be empty")) }) It("should return error upon creation if regexp is invalid", func() { - _, err := filter.NewComponentNameFilter("github.com/\\") + spec := filter.ComponentNameFilterSpec{ + IncludeComponentNames: []string{ + "github.com/\\", + }, + } + _, err := filter.NewComponentNameFilter(spec) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("error parsing regexp")) }) diff --git a/pkg/transport/filters/resource_access_type_filter.go b/pkg/transport/filters/resource_access_type_filter.go index 4e6d54ea..b393c297 100644 --- a/pkg/transport/filters/resource_access_type_filter.go +++ b/pkg/transport/filters/resource_access_type_filter.go @@ -9,11 +9,15 @@ import ( cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" ) -type resourceAccessTypeFilter struct { +type AccessTypeFilterSpec struct { + IncludeAccessTypes []string `json:"includeAccessTypes"` +} + +type accessTypeFilter struct { includeAccessTypes []string } -func (f resourceAccessTypeFilter) Matches(cd cdv2.ComponentDescriptor, r cdv2.Resource) bool { +func (f accessTypeFilter) Matches(cd cdv2.ComponentDescriptor, r cdv2.Resource) bool { for _, accessType := range f.includeAccessTypes { if r.Access.Type == accessType { return true @@ -22,14 +26,14 @@ func (f resourceAccessTypeFilter) Matches(cd cdv2.ComponentDescriptor, r cdv2.Re return false } -// NewResourceAccessTypeFilter creates a new resourceAccessTypeFilter -func NewResourceAccessTypeFilter(includeAccessTypes ...string) (Filter, error) { - if len(includeAccessTypes) == 0 { +// NewAccessTypeFilter creates a new accessTypeFilter +func NewAccessTypeFilter(spec AccessTypeFilterSpec) (Filter, error) { + if len(spec.IncludeAccessTypes) == 0 { return nil, fmt.Errorf("includeAccessTypes must not be empty") } - filter := resourceAccessTypeFilter{ - includeAccessTypes: includeAccessTypes, + filter := accessTypeFilter{ + includeAccessTypes: spec.IncludeAccessTypes, } return &filter, nil diff --git a/pkg/transport/filters/resource_type_filter.go b/pkg/transport/filters/resource_type_filter.go index 787973e8..caa52133 100644 --- a/pkg/transport/filters/resource_type_filter.go +++ b/pkg/transport/filters/resource_type_filter.go @@ -9,6 +9,10 @@ import ( cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" ) +type ResourceTypeFilterSpec struct { + IncludeResourceTypes []string `json:"includeResourceTypes"` +} + type resourceTypeFilter struct { includeResourceTypes []string } @@ -23,13 +27,13 @@ func (f resourceTypeFilter) Matches(cd cdv2.ComponentDescriptor, r cdv2.Resource } // NewResourceTypeFilter creates a new resourceTypeFilter -func NewResourceTypeFilter(includeResourceTypes ...string) (Filter, error) { - if len(includeResourceTypes) == 0 { +func NewResourceTypeFilter(spec ResourceTypeFilterSpec) (Filter, error) { + if len(spec.IncludeResourceTypes) == 0 { return nil, fmt.Errorf("includeResourceTypes must not be empty") } filter := resourceTypeFilter{ - includeResourceTypes: includeResourceTypes, + includeResourceTypes: spec.IncludeResourceTypes, } return &filter, nil diff --git a/pkg/utils/repo_ctx_override.go b/pkg/utils/repo_ctx_override.go new file mode 100644 index 00000000..e3cf6985 --- /dev/null +++ b/pkg/utils/repo_ctx_override.go @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package utils + +import ( + "fmt" + "os" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "sigs.k8s.io/yaml" + + "github.com/gardener/component-cli/pkg/transport/filters" +) + +type RepositoryContextOverride struct { + Overrides []Override +} + +type Override struct { + Filter filters.Filter + RepositoryContext *cdv2.OCIRegistryRepository +} + +func ParseRepositoryContextConfig(configPath string) (*RepositoryContextOverride, error) { + type meta struct { + Version string `json:"version"` + } + + type override struct { + ComponentNameFilterSpec *filters.ComponentNameFilterSpec `json:"componentNameFilterSpec"` + RepositoryContext *cdv2.OCIRegistryRepository `json:"repositoryContext"` + } + + type repositoryContextOverride struct { + Meta meta `json:"meta"` + Overrides []override `json:"overrides"` + } + + repoCtxOverrideCfgYaml, err := os.ReadFile(configPath) + if err != nil { + return nil, fmt.Errorf("unable to read config file: %w", err) + } + + var cfg repositoryContextOverride + if err := yaml.Unmarshal(repoCtxOverrideCfgYaml, &cfg); err != nil { + return nil, fmt.Errorf("unable to parse config file: %w", err) + } + + parsedCfg := RepositoryContextOverride{ + Overrides: []Override{}, + } + + for _, o := range cfg.Overrides { + f, err := filters.NewComponentNameFilter(*o.ComponentNameFilterSpec) + if err != nil { + return nil, fmt.Errorf("unable to create component name filter: %w", err) + } + po := Override{ + Filter: f, + RepositoryContext: o.RepositoryContext, + } + parsedCfg.Overrides = append(parsedCfg.Overrides, po) + } + + return &parsedCfg, nil +} + +func (c *RepositoryContextOverride) GetRepositoryContext(componentName string, defaultRepoCtx cdv2.OCIRegistryRepository) *cdv2.OCIRegistryRepository { + ctx := defaultRepoCtx + for _, o := range c.Overrides { + dummyCd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + ObjectMeta: cdv2.ObjectMeta{ + Name: componentName, + }, + }, + } + if o.Filter.Matches(dummyCd, cdv2.Resource{}) { + ctx = *o.RepositoryContext + } + } + return &ctx +} From d675ef232e6c986603530c39ff191fb0a1652636 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Mon, 13 Dec 2021 11:50:56 +0100 Subject: [PATCH 82/94] adds component descriptor signature generation --- pkg/commands/transport/transport.go | 123 +++++++++++++++--- pkg/transport/config/processor_factory.go | 10 ++ .../process/processors/blob_digester.go | 53 ++++++++ .../processors/oci_manifest_digester.go | 73 +++++++++++ .../utils/oci_artifact_serialization.go | 42 ++++++ .../process/utils/processor_message.go | 2 +- 6 files changed, 286 insertions(+), 17 deletions(-) create mode 100644 pkg/transport/process/processors/blob_digester.go create mode 100644 pkg/transport/process/processors/oci_manifest_digester.go diff --git a/pkg/commands/transport/transport.go b/pkg/commands/transport/transport.go index 90eff928..6da58983 100644 --- a/pkg/commands/transport/transport.go +++ b/pkg/commands/transport/transport.go @@ -12,6 +12,7 @@ import ( "sync" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "github.com/gardener/component-spec/bindings-go/apis/v2/signatures" "github.com/gardener/component-spec/bindings-go/ctf" cdoci "github.com/gardener/component-spec/bindings-go/oci" "github.com/go-logr/logr" @@ -43,6 +44,10 @@ type Options struct { // RepoCtxOverrideCfgPath is the path to the repository context override config file RepoCtxOverrideCfgPath string + GenerateSignature bool + SignatureName string + PrivateKeyPath string + // OCIOptions contains all oci client related options. OCIOptions ociopts.Options } @@ -73,6 +78,9 @@ func (o *Options) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&o.TargetRepository, "to", "", "target repository where the components are copied to") fs.StringVar(&o.TransportCfgPath, "transport-cfg", "", "path to the transport config file") fs.StringVar(&o.RepoCtxOverrideCfgPath, "repo-ctx-override-cfg", "", "path to the repository context override config file") + fs.BoolVar(&o.GenerateSignature, "sign", false, "sign the uploaded component descriptors") + fs.StringVar(&o.SignatureName, "signature-name", "", "name of the generated signature") + fs.StringVar(&o.PrivateKeyPath, "private-key", "", "path to the private key file used for signing") o.OCIOptions.AddFlags(fs) } @@ -97,12 +105,25 @@ func (o *Options) Complete(args []string) error { } if len(o.SourceRepository) == 0 { - return errors.New("the base url must be defined") + return errors.New("a source repository must be defined") + } + if len(o.TargetRepository) == 0 { + return errors.New("a target repository must be defined") } + if len(o.TransportCfgPath) == 0 { return errors.New("a path to a transport config file must be defined") } + if o.GenerateSignature { + if o.SignatureName == "" { + return errors.New("a signature name must be defined") + } + if o.PrivateKeyPath == "" { + return errors.New("a path to a private key file must be defined") + } + } + return nil } @@ -121,9 +142,12 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e return fmt.Errorf("unable to inject cache into oci client: %w", err) } - repoCtxOverrideCfg, err := utils.ParseRepositoryContextConfig(o.RepoCtxOverrideCfgPath) - if err != nil { - return fmt.Errorf("unable to parse repository context override config file: %w", err) + var repoCtxOverride *utils.RepositoryContextOverride + if o.RepoCtxOverrideCfgPath != "" { + repoCtxOverride, err = utils.ParseRepositoryContextConfig(o.RepoCtxOverrideCfgPath) + if err != nil { + return fmt.Errorf("unable to parse repository context override config file: %w", err) + } } transportCfg, err := transport_config.ParseConfig(o.TransportCfgPath) @@ -134,7 +158,7 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e sourceCtx := cdv2.NewOCIRegistryRepository(o.SourceRepository, "") targetCtx := cdv2.NewOCIRegistryRepository(o.TargetRepository, "") - cds, err := ResolveRecursive(ctx, ociClient, *sourceCtx, o.ComponentName, o.Version, *repoCtxOverrideCfg) + cds, err := ResolveRecursive(ctx, ociClient, *sourceCtx, o.ComponentName, o.Version, repoCtxOverride) if err != nil { return fmt.Errorf("unable to resolve component: %w", err) } @@ -148,35 +172,94 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e } wg := sync.WaitGroup{} + cdMap := map[string]*cdv2.ComponentDescriptor{} + for _, cd := range cds { cd := cd + + key := fmt.Sprintf("%s:%s", cd.Name, cd.Version) + if _, ok := cdMap[key]; ok { + return fmt.Errorf("component descriptor already exists in map: %+v", cd) + } + cdMap[key] = cd + wg.Add(1) go func() { defer wg.Done() processedResources, errs := handleResources(ctx, cd, *targetCtx, log, pjf) if len(errs) > 0 { for _, err := range errs { - log.Error(err, "") + log.Error(err, "unable to process resource") return } } cd.Resources = processedResources + }() + } + wg.Wait() + + if o.GenerateSignature { + // iterate backwards -> start with "leave" component descriptors w/o dependencies + for i := len(cds) - 1; i >= 0; i-- { + cd := cds[i] + + crr := func(context.Context, cdv2.ComponentDescriptor, cdv2.ComponentReference) (*cdv2.DigestSpec, error) { + key := fmt.Sprintf("%s:%s", cd.Name, cd.Version) + cd, ok := cdMap[key] + if !ok { + return nil, fmt.Errorf("unable to find component descriptor in map") + } + + signature, err := signatures.SelectSignatureByName(cd, o.SignatureName) + if err != nil { + return nil, fmt.Errorf("unable to get signature from component descriptor: %w", err) + } + + return &signature.Digest, nil + } + + signer, err := signatures.CreateRsaSignerFromKeyFile(o.PrivateKeyPath) + if err != nil { + return fmt.Errorf("unable to create signer: %w", err) + } + + hasher, err := signatures.HasherForName("SHA256") + if err != nil { + return fmt.Errorf("unable to create hasher: %w", err) + } + + if err := signatures.AddDigestsToComponentDescriptor(ctx, cd, crr, nil); err != nil { + return fmt.Errorf("unable to add digests to component descriptor: %w", err) + } + + if err := signatures.SignComponentDescriptor(cd, signer, *hasher, o.SignatureName); err != nil { + return fmt.Errorf("unable to sign component descriptor: %w", err) + } + } + } + + for _, cd := range cdMap { + cd := cd + + wg.Add(1) + go func() { + defer wg.Done() manifest, err := cdoci.NewManifestBuilder(ociCache, ctf.NewComponentArchive(cd, nil)).Build(ctx) if err != nil { - log.Error(err, "unable to build oci artifact for component acrchive") + log.Error(err, "unable to build oci artifact for component archive") return } ociRef, err := cdoci.OCIRef(*targetCtx, o.ComponentName, o.Version) if err != nil { - log.Error(err, "unable to build component reference") + log.Error(err, "unable to build component descriptor oci reference") return } if err := ociClient.PushManifest(ctx, ociRef, manifest); err != nil { - log.Error(err, "unable to push manifest") + log.Error(err, "unable to push component descriptor manifest") return } }() @@ -190,8 +273,9 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e func handleResources(ctx context.Context, cd *cdv2.ComponentDescriptor, targetCtx cdv2.OCIRegistryRepository, log logr.Logger, processingJobFactory *transport_config.ProcessingJobFactory) ([]cdv2.Resource, []error) { wg := sync.WaitGroup{} errs := []error{} - mux := sync.Mutex{} + errsMux := sync.Mutex{} processedResources := []cdv2.Resource{} + resMux := sync.Mutex{} for _, resource := range cd.Resources { resource := resource @@ -202,18 +286,22 @@ func handleResources(ctx context.Context, cd *cdv2.ComponentDescriptor, targetCt job, err := processingJobFactory.Create(*cd, resource) if err != nil { + errsMux.Lock() errs = append(errs, fmt.Errorf("unable to create processing job: %w", err)) + errsMux.Unlock() return } if err = job.Process(ctx); err != nil { + errsMux.Lock() errs = append(errs, fmt.Errorf("unable to process resource %+v: %w", resource, err)) + errsMux.Unlock() return } - mux.Lock() + resMux.Lock() processedResources = append(processedResources, *job.ProcessedResource) - mux.Unlock() + resMux.Unlock() }() } @@ -227,18 +315,21 @@ func ResolveRecursive( defaultRepo cdv2.OCIRegistryRepository, componentName, componentVersion string, - repoCtxOverrideCfg utils.RepositoryContextOverride, + repoCtxOverrideCfg *utils.RepositoryContextOverride, ) ([]*cdv2.ComponentDescriptor, error) { - repoCtx := repoCtxOverrideCfg.GetRepositoryContext(componentName, defaultRepo) + repoCtx := defaultRepo + if repoCtxOverrideCfg != nil { + repoCtx = *repoCtxOverrideCfg.GetRepositoryContext(componentName, defaultRepo) + } - ociRef, err := cdoci.OCIRef(*repoCtx, componentName, componentVersion) + ociRef, err := cdoci.OCIRef(repoCtx, componentName, componentVersion) if err != nil { return nil, fmt.Errorf("invalid component reference: %w", err) } cdresolver := cdoci.NewResolver(client) - cd, err := cdresolver.Resolve(ctx, repoCtx, componentName, componentVersion) + cd, err := cdresolver.Resolve(ctx, &repoCtx, componentName, componentVersion) if err != nil { return nil, fmt.Errorf("unable to to fetch component descriptor %s: %w", ociRef, err) } diff --git a/pkg/transport/config/processor_factory.go b/pkg/transport/config/processor_factory.go index bc70b0ef..366ba295 100644 --- a/pkg/transport/config/processor_factory.go +++ b/pkg/transport/config/processor_factory.go @@ -21,6 +21,12 @@ const ( // OCIArtifactFilterProcessorType defines the type of an oci artifact filter OCIArtifactFilterProcessorType = "OciArtifactFilter" + + // BlobDigesterProcessorType defines the type of a blob digester + BlobDigesterProcessorType = "BlobDigester" + + // OCIManifestDigesterProcessorType the type of an oci manifest digester + OCIManifestDigesterProcessorType = "OciManifestDigester" ) // NewProcessorFactory creates a new processor factory @@ -42,6 +48,10 @@ func (f *ProcessorFactory) Create(processorType string, spec *json.RawMessage) ( return f.createResourceLabeler(spec) case OCIArtifactFilterProcessorType: return f.createOCIArtifactFilter(spec) + case BlobDigesterProcessorType: + return processors.NewBlobDigester(), nil + case OCIManifestDigesterProcessorType: + return processors.NewOCIManifestDigester(), nil case ExecutableType: return createExecutable(spec) default: diff --git a/pkg/transport/process/processors/blob_digester.go b/pkg/transport/process/processors/blob_digester.go new file mode 100644 index 00000000..c837a258 --- /dev/null +++ b/pkg/transport/process/processors/blob_digester.go @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package processors + +import ( + "context" + "fmt" + "io" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "github.com/opencontainers/go-digest" + + "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/utils" +) + +type blobDigester struct{} + +func NewBlobDigester() process.ResourceStreamProcessor { + obj := blobDigester{} + return &obj +} + +func (p *blobDigester) Process(ctx context.Context, r io.Reader, w io.Writer) error { + cd, res, resBlobReader, err := utils.ReadProcessorMessage(r) + if err != nil { + return fmt.Errorf("unable to read processor message: %w", err) + } + if resBlobReader != nil { + defer resBlobReader.Close() + } + + dgst, err := digest.FromReader(resBlobReader) + if err != nil { + return fmt.Errorf("unable to calculate digest: %w", err) + } + digestspec := cdv2.DigestSpec{ + Algorithm: dgst.Algorithm().String(), + Value: dgst.Encoded(), + } + res.Digest = &digestspec + + if _, err := resBlobReader.Seek(0, io.SeekStart); err != nil { + return fmt.Errorf("unable to seek to beginning of resource blob file: %w", err) + } + + if err := utils.WriteProcessorMessage(*cd, res, resBlobReader, w); err != nil { + return fmt.Errorf("unable to write processor message: %w", err) + } + + return nil +} diff --git a/pkg/transport/process/processors/oci_manifest_digester.go b/pkg/transport/process/processors/oci_manifest_digester.go new file mode 100644 index 00000000..291f9a2a --- /dev/null +++ b/pkg/transport/process/processors/oci_manifest_digester.go @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package processors + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "github.com/opencontainers/go-digest" + + "github.com/gardener/component-cli/pkg/transport/process" + processutils "github.com/gardener/component-cli/pkg/transport/process/utils" +) + +type ociManifestDigester struct{} + +func NewOCIManifestDigester() process.ResourceStreamProcessor { + obj := ociManifestDigester{} + return &obj +} + +func (f *ociManifestDigester) Process(ctx context.Context, r io.Reader, w io.Writer) error { + cd, res, blobreader, err := processutils.ReadProcessorMessage(r) + if err != nil { + return fmt.Errorf("unable to read archive: %w", err) + } + if blobreader == nil { + return errors.New("resource blob must not be nil") + } + defer blobreader.Close() + + manifest, index, err := processutils.GetManifestOrIndexFromSerializedOCIArtifact(blobreader) + if err != nil { + return fmt.Errorf("unable to deserialize oci artifact: %w", err) + } + + var content []byte + if manifest != nil { + content, err = json.Marshal(manifest) + if err != nil { + return fmt.Errorf("unable to marshal manifest: %w", err) + } + } else if index != nil { + content, err = json.Marshal(index) + if err != nil { + return fmt.Errorf("unable to marshal image index: %w", err) + } + } else { + return errors.New("") + } + + dgst := digest.FromBytes(content) + digestspec := cdv2.DigestSpec{ + Algorithm: dgst.Algorithm().String(), + Value: dgst.Encoded(), + } + res.Digest = &digestspec + + if _, err := blobreader.Seek(0, io.SeekStart); err != nil { + return fmt.Errorf("unable to seek to beginning of resource blob file: %w", err) + } + + if err = processutils.WriteProcessorMessage(*cd, res, blobreader, w); err != nil { + return fmt.Errorf("unable to write archive: %w", err) + } + + return nil +} diff --git a/pkg/transport/process/utils/oci_artifact_serialization.go b/pkg/transport/process/utils/oci_artifact_serialization.go index 40f8b9bd..a053d27d 100644 --- a/pkg/transport/process/utils/oci_artifact_serialization.go +++ b/pkg/transport/process/utils/oci_artifact_serialization.go @@ -276,3 +276,45 @@ func DeserializeOCIArtifact(reader io.Reader, cache cache.Cache) (*oci.Artifact, return ociArtifact, nil } + +func GetManifestOrIndexFromSerializedOCIArtifact(reader io.Reader) (*ocispecv1.Manifest, *ocispecv1.Index, error) { + if reader == nil { + return nil, nil, errors.New("reader must not be nil") + } + + tr := tar.NewReader(reader) + buf := bytes.NewBuffer([]byte{}) + + manifest := &ocispecv1.Manifest{} + index := &ocispecv1.Index{} + + for { + header, err := tr.Next() + if err != nil { + if err == io.EOF { + break + } + return nil, nil, fmt.Errorf("unable to read tar header: %w", err) + } + + if header.Name == ManifestFile { + if _, err := io.Copy(buf, tr); err != nil { + return nil, nil, fmt.Errorf("unable to copy %s to buffer: %w", ManifestFile, err) + } + if err := json.Unmarshal(buf.Bytes(), &manifest); err != nil { + return nil, nil, fmt.Errorf("unable to unmarshal manifest: %w", err) + } + } else if header.Name == IndexFile { + if _, err := io.Copy(buf, tr); err != nil { + return nil, nil, fmt.Errorf("unable to copy %s to buffer: %w", IndexFile, err) + } + if err := json.Unmarshal(buf.Bytes(), &index); err != nil { + return nil, nil, fmt.Errorf("unable to unmarshal image index: %w", err) + } + } else { + continue + } + } + + return manifest, index, nil +} diff --git a/pkg/transport/process/utils/processor_message.go b/pkg/transport/process/utils/processor_message.go index 38500ee3..f6d74a1a 100644 --- a/pkg/transport/process/utils/processor_message.go +++ b/pkg/transport/process/utils/processor_message.go @@ -66,7 +66,7 @@ func WriteProcessorMessage(cd cdv2.ComponentDescriptor, res cdv2.Resource, resou // (tar archive with fixed filenames for component descriptor, resource, and resource blob) which is // produced by processors. The resource blob reader can be nil. If a non-nil value is returned, it must // be closed by the caller. -func ReadProcessorMessage(r io.Reader) (*cdv2.ComponentDescriptor, cdv2.Resource, io.ReadCloser, error) { +func ReadProcessorMessage(r io.Reader) (*cdv2.ComponentDescriptor, cdv2.Resource, io.ReadSeekCloser, error) { tr := tar.NewReader(r) var cd *cdv2.ComponentDescriptor From ade22aec7ac0e6c73e2a1b44baad8b425dc7eabb Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Mon, 13 Dec 2021 13:57:57 +0100 Subject: [PATCH 83/94] improves logging and error handling --- pkg/commands/transport/transport.go | 125 +++++++++++++++++----------- 1 file changed, 75 insertions(+), 50 deletions(-) diff --git a/pkg/commands/transport/transport.go b/pkg/commands/transport/transport.go index 6da58983..9f8c37ee 100644 --- a/pkg/commands/transport/transport.go +++ b/pkg/commands/transport/transport.go @@ -158,7 +158,7 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e sourceCtx := cdv2.NewOCIRegistryRepository(o.SourceRepository, "") targetCtx := cdv2.NewOCIRegistryRepository(o.TargetRepository, "") - cds, err := ResolveRecursive(ctx, ociClient, *sourceCtx, o.ComponentName, o.Version, repoCtxOverride) + cds, err := ResolveRecursive(ctx, ociClient, *sourceCtx, o.ComponentName, o.Version, repoCtxOverride, log) if err != nil { return fmt.Errorf("unable to resolve component: %w", err) } @@ -172,26 +172,31 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e } wg := sync.WaitGroup{} - cdMap := map[string]*cdv2.ComponentDescriptor{} + cdLookup := map[string]*cdv2.ComponentDescriptor{} + errs := []error{} + errsMux := sync.Mutex{} for _, cd := range cds { cd := cd + componentLog := log.WithValues("component-name", cd.Name, "component-version", cd.Version) key := fmt.Sprintf("%s:%s", cd.Name, cd.Version) - if _, ok := cdMap[key]; ok { - return fmt.Errorf("component descriptor already exists in map: %+v", cd) + if _, ok := cdLookup[key]; ok { + err := errors.New("component descriptor already exists in map") + componentLog.Error(err, "unable to add component descriptor to map") + return err } - cdMap[key] = cd + cdLookup[key] = cd wg.Add(1) go func() { defer wg.Done() - processedResources, errs := handleResources(ctx, cd, *targetCtx, log, pjf) - if len(errs) > 0 { - for _, err := range errs { - log.Error(err, "unable to process resource") - return - } + processedResources, err := processResources(ctx, cd, *targetCtx, componentLog, pjf) + if err != nil { + errsMux.Lock() + errs = append(errs, err) + errsMux.Unlock() + return } cd.Resources = processedResources @@ -200,66 +205,74 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e wg.Wait() - if o.GenerateSignature { - // iterate backwards -> start with "leave" component descriptors w/o dependencies - for i := len(cds) - 1; i >= 0; i-- { - cd := cds[i] + if len(errs) > 0 { + return fmt.Errorf("%d errors occurred during resource processing", len(errs)) + } - crr := func(context.Context, cdv2.ComponentDescriptor, cdv2.ComponentReference) (*cdv2.DigestSpec, error) { - key := fmt.Sprintf("%s:%s", cd.Name, cd.Version) - cd, ok := cdMap[key] - if !ok { - return nil, fmt.Errorf("unable to find component descriptor in map") - } + if o.GenerateSignature { + signer, err := signatures.CreateRsaSignerFromKeyFile(o.PrivateKeyPath) + if err != nil { + return fmt.Errorf("unable to create signer: %w", err) + } - signature, err := signatures.SelectSignatureByName(cd, o.SignatureName) - if err != nil { - return nil, fmt.Errorf("unable to get signature from component descriptor: %w", err) - } + hasher, err := signatures.HasherForName("SHA256") + if err != nil { + return fmt.Errorf("unable to create hasher: %w", err) + } - return &signature.Digest, nil + crr := func(ctx context.Context, cd cdv2.ComponentDescriptor, ref cdv2.ComponentReference) (*cdv2.DigestSpec, error) { + key := fmt.Sprintf("%s:%s", ref.Name, ref.Version) + cd2, ok := cdLookup[key] + if !ok { + return nil, fmt.Errorf("unable to find component descriptor in map: %w", err) } - signer, err := signatures.CreateRsaSignerFromKeyFile(o.PrivateKeyPath) + signature, err := signatures.SelectSignatureByName(cd2, o.SignatureName) if err != nil { - return fmt.Errorf("unable to create signer: %w", err) + return nil, fmt.Errorf("unable to get signature: %w", err) } - hasher, err := signatures.HasherForName("SHA256") - if err != nil { - return fmt.Errorf("unable to create hasher: %w", err) - } + return &signature.Digest, nil + } + + // iterate backwards -> start with "leave" component descriptors w/o dependencies + for i := len(cds) - 1; i >= 0; i-- { + cd := cds[i] + componentLog := log.WithValues("component-name", cd.Name, "component-version", cd.Version) if err := signatures.AddDigestsToComponentDescriptor(ctx, cd, crr, nil); err != nil { - return fmt.Errorf("unable to add digests to component descriptor: %w", err) + componentLog.Error(err, "unable to add digests to component descriptor") + return err } if err := signatures.SignComponentDescriptor(cd, signer, *hasher, o.SignatureName); err != nil { - return fmt.Errorf("unable to sign component descriptor: %w", err) + componentLog.Error(err, "unable to sign component descriptor") + return err } } } - for _, cd := range cdMap { + for _, cd := range cdLookup { cd := cd + componentLog := log.WithValues("component-name", cd.Name, "component-version", cd.Version) wg.Add(1) go func() { defer wg.Done() manifest, err := cdoci.NewManifestBuilder(ociCache, ctf.NewComponentArchive(cd, nil)).Build(ctx) if err != nil { - log.Error(err, "unable to build oci artifact for component archive") + componentLog.Error(err, "unable to build oci artifact for component archive") return } ociRef, err := cdoci.OCIRef(*targetCtx, o.ComponentName, o.Version) if err != nil { - log.Error(err, "unable to build component descriptor oci reference") + componentLog.Error(err, "unable to build component descriptor oci reference") return } if err := ociClient.PushManifest(ctx, ociRef, manifest); err != nil { - log.Error(err, "unable to push component descriptor manifest") + componentLog.Error(err, "unable to push component descriptor manifest") return } }() @@ -267,10 +280,14 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e wg.Wait() + if len(errs) > 0 { + return fmt.Errorf("%d errors occurred during component descriptor uploading", len(errs)) + } + return nil } -func handleResources(ctx context.Context, cd *cdv2.ComponentDescriptor, targetCtx cdv2.OCIRegistryRepository, log logr.Logger, processingJobFactory *transport_config.ProcessingJobFactory) ([]cdv2.Resource, []error) { +func processResources(ctx context.Context, cd *cdv2.ComponentDescriptor, targetCtx cdv2.OCIRegistryRepository, log logr.Logger, processingJobFactory *transport_config.ProcessingJobFactory) ([]cdv2.Resource, error) { wg := sync.WaitGroup{} errs := []error{} errsMux := sync.Mutex{} @@ -279,6 +296,7 @@ func handleResources(ctx context.Context, cd *cdv2.ComponentDescriptor, targetCt for _, resource := range cd.Resources { resource := resource + resourceLog := log.WithValues("resource-name", resource.Name, "resource-version", resource.Version) wg.Add(1) go func() { @@ -286,15 +304,17 @@ func handleResources(ctx context.Context, cd *cdv2.ComponentDescriptor, targetCt job, err := processingJobFactory.Create(*cd, resource) if err != nil { + resourceLog.Error(err, "unable to create processing job") errsMux.Lock() - errs = append(errs, fmt.Errorf("unable to create processing job: %w", err)) + errs = append(errs, err) errsMux.Unlock() return } if err = job.Process(ctx); err != nil { + resourceLog.Error(err, "unable to process resource") errsMux.Lock() - errs = append(errs, fmt.Errorf("unable to process resource %+v: %w", resource, err)) + errs = append(errs, err) errsMux.Unlock() return } @@ -306,7 +326,12 @@ func handleResources(ctx context.Context, cd *cdv2.ComponentDescriptor, targetCt } wg.Wait() - return processedResources, errs + + if len(errs) > 0 { + return nil, errors.New("unable to process resources") + } + + return processedResources, nil } func ResolveRecursive( @@ -316,31 +341,31 @@ func ResolveRecursive( componentName, componentVersion string, repoCtxOverrideCfg *utils.RepositoryContextOverride, + log logr.Logger, ) ([]*cdv2.ComponentDescriptor, error) { + componentLog := log.WithValues("component-name", componentName, "component-version", componentVersion) repoCtx := defaultRepo if repoCtxOverrideCfg != nil { repoCtx = *repoCtxOverrideCfg.GetRepositoryContext(componentName, defaultRepo) - } - - ociRef, err := cdoci.OCIRef(repoCtx, componentName, componentVersion) - if err != nil { - return nil, fmt.Errorf("invalid component reference: %w", err) + componentLog.V(7).Info("repository context after override", "repository-context", repoCtx) } cdresolver := cdoci.NewResolver(client) cd, err := cdresolver.Resolve(ctx, &repoCtx, componentName, componentVersion) if err != nil { - return nil, fmt.Errorf("unable to to fetch component descriptor %s: %w", ociRef, err) + componentLog.Error(err, "unable to fetch component descriptor") + return nil, err } cds := []*cdv2.ComponentDescriptor{ cd, } for _, ref := range cd.ComponentReferences { - cds2, err := ResolveRecursive(ctx, client, defaultRepo, ref.ComponentName, ref.Version, repoCtxOverrideCfg) + cds2, err := ResolveRecursive(ctx, client, defaultRepo, ref.ComponentName, ref.Version, repoCtxOverrideCfg, log) if err != nil { - return nil, fmt.Errorf("unable to resolve ref %+v: %w", ref, err) + componentLog.Error(err, "unable to resolve ref", "ref", ref) + return nil, err } cds = append(cds, cds2...) } From 038e79453f3c19682edec296f9dd4612af1f5426 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 14 Dec 2021 10:09:05 +0100 Subject: [PATCH 84/94] implements dry-run + refactoring --- pkg/commands/transport/transport.go | 37 ++++++++++++++++-- pkg/transport/config/processing_job.go | 48 ++++++++++++++++-------- pkg/transport/config/transport_config.go | 12 +++--- pkg/utils/repo_ctx_override.go | 2 +- 4 files changed, 74 insertions(+), 25 deletions(-) diff --git a/pkg/commands/transport/transport.go b/pkg/commands/transport/transport.go index 9f8c37ee..d5aa1c2c 100644 --- a/pkg/commands/transport/transport.go +++ b/pkg/commands/transport/transport.go @@ -48,6 +48,8 @@ type Options struct { SignatureName string PrivateKeyPath string + DryRun bool + // OCIOptions contains all oci client related options. OCIOptions ociopts.Options } @@ -81,6 +83,7 @@ func (o *Options) AddFlags(fs *pflag.FlagSet) { fs.BoolVar(&o.GenerateSignature, "sign", false, "sign the uploaded component descriptors") fs.StringVar(&o.SignatureName, "signature-name", "", "name of the generated signature") fs.StringVar(&o.PrivateKeyPath, "private-key", "", "path to the private key file used for signing") + fs.BoolVar(&o.DryRun, "dry-run", false, "only download component descriptors and perform matching of resources against transport config file. no component descriptors are uploaded, no resources are down/uploaded") o.OCIOptions.AddFlags(fs) } @@ -128,6 +131,10 @@ func (o *Options) Complete(args []string) error { } func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) error { + if o.DryRun { + log.Info("dry-run: no component descriptors are uploaded, no resources are down/uploaded") + } + ociClient, _, err := o.OCIOptions.Build(log, fs) if err != nil { return fmt.Errorf("unable to build oci client: %s", err.Error()) @@ -144,7 +151,7 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e var repoCtxOverride *utils.RepositoryContextOverride if o.RepoCtxOverrideCfgPath != "" { - repoCtxOverride, err = utils.ParseRepositoryContextConfig(o.RepoCtxOverrideCfgPath) + repoCtxOverride, err = utils.ParseRepositoryContextOverrideConfig(o.RepoCtxOverrideCfgPath) if err != nil { return fmt.Errorf("unable to parse repository context override config file: %w", err) } @@ -168,7 +175,23 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e uf := transport_config.NewUploaderFactory(ociClient, ociCache, *targetCtx) pjf, err := transport_config.NewProcessingJobFactory(*transportCfg, df, pf, uf) if err != nil { - return err + return fmt.Errorf("unable to create processing job factory: %w", err) + } + + if o.DryRun { + for _, cd := range cds { + componentLog := log.WithValues("component-name", cd.Name, "component-version", cd.Version) + for _, res := range cd.Resources { + resourceLog := componentLog.WithValues("resource-name", res.Name, "resource-version", res.Version) + job, err := pjf.Create(*cd, res) + if err != nil { + resourceLog.Error(err, "unable to create processing job") + return err + } + resourceLog.Info("matched resource", "matching", job.GetMatching()) + } + } + return nil } wg := sync.WaitGroup{} @@ -287,7 +310,13 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e return nil } -func processResources(ctx context.Context, cd *cdv2.ComponentDescriptor, targetCtx cdv2.OCIRegistryRepository, log logr.Logger, processingJobFactory *transport_config.ProcessingJobFactory) ([]cdv2.Resource, error) { +func processResources( + ctx context.Context, + cd *cdv2.ComponentDescriptor, + targetCtx cdv2.OCIRegistryRepository, + log logr.Logger, + processingJobFactory *transport_config.ProcessingJobFactory, +) ([]cdv2.Resource, error) { wg := sync.WaitGroup{} errs := []error{} errsMux := sync.Mutex{} @@ -311,6 +340,8 @@ func processResources(ctx context.Context, cd *cdv2.ComponentDescriptor, targetC return } + resourceLog.V(5).Info("matched resource", "matching", job.GetMatching()) + if err = job.Process(ctx); err != nil { resourceLog.Error(err, "unable to process resource") errsMux.Lock() diff --git a/pkg/transport/config/processing_job.go b/pkg/transport/config/processing_job.go index d7f8c808..0439ce33 100644 --- a/pkg/transport/config/processing_job.go +++ b/pkg/transport/config/processing_job.go @@ -18,12 +18,13 @@ import ( // ProcessingJob defines a type which contains all data for processing a single resource type ProcessingJob struct { - ComponentDescriptor *cdv2.ComponentDescriptor - Resource *cdv2.Resource - Downloaders []NamedResourceStreamProcessor - Processors []NamedResourceStreamProcessor - Uploaders []NamedResourceStreamProcessor - ProcessedResource *cdv2.Resource + ComponentDescriptor *cdv2.ComponentDescriptor + Resource *cdv2.Resource + Downloaders []NamedResourceStreamProcessor + Processors []NamedResourceStreamProcessor + Uploaders []NamedResourceStreamProcessor + ProcessedResource *cdv2.Resource + MatchedProcessingRules []string } type NamedResourceStreamProcessor struct { @@ -51,17 +52,17 @@ type parsedUploaderDefinition struct { Filters []filters.Filter } -type parsedRuleDefinition struct { +type parsedProcessingRuleDefinition struct { Name string Processors []string Filters []filters.Filter } type ParsedTransportConfig struct { - Downloaders []parsedDownloaderDefinition - Processors []parsedProcessorDefinition - Uploaders []parsedUploaderDefinition - Rules []parsedRuleDefinition + Downloaders []parsedDownloaderDefinition + Processors []parsedProcessorDefinition + Uploaders []parsedUploaderDefinition + ProcessingRules []parsedProcessingRuleDefinition } // NewProcessingJobFactory creates a new processing job factory @@ -136,7 +137,7 @@ func ParseConfig(configFilePath string) (*ParsedTransportConfig, error) { } // rules - for _, rule := range config.Rules { + for _, rule := range config.ProcessingRules { processors := []string{} for _, processor := range rule.Processors { processors = append(processors, processor.Name) @@ -145,12 +146,12 @@ func ParseConfig(configFilePath string) (*ParsedTransportConfig, error) { if err != nil { return nil, fmt.Errorf("unable to create rule %s: %w", rule.Name, err) } - rule := parsedRuleDefinition{ + rule := parsedProcessingRuleDefinition{ Name: rule.Name, Processors: processors, Filters: filters, } - parsedConfig.Rules = append(parsedConfig.Rules, rule) + parsedConfig.ProcessingRules = append(parsedConfig.ProcessingRules, rule) } return &parsedConfig, nil @@ -192,7 +193,7 @@ func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Reso } // find matching processing rules - for _, rule := range c.parsedConfig.Rules { + for _, rule := range c.parsedConfig.ProcessingRules { if areAllFiltersMatching(rule.Filters, cd, res) { for _, processorName := range rule.Processors { processorDefined, err := findProcessorByName(processorName, c.parsedConfig) @@ -207,6 +208,7 @@ func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Reso Name: processorDefined.Name, Processor: p, }) + job.MatchedProcessingRules = append(job.MatchedProcessingRules, rule.Name) } } } @@ -270,3 +272,19 @@ func (j *ProcessingJob) Process(ctx context.Context) error { return nil } + +func (j *ProcessingJob) GetMatching() map[string][]string { + matching := map[string][]string{ + "processingRules": j.MatchedProcessingRules, + } + + for _, d := range j.Downloaders { + matching["downloaders"] = append(matching["downloaders"], d.Name) + } + + for _, u := range j.Uploaders { + matching["uploaders"] = append(matching["uploaders"], u.Name) + } + + return matching +} diff --git a/pkg/transport/config/transport_config.go b/pkg/transport/config/transport_config.go index 26aac7a0..5d02677b 100644 --- a/pkg/transport/config/transport_config.go +++ b/pkg/transport/config/transport_config.go @@ -10,11 +10,11 @@ type meta struct { } type TransportConfig struct { - Meta meta `json:"meta"` - Uploaders []UploaderDefinition `json:"uploaders"` - Processors []ProcessorDefinition `json:"processors"` - Downloaders []DownloaderDefinition `json:"downloaders"` - Rules []Rule `json:"rules"` + Meta meta `json:"meta"` + Uploaders []UploaderDefinition `json:"uploaders"` + Processors []ProcessorDefinition `json:"processors"` + Downloaders []DownloaderDefinition `json:"downloaders"` + ProcessingRules []ProcessingRule `json:"processingRules"` } type BaseProcessorDefinition struct { @@ -47,7 +47,7 @@ type ProcessorReference struct { Type string `json:"type"` } -type Rule struct { +type ProcessingRule struct { Name string Filters []FilterDefinition `json:"filters"` Processors []ProcessorReference `json:"processors"` diff --git a/pkg/utils/repo_ctx_override.go b/pkg/utils/repo_ctx_override.go index e3cf6985..d1d4cb0f 100644 --- a/pkg/utils/repo_ctx_override.go +++ b/pkg/utils/repo_ctx_override.go @@ -22,7 +22,7 @@ type Override struct { RepositoryContext *cdv2.OCIRegistryRepository } -func ParseRepositoryContextConfig(configPath string) (*RepositoryContextOverride, error) { +func ParseRepositoryContextOverrideConfig(configPath string) (*RepositoryContextOverride, error) { type meta struct { Version string `json:"version"` } From aa6e24fd1c9bf6ad15ad5aea38ee63e2abc11c2d Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 14 Dec 2021 16:05:47 +0100 Subject: [PATCH 85/94] refactoring --- pkg/commands/transport/transport.go | 14 ++- pkg/transport/config/config_suite_test.go | 2 +- pkg/transport/config/processing_job.go | 107 ++++++++++++++++---- pkg/transport/config/processing_job_test.go | 66 ++++++++++++ pkg/transport/config/testdata/transport.cfg | 2 +- pkg/transport/process/pipeline.go | 91 ----------------- pkg/transport/process/pipeline_test.go | 63 ------------ pkg/transport/process/types.go | 12 --- 8 files changed, 160 insertions(+), 197 deletions(-) delete mode 100644 pkg/transport/process/pipeline.go delete mode 100644 pkg/transport/process/pipeline_test.go diff --git a/pkg/commands/transport/transport.go b/pkg/commands/transport/transport.go index d5aa1c2c..fbccbfd2 100644 --- a/pkg/commands/transport/transport.go +++ b/pkg/commands/transport/transport.go @@ -167,13 +167,13 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e cds, err := ResolveRecursive(ctx, ociClient, *sourceCtx, o.ComponentName, o.Version, repoCtxOverride, log) if err != nil { - return fmt.Errorf("unable to resolve component: %w", err) + return fmt.Errorf("unable to resolve component descriptors: %w", err) } df := transport_config.NewDownloaderFactory(ociClient, ociCache) pf := transport_config.NewProcessorFactory(ociCache) uf := transport_config.NewUploaderFactory(ociClient, ociCache, *targetCtx) - pjf, err := transport_config.NewProcessingJobFactory(*transportCfg, df, pf, uf) + pjf, err := transport_config.NewProcessingJobFactory(*transportCfg, df, pf, uf, log) if err != nil { return fmt.Errorf("unable to create processing job factory: %w", err) } @@ -333,9 +333,8 @@ func processResources( job, err := processingJobFactory.Create(*cd, resource) if err != nil { - resourceLog.Error(err, "unable to create processing job") errsMux.Lock() - errs = append(errs, err) + errs = append(errs, fmt.Errorf("unable to create processing job: %w", err)) errsMux.Unlock() return } @@ -343,9 +342,8 @@ func processResources( resourceLog.V(5).Info("matched resource", "matching", job.GetMatching()) if err = job.Process(ctx); err != nil { - resourceLog.Error(err, "unable to process resource") errsMux.Lock() - errs = append(errs, err) + errs = append(errs, fmt.Errorf("unable to process resource: %w", err)) errsMux.Unlock() return } @@ -393,12 +391,12 @@ func ResolveRecursive( cd, } for _, ref := range cd.ComponentReferences { - cds2, err := ResolveRecursive(ctx, client, defaultRepo, ref.ComponentName, ref.Version, repoCtxOverrideCfg, log) + cdDeps, err := ResolveRecursive(ctx, client, defaultRepo, ref.ComponentName, ref.Version, repoCtxOverrideCfg, log) if err != nil { componentLog.Error(err, "unable to resolve ref", "ref", ref) return nil, err } - cds = append(cds, cds2...) + cds = append(cds, cdDeps...) } return cds, nil diff --git a/pkg/transport/config/config_suite_test.go b/pkg/transport/config/config_suite_test.go index b3024e86..bce06bbd 100644 --- a/pkg/transport/config/config_suite_test.go +++ b/pkg/transport/config/config_suite_test.go @@ -38,6 +38,6 @@ var _ = BeforeSuite(func() { pf := config.NewProcessorFactory(ocicache) uf := config.NewUploaderFactory(client, ocicache, *targetCtx) - factory, err = config.NewProcessingJobFactory(*transportCfg, df, pf, uf) + factory, err = config.NewProcessingJobFactory(*transportCfg, df, pf, uf, logr.Discard()) Expect(err).ToNot(HaveOccurred()) }, 5) diff --git a/pkg/transport/config/processing_job.go b/pkg/transport/config/processing_job.go index 0439ce33..17513c7d 100644 --- a/pkg/transport/config/processing_job.go +++ b/pkg/transport/config/processing_job.go @@ -7,16 +7,27 @@ import ( "context" "encoding/json" "fmt" + "io" + "io/ioutil" "os" + "time" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "github.com/go-logr/logr" "sigs.k8s.io/yaml" "github.com/gardener/component-cli/pkg/transport/filters" "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/utils" ) +const processorTimeout = 600 * time.Second + // ProcessingJob defines a type which contains all data for processing a single resource +// ProcessingJob describes a chain of multiple processors for processing a resource. +// Each processor receives its input from the preceding processor and writes the output for the +// subsequent processor. To work correctly, a pipeline must consist of 1 downloader, 0..n processors, +// and 1..n uploaders. type ProcessingJob struct { ComponentDescriptor *cdv2.ComponentDescriptor Resource *cdv2.Resource @@ -25,6 +36,7 @@ type ProcessingJob struct { Uploaders []NamedResourceStreamProcessor ProcessedResource *cdv2.Resource MatchedProcessingRules []string + Log logr.Logger } type NamedResourceStreamProcessor struct { @@ -66,12 +78,13 @@ type ParsedTransportConfig struct { } // NewProcessingJobFactory creates a new processing job factory -func NewProcessingJobFactory(transportCfg ParsedTransportConfig, df *DownloaderFactory, pf *ProcessorFactory, uf *UploaderFactory) (*ProcessingJobFactory, error) { +func NewProcessingJobFactory(transportCfg ParsedTransportConfig, df *DownloaderFactory, pf *ProcessorFactory, uf *UploaderFactory, log logr.Logger) (*ProcessingJobFactory, error) { c := ProcessingJobFactory{ parsedConfig: &transportCfg, downloaderFactory: df, processorFactory: pf, uploaderFactory: uf, + log: log, } return &c, nil @@ -83,6 +96,7 @@ type ProcessingJobFactory struct { uploaderFactory *UploaderFactory downloaderFactory *DownloaderFactory processorFactory *ProcessorFactory + log logr.Logger } func ParseConfig(configFilePath string) (*ParsedTransportConfig, error) { @@ -159,9 +173,11 @@ func ParseConfig(configFilePath string) (*ParsedTransportConfig, error) { // Create creates a new processing job for a resource func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Resource) (*ProcessingJob, error) { + jobLog := c.log.WithValues("component-name", cd.Name, "component-version", cd.Version, "resource-name", res.Name, "resource-version", res.Version) job := ProcessingJob{ ComponentDescriptor: &cd, Resource: &res, + Log: jobLog, } // find matching downloader @@ -178,7 +194,7 @@ func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Reso } } - // find matching uploader + // find matching uploaders for _, uploader := range c.parsedConfig.Uploaders { if areAllFiltersMatching(uploader.Filters, cd, res) { ul, err := c.uploaderFactory.Create(string(uploader.Type), uploader.Spec) @@ -246,45 +262,94 @@ func findProcessorByName(name string, lookup *ParsedTransportConfig) (*parsedPro return nil, fmt.Errorf("unable to find processor %s", name) } -// Process processes the resource -func (j *ProcessingJob) Process(ctx context.Context) error { - processors := []process.ResourceStreamProcessor{} +func (j *ProcessingJob) GetMatching() map[string][]string { + matching := map[string][]string{ + "processingRules": j.MatchedProcessingRules, + } for _, d := range j.Downloaders { - processors = append(processors, d.Processor) + matching["downloaders"] = append(matching["downloaders"], d.Name) } - for _, p := range j.Processors { - processors = append(processors, p.Processor) + for _, u := range j.Uploaders { + matching["uploaders"] = append(matching["uploaders"], u.Name) } - for _, u := range j.Uploaders { - processors = append(processors, u.Processor) + return matching +} + +// Process processes the resource +func (p *ProcessingJob) Process(ctx context.Context) error { + inputFile, err := ioutil.TempFile("", "") + if err != nil { + p.Log.Error(err, "unable to create temporary input file") + return err + } + + if err := utils.WriteProcessorMessage(*p.ComponentDescriptor, *p.Resource, nil, inputFile); err != nil { + p.Log.Error(err, "unable to write processor message") + return err + } + + processors := []NamedResourceStreamProcessor{} + processors = append(processors, p.Downloaders...) + processors = append(processors, p.Processors...) + processors = append(processors, p.Uploaders...) + + for _, proc := range processors { + procLog := p.Log.WithValues("processor-name", proc.Name) + outputFile, err := p.runProcessor(ctx, inputFile, proc, procLog) + if err != nil { + procLog.Error(err, "unable to run processor") + return err + } + + // set the output file of the current processor as the input file for the next processor + // if the current processor isn't last in the chain -> close file in runProcessor() in next loop iteration + // if the current processor is last in the chain -> explicitely close file after loop + inputFile = outputFile } + defer inputFile.Close() - p := process.NewResourceProcessingPipeline(processors...) - _, processedResource, err := p.Process(ctx, *j.ComponentDescriptor, *j.Resource) + if _, err := inputFile.Seek(0, io.SeekStart); err != nil { + p.Log.Error(err, "unable to seek to beginning of file") + return err + } + + _, processedRes, blobreader, err := utils.ReadProcessorMessage(inputFile) if err != nil { + p.Log.Error(err, "unable to read processor message") return err } + if blobreader != nil { + defer blobreader.Close() + } - j.ProcessedResource = &processedResource + p.ProcessedResource = &processedRes return nil } -func (j *ProcessingJob) GetMatching() map[string][]string { - matching := map[string][]string{ - "processingRules": j.MatchedProcessingRules, +func (p *ProcessingJob) runProcessor(ctx context.Context, infile *os.File, proc NamedResourceStreamProcessor, log logr.Logger) (*os.File, error) { + defer infile.Close() + + if _, err := infile.Seek(0, io.SeekStart); err != nil { + return nil, fmt.Errorf("unable to seek to beginning of input file: %w", err) } - for _, d := range j.Downloaders { - matching["downloaders"] = append(matching["downloaders"], d.Name) + outfile, err := ioutil.TempFile("", "") + if err != nil { + return nil, fmt.Errorf("unable to create temporary output file: %w", err) } - for _, u := range j.Uploaders { - matching["uploaders"] = append(matching["uploaders"], u.Name) + ctx, cancelfunc := context.WithTimeout(ctx, processorTimeout) + defer cancelfunc() + + log.V(7).Info("starting processor") + if err := proc.Processor.Process(ctx, infile, outfile); err != nil { + return nil, fmt.Errorf("processor returned with error: %w", err) } + log.V(7).Info("processor finished successfully") - return matching + return outfile, nil } diff --git a/pkg/transport/config/processing_job_test.go b/pkg/transport/config/processing_job_test.go index 90a4121c..80c82343 100644 --- a/pkg/transport/config/processing_job_test.go +++ b/pkg/transport/config/processing_job_test.go @@ -4,9 +4,16 @@ package config_test import ( + "context" + "encoding/json" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "github.com/go-logr/logr" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + + "github.com/gardener/component-cli/pkg/transport/config" + "github.com/gardener/component-cli/pkg/transport/process/processors" ) var _ = Describe("processing job", func() { @@ -89,4 +96,63 @@ var _ = Describe("processing job", func() { }) + Context("processing job", func() { + + It("should correctly process resource", func() { + res := cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "my-res", + Version: "v0.1.0", + Type: "ociImage", + }, + } + + l1 := cdv2.Label{ + Name: "processor-0", + Value: json.RawMessage(`"true"`), + } + l2 := cdv2.Label{ + Name: "processor-1", + Value: json.RawMessage(`"true"`), + } + expectedRes := res + expectedRes.Labels = append(expectedRes.Labels, l1) + expectedRes.Labels = append(expectedRes.Labels, l2) + + cd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + Resources: []cdv2.Resource{ + res, + }, + }, + } + + p1 := processors.NewResourceLabeler(l1) + p2 := processors.NewResourceLabeler(l2) + + procs := []config.NamedResourceStreamProcessor{ + { + Name: "p1", + Processor: p1, + }, + { + Name: "p2", + Processor: p2, + }, + } + + pj := config.ProcessingJob{ + ComponentDescriptor: &cd, + Resource: &res, + Processors: procs, + Log: logr.Discard(), + } + + err := pj.Process(context.TODO()) + Expect(err).ToNot(HaveOccurred()) + Expect(*pj.ProcessedResource).To(Equal(expectedRes)) + }) + + }) + }) diff --git a/pkg/transport/config/testdata/transport.cfg b/pkg/transport/config/testdata/transport.cfg index 8b311f17..73fd7edb 100644 --- a/pkg/transport/config/testdata/transport.cfg +++ b/pkg/transport/config/testdata/transport.cfg @@ -57,7 +57,7 @@ uploaders: includeAccessTypes: - 'localOciBlob' -rules: +processingRules: - name: 'generic-image-filtering' processors: - name: 'my-oci-filter' diff --git a/pkg/transport/process/pipeline.go b/pkg/transport/process/pipeline.go deleted file mode 100644 index 5d92e910..00000000 --- a/pkg/transport/process/pipeline.go +++ /dev/null @@ -1,91 +0,0 @@ -// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. -// -// SPDX-License-Identifier: Apache-2.0 -package process - -import ( - "context" - "fmt" - "io" - "io/ioutil" - "os" - "time" - - cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" - - "github.com/gardener/component-cli/pkg/transport/process/utils" -) - -const processorTimeout = 600 * time.Second - -type resourceProcessingPipelineImpl struct { - processors []ResourceStreamProcessor -} - -func (p *resourceProcessingPipelineImpl) Process(ctx context.Context, cd cdv2.ComponentDescriptor, res cdv2.Resource) (*cdv2.ComponentDescriptor, cdv2.Resource, error) { - infile, err := ioutil.TempFile("", "") - if err != nil { - return nil, cdv2.Resource{}, fmt.Errorf("unable to create temporary infile: %w", err) - } - - if err := utils.WriteProcessorMessage(cd, res, nil, infile); err != nil { - return nil, cdv2.Resource{}, fmt.Errorf("unable to write: %w", err) - } - - for _, proc := range p.processors { - outfile, err := p.runProcessor(ctx, infile, proc) - if err != nil { - return nil, cdv2.Resource{}, err - } - - infile = outfile - } - defer infile.Close() - - if _, err := infile.Seek(0, io.SeekStart); err != nil { - return nil, cdv2.Resource{}, fmt.Errorf("unable to seek to beginning of file: %w", err) - } - - processedCD, processedRes, blobreader, err := utils.ReadProcessorMessage(infile) - if err != nil { - return nil, cdv2.Resource{}, fmt.Errorf("unable to read output data: %w", err) - } - if blobreader != nil { - defer blobreader.Close() - } - - return processedCD, processedRes, nil -} - -func (p *resourceProcessingPipelineImpl) runProcessor(ctx context.Context, infile *os.File, proc ResourceStreamProcessor) (*os.File, error) { - defer infile.Close() - - if _, err := infile.Seek(0, io.SeekStart); err != nil { - return nil, fmt.Errorf("unable to seek to beginning of input file: %w", err) - } - - outfile, err := ioutil.TempFile("", "") - if err != nil { - return nil, fmt.Errorf("unable to create temporary outfile: %w", err) - } - - inreader := infile - outwriter := outfile - - ctx, cancelfunc := context.WithTimeout(ctx, processorTimeout) - defer cancelfunc() - - if err := proc.Process(ctx, inreader, outwriter); err != nil { - return nil, fmt.Errorf("unable to process resource: %w", err) - } - - return outfile, nil -} - -// NewResourceProcessingPipeline returns a new ResourceProcessingPipeline -func NewResourceProcessingPipeline(processors ...ResourceStreamProcessor) ResourceProcessingPipeline { - p := resourceProcessingPipelineImpl{ - processors: processors, - } - return &p -} diff --git a/pkg/transport/process/pipeline_test.go b/pkg/transport/process/pipeline_test.go deleted file mode 100644 index baaff70a..00000000 --- a/pkg/transport/process/pipeline_test.go +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. -// -// SPDX-License-Identifier: Apache-2.0 -package process_test - -import ( - "context" - "encoding/json" - - cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "github.com/gardener/component-cli/pkg/transport/process" - "github.com/gardener/component-cli/pkg/transport/process/processors" -) - -var _ = Describe("pipeline", func() { - - Context("Process", func() { - - It("should correctly process resource", func() { - res := cdv2.Resource{ - IdentityObjectMeta: cdv2.IdentityObjectMeta{ - Name: "my-res", - Version: "v0.1.0", - Type: "ociImage", - }, - } - - l1 := cdv2.Label{ - Name: "processor-0", - Value: json.RawMessage(`"true"`), - } - l2 := cdv2.Label{ - Name: "processor-1", - Value: json.RawMessage(`"true"`), - } - expectedRes := res - expectedRes.Labels = append(expectedRes.Labels, l1) - expectedRes.Labels = append(expectedRes.Labels, l2) - - cd := cdv2.ComponentDescriptor{ - ComponentSpec: cdv2.ComponentSpec{ - Resources: []cdv2.Resource{ - res, - }, - }, - } - - p1 := processors.NewResourceLabeler(l1) - p2 := processors.NewResourceLabeler(l2) - pipeline := process.NewResourceProcessingPipeline(p1, p2) - - actualCD, actualRes, err := pipeline.Process(context.TODO(), cd, res) - Expect(err).ToNot(HaveOccurred()) - - Expect(*actualCD).To(Equal(cd)) - Expect(actualRes).To(Equal(expectedRes)) - }) - - }) -}) diff --git a/pkg/transport/process/types.go b/pkg/transport/process/types.go index d8b69eb3..cbfd49b8 100644 --- a/pkg/transport/process/types.go +++ b/pkg/transport/process/types.go @@ -6,20 +6,8 @@ package process import ( "context" "io" - - cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" ) -// ResourceProcessingPipeline describes a chain of multiple processors for processing a resource. -// Each processor receives its input from the preceding processor and writes the output for the -// subsequent processor. To work correctly, a pipeline must consist of 1 downloader, 0..n processors, -// and 1..n uploaders. -type ResourceProcessingPipeline interface { - // Process executes all processors for a resource. - // Returns the component descriptor and resource of the last processor. - Process(context.Context, cdv2.ComponentDescriptor, cdv2.Resource) (*cdv2.ComponentDescriptor, cdv2.Resource, error) -} - // ResourceStreamProcessor describes an individual processor for processing a resource. // A processor can upload, modify, or download a resource. type ResourceStreamProcessor interface { From 61edcc34a1154faa50cfca359c691ceb9f43cdff Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 14 Dec 2021 16:16:55 +0100 Subject: [PATCH 86/94] adds processor timeout cli option --- pkg/commands/transport/transport.go | 7 +++++-- pkg/transport/config/config_suite_test.go | 3 ++- pkg/transport/config/processing_job.go | 10 ++++++---- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/pkg/commands/transport/transport.go b/pkg/commands/transport/transport.go index fbccbfd2..a7ad1365 100644 --- a/pkg/commands/transport/transport.go +++ b/pkg/commands/transport/transport.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "sync" + "time" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" "github.com/gardener/component-spec/bindings-go/apis/v2/signatures" @@ -48,7 +49,8 @@ type Options struct { SignatureName string PrivateKeyPath string - DryRun bool + DryRun bool + ProcessorTimeout time.Duration // OCIOptions contains all oci client related options. OCIOptions ociopts.Options @@ -84,6 +86,7 @@ func (o *Options) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&o.SignatureName, "signature-name", "", "name of the generated signature") fs.StringVar(&o.PrivateKeyPath, "private-key", "", "path to the private key file used for signing") fs.BoolVar(&o.DryRun, "dry-run", false, "only download component descriptors and perform matching of resources against transport config file. no component descriptors are uploaded, no resources are down/uploaded") + fs.DurationVar(&o.ProcessorTimeout, "processor-timeout", 30*time.Second, "execution timeout for each individual processor") o.OCIOptions.AddFlags(fs) } @@ -173,7 +176,7 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e df := transport_config.NewDownloaderFactory(ociClient, ociCache) pf := transport_config.NewProcessorFactory(ociCache) uf := transport_config.NewUploaderFactory(ociClient, ociCache, *targetCtx) - pjf, err := transport_config.NewProcessingJobFactory(*transportCfg, df, pf, uf, log) + pjf, err := transport_config.NewProcessingJobFactory(*transportCfg, df, pf, uf, log, o.ProcessorTimeout) if err != nil { return fmt.Errorf("unable to create processing job factory: %w", err) } diff --git a/pkg/transport/config/config_suite_test.go b/pkg/transport/config/config_suite_test.go index bce06bbd..a8219a7a 100644 --- a/pkg/transport/config/config_suite_test.go +++ b/pkg/transport/config/config_suite_test.go @@ -5,6 +5,7 @@ package config_test import ( "testing" + "time" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" "github.com/go-logr/logr" @@ -38,6 +39,6 @@ var _ = BeforeSuite(func() { pf := config.NewProcessorFactory(ocicache) uf := config.NewUploaderFactory(client, ocicache, *targetCtx) - factory, err = config.NewProcessingJobFactory(*transportCfg, df, pf, uf, logr.Discard()) + factory, err = config.NewProcessingJobFactory(*transportCfg, df, pf, uf, logr.Discard(), 30*time.Second) Expect(err).ToNot(HaveOccurred()) }, 5) diff --git a/pkg/transport/config/processing_job.go b/pkg/transport/config/processing_job.go index 17513c7d..d8db962c 100644 --- a/pkg/transport/config/processing_job.go +++ b/pkg/transport/config/processing_job.go @@ -21,8 +21,6 @@ import ( "github.com/gardener/component-cli/pkg/transport/process/utils" ) -const processorTimeout = 600 * time.Second - // ProcessingJob defines a type which contains all data for processing a single resource // ProcessingJob describes a chain of multiple processors for processing a resource. // Each processor receives its input from the preceding processor and writes the output for the @@ -37,6 +35,7 @@ type ProcessingJob struct { ProcessedResource *cdv2.Resource MatchedProcessingRules []string Log logr.Logger + ProcessorTimeout time.Duration } type NamedResourceStreamProcessor struct { @@ -78,13 +77,14 @@ type ParsedTransportConfig struct { } // NewProcessingJobFactory creates a new processing job factory -func NewProcessingJobFactory(transportCfg ParsedTransportConfig, df *DownloaderFactory, pf *ProcessorFactory, uf *UploaderFactory, log logr.Logger) (*ProcessingJobFactory, error) { +func NewProcessingJobFactory(transportCfg ParsedTransportConfig, df *DownloaderFactory, pf *ProcessorFactory, uf *UploaderFactory, log logr.Logger, processorTimeout time.Duration) (*ProcessingJobFactory, error) { c := ProcessingJobFactory{ parsedConfig: &transportCfg, downloaderFactory: df, processorFactory: pf, uploaderFactory: uf, log: log, + processorTimeout: processorTimeout, } return &c, nil @@ -97,6 +97,7 @@ type ProcessingJobFactory struct { downloaderFactory *DownloaderFactory processorFactory *ProcessorFactory log logr.Logger + processorTimeout time.Duration } func ParseConfig(configFilePath string) (*ParsedTransportConfig, error) { @@ -178,6 +179,7 @@ func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Reso ComponentDescriptor: &cd, Resource: &res, Log: jobLog, + ProcessorTimeout: c.processorTimeout, } // find matching downloader @@ -342,7 +344,7 @@ func (p *ProcessingJob) runProcessor(ctx context.Context, infile *os.File, proc return nil, fmt.Errorf("unable to create temporary output file: %w", err) } - ctx, cancelfunc := context.WithTimeout(ctx, processorTimeout) + ctx, cancelfunc := context.WithTimeout(ctx, p.ProcessorTimeout) defer cancelfunc() log.V(7).Info("starting processor") From 0baa125513d8437772e8ce496a439fde802f9cae Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 14 Dec 2021 16:33:52 +0100 Subject: [PATCH 87/94] moves processing job to proces package --- ...ssing_job.go => processing_job_factory.go} | 128 +---------------- ...test.go => processing_job_factory_test.go} | 66 --------- pkg/transport/process/processing_job.go | 132 ++++++++++++++++++ pkg/transport/process/processing_job_test.go | 80 +++++++++++ 4 files changed, 217 insertions(+), 189 deletions(-) rename pkg/transport/config/{processing_job.go => processing_job_factory.go} (62%) rename pkg/transport/config/{processing_job_test.go => processing_job_factory_test.go} (63%) create mode 100644 pkg/transport/process/processing_job.go create mode 100644 pkg/transport/process/processing_job_test.go diff --git a/pkg/transport/config/processing_job.go b/pkg/transport/config/processing_job_factory.go similarity index 62% rename from pkg/transport/config/processing_job.go rename to pkg/transport/config/processing_job_factory.go index d8db962c..54992135 100644 --- a/pkg/transport/config/processing_job.go +++ b/pkg/transport/config/processing_job_factory.go @@ -4,11 +4,8 @@ package config import ( - "context" "encoding/json" "fmt" - "io" - "io/ioutil" "os" "time" @@ -18,31 +15,8 @@ import ( "github.com/gardener/component-cli/pkg/transport/filters" "github.com/gardener/component-cli/pkg/transport/process" - "github.com/gardener/component-cli/pkg/transport/process/utils" ) -// ProcessingJob defines a type which contains all data for processing a single resource -// ProcessingJob describes a chain of multiple processors for processing a resource. -// Each processor receives its input from the preceding processor and writes the output for the -// subsequent processor. To work correctly, a pipeline must consist of 1 downloader, 0..n processors, -// and 1..n uploaders. -type ProcessingJob struct { - ComponentDescriptor *cdv2.ComponentDescriptor - Resource *cdv2.Resource - Downloaders []NamedResourceStreamProcessor - Processors []NamedResourceStreamProcessor - Uploaders []NamedResourceStreamProcessor - ProcessedResource *cdv2.Resource - MatchedProcessingRules []string - Log logr.Logger - ProcessorTimeout time.Duration -} - -type NamedResourceStreamProcessor struct { - Processor process.ResourceStreamProcessor - Name string -} - type parsedDownloaderDefinition struct { Name string Type string @@ -173,9 +147,9 @@ func ParseConfig(configFilePath string) (*ParsedTransportConfig, error) { } // Create creates a new processing job for a resource -func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Resource) (*ProcessingJob, error) { +func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Resource) (*process.ProcessingJob, error) { jobLog := c.log.WithValues("component-name", cd.Name, "component-version", cd.Version, "resource-name", res.Name, "resource-version", res.Version) - job := ProcessingJob{ + job := process.ProcessingJob{ ComponentDescriptor: &cd, Resource: &res, Log: jobLog, @@ -189,7 +163,7 @@ func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Reso if err != nil { return nil, err } - job.Downloaders = append(job.Downloaders, NamedResourceStreamProcessor{ + job.Downloaders = append(job.Downloaders, process.NamedResourceStreamProcessor{ Name: downloader.Name, Processor: dl, }) @@ -203,7 +177,7 @@ func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Reso if err != nil { return nil, err } - job.Uploaders = append(job.Uploaders, NamedResourceStreamProcessor{ + job.Uploaders = append(job.Uploaders, process.NamedResourceStreamProcessor{ Name: uploader.Name, Processor: ul, }) @@ -222,7 +196,7 @@ func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Reso if err != nil { return nil, err } - job.Processors = append(job.Processors, NamedResourceStreamProcessor{ + job.Processors = append(job.Processors, process.NamedResourceStreamProcessor{ Name: processorDefined.Name, Processor: p, }) @@ -263,95 +237,3 @@ func findProcessorByName(name string, lookup *ParsedTransportConfig) (*parsedPro } return nil, fmt.Errorf("unable to find processor %s", name) } - -func (j *ProcessingJob) GetMatching() map[string][]string { - matching := map[string][]string{ - "processingRules": j.MatchedProcessingRules, - } - - for _, d := range j.Downloaders { - matching["downloaders"] = append(matching["downloaders"], d.Name) - } - - for _, u := range j.Uploaders { - matching["uploaders"] = append(matching["uploaders"], u.Name) - } - - return matching -} - -// Process processes the resource -func (p *ProcessingJob) Process(ctx context.Context) error { - inputFile, err := ioutil.TempFile("", "") - if err != nil { - p.Log.Error(err, "unable to create temporary input file") - return err - } - - if err := utils.WriteProcessorMessage(*p.ComponentDescriptor, *p.Resource, nil, inputFile); err != nil { - p.Log.Error(err, "unable to write processor message") - return err - } - - processors := []NamedResourceStreamProcessor{} - processors = append(processors, p.Downloaders...) - processors = append(processors, p.Processors...) - processors = append(processors, p.Uploaders...) - - for _, proc := range processors { - procLog := p.Log.WithValues("processor-name", proc.Name) - outputFile, err := p.runProcessor(ctx, inputFile, proc, procLog) - if err != nil { - procLog.Error(err, "unable to run processor") - return err - } - - // set the output file of the current processor as the input file for the next processor - // if the current processor isn't last in the chain -> close file in runProcessor() in next loop iteration - // if the current processor is last in the chain -> explicitely close file after loop - inputFile = outputFile - } - defer inputFile.Close() - - if _, err := inputFile.Seek(0, io.SeekStart); err != nil { - p.Log.Error(err, "unable to seek to beginning of file") - return err - } - - _, processedRes, blobreader, err := utils.ReadProcessorMessage(inputFile) - if err != nil { - p.Log.Error(err, "unable to read processor message") - return err - } - if blobreader != nil { - defer blobreader.Close() - } - - p.ProcessedResource = &processedRes - - return nil -} - -func (p *ProcessingJob) runProcessor(ctx context.Context, infile *os.File, proc NamedResourceStreamProcessor, log logr.Logger) (*os.File, error) { - defer infile.Close() - - if _, err := infile.Seek(0, io.SeekStart); err != nil { - return nil, fmt.Errorf("unable to seek to beginning of input file: %w", err) - } - - outfile, err := ioutil.TempFile("", "") - if err != nil { - return nil, fmt.Errorf("unable to create temporary output file: %w", err) - } - - ctx, cancelfunc := context.WithTimeout(ctx, p.ProcessorTimeout) - defer cancelfunc() - - log.V(7).Info("starting processor") - if err := proc.Processor.Process(ctx, infile, outfile); err != nil { - return nil, fmt.Errorf("processor returned with error: %w", err) - } - log.V(7).Info("processor finished successfully") - - return outfile, nil -} diff --git a/pkg/transport/config/processing_job_test.go b/pkg/transport/config/processing_job_factory_test.go similarity index 63% rename from pkg/transport/config/processing_job_test.go rename to pkg/transport/config/processing_job_factory_test.go index 80c82343..90a4121c 100644 --- a/pkg/transport/config/processing_job_test.go +++ b/pkg/transport/config/processing_job_factory_test.go @@ -4,16 +4,9 @@ package config_test import ( - "context" - "encoding/json" - cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" - "github.com/go-logr/logr" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - - "github.com/gardener/component-cli/pkg/transport/config" - "github.com/gardener/component-cli/pkg/transport/process/processors" ) var _ = Describe("processing job", func() { @@ -96,63 +89,4 @@ var _ = Describe("processing job", func() { }) - Context("processing job", func() { - - It("should correctly process resource", func() { - res := cdv2.Resource{ - IdentityObjectMeta: cdv2.IdentityObjectMeta{ - Name: "my-res", - Version: "v0.1.0", - Type: "ociImage", - }, - } - - l1 := cdv2.Label{ - Name: "processor-0", - Value: json.RawMessage(`"true"`), - } - l2 := cdv2.Label{ - Name: "processor-1", - Value: json.RawMessage(`"true"`), - } - expectedRes := res - expectedRes.Labels = append(expectedRes.Labels, l1) - expectedRes.Labels = append(expectedRes.Labels, l2) - - cd := cdv2.ComponentDescriptor{ - ComponentSpec: cdv2.ComponentSpec{ - Resources: []cdv2.Resource{ - res, - }, - }, - } - - p1 := processors.NewResourceLabeler(l1) - p2 := processors.NewResourceLabeler(l2) - - procs := []config.NamedResourceStreamProcessor{ - { - Name: "p1", - Processor: p1, - }, - { - Name: "p2", - Processor: p2, - }, - } - - pj := config.ProcessingJob{ - ComponentDescriptor: &cd, - Resource: &res, - Processors: procs, - Log: logr.Discard(), - } - - err := pj.Process(context.TODO()) - Expect(err).ToNot(HaveOccurred()) - Expect(*pj.ProcessedResource).To(Equal(expectedRes)) - }) - - }) - }) diff --git a/pkg/transport/process/processing_job.go b/pkg/transport/process/processing_job.go new file mode 100644 index 00000000..134eec3b --- /dev/null +++ b/pkg/transport/process/processing_job.go @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package process + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "os" + "time" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "github.com/go-logr/logr" + + "github.com/gardener/component-cli/pkg/transport/process/utils" +) + +// ProcessingJob defines a type which contains all data for processing a single resource +// ProcessingJob describes a chain of multiple processors for processing a resource. +// Each processor receives its input from the preceding processor and writes the output for the +// subsequent processor. To work correctly, a pipeline must consist of 1 downloader, 0..n processors, +// and 1..n uploaders. +type ProcessingJob struct { + ComponentDescriptor *cdv2.ComponentDescriptor + Resource *cdv2.Resource + Downloaders []NamedResourceStreamProcessor + Processors []NamedResourceStreamProcessor + Uploaders []NamedResourceStreamProcessor + ProcessedResource *cdv2.Resource + MatchedProcessingRules []string + Log logr.Logger + ProcessorTimeout time.Duration +} + +type NamedResourceStreamProcessor struct { + Processor ResourceStreamProcessor + Name string +} + +func (j *ProcessingJob) GetMatching() map[string][]string { + matching := map[string][]string{ + "processingRules": j.MatchedProcessingRules, + } + + for _, d := range j.Downloaders { + matching["downloaders"] = append(matching["downloaders"], d.Name) + } + + for _, u := range j.Uploaders { + matching["uploaders"] = append(matching["uploaders"], u.Name) + } + + return matching +} + +// Process processes the resource +func (p *ProcessingJob) Process(ctx context.Context) error { + inputFile, err := ioutil.TempFile("", "") + if err != nil { + p.Log.Error(err, "unable to create temporary input file") + return err + } + + if err := utils.WriteProcessorMessage(*p.ComponentDescriptor, *p.Resource, nil, inputFile); err != nil { + p.Log.Error(err, "unable to write processor message") + return err + } + + processors := []NamedResourceStreamProcessor{} + processors = append(processors, p.Downloaders...) + processors = append(processors, p.Processors...) + processors = append(processors, p.Uploaders...) + + for _, proc := range processors { + procLog := p.Log.WithValues("processor-name", proc.Name) + outputFile, err := p.runProcessor(ctx, inputFile, proc, procLog) + if err != nil { + procLog.Error(err, "unable to run processor") + return err + } + + // set the output file of the current processor as the input file for the next processor + // if the current processor isn't last in the chain -> close file in runProcessor() in next loop iteration + // if the current processor is last in the chain -> explicitely close file after loop + inputFile = outputFile + } + defer inputFile.Close() + + if _, err := inputFile.Seek(0, io.SeekStart); err != nil { + p.Log.Error(err, "unable to seek to beginning of file") + return err + } + + _, processedRes, blobreader, err := utils.ReadProcessorMessage(inputFile) + if err != nil { + p.Log.Error(err, "unable to read processor message") + return err + } + if blobreader != nil { + defer blobreader.Close() + } + + p.ProcessedResource = &processedRes + + return nil +} + +func (p *ProcessingJob) runProcessor(ctx context.Context, infile *os.File, proc NamedResourceStreamProcessor, log logr.Logger) (*os.File, error) { + defer infile.Close() + + if _, err := infile.Seek(0, io.SeekStart); err != nil { + return nil, fmt.Errorf("unable to seek to beginning of input file: %w", err) + } + + outfile, err := ioutil.TempFile("", "") + if err != nil { + return nil, fmt.Errorf("unable to create temporary output file: %w", err) + } + + ctx, cancelfunc := context.WithTimeout(ctx, p.ProcessorTimeout) + defer cancelfunc() + + log.V(7).Info("starting processor") + if err := proc.Processor.Process(ctx, infile, outfile); err != nil { + return nil, fmt.Errorf("processor returned with error: %w", err) + } + log.V(7).Info("processor finished successfully") + + return outfile, nil +} diff --git a/pkg/transport/process/processing_job_test.go b/pkg/transport/process/processing_job_test.go new file mode 100644 index 00000000..e973df50 --- /dev/null +++ b/pkg/transport/process/processing_job_test.go @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package process_test + +import ( + "context" + "encoding/json" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "github.com/go-logr/logr" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport/process/processors" +) + +var _ = Describe("processing job", func() { + + Context("processing job", func() { + + It("should correctly process resource", func() { + res := cdv2.Resource{ + IdentityObjectMeta: cdv2.IdentityObjectMeta{ + Name: "my-res", + Version: "v0.1.0", + Type: "ociImage", + }, + } + + l1 := cdv2.Label{ + Name: "processor-0", + Value: json.RawMessage(`"true"`), + } + l2 := cdv2.Label{ + Name: "processor-1", + Value: json.RawMessage(`"true"`), + } + expectedRes := res + expectedRes.Labels = append(expectedRes.Labels, l1) + expectedRes.Labels = append(expectedRes.Labels, l2) + + cd := cdv2.ComponentDescriptor{ + ComponentSpec: cdv2.ComponentSpec{ + Resources: []cdv2.Resource{ + res, + }, + }, + } + + p1 := processors.NewResourceLabeler(l1) + p2 := processors.NewResourceLabeler(l2) + + procs := []process.NamedResourceStreamProcessor{ + { + Name: "p1", + Processor: p1, + }, + { + Name: "p2", + Processor: p2, + }, + } + + pj := process.ProcessingJob{ + ComponentDescriptor: &cd, + Resource: &res, + Processors: procs, + Log: logr.Discard(), + } + + err := pj.Process(context.TODO()) + Expect(err).ToNot(HaveOccurred()) + Expect(*pj.ProcessedResource).To(Equal(expectedRes)) + }) + + }) + +}) From 30e4a65865fa54648c0150ae3dcf5e6c46addcdd Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Wed, 15 Dec 2021 11:14:20 +0100 Subject: [PATCH 88/94] refactoring --- pkg/commands/transport/transport.go | 16 +- pkg/transport/config/config.go | 56 ++++ pkg/transport/config/parsed_config.go | 188 ++++++++++++++ .../config/processing_job_factory.go | 239 ------------------ pkg/transport/config/transport_config.go | 54 ---- .../{config => filters}/filter_factory.go | 24 +- .../downloaders}/downloader_factory.go | 12 +- .../util.go => process/extensions/utils.go} | 12 +- .../processors}/processor_factory.go | 16 +- .../uploaders}/uploader_factory.go | 12 +- pkg/transport/{process => }/processing_job.go | 101 ++++++-- pkg/transport/processing_job_factory.go | 99 ++++++++ .../processing_job_factory_test.go | 2 +- .../{process => }/processing_job_test.go | 53 ++-- .../{config => }/testdata/transport.cfg | 0 ..._suite_test.go => transport_suite_test.go} | 15 +- 16 files changed, 506 insertions(+), 393 deletions(-) create mode 100644 pkg/transport/config/config.go create mode 100644 pkg/transport/config/parsed_config.go delete mode 100644 pkg/transport/config/processing_job_factory.go delete mode 100644 pkg/transport/config/transport_config.go rename pkg/transport/{config => filters}/filter_factory.go (78%) rename pkg/transport/{config => process/downloaders}/downloader_factory.go (83%) rename pkg/transport/{config/util.go => process/extensions/utils.go} (53%) rename pkg/transport/{config => process/processors}/processor_factory.go (86%) rename pkg/transport/{config => process/uploaders}/uploader_factory.go (86%) rename pkg/transport/{process => }/processing_job.go (51%) create mode 100644 pkg/transport/processing_job_factory.go rename pkg/transport/{config => }/processing_job_factory_test.go (99%) rename pkg/transport/{process => }/processing_job_test.go (55%) rename pkg/transport/{config => }/testdata/transport.cfg (100%) rename pkg/transport/{config/config_suite_test.go => transport_suite_test.go} (66%) diff --git a/pkg/commands/transport/transport.go b/pkg/commands/transport/transport.go index a7ad1365..2fec8cc9 100644 --- a/pkg/commands/transport/transport.go +++ b/pkg/commands/transport/transport.go @@ -27,7 +27,8 @@ import ( ociopts "github.com/gardener/component-cli/ociclient/options" "github.com/gardener/component-cli/pkg/commands/constants" "github.com/gardener/component-cli/pkg/logger" - transport_config "github.com/gardener/component-cli/pkg/transport/config" + "github.com/gardener/component-cli/pkg/transport" + "github.com/gardener/component-cli/pkg/transport/config" "github.com/gardener/component-cli/pkg/utils" ) @@ -160,7 +161,7 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e } } - transportCfg, err := transport_config.ParseConfig(o.TransportCfgPath) + transportCfg, err := config.ParseTransportConfig(o.TransportCfgPath) if err != nil { return fmt.Errorf("unable to parse transport config file: %w", err) } @@ -173,10 +174,7 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e return fmt.Errorf("unable to resolve component descriptors: %w", err) } - df := transport_config.NewDownloaderFactory(ociClient, ociCache) - pf := transport_config.NewProcessorFactory(ociCache) - uf := transport_config.NewUploaderFactory(ociClient, ociCache, *targetCtx) - pjf, err := transport_config.NewProcessingJobFactory(*transportCfg, df, pf, uf, log, o.ProcessorTimeout) + factory, err := transport.NewProcessingJobFactory(*transportCfg, ociClient, ociCache, *targetCtx, log, o.ProcessorTimeout) if err != nil { return fmt.Errorf("unable to create processing job factory: %w", err) } @@ -186,7 +184,7 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e componentLog := log.WithValues("component-name", cd.Name, "component-version", cd.Version) for _, res := range cd.Resources { resourceLog := componentLog.WithValues("resource-name", res.Name, "resource-version", res.Version) - job, err := pjf.Create(*cd, res) + job, err := factory.Create(*cd, res) if err != nil { resourceLog.Error(err, "unable to create processing job") return err @@ -217,7 +215,7 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e wg.Add(1) go func() { defer wg.Done() - processedResources, err := processResources(ctx, cd, *targetCtx, componentLog, pjf) + processedResources, err := processResources(ctx, cd, *targetCtx, componentLog, factory) if err != nil { errsMux.Lock() errs = append(errs, err) @@ -318,7 +316,7 @@ func processResources( cd *cdv2.ComponentDescriptor, targetCtx cdv2.OCIRegistryRepository, log logr.Logger, - processingJobFactory *transport_config.ProcessingJobFactory, + processingJobFactory *transport.ProcessingJobFactory, ) ([]cdv2.Resource, error) { wg := sync.WaitGroup{} errs := []error{} diff --git a/pkg/transport/config/config.go b/pkg/transport/config/config.go new file mode 100644 index 00000000..c005924b --- /dev/null +++ b/pkg/transport/config/config.go @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package config + +import ( + "encoding/json" +) + +type meta struct { + Version string `json:"version"` +} + +type transportConfig struct { + Meta meta `json:"meta"` + Uploaders []uploaderDefinition `json:"uploaders"` + Processors []processorDefinition `json:"processors"` + Downloaders []downloaderDefinition `json:"downloaders"` + ProcessingRules []processingRuleDefinition `json:"processingRules"` +} + +type baseProcessorDefinition struct { + Name string `json:"name"` + Type string `json:"type"` + Spec *json.RawMessage `json:"spec"` +} + +type filterDefinition struct { + Type string `json:"type"` + Spec *json.RawMessage `json:"spec"` +} + +type downloaderDefinition struct { + baseProcessorDefinition + Filters []filterDefinition `json:"filters"` +} + +type uploaderDefinition struct { + baseProcessorDefinition + Filters []filterDefinition `json:"filters"` +} + +type processorDefinition struct { + baseProcessorDefinition +} + +type processorReference struct { + Name string `json:"name"` + Type string `json:"type"` +} + +type processingRuleDefinition struct { + Name string + Filters []filterDefinition `json:"filters"` + Processors []processorReference `json:"processors"` +} diff --git a/pkg/transport/config/parsed_config.go b/pkg/transport/config/parsed_config.go new file mode 100644 index 00000000..adcfa0cb --- /dev/null +++ b/pkg/transport/config/parsed_config.go @@ -0,0 +1,188 @@ +package config + +import ( + "encoding/json" + "fmt" + "os" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "sigs.k8s.io/yaml" + + "github.com/gardener/component-cli/pkg/transport/filters" +) + +type ParsedTransportConfig struct { + Downloaders []ParsedDownloaderDefinition + Processors []ParsedProcessorDefinition + Uploaders []ParsedUploaderDefinition + ProcessingRules []ParsedProcessingRuleDefinition +} + +type ParsedDownloaderDefinition struct { + Name string + Type string + Spec *json.RawMessage + Filters []filters.Filter +} + +type ParsedProcessorDefinition struct { + Name string + Type string + Spec *json.RawMessage +} + +type ParsedUploaderDefinition struct { + Name string + Type string + Spec *json.RawMessage + Filters []filters.Filter +} + +type ParsedProcessingRuleDefinition struct { + Name string + Processors []ParsedProcessorDefinition + Filters []filters.Filter +} + +// ParseTransportConfig loads and parses a transport config file +func ParseTransportConfig(configFilePath string) (*ParsedTransportConfig, error) { + transportCfgYaml, err := os.ReadFile(configFilePath) + if err != nil { + return nil, fmt.Errorf("unable to read transport config file: %w", err) + } + + var config transportConfig + if err := yaml.Unmarshal(transportCfgYaml, &config); err != nil { + return nil, fmt.Errorf("unable to unmarshal transport config: %w", err) + } + + var parsedConfig ParsedTransportConfig + ff := filters.NewFilterFactory() + + // downloaders + for _, downloaderDefinition := range config.Downloaders { + filters, err := createFilterList(downloaderDefinition.Filters, ff) + if err != nil { + return nil, fmt.Errorf("unable to create filters for downloader %s: %w", downloaderDefinition.Name, err) + } + parsedConfig.Downloaders = append(parsedConfig.Downloaders, ParsedDownloaderDefinition{ + Name: downloaderDefinition.Name, + Type: downloaderDefinition.Type, + Spec: downloaderDefinition.Spec, + Filters: filters, + }) + } + + // processors + for _, processorsDefinition := range config.Processors { + parsedConfig.Processors = append(parsedConfig.Processors, ParsedProcessorDefinition{ + Name: processorsDefinition.Name, + Type: processorsDefinition.Type, + Spec: processorsDefinition.Spec, + }) + } + + // uploaders + for _, uploaderDefinition := range config.Uploaders { + filters, err := createFilterList(uploaderDefinition.Filters, ff) + if err != nil { + return nil, fmt.Errorf("unable to create filters for uploader %s: %w", uploaderDefinition.Name, err) + } + parsedConfig.Uploaders = append(parsedConfig.Uploaders, ParsedUploaderDefinition{ + Name: uploaderDefinition.Name, + Type: uploaderDefinition.Type, + Spec: uploaderDefinition.Spec, + Filters: filters, + }) + } + + // processing rules + for _, processingRule := range config.ProcessingRules { + filters, err := createFilterList(processingRule.Filters, ff) + if err != nil { + return nil, fmt.Errorf("unable to create filters for processing rule %s: %w", processingRule.Name, err) + } + + processors := []ParsedProcessorDefinition{} + for _, processorName := range processingRule.Processors { + processorDefined, err := findProcessorByName(processorName.Name, &parsedConfig) + if err != nil { + return nil, fmt.Errorf("unable to parse processing rule %s: %w", processingRule.Name, err) + } + processors = append(processors, *processorDefined) + } + + parsedProcessingRule := ParsedProcessingRuleDefinition{ + Name: processingRule.Name, + Processors: processors, + Filters: filters, + } + + parsedConfig.ProcessingRules = append(parsedConfig.ProcessingRules, parsedProcessingRule) + } + + return &parsedConfig, nil +} + +// MatchDownloaders finds all matching downloaders +func (c *ParsedTransportConfig) MatchDownloaders(cd cdv2.ComponentDescriptor, res cdv2.Resource) []ParsedDownloaderDefinition { + dls := []ParsedDownloaderDefinition{} + for _, downloader := range c.Downloaders { + if areAllFiltersMatching(downloader.Filters, cd, res) { + dls = append(dls, downloader) + } + } + return dls +} + +// MatchUploaders finds all matching uploaders +func (c *ParsedTransportConfig) MatchUploaders(cd cdv2.ComponentDescriptor, res cdv2.Resource) []ParsedUploaderDefinition { + uls := []ParsedUploaderDefinition{} + for _, uploader := range c.Uploaders { + if areAllFiltersMatching(uploader.Filters, cd, res) { + uls = append(uls, uploader) + } + } + return uls +} + +// MatchProcessingRules finds all matching processing rules +func (c *ParsedTransportConfig) MatchProcessingRules(cd cdv2.ComponentDescriptor, res cdv2.Resource) []ParsedProcessingRuleDefinition { + prs := []ParsedProcessingRuleDefinition{} + for _, processingRule := range c.ProcessingRules { + if areAllFiltersMatching(processingRule.Filters, cd, res) { + prs = append(prs, processingRule) + } + } + return prs +} + +func areAllFiltersMatching(filters []filters.Filter, cd cdv2.ComponentDescriptor, res cdv2.Resource) bool { + for _, filter := range filters { + if !filter.Matches(cd, res) { + return false + } + } + return true +} + +func findProcessorByName(name string, lookup *ParsedTransportConfig) (*ParsedProcessorDefinition, error) { + for _, processor := range lookup.Processors { + if processor.Name == name { + return &processor, nil + } + } + return nil, fmt.Errorf("unable to find processor %s", name) +} + +func createFilterList(filterDefinitions []filterDefinition, ff *filters.FilterFactory) ([]filters.Filter, error) { + var filters []filters.Filter + for _, f := range filterDefinitions { + filter, err := ff.Create(f.Type, f.Spec) + if err != nil { + return nil, fmt.Errorf("error creating filter list for type %s with args %s: %w", f.Type, string(*f.Spec), err) + } + filters = append(filters, filter) + } + return filters, nil +} diff --git a/pkg/transport/config/processing_job_factory.go b/pkg/transport/config/processing_job_factory.go deleted file mode 100644 index 54992135..00000000 --- a/pkg/transport/config/processing_job_factory.go +++ /dev/null @@ -1,239 +0,0 @@ -// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. -// -// SPDX-License-Identifier: Apache-2.0 -package config - -import ( - "encoding/json" - "fmt" - "os" - "time" - - cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" - "github.com/go-logr/logr" - "sigs.k8s.io/yaml" - - "github.com/gardener/component-cli/pkg/transport/filters" - "github.com/gardener/component-cli/pkg/transport/process" -) - -type parsedDownloaderDefinition struct { - Name string - Type string - Spec *json.RawMessage - Filters []filters.Filter -} - -type parsedProcessorDefinition struct { - Name string - Type string - Spec *json.RawMessage -} - -type parsedUploaderDefinition struct { - Name string - Type string - Spec *json.RawMessage - Filters []filters.Filter -} - -type parsedProcessingRuleDefinition struct { - Name string - Processors []string - Filters []filters.Filter -} - -type ParsedTransportConfig struct { - Downloaders []parsedDownloaderDefinition - Processors []parsedProcessorDefinition - Uploaders []parsedUploaderDefinition - ProcessingRules []parsedProcessingRuleDefinition -} - -// NewProcessingJobFactory creates a new processing job factory -func NewProcessingJobFactory(transportCfg ParsedTransportConfig, df *DownloaderFactory, pf *ProcessorFactory, uf *UploaderFactory, log logr.Logger, processorTimeout time.Duration) (*ProcessingJobFactory, error) { - c := ProcessingJobFactory{ - parsedConfig: &transportCfg, - downloaderFactory: df, - processorFactory: pf, - uploaderFactory: uf, - log: log, - processorTimeout: processorTimeout, - } - - return &c, nil -} - -// ProcessingJobFactory defines a helper struct for creating processing jobs -type ProcessingJobFactory struct { - parsedConfig *ParsedTransportConfig - uploaderFactory *UploaderFactory - downloaderFactory *DownloaderFactory - processorFactory *ProcessorFactory - log logr.Logger - processorTimeout time.Duration -} - -func ParseConfig(configFilePath string) (*ParsedTransportConfig, error) { - transportCfgYaml, err := os.ReadFile(configFilePath) - if err != nil { - return nil, fmt.Errorf("unable to read transport config file: %w", err) - } - - var config TransportConfig - if err := yaml.Unmarshal(transportCfgYaml, &config); err != nil { - return nil, fmt.Errorf("unable to parse transport config file: %w", err) - } - - var parsedConfig ParsedTransportConfig - ff := NewFilterFactory() - - // downloaders - for _, downloaderDefinition := range config.Downloaders { - filters, err := createFilterList(downloaderDefinition.Filters, ff) - if err != nil { - return nil, fmt.Errorf("unable to create downloader %s: %w", downloaderDefinition.Name, err) - } - parsedConfig.Downloaders = append(parsedConfig.Downloaders, parsedDownloaderDefinition{ - Name: downloaderDefinition.Name, - Type: downloaderDefinition.Type, - Spec: downloaderDefinition.Spec, - Filters: filters, - }) - } - - // processors - for _, processorsDefinition := range config.Processors { - parsedConfig.Processors = append(parsedConfig.Processors, parsedProcessorDefinition{ - Name: processorsDefinition.Name, - Type: processorsDefinition.Type, - Spec: processorsDefinition.Spec, - }) - } - - // uploaders - for _, uploaderDefinition := range config.Uploaders { - filters, err := createFilterList(uploaderDefinition.Filters, ff) - if err != nil { - return nil, fmt.Errorf("unable to create uploader %s: %w", uploaderDefinition.Name, err) - } - parsedConfig.Uploaders = append(parsedConfig.Uploaders, parsedUploaderDefinition{ - Name: uploaderDefinition.Name, - Type: uploaderDefinition.Type, - Spec: uploaderDefinition.Spec, - Filters: filters, - }) - } - - // rules - for _, rule := range config.ProcessingRules { - processors := []string{} - for _, processor := range rule.Processors { - processors = append(processors, processor.Name) - } - filters, err := createFilterList(rule.Filters, ff) - if err != nil { - return nil, fmt.Errorf("unable to create rule %s: %w", rule.Name, err) - } - rule := parsedProcessingRuleDefinition{ - Name: rule.Name, - Processors: processors, - Filters: filters, - } - parsedConfig.ProcessingRules = append(parsedConfig.ProcessingRules, rule) - } - - return &parsedConfig, nil -} - -// Create creates a new processing job for a resource -func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Resource) (*process.ProcessingJob, error) { - jobLog := c.log.WithValues("component-name", cd.Name, "component-version", cd.Version, "resource-name", res.Name, "resource-version", res.Version) - job := process.ProcessingJob{ - ComponentDescriptor: &cd, - Resource: &res, - Log: jobLog, - ProcessorTimeout: c.processorTimeout, - } - - // find matching downloader - for _, downloader := range c.parsedConfig.Downloaders { - if areAllFiltersMatching(downloader.Filters, cd, res) { - dl, err := c.downloaderFactory.Create(string(downloader.Type), downloader.Spec) - if err != nil { - return nil, err - } - job.Downloaders = append(job.Downloaders, process.NamedResourceStreamProcessor{ - Name: downloader.Name, - Processor: dl, - }) - } - } - - // find matching uploaders - for _, uploader := range c.parsedConfig.Uploaders { - if areAllFiltersMatching(uploader.Filters, cd, res) { - ul, err := c.uploaderFactory.Create(string(uploader.Type), uploader.Spec) - if err != nil { - return nil, err - } - job.Uploaders = append(job.Uploaders, process.NamedResourceStreamProcessor{ - Name: uploader.Name, - Processor: ul, - }) - } - } - - // find matching processing rules - for _, rule := range c.parsedConfig.ProcessingRules { - if areAllFiltersMatching(rule.Filters, cd, res) { - for _, processorName := range rule.Processors { - processorDefined, err := findProcessorByName(processorName, c.parsedConfig) - if err != nil { - return nil, fmt.Errorf("failed compiling rule %s: %w", rule.Name, err) - } - p, err := c.processorFactory.Create(string(processorDefined.Type), processorDefined.Spec) - if err != nil { - return nil, err - } - job.Processors = append(job.Processors, process.NamedResourceStreamProcessor{ - Name: processorDefined.Name, - Processor: p, - }) - job.MatchedProcessingRules = append(job.MatchedProcessingRules, rule.Name) - } - } - } - - return &job, nil -} - -func areAllFiltersMatching(filters []filters.Filter, cd cdv2.ComponentDescriptor, res cdv2.Resource) bool { - for _, filter := range filters { - if !filter.Matches(cd, res) { - return false - } - } - return true -} - -func createFilterList(filterDefinitions []FilterDefinition, ff *FilterFactory) ([]filters.Filter, error) { - var filters []filters.Filter - for _, f := range filterDefinitions { - filter, err := ff.Create(f.Type, f.Spec) - if err != nil { - return nil, fmt.Errorf("error creating filter list for type %s with args %s: %w", f.Type, string(*f.Spec), err) - } - filters = append(filters, filter) - } - return filters, nil -} - -func findProcessorByName(name string, lookup *ParsedTransportConfig) (*parsedProcessorDefinition, error) { - for _, processor := range lookup.Processors { - if processor.Name == name { - return &processor, nil - } - } - return nil, fmt.Errorf("unable to find processor %s", name) -} diff --git a/pkg/transport/config/transport_config.go b/pkg/transport/config/transport_config.go deleted file mode 100644 index 5d02677b..00000000 --- a/pkg/transport/config/transport_config.go +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. -// -// SPDX-License-Identifier: Apache-2.0 -package config - -import "encoding/json" - -type meta struct { - Version string `json:"version"` -} - -type TransportConfig struct { - Meta meta `json:"meta"` - Uploaders []UploaderDefinition `json:"uploaders"` - Processors []ProcessorDefinition `json:"processors"` - Downloaders []DownloaderDefinition `json:"downloaders"` - ProcessingRules []ProcessingRule `json:"processingRules"` -} - -type BaseProcessorDefinition struct { - Name string `json:"name"` - Type string `json:"type"` - Spec *json.RawMessage `json:"spec"` -} - -type FilterDefinition struct { - Type string `json:"type"` - Spec *json.RawMessage `json:"spec"` -} - -type DownloaderDefinition struct { - BaseProcessorDefinition - Filters []FilterDefinition `json:"filters"` -} - -type UploaderDefinition struct { - BaseProcessorDefinition - Filters []FilterDefinition `json:"filters"` -} - -type ProcessorDefinition struct { - BaseProcessorDefinition -} - -type ProcessorReference struct { - Name string `json:"name"` - Type string `json:"type"` -} - -type ProcessingRule struct { - Name string - Filters []FilterDefinition `json:"filters"` - Processors []ProcessorReference `json:"processors"` -} diff --git a/pkg/transport/config/filter_factory.go b/pkg/transport/filters/filter_factory.go similarity index 78% rename from pkg/transport/config/filter_factory.go rename to pkg/transport/filters/filter_factory.go index 1646d2d2..39cf6605 100644 --- a/pkg/transport/config/filter_factory.go +++ b/pkg/transport/filters/filter_factory.go @@ -1,15 +1,13 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // // SPDX-License-Identifier: Apache-2.0 -package config +package filters import ( "encoding/json" "fmt" "sigs.k8s.io/yaml" - - "github.com/gardener/component-cli/pkg/transport/filters" ) const ( @@ -32,7 +30,7 @@ func NewFilterFactory() *FilterFactory { type FilterFactory struct{} // Create creates a new filter defined by a type and a spec -func (f *FilterFactory) Create(filterType string, spec *json.RawMessage) (filters.Filter, error) { +func (f *FilterFactory) Create(filterType string, spec *json.RawMessage) (Filter, error) { switch filterType { case ComponentNameFilterType: return f.createComponentNameFilter(spec) @@ -45,29 +43,29 @@ func (f *FilterFactory) Create(filterType string, spec *json.RawMessage) (filter } } -func (f *FilterFactory) createComponentNameFilter(rawSpec *json.RawMessage) (filters.Filter, error) { - var spec filters.ComponentNameFilterSpec +func (f *FilterFactory) createComponentNameFilter(rawSpec *json.RawMessage) (Filter, error) { + var spec ComponentNameFilterSpec if err := yaml.Unmarshal(*rawSpec, &spec); err != nil { return nil, fmt.Errorf("unable to parse spec: %w", err) } - return filters.NewComponentNameFilter(spec) + return NewComponentNameFilter(spec) } -func (f *FilterFactory) createResourceTypeFilter(rawSpec *json.RawMessage) (filters.Filter, error) { - var spec filters.ResourceTypeFilterSpec +func (f *FilterFactory) createResourceTypeFilter(rawSpec *json.RawMessage) (Filter, error) { + var spec ResourceTypeFilterSpec if err := yaml.Unmarshal(*rawSpec, &spec); err != nil { return nil, fmt.Errorf("unable to parse spec: %w", err) } - return filters.NewResourceTypeFilter(spec) + return NewResourceTypeFilter(spec) } -func (f *FilterFactory) createAccessTypeFilter(rawSpec *json.RawMessage) (filters.Filter, error) { - var spec filters.AccessTypeFilterSpec +func (f *FilterFactory) createAccessTypeFilter(rawSpec *json.RawMessage) (Filter, error) { + var spec AccessTypeFilterSpec if err := yaml.Unmarshal(*rawSpec, &spec); err != nil { return nil, fmt.Errorf("unable to parse spec: %w", err) } - return filters.NewAccessTypeFilter(spec) + return NewAccessTypeFilter(spec) } diff --git a/pkg/transport/config/downloader_factory.go b/pkg/transport/process/downloaders/downloader_factory.go similarity index 83% rename from pkg/transport/config/downloader_factory.go rename to pkg/transport/process/downloaders/downloader_factory.go index cd0c57b5..d02ea6c5 100644 --- a/pkg/transport/config/downloader_factory.go +++ b/pkg/transport/process/downloaders/downloader_factory.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // // SPDX-License-Identifier: Apache-2.0 -package config +package downloaders import ( "encoding/json" @@ -10,7 +10,7 @@ import ( "github.com/gardener/component-cli/ociclient" "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/pkg/transport/process" - "github.com/gardener/component-cli/pkg/transport/process/downloaders" + "github.com/gardener/component-cli/pkg/transport/process/extensions" ) const ( @@ -39,11 +39,11 @@ type DownloaderFactory struct { func (f *DownloaderFactory) Create(downloaderType string, spec *json.RawMessage) (process.ResourceStreamProcessor, error) { switch downloaderType { case LocalOCIBlobDownloaderType: - return downloaders.NewLocalOCIBlobDownloader(f.client) + return NewLocalOCIBlobDownloader(f.client) case OCIArtifactDownloaderType: - return downloaders.NewOCIArtifactDownloader(f.client, f.cache) - case ExecutableType: - return createExecutable(spec) + return NewOCIArtifactDownloader(f.client, f.cache) + case extensions.ExecutableType: + return extensions.CreateExecutable(spec) default: return nil, fmt.Errorf("unknown downloader type %s", downloaderType) } diff --git a/pkg/transport/config/util.go b/pkg/transport/process/extensions/utils.go similarity index 53% rename from pkg/transport/config/util.go rename to pkg/transport/process/extensions/utils.go index 45cf9ebb..687c8c5b 100644 --- a/pkg/transport/config/util.go +++ b/pkg/transport/process/extensions/utils.go @@ -1,7 +1,4 @@ -// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. -// -// SPDX-License-Identifier: Apache-2.0 -package config +package extensions import ( "encoding/json" @@ -10,14 +7,15 @@ import ( "sigs.k8s.io/yaml" "github.com/gardener/component-cli/pkg/transport/process" - "github.com/gardener/component-cli/pkg/transport/process/extensions" ) const ( + // ExecutableType defines the type of an executable ExecutableType = "Executable" ) -func createExecutable(rawSpec *json.RawMessage) (process.ResourceStreamProcessor, error) { +// CreateExecutable creates a new executable defined by a spec +func CreateExecutable(rawSpec *json.RawMessage) (process.ResourceStreamProcessor, error) { type executableSpec struct { Bin string Args []string @@ -29,5 +27,5 @@ func createExecutable(rawSpec *json.RawMessage) (process.ResourceStreamProcessor return nil, fmt.Errorf("unable to parse spec: %w", err) } - return extensions.NewUnixDomainSocketExecutable(spec.Bin, spec.Args, spec.Env) + return NewUnixDomainSocketExecutable(spec.Bin, spec.Args, spec.Env) } diff --git a/pkg/transport/config/processor_factory.go b/pkg/transport/process/processors/processor_factory.go similarity index 86% rename from pkg/transport/config/processor_factory.go rename to pkg/transport/process/processors/processor_factory.go index 366ba295..193b6f2e 100644 --- a/pkg/transport/config/processor_factory.go +++ b/pkg/transport/process/processors/processor_factory.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // // SPDX-License-Identifier: Apache-2.0 -package config +package processors import ( "encoding/json" @@ -12,7 +12,7 @@ import ( "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/pkg/transport/process" - "github.com/gardener/component-cli/pkg/transport/process/processors" + "github.com/gardener/component-cli/pkg/transport/process/extensions" ) const ( @@ -49,11 +49,11 @@ func (f *ProcessorFactory) Create(processorType string, spec *json.RawMessage) ( case OCIArtifactFilterProcessorType: return f.createOCIArtifactFilter(spec) case BlobDigesterProcessorType: - return processors.NewBlobDigester(), nil + return NewBlobDigester(), nil case OCIManifestDigesterProcessorType: - return processors.NewOCIManifestDigester(), nil - case ExecutableType: - return createExecutable(spec) + return NewOCIManifestDigester(), nil + case extensions.ExecutableType: + return extensions.CreateExecutable(spec) default: return nil, fmt.Errorf("unknown processor type %s", processorType) } @@ -70,7 +70,7 @@ func (f *ProcessorFactory) createResourceLabeler(rawSpec *json.RawMessage) (proc return nil, fmt.Errorf("unable to parse spec: %w", err) } - return processors.NewResourceLabeler(spec.Labels...), nil + return NewResourceLabeler(spec.Labels...), nil } func (f *ProcessorFactory) createOCIArtifactFilter(rawSpec *json.RawMessage) (process.ResourceStreamProcessor, error) { @@ -84,5 +84,5 @@ func (f *ProcessorFactory) createOCIArtifactFilter(rawSpec *json.RawMessage) (pr return nil, fmt.Errorf("unable to parse spec: %w", err) } - return processors.NewOCIArtifactFilter(f.cache, spec.RemovePatterns) + return NewOCIArtifactFilter(f.cache, spec.RemovePatterns) } diff --git a/pkg/transport/config/uploader_factory.go b/pkg/transport/process/uploaders/uploader_factory.go similarity index 86% rename from pkg/transport/config/uploader_factory.go rename to pkg/transport/process/uploaders/uploader_factory.go index e46fcb3e..7715302e 100644 --- a/pkg/transport/config/uploader_factory.go +++ b/pkg/transport/process/uploaders/uploader_factory.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // // SPDX-License-Identifier: Apache-2.0 -package config +package uploaders import ( "encoding/json" @@ -13,7 +13,7 @@ import ( "github.com/gardener/component-cli/ociclient" "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/pkg/transport/process" - "github.com/gardener/component-cli/pkg/transport/process/uploaders" + "github.com/gardener/component-cli/pkg/transport/process/extensions" ) const ( @@ -44,11 +44,11 @@ type UploaderFactory struct { func (f *UploaderFactory) Create(uploaderType string, spec *json.RawMessage) (process.ResourceStreamProcessor, error) { switch uploaderType { case LocalOCIBlobUploaderType: - return uploaders.NewLocalOCIBlobUploader(f.client, f.targetCtx) + return NewLocalOCIBlobUploader(f.client, f.targetCtx) case OCIArtifactUploaderType: return f.createOCIArtifactUploader(spec) - case ExecutableType: - return createExecutable(spec) + case extensions.ExecutableType: + return extensions.CreateExecutable(spec) default: return nil, fmt.Errorf("unknown uploader type %s", uploaderType) } @@ -66,5 +66,5 @@ func (f *UploaderFactory) createOCIArtifactUploader(rawSpec *json.RawMessage) (p return nil, fmt.Errorf("unable to parse spec: %w", err) } - return uploaders.NewOCIArtifactUploader(f.client, f.cache, spec.BaseUrl, spec.KeepSourceRepo) + return NewOCIArtifactUploader(f.client, f.cache, spec.BaseUrl, spec.KeepSourceRepo) } diff --git a/pkg/transport/process/processing_job.go b/pkg/transport/processing_job.go similarity index 51% rename from pkg/transport/process/processing_job.go rename to pkg/transport/processing_job.go index 134eec3b..cf530be2 100644 --- a/pkg/transport/process/processing_job.go +++ b/pkg/transport/processing_job.go @@ -1,10 +1,11 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // // SPDX-License-Identifier: Apache-2.0 -package process +package transport import ( "context" + "errors" "fmt" "io" "io/ioutil" @@ -14,14 +15,44 @@ import ( cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" "github.com/go-logr/logr" + "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/transport/process/utils" ) +func NewProcessingJob( + cd cdv2.ComponentDescriptor, + res cdv2.Resource, + downloaders []NamedResourceStreamProcessor, + processors []NamedResourceStreamProcessor, + uploaders []NamedResourceStreamProcessor, + log logr.Logger, + processorTimeout time.Duration, +) (*ProcessingJob, error) { + if len(downloaders) != 1 { + return nil, fmt.Errorf("a processing job must exactly have 1 downloader, found %d", len(downloaders)) + } + + if len(uploaders) < 1 { + return nil, fmt.Errorf("a processing job must have at least 1 uploader, found %d", len(uploaders)) + } + + if log == nil { + return nil, errors.New("log must not be nil") + } + + j := ProcessingJob{ + ComponentDescriptor: &cd, + Resource: &res, + Downloaders: downloaders, + Processors: processors, + Uploaders: uploaders, + Log: log, + ProcessorTimeout: processorTimeout, + } + return &j, nil +} + // ProcessingJob defines a type which contains all data for processing a single resource -// ProcessingJob describes a chain of multiple processors for processing a resource. -// Each processor receives its input from the preceding processor and writes the output for the -// subsequent processor. To work correctly, a pipeline must consist of 1 downloader, 0..n processors, -// and 1..n uploaders. type ProcessingJob struct { ComponentDescriptor *cdv2.ComponentDescriptor Resource *cdv2.Resource @@ -35,10 +66,14 @@ type ProcessingJob struct { } type NamedResourceStreamProcessor struct { - Processor ResourceStreamProcessor + Processor process.ResourceStreamProcessor Name string } +func (j *ProcessingJob) GetProcessedResource() *cdv2.Resource { + return j.ProcessedResource +} + func (j *ProcessingJob) GetMatching() map[string][]string { matching := map[string][]string{ "processingRules": j.MatchedProcessingRules, @@ -55,27 +90,55 @@ func (j *ProcessingJob) GetMatching() map[string][]string { return matching } -// Process processes the resource -func (p *ProcessingJob) Process(ctx context.Context) error { +func (j *ProcessingJob) Validate() error { + if j.ComponentDescriptor == nil { + return errors.New("component descriptor must not be nil") + } + + if j.Resource == nil { + return errors.New("resource must not be nil") + } + + if len(j.Downloaders) != 1 { + return fmt.Errorf("a processing job must exactly have 1 downloader, found %d", len(j.Downloaders)) + } + + if len(j.Uploaders) < 1 { + return fmt.Errorf("a processing job must have at least 1 uploader, found %d", len(j.Uploaders)) + } + + return nil +} + +// Process runs the processing job, by calling downloader, processors, and uploaders sequentially +// for the defined component descriptor and resource. Each processor receives its input from the +// preceding processor and writes the output for the subsequent processor. To work correctly, +// a processing job must consist of 1 downloader, 0..n processors, and 1..n uploaders. +func (j *ProcessingJob) Process(ctx context.Context) error { + if err := j.Validate(); err != nil { + j.Log.Error(err, "invalid processing job") + return err + } + inputFile, err := ioutil.TempFile("", "") if err != nil { - p.Log.Error(err, "unable to create temporary input file") + j.Log.Error(err, "unable to create temporary input file") return err } - if err := utils.WriteProcessorMessage(*p.ComponentDescriptor, *p.Resource, nil, inputFile); err != nil { - p.Log.Error(err, "unable to write processor message") + if err := utils.WriteProcessorMessage(*j.ComponentDescriptor, *j.Resource, nil, inputFile); err != nil { + j.Log.Error(err, "unable to write processor message") return err } processors := []NamedResourceStreamProcessor{} - processors = append(processors, p.Downloaders...) - processors = append(processors, p.Processors...) - processors = append(processors, p.Uploaders...) + processors = append(processors, j.Downloaders...) + processors = append(processors, j.Processors...) + processors = append(processors, j.Uploaders...) for _, proc := range processors { - procLog := p.Log.WithValues("processor-name", proc.Name) - outputFile, err := p.runProcessor(ctx, inputFile, proc, procLog) + procLog := j.Log.WithValues("processor-name", proc.Name) + outputFile, err := j.runProcessor(ctx, inputFile, proc, procLog) if err != nil { procLog.Error(err, "unable to run processor") return err @@ -89,20 +152,20 @@ func (p *ProcessingJob) Process(ctx context.Context) error { defer inputFile.Close() if _, err := inputFile.Seek(0, io.SeekStart); err != nil { - p.Log.Error(err, "unable to seek to beginning of file") + j.Log.Error(err, "unable to seek to beginning of file") return err } _, processedRes, blobreader, err := utils.ReadProcessorMessage(inputFile) if err != nil { - p.Log.Error(err, "unable to read processor message") + j.Log.Error(err, "unable to read processor message") return err } if blobreader != nil { defer blobreader.Close() } - p.ProcessedResource = &processedRes + j.ProcessedResource = &processedRes return nil } diff --git a/pkg/transport/processing_job_factory.go b/pkg/transport/processing_job_factory.go new file mode 100644 index 00000000..6db49314 --- /dev/null +++ b/pkg/transport/processing_job_factory.go @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package transport + +import ( + "fmt" + "time" + + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "github.com/go-logr/logr" + + "github.com/gardener/component-cli/ociclient" + "github.com/gardener/component-cli/ociclient/cache" + "github.com/gardener/component-cli/pkg/transport/config" + "github.com/gardener/component-cli/pkg/transport/process/downloaders" + "github.com/gardener/component-cli/pkg/transport/process/processors" + "github.com/gardener/component-cli/pkg/transport/process/uploaders" +) + +// NewProcessingJobFactory creates a new processing job factory +func NewProcessingJobFactory(transportCfg config.ParsedTransportConfig, ociClient ociclient.Client, ocicache cache.Cache, targetCtx cdv2.OCIRegistryRepository, log logr.Logger, processorTimeout time.Duration) (*ProcessingJobFactory, error) { + df := downloaders.NewDownloaderFactory(ociClient, ocicache) + pf := processors.NewProcessorFactory(ocicache) + uf := uploaders.NewUploaderFactory(ociClient, ocicache, targetCtx) + + f := ProcessingJobFactory{ + parsedConfig: &transportCfg, + downloaderFactory: df, + processorFactory: pf, + uploaderFactory: uf, + log: log, + processorTimeout: processorTimeout, + } + + return &f, nil +} + +// ProcessingJobFactory defines a helper struct for creating processing jobs +type ProcessingJobFactory struct { + parsedConfig *config.ParsedTransportConfig + uploaderFactory *uploaders.UploaderFactory + downloaderFactory *downloaders.DownloaderFactory + processorFactory *processors.ProcessorFactory + log logr.Logger + processorTimeout time.Duration +} + +// Create creates a new processing job for a resource +func (c *ProcessingJobFactory) Create(cd cdv2.ComponentDescriptor, res cdv2.Resource) (*ProcessingJob, error) { + downloaderDefs := c.parsedConfig.MatchDownloaders(cd, res) + downloaders := []NamedResourceStreamProcessor{} + for _, dd := range downloaderDefs { + p, err := c.downloaderFactory.Create(dd.Type, dd.Spec) + if err != nil { + return nil, fmt.Errorf("unable to create downloader: %w", err) + } + downloaders = append(downloaders, NamedResourceStreamProcessor{ + Name: dd.Name, + Processor: p, + }) + } + + processingRuleDefs := c.parsedConfig.MatchProcessingRules(cd, res) + processors := []NamedResourceStreamProcessor{} + for _, rd := range processingRuleDefs { + for _, pd := range rd.Processors { + p, err := c.processorFactory.Create(pd.Type, pd.Spec) + if err != nil { + return nil, fmt.Errorf("unable to create processor: %w", err) + } + processors = append(processors, NamedResourceStreamProcessor{ + Name: pd.Name, + Processor: p, + }) + } + } + + uploaderDefs := c.parsedConfig.MatchUploaders(cd, res) + uploaders := []NamedResourceStreamProcessor{} + for _, ud := range uploaderDefs { + p, err := c.uploaderFactory.Create(ud.Type, ud.Spec) + if err != nil { + return nil, fmt.Errorf("unable to create uploader: %w", err) + } + uploaders = append(uploaders, NamedResourceStreamProcessor{ + Name: ud.Name, + Processor: p, + }) + } + + jobLog := c.log.WithValues("component-name", cd.Name, "component-version", cd.Version, "resource-name", res.Name, "resource-version", res.Version) + job, err := NewProcessingJob(cd, res, downloaders, processors, uploaders, jobLog, c.processorTimeout) + if err != nil { + return nil, fmt.Errorf("unable to create processing job: %w", err) + } + + return job, nil +} diff --git a/pkg/transport/config/processing_job_factory_test.go b/pkg/transport/processing_job_factory_test.go similarity index 99% rename from pkg/transport/config/processing_job_factory_test.go rename to pkg/transport/processing_job_factory_test.go index 90a4121c..38866c61 100644 --- a/pkg/transport/config/processing_job_factory_test.go +++ b/pkg/transport/processing_job_factory_test.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // // SPDX-License-Identifier: Apache-2.0 -package config_test +package transport_test import ( cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" diff --git a/pkg/transport/process/processing_job_test.go b/pkg/transport/processing_job_test.go similarity index 55% rename from pkg/transport/process/processing_job_test.go rename to pkg/transport/processing_job_test.go index e973df50..583e2405 100644 --- a/pkg/transport/process/processing_job_test.go +++ b/pkg/transport/processing_job_test.go @@ -1,18 +1,19 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // // SPDX-License-Identifier: Apache-2.0 -package process_test +package transport_test import ( "context" "encoding/json" + "time" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" "github.com/go-logr/logr" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - "github.com/gardener/component-cli/pkg/transport/process" + "github.com/gardener/component-cli/pkg/transport" "github.com/gardener/component-cli/pkg/transport/process/processors" ) @@ -37,9 +38,14 @@ var _ = Describe("processing job", func() { Name: "processor-1", Value: json.RawMessage(`"true"`), } + l3 := cdv2.Label{ + Name: "processor-2", + Value: json.RawMessage(`"true"`), + } expectedRes := res expectedRes.Labels = append(expectedRes.Labels, l1) expectedRes.Labels = append(expectedRes.Labels, l2) + expectedRes.Labels = append(expectedRes.Labels, l3) cd := cdv2.ComponentDescriptor{ ComponentSpec: cdv2.ComponentSpec{ @@ -49,30 +55,33 @@ var _ = Describe("processing job", func() { }, } - p1 := processors.NewResourceLabeler(l1) - p2 := processors.NewResourceLabeler(l2) - - procs := []process.NamedResourceStreamProcessor{ - { - Name: "p1", - Processor: p1, - }, - { - Name: "p2", - Processor: p2, - }, + p1 := transport.NamedResourceStreamProcessor{ + Name: "p1", + Processor: processors.NewResourceLabeler(l1), } - - pj := process.ProcessingJob{ - ComponentDescriptor: &cd, - Resource: &res, - Processors: procs, - Log: logr.Discard(), + p2 := transport.NamedResourceStreamProcessor{ + Name: "p2", + Processor: processors.NewResourceLabeler(l2), + } + p3 := transport.NamedResourceStreamProcessor{ + Name: "p3", + Processor: processors.NewResourceLabeler(l3), } - err := pj.Process(context.TODO()) + pj, err := transport.NewProcessingJob( + cd, + res, + []transport.NamedResourceStreamProcessor{p1}, + []transport.NamedResourceStreamProcessor{p2}, + []transport.NamedResourceStreamProcessor{p3}, + logr.Discard(), + 10*time.Second, + ) + Expect(err).ToNot(HaveOccurred()) + + err = pj.Process(context.TODO()) Expect(err).ToNot(HaveOccurred()) - Expect(*pj.ProcessedResource).To(Equal(expectedRes)) + Expect(*pj.GetProcessedResource()).To(Equal(expectedRes)) }) }) diff --git a/pkg/transport/config/testdata/transport.cfg b/pkg/transport/testdata/transport.cfg similarity index 100% rename from pkg/transport/config/testdata/transport.cfg rename to pkg/transport/testdata/transport.cfg diff --git a/pkg/transport/config/config_suite_test.go b/pkg/transport/transport_suite_test.go similarity index 66% rename from pkg/transport/config/config_suite_test.go rename to pkg/transport/transport_suite_test.go index a8219a7a..6385bd78 100644 --- a/pkg/transport/config/config_suite_test.go +++ b/pkg/transport/transport_suite_test.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // // SPDX-License-Identifier: Apache-2.0 -package config_test +package transport_test import ( "testing" @@ -14,20 +14,21 @@ import ( "github.com/gardener/component-cli/ociclient" "github.com/gardener/component-cli/ociclient/cache" + "github.com/gardener/component-cli/pkg/transport" "github.com/gardener/component-cli/pkg/transport/config" ) func TestConfig(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "Config Test Suite") + RunSpecs(t, "Transport Test Suite") } var ( - factory *config.ProcessingJobFactory + factory *transport.ProcessingJobFactory ) var _ = BeforeSuite(func() { - transportCfg, err := config.ParseConfig("./testdata/transport.cfg") + transportCfg, err := config.ParseTransportConfig("./testdata/transport.cfg") Expect(err).ToNot(HaveOccurred()) client, err := ociclient.NewClient(logr.Discard()) @@ -35,10 +36,6 @@ var _ = BeforeSuite(func() { ocicache := cache.NewInMemoryCache() targetCtx := cdv2.NewOCIRegistryRepository("my-target-registry.com/test", "") - df := config.NewDownloaderFactory(client, ocicache) - pf := config.NewProcessorFactory(ocicache) - uf := config.NewUploaderFactory(client, ocicache, *targetCtx) - - factory, err = config.NewProcessingJobFactory(*transportCfg, df, pf, uf, logr.Discard(), 30*time.Second) + factory, err = transport.NewProcessingJobFactory(*transportCfg, client, ocicache, *targetCtx, logr.Discard(), 30*time.Second) Expect(err).ToNot(HaveOccurred()) }, 5) From 1d9dce8bd165d0f4d93c3b1e608a44170c6c1ced Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 21 Dec 2021 10:36:28 +0100 Subject: [PATCH 89/94] adds newline to output --- hack/generate-docs/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hack/generate-docs/main.go b/hack/generate-docs/main.go index a8fa3462..e6877a46 100644 --- a/hack/generate-docs/main.go +++ b/hack/generate-docs/main.go @@ -30,7 +30,7 @@ func main() { cmd := app.NewComponentsCliCommand(context.TODO()) cmd.DisableAutoGenTag = true check(doc.GenMarkdownTree(cmd, outputDir)) - fmt.Printf("Successfully written docs to %s", outputDir) + fmt.Printf("Successfully written docs to %s\n", outputDir) } func printHelp() { From afb2371f9f75dac5aa785a24c2f9faa2306f6697 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Tue, 21 Dec 2021 10:38:08 +0100 Subject: [PATCH 90/94] adds test for repo ctx override --- pkg/utils/repo_ctx_override_test.go | 32 ++++++++++++++++++++++++ pkg/utils/testdata/repo-ctx-override.cfg | 8 ++++++ pkg/utils/utils_suite_test.go | 15 +++++++++++ 3 files changed, 55 insertions(+) create mode 100644 pkg/utils/repo_ctx_override_test.go create mode 100644 pkg/utils/testdata/repo-ctx-override.cfg diff --git a/pkg/utils/repo_ctx_override_test.go b/pkg/utils/repo_ctx_override_test.go new file mode 100644 index 00000000..a07400f1 --- /dev/null +++ b/pkg/utils/repo_ctx_override_test.go @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. +// +// SPDX-License-Identifier: Apache-2.0 +package utils_test + +import ( + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("repository context override", func() { + + Context("processing job", func() { + + It("should return overridden repository context if component name matches", func() { + componentName := "github.com/gardener/component-cli" + expectedRepoCtx := cdv2.NewOCIRegistryRepository("example-oci-registry.com/override", "") + actualRepoCtx := repoCtxOverride.GetRepositoryContext(componentName, *defaultRepoCtx) + Expect(actualRepoCtx).To(Equal(expectedRepoCtx)) + }) + + It("should return default repository context if component name doesn't match", func() { + componentName := "github.com/gardener/not-component-cli" + expectedRepoCtx := cdv2.NewOCIRegistryRepository("example-oci-registry.com/base", "") + actualRepoCtx := repoCtxOverride.GetRepositoryContext(componentName, *defaultRepoCtx) + Expect(actualRepoCtx).To(Equal(expectedRepoCtx)) + }) + + }) + +}) diff --git a/pkg/utils/testdata/repo-ctx-override.cfg b/pkg/utils/testdata/repo-ctx-override.cfg new file mode 100644 index 00000000..464f5f34 --- /dev/null +++ b/pkg/utils/testdata/repo-ctx-override.cfg @@ -0,0 +1,8 @@ +overrides: +- repositoryContext: + baseUrl: 'example-oci-registry.com/override' + componentNameMapping: urlPath + type: ociRegistry + componentNameFilterSpec: + includeComponentNames: + - 'github.com/gardener/component-cli' \ No newline at end of file diff --git a/pkg/utils/utils_suite_test.go b/pkg/utils/utils_suite_test.go index 03afdd59..8a9d8cab 100644 --- a/pkg/utils/utils_suite_test.go +++ b/pkg/utils/utils_suite_test.go @@ -6,11 +6,26 @@ package utils_test import ( "testing" + cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + + "github.com/gardener/component-cli/pkg/utils" ) func TestConfig(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Utils Test Suite") } + +var ( + defaultRepoCtx *cdv2.OCIRegistryRepository + repoCtxOverride *utils.RepositoryContextOverride +) + +var _ = BeforeSuite(func() { + defaultRepoCtx = cdv2.NewOCIRegistryRepository("example-oci-registry.com/base", "") + var err error + repoCtxOverride, err = utils.ParseRepositoryContextOverrideConfig("./testdata/repo-ctx-override.cfg") + Expect(err).ToNot(HaveOccurred()) +}, 5) From a0dc11a6f0772991edfb39879f2ea79dc0644901 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Mon, 10 Jan 2022 10:42:10 +0100 Subject: [PATCH 91/94] adds doc --- README.md | 179 +++++++++++++++++++++++++++++++++++++++++++ images/transport.png | Bin 0 -> 239127 bytes 2 files changed, 179 insertions(+) create mode 100644 images/transport.png diff --git a/README.md b/README.md index a3743174..ee7f9e99 100644 --- a/README.md +++ b/README.md @@ -41,3 +41,182 @@ In addition, an OCI Image is provided `docker run eu.gcr.io/gardener-project/com Commands of the cli are hierarchically defined in [pkg/commands](./pkg/commands). When a new command should be added make sure to commply to the google docs guidline for cli command (https://developers.google.com/style/code-syntax#required-items). + +### Transport +The `transport` subcommand copies [Open Component Model (OCM)](https://github.com/gardener/component-spec) based applications between OCI registries or CTF artifacts. The command supports copy-by-reference and copy-by-value. Copy-by-reference only copies the component descriptors. Copy-by-value additionally copies all resources to new locations. + +The basic flow of the `transport` subcommand is the following +``` +- load component descriptor and all component references +- for componentDescriptor : componentDescriptors + - if copy-by-value + - for every resource : componentDescriptor.Resources + - download resource content from source location + - upload resource content to target location + - set resource.Access to target location + - upload patched component descriptor +``` + +If copy-by-value is enable, it is possible to modify resources during the resource copy process, see [here](#modifying-resources). + +#### Configuration +The configuration for downloading and uploading the resources is handed over via the cli option `--transport-cfg-path`. The variable must point to a valid config file. The following snippet shows a basic config file for downloading and uploading resources of types `localOciBlob` and `ociImage`. It defines 1 downloader and 1 uploader for each resource type and matches these definitions to the actual resources via the `filters` attribute. + +```yaml +meta: + version: v1 + +downloaders: +- name: 'oci-artifact-downloader' + type: 'OciArtifactDownloader' + filters: + - type: 'ResourceTypeFilter' + spec: + includeResourceTypes: + - 'ociImage' +- name: 'local-oci-blob-downloader' + type: 'LocalOciBlobDownloader' + filters: + - type: 'AccessTypeFilter' + spec: + includeAccessTypes: + - 'localOciBlob' + +uploaders: +- name: 'oci-artifact-uploader' + type: 'OciArtifactUploader' + spec: + baseUrl: 'eu.gcr.io/target/images' + keepSourceRepo: false + filters: + - type: 'ResourceTypeFilter' + spec: + includeResourceTypes: + - 'ociImage' +- name: 'local-oci-blob-uploader' + type: 'LocalOciBlobUploader' + filters: + - type: 'AccessTypeFilter' + spec: + includeAccessTypes: + - 'localOciBlob' +``` + +Every resource that gets processed is matched against the filter definitions of each downloader and uploader. In order to work correctly, every resource must match against exactly 1 downloader and 1..n uploaders from the config file. There are builtin downloaders and uploaders, and also the possibility to extend the framework via external downloaders and uploaders, see [here](#extensions). + +##### Modifying resources +Additionally to only downloading and reuploading resources, resources can be modified during the process. The units that perform the modifications are called *processors* and are defined in the config file. As with downloaders and uploaders, there are builtin processors, and also the possibility to extend the framework via external processors, see [here](#extensions). + +The following snippet shows a config file for removing files from OCI artifact layers. It defines 2 processors of type `OciImageFilter` which should remove the *bin* and *etc* directories from all layers of an OCI artifact. These processors are matched against actual resources via `processingRules`. Every resource that gets processed is matched against the filter definitions of each processing rule. If all filters of a rule match, then the resource will get processed by the processors defined in the rule. It is possible that a resource matches 0..n processing rules. + +```yaml +meta: + version: v1 + +downloaders: + ... + +uploaders: + ... + +processors: +- name: 'remove-bin-dir' + type: 'OciImageFilter' + spec: + removePatterns: + - 'bin/*' +- name: 'remove-etc-dir' + type: 'OciImageFilter' + spec: + removePatterns: + - 'etc/*' + +processingRules: +- name: 'remove-bin-dir-from-test-component-images' + processors: + - name: 'remove-bin-dir' + type: 'processor' + filters: + - type: 'ComponentNameFilter' + spec: + includeComponentNames: + - 'github.com/test/test-component' + - type: 'ResourceTypeFilter' + spec: + includeResourceTypes: + - 'ociImage' +- name: 'remove-etc-dir-from-all-images' + processors: + - name: 'remove-etc-dir' + type: 'processor' + filters: + - type: 'ResourceTypeFilter' + spec: + includeResourceTypes: + - 'ociImage' +``` + +##### Repository Context Override +It is possible to load component descriptors from different OCI registries via the cli option `--repo-context-override-cfg`. It must point to a config file where the source repository contexts of component descriptors can be explicitely mapped. When downloading the source component descriptors, the program will first look into the repository context override file and try to match a component descriptor against each override definition. If no override matches, the default repository context from the cli option `--from` will be used. The following snippet shows a configuration, where the component `github.com/test/test-component` will be loaded from the repository context with base URL `eu.gcr.io/override`. + +```yaml +meta: + version: v1 +overrides: +- repositoryContext: + baseUrl: 'eu.gcr.io/override' + componentNameMapping: urlPath + type: ociRegistry + componentNameFilterSpec: + includeComponentNames: + - 'github.com/test/test-component' +``` + +#### Extensions +A pipeline for processing a single resource must consist of 1 downloader, 0..n processors, and 1..n uploaders. For each resource that gets processed, a unique chain of downloader, processors, and uploaders is created and is created based on the definitions in the config file. The elements in a chain are then called sequentially. + +![resource processing](images/transport.png) + +The downloader is the first element in a chain. It receives a TAR archive which contains 2 files. The first file is `component-descriptor.yaml` which contains the component descriptor in YAML format. The second file is `resource.yaml` which contains the YAML of the resource for which the chain is called. The downloader must then download the actual resource content based on this information. Once the content is downloaded, the downloader builds a new TAR archive for passing the information to subsequent processors. This TAR archive contains the `component-descriptor.yaml` and `resource.yaml`, and additionally the serialized resource content as a single blob file. + +The subsequent processors can open the TAR archive, deserialize the resource content, perform the modifications to either the `resource.yaml` or the actual resource content, and again serialize the content for subsequent processors. Beware that the serialization format of all processors in a chain must match. + +At the end of a processing pipeline are 1..n uploaders. They publish the resource content to a sink (OCI registry, CTF archive, S3 bucket, ...). As with the preceding steps, uploaders must also build a new TAR archive and pass the information to subsequent uploaders. Beware that uploaders must update the `access` attribute in the `resource.yaml` to point to the new resource location. + +After all uploaders run through, the orchestrating program collects the modified `resource.yaml` files from the TAR archive and replaces the original resources in the component descriptor. The original component descriptor now contains the updated resources. As a final step, the modified component descriptor is uploaded. + +Every stage of resource processing (downloaders, processors, uploaders) is extensible. An extension is a static binary, which reads a TAR archive as input (either via stdin or Unix Domain Sockets), performs its modifications, and then outputs a TAR archive with the modified content via stdout or Unix Domain Sockets. To describe extensions in the config file, use `type: 'Executable'` in the downloader/processor/uploader definition. The following snippet shows the definition of an extension processor. + +```yaml +processors: +- name: 'test-extension' + type: 'Executable' + spec: + bin: '/path/to/binary' + args: + - '--test-arg' + - '42' + env: + TEST_ENV: '42' +``` + +When creating extensions, some details must be considered. + +##### General points: +- One can chose whether the extension binary should support communication via stdin/stdout, Unix Domain Sockets, or both. + +- Modifications of the component-descriptor.yaml file in a TAR archive are ignored when reuploading component descriptors and should therefore not be performed. + +- The extension must read the input stream in a non-blocking fashion (e.g. directly write to temp file). If the stream is directly consumed as a TAR file, the writing side might block for large resources, as the TAR archive must first be completely written before it can be opened on the reading side. + +- Beware that the serialization formats of the resource content must match between the downloader, processors, and uploaders of a resource processing chain. The serialization format is therefore an unseen part of the processor interface. + +##### Points when usind Unix Domain Sockets: +- For Unix Domain Sockets, the calling program will set an environment variable which indicates the URL under which the Unix Domain Socket server of the extension binary should start. The calling program will try to connect to the extension via this URL. + +- The extension program must stop the Unix Domain Socket server and stop, once it receives SIGTERM from the calling program. + +- The extension program should remove the socket file in the file system once the program finishes. + +##### Points when using stdin/stdout +- When using stdin/stdout for reading and writing processor TAR archives, beware that no other output like log data is allowed to be written to stdout. This data would otherwise interfere with the program data. diff --git a/images/transport.png b/images/transport.png new file mode 100644 index 0000000000000000000000000000000000000000..0dbe84ae3b2e882fcb4843870b39071411806eef GIT binary patch literal 239127 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
  • W5c)Ls&UG+E*s0+L#aY!+uW+#3v}gVI3rxqG%=fF@dN(t_xD2F3Frgo&bF z?4s>#{%D>LPT9!)EQvG|A`m>h>R}e=C0ygYjoKc~Yd|eLh%T?nw84}LEp{LcWFd4V zAiM)S`J(##XU)@28Rp$sTtN~6*!7%sq%V3;FIUp*ou?6&yUBV`*2Ax6(bAr98Xa1t zxj4*R9)}cOU|`fg*oLURERta6rw#rHqa{c+?&LPQKd>zZP>oJ5i(mL)5WqxS^Nkkd z&*`LLWW4#llgoT)$J2HT6-Q&aeh9&#TxP+2RMa2(Gea(zRG@-tM2}s+*;c#$Mx&;k z3)7tmN1406w@4strk3W|uiCsL3*EQ5&LsL9nSY78BL?HvH(vGSq-a6m^nk0+xwD*F zzt9FP*MCu$p)z<7E-r3Ape%E#l;?e5VUjnBQ(V1W%eI)Uh?b}KLjPTFI$Cd?I4*%n zu+9Eb+xT!P3zxmo-f#>DlGPyO@=dbVquJg47;&VqLK{zF%~z>;;e6zvz@Iw5$8V^4;P{m$&Xw`Rfq)*UEPEun1w?BPCfz6b);D~B6Vtz%lJ;gpm z*;F2AB~5#Vw#PV(4*lZ!Ni|DMaX)B-*mfC;S)qTr4$^rb36@Ej%*nAvq8)$%$lf1 zPBTG7A+=n{-huedC4{2AfyzvK$l+_~?BV9uDLUGO-Cor^#+g!_9I8|4ivX$`F4)wX>Cj=CH>z2b|a+^$@&#zvX zcw{SaHzJ{7lRiY)OMh=P_Xze2@l{qh%%t;FN=%937AA?#;~%zdvc>B3?>fS1o7bIu zryav+>(k<3i{D?O?NnN+97~u`T5O>tpjpO$2%FuRtsgD%FZXf$5cmr^lNURIMGKBP zUe*4|QScxgF_Vp405D~ZeyE9;Ar(b=UlULDa`MQ`?0U+{JOg@(Q9eX#$q^l{K;nK2 z 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

    uFBeCd;#lj{x?C%OvLedsqz#v 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 literal 0 HcmV?d00001 From 302d47c2fa05c50e237d168f071d8a718347fbd0 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Mon, 24 Jan 2022 10:47:44 +0100 Subject: [PATCH 92/94] removes signature coding --- docs/reference/component-cli.md | 1 + docs/reference/component-cli_transport.md | 39 ++++++++++ ociclient/mock/client_mock.go | 24 +++++- pkg/commands/transport/transport.go | 47 ------------ .../process/processors/blob_digester.go | 53 -------------- .../processors/oci_manifest_digester.go | 73 ------------------- .../process/processors/processor_factory.go | 10 --- 7 files changed, 61 insertions(+), 186 deletions(-) create mode 100644 docs/reference/component-cli_transport.md delete mode 100644 pkg/transport/process/processors/blob_digester.go delete mode 100644 pkg/transport/process/processors/oci_manifest_digester.go diff --git a/docs/reference/component-cli.md b/docs/reference/component-cli.md index 3fed7f11..8c1294b7 100644 --- a/docs/reference/component-cli.md +++ b/docs/reference/component-cli.md @@ -21,5 +21,6 @@ component cli * [component-cli ctf](component-cli_ctf.md) - * [component-cli image-vector](component-cli_image-vector.md) - command to add resource from a image vector and retrieve from a component descriptor * [component-cli oci](component-cli_oci.md) - +* [component-cli transport](component-cli_transport.md) - * [component-cli version](component-cli_version.md) - displays the version diff --git a/docs/reference/component-cli_transport.md b/docs/reference/component-cli_transport.md new file mode 100644 index 00000000..5907b32b --- /dev/null +++ b/docs/reference/component-cli_transport.md @@ -0,0 +1,39 @@ +## component-cli transport + + + +``` +component-cli transport [flags] +``` + +### Options + +``` + --allow-plain-http allows the fallback to http if the oci registry does not support https + --cc-config string path to the local concourse config file + --dry-run only download component descriptors and perform matching of resources against transport config file. no component descriptors are uploaded, no resources are down/uploaded + --from string source repository base url + -h, --help help for transport + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --processor-timeout duration execution timeout for each individual processor (default 30s) + --registry-config string path to the dockerconfig.json with the oci registry authentication information + --repo-ctx-override-cfg string path to the repository context override config file + --to string target repository where the components are copied to + --transport-cfg string path to the transport config file +``` + +### Options inherited from parent commands + +``` + --cli logger runs as cli logger. enables cli logging + --dev enable development logging which result in console encoding, enabled stacktrace and enabled caller + --disable-caller disable the caller of logs (default true) + --disable-stacktrace disable the stacktrace of error logs (default true) + --disable-timestamp disable timestamp output (default true) + -v, --verbosity int number for the log level verbosity (default 1) +``` + +### SEE ALSO + +* [component-cli](component-cli.md) - component cli + diff --git a/ociclient/mock/client_mock.go b/ociclient/mock/client_mock.go index 08616ed5..bae52f27 100644 --- a/ociclient/mock/client_mock.go +++ b/ociclient/mock/client_mock.go @@ -9,11 +9,10 @@ import ( io "io" reflect "reflect" - gomock "github.com/golang/mock/gomock" - v1 "github.com/opencontainers/image-spec/specs-go/v1" - ociclient "github.com/gardener/component-cli/ociclient" oci "github.com/gardener/component-cli/ociclient/oci" + gomock "github.com/golang/mock/gomock" + v1 "github.com/opencontainers/image-spec/specs-go/v1" ) // MockClient is a mock of Client interface. @@ -83,6 +82,25 @@ func (mr *MockClientMockRecorder) GetOCIArtifact(arg0, arg1 interface{}) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOCIArtifact", reflect.TypeOf((*MockClient)(nil).GetOCIArtifact), arg0, arg1) } +// PushBlob mocks base method. +func (m *MockClient) PushBlob(arg0 context.Context, arg1 string, arg2 v1.Descriptor, arg3 ...ociclient.PushOption) error { + m.ctrl.T.Helper() + varargs := []interface{}{arg0, arg1, arg2} + for _, a := range arg3 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "PushBlob", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// PushBlob indicates an expected call of PushBlob. +func (mr *MockClientMockRecorder) PushBlob(arg0, arg1, arg2 interface{}, arg3 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0, arg1, arg2}, arg3...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PushBlob", reflect.TypeOf((*MockClient)(nil).PushBlob), varargs...) +} + // PushManifest mocks base method. func (m *MockClient) PushManifest(arg0 context.Context, arg1 string, arg2 *v1.Manifest, arg3 ...ociclient.PushOption) error { m.ctrl.T.Helper() diff --git a/pkg/commands/transport/transport.go b/pkg/commands/transport/transport.go index 2fec8cc9..455c7826 100644 --- a/pkg/commands/transport/transport.go +++ b/pkg/commands/transport/transport.go @@ -13,7 +13,6 @@ import ( "time" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" - "github.com/gardener/component-spec/bindings-go/apis/v2/signatures" "github.com/gardener/component-spec/bindings-go/ctf" cdoci "github.com/gardener/component-spec/bindings-go/oci" "github.com/go-logr/logr" @@ -83,9 +82,6 @@ func (o *Options) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&o.TargetRepository, "to", "", "target repository where the components are copied to") fs.StringVar(&o.TransportCfgPath, "transport-cfg", "", "path to the transport config file") fs.StringVar(&o.RepoCtxOverrideCfgPath, "repo-ctx-override-cfg", "", "path to the repository context override config file") - fs.BoolVar(&o.GenerateSignature, "sign", false, "sign the uploaded component descriptors") - fs.StringVar(&o.SignatureName, "signature-name", "", "name of the generated signature") - fs.StringVar(&o.PrivateKeyPath, "private-key", "", "path to the private key file used for signing") fs.BoolVar(&o.DryRun, "dry-run", false, "only download component descriptors and perform matching of resources against transport config file. no component descriptors are uploaded, no resources are down/uploaded") fs.DurationVar(&o.ProcessorTimeout, "processor-timeout", 30*time.Second, "execution timeout for each individual processor") o.OCIOptions.AddFlags(fs) @@ -233,49 +229,6 @@ func (o *Options) Run(ctx context.Context, log logr.Logger, fs vfs.FileSystem) e return fmt.Errorf("%d errors occurred during resource processing", len(errs)) } - if o.GenerateSignature { - signer, err := signatures.CreateRsaSignerFromKeyFile(o.PrivateKeyPath) - if err != nil { - return fmt.Errorf("unable to create signer: %w", err) - } - - hasher, err := signatures.HasherForName("SHA256") - if err != nil { - return fmt.Errorf("unable to create hasher: %w", err) - } - - crr := func(ctx context.Context, cd cdv2.ComponentDescriptor, ref cdv2.ComponentReference) (*cdv2.DigestSpec, error) { - key := fmt.Sprintf("%s:%s", ref.Name, ref.Version) - cd2, ok := cdLookup[key] - if !ok { - return nil, fmt.Errorf("unable to find component descriptor in map: %w", err) - } - - signature, err := signatures.SelectSignatureByName(cd2, o.SignatureName) - if err != nil { - return nil, fmt.Errorf("unable to get signature: %w", err) - } - - return &signature.Digest, nil - } - - // iterate backwards -> start with "leave" component descriptors w/o dependencies - for i := len(cds) - 1; i >= 0; i-- { - cd := cds[i] - componentLog := log.WithValues("component-name", cd.Name, "component-version", cd.Version) - - if err := signatures.AddDigestsToComponentDescriptor(ctx, cd, crr, nil); err != nil { - componentLog.Error(err, "unable to add digests to component descriptor") - return err - } - - if err := signatures.SignComponentDescriptor(cd, signer, *hasher, o.SignatureName); err != nil { - componentLog.Error(err, "unable to sign component descriptor") - return err - } - } - } - for _, cd := range cdLookup { cd := cd componentLog := log.WithValues("component-name", cd.Name, "component-version", cd.Version) diff --git a/pkg/transport/process/processors/blob_digester.go b/pkg/transport/process/processors/blob_digester.go deleted file mode 100644 index c837a258..00000000 --- a/pkg/transport/process/processors/blob_digester.go +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. -// -// SPDX-License-Identifier: Apache-2.0 -package processors - -import ( - "context" - "fmt" - "io" - - cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" - "github.com/opencontainers/go-digest" - - "github.com/gardener/component-cli/pkg/transport/process" - "github.com/gardener/component-cli/pkg/transport/process/utils" -) - -type blobDigester struct{} - -func NewBlobDigester() process.ResourceStreamProcessor { - obj := blobDigester{} - return &obj -} - -func (p *blobDigester) Process(ctx context.Context, r io.Reader, w io.Writer) error { - cd, res, resBlobReader, err := utils.ReadProcessorMessage(r) - if err != nil { - return fmt.Errorf("unable to read processor message: %w", err) - } - if resBlobReader != nil { - defer resBlobReader.Close() - } - - dgst, err := digest.FromReader(resBlobReader) - if err != nil { - return fmt.Errorf("unable to calculate digest: %w", err) - } - digestspec := cdv2.DigestSpec{ - Algorithm: dgst.Algorithm().String(), - Value: dgst.Encoded(), - } - res.Digest = &digestspec - - if _, err := resBlobReader.Seek(0, io.SeekStart); err != nil { - return fmt.Errorf("unable to seek to beginning of resource blob file: %w", err) - } - - if err := utils.WriteProcessorMessage(*cd, res, resBlobReader, w); err != nil { - return fmt.Errorf("unable to write processor message: %w", err) - } - - return nil -} diff --git a/pkg/transport/process/processors/oci_manifest_digester.go b/pkg/transport/process/processors/oci_manifest_digester.go deleted file mode 100644 index 291f9a2a..00000000 --- a/pkg/transport/process/processors/oci_manifest_digester.go +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. -// -// SPDX-License-Identifier: Apache-2.0 -package processors - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - - cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" - "github.com/opencontainers/go-digest" - - "github.com/gardener/component-cli/pkg/transport/process" - processutils "github.com/gardener/component-cli/pkg/transport/process/utils" -) - -type ociManifestDigester struct{} - -func NewOCIManifestDigester() process.ResourceStreamProcessor { - obj := ociManifestDigester{} - return &obj -} - -func (f *ociManifestDigester) Process(ctx context.Context, r io.Reader, w io.Writer) error { - cd, res, blobreader, err := processutils.ReadProcessorMessage(r) - if err != nil { - return fmt.Errorf("unable to read archive: %w", err) - } - if blobreader == nil { - return errors.New("resource blob must not be nil") - } - defer blobreader.Close() - - manifest, index, err := processutils.GetManifestOrIndexFromSerializedOCIArtifact(blobreader) - if err != nil { - return fmt.Errorf("unable to deserialize oci artifact: %w", err) - } - - var content []byte - if manifest != nil { - content, err = json.Marshal(manifest) - if err != nil { - return fmt.Errorf("unable to marshal manifest: %w", err) - } - } else if index != nil { - content, err = json.Marshal(index) - if err != nil { - return fmt.Errorf("unable to marshal image index: %w", err) - } - } else { - return errors.New("") - } - - dgst := digest.FromBytes(content) - digestspec := cdv2.DigestSpec{ - Algorithm: dgst.Algorithm().String(), - Value: dgst.Encoded(), - } - res.Digest = &digestspec - - if _, err := blobreader.Seek(0, io.SeekStart); err != nil { - return fmt.Errorf("unable to seek to beginning of resource blob file: %w", err) - } - - if err = processutils.WriteProcessorMessage(*cd, res, blobreader, w); err != nil { - return fmt.Errorf("unable to write archive: %w", err) - } - - return nil -} diff --git a/pkg/transport/process/processors/processor_factory.go b/pkg/transport/process/processors/processor_factory.go index 193b6f2e..4665e256 100644 --- a/pkg/transport/process/processors/processor_factory.go +++ b/pkg/transport/process/processors/processor_factory.go @@ -21,12 +21,6 @@ const ( // OCIArtifactFilterProcessorType defines the type of an oci artifact filter OCIArtifactFilterProcessorType = "OciArtifactFilter" - - // BlobDigesterProcessorType defines the type of a blob digester - BlobDigesterProcessorType = "BlobDigester" - - // OCIManifestDigesterProcessorType the type of an oci manifest digester - OCIManifestDigesterProcessorType = "OciManifestDigester" ) // NewProcessorFactory creates a new processor factory @@ -48,10 +42,6 @@ func (f *ProcessorFactory) Create(processorType string, spec *json.RawMessage) ( return f.createResourceLabeler(spec) case OCIArtifactFilterProcessorType: return f.createOCIArtifactFilter(spec) - case BlobDigesterProcessorType: - return NewBlobDigester(), nil - case OCIManifestDigesterProcessorType: - return NewOCIManifestDigester(), nil case extensions.ExecutableType: return extensions.CreateExecutable(spec) default: From fb08e8baad29f24904d4bc897ab98b2ee4960e43 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Mon, 24 Jan 2022 11:29:56 +0100 Subject: [PATCH 93/94] remove unix domain socket file after processor exit --- ociclient/mock/client_mock.go | 5 ++-- .../process/downloaders/downloader_factory.go | 8 ++++-- .../extensions/extensions_suite_test.go | 9 ++++--- .../unix_domain_socket_executable.go | 25 +++++++++++-------- pkg/transport/process/extensions/utils.go | 6 +++-- .../process/processors/processor_factory.go | 13 +++++----- .../process/uploaders/uploader_factory.go | 10 +++++--- pkg/transport/processing_job_factory.go | 6 ++--- 8 files changed, 49 insertions(+), 33 deletions(-) diff --git a/ociclient/mock/client_mock.go b/ociclient/mock/client_mock.go index bae52f27..9f11dc62 100644 --- a/ociclient/mock/client_mock.go +++ b/ociclient/mock/client_mock.go @@ -9,10 +9,11 @@ import ( io "io" reflect "reflect" - ociclient "github.com/gardener/component-cli/ociclient" - oci "github.com/gardener/component-cli/ociclient/oci" gomock "github.com/golang/mock/gomock" v1 "github.com/opencontainers/image-spec/specs-go/v1" + + ociclient "github.com/gardener/component-cli/ociclient" + oci "github.com/gardener/component-cli/ociclient/oci" ) // MockClient is a mock of Client interface. diff --git a/pkg/transport/process/downloaders/downloader_factory.go b/pkg/transport/process/downloaders/downloader_factory.go index a6c68a44..bea3b7ad 100644 --- a/pkg/transport/process/downloaders/downloader_factory.go +++ b/pkg/transport/process/downloaders/downloader_factory.go @@ -7,6 +7,8 @@ import ( "encoding/json" "fmt" + "github.com/go-logr/logr" + "github.com/gardener/component-cli/ociclient" "github.com/gardener/component-cli/ociclient/cache" "github.com/gardener/component-cli/pkg/transport/process" @@ -26,10 +28,11 @@ const ( // - Add Go file to downloader package which contains the source code of the new downloader // - Add string constant for new downloader type -> will be used in DownloaderFactory.Create() // - Add source code for creating new downloader to DownloaderFactory.Create() method -func NewDownloaderFactory(client ociclient.Client, ocicache cache.Cache) *DownloaderFactory { +func NewDownloaderFactory(client ociclient.Client, ocicache cache.Cache, log logr.Logger) *DownloaderFactory { return &DownloaderFactory{ client: client, cache: ocicache, + log: log, } } @@ -37,6 +40,7 @@ func NewDownloaderFactory(client ociclient.Client, ocicache cache.Cache) *Downlo type DownloaderFactory struct { client ociclient.Client cache cache.Cache + log logr.Logger } // Create creates a new downloader defined by a type and a spec @@ -47,7 +51,7 @@ func (f *DownloaderFactory) Create(downloaderType string, spec *json.RawMessage) case OCIArtifactDownloaderType: return NewOCIArtifactDownloader(f.client, f.cache) case extensions.ExecutableType: - return extensions.CreateExecutable(spec) + return extensions.CreateExecutable(spec, f.log) default: return nil, fmt.Errorf("unknown downloader type %s", downloaderType) } diff --git a/pkg/transport/process/extensions/extensions_suite_test.go b/pkg/transport/process/extensions/extensions_suite_test.go index 0d904f4c..ea6bf7e6 100644 --- a/pkg/transport/process/extensions/extensions_suite_test.go +++ b/pkg/transport/process/extensions/extensions_suite_test.go @@ -15,6 +15,7 @@ import ( "time" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "github.com/go-logr/logr" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -76,14 +77,14 @@ var _ = Describe("transport extensions", func() { Context("unix domain socket executable", func() { It("should create processor successfully if env is nil", func() { args := []string{} - _, err := extensions.NewUnixDomainSocketExecutable(exampleProcessorBinaryPath, args, nil) + _, err := extensions.NewUnixDomainSocketExecutable(exampleProcessorBinaryPath, args, nil, logr.Discard()) Expect(err).ToNot(HaveOccurred()) }) It("should modify the processed resource correctly", func() { args := []string{} env := map[string]string{} - processor, err := extensions.NewUnixDomainSocketExecutable(exampleProcessorBinaryPath, args, env) + processor, err := extensions.NewUnixDomainSocketExecutable(exampleProcessorBinaryPath, args, env, logr.Discard()) Expect(err).ToNot(HaveOccurred()) runExampleResourceTest(processor) @@ -94,7 +95,7 @@ var _ = Describe("transport extensions", func() { env := map[string]string{ extensions.ProcessorServerAddressEnv: "/tmp/my-processor.sock", } - _, err := extensions.NewUnixDomainSocketExecutable(exampleProcessorBinaryPath, args, env) + _, err := extensions.NewUnixDomainSocketExecutable(exampleProcessorBinaryPath, args, env, logr.Discard()) Expect(err).To(MatchError(fmt.Sprintf("the env variable %s is not allowed to be set manually", extensions.ProcessorServerAddressEnv))) }) @@ -103,7 +104,7 @@ var _ = Describe("transport extensions", func() { env := map[string]string{ sleepTimeEnv: sleepTime.String(), } - processor, err := extensions.NewUnixDomainSocketExecutable(sleepProcessorBinaryPath, args, env) + processor, err := extensions.NewUnixDomainSocketExecutable(sleepProcessorBinaryPath, args, env, logr.Discard()) Expect(err).ToNot(HaveOccurred()) runTimeoutTest(processor) diff --git a/pkg/transport/process/extensions/unix_domain_socket_executable.go b/pkg/transport/process/extensions/unix_domain_socket_executable.go index 9e6f3864..35f664af 100644 --- a/pkg/transport/process/extensions/unix_domain_socket_executable.go +++ b/pkg/transport/process/extensions/unix_domain_socket_executable.go @@ -13,6 +13,8 @@ import ( "syscall" "time" + "github.com/go-logr/logr" + "github.com/gardener/component-cli/pkg/transport/process" "github.com/gardener/component-cli/pkg/utils" ) @@ -26,11 +28,12 @@ type unixDomainSocketExecutable struct { args []string env []string addr string + log logr.Logger } // NewUnixDomainSocketExecutable returns a resource processor extension which runs an executable in the // background when calling Process(). It communicates with this processor via Unix Domain Sockets. -func NewUnixDomainSocketExecutable(bin string, args []string, env map[string]string) (process.ResourceStreamProcessor, error) { +func NewUnixDomainSocketExecutable(bin string, args []string, env map[string]string, log logr.Logger) (process.ResourceStreamProcessor, error) { if _, ok := env[ProcessorServerAddressEnv]; ok { return nil, fmt.Errorf("the env variable %s is not allowed to be set manually", ProcessorServerAddressEnv) } @@ -52,6 +55,7 @@ func NewUnixDomainSocketExecutable(bin string, args []string, env map[string]str args: args, env: parsedEnv, addr: addr, + log: log, } return &e, nil @@ -66,6 +70,16 @@ func (e *unixDomainSocketExecutable) Process(ctx context.Context, r io.Reader, w if err := cmd.Start(); err != nil { return fmt.Errorf("unable to start processor: %w", err) } + defer func() { + // remove socket file if server hasn't already cleaned up + if _, err := os.Stat(e.addr); err == nil { + if err := os.Remove(e.addr); err != nil { + e.log.Error(err, "unable to remove "+e.addr) + } + } else if !os.IsNotExist(err) { + e.log.Error(err, "unable to get file stats for "+e.addr) + } + }() conn, err := tryConnect(e.addr) if err != nil { @@ -94,15 +108,6 @@ func (e *unixDomainSocketExecutable) Process(ctx context.Context, r io.Reader, w return fmt.Errorf("unable to wait for processor: %w", err) } - // remove socket file if server hasn't already cleaned up - if _, err := os.Stat(e.addr); err == nil { - if err := os.Remove(e.addr); err != nil { - return fmt.Errorf("unable to remove %s: %w", e.addr, err) - } - } else if !os.IsNotExist(err) { - return fmt.Errorf("unable to get file stats for %s: %w", e.addr, err) - } - return nil } diff --git a/pkg/transport/process/extensions/utils.go b/pkg/transport/process/extensions/utils.go index 4a21b1ea..e131de43 100644 --- a/pkg/transport/process/extensions/utils.go +++ b/pkg/transport/process/extensions/utils.go @@ -9,6 +9,8 @@ import ( "sigs.k8s.io/yaml" + "github.com/go-logr/logr" + "github.com/gardener/component-cli/pkg/transport/process" ) @@ -18,7 +20,7 @@ const ( ) // CreateExecutable creates a new executable defined by a spec -func CreateExecutable(rawSpec *json.RawMessage) (process.ResourceStreamProcessor, error) { +func CreateExecutable(rawSpec *json.RawMessage, log logr.Logger) (process.ResourceStreamProcessor, error) { type executableSpec struct { Bin string Args []string @@ -30,5 +32,5 @@ func CreateExecutable(rawSpec *json.RawMessage) (process.ResourceStreamProcessor return nil, fmt.Errorf("unable to parse spec: %w", err) } - return NewUnixDomainSocketExecutable(spec.Bin, spec.Args, spec.Env) + return NewUnixDomainSocketExecutable(spec.Bin, spec.Args, spec.Env, log) } diff --git a/pkg/transport/process/processors/processor_factory.go b/pkg/transport/process/processors/processor_factory.go index 4665e256..16361741 100644 --- a/pkg/transport/process/processors/processor_factory.go +++ b/pkg/transport/process/processors/processor_factory.go @@ -8,6 +8,7 @@ import ( "fmt" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "github.com/go-logr/logr" "sigs.k8s.io/yaml" "github.com/gardener/component-cli/ociclient/cache" @@ -24,15 +25,17 @@ const ( ) // NewProcessorFactory creates a new processor factory -func NewProcessorFactory(ociCache cache.Cache) *ProcessorFactory { +func NewProcessorFactory(ociCache cache.Cache, log logr.Logger) *ProcessorFactory { return &ProcessorFactory{ cache: ociCache, + log: log, } } // ProcessorFactory defines a helper struct for creating processors type ProcessorFactory struct { cache cache.Cache + log logr.Logger } // Create creates a new processor defined by a type and a spec @@ -43,7 +46,7 @@ func (f *ProcessorFactory) Create(processorType string, spec *json.RawMessage) ( case OCIArtifactFilterProcessorType: return f.createOCIArtifactFilter(spec) case extensions.ExecutableType: - return extensions.CreateExecutable(spec) + return extensions.CreateExecutable(spec, f.log) default: return nil, fmt.Errorf("unknown processor type %s", processorType) } @@ -55,8 +58,7 @@ func (f *ProcessorFactory) createResourceLabeler(rawSpec *json.RawMessage) (proc } var spec processorSpec - err := yaml.Unmarshal(*rawSpec, &spec) - if err != nil { + if err := yaml.Unmarshal(*rawSpec, &spec); err != nil { return nil, fmt.Errorf("unable to parse spec: %w", err) } @@ -69,8 +71,7 @@ func (f *ProcessorFactory) createOCIArtifactFilter(rawSpec *json.RawMessage) (pr } var spec processorSpec - err := yaml.Unmarshal(*rawSpec, &spec) - if err != nil { + if err := yaml.Unmarshal(*rawSpec, &spec); err != nil { return nil, fmt.Errorf("unable to parse spec: %w", err) } diff --git a/pkg/transport/process/uploaders/uploader_factory.go b/pkg/transport/process/uploaders/uploader_factory.go index f7f96d62..90fa2838 100644 --- a/pkg/transport/process/uploaders/uploader_factory.go +++ b/pkg/transport/process/uploaders/uploader_factory.go @@ -8,6 +8,7 @@ import ( "fmt" cdv2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "github.com/go-logr/logr" "sigs.k8s.io/yaml" "github.com/gardener/component-cli/ociclient" @@ -29,11 +30,12 @@ const ( // - Add Go file to uploaders package which contains the source code of the new uploader // - Add string constant for new uploader type -> will be used in UploaderFactory.Create() // - Add source code for creating new uploader to UploaderFactory.Create() method -func NewUploaderFactory(client ociclient.Client, ocicache cache.Cache, targetCtx cdv2.OCIRegistryRepository) *UploaderFactory { +func NewUploaderFactory(client ociclient.Client, ocicache cache.Cache, targetCtx cdv2.OCIRegistryRepository, log logr.Logger) *UploaderFactory { return &UploaderFactory{ client: client, cache: ocicache, targetCtx: targetCtx, + log: log, } } @@ -42,6 +44,7 @@ type UploaderFactory struct { client ociclient.Client cache cache.Cache targetCtx cdv2.OCIRegistryRepository + log logr.Logger } // Create creates a new uploader defined by a type and a spec @@ -52,7 +55,7 @@ func (f *UploaderFactory) Create(uploaderType string, spec *json.RawMessage) (pr case OCIArtifactUploaderType: return f.createOCIArtifactUploader(spec) case extensions.ExecutableType: - return extensions.CreateExecutable(spec) + return extensions.CreateExecutable(spec, f.log) default: return nil, fmt.Errorf("unknown uploader type %s", uploaderType) } @@ -65,8 +68,7 @@ func (f *UploaderFactory) createOCIArtifactUploader(rawSpec *json.RawMessage) (p } var spec uploaderSpec - err := yaml.Unmarshal(*rawSpec, &spec) - if err != nil { + if err := yaml.Unmarshal(*rawSpec, &spec); err != nil { return nil, fmt.Errorf("unable to parse spec: %w", err) } diff --git a/pkg/transport/processing_job_factory.go b/pkg/transport/processing_job_factory.go index 6db49314..dd684053 100644 --- a/pkg/transport/processing_job_factory.go +++ b/pkg/transport/processing_job_factory.go @@ -20,9 +20,9 @@ import ( // NewProcessingJobFactory creates a new processing job factory func NewProcessingJobFactory(transportCfg config.ParsedTransportConfig, ociClient ociclient.Client, ocicache cache.Cache, targetCtx cdv2.OCIRegistryRepository, log logr.Logger, processorTimeout time.Duration) (*ProcessingJobFactory, error) { - df := downloaders.NewDownloaderFactory(ociClient, ocicache) - pf := processors.NewProcessorFactory(ocicache) - uf := uploaders.NewUploaderFactory(ociClient, ocicache, targetCtx) + df := downloaders.NewDownloaderFactory(ociClient, ocicache, log) + pf := processors.NewProcessorFactory(ocicache, log) + uf := uploaders.NewUploaderFactory(ociClient, ocicache, targetCtx, log) f := ProcessingJobFactory{ parsedConfig: &transportCfg, From fb6cf9d1050a968217a0a97a44b38797f5bf1e29 Mon Sep 17 00:00:00 2001 From: Johannes Schicktanz Date: Mon, 24 Jan 2022 13:06:13 +0100 Subject: [PATCH 94/94] moves transport cmd to remote command --- cmd/component-cli/app/app.go | 2 -- pkg/commands/componentarchive/remote/remote.go | 1 + .../{transport => componentarchive/remote}/transport.go | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) rename pkg/commands/{transport => componentarchive/remote}/transport.go (99%) diff --git a/cmd/component-cli/app/app.go b/cmd/component-cli/app/app.go index 9a6ec9cb..d0ea635c 100644 --- a/cmd/component-cli/app/app.go +++ b/cmd/component-cli/app/app.go @@ -16,7 +16,6 @@ import ( "github.com/gardener/component-cli/pkg/commands/ctf" "github.com/gardener/component-cli/pkg/commands/imagevector" "github.com/gardener/component-cli/pkg/commands/oci" - "github.com/gardener/component-cli/pkg/commands/transport" "github.com/gardener/component-cli/pkg/logcontext" "github.com/gardener/component-cli/pkg/logger" "github.com/gardener/component-cli/pkg/version" @@ -49,7 +48,6 @@ func NewComponentsCliCommand(ctx context.Context) *cobra.Command { cmd.AddCommand(imagevector.NewImageVectorCommand(ctx)) cmd.AddCommand(oci.NewOCICommand(ctx)) cmd.AddCommand(cachecmd.NewCacheCommand(ctx)) - cmd.AddCommand(transport.NewTransportCommand(ctx)) return cmd } diff --git a/pkg/commands/componentarchive/remote/remote.go b/pkg/commands/componentarchive/remote/remote.go index 89738ecf..7e80f4b8 100644 --- a/pkg/commands/componentarchive/remote/remote.go +++ b/pkg/commands/componentarchive/remote/remote.go @@ -20,6 +20,7 @@ func NewRemoteCommand(ctx context.Context) *cobra.Command { cmd.AddCommand(NewPushCommand(ctx)) cmd.AddCommand(NewGetCommand(ctx)) cmd.AddCommand(NewCopyCommand(ctx)) + cmd.AddCommand(NewTransportCommand(ctx)) return cmd } diff --git a/pkg/commands/transport/transport.go b/pkg/commands/componentarchive/remote/transport.go similarity index 99% rename from pkg/commands/transport/transport.go rename to pkg/commands/componentarchive/remote/transport.go index 455c7826..9e7626aa 100644 --- a/pkg/commands/transport/transport.go +++ b/pkg/commands/componentarchive/remote/transport.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2021 SAP SE or an SAP affiliate company and Gardener contributors. // // SPDX-License-Identifier: Apache-2.0 -package transport +package remote import ( "context"