diff --git a/app/api/auth/infraprovider.go b/app/api/auth/infraprovider.go new file mode 100644 index 000000000..e6f395ce8 --- /dev/null +++ b/app/api/auth/infraprovider.go @@ -0,0 +1,45 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package auth + +import ( + "context" + + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/app/auth/authz" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +// CheckInfraProvider checks if a gitspace 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 CheckInfraProvider( + ctx context.Context, + authorizer authz.Authorizer, + session *auth.Session, + parentPath, + identifier string, + permission enum.Permission, +) error { + scope := &types.Scope{SpacePath: parentPath} + resource := &types.Resource{ + Type: enum.ResourceTypeInfraProvider, + Identifier: identifier, + } + + return Check(ctx, authorizer, session, scope, resource, permission) +} diff --git a/app/api/controller/gitspace/action.go b/app/api/controller/gitspace/action.go index 6ace655f1..e12081b42 100644 --- a/app/api/controller/gitspace/action.go +++ b/app/api/controller/gitspace/action.go @@ -16,23 +16,172 @@ package gitspace import ( "context" - "errors" + "fmt" + "strconv" + "strings" + "time" + apiauth "github.com/harness/gitness/app/api/auth" "github.com/harness/gitness/app/auth" + events "github.com/harness/gitness/app/events/gitspace" "github.com/harness/gitness/types" + "github.com/harness/gitness/types/check" "github.com/harness/gitness/types/enum" + + gonanoid "github.com/matoous/go-nanoid" ) +const defaultAccessKey = "Harness@123" +const defaultMachineUser = "harness" + type ActionInput struct { - Action enum.GitspaceActionType `json:"action"` + Action enum.GitspaceActionType `json:"action"` + Identifier string `json:"-"` + SpaceRef string `json:"-"` // Ref of the parent space } func (c *Controller) Action( - _ context.Context, - _ *auth.Session, - _ string, - _ string, - _ *ActionInput, + ctx context.Context, + session *auth.Session, + in *ActionInput, ) (*types.GitspaceConfig, error) { - return nil, errors.New("unimplemented") + if err := c.sanitizeActionInput(in); err != nil { + return nil, fmt.Errorf("failed to sanitize input: %w", err) + } + space, err := c.spaceStore.FindByRef(ctx, in.SpaceRef) + if err != nil { + return nil, fmt.Errorf("failed to find space: %w", err) + } + err = apiauth.CheckGitspace(ctx, c.authorizer, session, space.Path, in.Identifier, enum.PermissionGitspaceAccess) + if err != nil { + return nil, fmt.Errorf("failed to authorize: %w", err) + } + gitspaceConfig, err := c.gitspaceConfigStore.FindByIdentifier(ctx, space.ID, in.Identifier) + gitspaceConfig.SpacePath = space.Path + gitspaceConfig.SpaceID = space.ID + if err != nil { + return nil, fmt.Errorf("failed to find gitspace config: %w", err) + } + // All the actions should be idempotent. + switch in.Action { + case enum.GitspaceActionTypeStart: + c.emitGitspaceConfigEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeGitspaceActionStart) + gitspace, err := c.startGitspace(ctx, gitspaceConfig) + if err != nil { + c.emitGitspaceConfigEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeGitspaceActionStartFailed) + } + return gitspace, err + case enum.GitspaceActionTypeStop: + c.emitGitspaceConfigEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeGitspaceActionStop) + gitspace, err := c.stopGitspace(ctx, gitspaceConfig) + if err != nil { + c.emitGitspaceConfigEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeGitspaceActionStopFailed) + } + return gitspace, err + default: + return nil, fmt.Errorf("unknown action %s on gitspace : %s", string(in.Action), gitspaceConfig.Identifier) + } +} + +func (c *Controller) startGitspace(ctx context.Context, config *types.GitspaceConfig) (*types.GitspaceConfig, error) { + savedGitspaceInstance, err := c.gitspaceInstanceStore.FindLatestByGitspaceConfigID(ctx, config.ID, config.SpaceID) + const resourceNotFoundErr = "Failed to find gitspace: resource not found" + if err != nil && err.Error() != resourceNotFoundErr { // TODO fix this + return nil, fmt.Errorf("failed to find gitspace with config ID : %s %w", config.Identifier, err) + } + config.GitspaceInstance = savedGitspaceInstance + if savedGitspaceInstance == nil || savedGitspaceInstance.State.IsFinalStatus() { + codeServerPassword := defaultAccessKey + gitspaceMachineUser := defaultMachineUser + now := time.Now().UnixMilli() + suffixUID, err := gonanoid.Generate(allowedUIDAlphabet, 6) + if err != nil { + return nil, fmt.Errorf("could not generate UID for gitspace config : %q %w", config.Identifier, err) + } + identifier := strings.ToLower(config.Identifier + "-" + suffixUID) + var gitspaceInstance = &types.GitspaceInstance{ + GitSpaceConfigID: config.ID, + Identifier: identifier, + State: enum.GitspaceInstanceStateUninitialized, + UserID: config.UserID, + SpaceID: config.SpaceID, + SpacePath: config.SpacePath, + Created: now, + Updated: now, + TotalTimeUsed: 0, + } + if config.IDE == enum.IDETypeVSCodeWeb || config.IDE == enum.IDETypeVSCode { + gitspaceInstance.AccessKey = &codeServerPassword + gitspaceInstance.AccessType = enum.GitspaceAccessTypeUserCredentials + gitspaceInstance.MachineUser = &gitspaceMachineUser + } + if err = c.gitspaceInstanceStore.Create(ctx, gitspaceInstance); err != nil { + return nil, fmt.Errorf("failed to create gitspace instance for %s %w", config.Identifier, err) + } + newGitspaceInstance, err := c.gitspaceInstanceStore.FindLatestByGitspaceConfigID(ctx, config.ID, config.SpaceID) + newGitspaceInstance.SpacePath = config.SpacePath + if err != nil { + return nil, fmt.Errorf("failed to find gitspace with config ID : %s %w", config.Identifier, err) + } + config.GitspaceInstance = newGitspaceInstance + } + + updatedGitspace, err := c.orchestrator.StartGitspace(ctx, config) + if err != nil { + return nil, fmt.Errorf("failed to find start gitspace : %s %w", config.Identifier, err) + } + if err = c.gitspaceInstanceStore.Update(ctx, updatedGitspace); err != nil { + return nil, fmt.Errorf("failed to update gitspace %w", err) + } + config.GitspaceInstance = updatedGitspace + config.State, _ = enum.GetGitspaceStateFromInstance(updatedGitspace.State) + return config, nil +} + +func (c *Controller) stopGitspace(ctx context.Context, config *types.GitspaceConfig) (*types.GitspaceConfig, error) { + savedGitspace, err := c.gitspaceInstanceStore.FindLatestByGitspaceConfigID(ctx, config.ID, config.SpaceID) + if err != nil { + return nil, fmt.Errorf("failed to find gitspace with config ID : %s %w", config.Identifier, err) + } + if savedGitspace.State.IsFinalStatus() { + return nil, fmt.Errorf( + "gitspace Instance cannot be stopped with ID %s %w", savedGitspace.Identifier, err) + } + config.GitspaceInstance = savedGitspace + if updatedGitspace, stopErr := c.orchestrator.StopGitspace(ctx, config); stopErr != nil { + return nil, fmt.Errorf( + "failed to stop gitspace instance with ID %s %w", savedGitspace.Identifier, stopErr) + } else if updatedGitspace != nil { + if stopErr = c.gitspaceInstanceStore.Update(ctx, updatedGitspace); stopErr != nil { + return nil, fmt.Errorf( + "unable to update the gitspace with config id %s %w", savedGitspace.Identifier, stopErr) + } + config.GitspaceInstance = updatedGitspace + config.State, _ = enum.GetGitspaceStateFromInstance(updatedGitspace.State) + } + return config, nil +} + +func (c *Controller) sanitizeActionInput(in *ActionInput) error { + if err := check.Identifier(in.Identifier); err != nil { + return err + } + parentRefAsID, err := strconv.ParseInt(in.SpaceRef, 10, 64) + if (err == nil && parentRefAsID <= 0) || (len(strings.TrimSpace(in.SpaceRef)) == 0) { + return ErrGitspaceRequiresParent + } + return nil +} + +func (c *Controller) emitGitspaceConfigEvent( + ctx context.Context, + config *types.GitspaceConfig, + eventType enum.GitspaceEventType) { + c.eventReporter.EmitGitspaceEvent(ctx, events.GitspaceEvent, &events.GitspaceEventPayload{ + QueryKey: config.Identifier, + EntityID: config.ID, + EntityType: enum.GitspaceEntityTypeGitspaceConfig, + EventType: eventType, + Created: time.Now().UnixMilli(), + }) } diff --git a/app/api/controller/gitspace/controller.go b/app/api/controller/gitspace/controller.go index 098178296..4388e4d1d 100644 --- a/app/api/controller/gitspace/controller.go +++ b/app/api/controller/gitspace/controller.go @@ -16,6 +16,8 @@ package gitspace import ( "github.com/harness/gitness/app/auth/authz" + gitspaceevents "github.com/harness/gitness/app/events/gitspace" + "github.com/harness/gitness/app/gitspace/orchestrator" "github.com/harness/gitness/app/store" ) @@ -25,16 +27,19 @@ type Controller struct { gitspaceConfigStore store.GitspaceConfigStore gitspaceInstanceStore store.GitspaceInstanceStore spaceStore store.SpaceStore + eventReporter *gitspaceevents.Reporter + orchestrator orchestrator.Orchestrator gitspaceEventStore store.GitspaceEventStore } -// TODO Stubbed Impl func NewController( authorizer authz.Authorizer, infraProviderResourceStore store.InfraProviderResourceStore, gitspaceConfigStore store.GitspaceConfigStore, gitspaceInstanceStore store.GitspaceInstanceStore, spaceStore store.SpaceStore, + eventReporter *gitspaceevents.Reporter, + orchestrator orchestrator.Orchestrator, gitspaceEventStore store.GitspaceEventStore, ) *Controller { return &Controller{ @@ -43,6 +48,8 @@ func NewController( gitspaceConfigStore: gitspaceConfigStore, gitspaceInstanceStore: gitspaceInstanceStore, spaceStore: spaceStore, + eventReporter: eventReporter, + orchestrator: orchestrator, gitspaceEventStore: gitspaceEventStore, } } diff --git a/app/api/controller/gitspace/create.go b/app/api/controller/gitspace/create.go index cf1840ca7..27924f2ed 100644 --- a/app/api/controller/gitspace/create.go +++ b/app/api/controller/gitspace/create.go @@ -16,32 +16,108 @@ package gitspace import ( "context" - "errors" + "fmt" + "strconv" + "strings" + "time" + apiauth "github.com/harness/gitness/app/api/auth" + "github.com/harness/gitness/app/api/usererror" "github.com/harness/gitness/app/auth" "github.com/harness/gitness/types" + "github.com/harness/gitness/types/check" "github.com/harness/gitness/types/enum" + + gonanoid "github.com/matoous/go-nanoid" +) + +const allowedUIDAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789" + +var ( + // errSecretRequiresParent if the user tries to create a secret without a parent space. + ErrGitspaceRequiresParent = usererror.BadRequest( + "Parent space required - standalone gitspace are not supported.") ) -// TODO Stubbed Impl // CreateInput is the input used for create operations. type CreateInput struct { - Identifier string `json:"identifier"` - Name string `json:"name"` - SpaceRef string `json:"space_ref"` // Ref of the parent space - IDE enum.IDEType `json:"ide"` - InfraProviderResourceID string `json:"resource_identifier"` - CodeRepoURL string `json:"code_repo_url"` - Branch string `json:"branch"` - DevcontainerPath string `json:"devcontainer_path"` - Metadata map[string]string `json:"metadata"` + Identifier string `json:"identifier"` + Name string `json:"name"` + SpaceRef string `json:"space_ref"` // Ref of the parent space + IDE enum.IDEType `json:"ide"` + ResourceIdentifier string `json:"resource_identifier"` + CodeRepoURL string `json:"code_repo_url"` + Branch string `json:"branch"` + DevcontainerPath *string `json:"devcontainer_path"` + Metadata map[string]string `json:"metadata"` } // Create creates a new gitspace. func (c *Controller) Create( - _ context.Context, - _ *auth.Session, - _ *CreateInput, + ctx context.Context, + session *auth.Session, + in *CreateInput, ) (*types.GitspaceConfig, error) { - return nil, errors.New("unimplemented") + parentSpace, err := c.spaceStore.FindByRef(ctx, in.SpaceRef) + if err != nil { + return nil, fmt.Errorf("failed to find parent by ref: %w", err) + } + if err = apiauth.CheckGitspace( + ctx, + c.authorizer, + session, + parentSpace.Path, + "", + enum.PermissionGitspaceEdit); err != nil { + return nil, err + } + suffixUID, err := gonanoid.Generate(allowedUIDAlphabet, 6) + if err != nil { + return nil, fmt.Errorf("could not generate UID for gitspace config : %q %w", in.Identifier, err) + } + identifier := strings.ToLower(in.Identifier + "-" + suffixUID) + if err = c.sanitizeCreateInput(in); err != nil { + return nil, fmt.Errorf("invalid input: %w", err) + } + now := time.Now().UnixMilli() + infraProviderResource, err := c.infraProviderResourceStore.FindByIdentifier( + ctx, + parentSpace.ID, + in.ResourceIdentifier) + if err != nil { + return nil, fmt.Errorf("could not find infra provider resource : %q %w", in.ResourceIdentifier, err) + } + gitspaceConfig := &types.GitspaceConfig{ + Identifier: identifier, + Name: in.Name, + IDE: in.IDE, + InfraProviderResourceID: infraProviderResource.ID, + InfraProviderResourceIdentifier: infraProviderResource.Identifier, + CodeRepoType: enum.CodeRepoTypeUnknown, // TODO fix this + State: enum.GitspaceStateUninitialized, + CodeRepoURL: in.CodeRepoURL, + Branch: in.Branch, + DevcontainerPath: in.DevcontainerPath, + UserID: session.Principal.UID, + SpaceID: parentSpace.ID, + SpacePath: parentSpace.Path, + Created: now, + Updated: now, + } + err = c.gitspaceConfigStore.Create(ctx, gitspaceConfig) + if err != nil { + return nil, fmt.Errorf("failed to create gitspace config for : %q %w", identifier, err) + } + return gitspaceConfig, nil +} + +func (c *Controller) sanitizeCreateInput(in *CreateInput) error { + if err := check.Identifier(in.Identifier); err != nil { + return err + } + parentRefAsID, err := strconv.ParseInt(in.SpaceRef, 10, 64) + if (err == nil && parentRefAsID <= 0) || (len(strings.TrimSpace(in.SpaceRef)) == 0) { + return ErrGitspaceRequiresParent + } + return nil } diff --git a/app/api/controller/gitspace/delete.go b/app/api/controller/gitspace/delete.go index cfc4fcc5f..456e3694b 100644 --- a/app/api/controller/gitspace/delete.go +++ b/app/api/controller/gitspace/delete.go @@ -16,16 +16,68 @@ package gitspace import ( "context" + "fmt" + "strconv" + apiauth "github.com/harness/gitness/app/api/auth" "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" + + "github.com/rs/zerolog/log" ) -// TODO Stubbed Impl +const gitspaceConfigNotFound = "Failed to find gitspace config with identifier " + func (c *Controller) Delete( - _ context.Context, - _ *auth.Session, - _ string, - _ string, + ctx context.Context, + session *auth.Session, + spaceRef string, + identifier string, ) error { + space, err := c.spaceStore.FindByRef(ctx, spaceRef) + if err != nil { + return fmt.Errorf("failed to find space: %w", err) + } + err = apiauth.CheckGitspace(ctx, c.authorizer, session, space.Path, identifier, enum.PermissionGitspaceDelete) + if err != nil { + return fmt.Errorf("failed to authorize: %w", err) + } + + gitspaceConfig, err := c.gitspaceConfigStore.FindByIdentifier(ctx, space.ID, identifier) + if err != nil || gitspaceConfig == nil { + log.Err(err).Msg(gitspaceConfigNotFound + identifier) + return err + } + instance, err := c.gitspaceInstanceStore.FindLatestByGitspaceConfigID(ctx, gitspaceConfig.ID, gitspaceConfig.SpaceID) + gitspaceConfig.GitspaceInstance = instance + if err != nil { + return fmt.Errorf("failed to find gitspace with config : %q %w", gitspaceConfig.Identifier, err) + } + stopErr := c.stopRunningGitspace(ctx, instance, gitspaceConfig) + if stopErr != nil { + return stopErr + } + gitspaceConfig.IsDeleted = true + if err = c.gitspaceConfigStore.Update(ctx, gitspaceConfig); err != nil { + log.Err(err).Msg("Failed to delete gitspace config with ID " + strconv.FormatInt(gitspaceConfig.ID, 10)) + return err + } + return nil +} + +func (c *Controller) stopRunningGitspace( + ctx context.Context, + instance *types.GitspaceInstance, + config *types.GitspaceConfig) error { + if instance != nil && + (instance.State == enum.GitspaceInstanceStateRunning || + instance.State == enum.GitspaceInstanceStateUnknown) { + if instanceUpdated, err := c.orchestrator.DeleteGitspace(ctx, config); err != nil { + return err + } else if err = c.gitspaceInstanceStore.Update(ctx, instanceUpdated); err != nil { + return err + } + } return nil } diff --git a/app/api/controller/gitspace/find.go b/app/api/controller/gitspace/find.go index 25f656ffa..2ced9bbc7 100644 --- a/app/api/controller/gitspace/find.go +++ b/app/api/controller/gitspace/find.go @@ -16,18 +16,53 @@ package gitspace import ( "context" - "errors" + "fmt" + apiauth "github.com/harness/gitness/app/api/auth" "github.com/harness/gitness/app/auth" "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" ) -// TODO Stubbed Impl func (c *Controller) Find( - _ context.Context, - _ *auth.Session, - _ string, - _ string, + ctx context.Context, + session *auth.Session, + spaceRef string, + identifier string, ) (*types.GitspaceConfig, error) { - return nil, errors.New("unimplemented") + space, err := c.spaceStore.FindByRef(ctx, spaceRef) + if err != nil { + return nil, fmt.Errorf("failed to find space: %w", err) + } + err = apiauth.CheckGitspace(ctx, c.authorizer, session, space.Path, identifier, enum.PermissionGitspaceView) + if err != nil { + return nil, fmt.Errorf("failed to authorize: %w", err) + } + gitspaceConfig, err := c.gitspaceConfigStore.FindByIdentifier(ctx, space.ID, identifier) + if err != nil { + return nil, fmt.Errorf("failed to find gitspace config: %w", err) + } + infraProviderResource, err := c.infraProviderResourceStore.Find( + ctx, + gitspaceConfig.InfraProviderResourceID) + if err != nil { + return nil, fmt.Errorf("failed to find infra provider resource for gitspace config: %w", err) + } + gitspaceConfig.SpacePath = space.Path + gitspaceConfig.InfraProviderResourceIdentifier = infraProviderResource.Identifier + instance, err := c.gitspaceInstanceStore.FindLatestByGitspaceConfigID(ctx, gitspaceConfig.ID, gitspaceConfig.SpaceID) + if err != nil { + return nil, err + } + if instance != nil { + gitspaceConfig.GitspaceInstance = instance + gitspaceStateType, err := enum.GetGitspaceStateFromInstance(instance.State) + if err != nil { + return nil, err + } + gitspaceConfig.State = gitspaceStateType + } else { + gitspaceConfig.State = enum.GitspaceStateUninitialized + } + return gitspaceConfig, nil } diff --git a/app/api/controller/gitspace/update.go b/app/api/controller/gitspace/update.go index 7a7eb3002..dbe4a153f 100644 --- a/app/api/controller/gitspace/update.go +++ b/app/api/controller/gitspace/update.go @@ -16,29 +16,62 @@ package gitspace import ( "context" - "errors" + "fmt" + "strconv" + "strings" + apiauth "github.com/harness/gitness/app/api/auth" "github.com/harness/gitness/app/auth" - "github.com/harness/gitness/types" + "github.com/harness/gitness/types/check" "github.com/harness/gitness/types/enum" ) -// TODO Stubbed Impl // UpdateInput is used for updating a gitspace. type UpdateInput struct { - IDE enum.IDEType `json:"ide"` - InfraProviderResourceID string `json:"infra_provider_resource_id"` - Name string `json:"name"` - Identifier string `json:"-"` - SpaceRef string `json:"-"` + IDE enum.IDEType `json:"ide"` + ResourceIdentifier string `json:"resource_identifier"` + Name string `json:"name"` + Identifier string `json:"-"` + SpaceRef string `json:"-"` } func (c *Controller) Update( - _ context.Context, - _ *auth.Session, - _ string, - _ string, - _ *UpdateInput, -) (*types.GitspaceConfig, error) { - return nil, errors.New("unimplemented") + ctx context.Context, + session *auth.Session, + spaceRef string, + identifier string, + in *UpdateInput, +) error { + in.SpaceRef = spaceRef + in.Identifier = identifier + if err := c.sanitizeUpdateInput(in); err != nil { + return fmt.Errorf("failed to sanitize input: %w", err) + } + space, err := c.spaceStore.FindByRef(ctx, spaceRef) + if err != nil { + return fmt.Errorf("failed to find space: %w", err) + } + err = apiauth.CheckGitspace(ctx, c.authorizer, session, space.Path, identifier, enum.PermissionGitspaceEdit) + if err != nil { + return fmt.Errorf("failed to authorize: %w", err) + } + + gitspaceConfig, err := c.gitspaceConfigStore.FindByIdentifier(ctx, space.ID, identifier) + if err != nil { + return fmt.Errorf("failed to find gitspace config: %w", err) + } + // TODO Update with proper locks + return c.gitspaceConfigStore.Update(ctx, gitspaceConfig) +} + +func (c *Controller) sanitizeUpdateInput(in *UpdateInput) error { + parentRefAsID, err := strconv.ParseInt(in.SpaceRef, 10, 64) + + if (err == nil && parentRefAsID <= 0) || (len(strings.TrimSpace(in.SpaceRef)) == 0) { + return ErrGitspaceRequiresParent + } + if err := check.Identifier(in.Identifier); err != nil { + return err + } + return nil } diff --git a/app/api/controller/gitspace/wire.go b/app/api/controller/gitspace/wire.go index 4d2389a6a..68aa08198 100644 --- a/app/api/controller/gitspace/wire.go +++ b/app/api/controller/gitspace/wire.go @@ -16,6 +16,8 @@ package gitspace import ( "github.com/harness/gitness/app/auth/authz" + gitspaceevents "github.com/harness/gitness/app/events/gitspace" + "github.com/harness/gitness/app/gitspace/orchestrator" "github.com/harness/gitness/app/store" "github.com/google/wire" @@ -32,7 +34,17 @@ func ProvideController( configStore store.GitspaceConfigStore, instanceStore store.GitspaceInstanceStore, spaceStore store.SpaceStore, + reporter *gitspaceevents.Reporter, + orchestrator orchestrator.Orchestrator, eventStore store.GitspaceEventStore, ) *Controller { - return NewController(authorizer, resourceStore, configStore, instanceStore, spaceStore, eventStore) + return NewController( + authorizer, + resourceStore, + configStore, + instanceStore, + spaceStore, + reporter, + orchestrator, + eventStore) } diff --git a/app/api/controller/infraprovider/create.go b/app/api/controller/infraprovider/create.go new file mode 100644 index 000000000..c2e49013a --- /dev/null +++ b/app/api/controller/infraprovider/create.go @@ -0,0 +1,148 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package infraprovider + +import ( + "context" + "fmt" + "strings" + "time" + + apiauth "github.com/harness/gitness/app/api/auth" + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/infraprovider" + infraproviderenum "github.com/harness/gitness/infraprovider/enum" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/check" + "github.com/harness/gitness/types/enum" +) + +type CreateInput struct { + Identifier string `json:"identifier"` + SpaceRef string `json:"space_ref"` // Ref of the parent space + Name string `json:"name"` + Type infraproviderenum.InfraProviderType `json:"type"` + Metadata map[string]string `json:"metadata"` + Resources []ResourceInput `json:"resources"` +} + +type ResourceInput struct { + Identifier string `json:"identifier"` + Name string `json:"name"` + InfraProviderType infraproviderenum.InfraProviderType `json:"infra_provider_type"` + CPU *string `json:"cpu"` + Memory *string `json:"memory"` + Disk *string `json:"disk"` + Network *string `json:"network"` + Region []string `json:"region"` + Metadata map[string]string `json:"metadata"` + GatewayHost *string `json:"gateway_host"` + GatewayPort *string `json:"gateway_port"` + TemplateIdentifier *string `json:"template_identifier"` +} + +// Create creates a new infraprovider config. +func (c *Controller) Create( + ctx context.Context, + session *auth.Session, + in *CreateInput, +) (*types.InfraProviderConfig, error) { + if err := c.sanitizeCreateInput(in); err != nil { + return nil, fmt.Errorf("invalid input: %w", err) + } + now := time.Now().UnixMilli() + parentSpace, err := c.spaceStore.FindByRef(ctx, in.SpaceRef) + if err != nil { + return nil, fmt.Errorf("failed to find parent by ref: %w", err) + } + if err = apiauth.CheckInfraProvider( + ctx, + c.authorizer, + session, + parentSpace.Path, + "", + enum.PermissionInfraProviderEdit); err != nil { + return nil, err + } + infraProviderConfig := &types.InfraProviderConfig{ + Identifier: in.Identifier, + Name: in.Name, + SpaceID: parentSpace.ID, + Type: in.Type, + Created: now, + Updated: now, + } + err = c.infraProviderConfigStore.Create(ctx, infraProviderConfig) + if err != nil { + return nil, fmt.Errorf("failed to create infraprovider config for : %q %w", infraProviderConfig.Identifier, err) + } + infraProviderConfiginDB, err := c.infraProviderConfigStore.FindByIdentifier( + ctx, + parentSpace.ID, + infraProviderConfig.Identifier) + if err != nil { + return nil, err + } + infraProvider, err := c.infraProviderFactory.GetInfraProvider(infraProviderConfiginDB.Type) + if err != nil { + return nil, err + } + if len(infraProvider.TemplateParams()) > 0 { + return nil, fmt.Errorf("failed to fetch templates") // TODO Implement + } + parameters := []infraprovider.Parameter{} + // TODO logic to populate paramteters as per the provider type + err = infraProvider.ValidateParams(parameters) + if err != nil { + return nil, err + } + for _, res := range in.Resources { + entity := &types.InfraProviderResource{ + Identifier: res.Identifier, + InfraProviderConfigID: infraProviderConfiginDB.ID, + InfraProviderType: res.InfraProviderType, + Name: res.Name, + SpaceID: parentSpace.ID, + CPU: res.CPU, + Memory: res.Memory, + Disk: res.Disk, + Network: res.Network, + Region: strings.Join(res.Region, " "), // TODO fix + Metadata: res.Metadata, + GatewayHost: res.GatewayHost, + GatewayPort: res.GatewayPort, // No template as of now + Created: now, + Updated: now, + } + err = c.infraProviderResourceStore.Create(ctx, infraProviderConfiginDB.ID, entity) + if err != nil { + return nil, fmt.Errorf("failed to create infraprovider resource for : %q %w", entity.Identifier, err) + } + } + resources, err := c.infraProviderResourceStore.List(ctx, infraProviderConfiginDB.ID, types.ListQueryFilter{}) + infraProviderConfig.Resources = resources + if err != nil { + return nil, fmt.Errorf( + "error creating infra provider resource for config : %q %w", infraProviderConfiginDB.Identifier, err) + } + return infraProviderConfig, nil +} + +func (c *Controller) sanitizeCreateInput(in *CreateInput) error { + if err := check.Identifier(in.Identifier); err != nil { + return err + } + return nil +} diff --git a/app/api/controller/infraprovider/find.go b/app/api/controller/infraprovider/find.go new file mode 100644 index 000000000..ca0043979 --- /dev/null +++ b/app/api/controller/infraprovider/find.go @@ -0,0 +1,51 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package infraprovider + +import ( + "context" + "fmt" + + apiauth "github.com/harness/gitness/app/api/auth" + "github.com/harness/gitness/app/auth" + "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" +) + +func (c *Controller) Find( + ctx context.Context, + session *auth.Session, + spaceRef string, + identifier string, +) (*types.InfraProviderConfig, error) { + space, err := c.spaceStore.FindByRef(ctx, spaceRef) + if err != nil { + return nil, fmt.Errorf("failed to find space: %w", err) + } + err = apiauth.CheckGitspace(ctx, c.authorizer, session, space.Path, identifier, enum.PermissionInfraProviderView) + if err != nil { + return nil, fmt.Errorf("failed to authorize: %w", err) + } + infraProviderConfig, err := c.infraProviderConfigStore.FindByIdentifier(ctx, space.ID, identifier) + if err != nil { + return nil, fmt.Errorf("failed to find infraprovider config: %w", err) + } + resources, err := c.infraProviderResourceStore.List(ctx, infraProviderConfig.ID, types.ListQueryFilter{}) + if err != nil { + return nil, fmt.Errorf("failed to find infraprovider resources: %w", err) + } + infraProviderConfig.Resources = resources + return infraProviderConfig, nil +} diff --git a/app/api/controller/space/controller.go b/app/api/controller/space/controller.go index b76430713..cb228eb3b 100644 --- a/app/api/controller/space/controller.go +++ b/app/api/controller/space/controller.go @@ -61,27 +61,28 @@ func (s SpaceOutput) MarshalJSON() ([]byte, error) { type Controller struct { nestedSpacesEnabled bool - tx dbtx.Transactor - urlProvider url.Provider - sseStreamer sse.Streamer - identifierCheck check.SpaceIdentifier - authorizer authz.Authorizer - spacePathStore store.SpacePathStore - pipelineStore store.PipelineStore - secretStore store.SecretStore - connectorStore store.ConnectorStore - templateStore store.TemplateStore - spaceStore store.SpaceStore - repoStore store.RepoStore - principalStore store.PrincipalStore - repoCtrl *repo.Controller - membershipStore store.MembershipStore - importer *importer.Repository - exporter *exporter.Repository - resourceLimiter limiter.ResourceLimiter - publicAccess publicaccess.Service - auditService audit.Service - gitspaceStore store.GitspaceConfigStore + tx dbtx.Transactor + urlProvider url.Provider + sseStreamer sse.Streamer + identifierCheck check.SpaceIdentifier + authorizer authz.Authorizer + spacePathStore store.SpacePathStore + pipelineStore store.PipelineStore + secretStore store.SecretStore + connectorStore store.ConnectorStore + templateStore store.TemplateStore + spaceStore store.SpaceStore + repoStore store.RepoStore + principalStore store.PrincipalStore + repoCtrl *repo.Controller + membershipStore store.MembershipStore + importer *importer.Repository + exporter *exporter.Repository + resourceLimiter limiter.ResourceLimiter + publicAccess publicaccess.Service + auditService audit.Service + gitspaceConfigStore store.GitspaceConfigStore + gitspaceInstanceStore store.GitspaceInstanceStore } func NewController(config *types.Config, tx dbtx.Transactor, urlProvider url.Provider, @@ -92,29 +93,31 @@ func NewController(config *types.Config, tx dbtx.Transactor, urlProvider url.Pro membershipStore store.MembershipStore, importer *importer.Repository, exporter *exporter.Repository, limiter limiter.ResourceLimiter, publicAccess publicaccess.Service, auditService audit.Service, gitspaceStore store.GitspaceConfigStore, + gitspaceInstanceStore store.GitspaceInstanceStore, ) *Controller { return &Controller{ - nestedSpacesEnabled: config.NestedSpacesEnabled, - tx: tx, - urlProvider: urlProvider, - sseStreamer: sseStreamer, - identifierCheck: identifierCheck, - authorizer: authorizer, - spacePathStore: spacePathStore, - pipelineStore: pipelineStore, - secretStore: secretStore, - connectorStore: connectorStore, - templateStore: templateStore, - spaceStore: spaceStore, - repoStore: repoStore, - principalStore: principalStore, - repoCtrl: repoCtrl, - membershipStore: membershipStore, - importer: importer, - exporter: exporter, - resourceLimiter: limiter, - publicAccess: publicAccess, - auditService: auditService, - gitspaceStore: gitspaceStore, + nestedSpacesEnabled: config.NestedSpacesEnabled, + tx: tx, + urlProvider: urlProvider, + sseStreamer: sseStreamer, + identifierCheck: identifierCheck, + authorizer: authorizer, + spacePathStore: spacePathStore, + pipelineStore: pipelineStore, + secretStore: secretStore, + connectorStore: connectorStore, + templateStore: templateStore, + spaceStore: spaceStore, + repoStore: repoStore, + principalStore: principalStore, + repoCtrl: repoCtrl, + membershipStore: membershipStore, + importer: importer, + exporter: exporter, + resourceLimiter: limiter, + publicAccess: publicAccess, + auditService: auditService, + gitspaceConfigStore: gitspaceStore, + gitspaceInstanceStore: gitspaceInstanceStore, } } diff --git a/app/api/controller/space/list_gitspaces.go b/app/api/controller/space/list_gitspaces.go index 10abfeaa8..2849f58d5 100644 --- a/app/api/controller/space/list_gitspaces.go +++ b/app/api/controller/space/list_gitspaces.go @@ -16,16 +16,76 @@ package space import ( "context" + "fmt" + apiauth "github.com/harness/gitness/app/api/auth" "github.com/harness/gitness/app/auth" "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" ) func (c *Controller) ListGitspaces( - _ context.Context, - _ *auth.Session, - _ string, - _ types.ListQueryFilter, + ctx context.Context, + session *auth.Session, + spaceRef string, + filter types.ListQueryFilter, ) ([]*types.GitspaceConfig, int64, error) { - return nil, 0, nil + space, err := c.spaceStore.FindByRef(ctx, spaceRef) + if err != nil { + return nil, 0, fmt.Errorf("failed to find space: %w", err) + } + err = apiauth.CheckGitspace(ctx, c.authorizer, session, space.Path, "", enum.PermissionGitspaceView) + if err != nil { + return nil, 0, fmt.Errorf("failed to authorize gitspace: %w", err) + } + gitspaceFilter := &types.GitspaceFilter{ + QueryFilter: filter, + UserID: session.Principal.UID, + SpaceIDs: []int64{space.ID}, + } + gitspaceConfigs, err := c.gitspaceConfigStore.List(ctx, gitspaceFilter) + if err != nil { + return nil, 0, fmt.Errorf("failed to list gitspace configs: %w", err) + } + count, err := c.gitspaceConfigStore.Count(ctx, gitspaceFilter) + if err != nil { + return nil, 0, fmt.Errorf("failed to count gitspaces in space: %w", err) + } + var gitspaceConfigIDs = make([]int64, 0) + for idx := 0; idx < len(gitspaceConfigs); idx++ { + if gitspaceConfigs[idx].IsDeleted { + continue + } + gitspaceConfigs[idx].SpacePath = space.Path // As the API is for a space, this will remain same + gitspaceConfigIDs = append(gitspaceConfigIDs, gitspaceConfigs[idx].ID) + } + gitspaceInstancesMap, err := c.getLatestInstanceMap(ctx, gitspaceConfigIDs) + if err != nil { + return nil, 0, err + } + for _, gitspaceConfig := range gitspaceConfigs { + gitspaceConfig.GitspaceInstance = gitspaceInstancesMap[gitspaceConfig.ID] + if gitspaceConfig.GitspaceInstance != nil { + gitspaceConfig.State, _ = enum.GetGitspaceStateFromInstance(gitspaceConfig.GitspaceInstance.State) + gitspaceConfig.GitspaceInstance.SpacePath = gitspaceConfig.SpacePath + } else { + gitspaceConfig.State = enum.GitspaceStateUninitialized + } + } + return gitspaceConfigs, count, nil +} + +func (c *Controller) getLatestInstanceMap( + ctx context.Context, + gitspaceConfigIDs []int64, +) (map[int64]*types.GitspaceInstance, error) { + var gitspaceInstances, err = c.gitspaceInstanceStore.FindAllLatestByGitspaceConfigID(ctx, gitspaceConfigIDs) + if err != nil { + return nil, err + } + var gitspaceInstancesMap = make(map[int64]*types.GitspaceInstance) + for _, gitspaceEntry := range gitspaceInstances { + gitspaceInstancesMap[gitspaceEntry.GitSpaceConfigID] = gitspaceEntry + } + return gitspaceInstancesMap, nil } diff --git a/app/api/controller/space/wire.go b/app/api/controller/space/wire.go index e496d0e6d..f8e71d6ad 100644 --- a/app/api/controller/space/wire.go +++ b/app/api/controller/space/wire.go @@ -44,11 +44,13 @@ func ProvideController(config *types.Config, tx dbtx.Transactor, urlProvider url spaceStore store.SpaceStore, repoStore store.RepoStore, principalStore store.PrincipalStore, repoCtrl *repo.Controller, membershipStore store.MembershipStore, importer *importer.Repository, exporter *exporter.Repository, limiter limiter.ResourceLimiter, publicAccess publicaccess.Service, - auditService audit.Service, gitspaceStore store.GitspaceConfigStore, + auditService audit.Service, gitspaceConfigStore store.GitspaceConfigStore, instanceStore store.GitspaceInstanceStore, ) *Controller { return NewController(config, tx, urlProvider, sseStreamer, identifierCheck, authorizer, spacePathStore, pipelineStore, secretStore, connectorStore, templateStore, spaceStore, repoStore, principalStore, - repoCtrl, membershipStore, importer, exporter, limiter, publicAccess, auditService, gitspaceStore) + repoCtrl, membershipStore, importer, + exporter, limiter, publicAccess, + auditService, gitspaceConfigStore, instanceStore) } diff --git a/app/api/handler/gitspace/action.go b/app/api/handler/gitspace/action.go index 8049d3117..eb31eb0b6 100644 --- a/app/api/handler/gitspace/action.go +++ b/app/api/handler/gitspace/action.go @@ -35,19 +35,19 @@ func HandleAction(gitspaceCtrl *gitspace.Controller) http.HandlerFunc { render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err) return } - gitspaceConfigRef, err := request.GetGitspaceRefFromPath(r) if err != nil { render.TranslatedUserError(ctx, w, err) return } spaceRef, gitspaceConfigIdentifier, err := paths.DisectLeaf(gitspaceConfigRef) + in.SpaceRef = spaceRef + in.Identifier = gitspaceConfigIdentifier if err != nil { render.TranslatedUserError(ctx, w, err) return } - - gitspaceConfig, err := gitspaceCtrl.Action(ctx, session, spaceRef, gitspaceConfigIdentifier, in) + gitspaceConfig, err := gitspaceCtrl.Action(ctx, session, in) if err != nil { render.TranslatedUserError(ctx, w, err) return diff --git a/app/api/handler/gitspace/update.go b/app/api/handler/gitspace/update.go index 26d9ef1fa..eb27037eb 100644 --- a/app/api/handler/gitspace/update.go +++ b/app/api/handler/gitspace/update.go @@ -47,12 +47,12 @@ func HandleUpdateConfig(gitspaceCtrl *gitspace.Controller) http.HandlerFunc { return } - gitspaceConfig, err := gitspaceCtrl.Update(ctx, session, spaceRef, gitspaceConfigIdentifier, in) + err = gitspaceCtrl.Update(ctx, session, spaceRef, gitspaceConfigIdentifier, in) if err != nil { render.TranslatedUserError(ctx, w, err) return } - render.JSON(w, http.StatusOK, gitspaceConfig) + render.JSON(w, http.StatusOK, nil) } } diff --git a/app/api/handler/infraprovider/create.go b/app/api/handler/infraprovider/create.go new file mode 100644 index 000000000..607d9f7b4 --- /dev/null +++ b/app/api/handler/infraprovider/create.go @@ -0,0 +1,47 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package infraprovider + +import ( + "encoding/json" + "net/http" + + "github.com/harness/gitness/app/api/controller/infraprovider" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" +) + +// HandleCreateConfig returns a http.HandlerFunc that creates a new InfraProviderConfig with its resources. +func HandleCreateConfig(infraProviderCtrl *infraprovider.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + + in := new(infraprovider.CreateInput) + err := json.NewDecoder(r.Body).Decode(in) + if err != nil { + render.BadRequestf(ctx, w, "Invalid Request Body: %s.", err) + return + } + + infraProviderConfig, err := infraProviderCtrl.Create(ctx, session, in) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusCreated, infraProviderConfig) + } +} diff --git a/app/api/handler/infraprovider/find.go b/app/api/handler/infraprovider/find.go new file mode 100644 index 000000000..b0028569a --- /dev/null +++ b/app/api/handler/infraprovider/find.go @@ -0,0 +1,49 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package infraprovider + +import ( + "net/http" + + "github.com/harness/gitness/app/api/controller/infraprovider" + "github.com/harness/gitness/app/api/render" + "github.com/harness/gitness/app/api/request" + "github.com/harness/gitness/app/paths" +) + +func HandleFind(infraProviderCtrl *infraprovider.Controller) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + session, _ := request.AuthSessionFrom(ctx) + infraProviderRefFromPath, err := request.GetInfraProviderRefFromPath(r) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + spaceRef, infraProviderIdentifier, err := paths.DisectLeaf(infraProviderRefFromPath) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + infraProviderConfig, err := infraProviderCtrl.Find(ctx, session, spaceRef, infraProviderIdentifier) + if err != nil { + render.TranslatedUserError(ctx, w, err) + return + } + + render.JSON(w, http.StatusOK, infraProviderConfig) + } +} diff --git a/app/api/openapi/infra_provider.go b/app/api/openapi/infra_provider.go new file mode 100644 index 000000000..30272b8cb --- /dev/null +++ b/app/api/openapi/infra_provider.go @@ -0,0 +1,59 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package openapi + +import ( + "net/http" + + "github.com/harness/gitness/app/api/controller/infraprovider" + "github.com/harness/gitness/app/api/usererror" + "github.com/harness/gitness/types" + + "github.com/swaggest/openapi-go/openapi3" +) + +type createInfraProviderConfigRequest struct { + infraprovider.CreateInput +} + +type getInfraProviderRequest struct { + Ref string `path:"infraprovider_identifier"` +} + +func infraProviderOperations(reflector *openapi3.Reflector) { + opFind := openapi3.Operation{} + opFind.WithTags("infraproviders") + opFind.WithSummary("Get infraProviderConfig") + opFind.WithMapOfAnything(map[string]interface{}{"operationId": "getInfraProvider"}) + _ = reflector.SetRequest(&opFind, new(getInfraProviderRequest), http.MethodGet) + _ = reflector.SetJSONResponse(&opFind, new(types.InfraProviderConfig), http.StatusOK) + _ = reflector.SetJSONResponse(&opFind, new(usererror.Error), http.StatusBadRequest) + _ = reflector.SetJSONResponse(&opFind, new(usererror.Error), http.StatusInternalServerError) + _ = reflector.SetJSONResponse(&opFind, new(usererror.Error), http.StatusNotFound) + _ = reflector.Spec.AddOperation( + http.MethodGet, "/infraproviders/{infraprovider_identifier}", opFind) + + opCreate := openapi3.Operation{} + opCreate.WithTags("infraproviders") + opCreate.WithSummary("Create infraProvider config") + opCreate.WithMapOfAnything(map[string]interface{}{"operationId": "createInfraProvider"}) + _ = reflector.SetRequest(&opCreate, new(createInfraProviderConfigRequest), http.MethodPost) + _ = reflector.SetJSONResponse(&opCreate, new(types.InfraProviderConfig), 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, "/infraproviders", opCreate) +} diff --git a/app/api/openapi/openapi.go b/app/api/openapi/openapi.go index 5ad5f02ed..73c0a4b72 100644 --- a/app/api/openapi/openapi.go +++ b/app/api/openapi/openapi.go @@ -71,6 +71,7 @@ func (*OpenAPI) Generate() *openapi3.Spec { checkOperations(&reflector) uploadOperations(&reflector) gitspaceOperations(&reflector) + infraProviderOperations(&reflector) // // define security scheme diff --git a/app/api/request/infra_provider.go b/app/api/request/infra_provider.go new file mode 100644 index 000000000..fcabe52e2 --- /dev/null +++ b/app/api/request/infra_provider.go @@ -0,0 +1,34 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package request + +import ( + "net/http" + "net/url" +) + +const ( + PathParamInfraProviderConfigIdentifier = "infraprovider_identifier" +) + +func GetInfraProviderRefFromPath(r *http.Request) (string, error) { + rawRef, err := PathParamOrError(r, PathParamInfraProviderConfigIdentifier) + if err != nil { + return "", err + } + + // paths are unescaped + return url.PathUnescape(rawRef) +} diff --git a/app/auth/authz/membership.go b/app/auth/authz/membership.go index b2d0378fe..4153f2ca0 100644 --- a/app/auth/authz/membership.go +++ b/app/auth/authz/membership.go @@ -107,6 +107,9 @@ func (a *MembershipAuthorizer) Check( case enum.ResourceTypeGitspace: spacePath = scope.SpacePath + case enum.ResourceTypeInfraProvider: + spacePath = scope.SpacePath + case enum.ResourceTypeUser: // a user is allowed to edit themselves if resource.Identifier == session.Principal.UID && diff --git a/app/gitspace/orchestrator/container/embedded_docker.go b/app/gitspace/orchestrator/container/embedded_docker.go index f2c79464d..64d2d7a71 100644 --- a/app/gitspace/orchestrator/container/embedded_docker.go +++ b/app/gitspace/orchestrator/container/embedded_docker.go @@ -266,7 +266,7 @@ func (e *EmbeddedDockerOrchestrator) setupSSHServer( sshServerScript, err := GenerateScriptFromTemplate( templateSetupSSHServer, &SetupSSHServerPayload{ Username: "harness", - Password: gitspaceInstance.AccessKey.String, + Password: *gitspaceInstance.AccessKey, WorkingDirectory: devcontainer.WorkingDir, }) if err != nil { diff --git a/app/gitspace/orchestrator/container/vscodeweb.go b/app/gitspace/orchestrator/container/vscodeweb.go index f5fc5c749..ca0341b10 100644 --- a/app/gitspace/orchestrator/container/vscodeweb.go +++ b/app/gitspace/orchestrator/container/vscodeweb.go @@ -47,7 +47,7 @@ func (v *VSCodeWeb) Setup( ) error { installScript, err := GenerateScriptFromTemplate( templateInstallVSCodeWeb, &InstallVSCodeWebPayload{ - Password: gitspaceInstance.AccessKey.String, + Password: *gitspaceInstance.AccessKey, Port: v.config.Port, }) if err != nil { diff --git a/app/gitspace/orchestrator/orchestrator_impl.go b/app/gitspace/orchestrator/orchestrator_impl.go index 33c3645b8..5dcf6dad3 100644 --- a/app/gitspace/orchestrator/orchestrator_impl.go +++ b/app/gitspace/orchestrator/orchestrator_impl.go @@ -27,7 +27,6 @@ import ( "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" - "github.com/guregu/null" "github.com/rs/zerolog/log" ) @@ -116,7 +115,8 @@ func (o orchestrator) StartGitspace( Path: fmt.Sprintf("ssh-remote+%s@%s:%s/gitspace/%s", "harness", infra.Host, port, repoName), } } - gitspaceInstance.URL = null.NewString(ideURL.String(), true) + ideURLString := ideURL.String() + gitspaceInstance.URL = &ideURLString gitspaceInstance.LastUsed = time.Now().UnixMilli() gitspaceInstance.State = enum.GitspaceInstanceStateRunning @@ -152,8 +152,6 @@ func (o orchestrator) StopGitspace( gitspaceInstance := gitspaceConfig.GitspaceInstance gitspaceInstance.State = enum.GitspaceInstanceStateDeleted - gitspaceInstance.URL = null.NewString("", false) - return gitspaceInstance, err } diff --git a/app/pipeline/runner/runner.go b/app/pipeline/runner/runner.go index 9d44386eb..68d46d478 100644 --- a/app/pipeline/runner/runner.go +++ b/app/pipeline/runner/runner.go @@ -71,7 +71,6 @@ func NewExecutionRunner( if err != nil { return nil, err } - exec := runtime.NewExecer(tracer, remote, upload, engine, int64(config.CI.ParallelWorkers)) diff --git a/app/router/api.go b/app/router/api.go index 7ad3b2be1..4e9fb19d7 100644 --- a/app/router/api.go +++ b/app/router/api.go @@ -24,6 +24,7 @@ import ( "github.com/harness/gitness/app/api/controller/execution" controllergithook "github.com/harness/gitness/app/api/controller/githook" "github.com/harness/gitness/app/api/controller/gitspace" + "github.com/harness/gitness/app/api/controller/infraprovider" "github.com/harness/gitness/app/api/controller/keywordsearch" "github.com/harness/gitness/app/api/controller/logs" "github.com/harness/gitness/app/api/controller/migrate" @@ -48,6 +49,7 @@ import ( handlerexecution "github.com/harness/gitness/app/api/handler/execution" handlergithook "github.com/harness/gitness/app/api/handler/githook" handlergitspace "github.com/harness/gitness/app/api/handler/gitspace" + handlerinfraProvider "github.com/harness/gitness/app/api/handler/infraprovider" handlerkeywordsearch "github.com/harness/gitness/app/api/handler/keywordsearch" handlerlogs "github.com/harness/gitness/app/api/handler/logs" handlerpipeline "github.com/harness/gitness/app/api/handler/pipeline" @@ -125,6 +127,7 @@ func NewAPIHandler( sysCtrl *system.Controller, uploadCtrl *upload.Controller, searchCtrl *keywordsearch.Controller, + infraProviderCtrl *infraprovider.Controller, migrateCtrl *migrate.Controller, gitspaceCtrl *gitspace.Controller, ) APIHandler { @@ -159,7 +162,7 @@ func NewAPIHandler( setupRoutesV1WithAuth(r, appCtx, config, repoCtrl, repoSettingsCtrl, executionCtrl, triggerCtrl, logCtrl, pipelineCtrl, connectorCtrl, templateCtrl, pluginCtrl, secretCtrl, spaceCtrl, pullreqCtrl, webhookCtrl, githookCtrl, git, saCtrl, userCtrl, principalCtrl, checkCtrl, uploadCtrl, - searchCtrl, gitspaceCtrl, migrateCtrl) + searchCtrl, gitspaceCtrl, infraProviderCtrl, migrateCtrl) }) }) @@ -206,6 +209,7 @@ func setupRoutesV1WithAuth(r chi.Router, uploadCtrl *upload.Controller, searchCtrl *keywordsearch.Controller, gitspaceCtrl *gitspace.Controller, + infraProviderCtrl *infraprovider.Controller, migrateCtrl *migrate.Controller, ) { setupAccountWithAuth(r, userCtrl, config) @@ -222,6 +226,7 @@ func setupRoutesV1WithAuth(r chi.Router, setupAdmin(r, userCtrl) setupPlugins(r, pluginCtrl) setupKeywordSearch(r, searchCtrl) + setupInfraProviders(r, infraProviderCtrl) setupGitspaces(r, gitspaceCtrl) setupMigrate(r, migrateCtrl) } @@ -707,7 +712,7 @@ func setupGitspaces(r chi.Router, gitspacesCtrl *gitspace.Controller) { r.Post("/", handlergitspace.HandleCreateConfig(gitspacesCtrl)) r.Route(fmt.Sprintf("/{%s}", request.PathParamGitspaceIdentifier), func(r chi.Router) { r.Get("/", handlergitspace.HandleFind(gitspacesCtrl)) - r.Post("/action", handlergitspace.HandleAction(gitspacesCtrl)) + r.Post("/actions", handlergitspace.HandleAction(gitspacesCtrl)) r.Delete("/", handlergitspace.HandleDeleteConfig(gitspacesCtrl)) r.Patch("/", handlergitspace.HandleUpdateConfig(gitspacesCtrl)) r.Get("/events", handlergitspace.HandleGetEvents(gitspacesCtrl)) @@ -715,6 +720,15 @@ func setupGitspaces(r chi.Router, gitspacesCtrl *gitspace.Controller) { }) } +func setupInfraProviders(r chi.Router, infraProviderCtrl *infraprovider.Controller) { + r.Route("/infraproviders", func(r chi.Router) { + r.Post("/", handlerinfraProvider.HandleCreateConfig(infraProviderCtrl)) + r.Route(fmt.Sprintf("/{%s}", request.PathParamInfraProviderConfigIdentifier), func(r chi.Router) { + r.Get("/", handlerinfraProvider.HandleFind(infraProviderCtrl)) + }) + }) +} + func setupAdmin(r chi.Router, userCtrl *user.Controller) { r.Route("/admin", func(r chi.Router) { r.Use(middlewareprincipal.RestrictToAdmin()) diff --git a/app/router/wire.go b/app/router/wire.go index cb6e3c23f..3bf9ee4ef 100644 --- a/app/router/wire.go +++ b/app/router/wire.go @@ -23,6 +23,7 @@ import ( "github.com/harness/gitness/app/api/controller/execution" "github.com/harness/gitness/app/api/controller/githook" "github.com/harness/gitness/app/api/controller/gitspace" + "github.com/harness/gitness/app/api/controller/infraprovider" "github.com/harness/gitness/app/api/controller/keywordsearch" "github.com/harness/gitness/app/api/controller/logs" "github.com/harness/gitness/app/api/controller/migrate" @@ -116,6 +117,7 @@ func ProvideAPIHandler( sysCtrl *system.Controller, blobCtrl *upload.Controller, searchCtrl *keywordsearch.Controller, + infraProviderCtrl *infraprovider.Controller, gitspaceCtrl *gitspace.Controller, migrateCtrl *migrate.Controller, ) APIHandler { @@ -123,7 +125,7 @@ func ProvideAPIHandler( authenticator, repoCtrl, repoSettingsCtrl, executionCtrl, logCtrl, spaceCtrl, pipelineCtrl, secretCtrl, triggerCtrl, connectorCtrl, templateCtrl, pluginCtrl, pullreqCtrl, webhookCtrl, githookCtrl, git, saCtrl, userCtrl, principalCtrl, checkCtrl, sysCtrl, blobCtrl, searchCtrl, - migrateCtrl, gitspaceCtrl) + infraProviderCtrl, migrateCtrl, gitspaceCtrl) } func ProvideWebHandler(config *types.Config, openapi openapi.Service) WebHandler { diff --git a/app/store/database.go b/app/store/database.go index f4a5d259c..9d99aabfc 100644 --- a/app/store/database.go +++ b/app/store/database.go @@ -572,19 +572,13 @@ type ( Create(ctx context.Context, gitspaceConfig *types.GitspaceConfig) error // Update tries to update a gitspace config in the datastore with optimistic locking. - Update(ctx context.Context, gitspaceConfig *types.GitspaceConfig) (*types.GitspaceConfig, error) + Update(ctx context.Context, gitspaceConfig *types.GitspaceConfig) error // List lists the gitspace configs present in a parent space ID in the datastore. List(ctx context.Context, filter *types.GitspaceFilter) ([]*types.GitspaceConfig, error) // Count the number of gitspace configs in a space matching the given filter. Count(ctx context.Context, filter *types.GitspaceFilter) (int64, error) - - // Delete deletes a pipeline ID from the datastore. - Delete(ctx context.Context, id int64) error - - // DeleteByIdentifier deletes the gitspaceConfig with the given identifier for the given space. - DeleteByIdentifier(ctx context.Context, spaceID int64, identifier string) error } GitspaceInstanceStore interface { @@ -602,13 +596,13 @@ type ( Create(ctx context.Context, gitspaceInstance *types.GitspaceInstance) error // Update tries to update a gitspace instance in the datastore with optimistic locking. - Update(ctx context.Context, gitspaceInstance *types.GitspaceInstance) (*types.GitspaceInstance, error) + Update(ctx context.Context, gitspaceInstance *types.GitspaceInstance) error // List lists the gitspace instance present in a parent space ID in the datastore. List(ctx context.Context, filter *types.GitspaceFilter) ([]*types.GitspaceInstance, error) - // Delete deletes a gitspace instance ID from the datastore. - Delete(ctx context.Context, id int64) error + // List lists the latest gitspace instance present for the gitspace configs in the datastore. + FindAllLatestByGitspaceConfigID(ctx context.Context, gitspaceConfigIDs []int64) ([]*types.GitspaceInstance, error) } InfraProviderConfigStore interface { @@ -620,9 +614,6 @@ type ( // Create creates a new infra provider config in the datastore. Create(ctx context.Context, infraProviderConfig *types.InfraProviderConfig) error - - // DeleteByIdentifier deletes the infra provider config with the given identifier for the given space. - DeleteByIdentifier(ctx context.Context, spaceID int64, identifier string) error } InfraProviderResourceStore interface { @@ -641,9 +632,6 @@ type ( filter types.ListQueryFilter, ) ([]*types.InfraProviderResource, error) - // ListAll lists all the infra provider resource in a given space. - ListAll(ctx context.Context, parentID int64, filter types.ListQueryFilter) ([]*types.InfraProviderResource, error) - // DeleteByIdentifier deletes the Infra provider resource with the given identifier for the given space. DeleteByIdentifier(ctx context.Context, spaceID int64, identifier string) error } diff --git a/app/store/database/gitspace_config.go b/app/store/database/gitspace_config.go index dd560c153..0b20c74b9 100644 --- a/app/store/database/gitspace_config.go +++ b/app/store/database/gitspace_config.go @@ -16,16 +16,66 @@ package database import ( "context" + "strings" "github.com/harness/gitness/app/store" + "github.com/harness/gitness/store/database" + "github.com/harness/gitness/store/database/dbtx" "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" + "github.com/Masterminds/squirrel" + "github.com/guregu/null" "github.com/jmoiron/sqlx" + "github.com/pkg/errors" ) +const ( + gitspaceConfigInsertColumns = ` + gconf_uid, + gconf_display_name, + gconf_ide, + gconf_infra_provider_resource_id, + gconf_code_auth_type, + gconf_code_auth_id, + gconf_code_repo_type, + gconf_code_repo_is_private, + gconf_code_repo_url, + gconf_devcontainer_path, + gconf_branch, + gconf_user_uid, + gconf_space_id, + gconf_created, + gconf_updated, + gconf_is_deleted + ` + gitspaceConfigsTable = `gitspace_configs` + ReturningClause = "RETURNING " + gitspaceConfigSelectColumns = "gconf_id," + gitspaceConfigInsertColumns +) + +type gitspaceConfig struct { + ID int64 `db:"gconf_id"` + Identifier string `db:"gconf_uid"` + Name string `db:"gconf_display_name"` + IDE enum.IDEType `db:"gconf_ide"` + InfraProviderResourceID int64 `db:"gconf_infra_provider_resource_id"` + CodeAuthType string `db:"gconf_code_auth_type"` + CodeAuthID string `db:"gconf_code_auth_id"` + CodeRepoType enum.GitspaceCodeRepoType `db:"gconf_code_repo_type"` + CodeRepoIsPrivate bool `db:"gconf_code_repo_is_private"` + CodeRepoURL string `db:"gconf_code_repo_url"` + DevcontainerPath null.String `db:"gconf_devcontainer_path"` + Branch string `db:"gconf_branch"` + UserUID string `db:"gconf_user_uid"` + SpaceID int64 `db:"gconf_space_id"` + Created int64 `db:"gconf_created"` + Updated int64 `db:"gconf_updated"` + IsDeleted bool `db:"gconf_is_deleted"` +} + var _ store.GitspaceConfigStore = (*gitspaceConfigStore)(nil) -// TODO Stubbed Impl // NewGitspaceConfigStore returns a new GitspaceConfigStore. func NewGitspaceConfigStore(db *sqlx.DB) store.GitspaceConfigStore { return &gitspaceConfigStore{ @@ -37,42 +87,204 @@ type gitspaceConfigStore struct { db *sqlx.DB } -func (g gitspaceConfigStore) Find(_ context.Context, _ int64) (*types.GitspaceConfig, error) { - // TODO implement me - panic("implement me") +func (s gitspaceConfigStore) Count(ctx context.Context, filter *types.GitspaceFilter) (int64, error) { + db := dbtx.GetAccessor(ctx, s.db) + countStmt := database.Builder. + Select("COUNT(*)"). + From(gitspaceConfigsTable). + Where(squirrel.Eq{"gconf_is_deleted": false}). + Where(squirrel.Eq{"gconf_user_uid": filter.UserID}). + Where(squirrel.Eq{"gconf_space_id": filter.SpaceIDs}) + sql, args, err := countStmt.ToSql() + if err != nil { + return 0, errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + var count int64 + err = db.QueryRowContext(ctx, sql, args...).Scan(&count) + if err != nil { + return 0, database.ProcessSQLErrorf(ctx, err, "Failed executing custom count query") + } + return count, nil } -func (g gitspaceConfigStore) FindByIdentifier(_ context.Context, _ int64, _ string) (*types.GitspaceConfig, error) { - // TODO implement me - panic("implement me") +func (s gitspaceConfigStore) Find(ctx context.Context, id int64) (*types.GitspaceConfig, error) { + stmt := database.Builder. + Select(gitspaceConfigSelectColumns). + From(gitspaceConfigsTable). + Where("gconf_id = $1", id). //nolint:goconst + Where("gconf_is_deleted = $2", false) + dst := new(gitspaceConfig) + sql, args, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + db := dbtx.GetAccessor(ctx, s.db) + if err := db.GetContext(ctx, dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find gitspace config") + } + return s.mapToGitspaceConfig(ctx, dst) } -func (g gitspaceConfigStore) Create(_ context.Context, _ *types.GitspaceConfig) error { - // TODO implement me - panic("implement me") +func (s gitspaceConfigStore) FindByIdentifier( + ctx context.Context, + spaceID int64, + identifier string, +) (*types.GitspaceConfig, error) { + stmt := database.Builder. + Select(gitspaceConfigSelectColumns). + From(gitspaceConfigsTable). + Where("LOWER(gconf_uid) = $1", strings.ToLower(identifier)). + Where("gconf_space_id = $2", spaceID). + Where("gconf_is_deleted = $3", false) + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + dst := new(gitspaceConfig) + db := dbtx.GetAccessor(ctx, s.db) + if err := db.GetContext(ctx, dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find gitspace config") + } + return s.mapToGitspaceConfig(ctx, dst) } -func (g gitspaceConfigStore) Update(_ context.Context, _ *types.GitspaceConfig) (*types.GitspaceConfig, error) { - // TODO implement me - panic("implement me") +func (s gitspaceConfigStore) Create(ctx context.Context, gitspaceConfig *types.GitspaceConfig) error { + stmt := database.Builder. + Insert(gitspaceConfigsTable). + Columns(gitspaceConfigInsertColumns). + Values( + gitspaceConfig.Identifier, + gitspaceConfig.Name, + gitspaceConfig.IDE, + gitspaceConfig.InfraProviderResourceID, + gitspaceConfig.CodeAuthType, + gitspaceConfig.CodeAuthID, + gitspaceConfig.CodeRepoType, + gitspaceConfig.CodeRepoIsPrivate, + gitspaceConfig.CodeRepoURL, + gitspaceConfig.DevcontainerPath, + gitspaceConfig.Branch, + gitspaceConfig.UserID, + gitspaceConfig.SpaceID, + gitspaceConfig.Created, + gitspaceConfig.Updated, + gitspaceConfig.IsDeleted, + ). + Suffix(ReturningClause + "gconf_id") + sql, args, err := stmt.ToSql() + if err != nil { + return errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + db := dbtx.GetAccessor(ctx, s.db) + if err = db.QueryRowContext(ctx, sql, args...).Scan(&gitspaceConfig.ID); err != nil { + return database.ProcessSQLErrorf(ctx, err, "gitspace config query failed") + } + return nil } -func (g gitspaceConfigStore) List(_ context.Context, _ *types.GitspaceFilter) ([]*types.GitspaceConfig, error) { - // TODO implement me - panic("implement me") +func (s gitspaceConfigStore) Update(ctx context.Context, + gitspaceConfig *types.GitspaceConfig) error { + dbGitspaceConfig := mapToInternalGitspaceConfig(gitspaceConfig) + stmt := database.Builder. + Update(gitspaceConfigsTable). + Set("gconf_display_name", dbGitspaceConfig.Name). + Set("gconf_ide", dbGitspaceConfig.IDE). + Set("gconf_updated", dbGitspaceConfig.Updated). + Set("gconf_infra_provider_resource_id", dbGitspaceConfig.InfraProviderResourceID). + Set("gconf_is_deleted", dbGitspaceConfig.IsDeleted). + Where("gconf_id = $6", gitspaceConfig.ID) + sql, args, err := stmt.ToSql() + if err != nil { + return errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + db := dbtx.GetAccessor(ctx, s.db) + if _, err := db.ExecContext(ctx, sql, args...); err != nil { + return database.ProcessSQLErrorf(ctx, err, "Failed to update gitspace config") + } + return nil } -func (g gitspaceConfigStore) Count(_ context.Context, _ *types.GitspaceFilter) (int64, error) { - // TODO implement me - panic("implement me") +func mapToInternalGitspaceConfig(config *types.GitspaceConfig) *gitspaceConfig { + return &gitspaceConfig{ + ID: config.ID, + Identifier: config.Identifier, + Name: config.Name, + IDE: config.IDE, + InfraProviderResourceID: config.InfraProviderResourceID, + CodeAuthType: config.CodeAuthType, + CodeAuthID: config.CodeAuthID, + CodeRepoIsPrivate: config.CodeRepoIsPrivate, + CodeRepoType: config.CodeRepoType, + CodeRepoURL: config.CodeRepoURL, + DevcontainerPath: null.StringFromPtr(config.DevcontainerPath), + Branch: config.Branch, + UserUID: config.UserID, + SpaceID: config.SpaceID, + IsDeleted: config.IsDeleted, + Created: config.Created, + Updated: config.Updated, + } } -func (g gitspaceConfigStore) Delete(_ context.Context, _ int64) error { - // TODO implement me - panic("implement me") +func (s gitspaceConfigStore) List(ctx context.Context, filter *types.GitspaceFilter) ([]*types.GitspaceConfig, error) { + stmt := database.Builder. + Select(gitspaceConfigSelectColumns). + From(gitspaceConfigsTable). + Where(squirrel.Eq{"gconf_is_deleted": false}). + Where(squirrel.Eq{"gconf_user_uid": filter.UserID}). + Where(squirrel.Eq{"gconf_space_id": filter.SpaceIDs}) + + queryFilter := filter.QueryFilter + stmt = stmt.Limit(database.Limit(queryFilter.Size)) + stmt = stmt.Offset(database.Offset(queryFilter.Page, queryFilter.Size)) + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + db := dbtx.GetAccessor(ctx, s.db) + var dst []*gitspaceConfig + if err = db.SelectContext(ctx, &dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing custom list query") + } + return s.mapToGitspaceConfigs(ctx, dst) } -func (g gitspaceConfigStore) DeleteByIdentifier(_ context.Context, _ int64, _ string) error { - // TODO implement me - panic("implement me") +func (s *gitspaceConfigStore) mapToGitspaceConfig( + _ context.Context, + in *gitspaceConfig, +) (*types.GitspaceConfig, error) { + var res = &types.GitspaceConfig{ + ID: in.ID, + Identifier: in.Identifier, + Name: in.Name, + InfraProviderResourceID: in.InfraProviderResourceID, + IDE: in.IDE, + CodeRepoType: in.CodeRepoType, + CodeRepoURL: in.CodeRepoURL, + Branch: in.Branch, + DevcontainerPath: in.DevcontainerPath.Ptr(), + UserID: in.UserUID, + SpaceID: in.SpaceID, + CodeAuthType: in.CodeAuthType, + CodeAuthID: in.CodeAuthID, + CodeRepoIsPrivate: in.CodeRepoIsPrivate, + Created: in.Created, + Updated: in.Updated, + } + return res, nil +} + +func (s *gitspaceConfigStore) mapToGitspaceConfigs(ctx context.Context, + configs []*gitspaceConfig) ([]*types.GitspaceConfig, error) { + var err error + res := make([]*types.GitspaceConfig, len(configs)) + for i := range configs { + res[i], err = s.mapToGitspaceConfig(ctx, configs[i]) + if err != nil { + return nil, err + } + } + return res, nil } diff --git a/app/store/database/gitspace_instance.go b/app/store/database/gitspace_instance.go index bf17987be..e070ad6bc 100644 --- a/app/store/database/gitspace_instance.go +++ b/app/store/database/gitspace_instance.go @@ -16,16 +16,63 @@ package database import ( "context" + "fmt" + "strings" "github.com/harness/gitness/app/store" + "github.com/harness/gitness/store/database" + "github.com/harness/gitness/store/database/dbtx" "github.com/harness/gitness/types" + "github.com/harness/gitness/types/enum" + "github.com/Masterminds/squirrel" + "github.com/guregu/null" "github.com/jmoiron/sqlx" + "github.com/pkg/errors" ) var _ store.GitspaceInstanceStore = (*gitspaceInstanceStore)(nil) -// TODO Stubbed Impl +const ( + gitspaceInstanceInsertColumns = ` + gits_gitspace_config_id, + gits_url, + gits_state, + gits_user_uid, + gits_resource_usage, + gits_space_id, + gits_created, + gits_updated, + gits_last_used, + gits_total_time_used, + gits_tracked_changes, + gits_access_key, + gits_access_type, + gits_machine_user, + gits_uid` + gitspaceInstanceSelectColumns = "gits_id," + gitspaceInstanceInsertColumns + gitspaceInstanceTable = `gitspaces` +) + +type gitspaceInstance struct { + ID int64 `db:"gits_id"` + GitSpaceConfigID int64 `db:"gits_gitspace_config_id"` + URL null.String `db:"gits_url"` + State enum.GitspaceInstanceStateType `db:"gits_state"` + UserUID string `db:"gits_user_uid"` + ResourceUsage null.String `db:"gits_resource_usage"` + SpaceID int64 `db:"gits_space_id"` + LastUsed int64 `db:"gits_last_used"` + TotalTimeUsed int64 `db:"gits_total_time_used"` + TrackedChanges null.String `db:"gits_tracked_changes"` + AccessKey null.String `db:"gits_access_key"` + AccessType enum.GitspaceAccessType `db:"gits_access_type"` + MachineUser null.String `db:"gits_machine_user"` + Identifier string `db:"gits_uid"` + Created int64 `db:"gits_created"` + Updated int64 `db:"gits_updated"` +} + // NewGitspaceInstanceStore returns a new GitspaceInstanceStore. func NewGitspaceInstanceStore(db *sqlx.DB) store.GitspaceInstanceStore { return &gitspaceInstanceStore{ @@ -37,35 +84,194 @@ type gitspaceInstanceStore struct { db *sqlx.DB } -func (g *gitspaceInstanceStore) Find(_ context.Context, _ int64) (*types.GitspaceInstance, error) { - // TODO implement me - panic("implement me") +func (g gitspaceInstanceStore) Find(ctx context.Context, id int64) (*types.GitspaceInstance, error) { + stmt := database.Builder. + Select(gitspaceInstanceSelectColumns). + From(gitspaceInstanceTable). + Where("gits_id = $1", id) + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + gitspace := new(gitspaceInstance) + db := dbtx.GetAccessor(ctx, g.db) + if err := db.GetContext(ctx, gitspace, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find gitspace") + } + return g.mapToGitspaceInstance(ctx, gitspace) } -func (g *gitspaceInstanceStore) FindLatestByGitspaceConfigID( +func (g gitspaceInstanceStore) Create(ctx context.Context, gitspaceInstance *types.GitspaceInstance) error { + stmt := database.Builder. + Insert(gitspaceInstanceTable). + Columns(gitspaceInstanceInsertColumns). + Values( + gitspaceInstance.GitSpaceConfigID, + gitspaceInstance.URL, + gitspaceInstance.State, + gitspaceInstance.UserID, + gitspaceInstance.ResourceUsage, + gitspaceInstance.SpaceID, + gitspaceInstance.Created, + gitspaceInstance.Updated, + gitspaceInstance.LastUsed, + gitspaceInstance.TotalTimeUsed, + gitspaceInstance.TrackedChanges, + gitspaceInstance.AccessKey, + gitspaceInstance.AccessType, + gitspaceInstance.MachineUser, + gitspaceInstance.Identifier, + ). + Suffix(ReturningClause + "gits_id") + sql, args, err := stmt.ToSql() + if err != nil { + return errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + db := dbtx.GetAccessor(ctx, g.db) + if err = db.QueryRowContext(ctx, sql, args...).Scan(&gitspaceInstance.ID); err != nil { + return database.ProcessSQLErrorf(ctx, err, "gitspace query failed") + } + return nil +} + +func (g gitspaceInstanceStore) Update( + ctx context.Context, + gitspaceInstance *types.GitspaceInstance, +) error { + stmt := database.Builder. + Update(gitspaceInstanceTable). + Set("gits_state", gitspaceInstance.State). + Set("gits_last_used", gitspaceInstance.LastUsed). + Set("gits_url", gitspaceInstance.URL). + Set("gits_updated", gitspaceInstance.Updated). + Where("gits_id = $5", gitspaceInstance.ID) + sql, args, err := stmt.ToSql() + if err != nil { + return errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + db := dbtx.GetAccessor(ctx, g.db) + if _, err := db.ExecContext(ctx, sql, args...); err != nil { + return database.ProcessSQLErrorf(ctx, err, "Failed to update gitspace") + } + return nil +} + +func (g gitspaceInstanceStore) FindLatestByGitspaceConfigID( + ctx context.Context, + gitspaceConfigID int64, + spaceID int64, +) (*types.GitspaceInstance, error) { + stmt := database.Builder. + Select(gitspaceInstanceSelectColumns). + From(gitspaceInstanceTable). + Where("gits_gitspace_config_id = $1", gitspaceConfigID). + Where("gits_space_id = $2", spaceID). + OrderBy("gits_created DESC") + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + gitspace := new(gitspaceInstance) + db := dbtx.GetAccessor(ctx, g.db) + if err := db.GetContext(ctx, gitspace, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find gitspace") + } + return g.mapToGitspaceInstance(ctx, gitspace) +} + +func (g gitspaceInstanceStore) List( + ctx context.Context, + filter *types.GitspaceFilter, +) ([]*types.GitspaceInstance, error) { + stmt := database.Builder. + Select(gitspaceInstanceSelectColumns). + From(gitspaceInstanceTable). + Where(squirrel.Eq{"gits_space_id": filter.SpaceIDs}). + Where(squirrel.Eq{"gits_user_uid": filter.UserID}). + OrderBy("gits_created ASC") + sql, args, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + db := dbtx.GetAccessor(ctx, g.db) + var dst []*gitspaceInstance + if err := db.SelectContext(ctx, &dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing custom list query") + } + return g.mapToGitspaceInstances(ctx, dst) +} + +func (g gitspaceInstanceStore) FindAllLatestByGitspaceConfigID( + ctx context.Context, + gitspaceConfigIDs []int64, +) ([]*types.GitspaceInstance, error) { + var whereClause = "(1=0)" + if len(gitspaceConfigIDs) > 0 { + whereClause = fmt.Sprintf("gits_gitspace_config_id IN (%s)", + strings.Trim(strings.Join(strings.Split(fmt.Sprint(gitspaceConfigIDs), " "), ","), "[]")) + } + baseSelect := squirrel.Select("*", + "ROW_NUMBER() OVER (PARTITION BY gits_gitspace_config_id "+ + "ORDER BY gits_created DESC) AS rn"). + From(gitspaceInstanceTable). + Where(whereClause) + + // Use the base select query in a common table expression (CTE) + stmt := squirrel.Select(gitspaceConfigSelectColumns). + FromSelect(baseSelect, "RankedRows"). + Where("rn = 1") + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + + db := dbtx.GetAccessor(ctx, g.db) + var dst []*gitspaceInstance + if err := db.SelectContext(ctx, &dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing custom list query") + } + return g.mapToGitspaceInstances(ctx, dst) +} + +func (g gitspaceInstanceStore) mapToGitspaceInstance( _ context.Context, - _ int64, - _ int64) (*types.GitspaceInstance, error) { - // TODO implement me - panic("implement me") + in *gitspaceInstance, +) (*types.GitspaceInstance, error) { + var res = &types.GitspaceInstance{ + ID: in.ID, + Identifier: in.Identifier, + GitSpaceConfigID: in.GitSpaceConfigID, + URL: in.URL.Ptr(), + State: in.State, + UserID: in.UserUID, + ResourceUsage: in.ResourceUsage.Ptr(), + LastUsed: in.LastUsed, + TotalTimeUsed: in.TotalTimeUsed, + TrackedChanges: in.TrackedChanges.Ptr(), + AccessKey: in.AccessKey.Ptr(), + AccessType: in.AccessType, + MachineUser: in.MachineUser.Ptr(), + SpaceID: in.SpaceID, + Created: in.Created, + Updated: in.Updated, + } + return res, nil } -func (g *gitspaceInstanceStore) Create(_ context.Context, _ *types.GitspaceInstance) error { - // TODO implement me - panic("implement me") -} - -func (g *gitspaceInstanceStore) Update(_ context.Context, _ *types.GitspaceInstance) (*types.GitspaceInstance, error) { - // TODO implement me - panic("implement me") -} - -func (g *gitspaceInstanceStore) List(_ context.Context, _ *types.GitspaceFilter) ([]*types.GitspaceInstance, error) { - // TODO implement me - panic("implement me") -} - -func (g *gitspaceInstanceStore) Delete(_ context.Context, _ int64) error { - // TODO implement me - panic("implement me") +func (g gitspaceInstanceStore) mapToGitspaceInstances( + ctx context.Context, + instances []*gitspaceInstance, +) ([]*types.GitspaceInstance, error) { + var err error + res := make([]*types.GitspaceInstance, len(instances)) + for i := range instances { + res[i], err = g.mapToGitspaceInstance(ctx, instances[i]) + if err != nil { + return nil, err + } + } + return res, nil } diff --git a/app/store/database/infra_provider_config.go b/app/store/database/infra_provider_config.go index a2a7b4dd8..e572d65ca 100644 --- a/app/store/database/infra_provider_config.go +++ b/app/store/database/infra_provider_config.go @@ -18,14 +18,41 @@ import ( "context" "github.com/harness/gitness/app/store" + "github.com/harness/gitness/infraprovider/enum" + "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" ) +const ( + infraProviderConfigIDColumn = `ipconf_id` + infraProviderConfigInsertColumns = ` + ipconf_uid, + ipconf_display_name, + ipconf_type, + ipconf_space_id, + ipconf_created, + ipconf_updated + ` + infraProviderConfigSelectColumns = "ipconf_id," + infraProviderConfigInsertColumns + infraProviderConfigTable = `infra_provider_configs` +) + +type infraProviderConfig struct { + ID int64 `db:"ipconf_id"` + Identifier string `db:"ipconf_uid"` + Name string `db:"ipconf_display_name"` + Type enum.InfraProviderType `db:"ipconf_type"` + SpaceID int64 `db:"ipconf_space_id"` + Created int64 `db:"ipconf_created"` + Updated int64 `db:"ipconf_updated"` +} + var _ store.InfraProviderConfigStore = (*infraProviderConfigStore)(nil) -// TODO Stubbed Impl // NewGitspaceConfigStore returns a new GitspaceConfigStore. func NewInfraProviderConfigStore(db *sqlx.DB) store.InfraProviderConfigStore { return &infraProviderConfigStore{ @@ -37,23 +64,80 @@ type infraProviderConfigStore struct { db *sqlx.DB } -func (i infraProviderConfigStore) Find(_ context.Context, _ int64) (*types.InfraProviderConfig, error) { - // TODO implement me - panic("implement me") +func (i infraProviderConfigStore) Find(ctx context.Context, id int64) (*types.InfraProviderConfig, error) { + stmt := database.Builder. + Select(infraProviderConfigSelectColumns). + From(infraProviderConfigTable). + Where(infraProviderConfigIDColumn+" = $1", id) + sql, args, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + dst := new(infraProviderConfig) + db := dbtx.GetAccessor(ctx, i.db) + if err := db.GetContext(ctx, dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find infraProviderConfig") + } + return i.mapToInfraProviderConfig(ctx, dst) } -func (i infraProviderConfigStore) FindByIdentifier(_ context.Context, _ int64, - _ string) (*types.InfraProviderConfig, error) { - // TODO implement me - panic("implement me") +func (i infraProviderConfigStore) FindByIdentifier( + ctx context.Context, + spaceID int64, + identifier string, +) (*types.InfraProviderConfig, error) { + stmt := database.Builder. + Select(infraProviderConfigSelectColumns). + From(infraProviderConfigTable). + Where("ipconf_uid = $1", identifier). + Where("ipconf_space_id = $2", spaceID) + sql, args, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + dst := new(infraProviderConfig) + db := dbtx.GetAccessor(ctx, i.db) + if err := db.GetContext(ctx, dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find infraProviderConfig") + } + return i.mapToInfraProviderConfig(ctx, dst) } -func (i infraProviderConfigStore) Create(_ context.Context, _ *types.InfraProviderConfig) error { - // TODO implement me - panic("implement me") +func (i infraProviderConfigStore) Create(ctx context.Context, infraProviderConfig *types.InfraProviderConfig) error { + stmt := database.Builder. + Insert(infraProviderConfigTable). + Columns(infraProviderConfigInsertColumns). + Values( + infraProviderConfig.Identifier, + infraProviderConfig.Name, + infraProviderConfig.Type, + infraProviderConfig.SpaceID, + infraProviderConfig.Created, + infraProviderConfig.Updated, + ). + Suffix(ReturningClause + infraProviderConfigIDColumn) + sql, args, err := stmt.ToSql() + if err != nil { + return errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + db := dbtx.GetAccessor(ctx, i.db) + if err = db.QueryRowContext(ctx, sql, args...).Scan(&infraProviderConfig.ID); err != nil { + return database.ProcessSQLErrorf(ctx, err, "infra config query failed") + } + return nil } -func (i infraProviderConfigStore) DeleteByIdentifier(_ context.Context, _ int64, _ string) error { - // TODO implement me - panic("implement me") +func (i infraProviderConfigStore) mapToInfraProviderConfig( + _ context.Context, + in *infraProviderConfig) (*types.InfraProviderConfig, error) { + infraProviderConfigEntity := &types.InfraProviderConfig{ + ID: in.ID, + Identifier: in.Identifier, + Name: in.Name, + Type: in.Type, + SpaceID: in.SpaceID, + Created: in.Created, + Updated: in.Updated, + } + return infraProviderConfigEntity, nil } diff --git a/app/store/database/infra_provider_resource.go b/app/store/database/infra_provider_resource.go index 5e1ac3686..383e2f5ed 100644 --- a/app/store/database/infra_provider_resource.go +++ b/app/store/database/infra_provider_resource.go @@ -16,16 +16,65 @@ package database import ( "context" + "encoding/json" "github.com/harness/gitness/app/store" + "github.com/harness/gitness/infraprovider/enum" + "github.com/harness/gitness/store/database" + "github.com/harness/gitness/store/database/dbtx" "github.com/harness/gitness/types" + "github.com/guregu/null" "github.com/jmoiron/sqlx" + "github.com/pkg/errors" ) +const ( + infraProviderResourceIDColumn = `ipreso_id` + infraProviderResourceInsertColumns = ` + ipreso_uid, + ipreso_display_name, + ipreso_infra_provider_config_id, + ipreso_type, + ipreso_space_id, + ipreso_created, + ipreso_updated, + ipreso_cpu, + ipreso_memory, + ipreso_disk, + ipreso_network, + ipreso_region, + ipreso_opentofu_params, + ipreso_gateway_host, + ipreso_gateway_port, + ipreso_infra_provider_template_id + ` + infraProviderResourceSelectColumns = "ipreso_id," + infraProviderResourceInsertColumns + infraProviderResourceTable = `infra_provider_resources` +) + +type infraProviderResource struct { + ID int64 `db:"ipreso_id"` + Identifier string `db:"ipreso_uid"` + Name string `db:"ipreso_display_name"` + InfraProviderConfigID int64 `db:"ipreso_infra_provider_config_id"` + InfraProviderType enum.InfraProviderType `db:"ipreso_type"` + SpaceID int64 `db:"ipreso_space_id"` + CPU null.String `db:"ipreso_cpu"` + Memory null.String `db:"ipreso_memory"` + Disk null.String `db:"ipreso_disk"` + Network null.String `db:"ipreso_network"` + Region string `db:"ipreso_region"` // need list maybe + OpenTofuParams []byte `db:"ipreso_opentofu_params"` + GatewayHost null.String `db:"ipreso_gateway_host"` + GatewayPort null.String `db:"ipreso_gateway_port"` + TemplateID null.Int `db:"ipreso_infra_provider_template_id"` + Created int64 `db:"ipreso_created"` + Updated int64 `db:"ipreso_updated"` +} + var _ store.InfraProviderResourceStore = (*infraProviderResourceStore)(nil) -// TODO Stubbed Impl // NewGitspaceConfigStore returns a new GitspaceConfigStore. func NewInfraProviderResourceStore(db *sqlx.DB) store.InfraProviderResourceStore { return &infraProviderResourceStore{ @@ -37,35 +86,161 @@ type infraProviderResourceStore struct { db *sqlx.DB } -func (i infraProviderResourceStore) Find(_ context.Context, _ int64) (*types.InfraProviderResource, error) { - // TODO implement me - panic("implement me") -} - -func (i infraProviderResourceStore) FindByIdentifier(_ context.Context, _ int64, - _ string) (*types.InfraProviderResource, error) { - // TODO implement me - panic("implement me") -} - -func (i infraProviderResourceStore) Create(_ context.Context, _ int64, _ *types.InfraProviderResource) error { - // TODO implement me - panic("implement me") -} - -func (i infraProviderResourceStore) List(_ context.Context, _ int64, +func (s infraProviderResourceStore) List(ctx context.Context, infraProviderConfigID int64, _ types.ListQueryFilter) ([]*types.InfraProviderResource, error) { - // TODO implement me - panic("implement me") + stmt := database.Builder. + Select(infraProviderResourceSelectColumns). + From(infraProviderResourceTable). + Where("ipreso_infra_provider_config_id = $1", infraProviderConfigID) + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + db := dbtx.GetAccessor(ctx, s.db) + dst := new([]infraProviderResource) + if err := db.SelectContext(ctx, dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed executing custom list query") + } + return s.mapToInfraProviderResources(ctx, *dst) } -func (i infraProviderResourceStore) ListAll(_ context.Context, _ int64, - _ types.ListQueryFilter) ([]*types.InfraProviderResource, error) { - // TODO implement me - panic("implement me") +func (s infraProviderResourceStore) mapToInfraProviderResource(_ context.Context, + in *infraProviderResource) (*types.InfraProviderResource, error) { + openTofuParamsMap := make(map[string]string) + marshalErr := json.Unmarshal(in.OpenTofuParams, &openTofuParamsMap) + if marshalErr != nil { + return nil, marshalErr + } + return &types.InfraProviderResource{ + Identifier: in.Identifier, + InfraProviderConfigID: in.InfraProviderConfigID, + ID: in.ID, + InfraProviderType: in.InfraProviderType, + Name: in.Name, + SpaceID: in.SpaceID, + CPU: in.CPU.Ptr(), + Memory: in.Memory.Ptr(), + Disk: in.Disk.Ptr(), + Network: in.Network.Ptr(), + Region: in.Region, + Metadata: openTofuParamsMap, + GatewayHost: in.GatewayHost.Ptr(), + GatewayPort: in.GatewayPort.Ptr(), + TemplateID: in.TemplateID.Ptr(), + Created: in.Created, + Updated: in.Updated, + }, nil } -func (i infraProviderResourceStore) DeleteByIdentifier(_ context.Context, _ int64, _ string) error { - // TODO implement me - panic("implement me") +func (s infraProviderResourceStore) Find(ctx context.Context, id int64) (*types.InfraProviderResource, error) { + stmt := database.Builder. + Select(infraProviderResourceSelectColumns). + From(infraProviderResourceTable). + Where(infraProviderResourceIDColumn+" = $1", id) + + sql, args, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + dst := new(infraProviderResource) + db := dbtx.GetAccessor(ctx, s.db) + if err := db.GetContext(ctx, dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find infraProviderResource") + } + return s.mapToInfraProviderResource(ctx, dst) +} + +func (s infraProviderResourceStore) FindByIdentifier( + ctx context.Context, + spaceID int64, + identifier string, +) (*types.InfraProviderResource, error) { + stmt := database.Builder. + Select(infraProviderResourceSelectColumns). + From(infraProviderResourceTable). + Where("ipreso_uid = $1", identifier). + Where("ipreso_space_id = $2", spaceID) + sql, args, err := stmt.ToSql() + if err != nil { + return nil, errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + dst := new(infraProviderResource) + db := dbtx.GetAccessor(ctx, s.db) + if err := db.GetContext(ctx, dst, sql, args...); err != nil { + return nil, database.ProcessSQLErrorf(ctx, err, "Failed to find infraProviderResource") + } + return s.mapToInfraProviderResource(ctx, dst) +} + +func (s infraProviderResourceStore) Create( + ctx context.Context, + infraProviderConfigID int64, + infraProviderResource *types.InfraProviderResource, +) error { + // TODO move this to proper place, maybe create mapper + jsonBytes, marshalErr := json.Marshal(infraProviderResource.Metadata) + if marshalErr != nil { + return marshalErr + } + stmt := database.Builder. + Insert(infraProviderResourceTable). + Columns(infraProviderResourceInsertColumns). + Values( + infraProviderResource.Identifier, + infraProviderResource.Name, + infraProviderConfigID, + infraProviderResource.InfraProviderType, + infraProviderResource.SpaceID, + infraProviderResource.Created, + infraProviderResource.Updated, + infraProviderResource.CPU, + infraProviderResource.Memory, + infraProviderResource.Disk, + infraProviderResource.Network, + infraProviderResource.Region, + jsonBytes, + infraProviderResource.GatewayHost, + infraProviderResource.GatewayPort, + infraProviderResource.TemplateID, + ). + Suffix(ReturningClause + infraProviderResourceIDColumn) + sql, args, err := stmt.ToSql() + if err != nil { + return errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + db := dbtx.GetAccessor(ctx, s.db) + if err = db.QueryRowContext(ctx, sql, args...).Scan(&infraProviderResource.ID); err != nil { + return database.ProcessSQLErrorf(ctx, err, "infra provider resource query failed") + } + return nil +} + +func (s infraProviderResourceStore) DeleteByIdentifier(ctx context.Context, spaceID int64, identifier string) error { + stmt := database.Builder. + Delete(infraProviderResourceTable). + Where("ipreso_uid = $1", identifier). + Where("ipreso_space_id = $2", spaceID) + sql, args, err := stmt.ToSql() + if err != nil { + return errors.Wrap(err, "Failed to convert squirrel builder to sql") + } + db := dbtx.GetAccessor(ctx, s.db) + if _, err := db.ExecContext(ctx, sql, args...); err != nil { + return database.ProcessSQLErrorf(ctx, err, "Failed to delete infra provider resource") + } + return nil +} + +func (s infraProviderResourceStore) mapToInfraProviderResources(ctx context.Context, + resources []infraProviderResource) ([]*types.InfraProviderResource, error) { + var err error + res := make([]*types.InfraProviderResource, len(resources)) + for i := range resources { + res[i], err = s.mapToInfraProviderResource(ctx, &resources[i]) + if err != nil { + return nil, err + } + } + return res, nil } diff --git a/cmd/gitness/wire.go b/cmd/gitness/wire.go index 8f88997d0..4e8826711 100644 --- a/cmd/gitness/wire.go +++ b/cmd/gitness/wire.go @@ -14,7 +14,8 @@ import ( "github.com/harness/gitness/app/api/controller/connector" "github.com/harness/gitness/app/api/controller/execution" githookCtrl "github.com/harness/gitness/app/api/controller/githook" - gitspacecontroller "github.com/harness/gitness/app/api/controller/gitspace" + gitspaceCtrl "github.com/harness/gitness/app/api/controller/gitspace" + infraproviderCtrl "github.com/harness/gitness/app/api/controller/infraprovider" controllerkeywordsearch "github.com/harness/gitness/app/api/controller/keywordsearch" "github.com/harness/gitness/app/api/controller/limiter" controllerlogs "github.com/harness/gitness/app/api/controller/logs" @@ -40,8 +41,13 @@ import ( "github.com/harness/gitness/app/auth/authz" "github.com/harness/gitness/app/bootstrap" gitevents "github.com/harness/gitness/app/events/git" + gitspaceevents "github.com/harness/gitness/app/events/gitspace" pullreqevents "github.com/harness/gitness/app/events/pullreq" repoevents "github.com/harness/gitness/app/events/repo" + infrastructure "github.com/harness/gitness/app/gitspace/infrastructure" + "github.com/harness/gitness/app/gitspace/orchestrator" + containerorchestrator "github.com/harness/gitness/app/gitspace/orchestrator/container" + "github.com/harness/gitness/app/gitspace/scm" "github.com/harness/gitness/app/pipeline/canceler" "github.com/harness/gitness/app/pipeline/commit" "github.com/harness/gitness/app/pipeline/converter" @@ -87,6 +93,7 @@ import ( "github.com/harness/gitness/git" "github.com/harness/gitness/git/api" "github.com/harness/gitness/git/storage" + infraproviderpkg "github.com/harness/gitness/infraprovider" "github.com/harness/gitness/job" "github.com/harness/gitness/livelog" "github.com/harness/gitness/lock" @@ -132,6 +139,7 @@ func initSystem(ctx context.Context, config *types.Config) (*cliserver.System, e system.WireSet, authn.WireSet, authz.WireSet, + gitspaceevents.WireSet, gitevents.WireSet, pullreqevents.WireSet, repoevents.WireSet, @@ -200,7 +208,16 @@ func initSystem(ctx context.Context, config *types.Config) (*cliserver.System, e ssh.WireSet, publickey.WireSet, migrate.WireSet, - gitspacecontroller.WireSet, + gitspaceCtrl.WireSet, + infraproviderCtrl.WireSet, + infraproviderpkg.WireSet, + scm.WireSet, + orchestrator.WireSet, + containerorchestrator.WireSet, + infrastructure.WireSet, + cliserver.ProvideIDEVSCodeWebConfig, + cliserver.ProvideDockerConfig, + cliserver.ProvideGitspaceContainerOrchestratorConfig, ) return &cliserver.System{}, nil } diff --git a/cmd/gitness/wire_gen.go b/cmd/gitness/wire_gen.go index 77d10043b..6d8e55088 100644 --- a/cmd/gitness/wire_gen.go +++ b/cmd/gitness/wire_gen.go @@ -14,6 +14,7 @@ import ( "github.com/harness/gitness/app/api/controller/execution" "github.com/harness/gitness/app/api/controller/githook" "github.com/harness/gitness/app/api/controller/gitspace" + infraprovider2 "github.com/harness/gitness/app/api/controller/infraprovider" keywordsearch2 "github.com/harness/gitness/app/api/controller/keywordsearch" "github.com/harness/gitness/app/api/controller/limiter" logs2 "github.com/harness/gitness/app/api/controller/logs" @@ -39,8 +40,13 @@ import ( "github.com/harness/gitness/app/auth/authz" "github.com/harness/gitness/app/bootstrap" events4 "github.com/harness/gitness/app/events/git" + events5 "github.com/harness/gitness/app/events/gitspace" events3 "github.com/harness/gitness/app/events/pullreq" events2 "github.com/harness/gitness/app/events/repo" + "github.com/harness/gitness/app/gitspace/infrastructure" + "github.com/harness/gitness/app/gitspace/orchestrator" + "github.com/harness/gitness/app/gitspace/orchestrator/container" + "github.com/harness/gitness/app/gitspace/scm" "github.com/harness/gitness/app/pipeline/canceler" "github.com/harness/gitness/app/pipeline/commit" "github.com/harness/gitness/app/pipeline/converter" @@ -86,6 +92,7 @@ import ( "github.com/harness/gitness/git" "github.com/harness/gitness/git/api" "github.com/harness/gitness/git/storage" + "github.com/harness/gitness/infraprovider" "github.com/harness/gitness/job" "github.com/harness/gitness/livelog" "github.com/harness/gitness/lock" @@ -234,7 +241,8 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro return nil, err } gitspaceConfigStore := database.ProvideGitspaceConfigStore(db) - spaceController := space.ProvideController(config, transactor, provider, streamer, spaceIdentifier, authorizer, spacePathStore, pipelineStore, secretStore, connectorStore, templateStore, spaceStore, repoStore, principalStore, repoController, membershipStore, repository, exporterRepository, resourceLimiter, publicaccessService, auditService, gitspaceConfigStore) + gitspaceInstanceStore := database.ProvideGitspaceInstanceStore(db) + spaceController := space.ProvideController(config, transactor, provider, streamer, spaceIdentifier, authorizer, spacePathStore, pipelineStore, secretStore, connectorStore, templateStore, spaceStore, repoStore, principalStore, repoController, membershipStore, repository, exporterRepository, resourceLimiter, publicaccessService, auditService, gitspaceConfigStore, gitspaceInstanceStore) pipelineController := pipeline.ProvideController(repoStore, triggerStore, authorizer, pipelineStore) secretController := secret.ProvideController(encrypter, secretStore, authorizer, spaceStore) triggerController := trigger.ProvideController(authorizer, triggerStore, pipelineStore, repoStore) @@ -310,11 +318,31 @@ func initSystem(ctx context.Context, config *types.Config) (*server.System, erro searcher := keywordsearch.ProvideSearcher(localIndexSearcher) keywordsearchController := keywordsearch2.ProvideController(authorizer, searcher, repoController, spaceController) infraProviderResourceStore := database.ProvideInfraProviderResourceStore(db) - gitspaceInstanceStore := database.ProvideGitspaceInstanceStore(db) + infraProviderConfigStore := database.ProvideInfraProviderConfigStore(db) + dockerConfig := server.ProvideDockerConfig(config) + dockerClientFactory := infraprovider.ProvideDockerClientFactory(dockerConfig) + dockerProvider := infraprovider.ProvideDockerProvider(dockerClientFactory) + factory := infraprovider.ProvideFactory(dockerProvider) + infraproviderController := infraprovider2.ProvideController(authorizer, infraProviderResourceStore, infraProviderConfigStore, spaceStore, factory) + reporter3, err := events5.ProvideReporter(eventsSystem) + if err != nil { + return nil, err + } + scmSCM := scm.ProvideSCM() + infraProvisioner := infrastructure.ProvideInfraProvisionerService(infraProviderConfigStore, infraProviderResourceStore, factory) + vsCode := container.ProvideVSCodeService() + vsCodeWebConfig := server.ProvideIDEVSCodeWebConfig(config) + vsCodeWeb := container.ProvideVSCodeWebService(vsCodeWebConfig) + containerConfig, err := server.ProvideGitspaceContainerOrchestratorConfig(config) + if err != nil { + return nil, err + } + containerOrchestrator := container.ProvideEmbeddedDockerOrchestrator(dockerClientFactory, vsCode, vsCodeWeb, containerConfig) + orchestratorOrchestrator := orchestrator.ProvideOrchestrator(scmSCM, infraProviderResourceStore, infraProvisioner, containerOrchestrator) gitspaceEventStore := database.ProvideGitspaceEventStore(db) - gitspaceController := gitspace.ProvideController(authorizer, infraProviderResourceStore, gitspaceConfigStore, gitspaceInstanceStore, spaceStore, gitspaceEventStore) + gitspaceController := gitspace.ProvideController(authorizer, infraProviderResourceStore, gitspaceConfigStore, gitspaceInstanceStore, spaceStore, reporter3, orchestratorOrchestrator, gitspaceEventStore) migrateController := migrate.ProvideController(authorizer, principalStore) - apiHandler := router.ProvideAPIHandler(ctx, config, authenticator, repoController, reposettingsController, executionController, logsController, spaceController, pipelineController, secretController, triggerController, connectorController, templateController, pluginController, pullreqController, webhookController, githookController, gitInterface, serviceaccountController, controller, principalController, checkController, systemController, uploadController, keywordsearchController, gitspaceController, migrateController) + apiHandler := router.ProvideAPIHandler(ctx, config, authenticator, repoController, reposettingsController, executionController, logsController, spaceController, pipelineController, secretController, triggerController, connectorController, templateController, pluginController, pullreqController, webhookController, githookController, gitInterface, serviceaccountController, controller, principalController, checkController, systemController, uploadController, keywordsearchController, infraproviderController, gitspaceController, migrateController) gitHandler := router.ProvideGitHandler(provider, authenticator, repoController) openapiService := openapi.ProvideOpenAPIService() webHandler := router.ProvideWebHandler(config, openapiService) diff --git a/types/enum/gitspace_state_type.go b/types/enum/gitspace_state_type.go index 61b0064e2..95c2655e6 100644 --- a/types/enum/gitspace_state_type.go +++ b/types/enum/gitspace_state_type.go @@ -14,6 +14,8 @@ package enum +import "fmt" + type GitspaceStateType string func (GitspaceStateType) Enum() []interface{} { @@ -30,3 +32,16 @@ const ( GitspaceStateError GitspaceStateType = "error" GitspaceStateUninitialized GitspaceStateType = "uninitialized" ) + +func GetGitspaceStateFromInstance( + instanceState GitspaceInstanceStateType) (GitspaceStateType, error) { + switch instanceState { //nolint:exhaustive + case GitspaceInstanceStateRunning: + return GitspaceStateRunning, nil + case GitspaceInstanceStateUninitialized, + GitspaceInstanceStateDeleted: + return GitspaceStateStopped, nil + default: + return GitspaceStateError, fmt.Errorf("unsupported gitspace instance state %s", string(instanceState)) + } +} diff --git a/types/enum/membership_role.go b/types/enum/membership_role.go index 4587eb362..671548288 100644 --- a/types/enum/membership_role.go +++ b/types/enum/membership_role.go @@ -39,6 +39,7 @@ var membershipRoleReaderPermissions = slices.Clip(slices.Insert([]Permission{}, PermissionConnectorView, PermissionTemplateView, PermissionGitspaceView, + PermissionInfraProviderView, )) var membershipRoleExecutorPermissions = slices.Clip(slices.Insert(membershipRoleReaderPermissions, 0, @@ -48,6 +49,7 @@ var membershipRoleExecutorPermissions = slices.Clip(slices.Insert(membershipRole PermissionConnectorAccess, PermissionTemplateAccess, PermissionGitspaceAccess, + PermissionInfraProviderAccess, )) var membershipRoleContributorPermissions = slices.Clip(slices.Insert(membershipRoleReaderPermissions, 0, @@ -87,6 +89,10 @@ var membershipRoleSpaceOwnerPermissions = slices.Clip(slices.Insert(membershipRo PermissionGitspaceEdit, PermissionGitspaceDelete, PermissionGitspaceAccess, + + PermissionInfraProviderEdit, + PermissionInfraProviderDelete, + PermissionInfraProviderAccess, )) func init() { diff --git a/types/enum/permission.go b/types/enum/permission.go index 80909d342..8df29077b 100644 --- a/types/enum/permission.go +++ b/types/enum/permission.go @@ -28,6 +28,7 @@ const ( ResourceTypeConnector ResourceType = "CONNECTOR" ResourceTypeTemplate ResourceType = "TEMPLATE" ResourceTypeGitspace ResourceType = "GITSPACE" + ResourceTypeInfraProvider ResourceType = "INFRAPROVIDER" ) // Permission represents the different types of permissions a principal can have. @@ -132,3 +133,13 @@ const ( PermissionGitspaceDelete Permission = "gitspace_delete" PermissionGitspaceAccess Permission = "gitspace_access" ) + +const ( + /* + ----- INFRAPROVIDER ----- + */ + PermissionInfraProviderView Permission = "infraprovider_view" + PermissionInfraProviderEdit Permission = "infraprovider_edit" + PermissionInfraProviderDelete Permission = "infraprovider_delete" + PermissionInfraProviderAccess Permission = "infraprovider_access" +) diff --git a/types/gitspace.go b/types/gitspace.go index af786dabf..055127eb8 100644 --- a/types/gitspace.go +++ b/types/gitspace.go @@ -16,8 +16,6 @@ package types import ( "github.com/harness/gitness/types/enum" - - "github.com/guregu/null" ) type GitspaceConfig struct { @@ -31,14 +29,14 @@ type GitspaceConfig struct { CodeRepoURL string `json:"code_repo_url"` CodeRepoType enum.GitspaceCodeRepoType `json:"code_repo_type"` Branch string `json:"branch"` - DevcontainerPath string `json:"devcontainer_path,omitempty"` + DevcontainerPath *string `json:"devcontainer_path,omitempty"` UserID string `json:"user_id"` SpaceID int64 `json:"-"` CodeAuthType string `json:"-"` CodeAuthID string `json:"-"` IsDeleted bool `json:"-"` CodeRepoIsPrivate bool `json:"-"` - GitspaceInstance *GitspaceInstance `json:"instance,omitempty"` + GitspaceInstance *GitspaceInstance `json:"instance"` SpacePath string `json:"space_path"` Created int64 `json:"created"` Updated int64 `json:"updated"` @@ -48,16 +46,16 @@ type GitspaceInstance struct { ID int64 `json:"-"` GitSpaceConfigID int64 `json:"-"` Identifier string `json:"identifier"` - URL null.String `json:"url,omitempty"` + URL *string `json:"url,omitempty"` State enum.GitspaceInstanceStateType `json:"state"` UserID string `json:"-"` - ResourceUsage null.String `json:"resource_usage"` + ResourceUsage *string `json:"resource_usage"` LastUsed int64 `json:"last_used,omitempty"` TotalTimeUsed int64 `json:"total_time_used"` - TrackedChanges string `json:"tracked_changes"` - AccessKey null.String `json:"access_key,omitempty"` + TrackedChanges *string `json:"tracked_changes"` + AccessKey *string `json:"access_key,omitempty"` AccessType enum.GitspaceAccessType `json:"access_type"` - MachineUser null.String `json:"machine_user,omitempty"` + MachineUser *string `json:"machine_user,omitempty"` SpacePath string `json:"space_path"` SpaceID int64 `json:"-"` Created int64 `json:"created"` diff --git a/types/infra_provider.go b/types/infra_provider.go index b28f751ec..3aa195fef 100644 --- a/types/infra_provider.go +++ b/types/infra_provider.go @@ -16,8 +16,6 @@ package types import ( "github.com/harness/gitness/infraprovider/enum" - - "github.com/guregu/null" ) type InfraProviderConfig struct { @@ -39,16 +37,16 @@ type InfraProviderResource struct { Name string `json:"name"` InfraProviderConfigID int64 `json:"-"` InfraProviderConfigIdentifier string `json:"config_identifier"` - CPU null.String `json:"cpu"` - Memory null.String `json:"memory"` - Disk null.String `json:"disk"` - Network null.String `json:"network"` + CPU *string `json:"cpu"` + Memory *string `json:"memory"` + Disk *string `json:"disk"` + Network *string `json:"network"` Region string `json:"region"` Metadata map[string]string `json:"metadata"` - GatewayHost null.String `json:"gateway_host"` - GatewayPort null.String `json:"gateway_port"` - TemplateID null.Int `json:"-"` - TemplateIdentifier null.String `json:"template_identifier"` + GatewayHost *string `json:"gateway_host"` + GatewayPort *string `json:"gateway_port"` + TemplateID *int64 `json:"-"` + TemplateIdentifier *string `json:"template_identifier"` SpaceID int64 `json:"-"` SpacePath string `json:"space_path"` InfraProviderType enum.InfraProviderType `json:"infra_provider_type"`