diff --git a/README.md b/README.md index a79e48f..32baf63 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,9 @@ container rename container delete delete the given container +container ttl + set TTL (in hours) for container versions + container list list containers diff --git a/cli/service/pb_mock.go b/cli/service/pb_mock.go index d072ecb..d070538 100644 --- a/cli/service/pb_mock.go +++ b/cli/service/pb_mock.go @@ -2,6 +2,7 @@ package service import ( "context" + "time" v1proto "github.com/teran/archived/manager/presenter/grpc/proto/v1" "github.com/teran/archived/repositories/blob/mock" @@ -60,6 +61,11 @@ func (m *protoClientMock) DeleteContainer(ctx context.Context, in *v1proto.Delet return &v1proto.DeleteContainerResponse{}, args.Error(0) } +func (m *protoClientMock) SetContainerParameters(ctx context.Context, in *v1proto.SetContainerParametersRequest, opts ...grpc.CallOption) (*v1proto.SetContainerParametersResponse, error) { + args := m.Called(in.GetNamespace(), in.GetName(), time.Duration(in.GetTtlSeconds())*time.Second) + return &v1proto.SetContainerParametersResponse{}, args.Error(0) +} + func (m *protoClientMock) ListContainers(ctx context.Context, in *v1proto.ListContainersRequest, opts ...grpc.CallOption) (*v1proto.ListContainersResponse, error) { args := m.Called(in.GetNamespace()) return &v1proto.ListContainersResponse{ diff --git a/cli/service/service.go b/cli/service/service.go index 7c17b47..860b08a 100644 --- a/cli/service/service.go +++ b/cli/service/service.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "time" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -12,6 +13,7 @@ import ( "github.com/teran/archived/cli/service/source" cache "github.com/teran/archived/cli/service/stat_cache" v1proto "github.com/teran/archived/manager/presenter/grpc/proto/v1" + ptr "github.com/teran/go-ptr" ) type Service interface { @@ -20,11 +22,12 @@ type Service interface { ListNamespaces() func(ctx context.Context) error DeleteNamespace(namespaceName string) func(ctx context.Context) error - CreateContainer(namespaceName, containerName string) func(ctx context.Context) error + CreateContainer(namespaceName, containerName string, ttl time.Duration) func(ctx context.Context) error MoveContainer(namespaceName, containerName, destinationNamespace string) func(ctx context.Context) error RenameContainer(namespaceName, oldName, newName string) func(ctx context.Context) error ListContainers(namespaceName string) func(ctx context.Context) error DeleteContainer(namespaceName, containerName string) func(ctx context.Context) error + SetContainerParameters(namespaceName, containerName string, ttl time.Duration) func(ctx context.Context) error CreateVersion(namespaceName, containerName string, shouldPublish bool, src source.Source) func(ctx context.Context) error DeleteVersion(namespaceName, containerName, versionID string) func(ctx context.Context) error @@ -103,11 +106,12 @@ func (s *service) DeleteNamespace(namespaceName string) func(ctx context.Context } } -func (s *service) CreateContainer(namespaceName, containerName string) func(ctx context.Context) error { +func (s *service) CreateContainer(namespaceName, containerName string, ttl time.Duration) func(ctx context.Context) error { return func(ctx context.Context) error { _, err := s.cli.CreateContainer(ctx, &v1proto.CreateContainerRequest{ - Namespace: namespaceName, - Name: containerName, + Namespace: namespaceName, + Name: containerName, + TtlSeconds: ptr.Int64(int64(ttl.Seconds())), }) if err != nil { return errors.Wrap(err, "error creating container") @@ -178,6 +182,22 @@ func (s *service) DeleteContainer(namespaceName, containerName string) func(ctx } } +func (s *service) SetContainerParameters(namespaceName, containerName string, ttl time.Duration) func(ctx context.Context) error { + return func(ctx context.Context) error { + _, err := s.cli.SetContainerParameters(ctx, &v1proto.SetContainerParametersRequest{ + Namespace: namespaceName, + Name: containerName, + TtlSeconds: ptr.Int64(int64(ttl.Seconds())), + }) + if err != nil { + return errors.Wrap(err, "error setting container versions TTL") + } + + fmt.Printf("container `%s` versions TTL set to %s\n", containerName, ttl) + return nil + } +} + func (s *service) CreateVersion(namespaceName, containerName string, shouldPublish bool, src source.Source) func(ctx context.Context) error { return func(ctx context.Context) error { log.Tracef("creating version ...") diff --git a/cli/service/service_test.go b/cli/service/service_test.go index 54ef59b..d642bd6 100644 --- a/cli/service/service_test.go +++ b/cli/service/service_test.go @@ -3,6 +3,7 @@ package service import ( "context" "testing" + "time" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/suite" @@ -50,7 +51,7 @@ func (s *serviceTestSuite) TestDeleteNamespace() { func (s *serviceTestSuite) TestCreateContainer() { s.cliMock.On("CreateContainer", defaultNamespace, "test-container").Return(nil).Once() - fn := s.svc.CreateContainer(defaultNamespace, "test-container") + fn := s.svc.CreateContainer(defaultNamespace, "test-container", -1) s.Require().NoError(fn(s.ctx)) } @@ -82,6 +83,13 @@ func (s *serviceTestSuite) TestDeleteContainer() { s.Require().NoError(fn(s.ctx)) } +func (s *serviceTestSuite) TestSetContainerParameters() { + s.cliMock.On("SetContainerParameters", defaultNamespace, "test-container1", 3600*time.Second).Return(nil).Once() + + fn := s.svc.SetContainerParameters(defaultNamespace, "test-container1", 3600*time.Second) + s.Require().NoError(fn(s.ctx)) +} + func (s *serviceTestSuite) TestCreateVersion() { s.cliMock.On("CreateVersion", defaultNamespace, "container1").Return("version_id", nil).Once() diff --git a/cmd/cli/main.go b/cmd/cli/main.go index d2379ba..c1f429b 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -83,6 +83,7 @@ var ( container = app.Command("container", "container operations") containerCreate = container.Command("create", "create new container") containerCreateName = containerCreate.Arg("name", "name of the container to create").Required().String() + containerCreateTTL = containerCreate.Flag("ttl", "Default container TTL").Default("-1ns").Duration() containerMove = container.Command("move", "move container to another namespace") containerMoveName = containerMove.Arg("name", "container namespace to move").Required().String() @@ -95,6 +96,10 @@ var ( containerDelete = container.Command("delete", "delete the given container") containerDeleteName = containerDelete.Arg("name", "name of the container to delete").Required().String() + containerSet = container.Command("set", "set parameters for container") + containerSetContainer = containerSet.Arg("name", "name of the container").Required().String() + containerSetTTL = containerSet.Flag("ttl", "Container TTL").Default("-1ns").Duration() + containerList = container.Command("list", "list containers") version = app.Command("version", "version operations") @@ -233,11 +238,12 @@ func main() { r.Register(namespaceList.FullCommand(), cliSvc.ListNamespaces()) r.Register(namespaceDelete.FullCommand(), cliSvc.DeleteNamespace(*namespaceDeleteName)) - r.Register(containerCreate.FullCommand(), cliSvc.CreateContainer(*namespaceName, *containerCreateName)) + r.Register(containerCreate.FullCommand(), cliSvc.CreateContainer(*namespaceName, *containerCreateName, *containerCreateTTL)) r.Register(containerMove.FullCommand(), cliSvc.MoveContainer(*namespaceName, *containerMoveName, *containerMoveNamespace)) r.Register(containerRename.FullCommand(), cliSvc.RenameContainer(*namespaceName, *containerRenameOldName, *containerRenameNewName)) r.Register(containerList.FullCommand(), cliSvc.ListContainers(*namespaceName)) r.Register(containerDelete.FullCommand(), cliSvc.DeleteContainer(*namespaceName, *containerDeleteName)) + r.Register(containerSet.FullCommand(), cliSvc.SetContainerParameters(*namespaceName, *containerSetContainer, *containerSetTTL)) r.Register(versionList.FullCommand(), cliSvc.ListVersions(*namespaceName, *versionListContainer)) r.Register(versionCreate.FullCommand(), cliSvc.CreateVersion( @@ -249,7 +255,6 @@ func main() { r.Register(objectList.FullCommand(), cliSvc.ListObjects(*namespaceName, *objectListContainer, *objectListVersion)) r.Register(objectURL.FullCommand(), cliSvc.GetObjectURL(*namespaceName, *objectURLContainer, *objectURLVersion, *objectURLKey)) r.Register(deleteObject.FullCommand(), cliSvc.DeleteObject(*namespaceName, *deleteObjectContainer, *deleteObjectVersion, *deleteObjectKey)) - r.Register(statCacheShowPath.FullCommand(), func(ctx context.Context) error { fmt.Println(*cacheDir) return nil diff --git a/cmd/seeder/main.go b/cmd/seeder/main.go index 0f6d336..cd7f5ba 100644 --- a/cmd/seeder/main.go +++ b/cmd/seeder/main.go @@ -110,7 +110,7 @@ func main() { } for j := 0; j <= cfg.CreateContainersPerNamespace; j++ { container := fmt.Sprintf("container-%06d", j) - if err := managerSvc.CreateContainer(ctx, namespace, container); err != nil { + if err := managerSvc.CreateContainer(ctx, namespace, container, -1); err != nil { panic(err) } for k := 0; k <= cfg.CreateVersionsPerContainer; k++ { diff --git a/gc/service/service.go b/gc/service/service.go index 35bbb87..a0c58b9 100644 --- a/gc/service/service.go +++ b/gc/service/service.go @@ -2,9 +2,11 @@ package service import ( "context" + "time" "github.com/pkg/errors" log "github.com/sirupsen/logrus" + "github.com/teran/archived/models" ) const defaultLimit uint64 = 1000 @@ -32,12 +34,19 @@ func New(cfg *Config) (Service, error) { func (s *service) Run(ctx context.Context) error { log.Info("running garbage collection ...") + log.Trace("listing namespaces ...") namespaces, err := s.cfg.MdRepo.ListNamespaces(ctx) if err != nil { return errors.Wrap(err, "error listing namespaces") } + log.Infof("found %d namespaces", len(namespaces)) + for _, namespace := range namespaces { + log.WithFields(log.Fields{ + "namespace": namespace, + }).Debug("processing namespace ...") + containers, err := s.cfg.MdRepo.ListContainers(ctx, namespace) if err != nil { return errors.Wrap(err, "error listing containers") @@ -48,91 +57,187 @@ func (s *service) Run(ctx context.Context) error { now := s.cfg.TimeNowFunc().UTC() for _, container := range containers { + log.WithFields(log.Fields{ + "namespace": namespace, + "container": container.Name, + "ttl_seconds": int64(container.VersionsTTL.Seconds()), + }).Debug("processing container ...") + + if container.VersionsTTL < 0 { + log.Debug("container TTL is set to infinite (< 0): handling unpublished versions ...") + + err = s.deleteExpiredUnpublishedVersions(ctx, now, namespace, container) + if err != nil { + return errors.Wrapf(err, "error deleting expired unpublished versions for container `%s/%s`", namespace, container.Name) + } + } else { + log.Debug("container TTL is set to positive value: handling all versions ...") + + err = s.deleteExpiredVersions(ctx, now, namespace, container) + if err != nil { + return errors.Wrapf(err, "error deleting expired versions for container `%s/%s`", namespace, container.Name) + } + } + } + } + return nil +} + +func (s *service) deleteExpiredVersions(ctx context.Context, now time.Time, namespace string, container models.Container) error { + log.WithFields(log.Fields{ + "namespace": namespace, + "container": container, + }).Debug("listing expired versions ...") + + versions, err := s.cfg.MdRepo.ListAllVersionsByContainer(ctx, namespace, container.Name) + if err != nil { + return errors.Wrapf(err, "error listing versions for container `%s/%s`", namespace, container) + } + + for _, version := range versions { + log.WithFields(log.Fields{ + "namespace": namespace, + "container": container, + "version": version.Name, + "is_published": version.IsPublished, + "created_at": version.CreatedAt.Format(time.RFC3339), + }).Trace("handling version ...") + + if version.CreatedAt.After(now.Add(-1 * container.VersionsTTL)) { + log.WithFields(log.Fields{ + "namespace": namespace, + "container": container, + "version": version.Name, + "is_published": version.IsPublished, + "created_at": version.CreatedAt.Format(time.RFC3339), + }).Trace("version is within TTL. Skipping ...") + continue + } + + log.WithFields(log.Fields{ + "namespace": namespace, + "container": container, + "version": version.Name, + "is_published": version.IsPublished, + "created_at": version.CreatedAt.Format(time.RFC3339), + }).Debug("version is older then TTL. Deleting ...") + + err = s.deleteVersion(ctx, namespace, container, version) + if err != nil { + return errors.Wrapf(err, "error deleting version `%s` for container `%s/%s`", version.Name, namespace, container.Name) + } + } + + return nil +} + +func (s *service) deleteExpiredUnpublishedVersions(ctx context.Context, now time.Time, namespace string, container models.Container) error { + log.WithFields(log.Fields{ + "namespace": namespace, + "container": container, + }).Debug("listing expired unpublished versions ...") + + versions, err := s.cfg.MdRepo.ListUnpublishedVersionsByContainer(ctx, namespace, container.Name) + if err != nil { + return errors.Wrapf(err, "error listing versions for container `%s/%s`", namespace, container) + } + + for _, version := range versions { + log.WithFields(log.Fields{ + "namespace": namespace, + "container": container, + "version": version.Name, + "is_published": version.IsPublished, + "created_at": version.CreatedAt.Format(time.RFC3339), + }).Trace("handling version ...") + + if version.CreatedAt.After(now.Add(-1 * s.cfg.UnpublishedVersionMaxAge)) { + log.WithFields(log.Fields{ + "namespace": namespace, + "container": container, + "version": version.Name, + "is_published": version.IsPublished, + "created_at": version.CreatedAt.Format(time.RFC3339), + }).Trace("unpublished version is within TTL. Skipping ...") + continue + } + + log.WithFields(log.Fields{ + "namespace": namespace, + "container": container, + "version": version.Name, + "is_published": version.IsPublished, + "created_at": version.CreatedAt.Format(time.RFC3339), + }).Debug("unpublished version is older then TTL. Deleting ...") + + err = s.deleteVersion(ctx, namespace, container, version) + if err != nil { + return errors.Wrapf(err, "error deleting version `%s` for container `%s/%s`", version.Name, namespace, container.Name) + } + } + + return nil +} + +func (s *service) deleteVersion(ctx context.Context, namespace string, container models.Container, version models.Version) error { + var ( + total uint64 + offset uint64 + err error + ) + + for { + log.WithFields(log.Fields{ + "namespace": namespace, + "container": container, + "version": version, + "offset": offset, + "limit": defaultLimit, + }).Tracef("list objects loop iteration ...") + + var objects []string + total, objects, err = s.cfg.MdRepo.ListObjects(ctx, namespace, container.Name, version.Name, offset, defaultLimit) + if err != nil { + return errors.Wrapf(err, "error listing objects for container `%s/%s`; version `%s`", namespace, container, version.Name) + } + + if total == 0 { + break + } + + if !s.cfg.DryRun { log.WithFields(log.Fields{ "namespace": namespace, "container": container, - }).Debugf("listing unpublished versions ...") + "version": version.Name, + "amount": len(objects), + }).Info("Performing actual metadata deletion: objects") - versions, err := s.cfg.MdRepo.ListUnpublishedVersionsByContainer(ctx, namespace, container.Name) + err = s.cfg.MdRepo.DeleteObject(ctx, namespace, container.Name, version.Name, objects...) if err != nil { - return errors.Wrapf(err, "error listing versions for container `%s/%s`", namespace, container) + return errors.Wrapf(err, "error removing object from `%s/%s/%s (%d objects)`", namespace, container, version.Name, len(objects)) } + } + } - for _, version := range versions { - log.WithFields(log.Fields{ - "namespace": namespace, - "container": container, - "version": version.Name, - }).Debugf("listing objects ...") - - if version.CreatedAt.After(now.Add(-1 * s.cfg.UnpublishedVersionMaxAge)) { - log.WithFields(log.Fields{ - "namespace": namespace, - "container": container, - "version": version.Name, - }).Debug("version is newer max version age. Skipping ...") - continue - } + log.WithFields(log.Fields{ + "namespace": namespace, + "container": container, + "version": version.Name, + }).Debug("deleting version ...") - var ( - total uint64 - offset uint64 - ) - - for { - log.WithFields(log.Fields{ - "namespace": namespace, - "container": container, - "version": version, - "offset": offset, - "limit": defaultLimit, - }).Tracef("list objects loop iteration ...") - - var objects []string - total, objects, err = s.cfg.MdRepo.ListObjects(ctx, namespace, container.Name, version.Name, offset, defaultLimit) - if err != nil { - return errors.Wrapf(err, "error listing objects for container `%s/%s`; version `%s`", namespace, container, version.Name) - } - - if total == 0 { - break - } - - if !s.cfg.DryRun { - log.WithFields(log.Fields{ - "namespace": namespace, - "container": container, - "version": version.Name, - "amount": len(objects), - }).Info("Performing actual metadata deletion: objects") - - err = s.cfg.MdRepo.DeleteObject(ctx, namespace, container.Name, version.Name, objects...) - if err != nil { - return errors.Wrapf(err, "error removing object from `%s/%s/%s (%d objects)`", namespace, container, version.Name, len(objects)) - } - } - } + if !s.cfg.DryRun { + log.WithFields(log.Fields{ + "namespace": namespace, + "container": container, + "version": version.Name, + }).Info("Performing actual metadata deletion: version") - log.WithFields(log.Fields{ - "namespace": namespace, - "container": container, - "version": version.Name, - }).Debug("deleting version ...") - - if !s.cfg.DryRun { - log.WithFields(log.Fields{ - "namespace": namespace, - "container": container, - "version": version.Name, - }).Info("Performing actual metadata deletion: version") - - err = s.cfg.MdRepo.DeleteVersion(ctx, namespace, container.Name, version.Name) - if err != nil { - return err - } - } - } + err = s.cfg.MdRepo.DeleteVersion(ctx, namespace, container.Name, version.Name) + if err != nil { + return err } } + return nil } diff --git a/gc/service/service_test.go b/gc/service/service_test.go index 9eda564..00ed1a6 100644 --- a/gc/service/service_test.go +++ b/gc/service/service_test.go @@ -19,12 +19,12 @@ func init() { log.SetLevel(log.TraceLevel) } -func (s *serviceTestSuite) TestAll() { +func (s *serviceTestSuite) TestDeleteUnpublishedExpiredVersions() { s.tp.On("Now").Return("2024-08-01T10:11:12Z").Once() call0 := s.repoMock.On("ListNamespaces").Return([]string{defaultNamespace}, nil).Once() - call1 := s.repoMock.On("ListContainers", defaultNamespace).Return([]models.Container{{Name: "container1"}}, nil).Once().NotBefore(call0) + call1 := s.repoMock.On("ListContainers", defaultNamespace).Return([]models.Container{{Name: "container1", VersionsTTL: -1 * time.Second}}, nil).Once().NotBefore(call0) call2 := s.repoMock.On("ListUnpublishedVersionsByContainer", defaultNamespace, "container1").Return([]models.Version{ { @@ -45,6 +45,32 @@ func (s *serviceTestSuite) TestAll() { s.Require().NoError(err) } +func (s *serviceTestSuite) TestDeleteExpiredVersions() { + s.tp.On("Now").Return("2024-08-01T10:11:12Z").Once() + + call0 := s.repoMock.On("ListNamespaces").Return([]string{defaultNamespace}, nil).Once() + + call1 := s.repoMock.On("ListContainers", defaultNamespace).Return([]models.Container{{Name: "container1", VersionsTTL: 1 * time.Hour}}, nil).Once().NotBefore(call0) + + call2 := s.repoMock.On("ListAllVersionsByContainer", defaultNamespace, "container1").Return([]models.Version{ + { + Name: "version1", + CreatedAt: time.Date(2024, 7, 31, 10, 1, 1, 0, time.UTC), + }, + { + Name: "version2", + CreatedAt: time.Date(2024, 8, 1, 10, 1, 1, 0, time.UTC), + }, + }, nil).Once().NotBefore(call1) + call3 := s.repoMock.On("ListObjects", defaultNamespace, "container1", "version1", uint64(0), uint64(1000)).Return(uint64(3), []string{"obj1", "obj2", "obj3"}, nil).Once().NotBefore(call2) + call4 := s.repoMock.On("DeleteObject", defaultNamespace, "container1", "version1", []string{"obj1", "obj2", "obj3"}).Return(nil).Once().NotBefore(call3) + call5 := s.repoMock.On("ListObjects", defaultNamespace, "container1", "version1", uint64(0), uint64(1000)).Return(uint64(0), []string{}, nil).Once().NotBefore(call4) + _ = s.repoMock.On("DeleteVersion", defaultNamespace, "container1", "version1").Return(nil).Once().NotBefore(call5) + + err := s.svc.Run(s.ctx) + s.Require().NoError(err) +} + // Definitions ... type serviceTestSuite struct { suite.Suite diff --git a/manager/presenter/grpc/handlers.go b/manager/presenter/grpc/handlers.go index 451a580..8d77896 100644 --- a/manager/presenter/grpc/handlers.go +++ b/manager/presenter/grpc/handlers.go @@ -2,15 +2,17 @@ package grpc import ( "context" + "time" "github.com/pkg/errors" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + ptr "github.com/teran/go-ptr" + v1 "github.com/teran/archived/manager/presenter/grpc/proto/v1" "github.com/teran/archived/service" - ptr "github.com/teran/go-ptr" ) var _ v1.ManageServiceServer = (*handlers)(nil) @@ -72,7 +74,7 @@ func (h *handlers) ListNamespaces(ctx context.Context, in *v1.ListNamespacesRequ } func (h *handlers) CreateContainer(ctx context.Context, in *v1.CreateContainerRequest) (*v1.CreateContainerResponse, error) { - err := h.svc.CreateContainer(ctx, in.GetNamespace(), in.GetName()) + err := h.svc.CreateContainer(ctx, in.GetNamespace(), in.GetName(), time.Duration(in.GetTtlSeconds())*time.Second) if err != nil { return nil, mapServiceError(err) } @@ -107,6 +109,15 @@ func (h *handlers) DeleteContainer(ctx context.Context, in *v1.DeleteContainerRe return &v1.DeleteContainerResponse{}, nil } +func (h *handlers) SetContainerParameters(ctx context.Context, in *v1.SetContainerParametersRequest) (*v1.SetContainerParametersResponse, error) { + err := h.svc.SetContainerParameters(ctx, in.GetNamespace(), in.GetName(), time.Duration(in.GetTtlSeconds())*time.Second) + if err != nil { + return nil, mapServiceError(err) + } + + return &v1.SetContainerParametersResponse{}, nil +} + func (h *handlers) ListContainers(ctx context.Context, in *v1.ListContainersRequest) (*v1.ListContainersResponse, error) { containers, err := h.svc.ListContainers(ctx, in.GetNamespace()) if err != nil { diff --git a/manager/presenter/grpc/handlers_test.go b/manager/presenter/grpc/handlers_test.go index da0bdc7..4bce227 100644 --- a/manager/presenter/grpc/handlers_test.go +++ b/manager/presenter/grpc/handlers_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/suite" grpctest "github.com/teran/go-grpctest" + ptr "github.com/teran/go-ptr" v1pb "github.com/teran/archived/manager/presenter/grpc/proto/v1" "github.com/teran/archived/models" @@ -54,11 +55,12 @@ func (s *manageHandlersTestSuite) TestDeleteNamespace() { } func (s *manageHandlersTestSuite) TestCreateContainer() { - s.svcMock.On("CreateContainer", defaultNamespace, "test-container").Return(nil).Once() + s.svcMock.On("CreateContainer", defaultNamespace, "test-container", 364*time.Second).Return(nil).Once() _, err := s.client.CreateContainer(s.ctx, &v1pb.CreateContainerRequest{ - Namespace: defaultNamespace, - Name: "test-container", + Namespace: defaultNamespace, + Name: "test-container", + TtlSeconds: ptr.Int64(364), }) s.Require().NoError(err) } @@ -75,11 +77,12 @@ func (s *manageHandlersTestSuite) TestMoveContainer() { } func (s *manageHandlersTestSuite) TestCreateContainerNotFound() { - s.svcMock.On("CreateContainer", defaultNamespace, "test-container").Return(service.ErrNotFound).Once() + s.svcMock.On("CreateContainer", defaultNamespace, "test-container", 3*time.Second).Return(service.ErrNotFound).Once() _, err := s.client.CreateContainer(s.ctx, &v1pb.CreateContainerRequest{ - Namespace: defaultNamespace, - Name: "test-container", + Namespace: defaultNamespace, + Name: "test-container", + TtlSeconds: ptr.Int64(3), }) s.Require().Error(err) s.Require().Equal("rpc error: code = NotFound desc = entity not found", err.Error()) @@ -108,6 +111,17 @@ func (s *manageHandlersTestSuite) TestRenameContainerNotFound() { s.Require().Equal("rpc error: code = NotFound desc = entity not found", err.Error()) } +func (s *manageHandlersTestSuite) TestSetContainerParameters() { + s.svcMock.On("SetContainerParameters", defaultNamespace, "test-container", 1*time.Hour).Return(nil).Once() + + _, err := s.client.SetContainerParameters(s.ctx, &v1pb.SetContainerParametersRequest{ + Namespace: defaultNamespace, + Name: "test-container", + TtlSeconds: ptr.Int64(3600), + }) + s.Require().NoError(err) +} + func (s *manageHandlersTestSuite) TestListContainers() { s.svcMock.On("ListContainers", defaultNamespace).Return([]models.Container{ {Name: "test-container1"}, diff --git a/manager/presenter/grpc/proto/v1/manager.proto b/manager/presenter/grpc/proto/v1/manager.proto index edd0949..dadfd3d 100644 --- a/manager/presenter/grpc/proto/v1/manager.proto +++ b/manager/presenter/grpc/proto/v1/manager.proto @@ -29,6 +29,7 @@ message ListNamespacesResponse { message CreateContainerRequest { string namespace = 1; string name = 2; + optional int64 ttl_seconds = 3; } message CreateContainerResponse {} @@ -53,6 +54,13 @@ message DeleteContainerRequest { } message DeleteContainerResponse {} +message SetContainerParametersRequest { + string namespace = 1; + string name = 2; + optional int64 ttl_seconds = 3; +} +message SetContainerParametersResponse {} + message ListContainersRequest { string namespace = 1; } @@ -148,6 +156,7 @@ service ManageService { rpc RenameContainer(RenameContainerRequest) returns (RenameContainerResponse); rpc DeleteContainer(DeleteContainerRequest) returns (DeleteContainerResponse); rpc ListContainers(ListContainersRequest) returns (ListContainersResponse); + rpc SetContainerParameters(SetContainerParametersRequest) returns (SetContainerParametersResponse); rpc CreateVersion(CreateVersionRequest) returns (CreateVersionResponse); rpc ListVersions(ListVersionsRequest) returns (ListVersionsResponse); diff --git a/repositories/cache/metadata/memcache/memcache.go b/repositories/cache/metadata/memcache/memcache.go index f77143f..3fbb698 100644 --- a/repositories/cache/metadata/memcache/memcache.go +++ b/repositories/cache/metadata/memcache/memcache.go @@ -90,14 +90,18 @@ func (m *memcache) DeleteNamespace(ctx context.Context, name string) error { return m.repo.DeleteNamespace(ctx, name) } -func (m *memcache) CreateContainer(ctx context.Context, namespace, name string) error { - return m.repo.CreateContainer(ctx, namespace, name) +func (m *memcache) CreateContainer(ctx context.Context, namespace, name string, ttl time.Duration) error { + return m.repo.CreateContainer(ctx, namespace, name, ttl) } func (m *memcache) RenameContainer(ctx context.Context, namespace, oldName, newNamespace, newName string) error { return m.repo.RenameContainer(ctx, namespace, oldName, newNamespace, newName) } +func (m *memcache) SetContainerParameters(ctx context.Context, namespace, name string, ttl time.Duration) error { + return m.repo.SetContainerParameters(ctx, namespace, name, ttl) +} + func (m *memcache) ListContainers(ctx context.Context, namespace string) ([]models.Container, error) { cacheKey := strings.Join([]string{ m.keyPrefix, diff --git a/repositories/cache/metadata/memcache/memcache_test.go b/repositories/cache/metadata/memcache/memcache_test.go index 1e932f5..47513a4 100644 --- a/repositories/cache/metadata/memcache/memcache_test.go +++ b/repositories/cache/metadata/memcache/memcache_test.go @@ -250,12 +250,12 @@ func (s *memcacheTestSuite) TestDeleteNamespace() { } func (s *memcacheTestSuite) TestCreateContainer() { - s.repoMock.On("CreateContainer", defaultNamespace, "container1").Return(nil).Twice() + s.repoMock.On("CreateContainer", defaultNamespace, "container1", time.Duration(-1)).Return(nil).Twice() - err := s.cache.CreateContainer(s.ctx, defaultNamespace, "container1") + err := s.cache.CreateContainer(s.ctx, defaultNamespace, "container1", -1) s.Require().NoError(err) - err = s.cache.CreateContainer(s.ctx, defaultNamespace, "container1") + err = s.cache.CreateContainer(s.ctx, defaultNamespace, "container1", -1) s.Require().NoError(err) } @@ -279,6 +279,16 @@ func (s *memcacheTestSuite) TestDeleteContainer() { s.Require().NoError(err) } +func (s *memcacheTestSuite) TestSetContainerVersionsParameters() { + s.repoMock.On("SetContainerParameters", defaultNamespace, "container1", 1*time.Hour).Return(nil).Twice() + + err := s.cache.SetContainerParameters(s.ctx, defaultNamespace, "container1", 1*time.Hour) + s.Require().NoError(err) + + err = s.cache.SetContainerParameters(s.ctx, defaultNamespace, "container1", 1*time.Hour) + s.Require().NoError(err) +} + func (s *memcacheTestSuite) TestCreateVersion() { s.repoMock.On("CreateVersion", defaultNamespace, "container1").Return("test-version", nil).Twice() diff --git a/repositories/metadata/metadata.go b/repositories/metadata/metadata.go index 16b1f2b..384b5b9 100644 --- a/repositories/metadata/metadata.go +++ b/repositories/metadata/metadata.go @@ -2,6 +2,7 @@ package metadata import ( "context" + "time" "github.com/pkg/errors" @@ -20,8 +21,9 @@ type Repository interface { ListNamespaces(ctx context.Context) ([]string, error) DeleteNamespace(ctx context.Context, name string) error - CreateContainer(ctx context.Context, namespace, name string) error + CreateContainer(ctx context.Context, namespace, name string, ttl time.Duration) error RenameContainer(ctx context.Context, namespace, oldName, newNamespace, newName string) error + SetContainerParameters(ctx context.Context, namespace, name string, ttl time.Duration) error ListContainers(ctx context.Context, namespace string) ([]models.Container, error) ListContainersByPage(ctx context.Context, namespace string, offset, limit uint64) (uint64, []models.Container, error) DeleteContainer(ctx context.Context, namespace, name string) error diff --git a/repositories/metadata/mock/mock.go b/repositories/metadata/mock/mock.go index 2b0200b..39c8cad 100644 --- a/repositories/metadata/mock/mock.go +++ b/repositories/metadata/mock/mock.go @@ -2,8 +2,10 @@ package mock import ( "context" + "time" "github.com/stretchr/testify/mock" + emodels "github.com/teran/archived/exporter/models" "github.com/teran/archived/models" "github.com/teran/archived/repositories/metadata" @@ -39,8 +41,8 @@ func (m *Mock) DeleteNamespace(ctx context.Context, name string) error { return args.Error(0) } -func (m *Mock) CreateContainer(_ context.Context, namespace, name string) error { - args := m.Called(namespace, name) +func (m *Mock) CreateContainer(_ context.Context, namespace, name string, ttl time.Duration) error { + args := m.Called(namespace, name, ttl) return args.Error(0) } @@ -49,6 +51,11 @@ func (m *Mock) RenameContainer(_ context.Context, namespace, oldName, newNamespa return args.Error(0) } +func (m *Mock) SetContainerParameters(_ context.Context, namespace, name string, ttl time.Duration) error { + args := m.Called(namespace, name, ttl) + return args.Error(0) +} + func (m *Mock) ListContainers(_ context.Context, namespace string) ([]models.Container, error) { args := m.Called(namespace) return args.Get(0).([]models.Container), args.Error(1) diff --git a/repositories/metadata/postgresql/blobs_test.go b/repositories/metadata/postgresql/blobs_test.go index 99ffedc..4088355 100644 --- a/repositories/metadata/postgresql/blobs_test.go +++ b/repositories/metadata/postgresql/blobs_test.go @@ -13,7 +13,7 @@ func (s *postgreSQLRepositoryTestSuite) TestBlobs() { s.tp.On("Now").Return("2024-01-02T01:02:03Z").Times(6) - err := s.repo.CreateContainer(s.ctx, defaultNamespace, containerName) + err := s.repo.CreateContainer(s.ctx, defaultNamespace, containerName, -1) s.Require().NoError(err) _, err = s.repo.CreateVersion(s.ctx, defaultNamespace, "not-existent") @@ -54,7 +54,7 @@ func (s *postgreSQLRepositoryTestSuite) TestGetBlobKeyByObjectErrors() { s.Require().Equal(metadata.ErrNotFound, err) // version & key doesn't exist - err = s.repo.CreateContainer(s.ctx, defaultNamespace, "container") + err = s.repo.CreateContainer(s.ctx, defaultNamespace, "container", -1) s.Require().NoError(err) _, err = s.repo.GetBlobKeyByObject(s.ctx, defaultNamespace, "container", "version", "key") @@ -89,7 +89,7 @@ func (s *postgreSQLRepositoryTestSuite) TestGetBlobByObjectErrors() { s.Require().Equal(metadata.ErrNotFound, err) // version & key doesn't exist - err = s.repo.CreateContainer(s.ctx, defaultNamespace, "container") + err = s.repo.CreateContainer(s.ctx, defaultNamespace, "container", -1) s.Require().NoError(err) _, err = s.repo.GetBlobByObject(s.ctx, defaultNamespace, "container", "version", "key") diff --git a/repositories/metadata/postgresql/containers.go b/repositories/metadata/postgresql/containers.go index 5b54833..8585534 100644 --- a/repositories/metadata/postgresql/containers.go +++ b/repositories/metadata/postgresql/containers.go @@ -10,7 +10,7 @@ import ( "github.com/teran/archived/models" ) -func (r *repository) CreateContainer(ctx context.Context, namespace, name string) error { +func (r *repository) CreateContainer(ctx context.Context, namespace, name string, ttl time.Duration) error { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return mapSQLErrors(err) @@ -42,11 +42,13 @@ func (r *repository) CreateContainer(ctx context.Context, namespace, name string Columns( "name", "namespace_id", + "version_ttl_seconds", "created_at", ). Values( name, namespaceID, + int64(ttl.Seconds()), r.tp().UTC(), )) if err != nil { @@ -133,6 +135,66 @@ func (r *repository) RenameContainer(ctx context.Context, namespace, oldName, ne return nil } +func (r *repository) SetContainerParameters(ctx context.Context, namespace, name string, ttl time.Duration) error { + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return mapSQLErrors(err) + } + defer func() { + err := tx.Rollback() + if err != nil { + log.WithFields(log.Fields{ + "error": err, + }).Error("error rolling back") + } + }() + + row, err := selectQueryRow(ctx, tx, psql. + Select("id"). + From("namespaces"). + Where(sq.Eq{"name": namespace})) + if err != nil { + return mapSQLErrors(err) + } + + var namespaceID uint + if err := row.Scan(&namespaceID); err != nil { + return mapSQLErrors(err) + } + + row, err = selectQueryRow(ctx, tx, psql. + Select("id"). + From("containers"). + Where(sq.Eq{ + "name": name, + "namespace_id": namespaceID, + })) + if err != nil { + return mapSQLErrors(err) + } + + var containerID uint + if err := row.Scan(&containerID); err != nil { + return mapSQLErrors(err) + } + + _, err = updateQuery(ctx, tx, psql. + Update("containers"). + Set("version_ttl_seconds", ttl.Seconds()). + Where(sq.Eq{ + "name": name, + "namespace_id": namespaceID, + })) + if err != nil { + return mapSQLErrors(err) + } + + if err := tx.Commit(); err != nil { + return mapSQLErrors(err) + } + return nil +} + func (r *repository) ListContainers(ctx context.Context, namespace string) ([]models.Container, error) { row, err := selectQueryRow(ctx, r.db, psql. Select("id"). @@ -164,10 +226,19 @@ func (r *repository) ListContainers(ctx context.Context, namespace string) ([]mo var ( r models.Container createdAt time.Time + ttl int64 ) - if err := rows.Scan(&r.Name, &createdAt, &r.VersionsTTL); err != nil { + + if err := rows.Scan(&r.Name, &createdAt, &ttl); err != nil { return nil, mapSQLErrors(err) } + + if ttl >= 0 { + r.VersionsTTL = time.Duration(ttl * int64(time.Second)) + } else { + r.VersionsTTL = -1 + } + r.CreatedAt = time.Date( createdAt.Year(), createdAt.Month(), createdAt.Day(), createdAt.Hour(), createdAt.Minute(), createdAt.Second(), createdAt.Nanosecond(), @@ -210,7 +281,11 @@ func (r *repository) ListContainersByPage(ctx context.Context, namespace string, } rows, err := selectQuery(ctx, r.db, psql. - Select("name", "created_at", "version_ttl_seconds"). + Select( + "name", + "version_ttl_seconds", + "created_at", + ). From("containers"). Where(sq.Eq{ "namespace_id": namespaceID, @@ -228,10 +303,18 @@ func (r *repository) ListContainersByPage(ctx context.Context, namespace string, var ( r models.Container createdAt time.Time + ttl int64 ) - if err := rows.Scan(&r.Name, &createdAt, &r.VersionsTTL); err != nil { + if err := rows.Scan(&r.Name, &ttl, &createdAt); err != nil { return 0, nil, mapSQLErrors(err) } + + if ttl >= 0 { + r.VersionsTTL = time.Duration(ttl * int64(time.Second)) + } else { + r.VersionsTTL = -1 + } + r.CreatedAt = time.Date( createdAt.Year(), createdAt.Month(), createdAt.Day(), createdAt.Hour(), createdAt.Minute(), createdAt.Second(), createdAt.Nanosecond(), diff --git a/repositories/metadata/postgresql/containers_test.go b/repositories/metadata/postgresql/containers_test.go index 50a1849..0246b96 100644 --- a/repositories/metadata/postgresql/containers_test.go +++ b/repositories/metadata/postgresql/containers_test.go @@ -17,21 +17,27 @@ func (s *postgreSQLRepositoryTestSuite) TestContainerOperations() { s.Require().NoError(err) s.Require().Equal([]models.Container{}, list) - err = s.repo.CreateContainer(s.ctx, defaultNamespace, "test-container9") + err = s.repo.CreateContainer(s.ctx, defaultNamespace, "test-container9", 86400*time.Second) s.Require().NoError(err) - err = s.repo.CreateContainer(s.ctx, defaultNamespace, "test-container5") + err = s.repo.CreateContainer(s.ctx, defaultNamespace, "test-container5", -1) s.Require().NoError(err) - err = s.repo.CreateContainer(s.ctx, defaultNamespace, "test-container9") + err = s.repo.CreateContainer(s.ctx, defaultNamespace, "test-container9", 3*time.Hour) s.Require().Error(err) s.Require().Equal(metadata.ErrConflict, err) + err = s.repo.SetContainerParameters(s.ctx, defaultNamespace, "test-container5", 1*time.Hour) + s.Require().NoError(err) + + err = s.repo.SetContainerParameters(s.ctx, defaultNamespace, "test-container9", 2*time.Hour) + s.Require().NoError(err) + list, err = s.repo.ListContainers(s.ctx, defaultNamespace) s.Require().NoError(err) s.Require().Equal([]models.Container{ - {Name: "test-container5", CreatedAt: time.Date(2024, 1, 2, 1, 2, 3, 0, time.UTC), VersionsTTL: -1}, - {Name: "test-container9", CreatedAt: time.Date(2024, 1, 2, 1, 2, 3, 0, time.UTC), VersionsTTL: -1}, + {Name: "test-container5", CreatedAt: time.Date(2024, 1, 2, 1, 2, 3, 0, time.UTC), VersionsTTL: 3600 * time.Second}, + {Name: "test-container9", CreatedAt: time.Date(2024, 1, 2, 1, 2, 3, 0, time.UTC), VersionsTTL: 7200 * time.Second}, }, list) err = s.repo.DeleteContainer(s.ctx, defaultNamespace, "test-container9") @@ -43,7 +49,7 @@ func (s *postgreSQLRepositoryTestSuite) TestContainerOperations() { list, err = s.repo.ListContainers(s.ctx, defaultNamespace) s.Require().NoError(err) s.Require().Equal([]models.Container{ - {Name: "test-container5", CreatedAt: time.Date(2024, 1, 2, 1, 2, 3, 0, time.UTC), VersionsTTL: -1}, + {Name: "test-container5", CreatedAt: time.Date(2024, 1, 2, 1, 2, 3, 0, time.UTC), VersionsTTL: 3600 * time.Second}, }, list) err = s.repo.RenameContainer(s.ctx, defaultNamespace, "test-container5", "new-namespace", "and-then-there-was-the-one") @@ -60,7 +66,7 @@ func (s *postgreSQLRepositoryTestSuite) TestContainerOperations() { list, err = s.repo.ListContainers(s.ctx, "new-namespace") s.Require().NoError(err) s.Require().Equal([]models.Container{ - {Name: "and-then-there-was-the-one", CreatedAt: time.Date(2024, 1, 2, 1, 2, 3, 0, time.UTC), VersionsTTL: -1}, + {Name: "and-then-there-was-the-one", CreatedAt: time.Date(2024, 1, 2, 1, 2, 3, 0, time.UTC), VersionsTTL: 3600 * time.Second}, }, list) } @@ -75,20 +81,20 @@ func (s *postgreSQLRepositoryTestSuite) TestContainersPagination() { s.Require().Equal(uint64(0), total) s.Require().Equal([]models.Container{}, list) - err = s.repo.CreateContainer(s.ctx, defaultNamespace, "test-container1") + err = s.repo.CreateContainer(s.ctx, defaultNamespace, "test-container1", 1*time.Hour) s.Require().NoError(err) - err = s.repo.CreateContainer(s.ctx, defaultNamespace, "test-container2") + err = s.repo.CreateContainer(s.ctx, defaultNamespace, "test-container2", 2*time.Hour) s.Require().NoError(err) - err = s.repo.CreateContainer(s.ctx, defaultNamespace, "test-container3") + err = s.repo.CreateContainer(s.ctx, defaultNamespace, "test-container3", 3*time.Hour) s.Require().NoError(err) total, list, err = s.repo.ListContainersByPage(s.ctx, defaultNamespace, 0, 2) s.Require().NoError(err) s.Require().Equal(uint64(3), total) s.Require().Equal([]models.Container{ - {Name: "test-container1", CreatedAt: time.Date(2024, 1, 2, 1, 2, 3, 0, time.UTC), VersionsTTL: -1}, - {Name: "test-container2", CreatedAt: time.Date(2024, 1, 2, 1, 2, 3, 0, time.UTC), VersionsTTL: -1}, + {Name: "test-container1", CreatedAt: time.Date(2024, 1, 2, 1, 2, 3, 0, time.UTC), VersionsTTL: 1 * time.Hour}, + {Name: "test-container2", CreatedAt: time.Date(2024, 1, 2, 1, 2, 3, 0, time.UTC), VersionsTTL: 2 * time.Hour}, }, list) } diff --git a/repositories/metadata/postgresql/objects_test.go b/repositories/metadata/postgresql/objects_test.go index 3518b80..27b7044 100644 --- a/repositories/metadata/postgresql/objects_test.go +++ b/repositories/metadata/postgresql/objects_test.go @@ -9,7 +9,7 @@ func (s *postgreSQLRepositoryTestSuite) TestObjects() { s.tp.On("Now").Return("2024-07-07T10:11:13Z").Times(5) s.tp.On("Now").Return("2024-07-07T10:11:14Z").Times(5) - err := s.repo.CreateContainer(s.ctx, defaultNamespace, containerName) + err := s.repo.CreateContainer(s.ctx, defaultNamespace, containerName, -1) s.Require().NoError(err) versionID, err := s.repo.CreateVersion(s.ctx, defaultNamespace, containerName) @@ -64,7 +64,7 @@ func (s *postgreSQLRepositoryTestSuite) TestListObjectsErrors() { s.Require().Equal(metadata.ErrNotFound, err) // version doesn't exist - err = s.repo.CreateContainer(s.ctx, defaultNamespace, "container") + err = s.repo.CreateContainer(s.ctx, defaultNamespace, "container", -1) s.Require().NoError(err) _, _, err = s.repo.ListObjects(s.ctx, defaultNamespace, "container", "version", 0, 1000) @@ -81,7 +81,7 @@ func (s *postgreSQLRepositoryTestSuite) TestRemapObjectErrors() { // Remap with not existent version s.tp.On("Now").Return("2024-01-02T01:02:03Z").Once() - err = s.repo.CreateContainer(s.ctx, defaultNamespace, "container1") + err = s.repo.CreateContainer(s.ctx, defaultNamespace, "container1", -1) s.Require().NoError(err) err = s.repo.RemapObject(s.ctx, defaultNamespace, "not-existent", "version", "data/some-key.txt", "deadbeef") diff --git a/repositories/metadata/postgresql/stats_test.go b/repositories/metadata/postgresql/stats_test.go index d4024a2..70751cb 100644 --- a/repositories/metadata/postgresql/stats_test.go +++ b/repositories/metadata/postgresql/stats_test.go @@ -10,7 +10,7 @@ func (s *postgreSQLRepositoryTestSuite) TestCountStats() { s.tp.On("Now").Return("2024-07-07T10:11:14Z").Twice() // Create container - err := s.repo.CreateContainer(s.ctx, defaultNamespace, containerName) + err := s.repo.CreateContainer(s.ctx, defaultNamespace, containerName, -1) s.Require().NoError(err) // Create first version diff --git a/repositories/metadata/postgresql/versions_test.go b/repositories/metadata/postgresql/versions_test.go index 3a57b24..0918ce6 100644 --- a/repositories/metadata/postgresql/versions_test.go +++ b/repositories/metadata/postgresql/versions_test.go @@ -11,10 +11,10 @@ func (s *postgreSQLRepositoryTestSuite) TestVersionsOperations() { s.tp.On("Now").Return("2024-07-07T10:11:12Z").Times(4) s.tp.On("Now").Return("2024-07-07T11:12:13Z").Times(2) - err := s.repo.CreateContainer(s.ctx, defaultNamespace, "container1") + err := s.repo.CreateContainer(s.ctx, defaultNamespace, "container1", -1) s.Require().NoError(err) - err = s.repo.CreateContainer(s.ctx, defaultNamespace, "container2") + err = s.repo.CreateContainer(s.ctx, defaultNamespace, "container2", -1) s.Require().NoError(err) vName, err := s.repo.CreateVersion(s.ctx, defaultNamespace, "container1") @@ -47,7 +47,7 @@ func (s *postgreSQLRepositoryTestSuite) TestVersionsOperations() { func (s *postgreSQLRepositoryTestSuite) TestPublishVersion() { s.tp.On("Now").Return("2024-07-07T10:11:12Z").Times(3) - err := s.repo.CreateContainer(s.ctx, defaultNamespace, "container1") + err := s.repo.CreateContainer(s.ctx, defaultNamespace, "container1", -1) s.Require().NoError(err) version, err := s.repo.CreateVersion(s.ctx, defaultNamespace, "container1") @@ -99,7 +99,7 @@ func (s *postgreSQLRepositoryTestSuite) TestPublishVersionErrors() { // Not existent version s.tp.On("Now").Return("2024-07-07T10:11:12Z").Once() - err = s.repo.CreateContainer(s.ctx, defaultNamespace, "container1") + err = s.repo.CreateContainer(s.ctx, defaultNamespace, "container1", -1) s.Require().NoError(err) err = s.repo.MarkVersionPublished(s.ctx, defaultNamespace, "not-existent", "version") @@ -122,7 +122,7 @@ func (s *postgreSQLRepositoryTestSuite) TestListObjectsErrorsNotExistentContaine func (s *postgreSQLRepositoryTestSuite) TestListObjectsErrorsNotExistentVersion() { s.tp.On("Now").Return("2024-07-07T10:11:12Z").Once() - err := s.repo.CreateContainer(s.ctx, defaultNamespace, "test-container") + err := s.repo.CreateContainer(s.ctx, defaultNamespace, "test-container", -1) s.Require().NoError(err) _, _, err = s.repo.ListObjects(s.ctx, defaultNamespace, "test-container", "2024-01-02T03:04:05Z", 0, 100) @@ -135,7 +135,7 @@ func (s *postgreSQLRepositoryTestSuite) TestVersionsPagination() { s.tp.On("Now").Return("2024-07-07T10:11:13Z").Times(2) s.tp.On("Now").Return("2024-07-07T10:11:14Z").Times(2) - err := s.repo.CreateContainer(s.ctx, defaultNamespace, "container1") + err := s.repo.CreateContainer(s.ctx, defaultNamespace, "container1", -1) s.Require().NoError(err) version1, err := s.repo.CreateVersion(s.ctx, defaultNamespace, "container1") @@ -188,10 +188,10 @@ func (s *postgreSQLRepositoryTestSuite) TestDeleteVersion() { s.tp.On("Now").Return("2024-07-07T10:11:15Z").Times(2) s.tp.On("Now").Return("2024-07-07T10:11:16Z").Times(3) - err := s.repo.CreateContainer(s.ctx, defaultNamespace, "container1") + err := s.repo.CreateContainer(s.ctx, defaultNamespace, "container1", -1) s.Require().NoError(err) - err = s.repo.CreateContainer(s.ctx, defaultNamespace, "container2") + err = s.repo.CreateContainer(s.ctx, defaultNamespace, "container2", -1) s.Require().NoError(err) version1, err := s.repo.CreateVersion(s.ctx, defaultNamespace, "container1") @@ -271,10 +271,10 @@ func (s *postgreSQLRepositoryTestSuite) TestGetLatestPublishedVersionByContainer s.tp.On("Now").Return("2024-07-07T10:11:15Z").Times(2) s.tp.On("Now").Return("2024-07-07T10:11:16Z").Times(2) - err := s.repo.CreateContainer(s.ctx, defaultNamespace, "container1") + err := s.repo.CreateContainer(s.ctx, defaultNamespace, "container1", -1) s.Require().NoError(err) - err = s.repo.CreateContainer(s.ctx, defaultNamespace, "container2") + err = s.repo.CreateContainer(s.ctx, defaultNamespace, "container2", -1) s.Require().NoError(err) _, err = s.repo.CreateVersion(s.ctx, defaultNamespace, "container1") diff --git a/service/mock.go b/service/mock.go index 149e46d..4088b09 100644 --- a/service/mock.go +++ b/service/mock.go @@ -2,8 +2,10 @@ package service import ( "context" + "time" "github.com/stretchr/testify/mock" + "github.com/teran/archived/models" ) @@ -40,8 +42,8 @@ func (m *Mock) DeleteNamespace(ctx context.Context, name string) error { return args.Error(0) } -func (m *Mock) CreateContainer(_ context.Context, namespace, name string) error { - args := m.Called(namespace, name) +func (m *Mock) CreateContainer(_ context.Context, namespace, name string, ttl time.Duration) error { + args := m.Called(namespace, name, ttl) return args.Error(0) } @@ -55,6 +57,11 @@ func (m *Mock) RenameContainer(_ context.Context, namespace, oldName, newName st return args.Error(0) } +func (m *Mock) SetContainerParameters(_ context.Context, namespace, name string, ttl time.Duration) error { + args := m.Called(namespace, name, ttl) + return args.Error(0) +} + func (m *Mock) ListContainers(_ context.Context, namespace string) ([]models.Container, error) { args := m.Called(namespace) return args.Get(0).([]models.Container), args.Error(1) diff --git a/service/service.go b/service/service.go index 6b551fa..54aa43d 100644 --- a/service/service.go +++ b/service/service.go @@ -3,6 +3,7 @@ package service import ( "context" "strings" + "time" "github.com/pkg/errors" @@ -20,10 +21,11 @@ type Manager interface { RenameNamespace(ctx context.Context, oldName, newName string) error DeleteNamespace(ctx context.Context, name string) error - CreateContainer(ctx context.Context, namespace, name string) error + CreateContainer(ctx context.Context, namespace, name string, ttl time.Duration) error MoveContainer(ctx context.Context, namespace, container, destNamespace string) error RenameContainer(ctx context.Context, namespace, oldName, newName string) error DeleteContainer(ctx context.Context, namespace, name string) error + SetContainerParameters(ctx context.Context, namespace, name string, ttl time.Duration) error CreateVersion(ctx context.Context, namespace, container string) (id string, err error) ListAllVersions(ctx context.Context, namespace, container string) ([]models.Version, error) @@ -102,8 +104,8 @@ func (s *service) DeleteNamespace(ctx context.Context, name string) error { return mapMetadataErrors(err) } -func (s *service) CreateContainer(ctx context.Context, namespace, name string) error { - err := s.mdRepo.CreateContainer(ctx, namespace, name) +func (s *service) CreateContainer(ctx context.Context, namespace, name string, ttl time.Duration) error { + err := s.mdRepo.CreateContainer(ctx, namespace, name, ttl) if err != nil { return mapMetadataErrors(err) } @@ -126,6 +128,14 @@ func (s *service) RenameContainer(ctx context.Context, namespace, oldName, newNa return nil } +func (s *service) SetContainerParameters(ctx context.Context, namespace, name string, ttl time.Duration) error { + err := s.mdRepo.SetContainerParameters(ctx, namespace, name, ttl) + if err != nil { + return mapMetadataErrors(err) + } + return nil +} + func (s *service) ListContainers(ctx context.Context, namespace string) ([]models.Container, error) { containers, err := s.mdRepo.ListContainers(ctx, namespace) return containers, mapMetadataErrors(err) diff --git a/service/service_test.go b/service/service_test.go index bd23efb..c78783f 100644 --- a/service/service_test.go +++ b/service/service_test.go @@ -3,6 +3,7 @@ package service import ( "context" "testing" + "time" "github.com/pkg/errors" "github.com/stretchr/testify/suite" @@ -74,15 +75,15 @@ func (s *serviceTestSuite) TestDeleteNamespace() { func (s *serviceTestSuite) TestCreateContainer() { // Happy path - s.mdRepoMock.On("CreateContainer", defaultNamespace, "container").Return(nil).Once() + s.mdRepoMock.On("CreateContainer", defaultNamespace, "container", time.Duration(-1)).Return(nil).Once() - err := s.svc.CreateContainer(s.ctx, defaultNamespace, "container") + err := s.svc.CreateContainer(s.ctx, defaultNamespace, "container", -1) s.Require().NoError(err) // return error - s.mdRepoMock.On("CreateContainer", defaultNamespace, "container").Return(errors.New("test error")).Once() + s.mdRepoMock.On("CreateContainer", defaultNamespace, "container", 3*time.Hour).Return(errors.New("test error")).Once() - err = s.svc.CreateContainer(s.ctx, defaultNamespace, "container") + err = s.svc.CreateContainer(s.ctx, defaultNamespace, "container", 3*time.Hour) s.Require().Error(err) s.Require().Equal("test error", err.Error()) } @@ -124,6 +125,13 @@ func (s *serviceTestSuite) TestDeleteContainer() { s.Require().NoError(err) } +func (s *serviceTestSuite) TestSetContainerParameters() { + s.mdRepoMock.On("SetContainerParameters", defaultNamespace, "container", 1*time.Hour).Return(nil).Once() + + err := s.svc.SetContainerParameters(s.ctx, defaultNamespace, "container", 1*time.Hour) + s.Require().NoError(err) +} + func (s *serviceTestSuite) TestCreateVersion() { s.mdRepoMock.On("CreateVersion", defaultNamespace, "container").Return("versionID", nil).Once()