mirror of https://github.com/harness/drone.git
Support Space Soft Delete, Restore, and Purge (#1076)
parent
cda2de9606
commit
5d0d28e4a3
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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"})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
2
go.mod
|
@ -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
2
go.sum
|
@ -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=
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue