Support Space Soft Delete, Restore, and Purge (#1076)

pull/3486/head
Atefeh Mohseni-Ejiyeh 2024-03-21 23:34:19 +00:00 committed by Harness
parent cda2de9606
commit 5d0d28e4a3
41 changed files with 1363 additions and 237 deletions

View File

@ -97,7 +97,7 @@ func (c *Controller) Create(ctx context.Context, session *auth.Session, in *Crea
}
err = c.repoStore.Create(ctx, repo)
if err != nil {
if dErr := c.deleteGitRepository(ctx, session, repo); dErr != nil {
if dErr := c.DeleteGitRepository(ctx, session, repo); dErr != nil {
log.Ctx(ctx).Warn().Err(dErr).Msg("failed to delete repo for cleanup")
}
return fmt.Errorf("failed to create repository in storage: %w", err)

View File

@ -68,8 +68,8 @@ func (c *Controller) PurgeNoAuth(
return fmt.Errorf("failed to delete repo from db: %w", err)
}
if err := c.deleteGitRepository(ctx, session, repo); err != nil {
return fmt.Errorf("failed to delete git repository: %w", err)
if err := c.DeleteGitRepository(ctx, session, repo); err != nil {
log.Ctx(ctx).Err(err).Msg("failed to remove git repository")
}
c.eventReporter.Deleted(
@ -81,7 +81,7 @@ func (c *Controller) PurgeNoAuth(
return nil
}
func (c *Controller) deleteGitRepository(
func (c *Controller) DeleteGitRepository(
ctx context.Context,
session *auth.Session,
repo *types.Repository,

View File

@ -21,12 +21,15 @@ import (
apiauth "github.com/harness/gitness/app/api/auth"
"github.com/harness/gitness/app/api/usererror"
"github.com/harness/gitness/app/auth"
"github.com/harness/gitness/errors"
"github.com/harness/gitness/store"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
)
type RestoreInput struct {
NewIdentifier string `json:"new_identifier,omitempty"`
NewIdentifier *string `json:"new_identifier,omitempty"`
NewParentRef *string `json:"new_parent_ref,omitempty"`
}
func (c *Controller) Restore(
@ -49,10 +52,31 @@ func (c *Controller) Restore(
return nil, usererror.BadRequest("cannot restore a repo that hasn't been deleted")
}
repo, err = c.repoStore.Restore(ctx, repo, in.NewIdentifier)
parentID := repo.ParentID
if in.NewParentRef != nil {
space, err := c.spaceStore.FindByRef(ctx, *in.NewParentRef)
if errors.Is(err, store.ErrResourceNotFound) {
return nil, usererror.BadRequest("The provided new parent ref wasn't found.")
}
if err != nil {
return nil, fmt.Errorf("failed to find the parent ref '%s': %w", *in.NewParentRef, err)
}
parentID = space.ID
}
return c.RestoreNoAuth(ctx, repo, in.NewIdentifier, &parentID)
}
func (c *Controller) RestoreNoAuth(
ctx context.Context,
repo *types.Repository,
newIdentifier *string,
newParentID *int64,
) (*types.Repository, error) {
repo, err := c.repoStore.Restore(ctx, repo, newIdentifier, newParentID)
if err != nil {
return nil, fmt.Errorf("failed to restore the repo: %w", err)
}
return repo, nil
}

View File

@ -87,7 +87,7 @@ func (c *Controller) createSpaceInnerInTX(
if err != nil {
return nil, fmt.Errorf("failed to find primary path for parent '%d': %w", parentID, err)
}
spacePath = paths.Concatinate(parentPath.Value, in.Identifier)
spacePath = paths.Concatenate(parentPath.Value, in.Identifier)
// ensure path is within accepted depth!
err = check.PathDepth(spacePath, true)

View File

@ -1,118 +0,0 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package space
import (
"context"
"fmt"
"math"
"time"
apiauth "github.com/harness/gitness/app/api/auth"
"github.com/harness/gitness/app/auth"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
)
// Delete deletes a space.
func (c *Controller) Delete(
ctx context.Context,
session *auth.Session,
spaceRef string,
) error {
space, err := c.spaceStore.FindByRef(ctx, spaceRef)
if err != nil {
return err
}
if err = apiauth.CheckSpace(ctx, c.authorizer, session, space, enum.PermissionSpaceDelete, false); err != nil {
return err
}
return c.DeleteNoAuth(ctx, session, space.ID)
}
// DeleteNoAuth deletes the space - no authorization is verified.
// WARNING this is meant for internal calls only.
func (c *Controller) DeleteNoAuth(
ctx context.Context,
session *auth.Session,
spaceID int64,
) error {
filter := &types.SpaceFilter{
Page: 1,
Size: math.MaxInt,
Query: "",
Order: enum.OrderAsc,
Sort: enum.SpaceAttrNone,
}
subSpaces, _, err := c.ListSpacesNoAuth(ctx, spaceID, filter)
if err != nil {
return fmt.Errorf("failed to list space %d sub spaces: %w", spaceID, err)
}
for _, space := range subSpaces {
err = c.DeleteNoAuth(ctx, session, space.ID)
if err != nil {
return fmt.Errorf("failed to delete space %d: %w", space.ID, err)
}
}
err = c.deleteRepositoriesNoAuth(ctx, session, spaceID)
if err != nil {
return fmt.Errorf("failed to delete repositories of space %d: %w", spaceID, err)
}
err = c.spaceStore.Delete(ctx, spaceID)
if err != nil {
return fmt.Errorf("spaceStore failed to delete space %d: %w", spaceID, err)
}
return nil
}
// deleteRepositoriesNoAuth deletes all repositories in a space - no authorization is verified.
// WARNING this is meant for internal calls only.
func (c *Controller) deleteRepositoriesNoAuth(
ctx context.Context,
session *auth.Session,
spaceID int64,
) error {
filter := &types.RepoFilter{
Page: 1,
Size: int(math.MaxInt),
Query: "",
Order: enum.OrderAsc,
Sort: enum.RepoAttrNone,
DeletedBeforeOrAt: nil,
}
repos, _, err := c.ListRepositoriesNoAuth(ctx, spaceID, filter)
if err != nil {
return fmt.Errorf("failed to list space repositories: %w", err)
}
// TEMPORARY until we support space delete/restore CODE-1413
recent := time.Now().Add(+time.Hour * 24).UnixMilli()
filter.DeletedBeforeOrAt = &recent
alreadyDeletedRepos, _, err := c.ListRepositoriesNoAuth(ctx, spaceID, filter)
if err != nil {
return fmt.Errorf("failed to list delete repositories for space %d: %w", spaceID, err)
}
repos = append(repos, alreadyDeletedRepos...)
for _, repo := range repos {
err = c.repoCtrl.PurgeNoAuth(ctx, session, repo)
if err != nil {
return fmt.Errorf("failed to delete repository %d: %w", repo.ID, err)
}
}
return nil
}

View File

@ -0,0 +1,123 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package space
import (
"context"
"fmt"
"math"
"time"
apiauth "github.com/harness/gitness/app/api/auth"
"github.com/harness/gitness/app/auth"
"github.com/harness/gitness/contextutil"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
"github.com/rs/zerolog/log"
)
// Purge deletes the space and all its subspaces and repositories permanently.
func (c *Controller) Purge(
ctx context.Context,
session *auth.Session,
spaceRef string,
deletedAt int64,
) error {
space, err := c.spaceStore.FindByRefAndDeletedAt(ctx, spaceRef, deletedAt)
if err != nil {
return err
}
// authz will check the permission within the first existing parent since space was deleted.
// purge top level space is limited to admin only.
err = apiauth.CheckSpace(ctx, c.authorizer, session, space, enum.PermissionSpaceDelete, false)
if err != nil {
return fmt.Errorf("failed to authorize on space purge: %w", err)
}
return c.PurgeNoAuth(ctx, session, space.ID, deletedAt)
}
// PurgeNoAuth purges the space - no authorization is verified.
// WARNING For internal calls only.
func (c *Controller) PurgeNoAuth(
ctx context.Context,
session *auth.Session,
spaceID int64,
deletedAt int64,
) error {
// the max time we give a purge space to succeed
const timeout = 15 * time.Minute
// create new, time-restricted context to guarantee space purge completion, even if request is canceled.
ctx, cancel := context.WithTimeout(
contextutil.WithNewValues(context.Background(), ctx),
timeout,
)
defer cancel()
var toBeDeletedRepos []*types.Repository
var err error
err = c.tx.WithTx(ctx, func(ctx context.Context) error {
toBeDeletedRepos, err = c.purgeSpaceInnerInTx(ctx, spaceID, deletedAt)
return err
})
if err != nil {
return fmt.Errorf("failed to purge space %d in a tnx: %w", spaceID, err)
}
// permanently purge all repositories in the space and its subspaces after successful space purge tnx.
// cleanup will handle failed repository deletions.
for _, repo := range toBeDeletedRepos {
err := c.repoCtrl.DeleteGitRepository(ctx, session, repo)
if err != nil {
log.Ctx(ctx).Warn().Err(err).
Str("repo_identifier", repo.Identifier).
Int64("repo_id", repo.ID).
Int64("repo_parent_id", repo.ParentID).
Msg("failed to delete repository")
}
}
return nil
}
func (c *Controller) purgeSpaceInnerInTx(
ctx context.Context,
spaceID int64,
deletedAt int64,
) ([]*types.Repository, error) {
filter := &types.RepoFilter{
Page: 1,
Size: int(math.MaxInt),
Query: "",
Order: enum.OrderAsc,
Sort: enum.RepoAttrDeleted,
DeletedBeforeOrAt: &deletedAt,
Recursive: true,
}
repos, err := c.repoStore.List(ctx, spaceID, filter)
if err != nil {
return nil, fmt.Errorf("failed to list space repositories: %w", err)
}
// purge cascade deletes all the child spaces from DB.
err = c.spaceStore.Purge(ctx, spaceID, &deletedAt)
if err != nil {
return nil, fmt.Errorf("spaceStore failed to delete space %d: %w", spaceID, err)
}
return repos, nil
}

View File

@ -0,0 +1,260 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package space
import (
"context"
"errors"
"fmt"
"math"
"time"
apiauth "github.com/harness/gitness/app/api/auth"
"github.com/harness/gitness/app/api/usererror"
"github.com/harness/gitness/app/auth"
"github.com/harness/gitness/app/paths"
"github.com/harness/gitness/store"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/check"
"github.com/harness/gitness/types/enum"
)
type RestoreInput struct {
NewIdentifier *string `json:"new_identifier,omitempty"`
NewParentRef *string `json:"new_parent_ref,omitempty"` // Reference of the new parent space
}
var errSpacePathInvalid = usererror.BadRequest("Space ref or identifier is invalid.")
func (c *Controller) Restore(
ctx context.Context,
session *auth.Session,
spaceRef string,
deletedAt int64,
in *RestoreInput,
) (*types.Space, error) {
if err := c.sanitizeRestoreInput(in); err != nil {
return nil, fmt.Errorf("failed to sanitize restore input: %w", err)
}
space, err := c.spaceStore.FindByRefAndDeletedAt(ctx, spaceRef, deletedAt)
if err != nil {
return nil, fmt.Errorf("failed to find the space: %w", err)
}
// check view permission on the original ref.
err = apiauth.CheckSpace(ctx, c.authorizer, session, space, enum.PermissionSpaceView, false)
if err != nil {
return nil, fmt.Errorf("failed to authorize on space restore: %w", err)
}
parentSpace, err := c.getParentSpace(ctx, space, in.NewParentRef)
if err != nil {
return nil, fmt.Errorf("failed to get space parent: %w", err)
}
// check create permissions within the parent space scope.
if err = apiauth.CheckSpaceScope(
ctx,
c.authorizer,
session,
parentSpace,
enum.ResourceTypeSpace,
enum.PermissionSpaceEdit,
false,
); err != nil {
return nil, fmt.Errorf("authorization failed on space restore: %w", err)
}
spacePath := paths.Concatenate(parentSpace.Path, space.Identifier)
if in.NewIdentifier != nil {
spacePath = paths.Concatenate(parentSpace.Path, *in.NewIdentifier)
}
err = c.tx.WithTx(ctx, func(ctx context.Context) error {
space, err = c.restoreSpaceInnerInTx(
ctx,
space,
deletedAt,
in.NewIdentifier,
&parentSpace.ID,
spacePath)
return err
})
if err != nil {
return nil, fmt.Errorf("failed to restore space in a tnx: %w", err)
}
return space, nil
}
func (c *Controller) restoreSpaceInnerInTx(
ctx context.Context,
space *types.Space,
deletedAt int64,
newIdentifier *string,
newParentID *int64,
spacePath string,
) (*types.Space, error) {
filter := &types.SpaceFilter{
Page: 1,
Size: math.MaxInt,
Query: "",
Order: enum.OrderDesc,
Sort: enum.SpaceAttrCreated,
DeletedBeforeOrAt: &deletedAt,
Recursive: true,
}
subSpaces, err := c.spaceStore.List(ctx, space.ID, filter)
if err != nil {
return nil, fmt.Errorf("failed to list space %d sub spaces recursively: %w", space.ID, err)
}
var subspacePath string
for _, subspace := range subSpaces {
// check the path depth before restore nested subspaces.
subspacePath = subspace.Path[len(space.Path):]
if err = check.PathDepth(paths.Concatenate(spacePath, subspacePath), true); err != nil {
return nil, fmt.Errorf("path is invalid: %w", err)
}
// identifier and parent ID of sub spaces shouldn't change.
_, err = c.restoreNoAuth(ctx, subspace, nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to restore subspace: %w", err)
}
}
if err := c.restoreRepositoriesNoAuth(ctx, space.ID, deletedAt); err != nil {
return nil, fmt.Errorf("failed to restore space %d repositories: %w", space.ID, err)
}
// restore the target space
restoredSpace, err := c.restoreNoAuth(ctx, space, newIdentifier, newParentID)
if err != nil {
return nil, fmt.Errorf("failed to restore space: %w", err)
}
if err = check.PathDepth(restoredSpace.Path, true); err != nil {
return nil, fmt.Errorf("path is invalid: %w", err)
}
return restoredSpace, nil
}
func (c *Controller) restoreNoAuth(
ctx context.Context,
space *types.Space,
newIdentifier *string,
newParentID *int64,
) (*types.Space, error) {
space, err := c.spaceStore.Restore(ctx, space, newIdentifier, newParentID)
if err != nil {
return nil, fmt.Errorf("failed to restore the space: %w", err)
}
now := time.Now().UnixMilli()
pathSegment := &types.SpacePathSegment{
Identifier: space.Identifier,
IsPrimary: true,
SpaceID: space.ID,
ParentID: space.ParentID,
CreatedBy: space.CreatedBy,
Created: now,
Updated: now,
}
err = c.spacePathStore.InsertSegment(ctx, pathSegment)
if errors.Is(err, store.ErrDuplicate) {
return nil, usererror.BadRequest(fmt.Sprintf("A primary path already exists for %s.",
space.Identifier))
}
if err != nil {
return nil, fmt.Errorf("failed to insert space path on restore: %w", err)
}
return space, nil
}
func (c *Controller) restoreRepositoriesNoAuth(
ctx context.Context,
spaceID int64,
deletedAt int64,
) error {
filter := &types.RepoFilter{
Page: 1,
Size: int(math.MaxInt),
Query: "",
Order: enum.OrderAsc,
Sort: enum.RepoAttrNone,
DeletedBeforeOrAt: &deletedAt,
Recursive: true,
}
repos, err := c.repoStore.List(ctx, spaceID, filter)
if err != nil {
return fmt.Errorf("failed to list space repositories: %w", err)
}
for _, repo := range repos {
_, err = c.repoCtrl.RestoreNoAuth(ctx, repo, nil, nil)
if err != nil {
return fmt.Errorf("failed to restore repository: %w", err)
}
}
return nil
}
func (c *Controller) getParentSpace(
ctx context.Context,
space *types.Space,
newParentRef *string,
) (*types.Space, error) {
var parentSpace *types.Space
var err error
if newParentRef == nil {
if space.ParentID == 0 {
return &types.Space{}, nil
}
parentSpace, err = c.spaceStore.Find(ctx, space.ParentID)
if err != nil {
return nil, fmt.Errorf("failed to find the space parent %d", space.ParentID)
}
return parentSpace, nil
}
// the provided new reference for space parent must exist.
parentSpace, err = c.spaceStore.FindByRef(ctx, *newParentRef)
if err != nil {
return nil, fmt.Errorf("failed to find the parent space by ref '%s': %w - returning usererror %w",
*newParentRef, err, errSpacePathInvalid)
}
return parentSpace, nil
}
func (c *Controller) sanitizeRestoreInput(in *RestoreInput) error {
if in.NewParentRef == nil {
return nil
}
if len(*in.NewParentRef) > 0 && !c.nestedSpacesEnabled {
return errNestedSpacesNotSupported
}
return nil
}

View File

@ -0,0 +1,149 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package space
import (
"context"
"fmt"
"math"
"time"
apiauth "github.com/harness/gitness/app/api/auth"
"github.com/harness/gitness/app/auth"
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
)
type SoftDeleteResponse struct {
DeletedAt int64 `json:"deleted_at"`
}
// SoftDelete marks deleted timestamp for the space and all its subspaces and repositories inside.
func (c *Controller) SoftDelete(
ctx context.Context,
session *auth.Session,
spaceRef string,
) (*SoftDeleteResponse, error) {
space, err := c.spaceStore.FindByRef(ctx, spaceRef)
if err != nil {
return nil, fmt.Errorf("failed to find space for soft delete: %w", err)
}
if err = apiauth.CheckSpace(
ctx,
c.authorizer,
session,
space,
enum.PermissionSpaceDelete,
false,
); err != nil {
return nil, fmt.Errorf("failed to check access: %w", err)
}
return c.SoftDeleteNoAuth(ctx, space)
}
// SoftDeleteNoAuth soft deletes the space - no authorization is verified.
// WARNING For internal calls only.
func (c *Controller) SoftDeleteNoAuth(
ctx context.Context,
space *types.Space,
) (*SoftDeleteResponse, error) {
var softDelRes *SoftDeleteResponse
var err error
err = c.tx.WithTx(ctx, func(ctx context.Context) error {
softDelRes, err = c.softDeleteInnerInTx(ctx, space)
return err
})
if err != nil {
return nil, fmt.Errorf("failed to soft delete the space: %w", err)
}
return softDelRes, nil
}
func (c *Controller) softDeleteInnerInTx(
ctx context.Context,
space *types.Space,
) (*SoftDeleteResponse, error) {
filter := &types.SpaceFilter{
Page: 1,
Size: math.MaxInt,
Query: "",
Order: enum.OrderAsc,
Sort: enum.SpaceAttrCreated,
DeletedBeforeOrAt: nil, // only filter active subspaces
Recursive: true,
}
subSpaces, err := c.spaceStore.List(ctx, space.ID, filter)
if err != nil {
return nil, fmt.Errorf("failed to list space %d sub spaces recursively: %w", space.ID, err)
}
now := time.Now().UnixMilli()
for _, space := range subSpaces {
if err := c.spaceStore.SoftDelete(ctx, space, now); err != nil {
return nil, fmt.Errorf("failed to soft delete subspace: %w", err)
}
}
err = c.softDeleteRepositoriesNoAuth(ctx, space.ID, now)
if err != nil {
return nil, fmt.Errorf("failed to soft delete repositories of space %d: %w", space.ID, err)
}
if err = c.spaceStore.SoftDelete(ctx, space, now); err != nil {
return nil, fmt.Errorf("spaceStore failed to soft delete space: %w", err)
}
err = c.spacePathStore.DeletePathsAndDescendandPaths(ctx, space.ID)
if err != nil {
return nil, fmt.Errorf("spacePathStore failed to delete descendant paths of %d: %w", space.ID, err)
}
return &SoftDeleteResponse{DeletedAt: now}, nil
}
// softDeleteRepositoriesNoAuth soft deletes all repositories in a space - no authorization is verified.
// WARNING For internal calls only.
func (c *Controller) softDeleteRepositoriesNoAuth(
ctx context.Context,
spaceID int64,
deletedAt int64,
) error {
filter := &types.RepoFilter{
Page: 1,
Size: int(math.MaxInt),
Query: "",
Order: enum.OrderAsc,
Sort: enum.RepoAttrNone,
DeletedBeforeOrAt: nil, // only filter active repos
Recursive: true,
}
repos, err := c.repoStore.List(ctx, spaceID, filter)
if err != nil {
return fmt.Errorf("failed to list space repositories: %w", err)
}
for _, repo := range repos {
err = c.repoCtrl.SoftDeleteNoAuth(ctx, repo, deletedAt)
if err != nil {
return fmt.Errorf("failed to soft delete repository: %w", err)
}
}
return nil
}

View File

@ -25,7 +25,6 @@ import (
func HandlePurge(repoCtrl *repo.Controller) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
session, _ := request.AuthSessionFrom(ctx)
repoRef, err := request.GetRepoRefFromPath(r)
@ -34,7 +33,7 @@ func HandlePurge(repoCtrl *repo.Controller) http.HandlerFunc {
return
}
deletedAt, err := request.GetDeletedAtFromQuery(r)
deletedAt, err := request.GetDeletedAtFromQueryOrError(r)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return

View File

@ -35,7 +35,7 @@ func HandleRestore(repoCtrl *repo.Controller) http.HandlerFunc {
return
}
deletedAt, err := request.GetDeletedAtFromQuery(r)
deletedAt, err := request.GetDeletedAtFromQueryOrError(r)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return

View File

@ -34,7 +34,12 @@ func HandleListSpaces(spaceCtrl *space.Controller) http.HandlerFunc {
return
}
spaceFilter := request.ParseSpaceFilter(r)
spaceFilter, err := request.ParseSpaceFilter(r)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return
}
if spaceFilter.Order == enum.OrderDefault {
spaceFilter.Order = enum.OrderAsc
}

View File

@ -34,17 +34,16 @@ func HandleListRepos(spaceCtrl *space.Controller) http.HandlerFunc {
return
}
filter := request.ParseRepoFilter(r)
if filter.Order == enum.OrderDefault {
filter.Order = enum.OrderAsc
}
filter.Recursive, err = request.ParseRecursiveFromQuery(r)
filter, err := request.ParseRepoFilter(r)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return
}
if filter.Order == enum.OrderDefault {
filter.Order = enum.OrderAsc
}
repos, count, err := spaceCtrl.ListRepositories(
ctx, session, spaceRef, filter)
if err != nil {

View File

@ -0,0 +1,51 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package space
import (
"net/http"
"github.com/harness/gitness/app/api/controller/space"
"github.com/harness/gitness/app/api/render"
"github.com/harness/gitness/app/api/request"
)
// HandlePurge handles the purge delete space HTTP API.
func HandlePurge(spaceCtrl *space.Controller) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
session, _ := request.AuthSessionFrom(ctx)
spaceRef, err := request.GetSpaceRefFromPath(r)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return
}
deletedAt, err := request.GetDeletedAtFromQueryOrError(r)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return
}
err = spaceCtrl.Purge(ctx, session, spaceRef, deletedAt)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return
}
render.DeleteSuccessful(w)
}
}

View File

@ -0,0 +1,59 @@
// Copyright 2023 Harness, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package space
import (
"encoding/json"
"net/http"
"github.com/harness/gitness/app/api/controller/space"
"github.com/harness/gitness/app/api/render"
"github.com/harness/gitness/app/api/request"
)
// HandleRestore handles the restore of soft deleted space HTTP API.
func HandleRestore(spaceCtrl *space.Controller) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
session, _ := request.AuthSessionFrom(ctx)
spaceRef, err := request.GetSpaceRefFromPath(r)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return
}
deletedAt, err := request.GetDeletedAtFromQueryOrError(r)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return
}
in := new(space.RestoreInput)
err = json.NewDecoder(r.Body).Decode(in)
if err != nil {
render.BadRequestf(ctx, w, "Invalid request body: %s.", err)
return
}
space, err := spaceCtrl.Restore(ctx, session, spaceRef, deletedAt, in)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return
}
render.JSON(w, http.StatusOK, space)
}
}

View File

@ -22,23 +22,25 @@ import (
"github.com/harness/gitness/app/api/request"
)
// HandleDelete handles the delete space HTTP API.
func HandleDelete(spaceCtrl *space.Controller) http.HandlerFunc {
// HandleSoftDelete handles the soft delete space HTTP API.
func HandleSoftDelete(spaceCtrl *space.Controller) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
session, _ := request.AuthSessionFrom(ctx)
spaceRef, err := request.GetSpaceRefFromPath(r)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return
}
err = spaceCtrl.Delete(ctx, session, spaceRef)
res, err := spaceCtrl.SoftDelete(ctx, session, spaceRef)
if err != nil {
render.TranslatedUserError(ctx, w, err)
return
}
render.DeleteSuccessful(w)
render.JSON(w, http.StatusOK, res)
}
}

View File

@ -50,6 +50,11 @@ type exportSpaceRequest struct {
space.ExportInput
}
type restoreSpaceRequest struct {
spaceRequest
space.RestoreInput
}
var queryParameterSortRepo = openapi3.ParameterOrRef{
Parameter: &openapi3.Parameter{
Name: request.QueryParamSort,
@ -246,13 +251,38 @@ func spaceOperations(reflector *openapi3.Reflector) {
opDelete.WithTags("space")
opDelete.WithMapOfAnything(map[string]interface{}{"operationId": "deleteSpace"})
_ = reflector.SetRequest(&opDelete, new(spaceRequest), http.MethodDelete)
_ = reflector.SetJSONResponse(&opDelete, nil, http.StatusNoContent)
_ = reflector.SetJSONResponse(&opDelete, new(space.SoftDeleteResponse), http.StatusOK)
_ = reflector.SetJSONResponse(&opDelete, new(usererror.Error), http.StatusInternalServerError)
_ = reflector.SetJSONResponse(&opDelete, new(usererror.Error), http.StatusUnauthorized)
_ = reflector.SetJSONResponse(&opDelete, new(usererror.Error), http.StatusForbidden)
_ = reflector.SetJSONResponse(&opDelete, new(usererror.Error), http.StatusNotFound)
_ = reflector.Spec.AddOperation(http.MethodDelete, "/spaces/{space_ref}", opDelete)
opPurge := openapi3.Operation{}
opPurge.WithTags("space")
opPurge.WithMapOfAnything(map[string]interface{}{"operationId": "purgeSpace"})
opPurge.WithParameters(queryParameterDeletedAt)
_ = reflector.SetRequest(&opPurge, new(spaceRequest), http.MethodPost)
_ = reflector.SetJSONResponse(&opPurge, nil, http.StatusNoContent)
_ = reflector.SetJSONResponse(&opPurge, new(usererror.Error), http.StatusInternalServerError)
_ = reflector.SetJSONResponse(&opPurge, new(usererror.Error), http.StatusUnauthorized)
_ = reflector.SetJSONResponse(&opPurge, new(usererror.Error), http.StatusForbidden)
_ = reflector.SetJSONResponse(&opPurge, new(usererror.Error), http.StatusNotFound)
_ = reflector.Spec.AddOperation(http.MethodPost, "/spaces/{space_ref}/purge", opPurge)
opRestore := openapi3.Operation{}
opRestore.WithTags("space")
opRestore.WithMapOfAnything(map[string]interface{}{"operationId": "restoreSpace"})
opRestore.WithParameters(queryParameterDeletedAt)
_ = reflector.SetRequest(&opRestore, new(restoreSpaceRequest), http.MethodPost)
_ = reflector.SetJSONResponse(&opRestore, new(types.Space), http.StatusOK)
_ = reflector.SetJSONResponse(&opRestore, new(usererror.Error), http.StatusBadRequest)
_ = reflector.SetJSONResponse(&opRestore, new(usererror.Error), http.StatusInternalServerError)
_ = reflector.SetJSONResponse(&opRestore, new(usererror.Error), http.StatusUnauthorized)
_ = reflector.SetJSONResponse(&opRestore, new(usererror.Error), http.StatusForbidden)
_ = reflector.SetJSONResponse(&opRestore, new(usererror.Error), http.StatusNotFound)
_ = reflector.Spec.AddOperation(http.MethodPost, "/spaces/{space_ref}/restore", opRestore)
opMove := openapi3.Operation{}
opMove.WithTags("space")
opMove.WithMapOfAnything(map[string]interface{}{"operationId": "moveSpace"})

View File

@ -29,6 +29,7 @@ const (
QueryParamSort = "sort"
QueryParamOrder = "order"
QueryParamQuery = "query"
QueryParamRecursive = "recursive"
QueryParamState = "state"
QueryParamKind = "kind"
@ -37,7 +38,8 @@ const (
QueryParamAfter = "after"
QueryParamBefore = "before"
QueryParamDeletedAt = "deleted_at"
QueryParamDeletedBeforeOrAt = "deleted_before_or_at"
QueryParamDeletedAt = "deleted_at"
QueryParamPage = "page"
QueryParamLimit = "limit"
@ -121,7 +123,25 @@ func GetContentEncodingFromHeadersOrDefault(r *http.Request, dflt string) string
return GetHeaderOrDefault(r, HeaderContentEncoding, dflt)
}
// GetDeletedAtFromQuery extracts the resource deleted timestamp from the query.
func GetDeletedAtFromQuery(r *http.Request) (int64, error) {
// ParseRecursiveFromQuery extracts the recursive option from the URL query.
func ParseRecursiveFromQuery(r *http.Request) (bool, error) {
return QueryParamAsBoolOrDefault(r, QueryParamRecursive, false)
}
// GetDeletedAtFromQueryOrError gets the exact resource deletion timestamp from the query.
func GetDeletedAtFromQueryOrError(r *http.Request) (int64, error) {
return QueryParamAsPositiveInt64(r, QueryParamDeletedAt)
}
// GetDeletedBeforeOrAtFromQuery gets the resource deletion timestamp from the query as an optional parameter.
func GetDeletedBeforeOrAtFromQuery(r *http.Request) (int64, bool, error) {
value, err := QueryParamAsPositiveInt64OrDefault(r, QueryParamDeletedBeforeOrAt, 0)
if err != nil {
return 0, false, err
}
if value == 0 {
return value, false, nil
}
return value, true, nil
}

View File

@ -23,9 +23,8 @@ import (
)
const (
PathParamRepoRef = "repo_ref"
QueryParamRepoID = "repo_id"
QueryParamRecursive = "recursive"
PathParamRepoRef = "repo_ref"
QueryParamRepoID = "repo_id"
)
func GetRepoRefFromPath(r *http.Request) (string, error) {
@ -46,17 +45,30 @@ func ParseSortRepo(r *http.Request) enum.RepoAttr {
}
// ParseRepoFilter extracts the repository filter from the url.
func ParseRepoFilter(r *http.Request) *types.RepoFilter {
return &types.RepoFilter{
Query: ParseQuery(r),
Order: ParseOrder(r),
Page: ParsePage(r),
Sort: ParseSortRepo(r),
Size: ParseLimit(r),
func ParseRepoFilter(r *http.Request) (*types.RepoFilter, error) {
// recursive is optional to get all repos in a sapce and its subsapces recursively.
recursive, err := ParseRecursiveFromQuery(r)
if err != nil {
return nil, err
}
}
// ParseRecursiveFromQuery extracts the recursive option from the URL query.
func ParseRecursiveFromQuery(r *http.Request) (bool, error) {
return QueryParamAsBoolOrDefault(r, QueryParamRecursive, false)
// deletedBeforeOrAt is optional to retrieve repos deleted before or at the specified timestamp.
var deletionTime *int64
value, ok, err := GetDeletedBeforeOrAtFromQuery(r)
if err != nil {
return nil, err
}
if ok {
deletionTime = &value
}
return &types.RepoFilter{
Query: ParseQuery(r),
Order: ParseOrder(r),
Page: ParsePage(r),
Sort: ParseSortRepo(r),
Size: ParseLimit(r),
Recursive: recursive,
DeletedBeforeOrAt: deletionTime,
}, nil
}

View File

@ -44,12 +44,30 @@ func ParseSortSpace(r *http.Request) enum.SpaceAttr {
}
// ParseSpaceFilter extracts the space filter from the url.
func ParseSpaceFilter(r *http.Request) *types.SpaceFilter {
return &types.SpaceFilter{
Query: ParseQuery(r),
Order: ParseOrder(r),
Page: ParsePage(r),
Sort: ParseSortSpace(r),
Size: ParseLimit(r),
func ParseSpaceFilter(r *http.Request) (*types.SpaceFilter, error) {
// recursive is optional to get sapce and its subsapces recursively.
recursive, err := ParseRecursiveFromQuery(r)
if err != nil {
return nil, err
}
// deletedBeforeOrAt is optional to retrieve spaces deleted before or at the specified timestamp.
var deletionTime *int64
value, ok, err := GetDeletedBeforeOrAtFromQuery(r)
if err != nil {
return nil, err
}
if ok {
deletionTime = &value
}
return &types.SpaceFilter{
Query: ParseQuery(r),
Order: ParseOrder(r),
Page: ParsePage(r),
Sort: ParseSortSpace(r),
Size: ParseLimit(r),
Recursive: recursive,
DeletedBeforeOrAt: deletionTime,
}, nil
}

View File

@ -81,7 +81,7 @@ func (a *MembershipAuthorizer) Check(
//nolint:exhaustive // we want to fail on anything else
switch resource.Type {
case enum.ResourceTypeSpace:
spacePath = paths.Concatinate(scope.SpacePath, resource.Identifier)
spacePath = paths.Concatenate(scope.SpacePath, resource.Identifier)
case enum.ResourceTypeRepo:
spacePath = scope.SpacePath

View File

@ -57,10 +57,14 @@ func (g permissionCacheGetter) Find(ctx context.Context, key PermissionCacheKey)
spaceRef := key.SpaceRef
principalID := key.PrincipalID
// Find the starting space.
space, err := g.spaceStore.FindByRef(ctx, spaceRef)
// Find the first existing space.
space, err := g.findFirstExistingSpace(ctx, spaceRef)
// authz fails if no active space is found on the path; admins can still operate on deleted top-level spaces.
if errors.Is(err, gitness_store.ErrResourceNotFound) {
return false, nil
}
if err != nil {
return false, fmt.Errorf("failed to find space '%s': %w", spaceRef, err)
return false, fmt.Errorf("failed to find an existing space on path '%s': %w", spaceRef, err)
}
// limit the depth to be safe (e.g. root/space1/space2 => maxDepth of 3)
@ -102,3 +106,27 @@ func roleHasPermission(role enum.MembershipRole, permission enum.Permission) boo
_, hasRole := slices.BinarySearch(role.Permissions(), permission)
return hasRole
}
// findFirstExistingSpace returns the initial or first existing ancestor space (permissions are inherited).
func (g permissionCacheGetter) findFirstExistingSpace(ctx context.Context, spaceRef string) (*types.Space, error) {
for {
space, err := g.spaceStore.FindByRef(ctx, spaceRef)
if err == nil {
return space, nil
}
if !errors.Is(err, gitness_store.ErrResourceNotFound) {
return nil, fmt.Errorf("failed to find space '%s': %w", spaceRef, err)
}
// check whether parent space exists as permissions are inherited.
spaceRef, _, err = paths.DisectLeaf(spaceRef)
if err != nil {
return nil, fmt.Errorf("failed to disect path '%s': %w", spaceRef, err)
}
if spaceRef == "" {
return nil, gitness_store.ErrResourceNotFound
}
}
}

View File

@ -60,12 +60,12 @@ func DisectRoot(path string) (string, string, error) {
}
/*
* Concatinate two paths together (takes care of leading / trailing '/')
* Concatenate two paths together (takes care of leading / trailing '/')
* e.g. (space1/, /space2/) -> space1/space2
*
* NOTE: "//" is not a valid path, so all '/' will be trimmed.
*/
func Concatinate(path1 string, path2 string) string {
func Concatenate(path1 string, path2 string) string {
path1 = strings.Trim(path1, types.PathSeparator)
path2 = strings.Trim(path2, types.PathSeparator)

View File

@ -214,7 +214,9 @@ func setupSpaces(r chi.Router, appCtx context.Context, spaceCtrl *space.Controll
// space operations
r.Get("/", handlerspace.HandleFind(spaceCtrl))
r.Patch("/", handlerspace.HandleUpdate(spaceCtrl))
r.Delete("/", handlerspace.HandleDelete(spaceCtrl))
r.Delete("/", handlerspace.HandleSoftDelete(spaceCtrl))
r.Post("/restore", handlerspace.HandleRestore(spaceCtrl))
r.Post("/purge", handlerspace.HandlePurge(spaceCtrl))
r.Get("/events", handlerspace.HandleEvents(appCtx, spaceCtrl))

View File

@ -64,7 +64,7 @@ func (c *pathCache) Get(ctx context.Context, key string) (*types.SpacePath, erro
segments := paths.Segments(key)
uniqueKey := ""
for i, segment := range segments {
uniqueKey = paths.Concatinate(uniqueKey, c.spacePathTransformation(segment, i == 0))
uniqueKey = paths.Concatenate(uniqueKey, c.spacePathTransformation(segment, i == 0))
}
return c.inner.Get(ctx, uniqueKey)

View File

@ -146,6 +146,9 @@ type (
// DeletePrimarySegment deletes the primary segment of a space.
DeletePrimarySegment(ctx context.Context, spaceID int64) error
// DeletePathsAndDescendandPaths deletes all space paths reachable from spaceID including itself.
DeletePathsAndDescendandPaths(ctx context.Context, spaceID int64) error
}
// SpaceStore defines the space data storage.
@ -156,6 +159,9 @@ type (
// FindByRef finds the space using the spaceRef as either the id or the space path.
FindByRef(ctx context.Context, spaceRef string) (*types.Space, error)
// FindByRefAndDeletedAt finds the space using the spaceRef and deleted timestamp.
FindByRefAndDeletedAt(ctx context.Context, spaceRef string, deletedAt int64) (*types.Space, error)
// GetRootSpace returns a space where space_parent_id is NULL.
GetRootSpace(ctx context.Context, spaceID int64) (*types.Space, error)
@ -169,8 +175,15 @@ type (
UpdateOptLock(ctx context.Context, space *types.Space,
mutateFn func(space *types.Space) error) (*types.Space, error)
// Delete deletes the space.
Delete(ctx context.Context, id int64) error
// SoftDelete deletes the space.
SoftDelete(ctx context.Context, space *types.Space, deletedAt int64) error
// Purge deletes a space permanently.
Purge(ctx context.Context, id int64, deletedAt *int64) error
// Restore restores a soft deleted space.
Restore(ctx context.Context, space *types.Space,
newIdentifier *string, newParentID *int64) (*types.Space, error)
// Count the child spaces of a space.
Count(ctx context.Context, id int64, opts *types.SpaceFilter) (int64, error)
@ -214,7 +227,7 @@ type (
// Restore a deleted repo using the optimistic locking mechanism.
Restore(ctx context.Context, repo *types.Repository,
newIdentifier string) (*types.Repository, error)
newIdentifier *string, newParentID *int64) (*types.Repository, error)
// Count of active repos in a space. With "DeletedBeforeOrAt" filter, counts deleted repos.
Count(ctx context.Context, parentID int64, opts *types.RepoFilter) (int64, error)

View File

@ -37,11 +37,13 @@ func NewMembershipStore(
db *sqlx.DB,
pCache store.PrincipalInfoCache,
spacePathStore store.SpacePathStore,
spaceStore store.SpaceStore,
) *MembershipStore {
return &MembershipStore{
db: db,
pCache: pCache,
spacePathStore: spacePathStore,
spaceStore: spaceStore,
}
}
@ -50,6 +52,7 @@ type MembershipStore struct {
db *sqlx.DB
pCache store.PrincipalInfoCache
spacePathStore store.SpacePathStore
spaceStore store.SpaceStore
}
type membership struct {
@ -472,7 +475,7 @@ func (s *MembershipStore) mapToMembershipSpaces(ctx context.Context,
for i := range ms {
m := ms[i]
res[i].Membership = mapToMembership(&m.membership)
space, err := mapToSpace(ctx, s.spacePathStore, &m.space)
space, err := mapToSpace(ctx, s.db, s.spacePathStore, &m.space)
if err != nil {
return nil, fmt.Errorf("faild to map space %d: %w", m.space.ID, err)
}

View File

@ -105,7 +105,7 @@ func migrateAfter_0039_alter_table_webhooks_uid(ctx context.Context, dbtx *sql.T
for i := 0; i < n; i++ {
wh := buffer[i]
// concatinate repoID + spaceID to get unique parent id (only used to identify same parents)
// concatenate repoID + spaceID to get unique parent id (only used to identify same parents)
newParentID := fmt.Sprintf("%d_%d", wh.repoID.ValueOrZero(), wh.spaceID.ValueOrZero())
if newParentID != parentID {
// new parent? reset child identifiers

View File

@ -0,0 +1,7 @@
ALTER TABLE spaces DROP COLUMN space_deleted;
DROP INDEX spaces_parent_id;
DROP INDEX spaces_deleted;
CREATE INDEX spaces_parent_id
ON spaces(space_parent_id);

View File

@ -0,0 +1,11 @@
ALTER TABLE spaces ADD COLUMN space_deleted BIGINT DEFAULT NULL;
DROP INDEX spaces_parent_id;
CREATE INDEX spaces_parent_id
ON spaces(space_parent_id)
WHERE space_deleted IS NULL;
CREATE INDEX spaces_deleted_parent_id
ON spaces(space_deleted, space_parent_id)
WHERE space_deleted IS NOT NULL;

View File

@ -0,0 +1,7 @@
ALTER TABLE spaces DROP COLUMN space_deleted;
DROP INDEX spaces_parent_id;
DROP INDEX spaces_deleted;
CREATE INDEX spaces_parent_id
ON spaces(space_parent_id);

View File

@ -0,0 +1,11 @@
ALTER TABLE spaces ADD COLUMN space_deleted BIGINT DEFAULT NULL;
DROP INDEX spaces_parent_id;
CREATE INDEX spaces_parent_id
ON spaces(space_parent_id)
WHERE space_deleted IS NULL;
CREATE INDEX spaces_deleted_parent_id
ON spaces(space_deleted, space_parent_id)
WHERE space_deleted IS NOT NULL;

View File

@ -42,11 +42,13 @@ func NewRepoStore(
db *sqlx.DB,
spacePathCache store.SpacePathCache,
spacePathStore store.SpacePathStore,
spaceStore store.SpaceStore,
) *RepoStore {
return &RepoStore{
db: db,
spacePathCache: spacePathCache,
spacePathStore: spacePathStore,
spaceStore: spaceStore,
}
}
@ -55,6 +57,7 @@ type RepoStore struct {
db *sqlx.DB
spacePathCache store.SpacePathCache
spacePathStore store.SpacePathStore
spaceStore store.SpaceStore
}
type repository struct {
@ -492,12 +495,16 @@ func (s *RepoStore) Purge(ctx context.Context, id int64, deletedAt *int64) error
func (s *RepoStore) Restore(
ctx context.Context,
repo *types.Repository,
newIdentifier string,
newIdentifier *string,
newParentID *int64,
) (*types.Repository, error) {
repo, err := s.updateDeletedOptLock(ctx, repo, func(r *types.Repository) error {
r.Deleted = nil
if newIdentifier != "" {
r.Identifier = newIdentifier
if newIdentifier != nil {
r.Identifier = *newIdentifier
}
if newParentID != nil {
r.ParentID = *newParentID
}
return nil
})
@ -752,10 +759,14 @@ func (s *RepoStore) mapToRepo(
func (s *RepoStore) getRepoPath(ctx context.Context, parentID int64, repoIdentifier string) (string, error) {
spacePath, err := s.spacePathStore.FindPrimaryBySpaceID(ctx, parentID)
// try to re-create the space path if was soft deleted.
if errors.Is(err, gitness_store.ErrResourceNotFound) {
return getPathForDeletedSpace(ctx, s.db, parentID)
}
if err != nil {
return "", fmt.Errorf("failed to get primary path for space %d: %w", parentID, err)
}
return paths.Concatinate(spacePath.Value, repoIdentifier), nil
return paths.Concatenate(spacePath.Value, repoIdentifier), nil
}
func (s *RepoStore) mapToRepos(

View File

@ -78,7 +78,7 @@ func setupStores(t *testing.T, db *sqlx.DB) (
spacePathCache := cache.New(spacePathStore, spacePathTransformation)
spaceStore := database.NewSpaceStore(db, spacePathCache, spacePathStore)
repoStore := database.NewRepoStore(db, spacePathCache, spacePathStore)
repoStore := database.NewRepoStore(db, spacePathCache, spacePathStore, spaceStore)
return principalStore, spaceStore, spacePathStore, repoStore
}

View File

@ -21,6 +21,7 @@ import (
"strings"
"time"
"github.com/harness/gitness/app/paths"
"github.com/harness/gitness/app/store"
gitness_store "github.com/harness/gitness/store"
"github.com/harness/gitness/store/database"
@ -28,6 +29,7 @@ import (
"github.com/harness/gitness/types"
"github.com/harness/gitness/types/enum"
"github.com/Masterminds/squirrel"
"github.com/guregu/null"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
@ -67,6 +69,7 @@ type space struct {
CreatedBy int64 `db:"space_created_by"`
Created int64 `db:"space_created"`
Updated int64 `db:"space_updated"`
Deleted null.Int `db:"space_deleted"`
}
const (
@ -79,7 +82,8 @@ const (
,space_is_public
,space_created_by
,space_created
,space_updated`
,space_updated
,space_deleted`
spaceSelectBase = `
SELECT` + spaceColumns + `
@ -88,21 +92,57 @@ const (
// Find the space by id.
func (s *SpaceStore) Find(ctx context.Context, id int64) (*types.Space, error) {
const sqlQuery = spaceSelectBase + `
WHERE space_id = $1`
return s.find(ctx, id, nil)
}
func (s *SpaceStore) find(ctx context.Context, id int64, deletedAt *int64) (*types.Space, error) {
stmt := database.Builder.
Select(spaceColumns).
From("spaces").
Where("space_id = ?", id)
if deletedAt != nil {
stmt = stmt.Where("space_deleted = ?", *deletedAt)
} else {
stmt = stmt.Where("space_deleted IS NULL")
}
db := dbtx.GetAccessor(ctx, s.db)
dst := new(space)
if err := db.GetContext(ctx, dst, sqlQuery, id); err != nil {
sql, args, err := stmt.ToSql()
if err != nil {
return nil, errors.Wrap(err, "Failed to convert query to sql")
}
if err = db.GetContext(ctx, dst, sql, args...); err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find space")
}
return mapToSpace(ctx, s.spacePathStore, dst)
return mapToSpace(ctx, s.db, s.spacePathStore, dst)
}
// FindByRef finds the space using the spaceRef as either the id or the space path.
func (s *SpaceStore) FindByRef(ctx context.Context, spaceRef string) (*types.Space, error) {
return s.findByRef(ctx, spaceRef, nil)
}
// FindByRefAndDeletedAt finds the space using the spaceRef as either the id or the space path and deleted timestamp.
func (s *SpaceStore) FindByRefAndDeletedAt(
ctx context.Context,
spaceRef string,
deletedAt int64,
) (*types.Space, error) {
// ASSUMPTION: digits only is not a valid space path
id, err := strconv.ParseInt(spaceRef, 10, 64)
if err != nil {
return s.findByPathAndDeletedAt(ctx, spaceRef, deletedAt)
}
return s.find(ctx, id, &deletedAt)
}
func (s *SpaceStore) findByRef(ctx context.Context, spaceRef string, deletedAt *int64) (*types.Space, error) {
// ASSUMPTION: digits only is not a valid space path
id, err := strconv.ParseInt(spaceRef, 10, 64)
if err != nil {
@ -114,8 +154,44 @@ func (s *SpaceStore) FindByRef(ctx context.Context, spaceRef string) (*types.Spa
id = path.SpaceID
}
return s.find(ctx, id, deletedAt)
}
return s.Find(ctx, id)
func (s *SpaceStore) findByPathAndDeletedAt(
ctx context.Context,
spaceRef string,
deletedAt int64,
) (*types.Space, error) {
segments := paths.Segments(spaceRef)
if len(segments) < 1 {
return nil, fmt.Errorf("invalid space reference provided")
}
var stmt squirrel.SelectBuilder
switch {
case len(segments) == 1:
stmt = database.Builder.
Select("space_id").
From("spaces").
Where("space_uid = ? AND space_deleted = ? AND space_parent_id IS NULL", segments[0], deletedAt)
case len(segments) > 1:
stmt = buildRecursiveSelectQueryUsingPath(segments, deletedAt)
}
sql, args, err := stmt.ToSql()
if err != nil {
return nil, errors.Wrap(err, "Failed to create sql query")
}
db := dbtx.GetAccessor(ctx, s.db)
var spaceID int64
if err = db.GetContext(ctx, &spaceID, sql, args...); err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing custom select query")
}
return s.find(ctx, spaceID, &deletedAt)
}
// GetRootSpace returns a space where space_parent_id is NULL.
@ -161,6 +237,7 @@ func (s *SpaceStore) Create(ctx context.Context, space *types.Space) error {
,space_created_by
,space_created
,space_updated
,space_deleted
) values (
:space_version
,:space_parent_id
@ -170,6 +247,7 @@ func (s *SpaceStore) Create(ctx context.Context, space *types.Space) error {
,:space_created_by
,:space_created
,:space_updated
,:space_deleted
) RETURNING space_id`
db := dbtx.GetAccessor(ctx, s.db)
@ -201,6 +279,7 @@ func (s *SpaceStore) Update(ctx context.Context, space *types.Space) error {
,space_uid = :space_uid
,space_description = :space_description
,space_is_public = :space_is_public
,space_deleted = :space_deleted
WHERE space_id = :space_id AND space_version = :space_version - 1`
dbSpace := mapToInternalSpace(space)
@ -234,7 +313,7 @@ func (s *SpaceStore) Update(ctx context.Context, space *types.Space) error {
space.Updated = dbSpace.Updated
// update path in case parent/identifier changed
space.Path, err = getSpacePath(ctx, s.spacePathStore, space.ID)
space.Path, err = getSpacePath(ctx, s.db, s.spacePathStore, space.ID)
if err != nil {
return err
}
@ -242,8 +321,9 @@ func (s *SpaceStore) Update(ctx context.Context, space *types.Space) error {
return nil
}
// UpdateOptLock updates the space using the optimistic locking mechanism.
func (s *SpaceStore) UpdateOptLock(ctx context.Context,
// updateOptLock updates the space using the optimistic locking mechanism.
func (s *SpaceStore) updateOptLock(
ctx context.Context,
space *types.Space,
mutateFn func(space *types.Space) error,
) (*types.Space, error) {
@ -263,30 +343,129 @@ func (s *SpaceStore) UpdateOptLock(ctx context.Context,
return nil, err
}
space, err = s.Find(ctx, space.ID)
space, err = s.find(ctx, space.ID, space.Deleted)
if err != nil {
return nil, err
}
}
}
// Delete deletes a space.
func (s *SpaceStore) Delete(ctx context.Context, id int64) error {
const sqlQuery = `
DELETE FROM spaces
WHERE space_id = $1`
// UpdateOptLock updates the space using the optimistic locking mechanism.
func (s *SpaceStore) UpdateOptLock(
ctx context.Context,
space *types.Space,
mutateFn func(space *types.Space) error,
) (*types.Space, error) {
return s.updateOptLock(
ctx,
space,
func(r *types.Space) error {
if space.Deleted != nil {
return gitness_store.ErrResourceNotFound
}
return mutateFn(r)
},
)
}
// UpdateDeletedOptLock updates a soft deleted space using the optimistic locking mechanism.
func (s *SpaceStore) updateDeletedOptLock(
ctx context.Context,
space *types.Space,
mutateFn func(space *types.Space) error,
) (*types.Space, error) {
return s.updateOptLock(
ctx,
space,
func(r *types.Space) error {
if space.Deleted == nil {
return gitness_store.ErrResourceNotFound
}
return mutateFn(r)
},
)
}
// SoftDelete deletes a space softly.
func (s *SpaceStore) SoftDelete(
ctx context.Context,
space *types.Space,
deletedAt int64,
) error {
_, err := s.UpdateOptLock(ctx, space, func(s *types.Space) error {
s.Deleted = &deletedAt
return nil
})
if err != nil {
return err
}
return nil
}
// Purge deletes a space permanently.
func (s *SpaceStore) Purge(ctx context.Context, id int64, deletedAt *int64) error {
stmt := database.Builder.
Delete("spaces").
Where("space_id = ?", id)
if deletedAt != nil {
stmt = stmt.Where("space_deleted = ?", *deletedAt)
} else {
stmt = stmt.Where("space_deleted IS NULL")
}
sql, args, err := stmt.ToSql()
if err != nil {
return fmt.Errorf("failed to convert purge space query to sql: %w", err)
}
db := dbtx.GetAccessor(ctx, s.db)
if _, err := db.ExecContext(ctx, sqlQuery, id); err != nil {
return database.ProcessSQLErrorf(ctx, err, "The delete query failed")
_, err = db.ExecContext(ctx, sql, args...)
if err != nil {
return database.ProcessSQLErrorf(ctx, err, "the delete query failed")
}
return nil
}
// Restore restores a soft deleted space.
func (s *SpaceStore) Restore(
ctx context.Context,
space *types.Space,
newIdentifier *string,
newParentID *int64,
) (*types.Space, error) {
space, err := s.updateDeletedOptLock(ctx, space, func(s *types.Space) error {
s.Deleted = nil
if newParentID != nil {
s.ParentID = *newParentID
}
if newIdentifier != nil {
s.Identifier = *newIdentifier
}
return nil
})
if err != nil {
return nil, err
}
return space, nil
}
// Count the child spaces of a space.
func (s *SpaceStore) Count(ctx context.Context, id int64, opts *types.SpaceFilter) (int64, error) {
if opts.Recursive {
return s.countAll(ctx, id, opts)
}
return s.count(ctx, id, opts)
}
func (s *SpaceStore) count(
ctx context.Context,
id int64,
opts *types.SpaceFilter,
) (int64, error) {
stmt := database.Builder.
Select("count(*)").
From("spaces").
@ -296,6 +475,8 @@ func (s *SpaceStore) Count(ctx context.Context, id int64, opts *types.SpaceFilte
stmt = stmt.Where("LOWER(space_uid) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(opts.Query)))
}
stmt = s.applyQueryFilter(stmt, opts)
sql, args, err := stmt.ToSql()
if err != nil {
return 0, errors.Wrap(err, "Failed to convert query to sql")
@ -312,33 +493,70 @@ func (s *SpaceStore) Count(ctx context.Context, id int64, opts *types.SpaceFilte
return count, nil
}
func (s *SpaceStore) countAll(
ctx context.Context,
id int64,
opts *types.SpaceFilter,
) (int64, error) {
ctePrefix := `WITH RECURSIVE SpaceHierarchy AS (
SELECT space_id, space_parent_id, space_deleted, space_uid
FROM spaces
WHERE space_id = ?
UNION
SELECT s.space_id, s.space_parent_id, s.space_deleted, s.space_uid
FROM spaces s
JOIN SpaceHierarchy h ON s.space_parent_id = h.space_id
)`
db := dbtx.GetAccessor(ctx, s.db)
stmt := database.Builder.
Select("COUNT(*)").
Prefix(ctePrefix, id).
From("SpaceHierarchy h1").
Where("h1.space_id <> ?", id)
stmt = s.applyQueryFilter(stmt, opts)
sql, args, err := stmt.ToSql()
if err != nil {
return 0, errors.Wrap(err, "Failed to convert query to sql")
}
var count int64
if err = db.GetContext(ctx, &count, sql, args...); err != nil {
return 0, database.ProcessSQLErrorf(ctx, err, "failed to count sub spaces")
}
return count, nil
}
// List returns a list of spaces under the parent space.
func (s *SpaceStore) List(ctx context.Context, id int64, opts *types.SpaceFilter) ([]*types.Space, error) {
func (s *SpaceStore) List(
ctx context.Context,
id int64,
opts *types.SpaceFilter,
) ([]*types.Space, error) {
if opts.Recursive {
return s.listAll(ctx, id, opts)
}
return s.list(ctx, id, opts)
}
func (s *SpaceStore) list(
ctx context.Context,
id int64,
opts *types.SpaceFilter,
) ([]*types.Space, error) {
stmt := database.Builder.
Select(spaceColumns).
From("spaces").
Where("space_parent_id = ?", fmt.Sprint(id))
stmt = stmt.Limit(database.Limit(opts.Size))
stmt = stmt.Offset(database.Offset(opts.Page, opts.Size))
if opts.Query != "" {
stmt = stmt.Where("LOWER(space_uid) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(opts.Query)))
}
switch opts.Sort {
case enum.SpaceAttrUID, enum.SpaceAttrIdentifier, enum.SpaceAttrNone:
// NOTE: string concatenation is safe because the
// order attribute is an enum and is not user-defined,
// and is therefore not subject to injection attacks.
stmt = stmt.OrderBy("space_uid " + opts.Order.String())
//TODO: Postgres does not support COLLATE NOCASE for UTF8
// stmt = stmt.OrderBy("space_uid COLLATE NOCASE " + opts.Order.String())
case enum.SpaceAttrCreated:
stmt = stmt.OrderBy("space_created " + opts.Order.String())
case enum.SpaceAttrUpdated:
stmt = stmt.OrderBy("space_updated " + opts.Order.String())
}
stmt = s.applyQueryFilter(stmt, opts)
stmt = s.applySortFilter(stmt, opts)
sql, args, err := stmt.ToSql()
if err != nil {
@ -352,11 +570,121 @@ func (s *SpaceStore) List(ctx context.Context, id int64, opts *types.SpaceFilter
return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing custom list query")
}
return s.mapToSpaces(ctx, dst)
return s.mapToSpaces(ctx, s.db, dst)
}
func (s *SpaceStore) listAll(ctx context.Context,
id int64,
opts *types.SpaceFilter,
) ([]*types.Space, error) {
ctePrefix := `WITH RECURSIVE SpaceHierarchy AS (
SELECT *
FROM spaces
WHERE space_id = ?
UNION
SELECT s.*
FROM spaces s
JOIN SpaceHierarchy h ON s.space_parent_id = h.space_id
)`
db := dbtx.GetAccessor(ctx, s.db)
stmt := database.Builder.
Select(spaceColumns).
Prefix(ctePrefix, id).
From("SpaceHierarchy h1").
Where("h1.space_id <> ?", id)
stmt = s.applyQueryFilter(stmt, opts)
stmt = s.applySortFilter(stmt, opts)
sql, args, err := stmt.ToSql()
if err != nil {
return nil, errors.Wrap(err, "Failed to convert query to sql")
}
var dst []*space
if err = db.SelectContext(ctx, &dst, sql, args...); err != nil {
return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing custom list query")
}
return s.mapToSpaces(ctx, s.db, dst)
}
func (s *SpaceStore) applyQueryFilter(
stmt squirrel.SelectBuilder,
opts *types.SpaceFilter,
) squirrel.SelectBuilder {
if opts.Query != "" {
stmt = stmt.Where("LOWER(space_uid) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(opts.Query)))
}
if opts.DeletedBeforeOrAt != nil {
stmt = stmt.Where("space_deleted <= ?", opts.DeletedBeforeOrAt)
} else {
stmt = stmt.Where("space_deleted IS NULL")
}
return stmt
}
func getPathForDeletedSpace(
ctx context.Context,
sqlxdb *sqlx.DB,
id int64,
) (string, error) {
sqlQuery := spaceSelectBase + `
where space_id = $1`
path := ""
nextSpaceID := null.IntFrom(id)
db := dbtx.GetAccessor(ctx, sqlxdb)
dst := new(space)
for nextSpaceID.Valid {
err := db.GetContext(ctx, dst, sqlQuery, nextSpaceID.Int64)
if err != nil {
return "", fmt.Errorf("failed to find the space %d: %w", id, err)
}
path = paths.Concatenate(dst.Identifier, path)
nextSpaceID = dst.ParentID
}
return path, nil
}
func (s *SpaceStore) applySortFilter(
stmt squirrel.SelectBuilder,
opts *types.SpaceFilter,
) squirrel.SelectBuilder {
stmt = stmt.Limit(database.Limit(opts.Size))
stmt = stmt.Offset(database.Offset(opts.Page, opts.Size))
switch opts.Sort {
case enum.SpaceAttrUID, enum.SpaceAttrIdentifier, enum.SpaceAttrNone:
// NOTE: string concatenation is safe because the
// order attribute is an enum and is not user-defined,
// and is therefore not subject to injection attacks.
stmt = stmt.OrderBy("space_uid " + opts.Order.String())
//TODO: Postgres does not support COLLATE NOCASE for UTF8
// stmt = stmt.OrderBy("space_uid COLLATE NOCASE " + opts.Order.String())
case enum.SpaceAttrCreated:
stmt = stmt.OrderBy("space_created " + opts.Order.String())
case enum.SpaceAttrUpdated:
stmt = stmt.OrderBy("space_updated " + opts.Order.String())
case enum.SpaceAttrDeleted:
stmt = stmt.OrderBy("space_deleted " + opts.Order.String())
}
return stmt
}
func mapToSpace(
ctx context.Context,
sqlxdb *sqlx.DB,
spacePathStore store.SpacePathStore,
in *space,
) (*types.Space, error) {
@ -370,6 +698,7 @@ func mapToSpace(
Created: in.Created,
CreatedBy: in.CreatedBy,
Updated: in.Updated,
Deleted: in.Deleted.Ptr(),
}
// Only overwrite ParentID if it's not a root space
@ -378,7 +707,7 @@ func mapToSpace(
}
// backfill path
res.Path, err = getSpacePath(ctx, spacePathStore, in.ID)
res.Path, err = getSpacePath(ctx, sqlxdb, spacePathStore, in.ID)
if err != nil {
return nil, fmt.Errorf("failed to get primary path for space %d: %w", in.ID, err)
}
@ -388,10 +717,15 @@ func mapToSpace(
func getSpacePath(
ctx context.Context,
sqlxdb *sqlx.DB,
spacePathStore store.SpacePathStore,
spaceID int64,
) (string, error) {
spacePath, err := spacePathStore.FindPrimaryBySpaceID(ctx, spaceID)
// delete space will delete paths; generate the path if space is soft deleted.
if errors.Is(err, gitness_store.ErrResourceNotFound) {
return getPathForDeletedSpace(ctx, sqlxdb, spaceID)
}
if err != nil {
return "", fmt.Errorf("failed to get primary path for space %d: %w", spaceID, err)
}
@ -401,12 +735,13 @@ func getSpacePath(
func (s *SpaceStore) mapToSpaces(
ctx context.Context,
sqlxdb *sqlx.DB,
spaces []*space,
) ([]*types.Space, error) {
var err error
res := make([]*types.Space, len(spaces))
for i := range spaces {
res[i], err = mapToSpace(ctx, s.spacePathStore, spaces[i])
res[i], err = mapToSpace(ctx, sqlxdb, s.spacePathStore, spaces[i])
if err != nil {
return nil, err
}
@ -424,6 +759,7 @@ func mapToInternalSpace(s *types.Space) *space {
Created: s.Created,
CreatedBy: s.CreatedBy,
Updated: s.Updated,
Deleted: null.IntFromPtr(s.Deleted),
}
// Only overwrite ParentID if it's not a root space
@ -434,3 +770,27 @@ func mapToInternalSpace(s *types.Space) *space {
return res
}
// buildRecursiveSelectQueryUsingPath builds the recursive select query using path among active or soft deleted spaces.
func buildRecursiveSelectQueryUsingPath(segments []string, deletedAt int64) squirrel.SelectBuilder {
leaf := "s" + fmt.Sprint(len(segments)-1)
// add the current space (leaf)
stmt := database.Builder.
Select(leaf+".space_id").
From("spaces "+leaf).
Where(leaf+".space_uid = ? AND "+leaf+".space_deleted = ?", segments[len(segments)-1], deletedAt)
for i := len(segments) - 2; i >= 0; i-- {
parentAlias := "s" + fmt.Sprint(i)
alias := "s" + fmt.Sprint(i+1)
stmt = stmt.InnerJoin(fmt.Sprintf("spaces %s ON %s.space_id = %s.space_parent_id", parentAlias, parentAlias, alias)).
Where(parentAlias+".space_uid = ?", segments[i])
}
// add parent check for root
stmt = stmt.Where("s0.space_parent_id IS NULL")
return stmt
}

View File

@ -31,7 +31,10 @@ import (
var _ store.SpacePathStore = (*SpacePathStore)(nil)
// NewSpacePathStore returns a new SpacePathStore.
func NewSpacePathStore(db *sqlx.DB, pathTransformation store.SpacePathTransformation) *SpacePathStore {
func NewSpacePathStore(
db *sqlx.DB,
pathTransformation store.SpacePathTransformation,
) *SpacePathStore {
return &SpacePathStore{
db: db,
spacePathTransformation: pathTransformation,
@ -132,7 +135,7 @@ func (s *SpacePathStore) FindPrimaryBySpaceID(ctx context.Context, spaceID int64
return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find primary segment for %d", nextSpaceID.Int64)
}
path = paths.Concatinate(dst.Identifier, path)
path = paths.Concatenate(dst.Identifier, path)
nextSpaceID = dst.ParentID
}
@ -176,7 +179,7 @@ func (s *SpacePathStore) FindByPath(ctx context.Context, path string) (*types.Sp
)
}
originalPath = paths.Concatinate(originalPath, segment.Identifier)
originalPath = paths.Concatenate(originalPath, segment.Identifier)
parentID = segment.SpaceID
isPrimary = isPrimary && segment.IsPrimary.ValueOrZero()
}
@ -203,6 +206,31 @@ func (s *SpacePathStore) DeletePrimarySegment(ctx context.Context, spaceID int64
return nil
}
// DeletePathsAndDescendandPaths deletes all space paths reachable from spaceID including itself.
func (s *SpacePathStore) DeletePathsAndDescendandPaths(ctx context.Context, spaceID int64) error {
const sqlQuery = `WITH RECURSIVE DescendantPaths AS (
SELECT space_path_id, space_path_space_id, space_path_parent_id
FROM space_paths
WHERE space_path_space_id = $1
UNION
SELECT sp.space_path_id, sp.space_path_space_id, sp.space_path_parent_id
FROM space_paths sp
JOIN DescendantPaths dp ON sp.space_path_parent_id = dp.space_path_space_id
)
DELETE FROM space_paths
WHERE space_path_id IN (SELECT space_path_id FROM DescendantPaths);`
db := dbtx.GetAccessor(ctx, s.db)
if _, err := db.ExecContext(ctx, sqlQuery, spaceID); err != nil {
return database.ProcessSQLErrorf(ctx, err, "the delete query failed")
}
return nil
}
func (s *SpacePathStore) mapToInternalSpacePathSegment(p *types.SpacePathSegment) *spacePathSegment {
res := &spacePathSegment{
ID: p.ID,

View File

@ -107,8 +107,9 @@ func ProvideRepoStore(
db *sqlx.DB,
spacePathCache store.SpacePathCache,
spacePathStore store.SpacePathStore,
spaceStore store.SpaceStore,
) store.RepoStore {
return NewRepoStore(db, spacePathCache, spacePathStore)
return NewRepoStore(db, spacePathCache, spacePathStore, spaceStore)
}
// ProvideRuleStore provides a rule store.
@ -178,8 +179,9 @@ func ProvideMembershipStore(
db *sqlx.DB,
principalInfoCache store.PrincipalInfoCache,
spacePathStore store.SpacePathStore,
spaceStore store.SpaceStore,
) store.MembershipStore {
return NewMembershipStore(db, principalInfoCache, spacePathStore)
return NewMembershipStore(db, principalInfoCache, spacePathStore, spaceStore)
}
// ProvideTokenStore provides a token store.

View File

@ -108,7 +108,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
spaceStore := database.ProvideSpaceStore(db, spacePathCache, spacePathStore)
principalInfoView := database.ProvidePrincipalInfoView(db)
principalInfoCache := cache.ProvidePrincipalInfoCache(principalInfoView)
membershipStore := database.ProvideMembershipStore(db, principalInfoCache, spacePathStore)
membershipStore := database.ProvideMembershipStore(db, principalInfoCache, spacePathStore, spaceStore)
permissionCache := authz.ProvidePermissionCache(spaceStore, membershipStore)
authorizer := authz.ProvideAuthorizer(permissionCache, spaceStore)
principalUIDTransformation := store.ProvidePrincipalUIDTransformation()
@ -122,7 +122,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro
if err != nil {
return nil, err
}
repoStore := database.ProvideRepoStore(db, spacePathCache, spacePathStore)
repoStore := database.ProvideRepoStore(db, spacePathCache, spacePathStore, spaceStore)
pipelineStore := database.ProvidePipelineStore(db)
ruleStore := database.ProvideRuleStore(db, principalInfoCache)
protectionManager, err := protection.ProvideManager(ruleStore)

2
go.mod
View File

@ -7,7 +7,7 @@ replace github.com/docker/docker => github.com/docker/engine v17.12.0-ce-rc1.0.2
require (
cloud.google.com/go/storage v1.33.0
code.gitea.io/gitea v1.17.2
github.com/Masterminds/squirrel v1.5.1
github.com/Masterminds/squirrel v1.5.4
github.com/adrg/xdg v0.3.2
github.com/aws/aws-sdk-go v1.44.322
github.com/bmatcuk/doublestar/v4 v4.6.0

2
go.sum
View File

@ -124,6 +124,8 @@ github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuN
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/Masterminds/squirrel v1.5.1 h1:kWAKlLLJFxZG7N2E0mBMNWVp5AuUX+JUrnhFN74Eg+w=
github.com/Masterminds/squirrel v1.5.1/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=

View File

@ -27,6 +27,7 @@ const (
SpaceAttrIdentifier
SpaceAttrCreated
SpaceAttrUpdated
SpaceAttrDeleted
)
// ParseSpaceAttr parses the space attribute string
@ -42,6 +43,8 @@ func ParseSpaceAttr(s string) SpaceAttr {
return SpaceAttrCreated
case updated, updatedAt:
return SpaceAttrUpdated
case deleted, deletedAt:
return SpaceAttrDeleted
default:
return SpaceAttrNone
}
@ -59,6 +62,8 @@ func (a SpaceAttr) String() string {
return created
case SpaceAttrUpdated:
return updated
case SpaceAttrDeleted:
return deleted
case SpaceAttrNone:
return ""
default:

View File

@ -43,6 +43,7 @@ type Space struct {
CreatedBy int64 `json:"created_by"`
Created int64 `json:"created"`
Updated int64 `json:"updated"`
Deleted *int64 `json:"deleted,omitempty"`
}
// TODO [CODE-1363]: remove after identifier migration.
@ -60,9 +61,11 @@ func (s Space) MarshalJSON() ([]byte, error) {
// Stores spaces query parameters.
type SpaceFilter struct {
Page int `json:"page"`
Size int `json:"size"`
Query string `json:"query"`
Sort enum.SpaceAttr `json:"sort"`
Order enum.Order `json:"order"`
Page int `json:"page"`
Size int `json:"size"`
Query string `json:"query"`
Sort enum.SpaceAttr `json:"sort"`
Order enum.Order `json:"order"`
DeletedBeforeOrAt *int64 `json:"deleted_before_or_at,omitempty"`
Recursive bool `json:"recursive"`
}