// 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 orchestrator import ( "context" "fmt" "net/url" "path/filepath" "strconv" "time" events "github.com/harness/gitness/app/events/gitspace" "github.com/harness/gitness/app/gitspace/infrastructure" "github.com/harness/gitness/app/gitspace/orchestrator/container" "github.com/harness/gitness/app/gitspace/orchestrator/ide" "github.com/harness/gitness/app/gitspace/scm" "github.com/harness/gitness/app/store" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" "github.com/rs/zerolog/log" ) type Config struct { DefaultBaseImage string } type orchestrator struct { scm scm.SCM infraProviderResourceStore store.InfraProviderResourceStore infraProvisioner infrastructure.InfraProvisioner containerOrchestrator container.Orchestrator eventReporter *events.Reporter config *Config vsCodeService *ide.VSCode vsCodeWebService *ide.VSCodeWeb } var _ Orchestrator = (*orchestrator)(nil) func NewOrchestrator( scm scm.SCM, infraProviderResourceStore store.InfraProviderResourceStore, infraProvisioner infrastructure.InfraProvisioner, containerOrchestrator container.Orchestrator, eventReporter *events.Reporter, config *Config, vsCodeService *ide.VSCode, vsCodeWebService *ide.VSCodeWeb, ) Orchestrator { return orchestrator{ scm: scm, infraProviderResourceStore: infraProviderResourceStore, infraProvisioner: infraProvisioner, containerOrchestrator: containerOrchestrator, eventReporter: eventReporter, config: config, vsCodeService: vsCodeService, vsCodeWebService: vsCodeWebService, } } func (o orchestrator) TriggerStartGitspace( ctx context.Context, gitspaceConfig types.GitspaceConfig, ) (enum.GitspaceInstanceStateType, error) { instanceState := enum.GitspaceInstanceStateError infraProviderResource, err := o.infraProviderResourceStore.Find(ctx, gitspaceConfig.InfraProviderResourceID) if err != nil { return instanceState, fmt.Errorf("cannot get the infraprovider resource for ID %d: %w", gitspaceConfig.InfraProviderResourceID, err) } ideSvc, err := o.getIDEService(gitspaceConfig) if err != nil { return instanceState, err } o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeInfraProvisioningStart) idePort := ideSvc.Port() err = o.infraProvisioner.TriggerProvision(ctx, infraProviderResource, gitspaceConfig, []int{idePort}) if err != nil { o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeInfraProvisioningFailed) return instanceState, fmt.Errorf( "cannot trigger provision infrastructure for ID %d: %w", gitspaceConfig.InfraProviderResourceID, err) } instanceState = enum.GitspaceInstanceStateStarting return instanceState, nil } func (o orchestrator) TriggerStopGitspace( ctx context.Context, gitspaceConfig types.GitspaceConfig, ) (enum.GitspaceInstanceStateType, error) { instanceState := enum.GitspaceInstanceStateError infraProviderResource, err := o.infraProviderResourceStore.Find(ctx, gitspaceConfig.InfraProviderResourceID) if err != nil { return instanceState, fmt.Errorf( "cannot get the infraProviderResource with ID %d: %w", gitspaceConfig.InfraProviderResourceID, err) } infra, err := o.infraProvisioner.Find(ctx, infraProviderResource, gitspaceConfig) if err != nil { return instanceState, fmt.Errorf("cannot find the provisioned infra: %w", err) } err = o.stopGitspaceContainer(ctx, gitspaceConfig, infra) if err != nil { return instanceState, err } o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeInfraStopStart) err = o.infraProvisioner.TriggerStop(ctx, infraProviderResource, gitspaceConfig) if err != nil { o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeInfraStopFailed) return instanceState, fmt.Errorf( "cannot trigger stop infrastructure with ID %d: %w", gitspaceConfig.InfraProviderResourceID, err) } instanceState = enum.GitspaceInstanceStateStopping return instanceState, nil } func (o orchestrator) stopGitspaceContainer( ctx context.Context, gitspaceConfig types.GitspaceConfig, infra *types.Infrastructure, ) error { o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentConnectStart) err := o.containerOrchestrator.Status(ctx, infra) if err != nil { o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentConnectFailed) return fmt.Errorf("couldn't call the agent health API: %w", err) } o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentConnectCompleted) o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentGitspaceStopStart) err = o.containerOrchestrator.StopGitspace(ctx, gitspaceConfig, infra) if err != nil { o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentGitspaceStopFailed) return fmt.Errorf("error stopping the Gitspace container: %w", err) } o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentGitspaceStopCompleted) return nil } func (o orchestrator) stopAndRemoveGitspaceContainer( ctx context.Context, gitspaceConfig types.GitspaceConfig, infra *types.Infrastructure, ) error { o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentConnectStart) err := o.containerOrchestrator.Status(ctx, infra) if err != nil { o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentConnectFailed) return fmt.Errorf("couldn't call the agent health API: %w", err) } o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentConnectCompleted) o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentGitspaceDeletionStart) err = o.containerOrchestrator.StopAndRemoveGitspace(ctx, gitspaceConfig, infra) if err != nil { o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentGitspaceDeletionFailed) return fmt.Errorf("error stopping the Gitspace container: %w", err) } o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentGitspaceDeletionCompleted) return nil } func (o orchestrator) TriggerDeleteGitspace( ctx context.Context, gitspaceConfig types.GitspaceConfig, ) (enum.GitspaceInstanceStateType, error) { instanceState := enum.GitspaceInstanceStateError infraProviderResource, err := o.infraProviderResourceStore.Find(ctx, gitspaceConfig.InfraProviderResourceID) if err != nil { return instanceState, fmt.Errorf( "cannot get the infraProviderResource with ID %d: %w", gitspaceConfig.InfraProviderResourceID, err) } infra, err := o.infraProvisioner.Find(ctx, infraProviderResource, gitspaceConfig) if err != nil { return instanceState, fmt.Errorf("cannot find the provisioned infra: %w", err) } err = o.stopAndRemoveGitspaceContainer(ctx, gitspaceConfig, infra) if err != nil { return instanceState, err } o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeInfraDeprovisioningStart) err = o.infraProvisioner.TriggerDeprovision(ctx, infraProviderResource, gitspaceConfig) if err != nil { o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeInfraDeprovisioningFailed) return instanceState, fmt.Errorf( "cannot trigger deprovision infrastructure with ID %d: %w", gitspaceConfig.InfraProviderResourceID, err) } instanceState = enum.GitspaceInstanceStateStopping return instanceState, nil } func (o orchestrator) emitGitspaceEvent( ctx context.Context, config types.GitspaceConfig, eventType enum.GitspaceEventType, ) { o.eventReporter.EmitGitspaceEvent( ctx, events.GitspaceEvent, &events.GitspaceEventPayload{ QueryKey: config.Identifier, EntityID: config.GitspaceInstance.ID, EntityType: enum.GitspaceEntityTypeGitspaceInstance, EventType: eventType, Timestamp: time.Now().UnixNano(), }) } func (o orchestrator) getIDEService(gitspaceConfig types.GitspaceConfig) (ide.IDE, error) { var ideService ide.IDE switch gitspaceConfig.IDE { case enum.IDETypeVSCode: ideService = o.vsCodeService case enum.IDETypeVSCodeWeb: ideService = o.vsCodeWebService default: return nil, fmt.Errorf("unsupported IDE: %s", gitspaceConfig.IDE) } return ideService, nil } func (o orchestrator) ResumeStartGitspace( ctx context.Context, gitspaceConfig types.GitspaceConfig, provisionedInfra *types.Infrastructure, ) (types.GitspaceInstance, error) { gitspaceInstance := *gitspaceConfig.GitspaceInstance gitspaceInstance.State = enum.GitspaceInstanceStateError infraProviderResource, err := o.infraProviderResourceStore.Find(ctx, gitspaceConfig.InfraProviderResourceID) if err != nil { return gitspaceInstance, fmt.Errorf("cannot get the infraprovider resource for ID %d: %w", gitspaceConfig.InfraProviderResourceID, err) } ideSvc, err := o.getIDEService(gitspaceConfig) if err != nil { return gitspaceInstance, err } idePort := ideSvc.Port() provisionedInfra, err = o.infraProvisioner.ResumeProvision( ctx, infraProviderResource, gitspaceConfig, []int{idePort}, provisionedInfra) if err != nil { o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeInfraProvisioningFailed) return gitspaceInstance, fmt.Errorf( "cannot provision infrastructure for ID %d: %w", gitspaceConfig.InfraProviderResourceID, err) } o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeInfraProvisioningCompleted) o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeFetchDevcontainerStart) scmResolvedDetails, err := o.scm.GetSCMRepoDetails(ctx, gitspaceConfig) if err != nil { o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeFetchDevcontainerFailed) return gitspaceInstance, fmt.Errorf( "failed to fetch code repo details for gitspace config ID %w %d", err, gitspaceConfig.ID) } devcontainerConfig := scmResolvedDetails.DevcontainerConfig repoName := scmResolvedDetails.RepoName if devcontainerConfig == nil { log.Warn().Err(err).Msg("devcontainer config is nil, using empty config") } o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeFetchDevcontainerCompleted) o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentConnectStart) err = o.containerOrchestrator.Status(ctx, provisionedInfra) if err != nil { o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentConnectFailed) return gitspaceInstance, fmt.Errorf("couldn't call the agent health API: %w", err) } o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentConnectCompleted) o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentGitspaceCreationStart) startResponse, err := o.containerOrchestrator.CreateAndStartGitspace( ctx, gitspaceConfig, provisionedInfra, scmResolvedDetails, o.config.DefaultBaseImage, ideSvc) if err != nil { o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentGitspaceCreationFailed) return gitspaceInstance, fmt.Errorf("couldn't call the agent start API: %w", err) } o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeAgentGitspaceCreationCompleted) var ideURL url.URL var forwardedPort string if provisionedInfra.PortMappings[idePort].PublishedPort == 0 { forwardedPort = startResponse.PublishedPorts[idePort] } else { forwardedPort = strconv.Itoa(provisionedInfra.PortMappings[idePort].ForwardedPort) } host := provisionedInfra.Host if provisionedInfra.ProxyHost != "" { host = provisionedInfra.ProxyHost } if gitspaceConfig.IDE == enum.IDETypeVSCodeWeb { ideURL = url.URL{ Scheme: "http", Host: host + ":" + forwardedPort, RawQuery: filepath.Join("folder=", repoName), } } else if gitspaceConfig.IDE == enum.IDETypeVSCode { // TODO: the following userID is hard coded and should be changed. userID := "harness" ideURL = url.URL{ Scheme: "vscode-remote", Host: "", // Empty since we include the host and port in the path Path: fmt.Sprintf( "ssh-remote+%s@%s:%s", userID, host, filepath.Join(forwardedPort, repoName), ), } } ideURLString := ideURL.String() gitspaceInstance.URL = &ideURLString gitspaceInstance.LastUsed = time.Now().UnixMilli() gitspaceInstance.State = enum.GitspaceInstanceStateRunning o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeGitspaceActionStartCompleted) return gitspaceInstance, nil } func (o orchestrator) ResumeStopGitspace( ctx context.Context, gitspaceConfig types.GitspaceConfig, stoppedInfra *types.Infrastructure, ) (enum.GitspaceInstanceStateType, error) { instanceState := enum.GitspaceInstanceStateError infraProviderResource, err := o.infraProviderResourceStore.Find(ctx, gitspaceConfig.InfraProviderResourceID) if err != nil { return instanceState, fmt.Errorf( "cannot get the infraProviderResource with ID %d: %w", gitspaceConfig.InfraProviderResourceID, err) } _, err = o.infraProvisioner.ResumeStop(ctx, infraProviderResource, gitspaceConfig, stoppedInfra) if err != nil { o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeInfraStopFailed) return instanceState, fmt.Errorf( "cannot stop provisioned infrastructure with ID %d: %w", gitspaceConfig.InfraProviderResourceID, err) } o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeInfraStopCompleted) instanceState = enum.GitspaceInstanceStateDeleted o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeGitspaceActionStopCompleted) return instanceState, nil } func (o orchestrator) ResumeDeleteGitspace( ctx context.Context, gitspaceConfig types.GitspaceConfig, deprovisionedInfra *types.Infrastructure, ) (enum.GitspaceInstanceStateType, error) { instanceState := enum.GitspaceInstanceStateError infraProviderResource, err := o.infraProviderResourceStore.Find(ctx, gitspaceConfig.InfraProviderResourceID) if err != nil { return instanceState, fmt.Errorf( "cannot get the infraProviderResource with ID %d: %w", gitspaceConfig.InfraProviderResourceID, err) } _, err = o.infraProvisioner.ResumeDeprovision(ctx, infraProviderResource, gitspaceConfig, deprovisionedInfra) if err != nil { o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeInfraDeprovisioningFailed) return instanceState, fmt.Errorf( "cannot deprovision infrastructure with ID %d: %w", gitspaceConfig.InfraProviderResourceID, err) } o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeInfraDeprovisioningCompleted) instanceState = enum.GitspaceInstanceStateDeleted o.emitGitspaceEvent(ctx, gitspaceConfig, enum.GitspaceEventTypeGitspaceActionStopCompleted) return instanceState, nil }