diff --git a/cmd/gitness/wire.go b/cmd/gitness/wire.go index ddd1bd0aa..ed157dbc2 100644 --- a/cmd/gitness/wire.go +++ b/cmd/gitness/wire.go @@ -11,15 +11,19 @@ import ( "context" cliserver "github.com/harness/gitness/cli/server" + "github.com/harness/gitness/encrypt" "github.com/harness/gitness/events" "github.com/harness/gitness/gitrpc" gitrpcserver "github.com/harness/gitness/gitrpc/server" gitrpccron "github.com/harness/gitness/gitrpc/server/cron" checkcontroller "github.com/harness/gitness/internal/api/controller/check" + "github.com/harness/gitness/internal/api/controller/execution" "github.com/harness/gitness/internal/api/controller/githook" + "github.com/harness/gitness/internal/api/controller/pipeline" "github.com/harness/gitness/internal/api/controller/principal" "github.com/harness/gitness/internal/api/controller/pullreq" "github.com/harness/gitness/internal/api/controller/repo" + "github.com/harness/gitness/internal/api/controller/secret" "github.com/harness/gitness/internal/api/controller/service" "github.com/harness/gitness/internal/api/controller/serviceaccount" "github.com/harness/gitness/internal/api/controller/space" @@ -79,6 +83,7 @@ func initSystem(ctx context.Context, config *types.Config) (*cliserver.System, e gitrpc.WireSet, store.WireSet, check.WireSet, + encrypt.WireSet, cliserver.ProvideEventsConfig, events.WireSet, cliserver.ProvideWebhookConfig, @@ -90,6 +95,9 @@ func initSystem(ctx context.Context, config *types.Config) (*cliserver.System, e codecomments.WireSet, gitrpccron.WireSet, checkcontroller.WireSet, + execution.WireSet, + pipeline.WireSet, + secret.WireSet, ) return &cliserver.System{}, nil } diff --git a/cmd/gitness/wire_gen.go b/cmd/gitness/wire_gen.go index 27aee0b41..75ef17a33 100644 --- a/cmd/gitness/wire_gen.go +++ b/cmd/gitness/wire_gen.go @@ -8,16 +8,21 @@ package main import ( "context" + "github.com/harness/gitness/cli/server" + "github.com/harness/gitness/encrypt" "github.com/harness/gitness/events" "github.com/harness/gitness/gitrpc" server3 "github.com/harness/gitness/gitrpc/server" "github.com/harness/gitness/gitrpc/server/cron" check2 "github.com/harness/gitness/internal/api/controller/check" + "github.com/harness/gitness/internal/api/controller/execution" "github.com/harness/gitness/internal/api/controller/githook" + "github.com/harness/gitness/internal/api/controller/pipeline" "github.com/harness/gitness/internal/api/controller/principal" "github.com/harness/gitness/internal/api/controller/pullreq" "github.com/harness/gitness/internal/api/controller/repo" + "github.com/harness/gitness/internal/api/controller/secret" "github.com/harness/gitness/internal/api/controller/service" "github.com/harness/gitness/internal/api/controller/serviceaccount" "github.com/harness/gitness/internal/api/controller/space" @@ -84,7 +89,17 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro return nil, err } repoController := repo.ProvideController(config, db, provider, pathUID, authorizer, pathStore, repoStore, spaceStore, principalStore, gitrpcInterface) - spaceController := space.ProvideController(db, provider, pathUID, authorizer, pathStore, spaceStore, repoStore, principalStore, repoController, membershipStore) + executionStore := database.ProvideExecutionStore(db) + pipelineStore := database.ProvidePipelineStore(db) + executionController := execution.ProvideController(db, authorizer, executionStore, pipelineStore, spaceStore) + secretStore := database.ProvideSecretStore(db) + spaceController := space.ProvideController(db, provider, pathUID, authorizer, pathStore, pipelineStore, secretStore, spaceStore, repoStore, principalStore, repoController, membershipStore) + pipelineController := pipeline.ProvideController(db, pathUID, pathStore, repoStore, authorizer, pipelineStore, spaceStore) + encrypter, err := encrypt.ProvideEncrypter(config) + if err != nil { + return nil, err + } + secretController := secret.ProvideController(db, pathUID, pathStore, encrypter, secretStore, authorizer, spaceStore) pullReqStore := database.ProvidePullReqStore(db, principalInfoCache) pullReqActivityStore := database.ProvidePullReqActivityStore(db, principalInfoCache) codeCommentView := database.ProvideCodeCommentView(db) @@ -138,7 +153,7 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro principalController := principal.ProvideController(principalStore) checkStore := database.ProvideCheckStore(db, principalInfoCache) checkController := check2.ProvideController(db, authorizer, repoStore, checkStore, gitrpcInterface) - apiHandler := router.ProvideAPIHandler(config, authenticator, repoController, spaceController, pullreqController, webhookController, githookController, serviceaccountController, controller, principalController, checkController) + apiHandler := router.ProvideAPIHandler(config, authenticator, repoController, executionController, spaceController, pipelineController, secretController, pullreqController, webhookController, githookController, serviceaccountController, controller, principalController, checkController) gitHandler := router.ProvideGitHandler(config, provider, repoStore, authenticator, authorizer, gitrpcInterface) webHandler := router.ProvideWebHandler(config) routerRouter := router.ProvideRouter(config, apiHandler, gitHandler, webHandler) diff --git a/encrypt/aesgcm.go b/encrypt/aesgcm.go new file mode 100644 index 000000000..4a01fbba2 --- /dev/null +++ b/encrypt/aesgcm.go @@ -0,0 +1,84 @@ +// Copyright 2023 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package encrypt + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "errors" + "io" +) + +// Aesgcm provides an encrypter that uses the aesgcm encryption +// algorithm. +type Aesgcm struct { + block cipher.Block + Compat bool +} + +// Encrypt encrypts the plaintext using aesgcm. +func (e *Aesgcm) Encrypt(plaintext string) ([]byte, error) { + gcm, err := cipher.NewGCM(e.block) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + _, err = io.ReadFull(rand.Reader, nonce) + if err != nil { + return nil, err + } + + return gcm.Seal(nonce, nonce, []byte(plaintext), nil), nil +} + +// Decrypt decrypts the ciphertext using aesgcm. +func (e *Aesgcm) Decrypt(ciphertext []byte) (string, error) { + gcm, err := cipher.NewGCM(e.block) + if err != nil { + return "", err + } + + if len(ciphertext) < gcm.NonceSize() { + // if the decryption utility is running in compatibility + // mode, it will return the ciphertext as plain text if + // decryption fails. This should be used when running the + // database in mixed-mode, where there is a mix of encrypted + // and unencrypted content. + if e.Compat { + return string(ciphertext), nil + } + return "", errors.New("malformed ciphertext") + } + + plaintext, err := gcm.Open(nil, + ciphertext[:gcm.NonceSize()], + ciphertext[gcm.NonceSize():], + nil, + ) + // if the decryption utility is running in compatibility + // mode, it will return the ciphertext as plain text if + // decryption fails. This should be used when running the + // database in mixed-mode, where there is a mix of encrypted + // and unencrypted content. + if err != nil && e.Compat { + return string(ciphertext), nil + } + return string(plaintext), err +} + +// New provides a new aesgcm encrypter +func New(key string, compat bool) (Encrypter, error) { + if len(key) != 32 { + return nil, errKeySize + } + b := []byte(key) + block, err := aes.NewCipher(b) + if err != nil { + return nil, err + } + return &Aesgcm{block: block, Compat: compat}, nil +} diff --git a/encrypt/encrypt.go b/encrypt/encrypt.go new file mode 100644 index 000000000..adfb42454 --- /dev/null +++ b/encrypt/encrypt.go @@ -0,0 +1,20 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package encrypt + +import ( + "errors" +) + +// indicates key size is too small. +var errKeySize = errors.New("encryption key must be 32 bytes") + +// Encrypter provides field encryption and decryption. +// Encrypted values are currently limited to strings, which is +// reflected in the interface design. +type Encrypter interface { + Encrypt(plaintext string) ([]byte, error) + Decrypt(ciphertext []byte) (string, error) +} diff --git a/encrypt/none.go b/encrypt/none.go new file mode 100644 index 000000000..91897cc09 --- /dev/null +++ b/encrypt/none.go @@ -0,0 +1,19 @@ +// Copyright 2023 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package encrypt + +// none is an encryption strategy that stores secret +// values in plain text. This is the default strategy +// when no key is specified. +type none struct { +} + +func (*none) Encrypt(plaintext string) ([]byte, error) { + return []byte(plaintext), nil +} + +func (*none) Decrypt(ciphertext []byte) (string, error) { + return string(ciphertext), nil +} diff --git a/encrypt/wire.go b/encrypt/wire.go new file mode 100644 index 000000000..61888a2e1 --- /dev/null +++ b/encrypt/wire.go @@ -0,0 +1,19 @@ +package encrypt + +import ( + "github.com/harness/gitness/types" + + "github.com/google/wire" +) + +// WireSet provides a wire set for this package. +var WireSet = wire.NewSet( + ProvideEncrypter, +) + +func ProvideEncrypter(config *types.Config) (Encrypter, error) { + if config.Encrypter.Secret == "" { + return &none{}, nil + } + return New(config.Encrypter.Secret, config.Encrypter.EncryptMixedContent) +} diff --git a/internal/api/auth/pipeline.go b/internal/api/auth/pipeline.go new file mode 100644 index 000000000..8c5c199a6 --- /dev/null +++ b/internal/api/auth/pipeline.go @@ -0,0 +1,29 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package auth + +import ( + "context" + + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/internal/auth/authz" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// CheckPipeline checks if a repo specific permission is granted for the current auth session +// in the scope of its parent. +// Returns nil if the permission is granted, otherwise returns an error. +// NotAuthenticated, NotAuthorized, or any underlying error. +func CheckPipeline(ctx context.Context, authorizer authz.Authorizer, session *auth.Session, + parentPath, uid string, permission enum.Permission) error { + scope := &types.Scope{SpacePath: parentPath} + resource := &types.Resource{ + Type: enum.ResourceTypePipeline, + Name: uid, + } + + return Check(ctx, authorizer, session, scope, resource, permission) +} diff --git a/internal/api/auth/secret.go b/internal/api/auth/secret.go new file mode 100644 index 000000000..7804f8c09 --- /dev/null +++ b/internal/api/auth/secret.go @@ -0,0 +1,29 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package auth + +import ( + "context" + + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/internal/auth/authz" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// CheckSecret checks if a repo specific permission is granted for the current auth session +// in the scope of its parent. +// Returns nil if the permission is granted, otherwise returns an error. +// NotAuthenticated, NotAuthorized, or any underlying error. +func CheckSecret(ctx context.Context, authorizer authz.Authorizer, session *auth.Session, + parentPath, uid string, permission enum.Permission) error { + scope := &types.Scope{SpacePath: parentPath} + resource := &types.Resource{ + Type: enum.ResourceTypeSecret, + Name: uid, + } + + return Check(ctx, authorizer, session, scope, resource, permission) +} diff --git a/internal/api/controller/execution/controller.go b/internal/api/controller/execution/controller.go new file mode 100644 index 000000000..abfa572eb --- /dev/null +++ b/internal/api/controller/execution/controller.go @@ -0,0 +1,36 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package execution + +import ( + "github.com/harness/gitness/internal/auth/authz" + "github.com/harness/gitness/internal/store" + + "github.com/jmoiron/sqlx" +) + +type Controller struct { + db *sqlx.DB + authorizer authz.Authorizer + executionStore store.ExecutionStore + pipelineStore store.PipelineStore + spaceStore store.SpaceStore +} + +func NewController( + db *sqlx.DB, + authorizer authz.Authorizer, + executionStore store.ExecutionStore, + pipelineStore store.PipelineStore, + spaceStore store.SpaceStore, +) *Controller { + return &Controller{ + db: db, + authorizer: authorizer, + executionStore: executionStore, + pipelineStore: pipelineStore, + spaceStore: spaceStore, + } +} diff --git a/internal/api/controller/execution/create.go b/internal/api/controller/execution/create.go new file mode 100644 index 000000000..595def806 --- /dev/null +++ b/internal/api/controller/execution/create.go @@ -0,0 +1,66 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package execution + +import ( + "context" + "fmt" + "time" + + apiauth "github.com/harness/gitness/internal/api/auth" + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// TODO: Add more as needed. +type CreateInput struct { + Status string `json:"status"` +} + +func (c *Controller) Create( + ctx context.Context, + session *auth.Session, + spaceRef string, + uid string, + in *CreateInput, +) (*types.Execution, error) { + space, err := c.spaceStore.FindByRef(ctx, spaceRef) + if err != nil { + return nil, fmt.Errorf("could not find space: %w", err) + } + + pipeline, err := c.pipelineStore.FindByUID(ctx, space.ID, uid) + if err != nil { + return nil, fmt.Errorf("could not find pipeline: %w", err) + } + + err = apiauth.CheckPipeline(ctx, c.authorizer, session, space.Path, pipeline.UID, enum.PermissionPipelineExecute) + if err != nil { + return nil, fmt.Errorf("failed to authorize: %w", err) + } + + pipeline, err = c.pipelineStore.IncrementSeqNum(ctx, pipeline) + if err != nil { + return nil, fmt.Errorf("failed to increment sequence number: %w", err) + } + + now := time.Now().UnixMilli() + execution := &types.Execution{ + Number: pipeline.Seq, + Status: in.Status, + RepoID: pipeline.RepoID, + PipelineID: pipeline.ID, + Created: now, + Updated: now, + Version: 0, + } + err = c.executionStore.Create(ctx, execution) + if err != nil { + return nil, fmt.Errorf("execution creation failed: %w", err) + } + + return execution, nil +} diff --git a/internal/api/controller/execution/delete.go b/internal/api/controller/execution/delete.go new file mode 100644 index 000000000..9b2a84aa9 --- /dev/null +++ b/internal/api/controller/execution/delete.go @@ -0,0 +1,41 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package execution + +import ( + "context" + "fmt" + + apiauth "github.com/harness/gitness/internal/api/auth" + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/types/enum" +) + +func (c *Controller) Delete( + ctx context.Context, + session *auth.Session, + spaceRef string, + pipelineUID string, + executionNum int64, +) error { + space, err := c.spaceStore.FindByRef(ctx, spaceRef) + if err != nil { + return fmt.Errorf("could not find parent space: %w", err) + } + + pipeline, err := c.pipelineStore.FindByUID(ctx, space.ID, pipelineUID) + if err != nil { + return fmt.Errorf("could not find pipeline: %w", err) + } + err = apiauth.CheckPipeline(ctx, c.authorizer, session, space.Path, pipeline.UID, enum.PermissionPipelineDelete) + if err != nil { + return fmt.Errorf("could not authorize: %w", err) + } + err = c.executionStore.Delete(ctx, pipeline.ID, executionNum) + if err != nil { + return fmt.Errorf("could not delete execution: %w", err) + } + return nil +} diff --git a/internal/api/controller/execution/find.go b/internal/api/controller/execution/find.go new file mode 100644 index 000000000..d597488fd --- /dev/null +++ b/internal/api/controller/execution/find.go @@ -0,0 +1,40 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package execution + +import ( + "context" + "fmt" + + apiauth "github.com/harness/gitness/internal/api/auth" + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +func (c *Controller) Find( + ctx context.Context, + session *auth.Session, + spaceRef string, + pipelineUID string, + executionNum int64, +) (*types.Execution, error) { + space, err := c.spaceStore.FindByRef(ctx, spaceRef) + if err != nil { + return nil, fmt.Errorf("could not find parent space: %w", err) + } + + pipeline, err := c.pipelineStore.FindByUID(ctx, space.ID, pipelineUID) + if err != nil { + return nil, fmt.Errorf("could not find pipeline: %w", err) + } + + err = apiauth.CheckPipeline(ctx, c.authorizer, session, space.Path, pipeline.UID, enum.PermissionPipelineView) + if err != nil { + return nil, fmt.Errorf("could not authorize: %w", err) + } + + return c.executionStore.Find(ctx, pipeline.ID, executionNum) +} diff --git a/internal/api/controller/execution/list.go b/internal/api/controller/execution/list.go new file mode 100644 index 000000000..a5ba72f6b --- /dev/null +++ b/internal/api/controller/execution/list.go @@ -0,0 +1,59 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. +package execution + +import ( + "context" + "fmt" + + apiauth "github.com/harness/gitness/internal/api/auth" + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/store/database/dbtx" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +func (c *Controller) List( + ctx context.Context, + session *auth.Session, + spaceRef string, + pipelineUID string, + pagination types.Pagination, +) ([]*types.Execution, int64, error) { + space, err := c.spaceStore.FindByRef(ctx, spaceRef) + if err != nil { + return nil, 0, fmt.Errorf("failed to find parent space: %w", err) + } + pipeline, err := c.pipelineStore.FindByUID(ctx, space.ID, pipelineUID) + if err != nil { + return nil, 0, fmt.Errorf("failed to find pipeline: %w", err) + } + + err = apiauth.CheckPipeline(ctx, c.authorizer, session, space.Path, pipeline.UID, enum.PermissionPipelineView) + if err != nil { + return nil, 0, fmt.Errorf("failed to authorize: %w", err) + } + + var count int64 + var executions []*types.Execution + + err = dbtx.New(c.db).WithTx(ctx, func(ctx context.Context) (err error) { + count, err = c.executionStore.Count(ctx, pipeline.ID) + if err != nil { + return fmt.Errorf("failed to count child executions: %w", err) + } + + executions, err = c.executionStore.List(ctx, pipeline.ID, pagination) + if err != nil { + return fmt.Errorf("failed to list child executions: %w", err) + } + + return + }, dbtx.TxDefaultReadOnly) + if err != nil { + return executions, count, fmt.Errorf("failed to fetch list: %w", err) + } + + return executions, count, nil +} diff --git a/internal/api/controller/execution/update.go b/internal/api/controller/execution/update.go new file mode 100644 index 000000000..0bd8a8959 --- /dev/null +++ b/internal/api/controller/execution/update.go @@ -0,0 +1,57 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package execution + +import ( + "context" + "fmt" + + apiauth "github.com/harness/gitness/internal/api/auth" + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +type UpdateInput struct { + Status string `json:"status"` +} + +func (c *Controller) Update( + ctx context.Context, + session *auth.Session, + spaceRef string, + pipelineUID string, + executionNum int64, + in *UpdateInput) (*types.Execution, error) { + space, err := c.spaceStore.FindByRef(ctx, spaceRef) + if err != nil { + return nil, fmt.Errorf("could not find space: %w", err) + } + + err = apiauth.CheckPipeline(ctx, c.authorizer, session, space.Path, pipelineUID, enum.PermissionPipelineEdit) + if err != nil { + return nil, fmt.Errorf("failed to check auth: %w", err) + } + + pipeline, err := c.pipelineStore.FindByUID(ctx, space.ID, pipelineUID) + if err != nil { + return nil, fmt.Errorf("failed to find pipeline: %w", err) + } + + execution, err := c.executionStore.Find(ctx, pipeline.ID, executionNum) + if err != nil { + return nil, fmt.Errorf("failed to find execution: %w", err) + } + + return c.executionStore.UpdateOptLock(ctx, + execution, func(original *types.Execution) error { + // update values only if provided + if in.Status != "" { + original.Status = in.Status + } + + return nil + }) +} diff --git a/internal/api/controller/execution/wire.go b/internal/api/controller/execution/wire.go new file mode 100644 index 000000000..c559831f9 --- /dev/null +++ b/internal/api/controller/execution/wire.go @@ -0,0 +1,27 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package execution + +import ( + "github.com/harness/gitness/internal/auth/authz" + "github.com/harness/gitness/internal/store" + + "github.com/google/wire" + "github.com/jmoiron/sqlx" +) + +// WireSet provides a wire set for this package. +var WireSet = wire.NewSet( + ProvideController, +) + +func ProvideController(db *sqlx.DB, + authorizer authz.Authorizer, + executionStore store.ExecutionStore, + pipelineStore store.PipelineStore, + spaceStore store.SpaceStore, +) *Controller { + return NewController(db, authorizer, executionStore, pipelineStore, spaceStore) +} diff --git a/internal/api/controller/pipeline/controller.go b/internal/api/controller/pipeline/controller.go new file mode 100644 index 000000000..61020c78a --- /dev/null +++ b/internal/api/controller/pipeline/controller.go @@ -0,0 +1,44 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipeline + +import ( + "github.com/harness/gitness/internal/auth/authz" + "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types/check" + + "github.com/jmoiron/sqlx" +) + +type Controller struct { + defaultBranch string + db *sqlx.DB + uidCheck check.PathUID + pathStore store.PathStore + repoStore store.RepoStore + authorizer authz.Authorizer + pipelineStore store.PipelineStore + spaceStore store.SpaceStore +} + +func NewController( + db *sqlx.DB, + uidCheck check.PathUID, + authorizer authz.Authorizer, + pathStore store.PathStore, + repoStore store.RepoStore, + pipelineStore store.PipelineStore, + spaceStore store.SpaceStore, +) *Controller { + return &Controller{ + db: db, + uidCheck: uidCheck, + pathStore: pathStore, + repoStore: repoStore, + authorizer: authorizer, + pipelineStore: pipelineStore, + spaceStore: spaceStore, + } +} diff --git a/internal/api/controller/pipeline/create.go b/internal/api/controller/pipeline/create.go new file mode 100644 index 000000000..74cbedc8d --- /dev/null +++ b/internal/api/controller/pipeline/create.go @@ -0,0 +1,106 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipeline + +import ( + "context" + "fmt" + "strconv" + "strings" + "time" + + apiauth "github.com/harness/gitness/internal/api/auth" + "github.com/harness/gitness/internal/api/usererror" + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/check" + "github.com/harness/gitness/types/enum" +) + +var ( + // errPipelineRequiresParent if the user tries to create a pipeline without a parent space. + errPipelineRequiresParent = usererror.BadRequest( + "Parent space required - standalone pipelines are not supported.") +) + +type CreateInput struct { + Description string `json:"description"` + SpaceRef string `json:"space_ref"` + UID string `json:"uid"` + RepoRef string `json:"repo_ref"` // empty if repo_type != gitness + RepoType enum.ScmType `json:"repo_type"` + DefaultBranch string `json:"default_branch"` + ConfigPath string `json:"config_path"` +} + +func (c *Controller) Create(ctx context.Context, session *auth.Session, in *CreateInput) (*types.Pipeline, error) { + parentSpace, err := c.spaceStore.FindByRef(ctx, in.SpaceRef) + if err != nil { + return nil, fmt.Errorf("could not find parent by ref: %w", err) + } + + err = apiauth.CheckPipeline(ctx, c.authorizer, session, parentSpace.Path, in.UID, enum.PermissionPipelineEdit) + if err != nil { + return nil, err + } + + var repoID int64 + + if in.RepoType == enum.ScmTypeGitness { + repo, err := c.repoStore.FindByRef(ctx, in.RepoRef) + if err != nil { + return nil, fmt.Errorf("could not find repo by ref: %w", err) + } + repoID = repo.ID + } + + if err := c.sanitizeCreateInput(in); err != nil { + return nil, fmt.Errorf("failed to sanitize input: %w", err) + } + + var pipeline *types.Pipeline + now := time.Now().UnixMilli() + pipeline = &types.Pipeline{ + Description: in.Description, + SpaceID: parentSpace.ID, + UID: in.UID, + Seq: 0, + RepoID: repoID, + RepoType: in.RepoType, + DefaultBranch: in.DefaultBranch, + ConfigPath: in.ConfigPath, + Created: now, + Updated: now, + Version: 0, + } + err = c.pipelineStore.Create(ctx, pipeline) + if err != nil { + return nil, fmt.Errorf("pipeline creation failed: %w", err) + } + + return pipeline, nil +} + +func (c *Controller) sanitizeCreateInput(in *CreateInput) error { + parentRefAsID, err := strconv.ParseInt(in.SpaceRef, 10, 64) + if (err == nil && parentRefAsID <= 0) || (len(strings.TrimSpace(in.SpaceRef)) == 0) { + return errPipelineRequiresParent + } + + if err := c.uidCheck(in.UID, false); err != nil { + return err + } + + in.Description = strings.TrimSpace(in.Description) + if err := check.Description(in.Description); err != nil { + return err + } + + if in.DefaultBranch == "" { + in.DefaultBranch = c.defaultBranch + } + + return nil +} diff --git a/internal/api/controller/pipeline/delete.go b/internal/api/controller/pipeline/delete.go new file mode 100644 index 000000000..cdae2a3cc --- /dev/null +++ b/internal/api/controller/pipeline/delete.go @@ -0,0 +1,31 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipeline + +import ( + "context" + "fmt" + + apiauth "github.com/harness/gitness/internal/api/auth" + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/types/enum" +) + +func (c *Controller) Delete(ctx context.Context, session *auth.Session, spaceRef string, uid string) error { + space, err := c.spaceStore.FindByRef(ctx, spaceRef) + if err != nil { + return fmt.Errorf("could not find parent space: %w", err) + } + + err = apiauth.CheckPipeline(ctx, c.authorizer, session, space.Path, uid, enum.PermissionPipelineDelete) + if err != nil { + return fmt.Errorf("could not authorize: %w", err) + } + err = c.pipelineStore.DeleteByUID(ctx, space.ID, uid) + if err != nil { + return fmt.Errorf("could not delete pipeline: %w", err) + } + return nil +} diff --git a/internal/api/controller/pipeline/find.go b/internal/api/controller/pipeline/find.go new file mode 100644 index 000000000..b065bc404 --- /dev/null +++ b/internal/api/controller/pipeline/find.go @@ -0,0 +1,32 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipeline + +import ( + "context" + "fmt" + + apiauth "github.com/harness/gitness/internal/api/auth" + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +func (c *Controller) Find( + ctx context.Context, + session *auth.Session, + spaceRef string, + uid string, +) (*types.Pipeline, error) { + space, err := c.spaceStore.FindByRef(ctx, spaceRef) + if err != nil { + return nil, fmt.Errorf("could not find parent space: %w", err) + } + err = apiauth.CheckPipeline(ctx, c.authorizer, session, space.Path, uid, enum.PermissionPipelineView) + if err != nil { + return nil, fmt.Errorf("could not authorize: %w", err) + } + return c.pipelineStore.FindByUID(ctx, space.ID, uid) +} diff --git a/internal/api/controller/pipeline/update.go b/internal/api/controller/pipeline/update.go new file mode 100644 index 000000000..7cc75cdc9 --- /dev/null +++ b/internal/api/controller/pipeline/update.go @@ -0,0 +1,58 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipeline + +import ( + "context" + "fmt" + + apiauth "github.com/harness/gitness/internal/api/auth" + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +type UpdateInput struct { + Description string `json:"description"` + UID string `json:"uid"` + ConfigPath string `json:"config_path"` +} + +func (c *Controller) Update( + ctx context.Context, + session *auth.Session, + spaceRef string, + uid string, + in *UpdateInput, +) (*types.Pipeline, error) { + space, err := c.spaceStore.FindByRef(ctx, spaceRef) + if err != nil { + return nil, fmt.Errorf("could not find parent space: %w", err) + } + + err = apiauth.CheckPipeline(ctx, c.authorizer, session, space.Path, uid, enum.PermissionPipelineEdit) + if err != nil { + return nil, fmt.Errorf("could not authorize: %w", err) + } + + pipeline, err := c.pipelineStore.FindByUID(ctx, space.ID, uid) + if err != nil { + return nil, fmt.Errorf("could not find pipeline: %w", err) + } + + return c.pipelineStore.UpdateOptLock(ctx, pipeline, func(pipeline *types.Pipeline) error { + if in.Description != "" { + pipeline.Description = in.Description + } + if in.UID != "" { + pipeline.UID = in.UID + } + if in.ConfigPath != "" { + pipeline.ConfigPath = in.ConfigPath + } + + return nil + }) +} diff --git a/internal/api/controller/pipeline/wire.go b/internal/api/controller/pipeline/wire.go new file mode 100644 index 000000000..d665a8ee6 --- /dev/null +++ b/internal/api/controller/pipeline/wire.go @@ -0,0 +1,30 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipeline + +import ( + "github.com/harness/gitness/internal/auth/authz" + "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types/check" + + "github.com/google/wire" + "github.com/jmoiron/sqlx" +) + +// WireSet provides a wire set for this package. +var WireSet = wire.NewSet( + ProvideController, +) + +func ProvideController(db *sqlx.DB, + uidCheck check.PathUID, + pathStore store.PathStore, + repoStore store.RepoStore, + authorizer authz.Authorizer, + pipelineStore store.PipelineStore, + spaceStore store.SpaceStore, +) *Controller { + return NewController(db, uidCheck, authorizer, pathStore, repoStore, pipelineStore, spaceStore) +} diff --git a/internal/api/controller/secret/controller.go b/internal/api/controller/secret/controller.go new file mode 100644 index 000000000..5a62633b2 --- /dev/null +++ b/internal/api/controller/secret/controller.go @@ -0,0 +1,44 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package secret + +import ( + "github.com/harness/gitness/encrypt" + "github.com/harness/gitness/internal/auth/authz" + "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types/check" + + "github.com/jmoiron/sqlx" +) + +type Controller struct { + db *sqlx.DB + uidCheck check.PathUID + pathStore store.PathStore + encrypter encrypt.Encrypter + secretStore store.SecretStore + authorizer authz.Authorizer + spaceStore store.SpaceStore +} + +func NewController( + db *sqlx.DB, + uidCheck check.PathUID, + authorizer authz.Authorizer, + pathStore store.PathStore, + encrypter encrypt.Encrypter, + secretStore store.SecretStore, + spaceStore store.SpaceStore, +) *Controller { + return &Controller{ + db: db, + uidCheck: uidCheck, + pathStore: pathStore, + encrypter: encrypter, + secretStore: secretStore, + authorizer: authorizer, + spaceStore: spaceStore, + } +} diff --git a/internal/api/controller/secret/create.go b/internal/api/controller/secret/create.go new file mode 100644 index 000000000..18b1f6288 --- /dev/null +++ b/internal/api/controller/secret/create.go @@ -0,0 +1,122 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package secret + +import ( + "context" + "fmt" + "strconv" + "strings" + "time" + + "github.com/harness/gitness/encrypt" + apiauth "github.com/harness/gitness/internal/api/auth" + "github.com/harness/gitness/internal/api/usererror" + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/check" + "github.com/harness/gitness/types/enum" +) + +var ( + // errSecretRequiresParent if the user tries to create a secret without a parent space. + errSecretRequiresParent = usererror.BadRequest( + "Parent space required - standalone secret are not supported.") +) + +type CreateInput struct { + Description string `json:"description"` + SpaceRef string `json:"space_ref"` // Ref of the parent space + UID string `json:"uid"` + Data string `json:"data"` +} + +func (c *Controller) Create(ctx context.Context, session *auth.Session, in *CreateInput) (*types.Secret, error) { + parentSpace, err := c.spaceStore.FindByRef(ctx, in.SpaceRef) + if err != nil { + return nil, fmt.Errorf("could not find parent by ref: %w", err) + } + + err = apiauth.CheckSecret(ctx, c.authorizer, session, parentSpace.Path, in.UID, enum.PermissionSecretEdit) + if err != nil { + return nil, err + } + + if err := c.sanitizeCreateInput(in); err != nil { + return nil, fmt.Errorf("failed to sanitize input: %w", err) + } + + var secret *types.Secret + now := time.Now().UnixMilli() + secret = &types.Secret{ + Description: in.Description, + Data: in.Data, + SpaceID: parentSpace.ID, + UID: in.UID, + Created: now, + Updated: now, + Version: 0, + } + secret, err = enc(c.encrypter, secret) + if err != nil { + return nil, fmt.Errorf("could not encrypt secret: %w", err) + } + err = c.secretStore.Create(ctx, secret) + if err != nil { + return nil, fmt.Errorf("secret creation failed: %w", err) + } + if err != nil { + return nil, err + } + + return secret, nil +} + +func (c *Controller) sanitizeCreateInput(in *CreateInput) error { + parentRefAsID, err := strconv.ParseInt(in.SpaceRef, 10, 64) + + if (err == nil && parentRefAsID <= 0) || (len(strings.TrimSpace(in.SpaceRef)) == 0) { + return errSecretRequiresParent + } + + if err := c.uidCheck(in.UID, false); err != nil { + return err + } + + in.Description = strings.TrimSpace(in.Description) + if err := check.Description(in.Description); err != nil { + return err + } + + return nil +} + +// helper function returns the same secret with encrypted data. +func enc(encrypt encrypt.Encrypter, secret *types.Secret) (*types.Secret, error) { + if secret == nil { + return nil, fmt.Errorf("cannot encrypt a nil secret") + } + s := *secret + ciphertext, err := encrypt.Encrypt(secret.Data) + if err != nil { + return nil, err + } + s.Data = string(ciphertext) + return &s, nil +} + +// helper function returns the same secret with decrypted data. +func dec(encrypt encrypt.Encrypter, secret *types.Secret) (*types.Secret, error) { + if secret == nil { + return nil, fmt.Errorf("cannot decrypt a nil secret") + } + s := *secret + plaintext, err := encrypt.Decrypt([]byte(secret.Data)) + if err != nil { + return nil, err + } + s.Data = plaintext + return &s, nil +} diff --git a/internal/api/controller/secret/delete.go b/internal/api/controller/secret/delete.go new file mode 100644 index 000000000..bcb31e1be --- /dev/null +++ b/internal/api/controller/secret/delete.go @@ -0,0 +1,31 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package secret + +import ( + "context" + "fmt" + + apiauth "github.com/harness/gitness/internal/api/auth" + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/types/enum" +) + +func (c *Controller) Delete(ctx context.Context, session *auth.Session, spaceRef string, uid string) error { + space, err := c.spaceStore.FindByRef(ctx, spaceRef) + if err != nil { + return fmt.Errorf("could not find space: %w", err) + } + + err = apiauth.CheckSecret(ctx, c.authorizer, session, space.Path, uid, enum.PermissionSecretDelete) + if err != nil { + return fmt.Errorf("failed to authorize: %w", err) + } + err = c.secretStore.DeleteByUID(ctx, space.ID, uid) + if err != nil { + return fmt.Errorf("could not delete secret: %w", err) + } + return nil +} diff --git a/internal/api/controller/secret/find.go b/internal/api/controller/secret/find.go new file mode 100644 index 000000000..34dcc9f3d --- /dev/null +++ b/internal/api/controller/secret/find.go @@ -0,0 +1,40 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package secret + +import ( + "context" + "fmt" + + apiauth "github.com/harness/gitness/internal/api/auth" + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +func (c *Controller) Find( + ctx context.Context, + session *auth.Session, + spaceRef string, + uid string, +) (*types.Secret, error) { + space, err := c.spaceStore.FindByRef(ctx, spaceRef) + if err != nil { + return nil, fmt.Errorf("could not find space: %w", err) + } + err = apiauth.CheckSecret(ctx, c.authorizer, session, space.Path, uid, enum.PermissionSecretView) + if err != nil { + return nil, fmt.Errorf("failed to authorize: %w", err) + } + secret, err := c.secretStore.FindByUID(ctx, space.ID, uid) + if err != nil { + return nil, fmt.Errorf("could not find secret: %w", err) + } + secret, err = dec(c.encrypter, secret) + if err != nil { + return nil, fmt.Errorf("could not decrypt secret: %w", err) + } + return secret, nil +} diff --git a/internal/api/controller/secret/update.go b/internal/api/controller/secret/update.go new file mode 100644 index 000000000..c3be74121 --- /dev/null +++ b/internal/api/controller/secret/update.go @@ -0,0 +1,63 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package secret + +import ( + "context" + "fmt" + + apiauth "github.com/harness/gitness/internal/api/auth" + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// UpdateInput is used for updating a repo. +type UpdateInput struct { + Description string `json:"description"` + UID string `json:"uid"` + Data string `json:"data"` +} + +func (c *Controller) Update( + ctx context.Context, + session *auth.Session, + spaceRef string, + uid string, + in *UpdateInput, +) (*types.Secret, error) { + space, err := c.spaceStore.FindByRef(ctx, spaceRef) + if err != nil { + return nil, fmt.Errorf("could not find space: %w", err) + } + + err = apiauth.CheckSecret(ctx, c.authorizer, session, space.Path, uid, enum.PermissionSecretEdit) + if err != nil { + return nil, fmt.Errorf("failed to authorize: %w", err) + } + + secret, err := c.secretStore.FindByUID(ctx, space.ID, uid) + if err != nil { + return nil, fmt.Errorf("could not find secret: %w", err) + } + + return c.secretStore.UpdateOptLock(ctx, secret, func(original *types.Secret) error { + if in.Description != "" { + original.Description = in.Description + } + if in.Data != "" { + data, err := c.encrypter.Encrypt(original.Data) + if err != nil { + return fmt.Errorf("could not encrypt secret: %w", err) + } + original.Data = string(data) + } + if in.UID != "" { + original.UID = in.UID + } + + return nil + }) +} diff --git a/internal/api/controller/secret/wire.go b/internal/api/controller/secret/wire.go new file mode 100644 index 000000000..807cac0d8 --- /dev/null +++ b/internal/api/controller/secret/wire.go @@ -0,0 +1,31 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package secret + +import ( + "github.com/harness/gitness/encrypt" + "github.com/harness/gitness/internal/auth/authz" + "github.com/harness/gitness/internal/store" + "github.com/harness/gitness/types/check" + + "github.com/google/wire" + "github.com/jmoiron/sqlx" +) + +// WireSet provides a wire set for this package. +var WireSet = wire.NewSet( + ProvideController, +) + +func ProvideController(db *sqlx.DB, + uidCheck check.PathUID, + pathStore store.PathStore, + encrypter encrypt.Encrypter, + secretStore store.SecretStore, + authorizer authz.Authorizer, + spaceStore store.SpaceStore, +) *Controller { + return NewController(db, uidCheck, authorizer, pathStore, encrypter, secretStore, spaceStore) +} diff --git a/internal/api/controller/space/controller.go b/internal/api/controller/space/controller.go index 25f1b626f..3249c82e1 100644 --- a/internal/api/controller/space/controller.go +++ b/internal/api/controller/space/controller.go @@ -20,6 +20,8 @@ type Controller struct { uidCheck check.PathUID authorizer authz.Authorizer pathStore store.PathStore + pipelineStore store.PipelineStore + secretStore store.SecretStore spaceStore store.SpaceStore repoStore store.RepoStore principalStore store.PrincipalStore @@ -29,9 +31,9 @@ type Controller struct { func NewController(db *sqlx.DB, urlProvider *url.Provider, uidCheck check.PathUID, authorizer authz.Authorizer, - pathStore store.PathStore, spaceStore store.SpaceStore, - repoStore store.RepoStore, principalStore store.PrincipalStore, repoCtrl *repo.Controller, - membershipStore store.MembershipStore, + pathStore store.PathStore, pipelineStore store.PipelineStore, secretStore store.SecretStore, + spaceStore store.SpaceStore, repoStore store.RepoStore, principalStore store.PrincipalStore, + repoCtrl *repo.Controller, membershipStore store.MembershipStore, ) *Controller { return &Controller{ db: db, @@ -39,6 +41,8 @@ func NewController(db *sqlx.DB, urlProvider *url.Provider, uidCheck: uidCheck, authorizer: authorizer, pathStore: pathStore, + pipelineStore: pipelineStore, + secretStore: secretStore, spaceStore: spaceStore, repoStore: repoStore, principalStore: principalStore, diff --git a/internal/api/controller/space/list_pipelines.go b/internal/api/controller/space/list_pipelines.go new file mode 100644 index 000000000..87d999bb4 --- /dev/null +++ b/internal/api/controller/space/list_pipelines.go @@ -0,0 +1,54 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. +package space + +import ( + "context" + "fmt" + + apiauth "github.com/harness/gitness/internal/api/auth" + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/store/database/dbtx" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// ListPipelines lists the pipelines in a space. +func (c *Controller) ListPipelines( + ctx context.Context, + session *auth.Session, + spaceRef string, + filter types.ListQueryFilter, +) ([]*types.Pipeline, int64, error) { + space, err := c.spaceStore.FindByRef(ctx, spaceRef) + if err != nil { + return nil, 0, fmt.Errorf("failed to find parent space: %w", err) + } + + err = apiauth.CheckSpace(ctx, c.authorizer, session, space, enum.PermissionPipelineView, false) + if err != nil { + return nil, 0, fmt.Errorf("could not authorize: %w", err) + } + + var count int64 + var pipelines []*types.Pipeline + + err = dbtx.New(c.db).WithTx(ctx, func(ctx context.Context) (err error) { + count, err = c.pipelineStore.Count(ctx, space.ID, filter) + if err != nil { + return fmt.Errorf("failed to count child executions: %w", err) + } + + pipelines, err = c.pipelineStore.List(ctx, space.ID, filter) + if err != nil { + return fmt.Errorf("failed to count child executions: %w", err) + } + return + }, dbtx.TxDefaultReadOnly) + if err != nil { + return pipelines, count, fmt.Errorf("failed to list pipelines: %w", err) + } + + return pipelines, count, nil +} diff --git a/internal/api/controller/space/list_secrets.go b/internal/api/controller/space/list_secrets.go new file mode 100644 index 000000000..1c091d639 --- /dev/null +++ b/internal/api/controller/space/list_secrets.go @@ -0,0 +1,54 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. +package space + +import ( + "context" + "fmt" + + apiauth "github.com/harness/gitness/internal/api/auth" + "github.com/harness/gitness/internal/auth" + "github.com/harness/gitness/store/database/dbtx" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// ListSecrets lists the secrets in a space. +func (c *Controller) ListSecrets( + ctx context.Context, + session *auth.Session, + spaceRef string, + filter types.ListQueryFilter, +) ([]*types.Secret, int64, error) { + space, err := c.spaceStore.FindByRef(ctx, spaceRef) + if err != nil { + return nil, 0, fmt.Errorf("failed to find parent space: %w", err) + } + + err = apiauth.CheckSpace(ctx, c.authorizer, session, space, enum.PermissionSecretView, false) + if err != nil { + return nil, 0, fmt.Errorf("could not authorize: %w", err) + } + + var count int64 + var secrets []*types.Secret + + err = dbtx.New(c.db).WithTx(ctx, func(ctx context.Context) (err error) { + count, err = c.secretStore.Count(ctx, space.ID, filter) + if err != nil { + return fmt.Errorf("failed to count child executions: %w", err) + } + + secrets, err = c.secretStore.List(ctx, space.ID, filter) + if err != nil { + return fmt.Errorf("failed to list child executions: %w", err) + } + return + }, dbtx.TxDefaultReadOnly) + if err != nil { + return secrets, count, fmt.Errorf("failed to list secrets: %w", err) + } + + return secrets, count, nil +} diff --git a/internal/api/controller/space/wire.go b/internal/api/controller/space/wire.go index d8b9cbbba..9b126de43 100644 --- a/internal/api/controller/space/wire.go +++ b/internal/api/controller/space/wire.go @@ -21,12 +21,11 @@ var WireSet = wire.NewSet( ) func ProvideController(db *sqlx.DB, urlProvider *url.Provider, uidCheck check.PathUID, authorizer authz.Authorizer, - pathStore store.PathStore, spaceStore store.SpaceStore, repoStore store.RepoStore, - principalStore store.PrincipalStore, repoCtrl *repo.Controller, - membershipStore store.MembershipStore, + pathStore store.PathStore, pipelineStore store.PipelineStore, secretStore store.SecretStore, + spaceStore store.SpaceStore, repoStore store.RepoStore, principalStore store.PrincipalStore, + repoCtrl *repo.Controller, membershipStore store.MembershipStore, ) *Controller { return NewController(db, urlProvider, uidCheck, authorizer, - pathStore, spaceStore, repoStore, - principalStore, repoCtrl, - membershipStore) + pathStore, pipelineStore, secretStore, spaceStore, repoStore, + principalStore, repoCtrl, membershipStore) } diff --git a/internal/api/handler/execution/create.go b/internal/api/handler/execution/create.go new file mode 100644 index 000000000..97366148e --- /dev/null +++ b/internal/api/handler/execution/create.go @@ -0,0 +1,47 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package execution + +import ( + "encoding/json" + "net/http" + + "github.com/harness/gitness/internal/api/controller/execution" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/paths" +) + +func HandleCreate(executionCtrl *execution.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + pipelineRef, err := request.GetPipelineRefFromPath(r) + if err != nil { + render.TranslatedUserError(w, err) + return + } + spaceRef, pipelineUID, err := paths.DisectLeaf(pipelineRef) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + in := new(execution.CreateInput) + err = json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(w, "Invalid Request Body: %s.", err) + return + } + + execution, err := executionCtrl.Create(ctx, session, spaceRef, pipelineUID, in) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + render.JSON(w, http.StatusCreated, execution) + } +} diff --git a/internal/api/handler/execution/delete.go b/internal/api/handler/execution/delete.go new file mode 100644 index 000000000..77ee63e2f --- /dev/null +++ b/internal/api/handler/execution/delete.go @@ -0,0 +1,44 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package execution + +import ( + "net/http" + + "github.com/harness/gitness/internal/api/controller/execution" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/paths" +) + +func HandleDelete(executionCtrl *execution.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + pipelineRef, err := request.GetPipelineRefFromPath(r) + if err != nil { + render.TranslatedUserError(w, err) + return + } + spaceRef, pipelineUID, err := paths.DisectLeaf(pipelineRef) + if err != nil { + render.TranslatedUserError(w, err) + return + } + n, err := request.GetExecutionNumberFromPath(r) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + err = executionCtrl.Delete(ctx, session, spaceRef, pipelineUID, n) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + render.DeleteSuccessful(w) + } +} diff --git a/internal/api/handler/execution/find.go b/internal/api/handler/execution/find.go new file mode 100644 index 000000000..9a4bc4c49 --- /dev/null +++ b/internal/api/handler/execution/find.go @@ -0,0 +1,44 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package execution + +import ( + "net/http" + + "github.com/harness/gitness/internal/api/controller/execution" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/paths" +) + +func HandleFind(executionCtrl *execution.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + pipelineRef, err := request.GetPipelineRefFromPath(r) + if err != nil { + render.TranslatedUserError(w, err) + return + } + n, err := request.GetExecutionNumberFromPath(r) + if err != nil { + render.TranslatedUserError(w, err) + return + } + spaceRef, pipelineUID, err := paths.DisectLeaf(pipelineRef) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + execution, err := executionCtrl.Find(ctx, session, spaceRef, pipelineUID, n) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + render.JSON(w, http.StatusOK, execution) + } +} diff --git a/internal/api/handler/execution/list.go b/internal/api/handler/execution/list.go new file mode 100644 index 000000000..eb590f239 --- /dev/null +++ b/internal/api/handler/execution/list.go @@ -0,0 +1,42 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package execution + +import ( + "net/http" + + "github.com/harness/gitness/internal/api/controller/execution" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/paths" +) + +func HandleList(executionCtrl *execution.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + pipelineRef, err := request.GetPipelineRefFromPath(r) + if err != nil { + render.TranslatedUserError(w, err) + return + } + spaceRef, pipelineUID, err := paths.DisectLeaf(pipelineRef) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + pagination := request.ParsePaginationFromRequest(r) + + repos, totalCount, err := executionCtrl.List(ctx, session, spaceRef, pipelineUID, pagination) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + render.Pagination(r, w, pagination.Page, pagination.Size, int(totalCount)) + render.JSON(w, http.StatusOK, repos) + } +} diff --git a/internal/api/handler/execution/update.go b/internal/api/handler/execution/update.go new file mode 100644 index 000000000..14fdf18f9 --- /dev/null +++ b/internal/api/handler/execution/update.go @@ -0,0 +1,53 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package execution + +import ( + "encoding/json" + "net/http" + + "github.com/harness/gitness/internal/api/controller/execution" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/paths" +) + +func HandleUpdate(executionCtrl *execution.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + + in := new(execution.UpdateInput) + err := json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(w, "Invalid Request Body: %s.", err) + return + } + + pipelineRef, err := request.GetPipelineRefFromPath(r) + if err != nil { + render.TranslatedUserError(w, err) + return + } + spaceRef, pipelineUID, err := paths.DisectLeaf(pipelineRef) + if err != nil { + render.TranslatedUserError(w, err) + return + } + n, err := request.GetExecutionNumberFromPath(r) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + pipeline, err := executionCtrl.Update(ctx, session, spaceRef, pipelineUID, n, in) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + render.JSON(w, http.StatusOK, pipeline) + } +} diff --git a/internal/api/handler/pipeline/create.go b/internal/api/handler/pipeline/create.go new file mode 100644 index 000000000..df1f49a9c --- /dev/null +++ b/internal/api/handler/pipeline/create.go @@ -0,0 +1,36 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipeline + +import ( + "encoding/json" + "net/http" + + "github.com/harness/gitness/internal/api/controller/pipeline" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" +) + +func HandleCreate(pipelineCtrl *pipeline.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + + in := new(pipeline.CreateInput) + err := json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(w, "Invalid Request Body: %s.", err) + return + } + + pipeline, err := pipelineCtrl.Create(ctx, session, in) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + render.JSON(w, http.StatusCreated, pipeline) + } +} diff --git a/internal/api/handler/pipeline/delete.go b/internal/api/handler/pipeline/delete.go new file mode 100644 index 000000000..1679df7a1 --- /dev/null +++ b/internal/api/handler/pipeline/delete.go @@ -0,0 +1,39 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipeline + +import ( + "net/http" + + "github.com/harness/gitness/internal/api/controller/pipeline" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/paths" +) + +func HandleDelete(pipelineCtrl *pipeline.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + pipelineRef, err := request.GetPipelineRefFromPath(r) + if err != nil { + render.TranslatedUserError(w, err) + return + } + spaceRef, pipelineUID, err := paths.DisectLeaf(pipelineRef) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + err = pipelineCtrl.Delete(ctx, session, spaceRef, pipelineUID) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + render.DeleteSuccessful(w) + } +} diff --git a/internal/api/handler/pipeline/find.go b/internal/api/handler/pipeline/find.go new file mode 100644 index 000000000..74ee6c77d --- /dev/null +++ b/internal/api/handler/pipeline/find.go @@ -0,0 +1,39 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipeline + +import ( + "net/http" + + "github.com/harness/gitness/internal/api/controller/pipeline" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/paths" +) + +func HandleFind(pipelineCtrl *pipeline.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + pipelineRef, err := request.GetPipelineRefFromPath(r) + if err != nil { + render.TranslatedUserError(w, err) + return + } + spaceRef, pipelineUID, err := paths.DisectLeaf(pipelineRef) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + pipeline, err := pipelineCtrl.Find(ctx, session, spaceRef, pipelineUID) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + render.JSON(w, http.StatusOK, pipeline) + } +} diff --git a/internal/api/handler/pipeline/update.go b/internal/api/handler/pipeline/update.go new file mode 100644 index 000000000..631ea4a77 --- /dev/null +++ b/internal/api/handler/pipeline/update.go @@ -0,0 +1,48 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package pipeline + +import ( + "encoding/json" + "net/http" + + "github.com/harness/gitness/internal/api/controller/pipeline" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/paths" +) + +func HandleUpdate(pipelineCtrl *pipeline.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + + in := new(pipeline.UpdateInput) + err := json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(w, "Invalid Request Body: %s.", err) + return + } + + pipelineRef, err := request.GetPipelineRefFromPath(r) + if err != nil { + render.TranslatedUserError(w, err) + return + } + spaceRef, pipelineUID, err := paths.DisectLeaf(pipelineRef) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + pipeline, err := pipelineCtrl.Update(ctx, session, spaceRef, pipelineUID, in) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + render.JSON(w, http.StatusOK, pipeline) + } +} diff --git a/internal/api/handler/secret/create.go b/internal/api/handler/secret/create.go new file mode 100644 index 000000000..f0e2ac143 --- /dev/null +++ b/internal/api/handler/secret/create.go @@ -0,0 +1,37 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package secret + +import ( + "encoding/json" + "net/http" + + "github.com/harness/gitness/internal/api/controller/secret" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" +) + +// HandleCreate returns a http.HandlerFunc that creates a new secretsitory. +func HandleCreate(secretCtrl *secret.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + + in := new(secret.CreateInput) + err := json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(w, "Invalid Request Body: %s.", err) + return + } + + secret, err := secretCtrl.Create(ctx, session, in) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + render.JSON(w, http.StatusCreated, secret.CopyWithoutData()) + } +} diff --git a/internal/api/handler/secret/delete.go b/internal/api/handler/secret/delete.go new file mode 100644 index 000000000..968a9d171 --- /dev/null +++ b/internal/api/handler/secret/delete.go @@ -0,0 +1,39 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package secret + +import ( + "net/http" + + "github.com/harness/gitness/internal/api/controller/secret" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/paths" +) + +func HandleDelete(secretCtrl *secret.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + secretRef, err := request.GetSecretRefFromPath(r) + if err != nil { + render.TranslatedUserError(w, err) + return + } + spaceRef, secretUID, err := paths.DisectLeaf(secretRef) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + err = secretCtrl.Delete(ctx, session, spaceRef, secretUID) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + render.DeleteSuccessful(w) + } +} diff --git a/internal/api/handler/secret/find.go b/internal/api/handler/secret/find.go new file mode 100644 index 000000000..e77890a71 --- /dev/null +++ b/internal/api/handler/secret/find.go @@ -0,0 +1,39 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package secret + +import ( + "net/http" + + "github.com/harness/gitness/internal/api/controller/secret" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/paths" +) + +// HandleFind finds a secret from the database. +func HandleFind(secretCtrl *secret.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + secretRef, err := request.GetSecretRefFromPath(r) + if err != nil { + render.TranslatedUserError(w, err) + return + } + spaceRef, secretUID, err := paths.DisectLeaf(secretRef) + if err != nil { + render.TranslatedUserError(w, err) + } + + secret, err := secretCtrl.Find(ctx, session, spaceRef, secretUID) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + render.JSON(w, http.StatusOK, secret.CopyWithoutData()) + } +} diff --git a/internal/api/handler/secret/update.go b/internal/api/handler/secret/update.go new file mode 100644 index 000000000..618319265 --- /dev/null +++ b/internal/api/handler/secret/update.go @@ -0,0 +1,47 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package secret + +import ( + "encoding/json" + "net/http" + + "github.com/harness/gitness/internal/api/controller/secret" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/internal/paths" +) + +func HandleUpdate(secretCtrl *secret.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + + in := new(secret.UpdateInput) + err := json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(w, "Invalid Request Body: %s.", err) + return + } + + secretRef, err := request.GetSecretRefFromPath(r) + if err != nil { + render.TranslatedUserError(w, err) + return + } + spaceRef, secretUID, err := paths.DisectLeaf(secretRef) + if err != nil { + render.TranslatedUserError(w, err) + } + + secret, err := secretCtrl.Update(ctx, session, spaceRef, secretUID, in) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + render.JSON(w, http.StatusOK, secret.CopyWithoutData()) + } +} diff --git a/internal/api/handler/space/list_pipelines.go b/internal/api/handler/space/list_pipelines.go new file mode 100644 index 000000000..30aba9b4b --- /dev/null +++ b/internal/api/handler/space/list_pipelines.go @@ -0,0 +1,35 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package space + +import ( + "net/http" + + "github.com/harness/gitness/internal/api/controller/space" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" +) + +func HandleListPipelines(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(w, err) + return + } + + filter := request.ParseListQueryFilterFromRequest(r) + repos, totalCount, err := spaceCtrl.ListPipelines(ctx, session, spaceRef, filter) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + render.Pagination(r, w, filter.Page, filter.Size, int(totalCount)) + render.JSON(w, http.StatusOK, repos) + } +} diff --git a/internal/api/handler/space/list_secrets.go b/internal/api/handler/space/list_secrets.go new file mode 100644 index 000000000..40c0002aa --- /dev/null +++ b/internal/api/handler/space/list_secrets.go @@ -0,0 +1,42 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package space + +import ( + "net/http" + + "github.com/harness/gitness/internal/api/controller/space" + "github.com/harness/gitness/internal/api/render" + "github.com/harness/gitness/internal/api/request" + "github.com/harness/gitness/types" +) + +func HandleListSecrets(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(w, err) + return + } + + filter := request.ParseListQueryFilterFromRequest(r) + ret, totalCount, err := spaceCtrl.ListSecrets(ctx, session, spaceRef, filter) + if err != nil { + render.TranslatedUserError(w, err) + return + } + + // Strip out data in the returned value + secrets := []types.Secret{} + for _, s := range ret { + secrets = append(secrets, *s.CopyWithoutData()) + } + + render.Pagination(r, w, filter.Page, filter.Size, int(totalCount)) + render.JSON(w, http.StatusOK, secrets) + } +} diff --git a/internal/api/openapi/openapi.go b/internal/api/openapi/openapi.go index 2e55f2e1d..5ea384d0e 100644 --- a/internal/api/openapi/openapi.go +++ b/internal/api/openapi/openapi.go @@ -41,6 +41,8 @@ func Generate() *openapi3.Spec { buildPrincipals(&reflector) spaceOperations(&reflector) repoOperations(&reflector) + pipelineOperations(&reflector) + secretOperations(&reflector) resourceOperations(&reflector) pullReqOperations(&reflector) webhookOperations(&reflector) diff --git a/internal/api/openapi/pipeline.go b/internal/api/openapi/pipeline.go new file mode 100644 index 000000000..533e29a09 --- /dev/null +++ b/internal/api/openapi/pipeline.go @@ -0,0 +1,162 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package openapi + +import ( + "net/http" + + "github.com/harness/gitness/internal/api/controller/execution" + "github.com/harness/gitness/internal/api/controller/pipeline" + "github.com/harness/gitness/internal/api/usererror" + "github.com/harness/gitness/types" + + "github.com/swaggest/openapi-go/openapi3" +) + +type pipelineRequest struct { + Ref string `path:"pipeline_ref"` +} + +type executionRequest struct { + pipelineRequest + Number string `path:"execution_number"` +} + +type createExecutionRequest struct { + pipelineRequest + execution.CreateInput +} + +type createPipelineRequest struct { + pipeline.CreateInput +} + +type getExecutionRequest struct { + executionRequest +} + +type getPipelineRequest struct { + pipelineRequest +} + +type updateExecutionRequest struct { + executionRequest + execution.UpdateInput +} + +type updatePipelineRequest struct { + pipelineRequest + pipeline.UpdateInput +} + +func pipelineOperations(reflector *openapi3.Reflector) { + opCreate := openapi3.Operation{} + opCreate.WithTags("pipeline") + opCreate.WithMapOfAnything(map[string]interface{}{"operationId": "createPipeline"}) + _ = reflector.SetRequest(&opCreate, new(createPipelineRequest), http.MethodPost) + _ = reflector.SetJSONResponse(&opCreate, new(types.Pipeline), http.StatusCreated) + _ = reflector.SetJSONResponse(&opCreate, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opCreate, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opCreate, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opCreate, new(usererror.Error), http.StatusForbidden) + _ = reflector.Spec.AddOperation(http.MethodPost, "/pipelines", opCreate) + + opFind := openapi3.Operation{} + opFind.WithTags("pipeline") + opFind.WithMapOfAnything(map[string]interface{}{"operationId": "findPipeline"}) + _ = reflector.SetRequest(&opFind, new(getPipelineRequest), http.MethodGet) + _ = reflector.SetJSONResponse(&opFind, new(types.Pipeline), http.StatusOK) + _ = reflector.SetJSONResponse(&opFind, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opFind, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opFind, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opFind, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodGet, "/pipelines/{pipeline_ref}", opFind) + + opDelete := openapi3.Operation{} + opDelete.WithTags("pipeline") + opDelete.WithMapOfAnything(map[string]interface{}{"operationId": "deletePipeline"}) + _ = reflector.SetRequest(&opDelete, new(getPipelineRequest), http.MethodDelete) + _ = reflector.SetJSONResponse(&opDelete, nil, http.StatusNoContent) + _ = reflector.SetJSONResponse(&opDelete, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opDelete, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opDelete, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opDelete, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodDelete, "/pipelines/{pipeline_ref}", opDelete) + + opUpdate := openapi3.Operation{} + opUpdate.WithTags("pipeline") + opUpdate.WithMapOfAnything(map[string]interface{}{"operationId": "updatePipeline"}) + _ = reflector.SetRequest(&opUpdate, new(updatePipelineRequest), http.MethodPatch) + _ = reflector.SetJSONResponse(&opUpdate, new(types.Pipeline), http.StatusOK) + _ = reflector.SetJSONResponse(&opUpdate, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opUpdate, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opUpdate, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opUpdate, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opUpdate, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodPatch, + "/pipelines/{pipeline_ref}", opUpdate) + + executionCreate := openapi3.Operation{} + executionCreate.WithTags("pipeline") + executionCreate.WithMapOfAnything(map[string]interface{}{"operationId": "createExecution"}) + _ = reflector.SetRequest(&executionCreate, new(createExecutionRequest), http.MethodPost) + _ = reflector.SetJSONResponse(&executionCreate, new(types.Execution), http.StatusCreated) + _ = reflector.SetJSONResponse(&executionCreate, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&executionCreate, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&executionCreate, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&executionCreate, new(usererror.Error), http.StatusForbidden) + _ = reflector.Spec.AddOperation(http.MethodPost, + "/pipelines/{pipeline_ref}/executions", executionCreate) + + executionFind := openapi3.Operation{} + executionFind.WithTags("pipeline") + executionFind.WithMapOfAnything(map[string]interface{}{"operationId": "findExecution"}) + _ = reflector.SetRequest(&executionFind, new(getExecutionRequest), http.MethodGet) + _ = reflector.SetJSONResponse(&executionFind, new(types.Execution), http.StatusOK) + _ = reflector.SetJSONResponse(&executionFind, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&executionFind, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&executionFind, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&executionFind, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodGet, + "/pipelines/{pipeline_ref}/executions/{execution_number}", executionFind) + + executionDelete := openapi3.Operation{} + executionDelete.WithTags("pipeline") + executionDelete.WithMapOfAnything(map[string]interface{}{"operationId": "deleteExecution"}) + _ = reflector.SetRequest(&executionDelete, new(getExecutionRequest), http.MethodDelete) + _ = reflector.SetJSONResponse(&executionDelete, nil, http.StatusNoContent) + _ = reflector.SetJSONResponse(&executionDelete, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&executionDelete, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&executionDelete, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&executionDelete, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodDelete, + "/pipelines/{pipeline_ref}/executions/{execution_number}", executionDelete) + + executionUpdate := openapi3.Operation{} + executionUpdate.WithTags("pipeline") + executionUpdate.WithMapOfAnything(map[string]interface{}{"operationId": "updateExecution"}) + _ = reflector.SetRequest(&executionUpdate, new(updateExecutionRequest), http.MethodPatch) + _ = reflector.SetJSONResponse(&executionUpdate, new(types.Execution), http.StatusOK) + _ = reflector.SetJSONResponse(&executionUpdate, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&executionUpdate, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&executionUpdate, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&executionUpdate, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&executionUpdate, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodPatch, + "/pipelines/{pipeline_ref}/executions/{execution_number}", executionUpdate) + + executionList := openapi3.Operation{} + executionList.WithTags("pipeline") + executionList.WithMapOfAnything(map[string]interface{}{"operationId": "listExecutions"}) + executionList.WithParameters(queryParameterPage, queryParameterLimit) + _ = reflector.SetRequest(&executionList, new(pipelineRequest), http.MethodGet) + _ = reflector.SetJSONResponse(&executionList, []types.Execution{}, http.StatusOK) + _ = reflector.SetJSONResponse(&executionList, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&executionList, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&executionList, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&executionList, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodGet, + "/pipelines/{pipeline_ref}/executions", executionList) +} diff --git a/internal/api/openapi/secret.go b/internal/api/openapi/secret.go new file mode 100644 index 000000000..f12fb77ea --- /dev/null +++ b/internal/api/openapi/secret.go @@ -0,0 +1,79 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package openapi + +import ( + "net/http" + + "github.com/harness/gitness/internal/api/controller/secret" + "github.com/harness/gitness/internal/api/usererror" + "github.com/harness/gitness/types" + + "github.com/swaggest/openapi-go/openapi3" +) + +type createSecretRequest struct { + secret.CreateInput +} + +type secretRequest struct { + Ref string `path:"secret_ref"` +} + +type getSecretRequest struct { + secretRequest +} + +type updateSecretRequest struct { + secretRequest + secret.UpdateInput +} + +func secretOperations(reflector *openapi3.Reflector) { + opCreate := openapi3.Operation{} + opCreate.WithTags("secret") + opCreate.WithMapOfAnything(map[string]interface{}{"operationId": "createSecret"}) + _ = reflector.SetRequest(&opCreate, new(createSecretRequest), http.MethodPost) + _ = reflector.SetJSONResponse(&opCreate, new(types.Secret), http.StatusCreated) + _ = reflector.SetJSONResponse(&opCreate, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opCreate, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opCreate, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opCreate, new(usererror.Error), http.StatusForbidden) + _ = reflector.Spec.AddOperation(http.MethodPost, "/secrets", opCreate) + + opFind := openapi3.Operation{} + opFind.WithTags("secret") + opFind.WithMapOfAnything(map[string]interface{}{"operationId": "findSecret"}) + _ = reflector.SetRequest(&opFind, new(getSecretRequest), http.MethodGet) + _ = reflector.SetJSONResponse(&opFind, new(types.Secret), http.StatusOK) + _ = reflector.SetJSONResponse(&opFind, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opFind, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opFind, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opFind, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodGet, "/secrets/{secret_ref}", opFind) + + opDelete := openapi3.Operation{} + opDelete.WithTags("secret") + opDelete.WithMapOfAnything(map[string]interface{}{"operationId": "deleteSecret"}) + _ = reflector.SetRequest(&opDelete, new(getSecretRequest), http.MethodDelete) + _ = reflector.SetJSONResponse(&opDelete, nil, http.StatusNoContent) + _ = reflector.SetJSONResponse(&opDelete, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opDelete, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opDelete, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opDelete, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodDelete, "/secrets/{secret_ref}", opDelete) + + opUpdate := openapi3.Operation{} + opUpdate.WithTags("secret") + opUpdate.WithMapOfAnything(map[string]interface{}{"operationId": "updateSecret"}) + _ = reflector.SetRequest(&opUpdate, new(updateSecretRequest), http.MethodPatch) + _ = reflector.SetJSONResponse(&opUpdate, new(types.Secret), http.StatusOK) + _ = reflector.SetJSONResponse(&opUpdate, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opUpdate, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opUpdate, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opUpdate, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opUpdate, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodPatch, "/secrets/{secret_ref}", opUpdate) +} diff --git a/internal/api/openapi/space.go b/internal/api/openapi/space.go index 04569762c..b6e5a0573 100644 --- a/internal/api/openapi/space.go +++ b/internal/api/openapi/space.go @@ -230,6 +230,30 @@ func spaceOperations(reflector *openapi3.Reflector) { _ = reflector.SetJSONResponse(&opRepos, new(usererror.Error), http.StatusNotFound) _ = reflector.Spec.AddOperation(http.MethodGet, "/spaces/{space_ref}/repos", opRepos) + opPipelines := openapi3.Operation{} + opPipelines.WithTags("space") + opPipelines.WithMapOfAnything(map[string]interface{}{"operationId": "listPipelines"}) + opPipelines.WithParameters(queryParameterQueryRepo, queryParameterPage, queryParameterLimit) + _ = reflector.SetRequest(&opPipelines, new(spaceRequest), http.MethodGet) + _ = reflector.SetJSONResponse(&opPipelines, []types.Pipeline{}, http.StatusOK) + _ = reflector.SetJSONResponse(&opPipelines, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opPipelines, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opPipelines, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opPipelines, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodGet, "/spaces/{space_ref}/pipelines", opPipelines) + + opSecrets := openapi3.Operation{} + opSecrets.WithTags("space") + opSecrets.WithMapOfAnything(map[string]interface{}{"operationId": "listSecrets"}) + opSecrets.WithParameters(queryParameterQueryRepo, queryParameterPage, queryParameterLimit) + _ = reflector.SetRequest(&opSecrets, new(spaceRequest), http.MethodGet) + _ = reflector.SetJSONResponse(&opSecrets, []types.Secret{}, http.StatusOK) + _ = reflector.SetJSONResponse(&opSecrets, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opSecrets, new(usererror.Error), http.StatusUnauthorized) + _ = reflector.SetJSONResponse(&opSecrets, new(usererror.Error), http.StatusForbidden) + _ = reflector.SetJSONResponse(&opSecrets, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation(http.MethodGet, "/spaces/{space_ref}/secrets", opSecrets) + opServiceAccounts := openapi3.Operation{} opServiceAccounts.WithTags("space") opServiceAccounts.WithMapOfAnything(map[string]interface{}{"operationId": "listServiceAccounts"}) diff --git a/internal/api/request/pipeline.go b/internal/api/request/pipeline.go new file mode 100644 index 000000000..8806102a3 --- /dev/null +++ b/internal/api/request/pipeline.go @@ -0,0 +1,29 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package request + +import ( + "net/http" + "net/url" +) + +const ( + PathParamPipelineRef = "pipeline_ref" + PathParamExecutionNumber = "execution_number" +) + +func GetPipelineRefFromPath(r *http.Request) (string, error) { + rawRef, err := PathParamOrError(r, PathParamPipelineRef) + if err != nil { + return "", err + } + + // paths are unescaped + return url.PathUnescape(rawRef) +} + +func GetExecutionNumberFromPath(r *http.Request) (int64, error) { + return PathParamAsPositiveInt64(r, PathParamExecutionNumber) +} diff --git a/internal/api/request/secret.go b/internal/api/request/secret.go new file mode 100644 index 000000000..d039a6f5b --- /dev/null +++ b/internal/api/request/secret.go @@ -0,0 +1,24 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package request + +import ( + "net/http" + "net/url" +) + +const ( + PathParamSecretRef = "secret_ref" +) + +func GetSecretRefFromPath(r *http.Request) (string, error) { + rawRef, err := PathParamOrError(r, PathParamSecretRef) + if err != nil { + return "", err + } + + // paths are unescaped + return url.PathUnescape(rawRef) +} diff --git a/internal/api/request/util.go b/internal/api/request/util.go index 8e3b47459..cc0be0a28 100644 --- a/internal/api/request/util.go +++ b/internal/api/request/util.go @@ -9,6 +9,7 @@ import ( "strconv" "github.com/harness/gitness/internal/api/usererror" + "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" "github.com/go-chi/chi" @@ -17,11 +18,10 @@ import ( const ( PathParamRemainder = "*" - QueryParamSort = "sort" - QueryParamOrder = "order" - QueryParamQuery = "query" - QueryParamCreatedBy = "created_by" + QueryParamSort = "sort" + QueryParamOrder = "order" + QueryParamQuery = "query" QueryParamState = "state" QueryParamKind = "kind" @@ -204,3 +204,19 @@ func ParseOrder(r *http.Request) enum.Order { func ParseSort(r *http.Request) string { return r.URL.Query().Get(QueryParamSort) } + +// ParsePaginationFromRequest parses pagination related info from the url. +func ParsePaginationFromRequest(r *http.Request) types.Pagination { + return types.Pagination{ + Page: ParsePage(r), + Size: ParseLimit(r), + } +} + +// ParseListQueryFilterFromRequest parses pagination and query related info from the url. +func ParseListQueryFilterFromRequest(r *http.Request) types.ListQueryFilter { + return types.ListQueryFilter{ + Query: ParseQuery(r), + Pagination: ParsePaginationFromRequest(r), + } +} diff --git a/internal/auth/authz/membership.go b/internal/auth/authz/membership.go index 0bcfc4f19..65bd4e398 100644 --- a/internal/auth/authz/membership.go +++ b/internal/auth/authz/membership.go @@ -63,6 +63,12 @@ func (a *MembershipAuthorizer) Check( case enum.ResourceTypeServiceAccount: spaceRef = scope.SpacePath + case enum.ResourceTypePipeline: + spaceRef = scope.SpacePath + + case enum.ResourceTypeSecret: + spaceRef = scope.SpacePath + case enum.ResourceTypeUser: // a user is allowed to view / edit themselves if resource.Name == session.Principal.UID && diff --git a/internal/router/api.go b/internal/router/api.go index a5c17226a..0e6f130d8 100644 --- a/internal/router/api.go +++ b/internal/router/api.go @@ -10,21 +10,27 @@ import ( "github.com/harness/gitness/githook" "github.com/harness/gitness/internal/api/controller/check" + "github.com/harness/gitness/internal/api/controller/execution" controllergithook "github.com/harness/gitness/internal/api/controller/githook" + "github.com/harness/gitness/internal/api/controller/pipeline" "github.com/harness/gitness/internal/api/controller/principal" "github.com/harness/gitness/internal/api/controller/pullreq" "github.com/harness/gitness/internal/api/controller/repo" + "github.com/harness/gitness/internal/api/controller/secret" "github.com/harness/gitness/internal/api/controller/serviceaccount" "github.com/harness/gitness/internal/api/controller/space" "github.com/harness/gitness/internal/api/controller/user" "github.com/harness/gitness/internal/api/controller/webhook" "github.com/harness/gitness/internal/api/handler/account" handlercheck "github.com/harness/gitness/internal/api/handler/check" + handlerexecution "github.com/harness/gitness/internal/api/handler/execution" handlergithook "github.com/harness/gitness/internal/api/handler/githook" + handlerpipeline "github.com/harness/gitness/internal/api/handler/pipeline" handlerprincipal "github.com/harness/gitness/internal/api/handler/principal" handlerpullreq "github.com/harness/gitness/internal/api/handler/pullreq" handlerrepo "github.com/harness/gitness/internal/api/handler/repo" "github.com/harness/gitness/internal/api/handler/resource" + handlersecret "github.com/harness/gitness/internal/api/handler/secret" handlerserviceaccount "github.com/harness/gitness/internal/api/handler/serviceaccount" handlerspace "github.com/harness/gitness/internal/api/handler/space" "github.com/harness/gitness/internal/api/handler/system" @@ -54,7 +60,7 @@ type APIHandler interface { var ( // terminatedPathPrefixesAPI is the list of prefixes that will require resolving terminated paths. - terminatedPathPrefixesAPI = []string{"/v1/spaces/", "/v1/repos/"} + terminatedPathPrefixesAPI = []string{"/v1/spaces/", "/v1/repos/", "/v1/pipelines/", "/v1/secrets/"} ) // NewAPIHandler returns a new APIHandler. @@ -62,7 +68,10 @@ func NewAPIHandler( config *types.Config, authenticator authn.Authenticator, repoCtrl *repo.Controller, + executionCtrl *execution.Controller, spaceCtrl *space.Controller, + pipelineCtrl *pipeline.Controller, + secretCtrl *secret.Controller, pullreqCtrl *pullreq.Controller, webhookCtrl *webhook.Controller, githookCtrl *controllergithook.Controller, @@ -92,7 +101,7 @@ func NewAPIHandler( r.Use(middlewareauthn.Attempt(authenticator, authn.SourceRouterAPI)) r.Route("/v1", func(r chi.Router) { - setupRoutesV1(r, repoCtrl, spaceCtrl, pullreqCtrl, webhookCtrl, githookCtrl, + setupRoutesV1(r, repoCtrl, executionCtrl, pipelineCtrl, secretCtrl, spaceCtrl, pullreqCtrl, webhookCtrl, githookCtrl, saCtrl, userCtrl, principalCtrl, checkCtrl) }) @@ -115,6 +124,9 @@ func corsHandler(config *types.Config) func(http.Handler) http.Handler { func setupRoutesV1(r chi.Router, repoCtrl *repo.Controller, + executionCtrl *execution.Controller, + pipelineCtrl *pipeline.Controller, + secretCtrl *secret.Controller, spaceCtrl *space.Controller, pullreqCtrl *pullreq.Controller, webhookCtrl *webhook.Controller, @@ -126,6 +138,8 @@ func setupRoutesV1(r chi.Router, ) { setupSpaces(r, spaceCtrl) setupRepos(r, repoCtrl, pullreqCtrl, webhookCtrl, checkCtrl) + setupPipelines(r, pipelineCtrl, executionCtrl) + setupSecrets(r, secretCtrl) setupUser(r, userCtrl) setupServiceAccounts(r, saCtrl) setupPrincipals(r, principalCtrl) @@ -151,6 +165,8 @@ func setupSpaces(r chi.Router, spaceCtrl *space.Controller) { r.Get("/spaces", handlerspace.HandleListSpaces(spaceCtrl)) r.Get("/repos", handlerspace.HandleListRepos(spaceCtrl)) r.Get("/service-accounts", handlerspace.HandleListServiceAccounts(spaceCtrl)) + r.Get("/pipelines", handlerspace.HandleListPipelines(spaceCtrl)) + r.Get("/secrets", handlerspace.HandleListSecrets(spaceCtrl)) // Child collections r.Route("/paths", func(r chi.Router) { @@ -268,6 +284,43 @@ func setupRepos(r chi.Router, }) } +func setupPipelines(r chi.Router, pipelineCtrl *pipeline.Controller, executionCtrl *execution.Controller) { + r.Route("/pipelines", func(r chi.Router) { + // Create takes path and parentId via body, not uri + r.Post("/", handlerpipeline.HandleCreate(pipelineCtrl)) + r.Route(fmt.Sprintf("/{%s}", request.PathParamPipelineRef), func(r chi.Router) { + r.Get("/", handlerpipeline.HandleFind(pipelineCtrl)) + r.Patch("/", handlerpipeline.HandleUpdate(pipelineCtrl)) + r.Delete("/", handlerpipeline.HandleDelete(pipelineCtrl)) + setupExecutions(r, pipelineCtrl, executionCtrl) + }) + }) +} + +func setupSecrets(r chi.Router, secretCtrl *secret.Controller) { + r.Route("/secrets", func(r chi.Router) { + // Create takes path and parentId via body, not uri + r.Post("/", handlersecret.HandleCreate(secretCtrl)) + r.Route(fmt.Sprintf("/{%s}", request.PathParamSecretRef), func(r chi.Router) { + r.Get("/", handlersecret.HandleFind(secretCtrl)) + r.Patch("/", handlersecret.HandleUpdate(secretCtrl)) + r.Delete("/", handlersecret.HandleDelete(secretCtrl)) + }) + }) +} + +func setupExecutions(r chi.Router, pipelineCtrl *pipeline.Controller, executionCtrl *execution.Controller) { + r.Route("/executions", func(r chi.Router) { + r.Get("/", handlerexecution.HandleList(executionCtrl)) + r.Post("/", handlerexecution.HandleCreate(executionCtrl)) + r.Route(fmt.Sprintf("/{%s}", request.PathParamExecutionNumber), func(r chi.Router) { + r.Get("/", handlerexecution.HandleFind(executionCtrl)) + r.Patch("/", handlerexecution.HandleUpdate(executionCtrl)) + r.Delete("/", handlerexecution.HandleDelete(executionCtrl)) + }) + }) +} + func setupInternal(r chi.Router, githookCtrl *controllergithook.Controller) { r.Route("/internal", func(r chi.Router) { SetupGitHooks(r, githookCtrl) diff --git a/internal/router/wire.go b/internal/router/wire.go index 3118ca790..d01d8e6d3 100644 --- a/internal/router/wire.go +++ b/internal/router/wire.go @@ -7,10 +7,13 @@ package router import ( "github.com/harness/gitness/gitrpc" "github.com/harness/gitness/internal/api/controller/check" + "github.com/harness/gitness/internal/api/controller/execution" "github.com/harness/gitness/internal/api/controller/githook" + "github.com/harness/gitness/internal/api/controller/pipeline" "github.com/harness/gitness/internal/api/controller/principal" "github.com/harness/gitness/internal/api/controller/pullreq" "github.com/harness/gitness/internal/api/controller/repo" + "github.com/harness/gitness/internal/api/controller/secret" "github.com/harness/gitness/internal/api/controller/serviceaccount" "github.com/harness/gitness/internal/api/controller/space" "github.com/harness/gitness/internal/api/controller/user" @@ -57,7 +60,10 @@ func ProvideAPIHandler( config *types.Config, authenticator authn.Authenticator, repoCtrl *repo.Controller, + executionCtrl *execution.Controller, spaceCtrl *space.Controller, + pipelineCtrl *pipeline.Controller, + secretCtrl *secret.Controller, pullreqCtrl *pullreq.Controller, webhookCtrl *webhook.Controller, githookCtrl *githook.Controller, @@ -66,8 +72,8 @@ func ProvideAPIHandler( principalCtrl principal.Controller, checkCtrl *check.Controller, ) APIHandler { - return NewAPIHandler(config, authenticator, repoCtrl, spaceCtrl, pullreqCtrl, - webhookCtrl, githookCtrl, saCtrl, userCtrl, principalCtrl, checkCtrl) + return NewAPIHandler(config, authenticator, repoCtrl, executionCtrl, spaceCtrl, pipelineCtrl, secretCtrl, + pullreqCtrl, webhookCtrl, githookCtrl, saCtrl, userCtrl, principalCtrl, checkCtrl) } func ProvideWebHandler(config *types.Config) WebHandler { diff --git a/internal/store/database.go b/internal/store/database.go index 5dc84cf30..f716f10ab 100644 --- a/internal/store/database.go +++ b/internal/store/database.go @@ -439,4 +439,90 @@ type ( // Delete removes a required status checks for a repo. Delete(ctx context.Context, repoID, reqCheckID int64) error } + PipelineStore interface { + // Find returns a pipeline given a pipeline ID from the datastore. + Find(ctx context.Context, id int64) (*types.Pipeline, error) + + // FindByUID returns a pipeline with a given UID in a space + FindByUID(ctx context.Context, id int64, uid string) (*types.Pipeline, error) + + // Create creates a new pipeline in the datastore. + Create(ctx context.Context, pipeline *types.Pipeline) error + + // Update tries to update a pipeline in the datastore + Update(ctx context.Context, pipeline *types.Pipeline) error + + // List lists the pipelines present in a parent space ID in the datastore. + List(ctx context.Context, spaceID int64, pagination types.ListQueryFilter) ([]*types.Pipeline, error) + + // UpdateOptLock updates the pipeline using the optimistic locking mechanism. + UpdateOptLock(ctx context.Context, pipeline *types.Pipeline, + mutateFn func(pipeline *types.Pipeline) error) (*types.Pipeline, error) + + // Delete deletes a pipeline ID from the datastore. + Delete(ctx context.Context, id int64) error + + // Count the number of pipelines in a space matching the given filter. + Count(ctx context.Context, spaceID int64, filter types.ListQueryFilter) (int64, error) + + // DeleteByUID deletes a pipeline with a given UID in a space + DeleteByUID(ctx context.Context, spaceID int64, uid string) error + + // IncrementSeqNum increments the sequence number of the pipeline + IncrementSeqNum(ctx context.Context, pipeline *types.Pipeline) (*types.Pipeline, error) + } + + SecretStore interface { + // Find returns a secret given an ID + Find(ctx context.Context, id int64) (*types.Secret, error) + + // FindByUID returns a secret given a space ID and a UID + FindByUID(ctx context.Context, spaceID int64, uid string) (*types.Secret, error) + + // Create creates a new secret + Create(ctx context.Context, secret *types.Secret) error + + // Count the number of secrets in a space matching the given filter. + Count(ctx context.Context, spaceID int64, pagination types.ListQueryFilter) (int64, error) + + // UpdateOptLock updates the secret using the optimistic locking mechanism. + UpdateOptLock(ctx context.Context, secret *types.Secret, + mutateFn func(secret *types.Secret) error) (*types.Secret, error) + + // Update tries to update a secret. + Update(ctx context.Context, secret *types.Secret) error + + // Delete deletes a secret given an ID. + Delete(ctx context.Context, id int64) error + + // DeleteByUID deletes a secret given a space ID and a uid + DeleteByUID(ctx context.Context, spaceID int64, uid string) error + + // List lists the secrets in a given space + List(ctx context.Context, spaceID int64, filter types.ListQueryFilter) ([]*types.Secret, error) + } + + ExecutionStore interface { + // Find returns a execution given a pipeline and an execution number + Find(ctx context.Context, pipelineID int64, num int64) (*types.Execution, error) + + // Create creates a new execution in the datastore. + Create(ctx context.Context, execution *types.Execution) error + + // Update tries to update an execution. + Update(ctx context.Context, execution *types.Execution) error + + // UpdateOptLock updates the execution using the optimistic locking mechanism. + UpdateOptLock(ctx context.Context, exectuion *types.Execution, + mutateFn func(execution *types.Execution) error) (*types.Execution, error) + + // List lists the executions for a given pipeline ID + List(ctx context.Context, pipelineID int64, pagination types.Pagination) ([]*types.Execution, error) + + // Delete deletes an execution given a pipeline ID and an execution number + Delete(ctx context.Context, pipelineID int64, num int64) error + + // Count the number of executions in a space + Count(ctx context.Context, parentID int64) (int64, error) + } ) diff --git a/internal/store/database/execution.go b/internal/store/database/execution.go new file mode 100644 index 000000000..187d33805 --- /dev/null +++ b/internal/store/database/execution.go @@ -0,0 +1,315 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package database + +import ( + "context" + "fmt" + "time" + + "github.com/harness/gitness/internal/store" + gitness_store "github.com/harness/gitness/store" + "github.com/harness/gitness/store/database" + "github.com/harness/gitness/store/database/dbtx" + "github.com/harness/gitness/types" + + "github.com/jmoiron/sqlx" + "github.com/pkg/errors" +) + +var _ store.ExecutionStore = (*executionStore)(nil) + +// NewExecutionStore returns a new ExecutionStore. +func NewExecutionStore(db *sqlx.DB) *executionStore { + return &executionStore{ + db: db, + } +} + +type executionStore struct { + db *sqlx.DB +} + +const ( + executionColumns = ` + execution_id + ,execution_pipeline_id + ,execution_repo_id + ,execution_trigger + ,execution_number + ,execution_parent + ,execution_status + ,execution_error + ,execution_event + ,execution_action + ,execution_link + ,execution_timestamp + ,execution_title + ,execution_message + ,execution_before + ,execution_after + ,execution_ref + ,execution_source_repo + ,execution_source + ,execution_target + ,execution_author + ,execution_author_name + ,execution_author_email + ,execution_author_avatar + ,execution_sender + ,execution_params + ,execution_cron + ,execution_deploy + ,execution_deploy_id + ,execution_debug + ,execution_started + ,execution_finished + ,execution_created + ,execution_updated + ,execution_version + ` +) + +// Find returns an execution given a pipeline ID and an execution number. +func (s *executionStore) Find(ctx context.Context, pipelineID int64, executionNum int64) (*types.Execution, error) { + const findQueryStmt = ` + SELECT` + executionColumns + ` + FROM executions + WHERE execution_pipeline_id = $1 AND execution_number = $2` + db := dbtx.GetAccessor(ctx, s.db) + + dst := new(types.Execution) + if err := db.GetContext(ctx, dst, findQueryStmt, pipelineID, executionNum); err != nil { + return nil, database.ProcessSQLErrorf(err, "Failed to find execution") + } + return dst, nil +} + +// Create creates a new execution in the datastore. +func (s *executionStore) Create(ctx context.Context, execution *types.Execution) error { + const executionInsertStmt = ` + INSERT INTO executions ( + execution_pipeline_id + ,execution_repo_id + ,execution_trigger + ,execution_number + ,execution_parent + ,execution_status + ,execution_error + ,execution_event + ,execution_action + ,execution_link + ,execution_timestamp + ,execution_title + ,execution_message + ,execution_before + ,execution_after + ,execution_ref + ,execution_source_repo + ,execution_source + ,execution_target + ,execution_author + ,execution_author_name + ,execution_author_email + ,execution_author_avatar + ,execution_sender + ,execution_params + ,execution_cron + ,execution_deploy + ,execution_deploy_id + ,execution_debug + ,execution_started + ,execution_finished + ,execution_created + ,execution_updated + ,execution_version + ) VALUES ( + :execution_pipeline_id + ,:execution_repo_id + ,:execution_trigger + ,:execution_number + ,:execution_parent + ,:execution_status + ,:execution_error + ,:execution_event + ,:execution_action + ,:execution_link + ,:execution_timestamp + ,:execution_title + ,:execution_message + ,:execution_before + ,:execution_after + ,:execution_ref + ,:execution_source_repo + ,:execution_source + ,:execution_target + ,:execution_author + ,:execution_author_name + ,:execution_author_email + ,:execution_author_avatar + ,:execution_sender + ,:execution_params + ,:execution_cron + ,:execution_deploy + ,:execution_deploy_id + ,:execution_debug + ,:execution_started + ,:execution_finished + ,:execution_created + ,:execution_updated + ,:execution_version + ) RETURNING execution_id` + db := dbtx.GetAccessor(ctx, s.db) + + query, arg, err := db.BindNamed(executionInsertStmt, execution) + if err != nil { + return database.ProcessSQLErrorf(err, "Failed to bind execution object") + } + + if err = db.QueryRowContext(ctx, query, arg...).Scan(&execution.ID); err != nil { + return database.ProcessSQLErrorf(err, "Execution query failed") + } + + return nil +} + +// Update tries to update an execution in the datastore with optimistic locking. +func (s *executionStore) Update(ctx context.Context, e *types.Execution) error { + const executionUpdateStmt = ` + UPDATE executions + SET + ,execution_status = :execution_status + ,execution_error = :execution_error + ,execution_event = :execution_event + ,execution_started = :execution_started + ,execution_finished = :execution_finished + ,execution_updated = :execution_updated + ,execution_version = :execution_version + WHERE execution_id = :execution_id AND execution_version = :execution_version - 1` + updatedAt := time.Now() + + execution := *e + + execution.Version++ + execution.Updated = updatedAt.UnixMilli() + + db := dbtx.GetAccessor(ctx, s.db) + + query, arg, err := db.BindNamed(executionUpdateStmt, execution) + if err != nil { + return database.ProcessSQLErrorf(err, "Failed to bind execution object") + } + + result, err := db.ExecContext(ctx, query, arg...) + if err != nil { + return database.ProcessSQLErrorf(err, "Failed to update execution") + } + + count, err := result.RowsAffected() + if err != nil { + return database.ProcessSQLErrorf(err, "Failed to get number of updated rows") + } + + if count == 0 { + return gitness_store.ErrVersionConflict + } + + e.Version = execution.Version + e.Updated = execution.Updated + return nil +} + +// UpdateOptLock updates the pipeline using the optimistic locking mechanism. +func (s *executionStore) UpdateOptLock(ctx context.Context, + execution *types.Execution, + mutateFn func(execution *types.Execution) error) (*types.Execution, error) { + for { + dup := *execution + + err := mutateFn(&dup) + if err != nil { + return nil, err + } + + err = s.Update(ctx, &dup) + if err == nil { + return &dup, nil + } + if !errors.Is(err, gitness_store.ErrVersionConflict) { + return nil, err + } + + execution, err = s.Find(ctx, execution.PipelineID, execution.Number) + if err != nil { + return nil, err + } + } +} + +// List lists the executions for a given pipeline ID. +func (s *executionStore) List( + ctx context.Context, + pipelineID int64, + pagination types.Pagination, +) ([]*types.Execution, error) { + stmt := database.Builder. + Select(executionColumns). + From("executions"). + Where("execution_pipeline_id = ?", fmt.Sprint(pipelineID)) + + stmt = stmt.Limit(database.Limit(pagination.Size)) + stmt = stmt.Offset(database.Offset(pagination.Page, pagination.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) + + dst := []*types.Execution{} + if err = db.SelectContext(ctx, &dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(err, "Failed executing custom list query") + } + + return dst, nil +} + +// Count of executions in a space. +func (s *executionStore) Count(ctx context.Context, pipelineID int64) (int64, error) { + stmt := database.Builder. + Select("count(*)"). + From("executions"). + Where("execution_pipeline_id = ?", pipelineID) + + sql, args, err := stmt.ToSql() + if err != nil { + return 0, errors.Wrap(err, "Failed to convert query to sql") + } + + db := dbtx.GetAccessor(ctx, s.db) + + var count int64 + err = db.QueryRowContext(ctx, sql, args...).Scan(&count) + if err != nil { + return 0, database.ProcessSQLErrorf(err, "Failed executing count query") + } + return count, nil +} + +// Delete deletes an execution given a pipeline ID and an execution number. +func (s *executionStore) Delete(ctx context.Context, pipelineID int64, executionNum int64) error { + const executionDeleteStmt = ` + DELETE FROM executions + WHERE execution_pipeline_id = $1 AND execution_number = $2` + + db := dbtx.GetAccessor(ctx, s.db) + + if _, err := db.ExecContext(ctx, executionDeleteStmt, pipelineID, executionNum); err != nil { + return database.ProcessSQLErrorf(err, "Could not delete execution") + } + + return nil +} diff --git a/internal/store/database/migrate/ci/ci_migrations.sql b/internal/store/database/migrate/ci/ci_migrations.sql new file mode 100644 index 000000000..2b1ed484d --- /dev/null +++ b/internal/store/database/migrate/ci/ci_migrations.sql @@ -0,0 +1,103 @@ +CREATE TABLE IF NOT EXISTS pipelines ( + pipeline_id INTEGER PRIMARY KEY AUTOINCREMENT + ,pipeline_description TEXT NOT NULL + ,pipeline_space_id INTEGER NOT NULL + ,pipeline_uid TEXT NOT NULL + ,pipeline_seq INTEGER NOT NULL DEFAULT 0 + ,pipeline_repo_id INTEGER + ,pipeline_repo_type TEXT NOT NULL + ,pipeline_repo_name TEXT + ,pipeline_default_branch TEXT + ,pipeline_config_path TEXT NOT NULL + ,pipeline_created INTEGER NOT NULL + ,pipeline_updated INTEGER NOT NULL + ,pipeline_version INTEGER NOT NULL + + -- Ensure unique combination of UID and ParentID + ,UNIQUE (pipeline_space_id, pipeline_uid) + + -- Foreign key to spaces table + ,CONSTRAINT fk_pipeline_space_id FOREIGN KEY (pipeline_space_id) + REFERENCES spaces (space_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE + + -- Foreign key to repositories table + ,CONSTRAINT fk_pipelines_repo_id FOREIGN KEY (pipeline_repo_id) + REFERENCES repositories (repo_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS executions ( + execution_id INTEGER PRIMARY KEY AUTOINCREMENT + ,execution_pipeline_id INTEGER NOT NULL + ,execution_repo_id INTEGER + ,execution_trigger TEXT + ,execution_number INTEGER NOT NULL + ,execution_parent INTEGER + ,execution_status TEXT + ,execution_error TEXT + ,execution_event TEXT + ,execution_action TEXT + ,execution_link TEXT + ,execution_timestamp INTEGER + ,execution_title TEXT + ,execution_message TEXT + ,execution_before TEXT + ,execution_after TEXT + ,execution_ref TEXT + ,execution_source_repo TEXT + ,execution_source TEXT + ,execution_target TEXT + ,execution_author TEXT + ,execution_author_name TEXT + ,execution_author_email TEXT + ,execution_author_avatar TEXT + ,execution_sender TEXT + ,execution_params TEXT + ,execution_cron TEXT + ,execution_deploy TEXT + ,execution_deploy_id INTEGER + ,execution_debug BOOLEAN NOT NULL DEFAULT 0 + ,execution_started INTEGER + ,execution_finished INTEGER + ,execution_created INTEGER NOT NULL + ,execution_updated INTEGER NOT NULL + ,execution_version INTEGER NOT NULL + + -- Ensure unique combination of pipeline ID and number + ,UNIQUE (execution_pipeline_id, execution_number) + + -- Foreign key to pipelines table + ,CONSTRAINT fk_executions_pipeline_id FOREIGN KEY (execution_pipeline_id) + REFERENCES pipelines (pipeline_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE + + -- Foreign key to repositories table + ,CONSTRAINT fk_executions_repo_id FOREIGN KEY (execution_repo_id) + REFERENCES repositories (repo_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS secrets ( + secret_id INTEGER PRIMARY KEY AUTOINCREMENT + ,secret_uid TEXT NOT NULL + ,secret_space_id INTEGER NOT NULL + ,secret_description TEXT NOT NULL + ,secret_data BLOB NOT NULL + ,secret_created INTEGER NOT NULL + ,secret_updated INTEGER NOT NULL + ,secret_version INTEGER NOT NULL + + -- Ensure unique combination of space ID and UID + ,UNIQUE (secret_space_id, secret_uid) + + -- Foreign key to spaces table + ,CONSTRAINT fk_secrets_space_id FOREIGN KEY (secret_space_id) + REFERENCES spaces (space_id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +); \ No newline at end of file diff --git a/internal/store/database/pipeline.go b/internal/store/database/pipeline.go new file mode 100644 index 000000000..9ac2aaaf7 --- /dev/null +++ b/internal/store/database/pipeline.go @@ -0,0 +1,307 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package database + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/harness/gitness/internal/store" + gitness_store "github.com/harness/gitness/store" + "github.com/harness/gitness/store/database" + "github.com/harness/gitness/store/database/dbtx" + "github.com/harness/gitness/types" + + "github.com/jmoiron/sqlx" + "github.com/pkg/errors" +) + +var _ store.PipelineStore = (*pipelineStore)(nil) + +const ( + pipelineQueryBase = ` + SELECT` + + pipelineColumns + ` + FROM pipelines` + + pipelineColumns = ` + pipeline_id + ,pipeline_description + ,pipeline_space_id + ,pipeline_uid + ,pipeline_seq + ,pipeline_repo_id + ,pipeline_repo_type + ,pipeline_repo_name + ,pipeline_default_branch + ,pipeline_config_path + ,pipeline_created + ,pipeline_updated + ,pipeline_version + ` +) + +// NewPipelineStore returns a new PipelineStore. +func NewPipelineStore(db *sqlx.DB) *pipelineStore { + return &pipelineStore{ + db: db, + } +} + +type pipelineStore struct { + db *sqlx.DB +} + +// Find returns a pipeline given a pipeline ID. +func (s *pipelineStore) Find(ctx context.Context, id int64) (*types.Pipeline, error) { + const findQueryStmt = pipelineQueryBase + ` + WHERE pipeline_id = $1` + db := dbtx.GetAccessor(ctx, s.db) + + dst := new(types.Pipeline) + if err := db.GetContext(ctx, dst, findQueryStmt, id); err != nil { + return nil, database.ProcessSQLErrorf(err, "Failed to find pipeline") + } + return dst, nil +} + +// FindByUID returns a pipeline in a given space with a given UID. +func (s *pipelineStore) FindByUID(ctx context.Context, spaceID int64, uid string) (*types.Pipeline, error) { + const findQueryStmt = pipelineQueryBase + ` + WHERE pipeline_space_id = $1 AND pipeline_uid = $2` + db := dbtx.GetAccessor(ctx, s.db) + + dst := new(types.Pipeline) + if err := db.GetContext(ctx, dst, findQueryStmt, spaceID, uid); err != nil { + return nil, database.ProcessSQLErrorf(err, "Failed to find pipeline") + } + return dst, nil +} + +// Create creates a pipeline. +func (s *pipelineStore) Create(ctx context.Context, pipeline *types.Pipeline) error { + const pipelineInsertStmt = ` + INSERT INTO pipelines ( + pipeline_description + ,pipeline_space_id + ,pipeline_uid + ,pipeline_seq + ,pipeline_repo_id + ,pipeline_repo_type + ,pipeline_repo_name + ,pipeline_default_branch + ,pipeline_config_path + ,pipeline_created + ,pipeline_updated + ,pipeline_version + ) VALUES ( + :pipeline_description, + :pipeline_space_id, + :pipeline_uid, + :pipeline_seq, + :pipeline_repo_id, + :pipeline_repo_type, + :pipeline_repo_name, + :pipeline_default_branch, + :pipeline_config_path, + :pipeline_created, + :pipeline_updated, + :pipeline_version + ) RETURNING pipeline_id` + db := dbtx.GetAccessor(ctx, s.db) + + query, arg, err := db.BindNamed(pipelineInsertStmt, pipeline) + if err != nil { + return database.ProcessSQLErrorf(err, "Failed to bind pipeline object") + } + + if err = db.QueryRowContext(ctx, query, arg...).Scan(&pipeline.ID); err != nil { + return database.ProcessSQLErrorf(err, "Pipeline query failed") + } + + return nil +} + +// Update updates a pipeline. +func (s *pipelineStore) Update(ctx context.Context, p *types.Pipeline) error { + const pipelineUpdateStmt = ` + UPDATE pipelines + SET + pipeline_description = :pipeline_description, + pipeline_uid = :pipeline_uid, + pipeline_seq = :pipeline_seq, + pipeline_default_branch = :pipeline_default_branch, + pipeline_config_path = :pipeline_config_path, + pipeline_updated = :pipeline_updated, + pipeline_version = :pipeline_version + WHERE pipeline_id = :pipeline_id AND pipeline_version = :pipeline_version - 1` + updatedAt := time.Now() + pipeline := *p + + pipeline.Version++ + pipeline.Updated = updatedAt.UnixMilli() + + db := dbtx.GetAccessor(ctx, s.db) + + query, arg, err := db.BindNamed(pipelineUpdateStmt, pipeline) + if err != nil { + return database.ProcessSQLErrorf(err, "Failed to bind pipeline object") + } + + result, err := db.ExecContext(ctx, query, arg...) + if err != nil { + return database.ProcessSQLErrorf(err, "Failed to update pipeline") + } + + count, err := result.RowsAffected() + if err != nil { + return database.ProcessSQLErrorf(err, "Failed to get number of updated rows") + } + + if count == 0 { + return gitness_store.ErrVersionConflict + } + + p.Updated = pipeline.Updated + p.Version = pipeline.Version + return nil +} + +// List lists all the pipelines present in a space. +func (s *pipelineStore) List( + ctx context.Context, + parentID int64, + filter types.ListQueryFilter, +) ([]*types.Pipeline, error) { + stmt := database.Builder. + Select(pipelineColumns). + From("pipelines"). + Where("pipeline_space_id = ?", fmt.Sprint(parentID)) + + if filter.Query != "" { + stmt = stmt.Where("LOWER(pipeline_uid) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(filter.Query))) + } + + stmt = stmt.Limit(database.Limit(filter.Size)) + stmt = stmt.Offset(database.Offset(filter.Page, filter.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) + + dst := []*types.Pipeline{} + if err = db.SelectContext(ctx, &dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(err, "Failed executing custom list query") + } + + return dst, nil +} + +// UpdateOptLock updates the pipeline using the optimistic locking mechanism. +func (s *pipelineStore) UpdateOptLock(ctx context.Context, + pipeline *types.Pipeline, + mutateFn func(pipeline *types.Pipeline) error) (*types.Pipeline, error) { + for { + dup := *pipeline + + err := mutateFn(&dup) + if err != nil { + return nil, err + } + + err = s.Update(ctx, &dup) + if err == nil { + return &dup, nil + } + if !errors.Is(err, gitness_store.ErrVersionConflict) { + return nil, err + } + + pipeline, err = s.Find(ctx, pipeline.ID) + if err != nil { + return nil, err + } + } +} + +// Count of pipelines in a space. +func (s *pipelineStore) Count(ctx context.Context, parentID int64, filter types.ListQueryFilter) (int64, error) { + stmt := database.Builder. + Select("count(*)"). + From("pipelines"). + Where("pipeline_space_id = ?", parentID) + + if filter.Query != "" { + stmt = stmt.Where("pipeline_uid LIKE ?", fmt.Sprintf("%%%s%%", filter.Query)) + } + + sql, args, err := stmt.ToSql() + if err != nil { + return 0, errors.Wrap(err, "Failed to convert query to sql") + } + + db := dbtx.GetAccessor(ctx, s.db) + + var count int64 + err = db.QueryRowContext(ctx, sql, args...).Scan(&count) + if err != nil { + return 0, database.ProcessSQLErrorf(err, "Failed executing count query") + } + return count, nil +} + +// Delete deletes a pipeline given a pipeline ID. +func (s *pipelineStore) Delete(ctx context.Context, id int64) error { + const pipelineDeleteStmt = ` + DELETE FROM pipelines + WHERE pipeline_id = $1` + + db := dbtx.GetAccessor(ctx, s.db) + + if _, err := db.ExecContext(ctx, pipelineDeleteStmt, id); err != nil { + return database.ProcessSQLErrorf(err, "Could not delete pipeline") + } + + return nil +} + +// DeleteByUID deletes a pipeline with a given UID in a space. +func (s *pipelineStore) DeleteByUID(ctx context.Context, spaceID int64, uid string) error { + const pipelineDeleteStmt = ` + DELETE FROM pipelines + WHERE pipeline_space_id = $1 AND pipeline_uid = $2` + + db := dbtx.GetAccessor(ctx, s.db) + + if _, err := db.ExecContext(ctx, pipelineDeleteStmt, spaceID, uid); err != nil { + return database.ProcessSQLErrorf(err, "Could not delete pipeline") + } + + return nil +} + +// Increment increments the pipeline sequence number. It will keep retrying in case +// of optimistic lock errors. +func (s *pipelineStore) IncrementSeqNum(ctx context.Context, pipeline *types.Pipeline) (*types.Pipeline, error) { + for { + var err error + pipeline.Seq++ + err = s.Update(ctx, pipeline) + if err == nil { + return pipeline, nil + } else if !errors.Is(err, gitness_store.ErrVersionConflict) { + return pipeline, errors.Wrap(err, "could not increment pipeline sequence number") + } + pipeline, err = s.Find(ctx, pipeline.ID) + if err != nil { + return nil, errors.Wrap(err, "could not increment pipeline sequence number") + } + } +} diff --git a/internal/store/database/secret.go b/internal/store/database/secret.go new file mode 100644 index 000000000..8ade23d57 --- /dev/null +++ b/internal/store/database/secret.go @@ -0,0 +1,266 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package database + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/harness/gitness/internal/store" + gitness_store "github.com/harness/gitness/store" + "github.com/harness/gitness/store/database" + "github.com/harness/gitness/store/database/dbtx" + "github.com/harness/gitness/types" + + "github.com/jmoiron/sqlx" + "github.com/pkg/errors" +) + +var _ store.SecretStore = (*secretStore)(nil) + +const ( + secretQueryBase = ` + SELECT` + secretColumns + ` + FROM secrets` + + secretColumns = ` + secret_id, + secret_description, + secret_space_id, + secret_uid, + secret_data, + secret_created, + secret_updated, + secret_version + ` +) + +// NewSecretStore returns a new SecretStore. +func NewSecretStore(db *sqlx.DB) *secretStore { + return &secretStore{ + db: db, + } +} + +type secretStore struct { + db *sqlx.DB +} + +// Find returns a secret given a secret ID. +func (s *secretStore) Find(ctx context.Context, id int64) (*types.Secret, error) { + const findQueryStmt = secretQueryBase + ` + WHERE secret_id = $1` + db := dbtx.GetAccessor(ctx, s.db) + + dst := new(types.Secret) + if err := db.GetContext(ctx, dst, findQueryStmt, id); err != nil { + return nil, database.ProcessSQLErrorf(err, "Failed to find secret") + } + return dst, nil +} + +// FindByUID returns a secret in a given space with a given UID. +func (s *secretStore) FindByUID(ctx context.Context, spaceID int64, uid string) (*types.Secret, error) { + const findQueryStmt = secretQueryBase + ` + WHERE secret_space_id = $1 AND secret_uid = $2` + db := dbtx.GetAccessor(ctx, s.db) + + dst := new(types.Secret) + if err := db.GetContext(ctx, dst, findQueryStmt, spaceID, uid); err != nil { + return nil, database.ProcessSQLErrorf(err, "Failed to find secret") + } + return dst, nil +} + +// Create creates a secret. +func (s *secretStore) Create(ctx context.Context, secret *types.Secret) error { + const secretInsertStmt = ` + INSERT INTO secrets ( + secret_description, + secret_space_id, + secret_uid, + secret_data, + secret_created, + secret_updated, + secret_version + ) VALUES ( + :secret_description, + :secret_space_id, + :secret_uid, + :secret_data, + :secret_created, + :secret_updated, + :secret_version + ) RETURNING secret_id` + db := dbtx.GetAccessor(ctx, s.db) + + query, arg, err := db.BindNamed(secretInsertStmt, secret) + if err != nil { + return database.ProcessSQLErrorf(err, "Failed to bind secret object") + } + + if err = db.QueryRowContext(ctx, query, arg...).Scan(&secret.ID); err != nil { + return database.ProcessSQLErrorf(err, "secret query failed") + } + + return nil +} + +func (s *secretStore) Update(ctx context.Context, p *types.Secret) error { + const secretUpdateStmt = ` + UPDATE secrets + SET + secret_description = :secret_description, + secret_uid = :secret_uid, + secret_data = :secret_data, + secret_updated = :secret_updated, + secret_version = :secret_version + WHERE secret_id = :secret_id AND secret_version = :secret_version - 1` + updatedAt := time.Now() + secret := *p + + secret.Version++ + secret.Updated = updatedAt.UnixMilli() + + db := dbtx.GetAccessor(ctx, s.db) + + query, arg, err := db.BindNamed(secretUpdateStmt, secret) + if err != nil { + return database.ProcessSQLErrorf(err, "Failed to bind secret object") + } + + result, err := db.ExecContext(ctx, query, arg...) + if err != nil { + return database.ProcessSQLErrorf(err, "Failed to update secret") + } + + count, err := result.RowsAffected() + if err != nil { + return database.ProcessSQLErrorf(err, "Failed to get number of updated rows") + } + + if count == 0 { + return gitness_store.ErrVersionConflict + } + + p.Version = secret.Version + p.Updated = secret.Updated + return nil +} + +// UpdateOptLock updates the pipeline using the optimistic locking mechanism. +func (s *secretStore) UpdateOptLock(ctx context.Context, + secret *types.Secret, + mutateFn func(secret *types.Secret) error, +) (*types.Secret, error) { + for { + dup := *secret + + err := mutateFn(&dup) + if err != nil { + return nil, err + } + + err = s.Update(ctx, &dup) + if err == nil { + return &dup, nil + } + if !errors.Is(err, gitness_store.ErrVersionConflict) { + return nil, err + } + + secret, err = s.Find(ctx, secret.ID) + if err != nil { + return nil, err + } + } +} + +// List lists all the secrets present in a space. +func (s *secretStore) List(ctx context.Context, parentID int64, filter types.ListQueryFilter) ([]*types.Secret, error) { + stmt := database.Builder. + Select(secretColumns). + From("secrets"). + Where("secret_space_id = ?", fmt.Sprint(parentID)) + + if filter.Query != "" { + stmt = stmt.Where("LOWER(secret_uid) LIKE ?", fmt.Sprintf("%%%s%%", strings.ToLower(filter.Query))) + } + + stmt = stmt.Limit(database.Limit(filter.Size)) + stmt = stmt.Offset(database.Offset(filter.Page, filter.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) + + dst := []*types.Secret{} + if err = db.SelectContext(ctx, &dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(err, "Failed executing custom list query") + } + + return dst, nil +} + +// Delete deletes a secret given a secret ID. +func (s *secretStore) Delete(ctx context.Context, id int64) error { + const secretDeleteStmt = ` + DELETE FROM secrets + WHERE secret_id = $1` + + db := dbtx.GetAccessor(ctx, s.db) + + if _, err := db.ExecContext(ctx, secretDeleteStmt, id); err != nil { + return database.ProcessSQLErrorf(err, "Could not delete secret") + } + + return nil +} + +// DeleteByUID deletes a secret with a given UID in a space. +func (s *secretStore) DeleteByUID(ctx context.Context, spaceID int64, uid string) error { + const secretDeleteStmt = ` + DELETE FROM secrets + WHERE secret_space_id = $1 AND secret_uid = $2` + + db := dbtx.GetAccessor(ctx, s.db) + + if _, err := db.ExecContext(ctx, secretDeleteStmt, spaceID, uid); err != nil { + return database.ProcessSQLErrorf(err, "Could not delete secret") + } + + return nil +} + +// Count of secrets in a space. +func (s *secretStore) Count(ctx context.Context, parentID int64, filter types.ListQueryFilter) (int64, error) { + stmt := database.Builder. + Select("count(*)"). + From("secrets"). + Where("secret_space_id = ?", parentID) + + if filter.Query != "" { + stmt = stmt.Where("secret_uid LIKE ?", fmt.Sprintf("%%%s%%", filter.Query)) + } + + sql, args, err := stmt.ToSql() + if err != nil { + return 0, errors.Wrap(err, "Failed to convert query to sql") + } + + db := dbtx.GetAccessor(ctx, s.db) + + var count int64 + err = db.QueryRowContext(ctx, sql, args...).Scan(&count) + if err != nil { + return 0, database.ProcessSQLErrorf(err, "Failed executing count query") + } + return count, nil +} diff --git a/internal/store/database/wire.go b/internal/store/database/wire.go index ff6a66870..d61f7879c 100644 --- a/internal/store/database/wire.go +++ b/internal/store/database/wire.go @@ -23,6 +23,9 @@ var WireSet = wire.NewSet( ProvidePathStore, ProvideSpaceStore, ProvideRepoStore, + ProvideExecutionStore, + ProvidePipelineStore, + ProvideSecretStore, ProvideRepoGitInfoView, ProvideMembershipStore, ProvideTokenStore, @@ -78,6 +81,21 @@ func ProvideRepoStore(db *sqlx.DB, pathCache store.PathCache) store.RepoStore { return NewRepoStore(db, pathCache) } +// ProvidePipelineStore provides a pipeline store. +func ProvidePipelineStore(db *sqlx.DB) store.PipelineStore { + return NewPipelineStore(db) +} + +// ProvideSecretStore provides a secret store. +func ProvideSecretStore(db *sqlx.DB) store.SecretStore { + return NewSecretStore(db) +} + +// ProvideExecutionStore provides an execution store. +func ProvideExecutionStore(db *sqlx.DB) store.ExecutionStore { + return NewExecutionStore(db) +} + // ProvideRepoGitInfoView provides a repo git UID view. func ProvideRepoGitInfoView(db *sqlx.DB) store.RepoGitInfoView { return NewRepoGitInfoView(db) diff --git a/mocks/mock_client.go b/mocks/mock_client.go index b16a6490e..3cb746055 100644 --- a/mocks/mock_client.go +++ b/mocks/mock_client.go @@ -8,9 +8,10 @@ import ( context "context" reflect "reflect" - gomock "github.com/golang/mock/gomock" user "github.com/harness/gitness/internal/api/controller/user" types "github.com/harness/gitness/types" + + gomock "github.com/golang/mock/gomock" ) // MockClient is a mock of Client interface. diff --git a/mocks/mock_store.go b/mocks/mock_store.go index 0af0bbc34..9310f3729 100644 --- a/mocks/mock_store.go +++ b/mocks/mock_store.go @@ -8,9 +8,10 @@ import ( context "context" reflect "reflect" - gomock "github.com/golang/mock/gomock" types "github.com/harness/gitness/types" enum "github.com/harness/gitness/types/enum" + + gomock "github.com/golang/mock/gomock" ) // MockPrincipalStore is a mock of PrincipalStore interface. diff --git a/types/config.go b/types/config.go index b08f391aa..a94fbcb91 100644 --- a/types/config.go +++ b/types/config.go @@ -52,6 +52,12 @@ type Config struct { DefaultBranch string `envconfig:"GITNESS_GIT_DEFAULTBRANCH" default:"main"` } + // Encrypter defines the parameters for the encrypter + Encrypter struct { + Secret string `envconfig:"GITNESS_ENCRYPTER_SECRET"` // key used for encryption + MixedContent bool `envconfig:"GITNESS_ENCRYPTER_MIXED_CONTENT"` + } + // Server defines the server configuration parameters. Server struct { // HTTP defines the http configuration parameters diff --git a/types/enum/membership_role.go b/types/enum/membership_role.go index f7888d2c2..fbe2b9d6a 100644 --- a/types/enum/membership_role.go +++ b/types/enum/membership_role.go @@ -24,10 +24,14 @@ var membershipRoleReaderPermissions = slices.Clip(slices.Insert([]Permission{}, PermissionRepoView, PermissionSpaceView, PermissionServiceAccountView, + PermissionPipelineView, + PermissionSecretView, )) var membershipRoleExecutorPermissions = slices.Clip(slices.Insert(membershipRoleReaderPermissions, 0, PermissionCommitCheckReport, + PermissionPipelineExecute, + PermissionSecretAccess, )) var membershipRoleContributorPermissions = slices.Clip(slices.Insert(membershipRoleReaderPermissions, 0, @@ -47,6 +51,16 @@ var membershipRoleSpaceOwnerPermissions = slices.Clip(slices.Insert(membershipRo PermissionServiceAccountCreate, PermissionServiceAccountEdit, PermissionServiceAccountDelete, + + PermissionPipelineEdit, + PermissionPipelineExecute, + PermissionPipelineDelete, + PermissionPipelineView, + + PermissionSecretAccess, + PermissionSecretDelete, + PermissionSecretEdit, + PermissionSecretView, )) func init() { diff --git a/types/enum/permission.go b/types/enum/permission.go index 6824f035a..fe8d0fc6e 100644 --- a/types/enum/permission.go +++ b/types/enum/permission.go @@ -13,6 +13,8 @@ const ( ResourceTypeUser ResourceType = "USER" ResourceTypeServiceAccount ResourceType = "SERVICEACCOUNT" ResourceTypeService ResourceType = "SERVICE" + ResourceTypePipeline ResourceType = "PIPELINE" + ResourceTypeSecret ResourceType = "SECRET" // ResourceType_Branch ResourceType = "BRANCH" ) @@ -71,6 +73,26 @@ const ( PermissionServiceEditAdmin Permission = "service_editAdmin" ) +const ( + /* + ----- PIPELINE ----- + */ + PermissionPipelineView Permission = "pipeline_view" + PermissionPipelineEdit Permission = "pipeline_edit" + PermissionPipelineDelete Permission = "pipeline_delete" + PermissionPipelineExecute Permission = "pipeline_execute" +) + +const ( + /* + ----- SECRET ----- + */ + PermissionSecretView Permission = "secret_view" + PermissionSecretEdit Permission = "secret_edit" + PermissionSecretDelete Permission = "secret_delete" + PermissionSecretAccess Permission = "secret_access" +) + const ( /* ----- COMMIT CHECK ----- diff --git a/types/enum/scm.go b/types/enum/scm.go new file mode 100644 index 000000000..88e1efdcb --- /dev/null +++ b/types/enum/scm.go @@ -0,0 +1,24 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package enum + +// ScmType defines the different SCM types supported for CI. +type ScmType string + +func (ScmType) Enum() []interface{} { return toInterfaceSlice(scmTypes) } + +var scmTypes = ([]ScmType{ + ScmTypeGitness, + ScmTypeGithub, + ScmTypeGitlab, + ScmTypeUnknown, +}) + +const ( + ScmTypeUnknown ScmType = "UNKNOWN" + ScmTypeGitness ScmType = "GITNESS" + ScmTypeGithub ScmType = "GITHUB" + ScmTypeGitlab ScmType = "GITLAB" +) diff --git a/types/execution.go b/types/execution.go new file mode 100644 index 000000000..a66ff6680 --- /dev/null +++ b/types/execution.go @@ -0,0 +1,46 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package types + +// Execution represents an instance of a pipeline execution. +type Execution struct { + ID int64 `db:"execution_id" json:"id"` + PipelineID int64 `db:"execution_pipeline_id" json:"pipeline_id"` + RepoID int64 `db:"execution_repo_id" json:"repo_id"` + Trigger string `db:"execution_trigger" json:"trigger"` + Number int64 `db:"execution_number" json:"number"` + Parent int64 `db:"execution_parent" json:"parent,omitempty"` + Status string `db:"execution_status" json:"status"` + Error string `db:"execution_error" json:"error,omitempty"` + Event string `db:"execution_event" json:"event"` + Action string `db:"execution_action" json:"action"` + Link string `db:"execution_link" json:"link"` + Timestamp int64 `db:"execution_timestamp" json:"timestamp"` + Title string `db:"execution_title" json:"title,omitempty"` + Message string `db:"execution_message" json:"message"` + Before string `db:"execution_before" json:"before"` + After string `db:"execution_after" json:"after"` + Ref string `db:"execution_ref" json:"ref"` + Fork string `db:"execution_source_repo" json:"source_repo"` + Source string `db:"execution_source" json:"source"` + Target string `db:"execution_target" json:"target"` + Author string `db:"execution_author" json:"author_login"` + AuthorName string `db:"execution_author_name" json:"author_name"` + AuthorEmail string `db:"execution_author_email" json:"author_email"` + AuthorAvatar string `db:"execution_author_avatar" json:"author_avatar"` + Sender string `db:"execution_sender" json:"sender"` + Params string `db:"execution_params" json:"params,omitempty"` + Cron string `db:"execution_cron" json:"cron,omitempty"` + Deploy string `db:"execution_deploy" json:"deploy_to,omitempty"` + DeployID int64 `db:"execution_deploy_id" json:"deploy_id,omitempty"` + Debug bool `db:"execution_debug" json:"debug,omitempty"` + Started int64 `db:"execution_started" json:"started"` + Finished int64 `db:"execution_finished" json:"finished"` + Created int64 `db:"execution_created" json:"created"` + Updated int64 `db:"execution_updated" json:"updated"` + Version int64 `db:"execution_version" json:"version"` + // TODO: (Vistaar) Add stages + // Stages []*Stage `db:"-" json:"stages,omitempty"` +} diff --git a/types/list_filters.go b/types/list_filters.go new file mode 100644 index 000000000..e7493bbe8 --- /dev/null +++ b/types/list_filters.go @@ -0,0 +1,7 @@ +package types + +// ListQueryFilter has pagination related info and a query param +type ListQueryFilter struct { + Pagination + Query string `json:"query"` +} diff --git a/types/pagination.go b/types/pagination.go new file mode 100644 index 000000000..a00c13333 --- /dev/null +++ b/types/pagination.go @@ -0,0 +1,7 @@ +package types + +// Pagination stores pagination related params +type Pagination struct { + Page int `json:"page"` + Size int `json:"size"` +} diff --git a/types/pipeline.go b/types/pipeline.go new file mode 100644 index 000000000..019bf99a1 --- /dev/null +++ b/types/pipeline.go @@ -0,0 +1,23 @@ +// Copyright 2022 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package types + +import "github.com/harness/gitness/types/enum" + +type Pipeline struct { + ID int64 `db:"pipeline_id" json:"id"` + Description string `db:"pipeline_description" json:"description"` + SpaceID int64 `db:"pipeline_space_id" json:"space_id"` + UID string `db:"pipeline_uid" json:"uid"` + Seq int64 `db:"pipeline_seq" json:"seq"` // last execution number for this pipeline + RepoID int64 `db:"pipeline_repo_id" json:"repo_id"` // null if repo_type != gitness + RepoType enum.ScmType `db:"pipeline_repo_type" json:"repo_type"` + RepoName string `db:"pipeline_repo_name" json:"repo_name"` + DefaultBranch string `db:"pipeline_default_branch" json:"default_branch"` + ConfigPath string `db:"pipeline_config_path" json:"config_path"` + Created int64 `db:"pipeline_created" json:"created"` + Updated int64 `db:"pipeline_updated" json:"updated"` + Version int64 `db:"pipeline_version" json:"version"` +} diff --git a/types/secret.go b/types/secret.go new file mode 100644 index 000000000..547a5fc69 --- /dev/null +++ b/types/secret.go @@ -0,0 +1,29 @@ +// Copyright 2023 Harness Inc. All rights reserved. +// Use of this source code is governed by the Polyform Free Trial License +// that can be found in the LICENSE.md file for this repository. + +package types + +type Secret struct { + ID int64 `db:"secret_id" json:"id"` + Description string `db:"secret_description" json:"description"` + SpaceID int64 `db:"secret_space_id" json:"space_id"` + UID string `db:"secret_uid" json:"uid"` + Data string `db:"secret_data" json:"-"` + Created int64 `db:"secret_created" json:"created"` + Updated int64 `db:"secret_updated" json:"updated"` + Version int64 `db:"secret_version" json:"version"` +} + +// Copy makes a copy of the secret without the value. +func (s *Secret) CopyWithoutData() *Secret { + return &Secret{ + ID: s.ID, + Description: s.Description, + UID: s.UID, + SpaceID: s.SpaceID, + Created: s.Created, + Updated: s.Updated, + Version: s.Version, + } +}