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 +}