From 81fcd524c83c1cbfcb2e32f4255aaede889621d5 Mon Sep 17 00:00:00 2001 From: Darko Draskovic Date: Thu, 25 Jul 2024 14:57:56 +0000 Subject: [PATCH] Add label feature (#2073) * Address PR comments * Add list label from ancestor scopes query param * Add label feature --- app/api/controller/pullreq/controller.go | 4 + app/api/controller/pullreq/label_assign.go | 55 +++ app/api/controller/pullreq/label_list.go | 51 +++ app/api/controller/pullreq/label_unassign.go | 49 +++ app/api/controller/pullreq/wire.go | 10 +- app/api/controller/repo/controller.go | 4 + app/api/controller/repo/label_define.go | 49 +++ app/api/controller/repo/label_delete.go | 42 ++ app/api/controller/repo/label_list.go | 44 ++ app/api/controller/repo/label_save.go | 49 +++ app/api/controller/repo/label_update.go | 49 +++ app/api/controller/repo/label_value_define.go | 65 +++ app/api/controller/repo/label_value_delete.go | 43 ++ app/api/controller/repo/label_value_list.go | 45 ++ app/api/controller/repo/label_value_update.go | 52 +++ app/api/controller/repo/wire.go | 4 +- app/api/controller/space/controller.go | 95 +++-- .../controller/space/import_repositories.go | 10 +- app/api/controller/space/label_define.go | 49 +++ app/api/controller/space/label_delete.go | 42 ++ app/api/controller/space/label_list.go | 44 ++ app/api/controller/space/label_save.go | 49 +++ app/api/controller/space/label_update.go | 49 +++ .../controller/space/label_value_define.go | 55 +++ .../controller/space/label_value_delete.go | 43 ++ app/api/controller/space/label_value_list.go | 45 ++ .../controller/space/label_value_update.go | 52 +++ app/api/controller/space/wire.go | 7 +- app/api/controller/upload/controller.go | 4 +- app/api/handler/pullreq/label_assign.go | 60 +++ app/api/handler/pullreq/label_list.go | 56 +++ app/api/handler/pullreq/label_unassign.go | 56 +++ app/api/handler/repo/label_define.go | 53 +++ app/api/handler/repo/label_delete.go | 50 +++ app/api/handler/repo/label_list.go | 50 +++ app/api/handler/repo/label_save.go | 53 +++ app/api/handler/repo/label_update.go | 59 +++ app/api/handler/repo/label_value_define.go | 59 +++ app/api/handler/repo/label_value_delete.go | 56 +++ app/api/handler/repo/label_value_list.go | 52 +++ app/api/handler/repo/label_value_update.go | 66 +++ app/api/handler/space/label_define.go | 53 +++ app/api/handler/space/label_delete.go | 50 +++ app/api/handler/space/label_list.go | 50 +++ app/api/handler/space/label_save.go | 53 +++ app/api/handler/space/label_update.go | 59 +++ app/api/handler/space/label_value_define.go | 59 +++ app/api/handler/space/label_value_delete.go | 56 +++ app/api/handler/space/label_value_list.go | 53 +++ app/api/handler/space/label_value_update.go | 66 +++ app/api/openapi/pullreq.go | 61 +++ app/api/openapi/repo.go | 194 +++++++++ app/api/openapi/space.go | 154 +++++++ app/api/request/common.go | 13 + app/api/request/label.go | 67 +++ app/api/request/util.go | 19 + app/api/usererror/translate.go | 4 +- app/router/api.go | 55 ++- app/services/label/label.go | 305 ++++++++++++++ app/services/label/label_pullreq.go | 274 ++++++++++++ app/services/label/label_value.go | 167 ++++++++ app/services/label/service.go | 44 ++ app/services/label/wire.go | 36 ++ app/store/database.go | 112 +++++ app/store/database/label.go | 398 ++++++++++++++++++ app/store/database/label_pullreq.go | 196 +++++++++ app/store/database/label_value.go | 358 ++++++++++++++++ .../0060_create_table_labels.down.sql | 3 + .../postgres/0060_create_table_labels.up.sql | 78 ++++ .../sqlite/0060_create_table_labels.down.sql | 3 + .../sqlite/0060_create_table_labels.up.sql | 78 ++++ app/store/database/space.go | 51 ++- app/store/database/wire.go | 18 + cmd/gitness/wire.go | 2 + cmd/gitness/wire_gen.go | 11 +- types/enum/label.go | 77 ++++ types/label.go | 292 +++++++++++++ 77 files changed, 5330 insertions(+), 68 deletions(-) create mode 100644 app/api/controller/pullreq/label_assign.go create mode 100644 app/api/controller/pullreq/label_list.go create mode 100644 app/api/controller/pullreq/label_unassign.go create mode 100644 app/api/controller/repo/label_define.go create mode 100644 app/api/controller/repo/label_delete.go create mode 100644 app/api/controller/repo/label_list.go create mode 100644 app/api/controller/repo/label_save.go create mode 100644 app/api/controller/repo/label_update.go create mode 100644 app/api/controller/repo/label_value_define.go create mode 100644 app/api/controller/repo/label_value_delete.go create mode 100644 app/api/controller/repo/label_value_list.go create mode 100644 app/api/controller/repo/label_value_update.go create mode 100644 app/api/controller/space/label_define.go create mode 100644 app/api/controller/space/label_delete.go create mode 100644 app/api/controller/space/label_list.go create mode 100644 app/api/controller/space/label_save.go create mode 100644 app/api/controller/space/label_update.go create mode 100644 app/api/controller/space/label_value_define.go create mode 100644 app/api/controller/space/label_value_delete.go create mode 100644 app/api/controller/space/label_value_list.go create mode 100644 app/api/controller/space/label_value_update.go create mode 100644 app/api/handler/pullreq/label_assign.go create mode 100644 app/api/handler/pullreq/label_list.go create mode 100644 app/api/handler/pullreq/label_unassign.go create mode 100644 app/api/handler/repo/label_define.go create mode 100644 app/api/handler/repo/label_delete.go create mode 100644 app/api/handler/repo/label_list.go create mode 100644 app/api/handler/repo/label_save.go create mode 100644 app/api/handler/repo/label_update.go create mode 100644 app/api/handler/repo/label_value_define.go create mode 100644 app/api/handler/repo/label_value_delete.go create mode 100644 app/api/handler/repo/label_value_list.go create mode 100644 app/api/handler/repo/label_value_update.go create mode 100644 app/api/handler/space/label_define.go create mode 100644 app/api/handler/space/label_delete.go create mode 100644 app/api/handler/space/label_list.go create mode 100644 app/api/handler/space/label_save.go create mode 100644 app/api/handler/space/label_update.go create mode 100644 app/api/handler/space/label_value_define.go create mode 100644 app/api/handler/space/label_value_delete.go create mode 100644 app/api/handler/space/label_value_list.go create mode 100644 app/api/handler/space/label_value_update.go create mode 100644 app/api/request/label.go create mode 100644 app/services/label/label.go create mode 100644 app/services/label/label_pullreq.go create mode 100644 app/services/label/label_value.go create mode 100644 app/services/label/service.go create mode 100644 app/services/label/wire.go create mode 100644 app/store/database/label.go create mode 100644 app/store/database/label_pullreq.go create mode 100644 app/store/database/label_value.go create mode 100644 app/store/database/migrate/postgres/0060_create_table_labels.down.sql create mode 100644 app/store/database/migrate/postgres/0060_create_table_labels.up.sql create mode 100644 app/store/database/migrate/sqlite/0060_create_table_labels.down.sql create mode 100644 app/store/database/migrate/sqlite/0060_create_table_labels.up.sql create mode 100644 types/enum/label.go create mode 100644 types/label.go diff --git a/app/api/controller/pullreq/controller.go b/app/api/controller/pullreq/controller.go index b53ec605f..3a56e75ca 100644 --- a/app/api/controller/pullreq/controller.go +++ b/app/api/controller/pullreq/controller.go @@ -26,6 +26,7 @@ import ( "github.com/harness/gitness/app/services/codecomments" "github.com/harness/gitness/app/services/codeowners" "github.com/harness/gitness/app/services/importer" + "github.com/harness/gitness/app/services/label" locker "github.com/harness/gitness/app/services/locker" "github.com/harness/gitness/app/services/protection" "github.com/harness/gitness/app/services/pullreq" @@ -65,6 +66,7 @@ type Controller struct { codeOwners *codeowners.Service locker *locker.Locker importer *importer.PullReq + labelSvc *label.Service } func NewController( @@ -91,6 +93,7 @@ func NewController( codeowners *codeowners.Service, locker *locker.Locker, importer *importer.PullReq, + labelSvc *label.Service, ) *Controller { return &Controller{ tx: tx, @@ -116,6 +119,7 @@ func NewController( codeOwners: codeowners, locker: locker, importer: importer, + labelSvc: labelSvc, } } diff --git a/app/api/controller/pullreq/label_assign.go b/app/api/controller/pullreq/label_assign.go new file mode 100644 index 000000000..279c0b5f9 --- /dev/null +++ b/app/api/controller/pullreq/label_assign.go @@ -0,0 +1,55 @@ +// 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 pullreq + +import ( + "context" + "fmt" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// AssignLabel assigns a label to a pull request . +func (c *Controller) AssignLabel( + ctx context.Context, + session *auth.Session, + repoRef string, + pullreqNum int64, + in *types.PullReqCreateInput, +) (*types.PullReqLabel, error) { + repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoPush) + if err != nil { + return nil, fmt.Errorf("failed to acquire access to target repo: %w", err) + } + + if err := in.Validate(); err != nil { + return nil, fmt.Errorf("failed to validate input: %w", err) + } + + pullreq, err := c.pullreqStore.FindByNumber(ctx, repo.ID, pullreqNum) + if err != nil { + return nil, fmt.Errorf("failed to find pullreq: %w", err) + } + + pullreqLabel, err := c.labelSvc.AssignToPullReq( + ctx, session.Principal.ID, pullreq.ID, repo.ID, repo.ParentID, in) + if err != nil { + return nil, fmt.Errorf("failed to create pullreq label: %w", err) + } + + return pullreqLabel, nil +} diff --git a/app/api/controller/pullreq/label_list.go b/app/api/controller/pullreq/label_list.go new file mode 100644 index 000000000..40478c2b4 --- /dev/null +++ b/app/api/controller/pullreq/label_list.go @@ -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 pullreq + +import ( + "context" + "fmt" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// ListLabels list labels assigned to a specified pullreq. +func (c *Controller) ListLabels( + ctx context.Context, + session *auth.Session, + repoRef string, + pullreqNum int64, + filter *types.AssignableLabelFilter, +) (*types.ScopesLabels, error) { + repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView) + if err != nil { + return nil, fmt.Errorf("failed to acquire access to target repo: %w", err) + } + + pullreq, err := c.pullreqStore.FindByNumber(ctx, repo.ID, pullreqNum) + if err != nil { + return nil, fmt.Errorf("failed to find pullreq: %w", err) + } + + scopeLabelsMap, err := c.labelSvc.ListPullReqLabels( + ctx, repo, repo.ParentID, pullreq.ID, filter) + if err != nil { + return nil, fmt.Errorf("failed to list pullreq labels: %w", err) + } + + return scopeLabelsMap, nil +} diff --git a/app/api/controller/pullreq/label_unassign.go b/app/api/controller/pullreq/label_unassign.go new file mode 100644 index 000000000..26b8c5e3b --- /dev/null +++ b/app/api/controller/pullreq/label_unassign.go @@ -0,0 +1,49 @@ +// 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 pullreq + +import ( + "context" + "fmt" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types/enum" +) + +// UnassignLabel removes a label from a pull request. +func (c *Controller) UnassignLabel( + ctx context.Context, + session *auth.Session, + repoRef string, + pullreqNum int64, + labelID int64, +) error { + repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoPush) + if err != nil { + return fmt.Errorf("failed to acquire access to target repo: %w", err) + } + + pullreq, err := c.pullreqStore.FindByNumber(ctx, repo.ID, pullreqNum) + if err != nil { + return fmt.Errorf("failed to find pullreq: %w", err) + } + + if err := c.labelSvc.UnassignFromPullReq( + ctx, repo.ID, repo.ParentID, pullreq.ID, labelID); err != nil { + return fmt.Errorf("failed to delete pullreq label: %w", err) + } + + return nil +} diff --git a/app/api/controller/pullreq/wire.go b/app/api/controller/pullreq/wire.go index dca5631c8..30e6327cc 100644 --- a/app/api/controller/pullreq/wire.go +++ b/app/api/controller/pullreq/wire.go @@ -20,6 +20,7 @@ import ( "github.com/harness/gitness/app/services/codecomments" "github.com/harness/gitness/app/services/codeowners" "github.com/harness/gitness/app/services/importer" + "github.com/harness/gitness/app/services/label" "github.com/harness/gitness/app/services/locker" "github.com/harness/gitness/app/services/protection" "github.com/harness/gitness/app/services/pullreq" @@ -41,21 +42,24 @@ func ProvideController(tx dbtx.Transactor, urlProvider url.Provider, authorizer pullReqStore store.PullReqStore, pullReqActivityStore store.PullReqActivityStore, codeCommentsView store.CodeCommentView, pullReqReviewStore store.PullReqReviewStore, pullReqReviewerStore store.PullReqReviewerStore, - repoStore store.RepoStore, principalStore store.PrincipalStore, principalInfoCache store.PrincipalInfoCache, + repoStore store.RepoStore, + principalStore store.PrincipalStore, principalInfoCache store.PrincipalInfoCache, fileViewStore store.PullReqFileViewStore, membershipStore store.MembershipStore, checkStore store.CheckStore, rpcClient git.Interface, eventReporter *pullreqevents.Reporter, codeCommentMigrator *codecomments.Migrator, pullreqService *pullreq.Service, ruleManager *protection.Manager, sseStreamer sse.Streamer, codeOwners *codeowners.Service, locker *locker.Locker, importer *importer.PullReq, + labelSvc *label.Service, ) *Controller { return NewController(tx, urlProvider, authorizer, pullReqStore, pullReqActivityStore, codeCommentsView, pullReqReviewStore, pullReqReviewerStore, - repoStore, principalStore, principalInfoCache, + repoStore, + principalStore, principalInfoCache, fileViewStore, membershipStore, checkStore, rpcClient, eventReporter, codeCommentMigrator, - pullreqService, ruleManager, sseStreamer, codeOwners, locker, importer) + pullreqService, ruleManager, sseStreamer, codeOwners, locker, importer, labelSvc) } diff --git a/app/api/controller/repo/controller.go b/app/api/controller/repo/controller.go index 200e533be..6217958a9 100644 --- a/app/api/controller/repo/controller.go +++ b/app/api/controller/repo/controller.go @@ -30,6 +30,7 @@ import ( "github.com/harness/gitness/app/services/codeowners" "github.com/harness/gitness/app/services/importer" "github.com/harness/gitness/app/services/keywordsearch" + "github.com/harness/gitness/app/services/label" "github.com/harness/gitness/app/services/locker" "github.com/harness/gitness/app/services/protection" "github.com/harness/gitness/app/services/publicaccess" @@ -92,6 +93,7 @@ type Controller struct { identifierCheck check.RepoIdentifier repoCheck Check publicAccess publicaccess.Service + labelSvc *label.Service } func NewController( @@ -119,6 +121,7 @@ func NewController( identifierCheck check.RepoIdentifier, repoCheck Check, publicAccess publicaccess.Service, + labelSvc *label.Service, ) *Controller { return &Controller{ defaultBranch: config.Git.DefaultBranch, @@ -145,6 +148,7 @@ func NewController( identifierCheck: identifierCheck, repoCheck: repoCheck, publicAccess: publicAccess, + labelSvc: labelSvc, } } diff --git a/app/api/controller/repo/label_define.go b/app/api/controller/repo/label_define.go new file mode 100644 index 000000000..e6e0d79e7 --- /dev/null +++ b/app/api/controller/repo/label_define.go @@ -0,0 +1,49 @@ +// 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 repo + +import ( + "context" + "fmt" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// DefineLabel defines a new label for the specified repository. +func (c *Controller) DefineLabel( + ctx context.Context, + session *auth.Session, + repoRef string, + in *types.DefineLabelInput, +) (*types.Label, error) { + repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoEdit) + if err != nil { + return nil, fmt.Errorf("failed to acquire access to repo: %w", err) + } + + if err := in.Validate(); err != nil { + return nil, fmt.Errorf("failed to validate input: %w", err) + } + + label, err := c.labelSvc.Define( + ctx, session.Principal.ID, nil, &repo.ID, in) + if err != nil { + return nil, fmt.Errorf("failed to create repo label: %w", err) + } + + return label, nil +} diff --git a/app/api/controller/repo/label_delete.go b/app/api/controller/repo/label_delete.go new file mode 100644 index 000000000..e7923e39e --- /dev/null +++ b/app/api/controller/repo/label_delete.go @@ -0,0 +1,42 @@ +// 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 repo + +import ( + "context" + "fmt" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types/enum" +) + +// DeleteLabel deletes a label for the specified repository. +func (c *Controller) DeleteLabel( + ctx context.Context, + session *auth.Session, + repoRef string, + key string, +) error { + repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoEdit) + if err != nil { + return fmt.Errorf("failed to acquire access to repo: %w", err) + } + + if err := c.labelSvc.Delete(ctx, nil, &repo.ID, key); err != nil { + return fmt.Errorf("failed to delete repo label: %w", err) + } + + return nil +} diff --git a/app/api/controller/repo/label_list.go b/app/api/controller/repo/label_list.go new file mode 100644 index 000000000..7e8eda6de --- /dev/null +++ b/app/api/controller/repo/label_list.go @@ -0,0 +1,44 @@ +// 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 repo + +import ( + "context" + "fmt" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// ListLabels lists all labels defined for the specified repository. +func (c *Controller) ListLabels( + ctx context.Context, + session *auth.Session, + repoRef string, + filter *types.LabelFilter, +) ([]*types.Label, error) { + repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView) + if err != nil { + return nil, fmt.Errorf("failed to acquire access to repo: %w", err) + } + + labels, err := c.labelSvc.List(ctx, &repo.ParentID, &repo.ID, filter) + if err != nil { + return nil, fmt.Errorf("failed to list repo labels: %w", err) + } + + return labels, nil +} diff --git a/app/api/controller/repo/label_save.go b/app/api/controller/repo/label_save.go new file mode 100644 index 000000000..f686f3712 --- /dev/null +++ b/app/api/controller/repo/label_save.go @@ -0,0 +1,49 @@ +// 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 repo + +import ( + "context" + "fmt" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// SaveLabel creates or updates a label and possibly label values for the specified repository. +func (c *Controller) SaveLabel( + ctx context.Context, + session *auth.Session, + repoRef string, + in *types.SaveInput, +) (*types.LabelWithValues, error) { + repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoEdit) + if err != nil { + return nil, fmt.Errorf("failed to acquire access to repo: %w", err) + } + + if err := in.Validate(); err != nil { + return nil, fmt.Errorf("failed to validate input: %w", err) + } + + labelWithValues, err := c.labelSvc.Save( + ctx, session.Principal.ID, nil, &repo.ID, in) + if err != nil { + return nil, fmt.Errorf("failed to save label: %w", err) + } + + return labelWithValues, nil +} diff --git a/app/api/controller/repo/label_update.go b/app/api/controller/repo/label_update.go new file mode 100644 index 000000000..227ca3bc0 --- /dev/null +++ b/app/api/controller/repo/label_update.go @@ -0,0 +1,49 @@ +// 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 repo + +import ( + "context" + "fmt" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// UpdateLabel updates a label for the specified repository. +func (c *Controller) UpdateLabel( + ctx context.Context, + session *auth.Session, + repoRef string, + key string, + in *types.UpdateLabelInput, +) (*types.Label, error) { + repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoEdit) + if err != nil { + return nil, fmt.Errorf("failed to acquire access to repo: %w", err) + } + + if err := in.Validate(); err != nil { + return nil, fmt.Errorf("failed to validate input: %w", err) + } + + label, err := c.labelSvc.Update(ctx, session.Principal.ID, nil, &repo.ID, key, in) + if err != nil { + return nil, fmt.Errorf("failed to update repo label: %w", err) + } + + return label, nil +} diff --git a/app/api/controller/repo/label_value_define.go b/app/api/controller/repo/label_value_define.go new file mode 100644 index 000000000..7e801c6a2 --- /dev/null +++ b/app/api/controller/repo/label_value_define.go @@ -0,0 +1,65 @@ +// 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 repo + +import ( + "context" + "fmt" + + 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" +) + +// DefineLabelValue defines a new label value for the specified repository. +func (c *Controller) DefineLabelValue( + ctx context.Context, + session *auth.Session, + repoRef string, + key string, + in *types.DefineValueInput, +) (*types.LabelValue, error) { + repo, err := GetRepo(ctx, c.repoStore, repoRef, []enum.RepoState{enum.RepoStateActive}) + if err != nil { + return nil, fmt.Errorf("failed to acquire access to repo: %w", err) + } + + if err := in.Validate(); err != nil { + return nil, fmt.Errorf("failed to validate input: %w", err) + } + + label, err := c.labelSvc.Find(ctx, nil, &repo.ID, key) + if err != nil { + return nil, fmt.Errorf("failed to find repo label: %w", err) + } + + permission := enum.PermissionRepoEdit + if label.Type == enum.LabelTypeDynamic { + permission = enum.PermissionRepoPush + } + + if err = apiauth.CheckRepo( + ctx, c.authorizer, session, repo, permission); err != nil { + return nil, fmt.Errorf("access check failed: %w", err) + } + + value, err := c.labelSvc.DefineValue(ctx, session.Principal.ID, label.ID, in) + if err != nil { + return nil, fmt.Errorf("failed to create repo label value: %w", err) + } + + return value, nil +} diff --git a/app/api/controller/repo/label_value_delete.go b/app/api/controller/repo/label_value_delete.go new file mode 100644 index 000000000..e145c1c0a --- /dev/null +++ b/app/api/controller/repo/label_value_delete.go @@ -0,0 +1,43 @@ +// 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 repo + +import ( + "context" + "fmt" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types/enum" +) + +// DeleteLabelValue deletes a label value for the specified repository. +func (c *Controller) DeleteLabelValue( + ctx context.Context, + session *auth.Session, + repoRef string, + key string, + value string, +) error { + repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoEdit) + if err != nil { + return fmt.Errorf("failed to acquire access to repo: %w", err) + } + + if err := c.labelSvc.DeleteValue(ctx, nil, &repo.ID, key, value); err != nil { + return fmt.Errorf("failed to delete repo label value: %w", err) + } + + return nil +} diff --git a/app/api/controller/repo/label_value_list.go b/app/api/controller/repo/label_value_list.go new file mode 100644 index 000000000..f98dadc32 --- /dev/null +++ b/app/api/controller/repo/label_value_list.go @@ -0,0 +1,45 @@ +// 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 repo + +import ( + "context" + "fmt" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// ListLabelValues lists all label values defined for the specified repository. +func (c *Controller) ListLabelValues( + ctx context.Context, + session *auth.Session, + repoRef string, + key string, + filter *types.ListQueryFilter, +) ([]*types.LabelValue, error) { + repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoView) + if err != nil { + return nil, fmt.Errorf("failed to acquire access to repo: %w", err) + } + + values, err := c.labelSvc.ListValues(ctx, nil, &repo.ID, key, filter) + if err != nil { + return nil, fmt.Errorf("failed to list repo label values: %w", err) + } + + return values, nil +} diff --git a/app/api/controller/repo/label_value_update.go b/app/api/controller/repo/label_value_update.go new file mode 100644 index 000000000..e45b44378 --- /dev/null +++ b/app/api/controller/repo/label_value_update.go @@ -0,0 +1,52 @@ +// 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 repo + +import ( + "context" + "fmt" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// UpdateLabelValue updates a label value for the specified label and repository. +func (c *Controller) UpdateLabelValue( + ctx context.Context, + session *auth.Session, + repoRef string, + key string, + value string, + in *types.UpdateValueInput, +) (*types.LabelValue, error) { + repo, err := c.getRepoCheckAccess(ctx, session, repoRef, enum.PermissionRepoEdit) + if err != nil { + return nil, fmt.Errorf("failed to acquire access to repo: %w", err) + } + + label, err := c.labelSvc.Find(ctx, nil, &repo.ID, key) + if err != nil { + return nil, fmt.Errorf("failed to find repo label: %w", err) + } + + labelValue, err := c.labelSvc.UpdateValue( + ctx, session.Principal.ID, label.ID, value, in) + if err != nil { + return nil, fmt.Errorf("failed to update repo label value: %w", err) + } + + return labelValue, nil +} diff --git a/app/api/controller/repo/wire.go b/app/api/controller/repo/wire.go index 36d995c3a..b6fe7e3bc 100644 --- a/app/api/controller/repo/wire.go +++ b/app/api/controller/repo/wire.go @@ -21,6 +21,7 @@ import ( "github.com/harness/gitness/app/services/codeowners" "github.com/harness/gitness/app/services/importer" "github.com/harness/gitness/app/services/keywordsearch" + "github.com/harness/gitness/app/services/label" "github.com/harness/gitness/app/services/locker" "github.com/harness/gitness/app/services/protection" "github.com/harness/gitness/app/services/publicaccess" @@ -67,13 +68,14 @@ func ProvideController( identifierCheck check.RepoIdentifier, repoChecks Check, publicAccess publicaccess.Service, + labelSvc *label.Service, ) *Controller { return NewController(config, tx, urlProvider, authorizer, repoStore, spaceStore, pipelineStore, principalStore, ruleStore, settings, principalInfoCache, protectionManager, rpcClient, importer, codeOwners, reporeporter, indexer, limiter, locker, auditService, mtxManager, identifierCheck, - repoChecks, publicAccess) + repoChecks, publicAccess, labelSvc) } func ProvideRepoCheck() Check { diff --git a/app/api/controller/space/controller.go b/app/api/controller/space/controller.go index 0f0534f06..722b8540f 100644 --- a/app/api/controller/space/controller.go +++ b/app/api/controller/space/controller.go @@ -24,6 +24,7 @@ import ( "github.com/harness/gitness/app/services/exporter" "github.com/harness/gitness/app/services/gitspace" "github.com/harness/gitness/app/services/importer" + "github.com/harness/gitness/app/services/label" "github.com/harness/gitness/app/services/publicaccess" "github.com/harness/gitness/app/sse" "github.com/harness/gitness/app/store" @@ -62,27 +63,30 @@ func (s SpaceOutput) MarshalJSON() ([]byte, error) { type Controller struct { nestedSpacesEnabled bool - tx dbtx.Transactor - urlProvider url.Provider - sseStreamer sse.Streamer - identifierCheck check.SpaceIdentifier - authorizer authz.Authorizer - spacePathStore store.SpacePathStore - pipelineStore store.PipelineStore - secretStore store.SecretStore - connectorStore store.ConnectorStore - templateStore store.TemplateStore - spaceStore store.SpaceStore - repoStore store.RepoStore - principalStore store.PrincipalStore - repoCtrl *repo.Controller - membershipStore store.MembershipStore - importer *importer.Repository - exporter *exporter.Repository - resourceLimiter limiter.ResourceLimiter - publicAccess publicaccess.Service - auditService audit.Service - gitspaceSvc *gitspace.Service + tx dbtx.Transactor + urlProvider url.Provider + sseStreamer sse.Streamer + identifierCheck check.SpaceIdentifier + authorizer authz.Authorizer + spacePathStore store.SpacePathStore + pipelineStore store.PipelineStore + secretStore store.SecretStore + connectorStore store.ConnectorStore + templateStore store.TemplateStore + spaceStore store.SpaceStore + repoStore store.RepoStore + principalStore store.PrincipalStore + repoCtrl *repo.Controller + membershipStore store.MembershipStore + importer *importer.Repository + exporter *exporter.Repository + resourceLimiter limiter.ResourceLimiter + publicAccess publicaccess.Service + auditService audit.Service + gitspaceSvc *gitspace.Service + gitspaceConfigStore store.GitspaceConfigStore + gitspaceInstanceStore store.GitspaceInstanceStore + labelSvc *label.Service } func NewController(config *types.Config, tx dbtx.Transactor, urlProvider url.Provider, @@ -93,29 +97,34 @@ func NewController(config *types.Config, tx dbtx.Transactor, urlProvider url.Pro membershipStore store.MembershipStore, importer *importer.Repository, exporter *exporter.Repository, limiter limiter.ResourceLimiter, publicAccess publicaccess.Service, auditService audit.Service, gitspaceSvc *gitspace.Service, + gitspaceStore store.GitspaceConfigStore, gitspaceInstanceStore store.GitspaceInstanceStore, + labelSvc *label.Service, ) *Controller { return &Controller{ - nestedSpacesEnabled: config.NestedSpacesEnabled, - tx: tx, - urlProvider: urlProvider, - sseStreamer: sseStreamer, - identifierCheck: identifierCheck, - authorizer: authorizer, - spacePathStore: spacePathStore, - pipelineStore: pipelineStore, - secretStore: secretStore, - connectorStore: connectorStore, - templateStore: templateStore, - spaceStore: spaceStore, - repoStore: repoStore, - principalStore: principalStore, - repoCtrl: repoCtrl, - membershipStore: membershipStore, - importer: importer, - exporter: exporter, - resourceLimiter: limiter, - publicAccess: publicAccess, - auditService: auditService, - gitspaceSvc: gitspaceSvc, + nestedSpacesEnabled: config.NestedSpacesEnabled, + tx: tx, + urlProvider: urlProvider, + sseStreamer: sseStreamer, + identifierCheck: identifierCheck, + authorizer: authorizer, + spacePathStore: spacePathStore, + pipelineStore: pipelineStore, + secretStore: secretStore, + connectorStore: connectorStore, + templateStore: templateStore, + spaceStore: spaceStore, + repoStore: repoStore, + principalStore: principalStore, + repoCtrl: repoCtrl, + membershipStore: membershipStore, + importer: importer, + exporter: exporter, + resourceLimiter: limiter, + publicAccess: publicAccess, + auditService: auditService, + gitspaceSvc: gitspaceSvc, + gitspaceConfigStore: gitspaceStore, + gitspaceInstanceStore: gitspaceInstanceStore, + labelSvc: labelSvc, } } diff --git a/app/api/controller/space/import_repositories.go b/app/api/controller/space/import_repositories.go index 2320e65f9..127386afc 100644 --- a/app/api/controller/space/import_repositories.go +++ b/app/api/controller/space/import_repositories.go @@ -43,12 +43,12 @@ type ImportRepositoriesOutput struct { DuplicateRepos []*repoctrl.RepositoryOutput `json:"duplicate_repos"` // repos which already exist in the space. } -// getSpaceCheckAuthRepoCreation checks whether the user has permissions to create repos -// in the given space. -func (c *Controller) getSpaceCheckAuthRepoCreation( +// getSpaceCheckAuth checks whether the user has repo permissions permission. +func (c *Controller) getSpaceCheckAuth( ctx context.Context, session *auth.Session, spaceRef string, + permission enum.Permission, ) (*types.Space, error) { space, err := c.spaceStore.FindByRef(ctx, spaceRef) if err != nil { @@ -62,7 +62,7 @@ func (c *Controller) getSpaceCheckAuthRepoCreation( Identifier: "", } - err = apiauth.Check(ctx, c.authorizer, session, scope, resource, enum.PermissionRepoEdit) + err = apiauth.Check(ctx, c.authorizer, session, scope, resource, permission) if err != nil { return nil, fmt.Errorf("auth check failed: %w", err) } @@ -80,7 +80,7 @@ func (c *Controller) ImportRepositories( spaceRef string, in *ImportRepositoriesInput, ) (ImportRepositoriesOutput, error) { - space, err := c.getSpaceCheckAuthRepoCreation(ctx, session, spaceRef) + space, err := c.getSpaceCheckAuth(ctx, session, spaceRef, enum.PermissionRepoEdit) if err != nil { return ImportRepositoriesOutput{}, err } diff --git a/app/api/controller/space/label_define.go b/app/api/controller/space/label_define.go new file mode 100644 index 000000000..42bc0b859 --- /dev/null +++ b/app/api/controller/space/label_define.go @@ -0,0 +1,49 @@ +// 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" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// DefineLabel defines a new label for the specified space. +func (c *Controller) DefineLabel( + ctx context.Context, + session *auth.Session, + spaceRef string, + in *types.DefineLabelInput, +) (*types.Label, error) { + space, err := c.getSpaceCheckAuth(ctx, session, spaceRef, enum.PermissionSpaceEdit) + if err != nil { + return nil, fmt.Errorf("failed to acquire access to space: %w", err) + } + + if err := in.Validate(); err != nil { + return nil, fmt.Errorf("failed to validate input: %w", err) + } + + label, err := c.labelSvc.Define( + ctx, session.Principal.ID, &space.ID, nil, in) + if err != nil { + return nil, fmt.Errorf("failed to create space label: %w", err) + } + + return label, nil +} diff --git a/app/api/controller/space/label_delete.go b/app/api/controller/space/label_delete.go new file mode 100644 index 000000000..47b953890 --- /dev/null +++ b/app/api/controller/space/label_delete.go @@ -0,0 +1,42 @@ +// 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" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types/enum" +) + +// DeleteLabel deletes a label for the specified space. +func (c *Controller) DeleteLabel( + ctx context.Context, + session *auth.Session, + spaceRef string, + key string, +) error { + space, err := c.getSpaceCheckAuth(ctx, session, spaceRef, enum.PermissionSpaceEdit) + if err != nil { + return fmt.Errorf("failed to acquire access to space: %w", err) + } + + if err := c.labelSvc.Delete(ctx, &space.ID, nil, key); err != nil { + return fmt.Errorf("failed to delete space label: %w", err) + } + + return nil +} diff --git a/app/api/controller/space/label_list.go b/app/api/controller/space/label_list.go new file mode 100644 index 000000000..ad8995c2b --- /dev/null +++ b/app/api/controller/space/label_list.go @@ -0,0 +1,44 @@ +// 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" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// ListLabels lists all labels defined for the specified space. +func (c *Controller) ListLabels( + ctx context.Context, + session *auth.Session, + spaceRef string, + filter *types.LabelFilter, +) ([]*types.Label, error) { + space, err := c.getSpaceCheckAuth(ctx, session, spaceRef, enum.PermissionSpaceView) + if err != nil { + return nil, fmt.Errorf("failed to acquire access to space: %w", err) + } + + labels, err := c.labelSvc.List(ctx, &space.ID, nil, filter) + if err != nil { + return nil, fmt.Errorf("failed to list space labels: %w", err) + } + + return labels, nil +} diff --git a/app/api/controller/space/label_save.go b/app/api/controller/space/label_save.go new file mode 100644 index 000000000..5c0e4a007 --- /dev/null +++ b/app/api/controller/space/label_save.go @@ -0,0 +1,49 @@ +// 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" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// SaveLabel defines a new label for the specified space. +func (c *Controller) SaveLabel( + ctx context.Context, + session *auth.Session, + spaceRef string, + in *types.SaveInput, +) (*types.LabelWithValues, error) { + space, err := c.getSpaceCheckAuth(ctx, session, spaceRef, enum.PermissionSpaceEdit) + if err != nil { + return nil, fmt.Errorf("failed to acquire access to space: %w", err) + } + + if err := in.Validate(); err != nil { + return nil, fmt.Errorf("failed to validate input: %w", err) + } + + labelWithValues, err := c.labelSvc.Save( + ctx, session.Principal.ID, &space.ID, nil, in) + if err != nil { + return nil, fmt.Errorf("failed to save label: %w", err) + } + + return labelWithValues, nil +} diff --git a/app/api/controller/space/label_update.go b/app/api/controller/space/label_update.go new file mode 100644 index 000000000..3ebc3c655 --- /dev/null +++ b/app/api/controller/space/label_update.go @@ -0,0 +1,49 @@ +// 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" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// UpdateLabel updates a label for the specified space. +func (c *Controller) UpdateLabel( + ctx context.Context, + session *auth.Session, + spaceRef string, + key string, + in *types.UpdateLabelInput, +) (*types.Label, error) { + space, err := c.getSpaceCheckAuth(ctx, session, spaceRef, enum.PermissionSpaceEdit) + if err != nil { + return nil, fmt.Errorf("failed to acquire access to space: %w", err) + } + + if err := in.Validate(); err != nil { + return nil, fmt.Errorf("failed to validate input: %w", err) + } + + label, err := c.labelSvc.Update(ctx, session.Principal.ID, &space.ID, nil, key, in) + if err != nil { + return nil, fmt.Errorf("failed to update space label: %w", err) + } + + return label, nil +} diff --git a/app/api/controller/space/label_value_define.go b/app/api/controller/space/label_value_define.go new file mode 100644 index 000000000..6acd2da45 --- /dev/null +++ b/app/api/controller/space/label_value_define.go @@ -0,0 +1,55 @@ +// 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" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// DefineLabelValue defines a new label value for the specified space and label. +func (c *Controller) DefineLabelValue( + ctx context.Context, + session *auth.Session, + spaceRef string, + key string, + in *types.DefineValueInput, +) (*types.LabelValue, error) { + // TODO: permission check should be based on static vs dynamic label + space, err := c.getSpaceCheckAuth(ctx, session, spaceRef, enum.PermissionSpaceEdit) + if err != nil { + return nil, fmt.Errorf("failed to acquire access to space: %w", err) + } + + if err := in.Validate(); err != nil { + return nil, fmt.Errorf("failed to validate input: %w", err) + } + + label, err := c.labelSvc.Find(ctx, &space.ID, nil, key) + if err != nil { + return nil, fmt.Errorf("failed to find repo label: %w", err) + } + + value, err := c.labelSvc.DefineValue(ctx, session.Principal.ID, label.ID, in) + if err != nil { + return nil, fmt.Errorf("failed to create space label value: %w", err) + } + + return value, nil +} diff --git a/app/api/controller/space/label_value_delete.go b/app/api/controller/space/label_value_delete.go new file mode 100644 index 000000000..ff12f82f8 --- /dev/null +++ b/app/api/controller/space/label_value_delete.go @@ -0,0 +1,43 @@ +// 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" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types/enum" +) + +// DeleteLabelValue deletes a label value for the specified space. +func (c *Controller) DeleteLabelValue( + ctx context.Context, + session *auth.Session, + spaceRef string, + key string, + value string, +) error { + space, err := c.getSpaceCheckAuth(ctx, session, spaceRef, enum.PermissionSpaceEdit) + if err != nil { + return fmt.Errorf("failed to acquire access to space: %w", err) + } + + if err := c.labelSvc.DeleteValue(ctx, &space.ID, nil, key, value); err != nil { + return fmt.Errorf("failed to delete space label value: %w", err) + } + + return nil +} diff --git a/app/api/controller/space/label_value_list.go b/app/api/controller/space/label_value_list.go new file mode 100644 index 000000000..551afbaeb --- /dev/null +++ b/app/api/controller/space/label_value_list.go @@ -0,0 +1,45 @@ +// 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" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// ListLabelValues lists all label values defined in the specified space. +func (c *Controller) ListLabelValues( + ctx context.Context, + session *auth.Session, + spaceRef string, + key string, + filter *types.ListQueryFilter, +) ([]*types.LabelValue, error) { + space, err := c.getSpaceCheckAuth(ctx, session, spaceRef, enum.PermissionSpaceView) + if err != nil { + return nil, fmt.Errorf("failed to acquire access to space: %w", err) + } + + values, err := c.labelSvc.ListValues(ctx, &space.ID, nil, key, filter) + if err != nil { + return nil, fmt.Errorf("failed to list space label values: %w", err) + } + + return values, nil +} diff --git a/app/api/controller/space/label_value_update.go b/app/api/controller/space/label_value_update.go new file mode 100644 index 000000000..382c6e84c --- /dev/null +++ b/app/api/controller/space/label_value_update.go @@ -0,0 +1,52 @@ +// 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" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// UpdateLabelValue updates a label value for the specified space and label. +func (c *Controller) UpdateLabelValue( + ctx context.Context, + session *auth.Session, + spaceRef string, + key string, + value string, + in *types.UpdateValueInput, +) (*types.LabelValue, error) { + space, err := c.getSpaceCheckAuth(ctx, session, spaceRef, enum.PermissionSpaceEdit) + if err != nil { + return nil, fmt.Errorf("failed to acquire access to space: %w", err) + } + + label, err := c.labelSvc.Find(ctx, &space.ID, nil, key) + if err != nil { + return nil, fmt.Errorf("failed to find space label: %w", err) + } + + labelValue, err := c.labelSvc.UpdateValue( + ctx, session.Principal.ID, label.ID, value, in) + if err != nil { + return nil, fmt.Errorf("failed to update space label value: %w", err) + } + + return labelValue, nil +} diff --git a/app/api/controller/space/wire.go b/app/api/controller/space/wire.go index e555ef269..78243683b 100644 --- a/app/api/controller/space/wire.go +++ b/app/api/controller/space/wire.go @@ -21,6 +21,7 @@ import ( "github.com/harness/gitness/app/services/exporter" "github.com/harness/gitness/app/services/gitspace" "github.com/harness/gitness/app/services/importer" + "github.com/harness/gitness/app/services/label" "github.com/harness/gitness/app/services/publicaccess" "github.com/harness/gitness/app/sse" "github.com/harness/gitness/app/store" @@ -46,6 +47,8 @@ func ProvideController(config *types.Config, tx dbtx.Transactor, urlProvider url repoCtrl *repo.Controller, membershipStore store.MembershipStore, importer *importer.Repository, exporter *exporter.Repository, limiter limiter.ResourceLimiter, publicAccess publicaccess.Service, auditService audit.Service, gitspaceService *gitspace.Service, + gitspaceConfigStore store.GitspaceConfigStore, instanceStore store.GitspaceInstanceStore, + labelSvc *label.Service, ) *Controller { return NewController(config, tx, urlProvider, sseStreamer, identifierCheck, authorizer, spacePathStore, pipelineStore, secretStore, @@ -53,5 +56,7 @@ func ProvideController(config *types.Config, tx dbtx.Transactor, urlProvider url spaceStore, repoStore, principalStore, repoCtrl, membershipStore, importer, exporter, limiter, publicAccess, - auditService, gitspaceService) + auditService, gitspaceService, + gitspaceConfigStore, instanceStore, + labelSvc) } diff --git a/app/api/controller/upload/controller.go b/app/api/controller/upload/controller.go index 7f3a7396a..36ad262ac 100644 --- a/app/api/controller/upload/controller.go +++ b/app/api/controller/upload/controller.go @@ -64,7 +64,7 @@ func NewController(authorizer authz.Authorizer, func (c *Controller) getRepoCheckAccess(ctx context.Context, session *auth.Session, repoRef string, - reqPermission enum.Permission, + permission enum.Permission, ) (*types.Repository, error) { if repoRef == "" { return nil, usererror.BadRequest("A valid repository reference must be provided.") @@ -75,7 +75,7 @@ func (c *Controller) getRepoCheckAccess(ctx context.Context, return nil, fmt.Errorf("failed to find repo: %w", err) } - if err = apiauth.CheckRepo(ctx, c.authorizer, session, repo, reqPermission); err != nil { + if err = apiauth.CheckRepo(ctx, c.authorizer, session, repo, permission); err != nil { return nil, fmt.Errorf("failed to verify authorization: %w", err) } diff --git a/app/api/handler/pullreq/label_assign.go b/app/api/handler/pullreq/label_assign.go new file mode 100644 index 000000000..781a91752 --- /dev/null +++ b/app/api/handler/pullreq/label_assign.go @@ -0,0 +1,60 @@ +// 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 pullreq + +import ( + "encoding/json" + "net/http" + + "github.com/harness/gitness/app/api/controller/pullreq" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" + "github.com/harness/gitness/types" +) + +func HandleAssignLabel(pullreqCtrl *pullreq.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + + repoRef, err := request.GetRepoRefFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + pullreqNumber, err := request.GetPullReqNumberFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + in := new(types.PullReqCreateInput) + err = json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(ctx, w, "Invalid request body: %s.", err) + return + } + + label, err := pullreqCtrl.AssignLabel( + ctx, session, repoRef, pullreqNumber, in) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusOK, label) + } +} diff --git a/app/api/handler/pullreq/label_list.go b/app/api/handler/pullreq/label_list.go new file mode 100644 index 000000000..b6d600907 --- /dev/null +++ b/app/api/handler/pullreq/label_list.go @@ -0,0 +1,56 @@ +// 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 pullreq + +import ( + "net/http" + + "github.com/harness/gitness/app/api/controller/pullreq" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" +) + +func HandleListLabels(pullreqCtrl *pullreq.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + + repoRef, err := request.GetRepoRefFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + pullreqNumber, err := request.GetPullReqNumberFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + filter, err := request.ParseAssignableLabelFilter(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + labels, err := pullreqCtrl.ListLabels(ctx, session, repoRef, pullreqNumber, filter) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusOK, labels) + } +} diff --git a/app/api/handler/pullreq/label_unassign.go b/app/api/handler/pullreq/label_unassign.go new file mode 100644 index 000000000..1edc34569 --- /dev/null +++ b/app/api/handler/pullreq/label_unassign.go @@ -0,0 +1,56 @@ +// 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 pullreq + +import ( + "net/http" + + "github.com/harness/gitness/app/api/controller/pullreq" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" +) + +func HandleUnassignLabel(pullreqCtrl *pullreq.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + + repoRef, err := request.GetRepoRefFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + pullreqNumber, err := request.GetPullReqNumberFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + labelID, err := request.GetLabelIDFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + if err := pullreqCtrl.UnassignLabel( + ctx, session, repoRef, pullreqNumber, labelID); err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.DeleteSuccessful(w) + } +} diff --git a/app/api/handler/repo/label_define.go b/app/api/handler/repo/label_define.go new file mode 100644 index 000000000..1a5e8ed71 --- /dev/null +++ b/app/api/handler/repo/label_define.go @@ -0,0 +1,53 @@ +// 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 repo + +import ( + "encoding/json" + "net/http" + + "github.com/harness/gitness/app/api/controller/repo" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" + "github.com/harness/gitness/types" +) + +func HandleDefineLabel(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) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + in := new(types.DefineLabelInput) + err = json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(ctx, w, "Invalid request body: %s.", err) + return + } + + label, err := repoCtrl.DefineLabel(ctx, session, repoRef, in) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusCreated, label) + } +} diff --git a/app/api/handler/repo/label_delete.go b/app/api/handler/repo/label_delete.go new file mode 100644 index 000000000..0b5103b63 --- /dev/null +++ b/app/api/handler/repo/label_delete.go @@ -0,0 +1,50 @@ +// 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 repo + +import ( + "net/http" + + "github.com/harness/gitness/app/api/controller/repo" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" +) + +func HandleDeleteLabel(labelCtrl *repo.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + + repoRef, err := request.GetRepoRefFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + key, err := request.GetLabelKeyFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + err = labelCtrl.DeleteLabel(ctx, session, repoRef, key) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.DeleteSuccessful(w) + } +} diff --git a/app/api/handler/repo/label_list.go b/app/api/handler/repo/label_list.go new file mode 100644 index 000000000..cdb37bf30 --- /dev/null +++ b/app/api/handler/repo/label_list.go @@ -0,0 +1,50 @@ +// 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 repo + +import ( + "net/http" + + "github.com/harness/gitness/app/api/controller/repo" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" +) + +func HandleListLabels(labelCtrl *repo.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + + repoRef, err := request.GetRepoRefFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + filter, err := request.ParseLabelFilter(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + labels, err := labelCtrl.ListLabels(ctx, session, repoRef, filter) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusOK, labels) + } +} diff --git a/app/api/handler/repo/label_save.go b/app/api/handler/repo/label_save.go new file mode 100644 index 000000000..d58d27657 --- /dev/null +++ b/app/api/handler/repo/label_save.go @@ -0,0 +1,53 @@ +// 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 repo + +import ( + "encoding/json" + "net/http" + + "github.com/harness/gitness/app/api/controller/repo" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" + "github.com/harness/gitness/types" +) + +func HandleSaveLabel(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) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + in := new(types.SaveInput) + err = json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(ctx, w, "Invalid request body: %s.", err) + return + } + + label, err := repoCtrl.SaveLabel(ctx, session, repoRef, in) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusOK, label) + } +} diff --git a/app/api/handler/repo/label_update.go b/app/api/handler/repo/label_update.go new file mode 100644 index 000000000..77f659657 --- /dev/null +++ b/app/api/handler/repo/label_update.go @@ -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 repo + +import ( + "encoding/json" + "net/http" + + "github.com/harness/gitness/app/api/controller/repo" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" + "github.com/harness/gitness/types" +) + +func HandleUpdateLabel(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) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + key, err := request.GetLabelKeyFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + in := new(types.UpdateLabelInput) + err = json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(ctx, w, "Invalid request body: %s.", err) + return + } + + label, err := repoCtrl.UpdateLabel(ctx, session, repoRef, key, in) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusOK, label) + } +} diff --git a/app/api/handler/repo/label_value_define.go b/app/api/handler/repo/label_value_define.go new file mode 100644 index 000000000..1ae27cd3a --- /dev/null +++ b/app/api/handler/repo/label_value_define.go @@ -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 repo + +import ( + "encoding/json" + "net/http" + + "github.com/harness/gitness/app/api/controller/repo" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" + "github.com/harness/gitness/types" +) + +func HandleDefineLabelValue(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) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + in := new(types.DefineValueInput) + err = json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(ctx, w, "Invalid request body: %s.", err) + return + } + + key, err := request.GetLabelKeyFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + label, err := repoCtrl.DefineLabelValue(ctx, session, repoRef, key, in) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusCreated, label) + } +} diff --git a/app/api/handler/repo/label_value_delete.go b/app/api/handler/repo/label_value_delete.go new file mode 100644 index 000000000..0ed580490 --- /dev/null +++ b/app/api/handler/repo/label_value_delete.go @@ -0,0 +1,56 @@ +// 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 repo + +import ( + "net/http" + + "github.com/harness/gitness/app/api/controller/repo" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" +) + +func HandleDeleteLabelValue(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) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + key, err := request.GetLabelKeyFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + value, err := request.GetLabelValueFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + err = repoCtrl.DeleteLabelValue(ctx, session, repoRef, key, value) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.DeleteSuccessful(w) + } +} diff --git a/app/api/handler/repo/label_value_list.go b/app/api/handler/repo/label_value_list.go new file mode 100644 index 000000000..1ba07b7ce --- /dev/null +++ b/app/api/handler/repo/label_value_list.go @@ -0,0 +1,52 @@ +// 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 repo + +import ( + "net/http" + + "github.com/harness/gitness/app/api/controller/repo" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" +) + +func HandleListLabelValues(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) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + key, err := request.GetLabelKeyFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + filter := request.ParseListQueryFilterFromRequest(r) + + labels, err := repoCtrl.ListLabelValues(ctx, session, repoRef, key, &filter) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusOK, labels) + } +} diff --git a/app/api/handler/repo/label_value_update.go b/app/api/handler/repo/label_value_update.go new file mode 100644 index 000000000..7f56eadde --- /dev/null +++ b/app/api/handler/repo/label_value_update.go @@ -0,0 +1,66 @@ +// 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 repo + +import ( + "encoding/json" + "net/http" + + "github.com/harness/gitness/app/api/controller/repo" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" + "github.com/harness/gitness/types" +) + +func HandleUpdateLabelValue(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) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + key, err := request.GetLabelKeyFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + value, err := request.GetLabelValueFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + in := new(types.UpdateValueInput) + err = json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(ctx, w, "Invalid request body: %s.", err) + return + } + + label, err := repoCtrl.UpdateLabelValue( + ctx, session, repoRef, key, value, in) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusOK, label) + } +} diff --git a/app/api/handler/space/label_define.go b/app/api/handler/space/label_define.go new file mode 100644 index 000000000..5d1afc064 --- /dev/null +++ b/app/api/handler/space/label_define.go @@ -0,0 +1,53 @@ +// 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" + "github.com/harness/gitness/types" +) + +func HandleDefineLabel(labelCtrl *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 + } + + in := new(types.DefineLabelInput) + err = json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(ctx, w, "Invalid request body: %s.", err) + return + } + + label, err := labelCtrl.DefineLabel(ctx, session, spaceRef, in) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusCreated, label) + } +} diff --git a/app/api/handler/space/label_delete.go b/app/api/handler/space/label_delete.go new file mode 100644 index 000000000..b7e631ca5 --- /dev/null +++ b/app/api/handler/space/label_delete.go @@ -0,0 +1,50 @@ +// 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" +) + +func HandleDeleteLabel(labelCtrl *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 + } + + identifier, err := request.GetLabelKeyFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + err = labelCtrl.DeleteLabel(ctx, session, spaceRef, identifier) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.DeleteSuccessful(w) + } +} diff --git a/app/api/handler/space/label_list.go b/app/api/handler/space/label_list.go new file mode 100644 index 000000000..a4e247d5b --- /dev/null +++ b/app/api/handler/space/label_list.go @@ -0,0 +1,50 @@ +// 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" +) + +func HandleListLabels(labelCtrl *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 + } + + filter, err := request.ParseLabelFilter(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + labels, err := labelCtrl.ListLabels(ctx, session, spaceRef, filter) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusOK, labels) + } +} diff --git a/app/api/handler/space/label_save.go b/app/api/handler/space/label_save.go new file mode 100644 index 000000000..6eb3f8c04 --- /dev/null +++ b/app/api/handler/space/label_save.go @@ -0,0 +1,53 @@ +// 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" + "github.com/harness/gitness/types" +) + +func HandleSaveLabel(labelCtrl *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 + } + + in := new(types.SaveInput) + err = json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(ctx, w, "Invalid request body: %s.", err) + return + } + + label, err := labelCtrl.SaveLabel(ctx, session, spaceRef, in) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusOK, label) + } +} diff --git a/app/api/handler/space/label_update.go b/app/api/handler/space/label_update.go new file mode 100644 index 000000000..a6d088509 --- /dev/null +++ b/app/api/handler/space/label_update.go @@ -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" + "github.com/harness/gitness/types" +) + +func HandleUpdateLabel(labelCtrl *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 + } + + key, err := request.GetLabelKeyFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + in := new(types.UpdateLabelInput) + err = json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(ctx, w, "Invalid request body: %s.", err) + return + } + + label, err := labelCtrl.UpdateLabel(ctx, session, spaceRef, key, in) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusOK, label) + } +} diff --git a/app/api/handler/space/label_value_define.go b/app/api/handler/space/label_value_define.go new file mode 100644 index 000000000..21950ef6e --- /dev/null +++ b/app/api/handler/space/label_value_define.go @@ -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" + "github.com/harness/gitness/types" +) + +func HandleDefineLabelValue(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 + } + + in := new(types.DefineValueInput) + err = json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(ctx, w, "Invalid request body: %s.", err) + return + } + + key, err := request.GetLabelKeyFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + label, err := spaceCtrl.DefineLabelValue(ctx, session, spaceRef, key, in) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusCreated, label) + } +} diff --git a/app/api/handler/space/label_value_delete.go b/app/api/handler/space/label_value_delete.go new file mode 100644 index 000000000..3ec65fd5b --- /dev/null +++ b/app/api/handler/space/label_value_delete.go @@ -0,0 +1,56 @@ +// 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" +) + +func HandleDeleteLabelValue(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 + } + + key, err := request.GetLabelKeyFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + value, err := request.GetLabelValueFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + err = spaceCtrl.DeleteLabelValue(ctx, session, spaceRef, key, value) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.DeleteSuccessful(w) + } +} diff --git a/app/api/handler/space/label_value_list.go b/app/api/handler/space/label_value_list.go new file mode 100644 index 000000000..8e254942f --- /dev/null +++ b/app/api/handler/space/label_value_list.go @@ -0,0 +1,53 @@ +// 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" +) + +func HandleListLabelValues(labelValueCtrl *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 + } + + key, err := request.GetLabelKeyFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + filter := request.ParseListQueryFilterFromRequest(r) + + labels, err := labelValueCtrl.ListLabelValues( + ctx, session, spaceRef, key, &filter) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusOK, labels) + } +} diff --git a/app/api/handler/space/label_value_update.go b/app/api/handler/space/label_value_update.go new file mode 100644 index 000000000..ba1cdb5d6 --- /dev/null +++ b/app/api/handler/space/label_value_update.go @@ -0,0 +1,66 @@ +// 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" + "github.com/harness/gitness/types" +) + +func HandleUpdateLabelValue(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 + } + + key, err := request.GetLabelKeyFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + value, err := request.GetLabelValueFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + in := new(types.UpdateValueInput) + err = json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(ctx, w, "Invalid request body: %s.", err) + return + } + + label, err := spaceCtrl.UpdateLabelValue( + ctx, session, spaceRef, key, value, in) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusOK, label) + } +} diff --git a/app/api/openapi/pullreq.go b/app/api/openapi/pullreq.go index f6939bf20..dd0a5cf00 100644 --- a/app/api/openapi/pullreq.go +++ b/app/api/openapi/pullreq.go @@ -142,6 +142,11 @@ type getPullReqChecksRequest struct { pullReqRequest } +type pullReqAssignLabelInput struct { + pullReqRequest + types.PullReqCreateInput +} + var queryParameterQueryPullRequest = openapi3.ParameterOrRef{ Parameter: &openapi3.Parameter{ Name: request.QueryParamQuery, @@ -314,6 +319,21 @@ var queryParameterBeforePullRequestActivity = openapi3.ParameterOrRef{ }, } +var queryParameterAssignable = openapi3.ParameterOrRef{ + Parameter: &openapi3.Parameter{ + Name: request.QueryParamAssignable, + In: openapi3.ParameterInQuery, + Description: ptr.String("The result should contain all labels assignable to the pullreq."), + Required: ptr.Bool(false), + Schema: &openapi3.SchemaOrRef{ + Schema: &openapi3.Schema{ + Type: ptrSchemaType(openapi3.SchemaTypeBoolean), + Default: ptrptr(false), + }, + }, + }, +} + //nolint:funlen func pullReqOperations(reflector *openapi3.Reflector) { createPullReq := openapi3.Operation{} @@ -624,4 +644,45 @@ func pullReqOperations(reflector *openapi3.Reflector) { panicOnErr(reflector.SetJSONResponse(&opChecks, new(usererror.Error), http.StatusForbidden)) panicOnErr(reflector.SetJSONResponse(&opChecks, new(usererror.Error), http.StatusNotFound)) panicOnErr(reflector.Spec.AddOperation(http.MethodGet, "/repos/{repo_ref}/pullreq/{pullreq_number}/checks", opChecks)) + + opAssignLabel := openapi3.Operation{} + opAssignLabel.WithTags("pullreq") + opAssignLabel.WithMapOfAnything(map[string]interface{}{"operationId": "assignLabel"}) + _ = reflector.SetRequest(&opAssignLabel, new(pullReqAssignLabelInput), http.MethodPut) + _ = reflector.SetJSONResponse(&opAssignLabel, new(types.PullReqLabel), http.StatusOK) + _ = reflector.SetJSONResponse(&opAssignLabel, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opAssignLabel, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opAssignLabel, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opAssignLabel, new(usererror.Error), http.StatusForbidden) + _ = reflector.Spec.AddOperation(http.MethodPut, + "/repos/{repo_ref}/pullreq/{pullreq_number}/labels", opAssignLabel) + + opListLabels := openapi3.Operation{} + opListLabels.WithTags("pullreq") + opListLabels.WithMapOfAnything(map[string]interface{}{"operationId": "listLabels"}) + opListLabels.WithParameters( + QueryParameterPage, QueryParameterLimit, queryParameterAssignable, queryParameterQueryLabel) + _ = reflector.SetRequest(&opListLabels, new(pullReqRequest), http.MethodGet) + _ = reflector.SetJSONResponse(&opListLabels, new(types.ScopesLabels), http.StatusOK) + _ = reflector.SetJSONResponse(&opListLabels, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opListLabels, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opListLabels, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opListLabels, new(usererror.Error), http.StatusForbidden) + _ = reflector.Spec.AddOperation(http.MethodGet, + "/repos/{repo_ref}/pullreq/{pullreq_number}/labels", opListLabels) + + opUnassignLabel := openapi3.Operation{} + opUnassignLabel.WithTags("pullreq") + opUnassignLabel.WithMapOfAnything(map[string]interface{}{"operationId": "unassignLabel"}) + _ = reflector.SetRequest(&opUnassignLabel, struct { + pullReqRequest + LabelID int64 `path:"label_id"` + }{}, http.MethodDelete) + _ = reflector.SetJSONResponse(&opUnassignLabel, nil, http.StatusNoContent) + _ = reflector.SetJSONResponse(&opUnassignLabel, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opUnassignLabel, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opUnassignLabel, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opUnassignLabel, new(usererror.Error), http.StatusForbidden) + _ = reflector.Spec.AddOperation(http.MethodDelete, + "/repos/{repo_ref}/pullreq/{pullreq_number}/labels/{label_id}", opUnassignLabel) } diff --git a/app/api/openapi/repo.go b/app/api/openapi/repo.go index d38e145b5..9a1a8bc49 100644 --- a/app/api/openapi/repo.go +++ b/app/api/openapi/repo.go @@ -221,6 +221,18 @@ type archiveRequest struct { Format string `path:"format" required:"true"` } +type labelRequest struct { + Key string `json:"key"` + Description string `json:"description"` + Type enum.LabelType `json:"type"` + Color enum.LabelColor `json:"color"` +} + +type labelValueRequest struct { + Value string `json:"value"` + Color enum.LabelColor `json:"color"` +} + var queryParameterGitRef = openapi3.ParameterOrRef{ Parameter: &openapi3.Parameter{ Name: request.QueryParamGitRef, @@ -603,6 +615,35 @@ var queryParamArchiveCompression = openapi3.ParameterOrRef{ }, } +var queryParameterInherited = openapi3.ParameterOrRef{ + Parameter: &openapi3.Parameter{ + Name: request.QueryParamInherited, + In: openapi3.ParameterInQuery, + Description: ptr.String("The result should inherit labels from parent parent spaces."), + Required: ptr.Bool(false), + Schema: &openapi3.SchemaOrRef{ + Schema: &openapi3.Schema{ + Type: ptrSchemaType(openapi3.SchemaTypeBoolean), + Default: ptrptr(false), + }, + }, + }, +} + +var queryParameterQueryLabel = openapi3.ParameterOrRef{ + Parameter: &openapi3.Parameter{ + Name: request.QueryParamQuery, + In: openapi3.ParameterInQuery, + Description: ptr.String("The substring which is used to filter the labels by their key."), + Required: ptr.Bool(false), + Schema: &openapi3.SchemaOrRef{ + Schema: &openapi3.Schema{ + Type: ptrSchemaType(openapi3.SchemaTypeString), + }, + }, + }, +} + //nolint:funlen func repoOperations(reflector *openapi3.Reflector) { createRepository := openapi3.Operation{} @@ -1168,4 +1209,157 @@ func repoOperations(reflector *openapi3.Reflector) { _ = reflector.SetJSONResponse(&opSummary, new(usererror.Error), http.StatusForbidden) _ = reflector.SetJSONResponse(&opSummary, new(usererror.Error), http.StatusNotFound) _ = reflector.Spec.AddOperation(http.MethodGet, "/repos/{repo_ref}/summary", opSummary) + + opDefineLabel := openapi3.Operation{} + opDefineLabel.WithTags("repository") + opDefineLabel.WithMapOfAnything( + map[string]interface{}{"operationId": "defineRepoLabel"}) + _ = reflector.SetRequest(&opDefineLabel, &struct { + repoRequest + labelRequest + }{}, http.MethodPost) + _ = reflector.SetJSONResponse(&opDefineLabel, new(types.Label), http.StatusCreated) + _ = reflector.SetJSONResponse(&opDefineLabel, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opDefineLabel, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opDefineLabel, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opDefineLabel, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opDefineLabel, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodPost, "/repos/{repo_ref}/labels", opDefineLabel) + + opSaveLabel := openapi3.Operation{} + opSaveLabel.WithTags("repository") + opSaveLabel.WithMapOfAnything( + map[string]interface{}{"operationId": "saveRepoLabel"}) + _ = reflector.SetRequest(&opSaveLabel, &struct { + repoRequest + types.SaveInput + }{}, http.MethodPut) + _ = reflector.SetJSONResponse(&opSaveLabel, new(types.LabelWithValues), http.StatusOK) + _ = reflector.SetJSONResponse(&opSaveLabel, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opSaveLabel, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opSaveLabel, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opSaveLabel, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opSaveLabel, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodPut, "/repos/{repo_ref}/labels", opSaveLabel) + + opListLabels := openapi3.Operation{} + opListLabels.WithTags("repository") + opListLabels.WithMapOfAnything( + map[string]interface{}{"operationId": "listRepoLabels"}) + opListLabels.WithParameters( + QueryParameterPage, QueryParameterLimit, queryParameterInherited, queryParameterQueryLabel) + _ = reflector.SetRequest(&opListLabels, new(repoRequest), http.MethodGet) + _ = reflector.SetJSONResponse(&opListLabels, new([]*types.Label), http.StatusOK) + _ = reflector.SetJSONResponse(&opListLabels, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opListLabels, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opListLabels, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opListLabels, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opListLabels, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodGet, "/repos/{repo_ref}/labels", opListLabels) + + opDeleteLabel := openapi3.Operation{} + opDeleteLabel.WithTags("repository") + opDeleteLabel.WithMapOfAnything( + map[string]interface{}{"operationId": "deleteRepoLabel"}) + _ = reflector.SetRequest(&opDeleteLabel, &struct { + repoRequest + Key string `path:"key"` + }{}, http.MethodDelete) + _ = reflector.SetJSONResponse(&opDeleteLabel, nil, http.StatusNoContent) + _ = reflector.SetJSONResponse(&opDeleteLabel, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opDeleteLabel, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opDeleteLabel, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opDeleteLabel, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opDeleteLabel, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation( + http.MethodDelete, "/repos/{repo_ref}/labels/{key}", opDeleteLabel) + + opUpdateLabel := openapi3.Operation{} + opUpdateLabel.WithTags("repository") + opUpdateLabel.WithMapOfAnything( + map[string]interface{}{"operationId": "updateRepoLabel"}) + _ = reflector.SetRequest(&opUpdateLabel, &struct { + repoRequest + labelRequest + Key string `path:"key"` + }{}, http.MethodPatch) + _ = reflector.SetJSONResponse(&opUpdateLabel, new(types.Label), http.StatusOK) + _ = reflector.SetJSONResponse(&opUpdateLabel, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opUpdateLabel, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opUpdateLabel, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opUpdateLabel, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opUpdateLabel, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodPatch, "/repos/{repo_ref}/labels/{key}", opUpdateLabel) + + opDefineLabelValue := openapi3.Operation{} + opDefineLabelValue.WithTags("repository") + opDefineLabelValue.WithMapOfAnything( + map[string]interface{}{"operationId": "defineRepoLabelValue"}) + _ = reflector.SetRequest(&opDefineLabelValue, &struct { + repoRequest + labelValueRequest + Key string `path:"key"` + }{}, http.MethodPost) + _ = reflector.SetJSONResponse(&opDefineLabelValue, new(types.LabelValue), http.StatusCreated) + _ = reflector.SetJSONResponse(&opDefineLabelValue, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opDefineLabelValue, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opDefineLabelValue, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opDefineLabelValue, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opDefineLabelValue, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodPost, + "/repos/{repo_ref}/labels/{key}/values", opDefineLabelValue) + + opListLabelValues := openapi3.Operation{} + opListLabelValues.WithTags("repository") + opListLabelValues.WithMapOfAnything( + map[string]interface{}{"operationId": "listRepoLabelValues"}) + _ = reflector.SetRequest(&opListLabelValues, &struct { + repoRequest + Key string `path:"key"` + }{}, http.MethodGet) + _ = reflector.SetJSONResponse(&opListLabelValues, new([]*types.LabelValue), http.StatusOK) + _ = reflector.SetJSONResponse(&opListLabelValues, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opListLabelValues, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opListLabelValues, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opListLabelValues, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opListLabelValues, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodGet, + "/repos/{repo_ref}/labels/{key}/values", opListLabelValues) + + opDeleteLabelValue := openapi3.Operation{} + opDeleteLabelValue.WithTags("repository") + opDeleteLabelValue.WithMapOfAnything( + map[string]interface{}{"operationId": "deleteRepoLabelValue"}) + _ = reflector.SetRequest(&opDeleteLabelValue, &struct { + repoRequest + Key string `path:"key"` + Value string `path:"value"` + }{}, http.MethodDelete) + _ = reflector.SetJSONResponse(&opDeleteLabelValue, nil, http.StatusNoContent) + _ = reflector.SetJSONResponse(&opDeleteLabelValue, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opDeleteLabelValue, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opDeleteLabelValue, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opDeleteLabelValue, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opDeleteLabelValue, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation( + http.MethodDelete, "/repos/{repo_ref}/labels/{key}/values/{value}", opDeleteLabelValue) + + opUpdateLabelValue := openapi3.Operation{} + opUpdateLabelValue.WithTags("repository") + opUpdateLabelValue.WithMapOfAnything( + map[string]interface{}{"operationId": "updateRepoLabelValue"}) + _ = reflector.SetRequest(&opUpdateLabelValue, &struct { + repoRequest + labelValueRequest + Key string `path:"key"` + Value string `path:"value"` + }{}, http.MethodPatch) + _ = reflector.SetJSONResponse(&opUpdateLabelValue, new(types.LabelValue), http.StatusOK) + _ = reflector.SetJSONResponse(&opUpdateLabelValue, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opUpdateLabelValue, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opUpdateLabelValue, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opUpdateLabelValue, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opUpdateLabelValue, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodPatch, + "/repos/{repo_ref}/labels/{key}/values/{value}", opUpdateLabelValue) } diff --git a/app/api/openapi/space.go b/app/api/openapi/space.go index 089be02fa..62afc9ed6 100644 --- a/app/api/openapi/space.go +++ b/app/api/openapi/space.go @@ -446,4 +446,158 @@ func spaceOperations(reflector *openapi3.Reflector) { _ = reflector.SetJSONResponse(&opMembershipList, new(usererror.Error), http.StatusForbidden) _ = reflector.SetJSONResponse(&opMembershipList, new(usererror.Error), http.StatusNotFound) _ = reflector.Spec.AddOperation(http.MethodGet, "/spaces/{space_ref}/members", opMembershipList) + + opDefineLabel := openapi3.Operation{} + opDefineLabel.WithTags("space") + opDefineLabel.WithMapOfAnything( + map[string]interface{}{"operationId": "defineSpaceLabel"}) + _ = reflector.SetRequest(&opDefineLabel, &struct { + spaceRequest + labelRequest + }{}, http.MethodPost) + _ = reflector.SetJSONResponse(&opDefineLabel, new(types.Label), http.StatusCreated) + _ = reflector.SetJSONResponse(&opDefineLabel, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opDefineLabel, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opDefineLabel, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opDefineLabel, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opDefineLabel, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodPost, "/spaces/{space_ref}/labels", opDefineLabel) + + opSaveLabel := openapi3.Operation{} + opSaveLabel.WithTags("space") + opSaveLabel.WithMapOfAnything( + map[string]interface{}{"operationId": "saveSpaceLabel"}) + _ = reflector.SetRequest(&opSaveLabel, &struct { + spaceRequest + types.SaveInput + }{}, http.MethodPut) + _ = reflector.SetJSONResponse(&opSaveLabel, new(types.LabelWithValues), http.StatusOK) + _ = reflector.SetJSONResponse(&opSaveLabel, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opSaveLabel, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opSaveLabel, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opSaveLabel, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opSaveLabel, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodPut, "/spaces/{space_ref}/labels", opSaveLabel) + + opListLabels := openapi3.Operation{} + opListLabels.WithTags("space") + opListLabels.WithMapOfAnything( + map[string]interface{}{"operationId": "listSpaceLabels"}) + opListLabels.WithParameters( + QueryParameterPage, QueryParameterLimit, queryParameterInherited, queryParameterQueryLabel) + _ = reflector.SetRequest(&opListLabels, new(spaceRequest), http.MethodGet) + _ = reflector.SetJSONResponse(&opListLabels, new([]*types.Label), http.StatusOK) + _ = reflector.SetJSONResponse(&opListLabels, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opListLabels, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opListLabels, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opListLabels, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opListLabels, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodGet, "/spaces/{space_ref}/labels", opListLabels) + + opDeleteLabel := openapi3.Operation{} + opDeleteLabel.WithTags("space") + opDeleteLabel.WithMapOfAnything( + map[string]interface{}{"operationId": "deleteSpaceLabel"}) + _ = reflector.SetRequest(&opDeleteLabel, &struct { + spaceRequest + Key string `path:"key"` + }{}, http.MethodDelete) + _ = reflector.SetJSONResponse(&opDeleteLabel, nil, http.StatusNoContent) + _ = reflector.SetJSONResponse(&opDeleteLabel, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opDeleteLabel, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opDeleteLabel, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opDeleteLabel, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opDeleteLabel, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation( + http.MethodDelete, "/spaces/{space_ref}/labels/{key}", opDeleteLabel) + + opUpdateLabel := openapi3.Operation{} + opUpdateLabel.WithTags("space") + opUpdateLabel.WithMapOfAnything( + map[string]interface{}{"operationId": "updateSpaceLabel"}) + _ = reflector.SetRequest(&opUpdateLabel, &struct { + spaceRequest + labelRequest + Key string `path:"key"` + }{}, http.MethodPatch) + _ = reflector.SetJSONResponse(&opUpdateLabel, new(types.Label), http.StatusOK) + _ = reflector.SetJSONResponse(&opUpdateLabel, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opUpdateLabel, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opUpdateLabel, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opUpdateLabel, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opUpdateLabel, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodPatch, + "/spaces/{space_ref}/labels/{key}", opUpdateLabel) + + opDefineLabelValue := openapi3.Operation{} + opDefineLabelValue.WithTags("space") + opDefineLabelValue.WithMapOfAnything( + map[string]interface{}{"operationId": "defineSpaceLabelValue"}) + _ = reflector.SetRequest(&opDefineLabelValue, &struct { + spaceRequest + labelValueRequest + Key string `path:"key"` + }{}, http.MethodPost) + _ = reflector.SetJSONResponse(&opDefineLabelValue, new(types.LabelValue), http.StatusCreated) + _ = reflector.SetJSONResponse(&opDefineLabelValue, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opDefineLabelValue, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opDefineLabelValue, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opDefineLabelValue, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opDefineLabelValue, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodPost, + "/spaces/{space_ref}/labels/{key}/values", opDefineLabelValue) + + opListLabelValues := openapi3.Operation{} + opListLabelValues.WithTags("space") + opListLabelValues.WithMapOfAnything( + map[string]interface{}{"operationId": "listSpaceLabelValues"}) + _ = reflector.SetRequest(&opListLabelValues, &struct { + spaceRequest + Key string `path:"key"` + }{}, http.MethodGet) + _ = reflector.SetJSONResponse(&opListLabelValues, new([]*types.LabelValue), http.StatusOK) + _ = reflector.SetJSONResponse(&opListLabelValues, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opListLabelValues, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opListLabelValues, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opListLabelValues, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opListLabelValues, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodGet, + "/spaces/{space_ref}/labels/{key}/values", opListLabelValues) + + opDeleteLabelValue := openapi3.Operation{} + opDeleteLabelValue.WithTags("space") + opDeleteLabelValue.WithMapOfAnything( + map[string]interface{}{"operationId": "deleteSpaceLabelValue"}) + _ = reflector.SetRequest(&opDeleteLabelValue, &struct { + spaceRequest + Key string `path:"key"` + Value string `path:"value"` + }{}, http.MethodDelete) + _ = reflector.SetJSONResponse(&opDeleteLabelValue, nil, http.StatusNoContent) + _ = reflector.SetJSONResponse(&opDeleteLabelValue, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opDeleteLabelValue, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opDeleteLabelValue, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opDeleteLabelValue, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opDeleteLabelValue, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation( + http.MethodDelete, "/spaces/{space_ref}/labels/{key}/values/{value}", opDeleteLabelValue) + + opUpdateLabelValue := openapi3.Operation{} + opUpdateLabelValue.WithTags("space") + opUpdateLabelValue.WithMapOfAnything( + map[string]interface{}{"operationId": "updateSpaceLabelValue"}) + _ = reflector.SetRequest(&opUpdateLabelValue, &struct { + spaceRequest + labelValueRequest + Key string `path:"key"` + Value string `path:"value"` + }{}, http.MethodPatch) + _ = reflector.SetJSONResponse(&opUpdateLabelValue, new(types.LabelValue), http.StatusOK) + _ = reflector.SetJSONResponse(&opUpdateLabelValue, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opUpdateLabelValue, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opUpdateLabelValue, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opUpdateLabelValue, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opUpdateLabelValue, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodPatch, + "/spaces/{space_ref}/labels/{key}/values/{value}", opUpdateLabelValue) } diff --git a/app/api/request/common.go b/app/api/request/common.go index 18503f2b1..ba58cbbd4 100644 --- a/app/api/request/common.go +++ b/app/api/request/common.go @@ -50,6 +50,9 @@ const ( PerPageDefault = 30 PerPageMax = 100 + QueryParamInherited = "inherited" + QueryParamAssignable = "assignable" + // TODO: have shared constants across all services? HeaderRequestID = "X-Request-Id" HeaderUserAgent = "User-Agent" @@ -155,6 +158,16 @@ func ParseRecursiveFromQuery(r *http.Request) (bool, error) { return QueryParamAsBoolOrDefault(r, QueryParamRecursive, false) } +// ParseInheritedFromQuery extracts the inherited option from the URL query. +func ParseInheritedFromQuery(r *http.Request) (bool, error) { + return QueryParamAsBoolOrDefault(r, QueryParamInherited, false) +} + +// ParseAssignableFromQuery extracts the assignable option from the URL query. +func ParseAssignableFromQuery(r *http.Request) (bool, error) { + return QueryParamAsBoolOrDefault(r, QueryParamAssignable, false) +} + // GetDeletedAtFromQueryOrError gets the exact resource deletion timestamp from the query. func GetDeletedAtFromQueryOrError(r *http.Request) (int64, error) { return QueryParamAsPositiveInt64OrError(r, QueryParamDeletedAt) diff --git a/app/api/request/label.go b/app/api/request/label.go new file mode 100644 index 000000000..39cc36086 --- /dev/null +++ b/app/api/request/label.go @@ -0,0 +1,67 @@ +// 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 request + +import ( + "net/http" + + "github.com/harness/gitness/types" +) + +const ( + PathParamLabelKey = "label_key" + PathParamLabelValue = "label_value" + PathParamLabelID = "label_id" +) + +func GetLabelKeyFromPath(r *http.Request) (string, error) { + return EncodedPathParamOrError(r, PathParamLabelKey) +} + +func GetLabelValueFromPath(r *http.Request) (string, error) { + return EncodedPathParamOrError(r, PathParamLabelValue) +} + +func GetLabelIDFromPath(r *http.Request) (int64, error) { + return PathParamAsPositiveInt64(r, PathParamLabelID) +} + +// ParseLabelFilter extracts the label filter from the url. +func ParseLabelFilter(r *http.Request) (*types.LabelFilter, error) { + // inherited is used to list labels from parent scopes + inherited, err := ParseInheritedFromQuery(r) + if err != nil { + return nil, err + } + + return &types.LabelFilter{ + Inherited: inherited, + ListQueryFilter: ParseListQueryFilterFromRequest(r), + }, nil +} + +// ParseAssignableLabelFilter extracts the assignable label filter from the url. +func ParseAssignableLabelFilter(r *http.Request) (*types.AssignableLabelFilter, error) { + // assignable is used to list all labels assignable to pullreq + assignable, err := ParseAssignableFromQuery(r) + if err != nil { + return nil, err + } + + return &types.AssignableLabelFilter{ + Assignable: assignable, + ListQueryFilter: ParseListQueryFilterFromRequest(r), + }, nil +} diff --git a/app/api/request/util.go b/app/api/request/util.go index cbae5aa0b..24753a853 100644 --- a/app/api/request/util.go +++ b/app/api/request/util.go @@ -18,6 +18,7 @@ import ( "errors" "fmt" "net/http" + "net/url" "strconv" "github.com/harness/gitness/app/api/usererror" @@ -72,6 +73,24 @@ func PathParamOrError(r *http.Request, paramName string) (string, error) { return val, nil } +// EncodedPathParamOrError tries to retrieve the parameter from the request and +// returns the parameter if it exists and is not empty, otherwise returns an error, +// then it tries to URL decode parameter value, +// and returns decoded value, or an error on decoding failure. +func EncodedPathParamOrError(r *http.Request, paramName string) (string, error) { + val, err := PathParamOrError(r, paramName) + if err != nil { + return "", err + } + + decoded, err := url.PathUnescape(val) + if err != nil { + return "", usererror.BadRequestf("Value %s for param %s has incorrect encoding", val, paramName) + } + + return decoded, nil +} + // PathParamOrEmpty retrieves the path parameter or returns an empty string otherwise. func PathParamOrEmpty(r *http.Request, paramName string) string { val, ok := PathParam(r, paramName) diff --git a/app/api/usererror/translate.go b/app/api/usererror/translate.go index 08b753903..ee5b841a1 100644 --- a/app/api/usererror/translate.go +++ b/app/api/usererror/translate.go @@ -91,8 +91,8 @@ func Translate(ctx context.Context, err error) *Error { log.Ctx(ctx).Warn().Err(appError.Err).Msgf("Application error translation is omitting internal details.") } - return NewWithPayload(httpStatusCode( - appError.Status), + return NewWithPayload( + httpStatusCode(appError.Status), appError.Message, appError.Details, ) diff --git a/app/router/api.go b/app/router/api.go index b130fe653..1d22dbfc3 100644 --- a/app/router/api.go +++ b/app/router/api.go @@ -227,7 +227,12 @@ func setupRoutesV1WithAuth(r chi.Router, } // nolint: revive // it's the app context, it shouldn't be the first argument -func setupSpaces(r chi.Router, appCtx context.Context, spaceCtrl *space.Controller) { +func setupSpaces( + r chi.Router, + appCtx context.Context, + spaceCtrl *space.Controller, + +) { r.Route("/spaces", func(r chi.Router) { // Create takes path and parentId via body, not uri r.Post("/", handlerspace.HandleCreate(spaceCtrl)) @@ -264,6 +269,26 @@ func setupSpaces(r chi.Router, appCtx context.Context, spaceCtrl *space.Controll r.Patch("/", handlerspace.HandleMembershipUpdate(spaceCtrl)) }) }) + + r.Route("/labels", func(r chi.Router) { + r.Post("/", handlerspace.HandleDefineLabel(spaceCtrl)) + r.Get("/", handlerspace.HandleListLabels(spaceCtrl)) + r.Put("/", handlerspace.HandleSaveLabel(spaceCtrl)) + + r.Route(fmt.Sprintf("/{%s}", request.PathParamLabelKey), func(r chi.Router) { + r.Delete("/", handlerspace.HandleDeleteLabel(spaceCtrl)) + r.Patch("/", handlerspace.HandleUpdateLabel(spaceCtrl)) + + r.Route("/values", func(r chi.Router) { + r.Post("/", handlerspace.HandleDefineLabelValue(spaceCtrl)) + r.Get("/", handlerspace.HandleListLabelValues(spaceCtrl)) + r.Route(fmt.Sprintf("/{%s}", request.PathParamLabelValue), func(r chi.Router) { + r.Delete("/", handlerspace.HandleDeleteLabelValue(spaceCtrl)) + r.Patch("/", handlerspace.HandleUpdateLabelValue(spaceCtrl)) + }) + }) + }) + }) }) }) } @@ -385,6 +410,26 @@ func setupRepos(r chi.Router, SetupUploads(r, uploadCtrl) SetupRules(r, repoCtrl) + + r.Route("/labels", func(r chi.Router) { + r.Post("/", handlerrepo.HandleDefineLabel(repoCtrl)) + r.Get("/", handlerrepo.HandleListLabels(repoCtrl)) + r.Put("/", handlerrepo.HandleSaveLabel(repoCtrl)) + + r.Route(fmt.Sprintf("/{%s}", request.PathParamLabelKey), func(r chi.Router) { + r.Delete("/", handlerrepo.HandleDeleteLabel(repoCtrl)) + r.Patch("/", handlerrepo.HandleUpdateLabel(repoCtrl)) + + r.Route("/values", func(r chi.Router) { + r.Post("/", handlerrepo.HandleDefineLabelValue(repoCtrl)) + r.Get("/", handlerrepo.HandleListLabelValues(repoCtrl)) + r.Route(fmt.Sprintf("/{%s}", request.PathParamLabelValue), func(r chi.Router) { + r.Delete("/", handlerrepo.HandleDeleteLabelValue(repoCtrl)) + r.Patch("/", handlerrepo.HandleUpdateLabelValue(repoCtrl)) + }) + }) + }) + }) }) }) } @@ -565,6 +610,14 @@ func SetupPullReq(r chi.Router, pullreqCtrl *pullreq.Controller) { r.Get("/diff", handlerpullreq.HandleDiff(pullreqCtrl)) r.Post("/diff", handlerpullreq.HandleDiff(pullreqCtrl)) r.Get("/checks", handlerpullreq.HandleCheckList(pullreqCtrl)) + + r.Route("/labels", func(r chi.Router) { + r.Put("/", handlerpullreq.HandleAssignLabel(pullreqCtrl)) + r.Get("/", handlerpullreq.HandleListLabels(pullreqCtrl)) + r.Route(fmt.Sprintf("/{%s}", request.PathParamLabelID), func(r chi.Router) { + r.Delete("/", handlerpullreq.HandleUnassignLabel(pullreqCtrl)) + }) + }) }) }) } diff --git a/app/services/label/label.go b/app/services/label/label.go new file mode 100644 index 000000000..db1060955 --- /dev/null +++ b/app/services/label/label.go @@ -0,0 +1,305 @@ +// 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 label + +import ( + "context" + "fmt" + "sort" + "time" + + "github.com/harness/gitness/errors" + "github.com/harness/gitness/store" + "github.com/harness/gitness/types" +) + +func (s *Service) Define( + ctx context.Context, + principalID int64, + spaceID, repoID *int64, + in *types.DefineLabelInput, +) (*types.Label, error) { + var scope int64 + if spaceID != nil { + spaceIDs, err := s.spaceStore.GetAncestorIDs(ctx, *spaceID) + if err != nil { + return nil, fmt.Errorf("failed to get space ids hierarchy: %w", err) + } + scope = int64(len(spaceIDs)) + } + + label := newLabel(principalID, spaceID, repoID, scope, in) + + if err := s.labelStore.Define(ctx, label); err != nil { + return nil, err + } + + return label, nil +} + +func (s *Service) Update( + ctx context.Context, + principalID int64, + spaceID, repoID *int64, + key string, + in *types.UpdateLabelInput, +) (*types.Label, error) { + label, err := s.labelStore.Find(ctx, spaceID, repoID, key) + if err != nil { + return nil, fmt.Errorf("failed to find repo label: %w", err) + } + + return s.update(ctx, principalID, label, in) +} + +func (s *Service) update( + ctx context.Context, + principalID int64, + label *types.Label, + in *types.UpdateLabelInput, +) (*types.Label, error) { + label, hasChanges := applyChanges(principalID, label, in) + if !hasChanges { + return label, nil + } + + err := s.labelStore.Update(ctx, label) + if err != nil { + return nil, fmt.Errorf("failed to update label: %w", err) + } + + return label, nil +} + +//nolint:gocognit +func (s *Service) Save( + ctx context.Context, + principalID int64, + spaceID, repoID *int64, + in *types.SaveInput, +) (*types.LabelWithValues, error) { + var label *types.Label + var valuesToReturn []*types.LabelValue + var err error + + err = s.tx.WithTx(ctx, func(ctx context.Context) error { + label, err = s.labelStore.FindByID(ctx, in.Label.ID) + if err != nil { + if !errors.Is(err, store.ErrResourceNotFound) { + return err + } + label, err = s.Define(ctx, principalID, spaceID, repoID, &in.Label.DefineLabelInput) + if err != nil { + return err + } + } else { + label, err = s.update(ctx, principalID, label, &types.UpdateLabelInput{ + Key: &in.Label.Key, + Type: &label.Type, + Description: &label.Description, + Color: &in.Label.Color, + }) + if err != nil { + return err + } + } + + existingValues, err := s.labelValueStore.List(ctx, label.ID, &types.ListQueryFilter{}) + if err != nil { + return err + } + existingValuesMap := make(map[int64]*types.LabelValue, len(existingValues)) + for _, value := range existingValues { + existingValuesMap[value.ID] = value + } + + var valuesToCreate []*types.SaveLabelValueInput + valuesToUpdate := make(map[int64]*types.SaveLabelValueInput) + var valuesToDelete []string + + for _, value := range in.Values { + if _, ok := existingValuesMap[value.ID]; ok { + valuesToUpdate[value.ID] = value + } else { + valuesToCreate = append(valuesToCreate, value) + } + } + + for _, value := range existingValues { + if _, ok := valuesToUpdate[value.ID]; !ok { + valuesToDelete = append(valuesToDelete, value.Value) + } + } + + valuesToReturn = make([]*types.LabelValue, len(valuesToCreate)+len(valuesToUpdate)) + + for i, value := range valuesToCreate { + valuesToReturn[i] = newLabelValue(principalID, label.ID, &value.DefineValueInput) + if err = s.labelValueStore.Define(ctx, valuesToReturn[i]); err != nil { + return err + } + } + + i := len(valuesToCreate) + for _, value := range valuesToUpdate { + if valuesToReturn[i], err = s.updateValue(ctx, principalID, existingValuesMap[value.ID], &types.UpdateValueInput{ + Value: &value.Value, + Color: &value.Color, + }); err != nil { + return err + } + i++ + } + + if err = s.labelValueStore.DeleteMany(ctx, label.ID, valuesToDelete); err != nil { + return err + } + + if label.ValueCount, err = s.labelStore.IncrementValueCount( + ctx, label.ID, len(valuesToCreate)-len(valuesToDelete)); err != nil { + return err + } + + sort.Slice(valuesToReturn, func(i, j int) bool { + return valuesToReturn[i].Value < valuesToReturn[j].Value + }) + + return nil + }) + if err != nil { + return nil, fmt.Errorf("failed to save label: %w", err) + } + + return &types.LabelWithValues{ + Label: *label, + Values: valuesToReturn, + }, nil +} + +func (s *Service) Find( + ctx context.Context, + spaceID, repoID *int64, + key string, +) (*types.Label, error) { + return s.labelStore.Find(ctx, spaceID, repoID, key) +} + +func (s *Service) List( + ctx context.Context, + spaceID, repoID *int64, + filter *types.LabelFilter, +) ([]*types.Label, error) { + if filter.Inherited { + return s.listInScopes(ctx, spaceID, repoID, filter) + } + + return s.list(ctx, spaceID, repoID, filter) +} + +func (s *Service) list( + ctx context.Context, + spaceID, repoID *int64, + filter *types.LabelFilter, +) ([]*types.Label, error) { + if repoID != nil { + return s.labelStore.List(ctx, nil, repoID, filter) + } + return s.labelStore.List(ctx, spaceID, nil, filter) +} + +func (s *Service) listInScopes( + ctx context.Context, + spaceID, repoID *int64, + filter *types.LabelFilter, +) ([]*types.Label, error) { + var spaceIDs []int64 + var repoIDVal int64 + var err error + if repoID != nil { + spaceIDs, err = s.spaceStore.GetAncestorIDs(ctx, *spaceID) + if err != nil { + return nil, err + } + repoIDVal = *repoID + } else { + spaceIDs, err = s.spaceStore.GetAncestorIDs(ctx, *spaceID) + if err != nil { + return nil, err + } + } + + return s.labelStore.ListInScopes(ctx, repoIDVal, spaceIDs, filter) +} + +func (s *Service) Delete( + ctx context.Context, + spaceID, repoID *int64, + key string, +) error { + return s.labelStore.Delete(ctx, spaceID, repoID, key) +} + +func newLabel( + principalID int64, + spaceID, repoID *int64, + scope int64, + in *types.DefineLabelInput, +) *types.Label { + now := time.Now().UnixMilli() + return &types.Label{ + RepoID: repoID, + SpaceID: spaceID, + Scope: scope, + Key: in.Key, + Type: in.Type, + Description: in.Description, + Color: in.Color, + Created: now, + Updated: now, + CreatedBy: principalID, + UpdatedBy: principalID, + } +} + +func applyChanges(principalID int64, label *types.Label, in *types.UpdateLabelInput) (*types.Label, bool) { + hasChanges := false + + if label.UpdatedBy != principalID { + hasChanges = true + label.UpdatedBy = principalID + } + if in.Key != nil && label.Key != *in.Key { + hasChanges = true + label.Key = *in.Key + } + if in.Description != nil && label.Description != *in.Description { + hasChanges = true + label.Description = *in.Description + } + if in.Color != nil && label.Color != *in.Color { + hasChanges = true + label.Color = *in.Color + } + if in.Type != nil && label.Type != *in.Type { + hasChanges = true + label.Type = *in.Type + } + + if hasChanges { + label.Updated = time.Now().UnixMilli() + } + + return label, hasChanges +} diff --git a/app/services/label/label_pullreq.go b/app/services/label/label_pullreq.go new file mode 100644 index 000000000..ad8918509 --- /dev/null +++ b/app/services/label/label_pullreq.go @@ -0,0 +1,274 @@ +// 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 label + +import ( + "context" + "fmt" + "slices" + "sort" + "time" + + "github.com/harness/gitness/errors" + "github.com/harness/gitness/store" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" + + "golang.org/x/exp/maps" +) + +func (s *Service) AssignToPullReq( + ctx context.Context, + principalID int64, + pullreqID int64, + repoID int64, + repoParentID int64, + in *types.PullReqCreateInput, +) (*types.PullReqLabel, error) { + label, err := s.labelStore.FindByID(ctx, in.LabelID) + if err != nil { + return nil, fmt.Errorf("failed to find label by id: %w", err) + } + + if label.SpaceID != nil { + spaceIDs, err := s.spaceStore.GetAncestorIDs(ctx, repoParentID) + if err != nil { + return nil, fmt.Errorf("failed to get parent space ids: %w", err) + } + if ok := slices.Contains(spaceIDs, *label.SpaceID); !ok { + return nil, errors.NotFound( + "label %d is not defined in current space tree path", label.ID) + } + } else if label.RepoID != nil && *label.RepoID != repoID { + return nil, errors.InvalidArgument( + "label %d is not defined in current repo", label.ID) + } + + pullreqLabel := newPullReqLabel(pullreqID, principalID, in) + + if in.ValueID != nil { + labelValue, err := s.labelValueStore.FindByID(ctx, *in.ValueID) + if err != nil { + return nil, fmt.Errorf("failed to find label value by id: %w", err) + } + if label.ID != labelValue.LabelID { + return nil, errors.InvalidArgument("label value is not associated with label") + } + } + + if in.Value != "" { + valueID, err := s.getOrDefineValue(ctx, principalID, label, in.Value) + if err != nil { + return nil, err + } + pullreqLabel.ValueID = &valueID + } + + err = s.pullReqLabelAssignmentStore.Assign(ctx, pullreqLabel) + if err != nil { + return nil, fmt.Errorf("failed to assign label to pullreq: %w", err) + } + + return pullreqLabel, nil +} + +func (s *Service) getOrDefineValue( + ctx context.Context, + principalID int64, + label *types.Label, + value string, +) (int64, error) { + if label.Type != enum.LabelTypeDynamic { + return 0, errors.InvalidArgument("label doesn't allow new value assignment") + } + + labelValue, err := s.labelValueStore.FindByLabelID(ctx, label.ID, value) + if err == nil { + return labelValue.ID, nil + } + if !errors.Is(err, store.ErrResourceNotFound) { + return 0, fmt.Errorf("failed to find label value: %w", err) + } + + labelValue, err = s.DefineValue( + ctx, + principalID, + label.ID, + &types.DefineValueInput{ + Value: value, + Color: label.Color, + }, + ) + if err != nil { + return 0, fmt.Errorf("failed to create label value: %w", err) + } + + return labelValue.ID, nil +} + +func (s *Service) UnassignFromPullReq( + ctx context.Context, repoID, repoParentID, pullreqID, labelID int64, +) error { + label, err := s.labelStore.FindByID(ctx, labelID) + if err != nil { + return fmt.Errorf("failed to find label by id: %w", err) + } + + if label.RepoID != nil && *label.RepoID != repoID { + return errors.InvalidArgument( + "label %d is not defined in current repo", label.ID) + } else if label.SpaceID != nil { + spaceIDs, err := s.spaceStore.GetAncestorIDs(ctx, repoParentID) + if err != nil { + return fmt.Errorf("failed to get parent space ids: %w", err) + } + if ok := slices.Contains(spaceIDs, *label.SpaceID); !ok { + return errors.NotFound( + "label %d is not defined in current space tree path", label.ID) + } + } + + return s.pullReqLabelAssignmentStore.Unassign(ctx, pullreqID, labelID) +} + +func (s *Service) ListPullReqLabels( + ctx context.Context, + repo *types.Repository, + spaceID int64, + pullreqID int64, + filter *types.AssignableLabelFilter, +) (*types.ScopesLabels, error) { + spaces, err := s.spaceStore.GetHierarchy(ctx, spaceID) + if err != nil { + return nil, fmt.Errorf("failed to get space hierarchy: %w", err) + } + + spaceIDs := make([]int64, len(spaces)) + for i, space := range spaces { + spaceIDs[i] = space.ID + } + + scopeLabelsMap := make(map[int64]*types.ScopeData) + + pullreqAssignments, err := s.pullReqLabelAssignmentStore.ListAssigned( + ctx, pullreqID) + if err != nil { + return nil, fmt.Errorf("failed to list labels assigned to pullreq: %w", err) + } + + if !filter.Assignable { + sortedAssignments := maps.Values(pullreqAssignments) + sort.Slice(sortedAssignments, func(i, j int) bool { + if sortedAssignments[i].Key != sortedAssignments[j].Key { + return sortedAssignments[i].Key < sortedAssignments[j].Key + } + return sortedAssignments[i].Scope < sortedAssignments[j].Scope + }) + + populateScopeLabelsMap(sortedAssignments, scopeLabelsMap, repo, spaces) + return createScopeLabels(sortedAssignments, scopeLabelsMap), nil + } + + labelInfos, err := s.labelStore.ListInfosInScopes(ctx, repo.ID, spaceIDs, filter) + if err != nil { + return nil, fmt.Errorf("failed to list repo and spaces label infos: %w", err) + } + + labelIDs := make([]int64, len(labelInfos)) + for i, labelInfo := range labelInfos { + labelIDs[i] = labelInfo.ID + } + + valueInfos, err := s.labelValueStore.ListInfosByLabelIDs(ctx, labelIDs) + if err != nil { + return nil, fmt.Errorf("failed to list label value infos by label ids: %w", err) + } + + allAssignments := make([]*types.LabelAssignment, len(labelInfos)) + for i, labelInfo := range labelInfos { + assignment, ok := pullreqAssignments[labelInfo.ID] + if !ok { + assignment = &types.LabelAssignment{ + LabelInfo: *labelInfo, + } + } + assignment.LabelInfo.Assigned = &ok + allAssignments[i] = assignment + allAssignments[i].Values = valueInfos[labelInfo.ID] + } + + populateScopeLabelsMap(allAssignments, scopeLabelsMap, repo, spaces) + return createScopeLabels(allAssignments, scopeLabelsMap), nil +} + +func populateScopeLabelsMap( + assignments []*types.LabelAssignment, + scopeLabelsMap map[int64]*types.ScopeData, + repo *types.Repository, + spaces []*types.Space, +) { + for _, assignment := range assignments { + _, ok := scopeLabelsMap[assignment.Scope] + if ok { + continue + } + scopeLabelsMap[assignment.Scope] = &types.ScopeData{Scope: assignment.Scope} + if assignment.Scope == 0 { + scopeLabelsMap[assignment.Scope].Repo = repo + } else { + for _, space := range spaces { + if space.ID == *assignment.SpaceID { + scopeLabelsMap[assignment.Scope].Space = space + } + } + } + } +} + +func createScopeLabels( + assignments []*types.LabelAssignment, + scopeLabelsMap map[int64]*types.ScopeData, +) *types.ScopesLabels { + scopeData := make([]*types.ScopeData, len(scopeLabelsMap)) + for i, scopeLabel := range maps.Values(scopeLabelsMap) { + scopeData[i] = scopeLabel + } + + sort.Slice(scopeData, func(i, j int) bool { + return scopeData[i].Scope < scopeData[j].Scope + }) + + return &types.ScopesLabels{ + LabelData: assignments, + ScopeData: scopeData, + } +} + +func newPullReqLabel( + pullreqID int64, + principalID int64, + in *types.PullReqCreateInput, +) *types.PullReqLabel { + now := time.Now().UnixMilli() + return &types.PullReqLabel{ + PullReqID: pullreqID, + LabelID: in.LabelID, + ValueID: in.ValueID, + Created: now, + Updated: now, + CreatedBy: principalID, + UpdatedBy: principalID, + } +} diff --git a/app/services/label/label_value.go b/app/services/label/label_value.go new file mode 100644 index 000000000..8ca5d1b27 --- /dev/null +++ b/app/services/label/label_value.go @@ -0,0 +1,167 @@ +// 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 label + +import ( + "context" + "fmt" + "time" + + "github.com/harness/gitness/types" +) + +func (s *Service) DefineValue( + ctx context.Context, + principalID int64, + labelID int64, + in *types.DefineValueInput, +) (*types.LabelValue, error) { + labelValue := newLabelValue(principalID, labelID, in) + + err := s.tx.WithTx(ctx, func(ctx context.Context) error { + if err := s.labelValueStore.Define(ctx, labelValue); err != nil { + return err + } + if _, err := s.labelStore.IncrementValueCount(ctx, labelID, 1); err != nil { + return err + } + return nil + }) + if err != nil { + return nil, err + } + + return labelValue, nil +} + +func applyValueChanges( + principalID int64, + value *types.LabelValue, + in *types.UpdateValueInput, +) (*types.LabelValue, bool) { + hasChanges := false + + if value.UpdatedBy != principalID { + hasChanges = true + value.UpdatedBy = principalID + } + + if in.Value != nil && value.Value != *in.Value { + hasChanges = true + value.Value = *in.Value + } + if in.Color != nil && value.Color != *in.Color { + hasChanges = true + value.Color = *in.Color + } + + if hasChanges { + value.Updated = time.Now().UnixMilli() + } + + return value, hasChanges +} + +func (s *Service) UpdateValue( + ctx context.Context, + principalID int64, + labelID int64, + value string, + in *types.UpdateValueInput, +) (*types.LabelValue, error) { + labelValue, err := s.labelValueStore.FindByLabelID(ctx, labelID, value) + if err != nil { + return nil, fmt.Errorf("failed to find label value: %w", err) + } + + return s.updateValue(ctx, principalID, labelValue, in) +} + +func (s *Service) updateValue( + ctx context.Context, + principalID int64, + labelValue *types.LabelValue, + in *types.UpdateValueInput, +) (*types.LabelValue, error) { + labelValue, hasChanges := applyValueChanges( + principalID, labelValue, in) + if !hasChanges { + return labelValue, nil + } + + if err := s.labelValueStore.Update(ctx, labelValue); err != nil { + return nil, fmt.Errorf("failed to update label value: %w", err) + } + + return labelValue, nil +} + +func (s *Service) ListValues( + ctx context.Context, + spaceID, repoID *int64, + labelKey string, + filter *types.ListQueryFilter, +) ([]*types.LabelValue, error) { + label, err := s.labelStore.Find(ctx, spaceID, repoID, labelKey) + if err != nil { + return nil, err + } + + return s.labelValueStore.List(ctx, label.ID, filter) +} + +func (s *Service) DeleteValue( + ctx context.Context, + spaceID, repoID *int64, + labelKey string, + value string, +) error { + label, err := s.labelStore.Find(ctx, spaceID, repoID, labelKey) + if err != nil { + return err + } + + err = s.tx.WithTx(ctx, func(ctx context.Context) error { + if err := s.labelValueStore.Delete(ctx, label.ID, value); err != nil { + return err + } + if _, err := s.labelStore.IncrementValueCount(ctx, label.ID, -1); err != nil { + return err + } + return nil + }) + if err != nil { + return err + } + + return nil +} + +func newLabelValue( + principalID int64, + labelID int64, + in *types.DefineValueInput, +) *types.LabelValue { + now := time.Now().UnixMilli() + return &types.LabelValue{ + LabelID: labelID, + Value: in.Value, + Color: in.Color, + Created: now, + Updated: now, + CreatedBy: principalID, + UpdatedBy: principalID, + } +} diff --git a/app/services/label/service.go b/app/services/label/service.go new file mode 100644 index 000000000..b9cfa1989 --- /dev/null +++ b/app/services/label/service.go @@ -0,0 +1,44 @@ +// 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 label + +import ( + "github.com/harness/gitness/app/store" + "github.com/harness/gitness/store/database/dbtx" +) + +type Service struct { + tx dbtx.Transactor + spaceStore store.SpaceStore + labelStore store.LabelStore + labelValueStore store.LabelValueStore + pullReqLabelAssignmentStore store.PullReqLabelAssignmentStore +} + +func New( + tx dbtx.Transactor, + spaceStore store.SpaceStore, + labelStore store.LabelStore, + labelValueStore store.LabelValueStore, + pullReqLabelAssignmentStore store.PullReqLabelAssignmentStore, +) *Service { + return &Service{ + tx: tx, + spaceStore: spaceStore, + labelStore: labelStore, + labelValueStore: labelValueStore, + pullReqLabelAssignmentStore: pullReqLabelAssignmentStore, + } +} diff --git a/app/services/label/wire.go b/app/services/label/wire.go new file mode 100644 index 000000000..1541f1451 --- /dev/null +++ b/app/services/label/wire.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 label + +import ( + "github.com/harness/gitness/app/store" + "github.com/harness/gitness/store/database/dbtx" + + "github.com/google/wire" +) + +var WireSet = wire.NewSet( + ProvideLabel, +) + +func ProvideLabel( + tx dbtx.Transactor, + spaceStore store.SpaceStore, + labelStore store.LabelStore, + labelValueStore store.LabelValueStore, + pullReqLabelStore store.PullReqLabelAssignmentStore, +) *Service { + return New(tx, spaceStore, labelStore, labelValueStore, pullReqLabelStore) +} diff --git a/app/store/database.go b/app/store/database.go index c1330b04b..7ef654535 100644 --- a/app/store/database.go +++ b/app/store/database.go @@ -166,6 +166,14 @@ type ( // GetRootSpace returns a space where space_parent_id is NULL. GetRootSpace(ctx context.Context, spaceID int64) (*types.Space, error) + // GetAncestorIDs returns a list of all space IDs along the recursive path to the root space. + GetAncestorIDs(ctx context.Context, spaceID int64) ([]int64, error) + + GetHierarchy( + ctx context.Context, + spaceID int64, + ) ([]*types.Space, error) + // Create creates a new space Create(ctx context.Context, space *types.Space) error @@ -930,4 +938,108 @@ type ( gitspaceConfigID int64, ) (*types.GitspaceEvent, error) } + + LabelStore interface { + // Define defines a label. + Define(ctx context.Context, lbl *types.Label) error + + // Update updates a label. + Update(ctx context.Context, lbl *types.Label) error + + // Find finds a label defined in a specified space/repo with a specified key. + Find( + ctx context.Context, + spaceID, repoID *int64, + key string, + ) (*types.Label, error) + + // Delete deletes a label defined in a specified space/repo with a specified key. + Delete(ctx context.Context, spaceID, repoID *int64, key string) error + + // List list labels defined in a specified space/repo. + List( + ctx context.Context, + spaceID, repoID *int64, + filter *types.LabelFilter, + ) ([]*types.Label, error) + + // FindByID finds label with a specified id. + FindByID(ctx context.Context, id int64) (*types.Label, error) + + // ListInScopes lists labels defined in repo and/or specified spaces. + ListInScopes( + ctx context.Context, + repoID int64, + scopeIDs []int64, + filter *types.LabelFilter, + ) ([]*types.Label, error) + + // ListInfosInScopes lists label infos defined in repo and/or specified spaces. + ListInfosInScopes( + ctx context.Context, + repoID int64, + spaceIDs []int64, + filter *types.AssignableLabelFilter, + ) ([]*types.LabelInfo, error) + + // IncrementValueCount increments count of values defined for a specified label. + IncrementValueCount(ctx context.Context, labelID int64, increment int) (int64, error) + } + + LabelValueStore interface { + // Define defines a label value. + Define(ctx context.Context, lbl *types.LabelValue) error + + // Update updates a label value. + Update(ctx context.Context, lblVal *types.LabelValue) error + + // Delete deletes a label value associated with a specified label. + Delete(ctx context.Context, labelID int64, value string) error + + // Delete deletes specified label values associated with a specified label. + DeleteMany(ctx context.Context, labelID int64, values []string) error + + // FindByLabelID finds a label value defined for a specified label. + FindByLabelID( + ctx context.Context, + labelID int64, + value string, + ) (*types.LabelValue, error) + + // List lists label values defined for a specified label. + List( + ctx context.Context, + labelID int64, + opts *types.ListQueryFilter, + ) ([]*types.LabelValue, error) + + // FindByID finds label value with a specified id. + FindByID(ctx context.Context, id int64) (*types.LabelValue, error) + + // ListInfosByLabelIDs list label infos by a specified label id. + ListInfosByLabelIDs( + ctx context.Context, + labelIDs []int64, + ) (map[int64][]*types.LabelValueInfo, error) + + Upsert(ctx context.Context, lblVal *types.LabelValue) error + } + + PullReqLabelAssignmentStore interface { + // Assign assigns a label to a pullreq. + Assign(ctx context.Context, label *types.PullReqLabel) error + + // Unassign removes a label from a pullreq with a specified id. + Unassign(ctx context.Context, pullreqID int64, labelID int64) error + + // ListAssigned list labels assigned to a specified pullreq. + ListAssigned(ctx context.Context, pullreqID int64) (map[int64]*types.LabelAssignment, error) + + // Find finds a label assigned to a pullreq with a specified id. + FindByLabelID( + ctx context.Context, + pullreqID int64, + labelID int64, + ) (*types.PullReqLabel, error) + } ) diff --git a/app/store/database/label.go b/app/store/database/label.go new file mode 100644 index 000000000..53565caf6 --- /dev/null +++ b/app/store/database/label.go @@ -0,0 +1,398 @@ +// 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 database + +import ( + "context" + "strings" + + "github.com/harness/gitness/app/store" + "github.com/harness/gitness/store/database" + "github.com/harness/gitness/store/database/dbtx" + "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" +) + +const ( + labelColumns = ` + label_space_id + ,label_repo_id + ,label_scope + ,label_key + ,label_description + ,label_type + ,label_color + ,label_created + ,label_updated + ,label_created_by + ,label_updated_by` + + labelSelectBase = `SELECT label_id, ` + labelColumns + ` FROM labels` +) + +type label struct { + ID int64 `db:"label_id"` + SpaceID null.Int `db:"label_space_id"` + RepoID null.Int `db:"label_repo_id"` + Scope int64 `db:"label_scope"` + Key string `db:"label_key"` + Description string `db:"label_description"` + Type enum.LabelType `db:"label_type"` + Color enum.LabelColor `db:"label_color"` + ValueCount int64 `db:"label_value_count"` + Created int64 `db:"label_created"` + Updated int64 `db:"label_updated"` + CreatedBy int64 `db:"label_created_by"` + UpdatedBy int64 `db:"label_updated_by"` +} + +type labelInfo struct { + LabelID int64 `db:"label_id"` + SpaceID null.Int `db:"label_space_id"` + RepoID null.Int `db:"label_repo_id"` + Scope int64 `db:"label_scope"` + Key string `db:"label_key"` + Type enum.LabelType `db:"label_type"` + LabelColor enum.LabelColor `db:"label_color"` +} + +type labelStore struct { + db *sqlx.DB +} + +func NewLabelStore( + db *sqlx.DB, +) store.LabelStore { + return &labelStore{ + db: db, + } +} + +var _ store.LabelStore = (*labelStore)(nil) + +func (s *labelStore) Define(ctx context.Context, lbl *types.Label) error { + const sqlQuery = ` + INSERT INTO labels (` + labelColumns + `)` + ` + values ( + :label_space_id + ,:label_repo_id + ,:label_scope + ,:label_key + ,:label_description + ,:label_type + ,:label_color + ,:label_created + ,:label_updated + ,:label_created_by + ,:label_updated_by + ) + RETURNING label_id` + + db := dbtx.GetAccessor(ctx, s.db) + query, args, err := db.BindNamed(sqlQuery, mapInternalLabel(lbl)) + if err != nil { + return database.ProcessSQLErrorf(ctx, err, "Failed to bind query") + } + + if err = db.QueryRowContext(ctx, query, args...).Scan(&lbl.ID); err != nil { + return database.ProcessSQLErrorf(ctx, err, "Failed to create label") + } + + return nil +} + +func (s *labelStore) Update(ctx context.Context, lbl *types.Label) error { + const sqlQuery = ` + UPDATE labels SET + label_key = :label_key + ,label_description = :label_description + ,label_type = :label_type + ,label_color = :label_color + ,label_updated = :label_updated + ,label_updated_by = :label_updated_by + WHERE label_id = :label_id` + + db := dbtx.GetAccessor(ctx, s.db) + query, args, err := db.BindNamed(sqlQuery, mapInternalLabel(lbl)) + if err != nil { + return database.ProcessSQLErrorf(ctx, err, "Failed to bind query") + } + + if _, err := db.ExecContext(ctx, query, args...); err != nil { + return database.ProcessSQLErrorf(ctx, err, "Failed to update label") + } + + return nil +} + +func (s *labelStore) IncrementValueCount( + ctx context.Context, + labelID int64, + increment int, +) (int64, error) { + const sqlQuery = ` + UPDATE labels + SET label_value_count = label_value_count + $1 + WHERE label_id = $2 + RETURNING label_value_count + ` + + db := dbtx.GetAccessor(ctx, s.db) + + var valueCount int64 + if err := db.QueryRowContext(ctx, sqlQuery, increment, labelID).Scan(&valueCount); err != nil { + return 0, database.ProcessSQLErrorf(ctx, err, "Failed to increment label_value_count") + } + + return valueCount, nil +} + +func (s *labelStore) Find( + ctx context.Context, + spaceID, repoID *int64, + key string, +) (*types.Label, error) { + const sqlQuery = labelSelectBase + ` + WHERE (label_space_id = $1 OR label_repo_id = $2) AND LOWER(label_key) = LOWER($3)` + + db := dbtx.GetAccessor(ctx, s.db) + + var dst label + if err := db.GetContext(ctx, &dst, sqlQuery, spaceID, repoID, key); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find label") + } + + return mapLabel(&dst), nil +} + +func (s *labelStore) FindByID(ctx context.Context, id int64) (*types.Label, error) { + const sqlQuery = labelSelectBase + ` + WHERE label_id = $1` + + db := dbtx.GetAccessor(ctx, s.db) + + var dst label + if err := db.GetContext(ctx, &dst, sqlQuery, id); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find label") + } + + return mapLabel(&dst), nil +} + +func (s *labelStore) Delete(ctx context.Context, spaceID, repoID *int64, name string) error { + const sqlQuery = ` + DELETE FROM labels + WHERE (label_space_id = $1 OR label_repo_id = $2) AND LOWER(label_key) = LOWER($3)` + + db := dbtx.GetAccessor(ctx, s.db) + + if _, err := db.ExecContext(ctx, sqlQuery, spaceID, repoID, name); err != nil { + return database.ProcessSQLErrorf(ctx, err, "Failed to delete label") + } + + return nil +} + +// List returns a list of pull requests for a repo or space. +func (s *labelStore) List( + ctx context.Context, + spaceID, repoID *int64, + filter *types.LabelFilter, +) ([]*types.Label, error) { + stmt := database.Builder. + Select(`label_id, ` + labelColumns + `, label_value_count`). + From("labels"). + OrderBy("label_key") + + stmt = stmt.Where("(label_space_id = ? OR label_repo_id = ?)", spaceID, repoID) + stmt = stmt.Limit(database.Limit(filter.Size)) + stmt = stmt.Offset(database.Offset(filter.Page, filter.Size)) + if filter.Query != "" { + stmt = stmt.Where( + "LOWER(label_key) LIKE ?", strings.ToLower(filter.Query)) + } + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert query to sql") + } + + db := dbtx.GetAccessor(ctx, s.db) + + var dst []*label + if err = db.SelectContext(ctx, &dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed to list labels") + } + + return mapSliceLabel(dst), nil +} + +func (s *labelStore) ListInScopes( + ctx context.Context, + repoID int64, + scopeIDs []int64, + filter *types.LabelFilter, +) ([]*types.Label, error) { + stmt := database.Builder. + Select(`label_id, ` + labelColumns + `, label_value_count`). + From("labels") + + stmt = stmt.Where(squirrel.Or{ + squirrel.Eq{"label_space_id": scopeIDs}, + squirrel.Eq{"label_repo_id": repoID}, + }). + OrderBy("label_key"). + OrderBy("label_scope") + + stmt = stmt.Limit(database.Limit(filter.Size)) + stmt = stmt.Offset(database.Offset(filter.Page, filter.Size)) + if filter.Query != "" { + stmt = stmt.Where( + "LOWER(label_key) LIKE ?", strings.ToLower(filter.Query)) + } + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert query to sql") + } + + db := dbtx.GetAccessor(ctx, s.db) + + var dst []*label + if err = db.SelectContext(ctx, &dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed to list labels in hierarchy") + } + + return mapSliceLabel(dst), nil +} + +func (s *labelStore) ListInfosInScopes( + ctx context.Context, + repoID int64, + spaceIDs []int64, + filter *types.AssignableLabelFilter, +) ([]*types.LabelInfo, error) { + stmt := database.Builder. + Select(` + label_id + ,label_space_id + ,label_repo_id + ,label_scope + ,label_key + ,label_type + ,label_color`). + From("labels"). + Where(squirrel.Or{ + squirrel.Eq{"label_space_id": spaceIDs}, + squirrel.Eq{"label_repo_id": repoID}, + }). + OrderBy("label_key"). + OrderBy("label_scope") + + stmt = stmt.Limit(database.Limit(filter.Size)) + stmt = stmt.Offset(database.Offset(filter.Page, filter.Size)) + if filter.Query != "" { + stmt = stmt.Where( + "LOWER(label_key) LIKE ?", strings.ToLower(filter.Query)) + } + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert query to sql") + } + + db := dbtx.GetAccessor(ctx, s.db) + + var dst []*labelInfo + if err = db.SelectContext(ctx, &dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed to list labels") + } + + return mapLabelInfos(dst), nil +} + +func mapLabel(lbl *label) *types.Label { + return &types.Label{ + ID: lbl.ID, + SpaceID: lbl.SpaceID.Ptr(), + RepoID: lbl.RepoID.Ptr(), + Scope: lbl.Scope, + Key: lbl.Key, + Type: lbl.Type, + Description: lbl.Description, + ValueCount: lbl.ValueCount, + Color: lbl.Color, + Created: lbl.Created, + Updated: lbl.Updated, + CreatedBy: lbl.CreatedBy, + UpdatedBy: lbl.UpdatedBy, + } +} + +func mapSliceLabel(dbLabels []*label) []*types.Label { + result := make([]*types.Label, len(dbLabels)) + + for i, lbl := range dbLabels { + result[i] = mapLabel(lbl) + } + + return result +} + +func mapInternalLabel(lbl *types.Label) *label { + return &label{ + ID: lbl.ID, + SpaceID: null.IntFromPtr(lbl.SpaceID), + RepoID: null.IntFromPtr(lbl.RepoID), + Scope: lbl.Scope, + Key: lbl.Key, + Description: lbl.Description, + Type: lbl.Type, + Color: lbl.Color, + Created: lbl.Created, + Updated: lbl.Updated, + CreatedBy: lbl.CreatedBy, + UpdatedBy: lbl.UpdatedBy, + } +} + +func mapLabelInfo(internal *labelInfo) *types.LabelInfo { + return &types.LabelInfo{ + ID: internal.LabelID, + RepoID: internal.RepoID.Ptr(), + SpaceID: internal.SpaceID.Ptr(), + Scope: internal.Scope, + Key: internal.Key, + Type: internal.Type, + Color: internal.LabelColor, + } +} + +func mapLabelInfos( + dbLabels []*labelInfo, +) []*types.LabelInfo { + result := make([]*types.LabelInfo, len(dbLabels)) + + for i, lbl := range dbLabels { + result[i] = mapLabelInfo(lbl) + } + + return result +} diff --git a/app/store/database/label_pullreq.go b/app/store/database/label_pullreq.go new file mode 100644 index 000000000..8cb2f3636 --- /dev/null +++ b/app/store/database/label_pullreq.go @@ -0,0 +1,196 @@ +// 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 database + +import ( + "context" + + "github.com/harness/gitness/app/store" + "github.com/harness/gitness/store/database" + "github.com/harness/gitness/store/database/dbtx" + "github.com/harness/gitness/types" + + "github.com/guregu/null" + "github.com/jmoiron/sqlx" +) + +var _ store.PullReqLabelAssignmentStore = (*pullReqLabelStore)(nil) + +func NewPullReqLabelStore(db *sqlx.DB) store.PullReqLabelAssignmentStore { + return &pullReqLabelStore{ + db: db, + } +} + +type pullReqLabelStore struct { + db *sqlx.DB +} + +type pullReqLabel struct { + PullReqID int64 `db:"pullreq_label_pullreq_id"` + LabelID int64 `db:"pullreq_label_label_id"` + LabelValueID null.Int `db:"pullreq_label_label_value_id"` + Created int64 `db:"pullreq_label_created"` + Updated int64 `db:"pullreq_label_updated"` + CreatedBy int64 `db:"pullreq_label_created_by"` + UpdatedBy int64 `db:"pullreq_label_updated_by"` +} + +const ( + pullReqLabelColumns = ` + pullreq_label_pullreq_id + ,pullreq_label_label_id + ,pullreq_label_label_value_id + ,pullreq_label_created + ,pullreq_label_updated + ,pullreq_label_created_by + ,pullreq_label_updated_by` +) + +func (s *pullReqLabelStore) Assign(ctx context.Context, label *types.PullReqLabel) error { + const sqlQuery = ` + INSERT INTO pullreq_labels (` + pullReqLabelColumns + `) + values ( + :pullreq_label_pullreq_id + ,:pullreq_label_label_id + ,:pullreq_label_label_value_id + ,:pullreq_label_created + ,:pullreq_label_updated + ,:pullreq_label_created_by + ,:pullreq_label_updated_by + ) + ON CONFLICT (pullreq_label_pullreq_id, pullreq_label_label_id) + DO UPDATE SET + pullreq_label_label_value_id = EXCLUDED.pullreq_label_label_value_id, + pullreq_label_updated = EXCLUDED.pullreq_label_updated, + pullreq_label_updated_by = EXCLUDED.pullreq_label_updated_by + RETURNING pullreq_label_created, pullreq_label_created_by + ` + + db := dbtx.GetAccessor(ctx, s.db) + + query, args, err := db.BindNamed(sqlQuery, mapInternalPullReqLabel(label)) + if err != nil { + return database.ProcessSQLErrorf(ctx, err, "failed to bind query") + } + + if err = db.QueryRowContext(ctx, query, args...).Scan(&label.Created, &label.CreatedBy); err != nil { + return database.ProcessSQLErrorf(ctx, err, "failed to create pull request label") + } + + return nil +} + +func (s *pullReqLabelStore) Unassign(ctx context.Context, pullreqID int64, labelID int64) error { + const sqlQuery = ` + DELETE FROM pullreq_labels + WHERE pullreq_label_pullreq_id = $1 AND pullreq_label_label_id = $2` + + db := dbtx.GetAccessor(ctx, s.db) + + if _, err := db.ExecContext(ctx, sqlQuery, pullreqID, labelID); err != nil { + return database.ProcessSQLErrorf(ctx, err, "failed to delete pullreq label") + } + + return nil +} + +func (s *pullReqLabelStore) FindByLabelID( + ctx context.Context, + pullreqID int64, + labelID int64, +) (*types.PullReqLabel, error) { + const sqlQuery = `SELECT ` + pullReqLabelColumns + ` + FROM pullreq_labels + WHERE pullreq_label_pullreq_id = $1 AND pullreq_label_label_id = $2` + + db := dbtx.GetAccessor(ctx, s.db) + + var dst pullReqLabel + if err := db.GetContext(ctx, &dst, sqlQuery, pullreqID, labelID); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "failed to delete pullreq label") + } + + return mapPullReqLabel(&dst), nil +} + +func (s *pullReqLabelStore) ListAssigned( + ctx context.Context, + pullreqID int64, +) (map[int64]*types.LabelAssignment, error) { + const sqlQueryAssigned = ` + SELECT + label_id + ,label_repo_id + ,label_space_id + ,label_key + ,label_value_id + ,label_value_label_id + ,label_value_value + ,label_color + ,label_value_color + ,label_scope + ,label_type + FROM pullreq_labels prl + INNER JOIN labels l ON prl.pullreq_label_label_id = l.label_id + LEFT JOIN label_values lv ON prl.pullreq_label_label_value_id = lv.label_value_id + WHERE prl.pullreq_label_pullreq_id = $1` + + db := dbtx.GetAccessor(ctx, s.db) + + var dst []*struct { + labelInfo + labelValueInfo + } + if err := db.SelectContext(ctx, &dst, sqlQueryAssigned, pullreqID); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "failed to find label") + } + + ret := make(map[int64]*types.LabelAssignment, len(dst)) + for _, res := range dst { + li := mapLabelInfo(&res.labelInfo) + lvi := mapLabeValuelInfo(&res.labelValueInfo) + ret[li.ID] = &types.LabelAssignment{ + LabelInfo: *li, + AssignedValue: lvi, + } + } + + return ret, nil +} + +func mapInternalPullReqLabel(lbl *types.PullReqLabel) *pullReqLabel { + return &pullReqLabel{ + PullReqID: lbl.PullReqID, + LabelID: lbl.LabelID, + LabelValueID: null.IntFromPtr(lbl.ValueID), + Created: lbl.Created, + Updated: lbl.Updated, + CreatedBy: lbl.CreatedBy, + UpdatedBy: lbl.UpdatedBy, + } +} + +func mapPullReqLabel(lbl *pullReqLabel) *types.PullReqLabel { + return &types.PullReqLabel{ + PullReqID: lbl.PullReqID, + LabelID: lbl.LabelID, + ValueID: lbl.LabelValueID.Ptr(), + Created: lbl.Created, + Updated: lbl.Updated, + CreatedBy: lbl.CreatedBy, + UpdatedBy: lbl.UpdatedBy, + } +} diff --git a/app/store/database/label_value.go b/app/store/database/label_value.go new file mode 100644 index 000000000..6d23ed5b9 --- /dev/null +++ b/app/store/database/label_value.go @@ -0,0 +1,358 @@ +// 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 database + +import ( + "context" + + "github.com/harness/gitness/app/store" + "github.com/harness/gitness/store/database" + "github.com/harness/gitness/store/database/dbtx" + "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" +) + +const ( + labelValueColumns = ` + label_value_label_id + ,label_value_value + ,label_value_color + ,label_value_created + ,label_value_updated + ,label_value_created_by + ,label_value_updated_by` + + labelValueSelectBase = `SELECT label_value_id, ` + labelValueColumns + ` FROM label_values` +) + +type labelValue struct { + ID int64 `db:"label_value_id"` + LabelID int64 `db:"label_value_label_id"` + Value string `db:"label_value_value"` + Color enum.LabelColor `db:"label_value_color"` + Created int64 `db:"label_value_created"` + Updated int64 `db:"label_value_updated"` + CreatedBy int64 `db:"label_value_created_by"` + UpdatedBy int64 `db:"label_value_updated_by"` +} + +type labelValueInfo struct { + ValueID null.Int `db:"label_value_id"` + LabelID null.Int `db:"label_value_label_id"` + Value null.String `db:"label_value_value"` + ValueColor null.String `db:"label_value_color"` +} + +type labelValueStore struct { + db *sqlx.DB +} + +func NewLabelValueStore( + db *sqlx.DB, +) store.LabelValueStore { + return &labelValueStore{ + db: db, + } +} + +var _ store.LabelValueStore = (*labelValueStore)(nil) + +func (s *labelValueStore) Define(ctx context.Context, lblVal *types.LabelValue) error { + const sqlQuery = ` + INSERT INTO label_values (` + labelValueColumns + `)` + ` + values ( + :label_value_label_id + ,:label_value_value + ,:label_value_color + ,:label_value_created + ,:label_value_updated + ,:label_value_created_by + ,:label_value_updated_by + ) + RETURNING label_value_id` + + db := dbtx.GetAccessor(ctx, s.db) + + query, args, err := db.BindNamed(sqlQuery, mapInternalLabelValue(lblVal)) + if err != nil { + return database.ProcessSQLErrorf(ctx, err, "Failed to bind query") + } + + if err = db.QueryRowContext(ctx, query, args...).Scan(&lblVal.ID); err != nil { + return database.ProcessSQLErrorf(ctx, err, "Failed to create label value") + } + + return nil +} + +func (s *labelValueStore) Update(ctx context.Context, lblVal *types.LabelValue) error { + const sqlQuery = ` + UPDATE label_values SET + label_value_value = :label_value_value + ,label_value_color = :label_value_color + ,label_value_updated = :label_value_updated + ,label_value_updated_by = :label_value_updated_by + WHERE label_value_id = :label_value_id` + + db := dbtx.GetAccessor(ctx, s.db) + query, args, err := db.BindNamed(sqlQuery, mapInternalLabelValue(lblVal)) + if err != nil { + return database.ProcessSQLErrorf(ctx, err, "Failed to bind query") + } + + if _, err := db.ExecContext(ctx, query, args...); err != nil { + return database.ProcessSQLErrorf(ctx, err, "Failed to update label value") + } + + return nil +} + +func (s *labelValueStore) Upsert(ctx context.Context, lblVal *types.LabelValue) error { + const sqlQuery = ` + INSERT INTO label_values (` + labelValueColumns + `) + VALUES ( + :label_value_label_id, + :label_value_value, + :label_value_color, + :label_value_created, + :label_value_updated, + :label_value_created_by, + :label_value_updated_by + ) + ON CONFLICT (label_value_id) DO UPDATE SET + label_value_value = EXCLUDED.label_value_value, + label_value_color = EXCLUDED.label_value_color, + label_value_updated = EXCLUDED.label_value_updated, + label_value_updated_by = EXCLUDED.label_value_updated_by + RETURNING label_value_id` + + db := dbtx.GetAccessor(ctx, s.db) + + query, args, err := db.BindNamed(sqlQuery, mapInternalLabelValue(lblVal)) + if err != nil { + return database.ProcessSQLErrorf(ctx, err, "Failed to bind query") + } + + if err = db.QueryRowContext(ctx, query, args...).Scan(&lblVal.ID); err != nil { + return database.ProcessSQLErrorf(ctx, err, "Failed to upsert label value") + } + + return nil +} + +func (s *labelValueStore) Delete( + ctx context.Context, + labelID int64, + value string, +) error { + const sqlQuery = ` + DELETE FROM label_values + WHERE label_value_label_id = $1 AND LOWER(label_value_value) = LOWER($2)` + + db := dbtx.GetAccessor(ctx, s.db) + + if _, err := db.ExecContext(ctx, sqlQuery, labelID, value); err != nil { + return database.ProcessSQLErrorf(ctx, err, "Failed to delete label") + } + + return nil +} + +func (s *labelValueStore) DeleteMany( + ctx context.Context, + labelID int64, + values []string, +) error { + stmt := database.Builder. + Delete("label_values"). + Where("label_value_label_id = ?", labelID). + Where(squirrel.Eq{"label_value_value": values}) + + sql, args, err := stmt.ToSql() + if err != nil { + return errors.Wrap(err, "Failed to convert query to sql") + } + + db := dbtx.GetAccessor(ctx, s.db) + + if _, err := db.ExecContext(ctx, sql, args...); err != nil { + return database.ProcessSQLErrorf(ctx, err, "Failed to delete label") + } + + return nil +} + +// List returns a list of label values for a specified label. +func (s *labelValueStore) List( + ctx context.Context, + labelID int64, + opts *types.ListQueryFilter, +) ([]*types.LabelValue, error) { + stmt := database.Builder. + Select(`label_value_id, ` + labelValueColumns). + From("label_values") + + stmt = stmt.Where("label_value_label_id = ?", labelID) + + stmt = stmt.Limit(database.Limit(opts.Size)) + stmt = stmt.Offset(database.Offset(opts.Page, opts.Size)) + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert query to sql") + } + + db := dbtx.GetAccessor(ctx, s.db) + + var dst []*labelValue + if err = db.SelectContext(ctx, &dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Fail to list labels") + } + + return mapSliceLabelValue(dst), nil +} + +func (s *labelValueStore) ListInfosByLabelIDs( + ctx context.Context, + labelIDs []int64, +) (map[int64][]*types.LabelValueInfo, error) { + stmt := database.Builder. + Select(` + label_value_id + ,label_value_label_id + ,label_value_value + ,label_value_color + `). + From("label_values"). + Where(squirrel.Eq{"label_value_label_id": labelIDs}). + OrderBy("label_value_value") + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert query to sql") + } + + db := dbtx.GetAccessor(ctx, s.db) + + var dst []*labelValueInfo + if err = db.SelectContext(ctx, &dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Fail to list labels") + } + + valueInfos := mapLabelValuInfos(dst) + labelValueMap := make(map[int64][]*types.LabelValueInfo) + for _, info := range valueInfos { + labelValueMap[*info.LabelID] = append(labelValueMap[*info.LabelID], info) + } + + return labelValueMap, nil +} + +func (s *labelValueStore) FindByLabelID( + ctx context.Context, + labelID int64, + value string, +) (*types.LabelValue, error) { + const sqlQuery = labelValueSelectBase + ` + WHERE label_value_label_id = $1 AND LOWER(label_value_value) = LOWER($2)` + + db := dbtx.GetAccessor(ctx, s.db) + + var dst labelValue + if err := db.GetContext(ctx, &dst, sqlQuery, labelID, value); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find label") + } + + return mapLabelValue(&dst), nil +} + +func (s *labelValueStore) FindByID(ctx context.Context, id int64) (*types.LabelValue, error) { + const sqlQuery = labelValueSelectBase + ` + WHERE label_value_id = $1` + + db := dbtx.GetAccessor(ctx, s.db) + + var dst labelValue + if err := db.GetContext(ctx, &dst, sqlQuery, id); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find label") + } + + return mapLabelValue(&dst), nil +} + +func mapLabelValue(lbl *labelValue) *types.LabelValue { + return &types.LabelValue{ + ID: lbl.ID, + LabelID: lbl.LabelID, + Value: lbl.Value, + Color: lbl.Color, + Created: lbl.Created, + Updated: lbl.Updated, + CreatedBy: lbl.CreatedBy, + UpdatedBy: lbl.UpdatedBy, + } +} + +func mapSliceLabelValue(dbLabelValues []*labelValue) []*types.LabelValue { + result := make([]*types.LabelValue, len(dbLabelValues)) + + for i, lbl := range dbLabelValues { + result[i] = mapLabelValue(lbl) + } + + return result +} + +func mapInternalLabelValue(lblVal *types.LabelValue) *labelValue { + return &labelValue{ + ID: lblVal.ID, + LabelID: lblVal.LabelID, + Value: lblVal.Value, + Color: lblVal.Color, + Created: lblVal.Created, + Updated: lblVal.Updated, + CreatedBy: lblVal.CreatedBy, + UpdatedBy: lblVal.UpdatedBy, + } +} + +func mapLabeValuelInfo(internal *labelValueInfo) *types.LabelValueInfo { + if !internal.ValueID.Valid { + return nil + } + return &types.LabelValueInfo{ + ID: internal.ValueID.Ptr(), + LabelID: internal.LabelID.Ptr(), + Value: internal.Value.Ptr(), + Color: internal.ValueColor.Ptr(), + } +} + +func mapLabelValuInfos( + dbLabels []*labelValueInfo, +) []*types.LabelValueInfo { + result := make([]*types.LabelValueInfo, len(dbLabels)) + + for i, lbl := range dbLabels { + result[i] = mapLabeValuelInfo(lbl) + } + + return result +} diff --git a/app/store/database/migrate/postgres/0060_create_table_labels.down.sql b/app/store/database/migrate/postgres/0060_create_table_labels.down.sql new file mode 100644 index 000000000..49d61815a --- /dev/null +++ b/app/store/database/migrate/postgres/0060_create_table_labels.down.sql @@ -0,0 +1,3 @@ +DROP TABLE pullreq_labels; +DROP TABLE label_values; +DROP TABLE labels; diff --git a/app/store/database/migrate/postgres/0060_create_table_labels.up.sql b/app/store/database/migrate/postgres/0060_create_table_labels.up.sql new file mode 100644 index 000000000..afde2dac5 --- /dev/null +++ b/app/store/database/migrate/postgres/0060_create_table_labels.up.sql @@ -0,0 +1,78 @@ +CREATE TABLE labels ( + label_id SERIAL PRIMARY KEY, + label_space_id INTEGER DEFAULT NULL, + label_repo_id INTEGER DEFAULT NULL, + label_scope INTEGER DEFAULT 0, + label_key TEXT NOT NULL, + label_description TEXT NOT NULL DEFAULT '', + label_color TEXT NOT NULL DEFAULT 'black', + label_type TEXT NOT NULL DEFAULT 'static', + label_created BIGINT NOT NULL, + label_updated BIGINT NOT NULL, + label_created_by INTEGER NOT NULL, + label_updated_by INTEGER NOT NULL, + label_value_count INTEGER DEFAULT 0, + + CONSTRAINT fk_labels_space_id FOREIGN KEY (label_space_id) + REFERENCES spaces (space_id) ON DELETE CASCADE, + CONSTRAINT fk_labels_repo_id FOREIGN KEY (label_repo_id) + REFERENCES repositories (repo_id) ON DELETE CASCADE, + CONSTRAINT chk_label_space_or_repo + CHECK (label_space_id IS NULL OR label_repo_id IS NULL), + CONSTRAINT fk_labels_created_by FOREIGN KEY (label_created_by) + REFERENCES principals (principal_id), + CONSTRAINT fk_labels_updated_by FOREIGN KEY (label_updated_by) + REFERENCES principals (principal_id) +); + +CREATE UNIQUE INDEX labels_space_id_key +ON labels(label_space_id, LOWER(label_key)) +WHERE label_space_id IS NOT NULL; + +CREATE UNIQUE INDEX labels_repo_id_key +ON labels(label_repo_id, LOWER(label_key)) +WHERE label_repo_id IS NOT NULL; + +CREATE TABLE label_values ( + label_value_id SERIAL PRIMARY KEY, + label_value_label_id INTEGER NOT NULL, + label_value_value TEXT NOT NULL, + label_value_color TEXT NOT NULL, + label_value_created BIGINT NOT NULL, + label_value_updated BIGINT NOT NULL, + label_value_created_by INTEGER NOT NULL, + label_value_updated_by INTEGER NOT NULL, + + CONSTRAINT fk_label_values_label_id FOREIGN KEY (label_value_label_id) + REFERENCES labels (label_id) ON DELETE CASCADE, + CONSTRAINT fk_label_values_created_by FOREIGN KEY (label_value_created_by) + REFERENCES principals (principal_id), + CONSTRAINT fk_labels_values_updated_by FOREIGN KEY (label_value_updated_by) + REFERENCES principals (principal_id) +); + +CREATE UNIQUE INDEX unique_label_value_label_id_value +ON label_values(label_value_label_id, LOWER(label_value_value)); + +CREATE TABLE pullreq_labels ( + pullreq_label_pullreq_id INTEGER NOT NULL, + pullreq_label_label_id INTEGER NOT NULL, + pullreq_label_label_value_id INTEGER DEFAULT NULL, + pullreq_label_created BIGINT NOT NULL, + pullreq_label_updated BIGINT NOT NULL, + pullreq_label_created_by INTEGER NOT NULL, + pullreq_label_updated_by INTEGER NOT NULL, + + CONSTRAINT fk_pullreq_labels_pullreq_id FOREIGN KEY (pullreq_label_pullreq_id) + REFERENCES pullreqs (pullreq_id) ON DELETE CASCADE, + CONSTRAINT fk_pullreq_labels_label_id FOREIGN KEY (pullreq_label_label_id) + REFERENCES labels (label_id) ON DELETE CASCADE, + CONSTRAINT fk_pullreq_labels_label_value_id FOREIGN KEY (pullreq_label_label_value_id) + REFERENCES label_values (label_value_id) ON DELETE SET NULL, + CONSTRAINT fk_pullreq_labels_created_by FOREIGN KEY (pullreq_label_created_by) + REFERENCES principals (principal_id), + CONSTRAINT fk_pullreq_labels_updated_by FOREIGN KEY (pullreq_label_updated_by) + REFERENCES principals (principal_id), + + PRIMARY KEY (pullreq_label_pullreq_id, pullreq_label_label_id) +); diff --git a/app/store/database/migrate/sqlite/0060_create_table_labels.down.sql b/app/store/database/migrate/sqlite/0060_create_table_labels.down.sql new file mode 100644 index 000000000..49d61815a --- /dev/null +++ b/app/store/database/migrate/sqlite/0060_create_table_labels.down.sql @@ -0,0 +1,3 @@ +DROP TABLE pullreq_labels; +DROP TABLE label_values; +DROP TABLE labels; diff --git a/app/store/database/migrate/sqlite/0060_create_table_labels.up.sql b/app/store/database/migrate/sqlite/0060_create_table_labels.up.sql new file mode 100644 index 000000000..d998c4770 --- /dev/null +++ b/app/store/database/migrate/sqlite/0060_create_table_labels.up.sql @@ -0,0 +1,78 @@ +CREATE TABLE labels ( + label_id INTEGER PRIMARY KEY AUTOINCREMENT, + label_space_id INTEGER DEFAULT NULL, + label_repo_id INTEGER DEFAULT NULL, + label_scope INTEGER DEFAULT 0, + label_key TEXT NOT NULL, + label_description TEXT NOT NULL DEFAULT '', + label_color TEXT NOT NULL DEFAULT 'black', + label_type TEXT NOT NULL DEFAULT 'static', + label_created BIGINT NOT NULL, + label_updated BIGINT NOT NULL, + label_created_by INTEGER NOT NULL, + label_updated_by INTEGER NOT NULL, + label_value_count INTEGER DEFAULT 0, + + CONSTRAINT fk_labels_space_id FOREIGN KEY (label_space_id) + REFERENCES spaces (space_id) ON DELETE CASCADE, + CONSTRAINT fk_labels_repo_id FOREIGN KEY (label_repo_id) + REFERENCES repositories (repo_id) ON DELETE CASCADE, + CONSTRAINT chk_label_space_or_repo + CHECK (label_space_id IS NULL OR label_repo_id IS NULL), + CONSTRAINT fk_labels_created_by FOREIGN KEY (label_created_by) + REFERENCES principals (principal_id), + CONSTRAINT fk_labels_updated_by FOREIGN KEY (label_updated_by) + REFERENCES principals (principal_id) +); + +CREATE UNIQUE INDEX labels_space_id_key +ON labels(label_space_id, LOWER(label_key)) +WHERE label_space_id IS NOT NULL; + +CREATE UNIQUE INDEX labels_repo_id_key +ON labels(label_repo_id, LOWER(label_key)) +WHERE label_repo_id IS NOT NULL; + +CREATE TABLE label_values ( + label_value_id INTEGER PRIMARY KEY AUTOINCREMENT, + label_value_label_id INTEGER NOT NULL, + label_value_value TEXT NOT NULL, + label_value_color TEXT NOT NULL, + label_value_created BIGINT NOT NULL, + label_value_updated BIGINT NOT NULL, + label_value_created_by INTEGER NOT NULL, + label_value_updated_by INTEGER NOT NULL, + + CONSTRAINT fk_label_values_label_id FOREIGN KEY (label_value_label_id) + REFERENCES labels (label_id) ON DELETE CASCADE, + CONSTRAINT fk_label_values_created_by FOREIGN KEY (label_value_created_by) + REFERENCES principals (principal_id), + CONSTRAINT fk_labels_values_updated_by FOREIGN KEY (label_value_updated_by) + REFERENCES principals (principal_id) +); + +CREATE UNIQUE INDEX unique_label_value_label_id_value +ON label_values(label_value_label_id, LOWER(label_value_value)); + +CREATE TABLE pullreq_labels ( + pullreq_label_pullreq_id INTEGER NOT NULL, + pullreq_label_label_id INTEGER NOT NULL, + pullreq_label_label_value_id INTEGER DEFAULT NULL, + pullreq_label_created BIGINT NOT NULL, + pullreq_label_updated BIGINT NOT NULL, + pullreq_label_created_by INTEGER NOT NULL, + pullreq_label_updated_by INTEGER NOT NULL, + + CONSTRAINT fk_pullreq_labels_pullreq_id FOREIGN KEY (pullreq_label_pullreq_id) + REFERENCES pullreqs (pullreq_id) ON DELETE CASCADE, + CONSTRAINT fk_pullreq_labels_label_id FOREIGN KEY (pullreq_label_label_id) + REFERENCES labels (label_id) ON DELETE CASCADE, + CONSTRAINT fk_pullreq_labels_label_value_id FOREIGN KEY (pullreq_label_label_value_id) + REFERENCES label_values (label_value_id) ON DELETE SET NULL, + CONSTRAINT fk_pullreq_labels_created_by FOREIGN KEY (pullreq_label_created_by) + REFERENCES principals (principal_id), + CONSTRAINT fk_pullreq_labels_updated_by FOREIGN KEY (pullreq_label_updated_by) + REFERENCES principals (principal_id), + + PRIMARY KEY (pullreq_label_pullreq_id, pullreq_label_label_id) +); diff --git a/app/store/database/space.go b/app/store/database/space.go index 7a71f73f4..2c1c179d5 100644 --- a/app/store/database/space.go +++ b/app/store/database/space.go @@ -192,9 +192,8 @@ func (s *SpaceStore) findByPathAndDeletedAt( return s.find(ctx, spaceID, &deletedAt) } -// GetRootSpace returns a space where space_parent_id is NULL. -func (s *SpaceStore) GetRootSpace(ctx context.Context, spaceID int64) (*types.Space, error) { - query := `WITH RECURSIVE SpaceHierarchy AS ( +const spaceRecursiveQuery = ` +WITH RECURSIVE SpaceHierarchy(space_hierarchy_id, space_hierarchy_parent_id) AS ( SELECT space_id, space_parent_id FROM spaces WHERE space_id = $1 @@ -203,11 +202,16 @@ func (s *SpaceStore) GetRootSpace(ctx context.Context, spaceID int64) (*types.Sp SELECT s.space_id, s.space_parent_id FROM spaces s - JOIN SpaceHierarchy h ON s.space_id = h.space_parent_id + JOIN SpaceHierarchy h ON s.space_id = h.space_hierarchy_parent_id ) -SELECT space_id -FROM SpaceHierarchy -WHERE space_parent_id IS NULL;` +` + +// GetRootSpace returns a space where space_parent_id is NULL. +func (s *SpaceStore) GetRootSpace(ctx context.Context, spaceID int64) (*types.Space, error) { + query := spaceRecursiveQuery + ` + SELECT space_hierarchy_id + FROM SpaceHierarchy + WHERE space_hierarchy_parent_id IS NULL;` db := dbtx.GetAccessor(ctx, s.db) @@ -219,6 +223,39 @@ WHERE space_parent_id IS NULL;` return s.Find(ctx, rootID) } +// GetAncestorIDs returns a list of all space IDs along the recursive path to the root space. +func (s *SpaceStore) GetAncestorIDs(ctx context.Context, spaceID int64) ([]int64, error) { + query := spaceRecursiveQuery + ` + SELECT space_hierarchy_id FROM SpaceHierarchy` + + db := dbtx.GetAccessor(ctx, s.db) + + var spaceIDs []int64 + if err := db.SelectContext(ctx, &spaceIDs, query, spaceID); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "failed to get space hierarchy") + } + + return spaceIDs, nil +} + +func (s *SpaceStore) GetHierarchy( + ctx context.Context, + spaceID int64, +) ([]*types.Space, error) { + query := spaceRecursiveQuery + ` + SELECT ` + spaceColumns + ` + FROM spaces INNER JOIN SpaceHierarchy ON space_id = space_hierarchy_id` + + db := dbtx.GetAccessor(ctx, s.db) + + var dst []*space + if err := db.SelectContext(ctx, &dst, query, spaceID); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing custom list query") + } + + return s.mapToSpaces(ctx, s.db, dst) +} + // Create a new space. func (s *SpaceStore) Create(ctx context.Context, space *types.Space) error { if space == nil { diff --git a/app/store/database/wire.go b/app/store/database/wire.go index 8e54ae2f2..943a17678 100644 --- a/app/store/database/wire.go +++ b/app/store/database/wire.go @@ -65,6 +65,9 @@ var WireSet = wire.NewSet( ProvideGitspaceConfigStore, ProvideGitspaceInstanceStore, ProvideGitspaceEventStore, + ProvideLabelStore, + ProvideLabelValueStore, + ProvidePullReqLabelStore, ) // migrator is helper function to set up the database by performing automated @@ -289,3 +292,18 @@ func ProvidePublicKeyStore(db *sqlx.DB) store.PublicKeyStore { func ProvideGitspaceEventStore(db *sqlx.DB) store.GitspaceEventStore { return NewGitspaceEventStore(db) } + +// ProvideLabelStore provides a label store. +func ProvideLabelStore(db *sqlx.DB) store.LabelStore { + return NewLabelStore(db) +} + +// ProvideLabelValueStore provides a label value store. +func ProvideLabelValueStore(db *sqlx.DB) store.LabelValueStore { + return NewLabelValueStore(db) +} + +// ProvideLabelValueStore provides a label value store. +func ProvidePullReqLabelStore(db *sqlx.DB) store.PullReqLabelAssignmentStore { + return NewPullReqLabelStore(db) +} diff --git a/cmd/gitness/wire.go b/cmd/gitness/wire.go index 7f61d800a..2ef4cc642 100644 --- a/cmd/gitness/wire.go +++ b/cmd/gitness/wire.go @@ -70,6 +70,7 @@ import ( "github.com/harness/gitness/app/services/importer" infraproviderSvc "github.com/harness/gitness/app/services/infraprovider" "github.com/harness/gitness/app/services/keywordsearch" + svclabel "github.com/harness/gitness/app/services/label" locker "github.com/harness/gitness/app/services/locker" "github.com/harness/gitness/app/services/metric" "github.com/harness/gitness/app/services/notification" @@ -135,6 +136,7 @@ func initSystem(ctx context.Context, config *types.Config) (*cliserver.System, e reposettings.WireSet, pullreq.WireSet, controllerwebhook.WireSet, + svclabel.WireSet, serviceaccount.WireSet, user.WireSet, upload.WireSet, diff --git a/cmd/gitness/wire_gen.go b/cmd/gitness/wire_gen.go index af355664d..31889e917 100644 --- a/cmd/gitness/wire_gen.go +++ b/cmd/gitness/wire_gen.go @@ -69,6 +69,7 @@ import ( "github.com/harness/gitness/app/services/importer" infraprovider2 "github.com/harness/gitness/app/services/infraprovider" "github.com/harness/gitness/app/services/keywordsearch" + "github.com/harness/gitness/app/services/label" "github.com/harness/gitness/app/services/locker" "github.com/harness/gitness/app/services/metric" "github.com/harness/gitness/app/services/notification" @@ -216,7 +217,11 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro lockerLocker := locker.ProvideLocker(mutexManager) repoIdentifier := check.ProvideRepoIdentifierCheck() repoCheck := repo.ProvideRepoCheck() - repoController := repo.ProvideController(config, transactor, provider, authorizer, repoStore, spaceStore, pipelineStore, principalStore, ruleStore, settingsService, principalInfoCache, protectionManager, gitInterface, repository, codeownersService, reporter, indexer, resourceLimiter, lockerLocker, auditService, mutexManager, repoIdentifier, repoCheck, publicaccessService) + labelStore := database.ProvideLabelStore(db) + labelValueStore := database.ProvideLabelValueStore(db) + pullReqLabelAssignmentStore := database.ProvidePullReqLabelStore(db) + labelService := label.ProvideLabel(transactor, spaceStore, labelStore, labelValueStore, pullReqLabelAssignmentStore) + repoController := repo.ProvideController(config, transactor, provider, authorizer, repoStore, spaceStore, pipelineStore, principalStore, ruleStore, settingsService, principalInfoCache, protectionManager, gitInterface, repository, codeownersService, reporter, indexer, resourceLimiter, lockerLocker, auditService, mutexManager, repoIdentifier, repoCheck, publicaccessService, labelService) reposettingsController := reposettings.ProvideController(authorizer, repoStore, settingsService, auditService) executionStore := database.ProvideExecutionStore(db) checkStore := database.ProvideCheckStore(db, principalInfoCache) @@ -247,7 +252,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro gitspaceConfigStore := database.ProvideGitspaceConfigStore(db) gitspaceInstanceStore := database.ProvideGitspaceInstanceStore(db) gitspaceService := gitspace.ProvideGitspace(transactor, gitspaceConfigStore, gitspaceInstanceStore, spaceStore) - spaceController := space.ProvideController(config, transactor, provider, streamer, spaceIdentifier, authorizer, spacePathStore, pipelineStore, secretStore, connectorStore, templateStore, spaceStore, repoStore, principalStore, repoController, membershipStore, repository, exporterRepository, resourceLimiter, publicaccessService, auditService, gitspaceService) + spaceController := space.ProvideController(config, transactor, provider, streamer, spaceIdentifier, authorizer, spacePathStore, pipelineStore, secretStore, connectorStore, templateStore, spaceStore, repoStore, principalStore, repoController, membershipStore, repository, exporterRepository, resourceLimiter, publicaccessService, auditService, gitspaceService, gitspaceConfigStore, gitspaceInstanceStore, labelService) pipelineController := pipeline.ProvideController(repoStore, triggerStore, authorizer, pipelineStore) secretController := secret.ProvideController(encrypter, secretStore, authorizer, spaceStore) triggerController := trigger.ProvideController(authorizer, triggerStore, pipelineStore, repoStore) @@ -280,7 +285,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro return nil, err } pullReq := importer.ProvidePullReqImporter(provider, gitInterface, principalStore, repoStore, pullReqStore, pullReqActivityStore, transactor) - pullreqController := pullreq2.ProvideController(transactor, provider, authorizer, pullReqStore, pullReqActivityStore, codeCommentView, pullReqReviewStore, pullReqReviewerStore, repoStore, principalStore, principalInfoCache, pullReqFileViewStore, membershipStore, checkStore, gitInterface, eventsReporter, migrator, pullreqService, protectionManager, streamer, codeownersService, lockerLocker, pullReq) + pullreqController := pullreq2.ProvideController(transactor, provider, authorizer, pullReqStore, pullReqActivityStore, codeCommentView, pullReqReviewStore, pullReqReviewerStore, repoStore, principalStore, principalInfoCache, pullReqFileViewStore, membershipStore, checkStore, gitInterface, eventsReporter, migrator, pullreqService, protectionManager, streamer, codeownersService, lockerLocker, pullReq, labelService) webhookConfig := server.ProvideWebhookConfig(config) webhookStore := database.ProvideWebhookStore(db) webhookExecutionStore := database.ProvideWebhookExecutionStore(db) diff --git a/types/enum/label.go b/types/enum/label.go new file mode 100644 index 000000000..59a29cae6 --- /dev/null +++ b/types/enum/label.go @@ -0,0 +1,77 @@ +// 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 enum + +type LabelType string + +func (LabelType) Enum() []interface{} { return toInterfaceSlice(LabelTypes) } +func (t LabelType) Sanitize() (LabelType, bool) { return Sanitize(t, GetAllLabelTypes) } +func GetAllLabelTypes() ([]LabelType, LabelType) { return LabelTypes, LabelTypeStatic } + +const ( + LabelTypeStatic LabelType = "static" + LabelTypeDynamic LabelType = "dynamic" +) + +var LabelTypes = sortEnum([]LabelType{ + LabelTypeStatic, + LabelTypeDynamic, +}) + +type LabelColor string + +func (LabelColor) Enum() []interface{} { return toInterfaceSlice(LabelColors) } +func (t LabelColor) Sanitize() (LabelColor, bool) { return Sanitize(t, GetAllLabelColors) } +func GetAllLabelColors() ([]LabelColor, LabelColor) { return LabelColors, LabelColorBackground } + +const ( + LabelColorBackground LabelColor = "background" + LabelColorStroke LabelColor = "stroke" + LabelColorText LabelColor = "text" + LabelColorAccent LabelColor = "accent" + LabelColorRed LabelColor = "red" + LabelColorGreen LabelColor = "green" + LabelColorYellow LabelColor = "yellow" + LabelColorBlue LabelColor = "blue" + LabelColorPink LabelColor = "pink" + LabelColorPurple LabelColor = "purple" + LabelColorViolet LabelColor = "violet" + LabelColorIndigo LabelColor = "indigo" + LabelColorCyan LabelColor = "cyan" + LabelColorOrange LabelColor = "orange" + LabelColorBrown LabelColor = "brown" + LabelColorMint LabelColor = "mint" + LabelColorLime LabelColor = "lime" +) + +var LabelColors = sortEnum([]LabelColor{ + LabelColorBackground, + LabelColorStroke, + LabelColorText, + LabelColorAccent, + LabelColorRed, + LabelColorGreen, + LabelColorYellow, + LabelColorBlue, + LabelColorPink, + LabelColorPurple, + LabelColorViolet, + LabelColorIndigo, + LabelColorCyan, + LabelColorOrange, + LabelColorBrown, + LabelColorMint, + LabelColorLime, +}) diff --git a/types/label.go b/types/label.go new file mode 100644 index 000000000..4f145ee69 --- /dev/null +++ b/types/label.go @@ -0,0 +1,292 @@ +// 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 types + +import ( + "unicode" + "unicode/utf8" + + "github.com/harness/gitness/errors" + "github.com/harness/gitness/types/enum" +) + +const ( + maxLabelLength = 50 +) + +type Label struct { + ID int64 `json:"id"` + SpaceID *int64 `json:"space_id,omitempty"` + RepoID *int64 `json:"repo_id,omitempty"` + Scope int64 `json:"scope"` + Key string `json:"key"` + Description string `json:"description"` + Type enum.LabelType `json:"type"` + Color enum.LabelColor `json:"color"` + ValueCount int64 `json:"value_count"` + Created int64 `json:"created"` + Updated int64 `json:"updated"` + CreatedBy int64 `json:"created_by"` + UpdatedBy int64 `json:"updated_by"` +} + +type LabelValue struct { + ID int64 `json:"id"` + LabelID int64 `json:"label_id"` + Value string `json:"value"` + Color enum.LabelColor `json:"color"` + Created int64 `json:"created"` + Updated int64 `json:"updated"` + CreatedBy int64 `json:"created_by"` + UpdatedBy int64 `json:"updated_by"` +} + +type LabelWithValues struct { + Label `json:"label"` + Values []*LabelValue `json:"values"` +} + +// Used to assign label to pullreq. +type PullReqLabel struct { + PullReqID int64 `json:"pullreq_id"` + LabelID int64 `json:"label_id"` + ValueID *int64 `json:"value_id,omitempty"` + Created int64 `json:"created"` + Updated int64 `json:"updated"` + CreatedBy int64 `json:"created_by"` + UpdatedBy int64 `json:"updated_by"` +} + +type LabelInfo struct { + SpaceID *int64 `json:"-"` + RepoID *int64 `json:"-"` + Scope int64 `json:"scope"` + ID int64 `json:"id"` + Type enum.LabelType `json:"type"` + Key string `json:"key"` + Color enum.LabelColor `json:"color"` + Assigned *bool `json:"assigned,omitempty"` +} + +type LabelValueInfo struct { + LabelID *int64 `json:"-"` + ID *int64 `json:"id,omitempty"` + Value *string `json:"value,omitempty"` + Color *string `json:"color,omitempty"` +} + +type LabelAssignment struct { + LabelInfo + AssignedValue *LabelValueInfo `json:"assigned_value,omitempty"` + Values []*LabelValueInfo `json:"values,omitempty"` // query param ?assignable=true +} + +type ScopeData struct { + // Scope = 0 is repo, scope >= 1 is a depth level of a space + Scope int64 `json:"scope"` + Space *Space `json:"space,omitempty"` + Repo *Repository `json:"repository,omitempty"` +} + +// Used to fetch label and values from a repo and space hierarchy. +type ScopesLabels struct { + ScopeData []*ScopeData `json:"scope_data"` + LabelData []*LabelAssignment `json:"label_data"` +} + +// LabelFilter stores label query parameters. +type AssignableLabelFilter struct { + ListQueryFilter + Assignable bool `json:"assignable,omitempty"` +} +type LabelFilter struct { + ListQueryFilter + Inherited bool `json:"inherited,omitempty"` +} + +type DefineLabelInput struct { + Key string `json:"key"` + Type enum.LabelType `json:"type"` + Description string `json:"description"` + Color enum.LabelColor `json:"color"` +} + +func (in DefineLabelInput) Validate() error { + if err := validateLabelText(in.Key, "key"); err != nil { + return err + } + + if err := validateLabelType(in.Type); err != nil { + return err + } + + err := validateLabelColor(in.Color) + if err != nil { + return err + } + + return nil +} + +type UpdateLabelInput struct { + Key *string `json:"key,omitempty"` + Type *enum.LabelType `json:"type,omitempty"` + Description *string `json:"description,omitempty"` + Color *enum.LabelColor `json:"color,omitempty"` +} + +func (in UpdateLabelInput) Validate() error { + if in.Key != nil { + if err := validateLabelText(*in.Key, "key"); err != nil { + return err + } + } + + if in.Type != nil { + if err := validateLabelType(*in.Type); err != nil { + return err + } + } + + if in.Color != nil { + err := validateLabelColor(*in.Color) + if err != nil { + return err + } + } + + return nil +} + +type DefineValueInput struct { + Value string `json:"value"` + Color enum.LabelColor `json:"color"` +} + +func (in DefineValueInput) Validate() error { + if err := validateLabelText(in.Value, "value"); err != nil { + return err + } + + if err := validateLabelColor(in.Color); err != nil { + return err + } + + return nil +} + +type UpdateValueInput struct { + Value *string `json:"value"` + Color *enum.LabelColor `json:"color"` +} + +func (in UpdateValueInput) Validate() error { + if in.Value != nil { + if err := validateLabelText(*in.Value, "value"); err != nil { + return err + } + } + + if in.Color != nil { + if err := validateLabelColor(*in.Color); err != nil { + return err + } + } + + return nil +} + +type PullReqCreateInput struct { + LabelID int64 `json:"label_id"` + ValueID *int64 `json:"value_id"` + Value string `json:"value"` +} + +type PullReqUpdateInput struct { + LabelValueID *int64 `json:"label_value_id,omitempty"` +} + +func (in PullReqCreateInput) Validate() error { + if (in.ValueID != nil && *in.ValueID > 0) && in.Value != "" { + return errors.InvalidArgument("cannot accept both value id and value") + } + return nil +} + +type SaveLabelInput struct { + ID int64 `json:"id"` + DefineLabelInput +} +type SaveLabelValueInput struct { + ID int64 `json:"id"` + DefineValueInput +} + +type SaveInput struct { + Label SaveLabelInput `json:"label"` + Values []*SaveLabelValueInput `json:"values,omitempty"` +} + +func (in *SaveInput) Validate() error { + if err := in.Label.Validate(); err != nil { + return err + } + + for _, value := range in.Values { + if err := value.Validate(); err != nil { + return err + } + } + + return nil +} + +var labelTypes, _ = enum.GetAllLabelTypes() + +func validateLabelText(text string, typ string) error { + if len(text) == 0 { + return errors.InvalidArgument("%s must be a non-empty string", typ) + } + + if utf8.RuneCountInString(text) > maxLabelLength { + return errors.InvalidArgument("%s can have at most %d characters", typ, maxLabelLength) + } + + for _, ch := range text { + if unicode.IsControl(ch) { + return errors.InvalidArgument("%s cannot contain control characters", typ) + } + } + + return nil +} + +func validateLabelType(typ enum.LabelType) error { + if _, ok := typ.Sanitize(); !ok { + return errors.InvalidArgument("label type must be in %v", labelTypes) + } + return nil +} + +var colorTypes, _ = enum.GetAllLabelColors() + +func validateLabelColor(color enum.LabelColor) error { + _, ok := color.Sanitize() + if !ok { + return errors.InvalidArgument("color type must be in %v", colorTypes) + } + + return nil +}