From afd24f911b8d75f6e1e677468e1c6f40107fa10d Mon Sep 17 00:00:00 2001 From: Dhruv Dhruv Date: Mon, 15 Jul 2024 09:43:50 +0000 Subject: [PATCH] feat: [CDE-146]: API to list all gitspaces for a user. (#2209) * feat: [CDE-146]: Addressing review comments- restructuring method. * feat: [CDE-146]: API to list all gitspaces. * feat: [CDE-146]: API to list all gitspaces. * feat: [CDE-146]: API to list all gitspaces for a user. --- app/api/controller/gitspace/list_all.go | 168 ++++++++++++++++++ .../handler/gitspace/list_all_gitspaces.go | 36 ++++ app/api/openapi/gitspace.go | 14 ++ app/router/api.go | 1 + app/store/database.go | 3 + app/store/database/gitspace_config.go | 28 ++- 6 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 app/api/controller/gitspace/list_all.go create mode 100644 app/api/handler/gitspace/list_all_gitspaces.go diff --git a/app/api/controller/gitspace/list_all.go b/app/api/controller/gitspace/list_all.go new file mode 100644 index 000000000..39428d657 --- /dev/null +++ b/app/api/controller/gitspace/list_all.go @@ -0,0 +1,168 @@ +// 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 gitspace + +import ( + "context" + "errors" + "fmt" + + apiauth "github.com/harness/gitness/app/api/auth" + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/store/database/dbtx" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +func (c *Controller) ListAllGitspaces( // nolint:gocognit + ctx context.Context, + session *auth.Session, +) ([]*types.GitspaceConfig, error) { + var result []*types.GitspaceConfig + + err := c.tx.WithTx(ctx, func(ctx context.Context) (err error) { + allGitspaceConfigs, err := c.gitspaceConfigStore.ListAll(ctx, session.Principal.UID) + if err != nil { + return fmt.Errorf("failed to list gitspace configs: %w", err) + } + + var spacesMap = make(map[int64]string) + + for idx := 0; idx < len(allGitspaceConfigs); idx++ { + if spacesMap[allGitspaceConfigs[idx].SpaceID] == "" { + space, findSpaceErr := c.spaceStore.Find(ctx, allGitspaceConfigs[idx].SpaceID) + if findSpaceErr != nil { + return fmt.Errorf( + "failed to find space for ID %d: %w", allGitspaceConfigs[idx].SpaceID, findSpaceErr) + } + spacesMap[allGitspaceConfigs[idx].SpaceID] = space.Path + } + + allGitspaceConfigs[idx].SpacePath = spacesMap[allGitspaceConfigs[idx].SpaceID] + } + + authorizedSpaceIDs, err := c.getAuthorizedSpaces(ctx, session, spacesMap) + if err != nil { + return err + } + + finalGitspaceConfigs, err := c.filterAndPopulateInstanceDetails(ctx, allGitspaceConfigs, authorizedSpaceIDs) + if err != nil { + return err + } + + result = finalGitspaceConfigs + + return nil + }, dbtx.TxDefaultReadOnly) + + if err != nil { + return nil, err + } + + return result, nil +} + +func (c *Controller) filterAndPopulateInstanceDetails( + ctx context.Context, + allGitspaceConfigs []*types.GitspaceConfig, + authorizedSpaceIDs map[int64]bool, +) ([]*types.GitspaceConfig, error) { + authorizedGitspaceConfigs := c.getAuthorizedGitspaceConfigs(allGitspaceConfigs, authorizedSpaceIDs) + + gitspaceInstancesMap, err := c.getLatestInstanceMap(ctx, authorizedGitspaceConfigs) + if err != nil { + return nil, err + } + + var result []*types.GitspaceConfig + + for _, gitspaceConfig := range authorizedGitspaceConfigs { + instance := gitspaceInstancesMap[gitspaceConfig.ID] + + gitspaceConfig.GitspaceInstance = instance + + if instance != nil { + gitspaceStateType, stateErr := enum.GetGitspaceStateFromInstance(instance.State) + if stateErr != nil { + return nil, stateErr + } + + gitspaceConfig.State = gitspaceStateType + + instance.SpacePath = gitspaceConfig.SpacePath + } else { + gitspaceConfig.State = enum.GitspaceStateUninitialized + } + + result = append(result, gitspaceConfig) + } + return result, nil +} + +func (c *Controller) getAuthorizedGitspaceConfigs( + allGitspaceConfigs []*types.GitspaceConfig, + authorizedSpaceIDs map[int64]bool, +) []*types.GitspaceConfig { + var authorizedGitspaceConfigs = make([]*types.GitspaceConfig, 0) + for idx := 0; idx < len(allGitspaceConfigs); idx++ { + if authorizedSpaceIDs[allGitspaceConfigs[idx].SpaceID] { + authorizedGitspaceConfigs = append(authorizedGitspaceConfigs, allGitspaceConfigs[idx]) + } + } + return authorizedGitspaceConfigs +} + +func (c *Controller) getAuthorizedSpaces( + ctx context.Context, + session *auth.Session, + spacesMap map[int64]string, +) (map[int64]bool, error) { + var authorizedSpaceIDs = make(map[int64]bool, 0) + + for spaceID, spacePath := range spacesMap { + authErr := apiauth.CheckGitspace(ctx, c.authorizer, session, spacePath, "", enum.PermissionGitspaceView) + if authErr != nil && !errors.Is(authErr, apiauth.ErrNotAuthorized) { + return nil, fmt.Errorf("failed to check gitspace auth for space ID %d: %w", spaceID, authErr) + } + + authorizedSpaceIDs[spaceID] = true + } + + return authorizedSpaceIDs, nil +} + +func (c *Controller) getLatestInstanceMap( + ctx context.Context, + authorizedGitspaceConfigs []*types.GitspaceConfig, +) (map[int64]*types.GitspaceInstance, error) { + var authorizedConfigIDs = make([]int64, 0) + for _, config := range authorizedGitspaceConfigs { + authorizedConfigIDs = append(authorizedConfigIDs, config.ID) + } + + var gitspaceInstances, err = c.gitspaceInstanceStore.FindAllLatestByGitspaceConfigID(ctx, authorizedConfigIDs) + if err != nil { + return nil, err + } + + var gitspaceInstancesMap = make(map[int64]*types.GitspaceInstance) + + for _, gitspaceEntry := range gitspaceInstances { + gitspaceInstancesMap[gitspaceEntry.GitSpaceConfigID] = gitspaceEntry + } + + return gitspaceInstancesMap, nil +} diff --git a/app/api/handler/gitspace/list_all_gitspaces.go b/app/api/handler/gitspace/list_all_gitspaces.go new file mode 100644 index 000000000..5247fbee3 --- /dev/null +++ b/app/api/handler/gitspace/list_all_gitspaces.go @@ -0,0 +1,36 @@ +// 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 gitspace + +import ( + "net/http" + + "github.com/harness/gitness/app/api/controller/gitspace" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" +) + +func HandleListAllGitspaces(gitspaceCtrl *gitspace.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + gitspaces, err := gitspaceCtrl.ListAllGitspaces(ctx, session) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + render.JSON(w, http.StatusOK, gitspaces) + } +} diff --git a/app/api/openapi/gitspace.go b/app/api/openapi/gitspace.go index 672ca9f96..950cf45d2 100644 --- a/app/api/openapi/gitspace.go +++ b/app/api/openapi/gitspace.go @@ -58,6 +58,9 @@ type gitspaceEventsListRequest struct { paginationRequest } +type gitspacesListAllRequest struct { +} + func gitspaceOperations(reflector *openapi3.Reflector) { opCreate := openapi3.Operation{} opCreate.WithTags("gitspaces") @@ -155,4 +158,15 @@ func gitspaceOperations(reflector *openapi3.Reflector) { _ = reflector.SetJSONResponse(&opRepoLookup, new(usererror.Error), http.StatusUnauthorized) _ = reflector.SetJSONResponse(&opRepoLookup, new(usererror.Error), http.StatusForbidden) _ = reflector.Spec.AddOperation(http.MethodPost, "/gitspaces/lookup-repo", opCreate) + + opListAll := openapi3.Operation{} + opListAll.WithTags("gitspaces") + opListAll.WithSummary("List all gitspaces") + opListAll.WithMapOfAnything(map[string]interface{}{"operationId": "listAllGitspaces"}) + _ = reflector.SetRequest(&opListAll, new(gitspacesListAllRequest), http.MethodGet) + _ = reflector.SetJSONResponse(&opListAll, new([]*types.GitspaceConfig), http.StatusOK) + _ = reflector.SetJSONResponse(&opListAll, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opListAll, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opListAll, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodGet, "/gitspaces", opListAll) } diff --git a/app/router/api.go b/app/router/api.go index df7c5fc7e..acf6040c9 100644 --- a/app/router/api.go +++ b/app/router/api.go @@ -711,6 +711,7 @@ func setupGitspaces(r chi.Router, gitspacesCtrl *gitspace.Controller) { r.Route("/gitspaces", func(r chi.Router) { r.Post("/lookup-repo", handlergitspace.HandleLookupRepo(gitspacesCtrl)) r.Post("/", handlergitspace.HandleCreateConfig(gitspacesCtrl)) + r.Get("/", handlergitspace.HandleListAllGitspaces(gitspacesCtrl)) r.Route(fmt.Sprintf("/{%s}", request.PathParamGitspaceIdentifier), func(r chi.Router) { r.Get("/", handlergitspace.HandleFind(gitspacesCtrl)) r.Post("/actions", handlergitspace.HandleAction(gitspacesCtrl)) diff --git a/app/store/database.go b/app/store/database.go index 0b08d0f7b..3eb1b9ff9 100644 --- a/app/store/database.go +++ b/app/store/database.go @@ -579,6 +579,9 @@ type ( // Count the number of gitspace configs in a space matching the given filter. Count(ctx context.Context, filter *types.GitspaceFilter) (int64, error) + + // ListAll lists all the gitspace configs present for a user in the given spaces in the datastore. + ListAll(ctx context.Context, userUID string) ([]*types.GitspaceConfig, error) } GitspaceInstanceStore interface { diff --git a/app/store/database/gitspace_config.go b/app/store/database/gitspace_config.go index 0b20c74b9..7983c8dc0 100644 --- a/app/store/database/gitspace_config.go +++ b/app/store/database/gitspace_config.go @@ -251,6 +251,28 @@ func (s gitspaceConfigStore) List(ctx context.Context, filter *types.GitspaceFil return s.mapToGitspaceConfigs(ctx, dst) } +func (s gitspaceConfigStore) ListAll( + ctx context.Context, + userUID string, +) ([]*types.GitspaceConfig, error) { + stmt := database.Builder. + Select(gitspaceConfigSelectColumns). + From(gitspaceConfigsTable). + Where(squirrel.Eq{"gconf_is_deleted": false}). + Where(squirrel.Eq{"gconf_user_uid": userUID}) + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + db := dbtx.GetAccessor(ctx, s.db) + var dst []*gitspaceConfig + if err = db.SelectContext(ctx, &dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing custom list query") + } + return s.mapToGitspaceConfigs(ctx, dst) +} + func (s *gitspaceConfigStore) mapToGitspaceConfig( _ context.Context, in *gitspaceConfig, @@ -276,8 +298,10 @@ func (s *gitspaceConfigStore) mapToGitspaceConfig( return res, nil } -func (s *gitspaceConfigStore) mapToGitspaceConfigs(ctx context.Context, - configs []*gitspaceConfig) ([]*types.GitspaceConfig, error) { +func (s *gitspaceConfigStore) mapToGitspaceConfigs( + ctx context.Context, + configs []*gitspaceConfig, +) ([]*types.GitspaceConfig, error) { var err error res := make([]*types.GitspaceConfig, len(configs)) for i := range configs {