From e77f8c6bab009c6002116f06c7dbc403ec8739a6 Mon Sep 17 00:00:00 2001 From: Uwe Krueger Date: Fri, 23 Aug 2024 12:47:51 +0200 Subject: [PATCH] Validate method for oci/ocm repository specs (#866) #### What this PR does / why we need it: This PR introduces a `Validate` method for OCI and OCM repository specifications. Its task is to validate the spec as well as the connectivity (and potentially the credentials), if possible. Checking credentials makes only sense, if there is no possibility to use different credentials for different shards of the repository (for example organizations in the GitHub OCI registry). --- .../repositories/artifactset/format.go | 21 +++-- .../repositories/artifactset/type.go | 11 ++- api/oci/extensions/repositories/ctf/format.go | 7 ++ api/oci/extensions/repositories/ctf/type.go | 8 ++ .../repositories/docker/docker_test.go | 20 +++++ .../repositories/docker/suite_test.go | 13 +++ .../extensions/repositories/docker/type.go | 12 +++ api/oci/extensions/repositories/empty/type.go | 4 + .../extensions/repositories/ocireg/type.go | 14 ++++ api/oci/internal/repotypes.go | 14 ++++ .../repositories/comparch/comparch_test.go | 62 +++++++++++++- .../repositories/comparch/format.go | 8 +- .../extensions/repositories/comparch/type.go | 15 +++- .../repositories/composition/type.go | 4 + .../repositories/genericocireg/type.go | 4 + .../extensions/repositories/virtual/type.go | 6 +- api/ocm/internal/repotypes.go | 30 +++++++ api/utils/accessio/opts.go | 9 +- api/utils/accessobj/accessobject.go | 84 +++++++++++++++++++ api/utils/tcp/ping.go | 30 +++++++ 20 files changed, 359 insertions(+), 17 deletions(-) create mode 100644 api/oci/extensions/repositories/docker/docker_test.go create mode 100644 api/oci/extensions/repositories/docker/suite_test.go create mode 100644 api/utils/tcp/ping.go diff --git a/api/oci/extensions/repositories/artifactset/format.go b/api/oci/extensions/repositories/artifactset/format.go index 485f97dbd5..32e827fec0 100644 --- a/api/oci/extensions/repositories/artifactset/format.go +++ b/api/oci/extensions/repositories/artifactset/format.go @@ -6,6 +6,7 @@ import ( "github.com/mandelsoft/goutils/errors" "github.com/mandelsoft/vfs/pkg/vfs" + "ocm.software/ocm/api/oci/artdesc" "ocm.software/ocm/api/utils/accessio" "ocm.software/ocm/api/utils/accessobj" "ocm.software/ocm/api/utils/blobaccess/blobaccess" @@ -45,14 +46,22 @@ type accessObjectInfo struct { var _ accessobj.AccessObjectInfo = (*accessObjectInfo)(nil) +var baseInfo = accessobj.DefaultAccessObjectInfo{ + ObjectTypeName: "artifactset", + ElementDirectoryName: BlobsDirectoryName, + ElementTypeName: "blob", + DescriptorHandlerFactory: NewStateHandler, + DescriptorValidator: validateDescriptor, +} + +func validateDescriptor(data []byte) error { + _, err := artdesc.DecodeIndex(data) + return err +} + func NewAccessObjectInfo(fmts ...string) accessobj.AccessObjectInfo { a := &accessObjectInfo{ - accessobj.DefaultAccessObjectInfo{ - ObjectTypeName: "artifactset", - ElementDirectoryName: BlobsDirectoryName, - ElementTypeName: "blob", - DescriptorHandlerFactory: NewStateHandler, - }, + baseInfo, } oci := IsOCIDefaultFormat() if len(fmts) > 0 { diff --git a/api/oci/extensions/repositories/artifactset/type.go b/api/oci/extensions/repositories/artifactset/type.go index 512c9bc36b..ae8bb18cb7 100644 --- a/api/oci/extensions/repositories/artifactset/type.go +++ b/api/oci/extensions/repositories/artifactset/type.go @@ -4,6 +4,7 @@ import ( "github.com/mandelsoft/vfs/pkg/vfs" "ocm.software/ocm/api/credentials" + "ocm.software/ocm/api/datacontext/attrs/vfsattr" "ocm.software/ocm/api/oci/cpi" "ocm.software/ocm/api/utils/accessio" "ocm.software/ocm/api/utils/accessobj" @@ -78,11 +79,19 @@ func (a *RepositorySpec) Repository(ctx cpi.Context, creds credentials.Credentia return NewRepository(ctx, a) } -func (a *RepositorySpec) AsUniformSpec(cpi.Context) cpi.UniformRepositorySpec { +func (a *RepositorySpec) AsUniformSpec(ctx cpi.Context) cpi.UniformRepositorySpec { opts, _ := NewOptions(&a.Options) // now unknown option possible (same Options type) + opts.Default(vfsattr.Get(ctx)) p, err := vfs.Canonical(opts.GetPathFileSystem(), a.FilePath, false) if err != nil { return cpi.UniformRepositorySpec{Type: a.GetKind(), Info: a.FilePath} } return cpi.UniformRepositorySpec{Type: a.GetKind(), Info: p} } + +func (a *RepositorySpec) Validate(ctx cpi.Context, creds credentials.Credentials, context ...credentials.UsageContext) error { + opts := a.Options + opts.Default(vfsattr.Get(ctx)) + + return accessobj.ValidateDescriptor(&baseInfo, a.FilePath, opts.GetPathFileSystem()) +} diff --git a/api/oci/extensions/repositories/ctf/format.go b/api/oci/extensions/repositories/ctf/format.go index 5143cb2f3d..90ed959b71 100644 --- a/api/oci/extensions/repositories/ctf/format.go +++ b/api/oci/extensions/repositories/ctf/format.go @@ -10,6 +10,7 @@ import ( "ocm.software/ocm/api/oci/cpi" "ocm.software/ocm/api/oci/extensions/repositories/ctf/format" + "ocm.software/ocm/api/oci/extensions/repositories/ctf/index" "ocm.software/ocm/api/utils/accessio" "ocm.software/ocm/api/utils/accessobj" "ocm.software/ocm/api/utils/blobaccess/blobaccess" @@ -26,6 +27,12 @@ var accessObjectInfo = &accessobj.DefaultAccessObjectInfo{ ElementDirectoryName: BlobsDirectoryName, ElementTypeName: "blob", DescriptorHandlerFactory: NewStateHandler, + DescriptorValidator: validateDescriptor, +} + +func validateDescriptor(data []byte) error { + _, err := index.Decode(data) + return err } type Object = Repository diff --git a/api/oci/extensions/repositories/ctf/type.go b/api/oci/extensions/repositories/ctf/type.go index 433442212c..67d782aeb5 100644 --- a/api/oci/extensions/repositories/ctf/type.go +++ b/api/oci/extensions/repositories/ctf/type.go @@ -4,6 +4,7 @@ import ( "strings" "ocm.software/ocm/api/credentials" + "ocm.software/ocm/api/datacontext/attrs/vfsattr" "ocm.software/ocm/api/oci/cpi" "ocm.software/ocm/api/utils/accessio" "ocm.software/ocm/api/utils/accessobj" @@ -81,3 +82,10 @@ func (s *RepositorySpec) UniformRepositorySpec() *cpi.UniformRepositorySpec { func (a *RepositorySpec) Repository(ctx cpi.Context, creds credentials.Credentials) (cpi.Repository, error) { return Open(ctx, a.AccessMode, a.FilePath, 0o700, &a.StandardOptions) } + +func (a *RepositorySpec) Validate(ctx cpi.Context, creds credentials.Credentials, context ...credentials.UsageContext) error { + opts := a.StandardOptions + opts.Default(vfsattr.Get(ctx)) + + return accessobj.ValidateDescriptor(accessObjectInfo, a.FilePath, opts.GetPathFileSystem()) +} diff --git a/api/oci/extensions/repositories/docker/docker_test.go b/api/oci/extensions/repositories/docker/docker_test.go new file mode 100644 index 0000000000..f0196740a6 --- /dev/null +++ b/api/oci/extensions/repositories/docker/docker_test.go @@ -0,0 +1,20 @@ +//go:build docker_test + +package docker_test + +import ( + . "github.com/onsi/ginkgo/v2" + + "github.com/mandelsoft/goutils/testutils" + + "ocm.software/ocm/api/oci" + "ocm.software/ocm/api/oci/extensions/repositories/docker" +) + +var _ = Describe("Local Docker Daemon", func() { + It("validated access", func() { + octx := oci.DefaultContext() + spec := docker.NewRepositorySpec() + testutils.MustBeSuccessful(spec.Validate(octx, nil)) + }) +}) diff --git a/api/oci/extensions/repositories/docker/suite_test.go b/api/oci/extensions/repositories/docker/suite_test.go new file mode 100644 index 0000000000..088cec0b6f --- /dev/null +++ b/api/oci/extensions/repositories/docker/suite_test.go @@ -0,0 +1,13 @@ +package docker_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "docker daemon Test Suite") +} diff --git a/api/oci/extensions/repositories/docker/type.go b/api/oci/extensions/repositories/docker/type.go index dbe79e5312..adcf5fa05d 100644 --- a/api/oci/extensions/repositories/docker/type.go +++ b/api/oci/extensions/repositories/docker/type.go @@ -1,6 +1,8 @@ package docker import ( + "context" + "ocm.software/ocm/api/credentials" "ocm.software/ocm/api/oci/cpi" "ocm.software/ocm/api/utils" @@ -46,3 +48,13 @@ func (a *RepositorySpec) UniformRepositorySpec() *cpi.UniformRepositorySpec { func (a *RepositorySpec) Repository(ctx cpi.Context, creds credentials.Credentials) (cpi.Repository, error) { return NewRepository(ctx, a) } + +func (a *RepositorySpec) Validate(ctx cpi.Context, creds credentials.Credentials, usageContext ...credentials.UsageContext) error { + client, err := newDockerClient(a.DockerHost) + if err != nil { + return err + } + + _, err = client.Ping(context.Background()) + return err +} diff --git a/api/oci/extensions/repositories/empty/type.go b/api/oci/extensions/repositories/empty/type.go index c95c89ba58..3f59ac7b9e 100644 --- a/api/oci/extensions/repositories/empty/type.go +++ b/api/oci/extensions/repositories/empty/type.go @@ -49,3 +49,7 @@ func (a *RepositorySpec) UniformRepositorySpec() *cpi.UniformRepositorySpec { func (a *RepositorySpec) Repository(ctx cpi.Context, creds credentials.Credentials) (cpi.Repository, error) { return ctx.GetAttributes().GetOrCreateAttribute(ATTR_REPOS, func(datacontext.Context) interface{} { return NewRepository(ctx) }).(cpi.Repository), nil } + +func (a *RepositorySpec) Validate(cpi.Context, credentials.Credentials, ...credentials.UsageContext) error { + return nil +} diff --git a/api/oci/extensions/repositories/ocireg/type.go b/api/oci/extensions/repositories/ocireg/type.go index 8c3d92b1b9..52eac8df97 100644 --- a/api/oci/extensions/repositories/ocireg/type.go +++ b/api/oci/extensions/repositories/ocireg/type.go @@ -4,6 +4,7 @@ import ( "fmt" "net/url" "strings" + "time" "github.com/containerd/containerd/reference" @@ -12,6 +13,7 @@ import ( "ocm.software/ocm/api/oci/cpi" "ocm.software/ocm/api/utils" "ocm.software/ocm/api/utils/runtime" + "ocm.software/ocm/api/utils/tcp" ) const ( @@ -128,6 +130,18 @@ func (a *RepositorySpec) Repository(ctx cpi.Context, creds credentials.Credentia return NewRepository(ctx, a, info) } +func (a *RepositorySpec) Validate(ctx cpi.Context, creds credentials.Credentials, context ...credentials.UsageContext) error { + info, err := a.getInfo(creds) + if err != nil { + return err + } + h, p, _ := info.HostInfo() + if p == "" { + p = "443" + } + return tcp.PingTCPServer(h+":"+p, time.Second) +} + func (a *RepositorySpec) GetConsumerId(uctx ...credentials.UsageContext) credentials.ConsumerIdentity { info, err := a.getInfo(nil) if err != nil { diff --git a/api/oci/internal/repotypes.go b/api/oci/internal/repotypes.go index 9125e57a23..c2300088a4 100644 --- a/api/oci/internal/repotypes.go +++ b/api/oci/internal/repotypes.go @@ -26,6 +26,8 @@ type RepositorySpec interface { Name() string UniformRepositorySpec() *UniformRepositorySpec Repository(Context, credentials.Credentials) (Repository, error) + + Validate(Context, credentials.Credentials, ...credentials.UsageContext) error } type ( @@ -95,6 +97,10 @@ func (r *UnknownRepositorySpec) Repository(Context, credentials.Credentials) (Re return nil, errors.ErrUnknown("repository type", r.GetType()) } +func (r *UnknownRepositorySpec) Validate(Context, credentials.Credentials, ...credentials.UsageContext) error { + return errors.ErrUnknown("repository type", r.GetType()) +} + //////////////////////////////////////////////////////////////////////////////// type GenericRepositorySpec struct { @@ -157,4 +163,12 @@ func (s *GenericRepositorySpec) Repository(ctx Context, creds credentials.Creden return spec.Repository(ctx, creds) } +func (s *GenericRepositorySpec) Validate(ctx Context, creds credentials.Credentials, context ...credentials.UsageContext) error { + spec, err := s.Evaluate(ctx) + if err != nil { + return err + } + return spec.Validate(ctx, creds) +} + //////////////////////////////////////////////////////////////////////////////// diff --git a/api/ocm/extensions/repositories/comparch/comparch_test.go b/api/ocm/extensions/repositories/comparch/comparch_test.go index f8f6cfe904..5c64bd167a 100644 --- a/api/ocm/extensions/repositories/comparch/comparch_test.go +++ b/api/ocm/extensions/repositories/comparch/comparch_test.go @@ -49,6 +49,22 @@ var _ = Describe("Repository", func() { // spec will not equal r as the filesystem cannot be serialized }) + It("validates component archive with resource stored as tar", func() { + // this is the typical use case + octx := ocm.DefaultContext() + spec := Must(comparch.NewRepositorySpec(accessobj.ACC_READONLY, TAR_COMPARCH)) + + MustBeSuccessful(spec.Validate(octx, nil)) + }) + + It("validates component archive with resource stored as dir", func() { + // this is the typical use case + octx := ocm.DefaultContext() + spec := Must(comparch.NewRepositorySpec(accessobj.ACC_READONLY, DIR_COMPARCH)) + + MustBeSuccessful(spec.Validate(octx, nil)) + }) + It("component archive with resource stored as tar", func() { // this is the typical use case octx := ocm.DefaultContext() @@ -87,7 +103,7 @@ var _ = Describe("Repository", func() { Expect(bufferA).To(Equal(bufferB)) }) - It("creates component archive", func() { + It("creates component archive directory", func() { octx := ocm.DefaultContext() memfs := memoryfs.New() @@ -111,6 +127,50 @@ var _ = Describe("Repository", func() { })) MustBeSuccessful(finalize.Finalize()) + Expect(vfs.DirExists(memfs, "test")).To(BeTrue()) + + spec := Must(comparch.NewRepositorySpec(accessobj.ACC_READONLY, "test", accessio.PathFileSystem(memfs))) + MustBeSuccessful(spec.Validate(octx, nil)) + + arch = Must(comparch.Open(octx, accessobj.ACC_WRITABLE, "test", 0o0700, accessio.PathFileSystem(memfs))) + finalize.Close(arch, "comparch)") + + res = Must(arch.GetResourcesByName("blob")) + Expect(res[0].Meta().Digest).To(DeepEqual(&metav1.DigestSpec{ + HashAlgorithm: sha256.Algorithm, + NormalisationAlgorithm: blob.GenericBlobDigestV1, + Value: D_TESTDATA, + })) + }) + + It("creates component archive tgz", func() { + octx := ocm.DefaultContext() + memfs := memoryfs.New() + + var finalize finalizer.Finalizer + defer Defer(finalize.Finalize) + + arch := Must(comparch.Create(octx, accessobj.ACC_WRITABLE, "test", 0o0700, accessio.FormatTGZ, accessio.PathFileSystem(memfs))) + finalize.Close(arch, "comparch)") + + arch.SetName("acme.org/test") + arch.SetVersion("v1.0.1") + + MustBeSuccessful(arch.SetResourceBlob(compdesc.NewResourceMeta("blob", resourcetypes.PLAIN_TEXT, metav1.LocalRelation), + blobaccess.ForString(mime.MIME_TEXT, S_TESTDATA), "", nil)) + + res := Must(arch.GetResourcesByName("blob")) + Expect(res[0].Meta().Digest).To(DeepEqual(&metav1.DigestSpec{ + HashAlgorithm: sha256.Algorithm, + NormalisationAlgorithm: blob.GenericBlobDigestV1, + Value: D_TESTDATA, + })) + + MustBeSuccessful(finalize.Finalize()) + Expect(vfs.FileExists(memfs, "test")).To(BeTrue()) + + spec := Must(comparch.NewRepositorySpec(accessobj.ACC_READONLY, "test", accessio.PathFileSystem(memfs))) + MustBeSuccessful(spec.Validate(octx, nil)) arch = Must(comparch.Open(octx, accessobj.ACC_WRITABLE, "test", 0o0700, accessio.PathFileSystem(memfs))) finalize.Close(arch, "comparch)") diff --git a/api/ocm/extensions/repositories/comparch/format.go b/api/ocm/extensions/repositories/comparch/format.go index 5548cc899f..0946f33e97 100644 --- a/api/ocm/extensions/repositories/comparch/format.go +++ b/api/ocm/extensions/repositories/comparch/format.go @@ -20,10 +20,16 @@ const BlobsDirectoryName = "blobs" var accessObjectInfo = &accessobj.DefaultAccessObjectInfo{ DescriptorFileName: ComponentDescriptorFileName, - ObjectTypeName: "artifactset", + ObjectTypeName: "component archive", ElementDirectoryName: BlobsDirectoryName, ElementTypeName: "blob", DescriptorHandlerFactory: NewStateHandler, + DescriptorValidator: validateDescriptor, +} + +func validateDescriptor(data []byte) error { + _, err := compdesc.Decode(data) + return err } type Object = ComponentArchive diff --git a/api/ocm/extensions/repositories/comparch/type.go b/api/ocm/extensions/repositories/comparch/type.go index c9a4f94a48..00af926f0b 100644 --- a/api/ocm/extensions/repositories/comparch/type.go +++ b/api/ocm/extensions/repositories/comparch/type.go @@ -4,6 +4,7 @@ import ( "github.com/mandelsoft/vfs/pkg/vfs" "ocm.software/ocm/api/credentials" + "ocm.software/ocm/api/datacontext/attrs/vfsattr" "ocm.software/ocm/api/ocm/cpi" "ocm.software/ocm/api/utils/accessio" "ocm.software/ocm/api/utils/accessobj" @@ -63,12 +64,20 @@ func (a *RepositorySpec) Repository(ctx cpi.Context, creds credentials.Credentia return NewRepository(ctx, a) } -func (a *RepositorySpec) AsUniformSpec(cpi.Context) *cpi.UniformRepositorySpec { - opts := &accessio.StandardOptions{} - opts.Default() +func (a *RepositorySpec) AsUniformSpec(ctx cpi.Context) *cpi.UniformRepositorySpec { + opts := a.StandardOptions + opts.Default(vfsattr.Get(ctx)) + p, err := vfs.Canonical(opts.GetPathFileSystem(), a.FilePath, false) if err != nil { return &cpi.UniformRepositorySpec{Type: a.GetKind(), SubPath: a.FilePath} } return &cpi.UniformRepositorySpec{Type: a.GetKind(), SubPath: p} } + +func (a *RepositorySpec) Validate(ctx cpi.Context, creds credentials.Credentials, context ...credentials.UsageContext) error { + opts := a.StandardOptions + opts.Default(vfsattr.Get(ctx)) + + return accessobj.ValidateDescriptor(accessObjectInfo, a.FilePath, opts.GetPathFileSystem()) +} diff --git a/api/ocm/extensions/repositories/composition/type.go b/api/ocm/extensions/repositories/composition/type.go index 95fdb9b10b..57668e971a 100644 --- a/api/ocm/extensions/repositories/composition/type.go +++ b/api/ocm/extensions/repositories/composition/type.go @@ -38,4 +38,8 @@ func (r *RepositorySpec) Repository(ctx cpi.Context, credentials credentials.Cre return NewRepository(ctx, r.Name), nil } +func (a *RepositorySpec) Validate(ctx cpi.Context, creds credentials.Credentials, context ...credentials.UsageContext) error { + return nil +} + var _ cpi.RepositorySpec = (*RepositorySpec)(nil) diff --git a/api/ocm/extensions/repositories/genericocireg/type.go b/api/ocm/extensions/repositories/genericocireg/type.go index 9b43dcae63..2ae27c12b2 100644 --- a/api/ocm/extensions/repositories/genericocireg/type.go +++ b/api/ocm/extensions/repositories/genericocireg/type.go @@ -180,6 +180,10 @@ func (s *RepositorySpec) GetIdentityMatcher() string { return credentials.GetProvidedIdentityMatcher(s.RepositorySpec) } +func (s *RepositorySpec) Validate(ctx cpi.Context, creds credentials.Credentials, uctx ...credentials.UsageContext) error { + return s.RepositorySpec.Validate(ctx.OCIContext(), creds, uctx...) +} + func DefaultComponentRepositoryMeta(meta *ComponentRepositoryMeta) *ComponentRepositoryMeta { if meta == nil { meta = &ComponentRepositoryMeta{} diff --git a/api/ocm/extensions/repositories/virtual/type.go b/api/ocm/extensions/repositories/virtual/type.go index 48f3d4d77f..a73bf8ce8a 100644 --- a/api/ocm/extensions/repositories/virtual/type.go +++ b/api/ocm/extensions/repositories/virtual/type.go @@ -16,6 +16,8 @@ type RepositorySpec struct { Access Access `json:"-"` } +var _ cpi.RepositorySpec = (*RepositorySpec)(nil) + func NewRepositorySpec(acc Access) *RepositorySpec { return &RepositorySpec{ ObjectVersionedTypedObject: runtime.NewVersionedTypedObject(Type), @@ -31,4 +33,6 @@ func (r *RepositorySpec) Repository(ctx cpi.Context, credentials credentials.Cre return NewRepository(ctx, r.Access), nil } -var _ cpi.RepositorySpec = (*RepositorySpec)(nil) +func (a *RepositorySpec) Validate(ctx cpi.Context, creds credentials.Credentials, context ...credentials.UsageContext) error { + return nil +} diff --git a/api/ocm/internal/repotypes.go b/api/ocm/internal/repotypes.go index 5ec9e63fd0..f3b2cbb445 100644 --- a/api/ocm/internal/repotypes.go +++ b/api/ocm/internal/repotypes.go @@ -26,8 +26,26 @@ type IntermediateRepositorySpecAspect = oci.IntermediateRepositorySpecAspect type RepositorySpec interface { runtime.VersionedTypedObject + // AsUniformSpec transforms the specification object + // into a uniform repository spec as provided by + // the string based parsing for repository/component/version + // notations. The provided spec MUST be convertable again + // into a repository spec with the same meaning + // by registering an appropriate RepositorySpecHandler. AsUniformSpec(Context) *UniformRepositorySpec + + // Repository provides a repository implementation for the + // repository specification. This repository can then be used + // to access component versions in the technical OCM repository + // described by the repository specification. Repository(Context, credentials.Credentials) (Repository, error) + + // Validate can be used to execute a type specific validation for + // a repository specification. It should check the consistency of the + // specified spec fields as well as the connectivity to the repository + // as far as this is possible without a concrete component/version + // in mind. + Validate(Context, credentials.Credentials, ...credentials.UsageContext) error } type ( @@ -93,6 +111,10 @@ func (r *UnknownRepositorySpec) Repository(Context, credentials.Credentials) (Re return nil, errors.ErrUnknown("repository type", r.GetType()) } +func (r *UnknownRepositorySpec) Validate(ctx Context, creds credentials.Credentials, context ...credentials.UsageContext) error { + return errors.ErrUnknown("repository type", r.GetType()) +} + //////////////////////////////////////////////////////////////////////////////// type GenericRepositorySpec struct { @@ -155,4 +177,12 @@ func (s *GenericRepositorySpec) Repository(ctx Context, creds credentials.Creden return spec.Repository(ctx, creds) } +func (s *GenericRepositorySpec) Validate(ctx Context, creds credentials.Credentials, context ...credentials.UsageContext) error { + spec, err := s.Evaluate(ctx) + if err != nil { + return err + } + return spec.Validate(ctx, creds) +} + //////////////////////////////////////////////////////////////////////////////// diff --git a/api/utils/accessio/opts.go b/api/utils/accessio/opts.go index 2ac8e847c8..8f91a279ff 100644 --- a/api/utils/accessio/opts.go +++ b/api/utils/accessio/opts.go @@ -7,6 +7,7 @@ import ( "github.com/mandelsoft/vfs/pkg/osfs" "github.com/mandelsoft/vfs/pkg/vfs" + "ocm.software/ocm/api/utils" "ocm.software/ocm/api/utils/compression" ) @@ -32,13 +33,13 @@ type Options interface { WriterFor(path string, mode vfs.FileMode) (io.WriteCloser, error) DefaultFormat(fmt FileFormat) - Default() + Default(fss ...vfs.FileSystem) DefaultForPath(path string) error } type StandardOptions struct { - // FilePath is the path of the repository base in the filesystem + // FileFormat is the optional format. FileFormat *FileFormat `json:"fileFormat,omitempty"` // FileSystem is the virtual filesystem to evaluate the file path. Default is the OS filesytem // or the filesystem defined as base filesystem for the context @@ -122,9 +123,9 @@ func (o *StandardOptions) ApplyOption(options Options) error { var _osfs = osfs.New() -func (o *StandardOptions) Default() { +func (o *StandardOptions) Default(fss ...vfs.FileSystem) { if o.PathFileSystem == nil { - o.PathFileSystem = _osfs + o.PathFileSystem = utils.FileSystem(fss...) } } diff --git a/api/utils/accessobj/accessobject.go b/api/utils/accessobj/accessobject.go index 4005dbc621..2335198b22 100644 --- a/api/utils/accessobj/accessobject.go +++ b/api/utils/accessobj/accessobject.go @@ -1,13 +1,17 @@ package accessobj import ( + "archive/tar" "fmt" + "io" "github.com/mandelsoft/filepath/pkg/filepath" "github.com/mandelsoft/goutils/errors" "github.com/mandelsoft/vfs/pkg/vfs" + "ocm.software/ocm/api/utils" "ocm.software/ocm/api/utils/accessio" + "ocm.software/ocm/api/utils/compression" ) type DescriptorHandlerFactory func(fs vfs.FileSystem) StateHandler @@ -26,6 +30,8 @@ type AccessObjectInfo interface { SetupFileSystem(fs vfs.FileSystem, mode vfs.FileMode) error SetupDescriptorState(fs vfs.FileSystem) StateHandler SubPath(name string) string + + ValidateDescriptor(data []byte) error } // DefaultAccessObjectInfo is a default implementation for AccessObjectInfo @@ -38,6 +44,7 @@ type DefaultAccessObjectInfo struct { ElementTypeName string DescriptorHandlerFactory DescriptorHandlerFactory AdditionalFiles []string + DescriptorValidator func([]byte) error } var _ AccessObjectInfo = (*DefaultAccessObjectInfo)(nil) @@ -81,6 +88,13 @@ func (i *DefaultAccessObjectInfo) SubPath(name string) string { return filepath.Join(i.ElementDirectoryName, name) } +func (i *DefaultAccessObjectInfo) ValidateDescriptor(data []byte) error { + if i.DescriptorValidator != nil { + return i.DescriptorValidator(data) + } + return nil +} + // AccessObject provides a basic functionality for descriptor based access objects // using a virtual filesystem for the internal representation. type AccessObject struct { @@ -198,3 +212,73 @@ func (a *AccessObject) Close() error { a.fs = nil return list.Result() } + +//////////////////////////////////////////////////////////////////////////////// + +func ValidateDescriptor(info AccessObjectInfo, path string, fss ...vfs.FileSystem) error { + fs := utils.FileSystem(fss...) + _, err := vfs.Canonical(fs, path, true) + if err != nil { + return err + } + ok, err := vfs.Exists(fs, path) + if err != nil { + return err + } + if !ok { + return errors.ErrNotFound(info.GetObjectTypeName(), path) + } + + ok, err = vfs.IsDir(fs, path) + if err != nil { + return err + } + // check directory mode + if ok { + data, err := vfs.ReadFile(fs, filepath.Join(path, info.GetDescriptorFileName())) + if err != nil { + return err + } + return info.ValidateDescriptor(data) + } + + // check file mode + ok, err = vfs.IsFile(fs, path) + if err != nil { + return err + } + if !ok { + return errors.ErrInvalid(info.GetObjectTypeName(), path) + } + + r, err := fs.Open(path) + if err != nil { + return err + } + defer r.Close() + + eff, _, err := compression.AutoDecompress(r) + if err != nil { + return err + } + defer eff.Close() + + tr := tar.NewReader(eff) + for { + header, err := tr.Next() + if err != nil { + if errors.Is(err, io.EOF) { + return fmt.Errorf("no descriptor file (%s) found for %s", info.GetDescriptorFileName(), info.GetObjectTypeName()) + } + return err + } + + if header.Typeflag == tar.TypeReg && header.Name == info.GetDescriptorFileName() { + data, err := io.ReadAll(tr) + if err != nil { + return err + } + return info.ValidateDescriptor(data) + } + } +} diff --git a/api/utils/tcp/ping.go b/api/utils/tcp/ping.go new file mode 100644 index 0000000000..9e8f08bf5f --- /dev/null +++ b/api/utils/tcp/ping.go @@ -0,0 +1,30 @@ +package tcp + +import ( + "context" + "net" + "time" + + "github.com/mandelsoft/goutils/errors" +) + +func PingTCPServer(address string, dur time.Duration) error { + var conn net.Conn + var d net.Dialer + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + end := time.Now().Add(dur) + err := errors.New("timed out waiting for server to start") + for time.Now().Before(end) { + conn, err = d.DialContext(ctx, "tcp", address) + if err != nil { + time.Sleep(time.Second) + continue + } + conn.Close() + break + } + return err +}