// 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 infrastructure import ( "context" "encoding/json" "fmt" "time" "github.com/harness/gitness/app/store" "github.com/harness/gitness/infraprovider" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" "github.com/rs/zerolog/log" ) type Config struct { AgentPort int } type InfraEventOpts struct { RequiredGitspacePorts []types.GitspacePort CanDeleteUserData bool } type InfraProvisioner struct { infraProviderConfigStore store.InfraProviderConfigStore infraProviderResourceStore store.InfraProviderResourceStore providerFactory infraprovider.Factory infraProviderTemplateStore store.InfraProviderTemplateStore infraProvisionedStore store.InfraProvisionedStore config *Config } func NewInfraProvisionerService( infraProviderConfigStore store.InfraProviderConfigStore, infraProviderResourceStore store.InfraProviderResourceStore, providerFactory infraprovider.Factory, infraProviderTemplateStore store.InfraProviderTemplateStore, infraProvisionedStore store.InfraProvisionedStore, config *Config, ) InfraProvisioner { return InfraProvisioner{ infraProviderConfigStore: infraProviderConfigStore, infraProviderResourceStore: infraProviderResourceStore, providerFactory: providerFactory, infraProviderTemplateStore: infraProviderTemplateStore, infraProvisionedStore: infraProvisionedStore, config: config, } } // TriggerInfraEvent an interaction with the infrastructure, and once completed emits event in an asynchronous manner. func (i InfraProvisioner) TriggerInfraEvent( ctx context.Context, eventType enum.InfraEvent, gitspaceConfig types.GitspaceConfig, infra *types.Infrastructure, ) error { opts := InfraEventOpts{CanDeleteUserData: false} return i.TriggerInfraEventWithOpts(ctx, eventType, gitspaceConfig, infra, opts) } // TriggerInfraEventWithOpts triggers the provisionining of infra resources using the // infraProviderResource with different infra providers. // Stop event deprovisions those resources which can be stopped without losing the Gitspace data. // CleanupInstance event cleans up resources exclusive for a gitspace instance // Deprovision triggers deprovisioning of resources created for a Gitspace. // canDeleteUserData = true -> deprovision of all resources // canDeleteUserData = false -> deprovision of all resources except storage associated to user data. func (i InfraProvisioner) TriggerInfraEventWithOpts( ctx context.Context, eventType enum.InfraEvent, gitspaceConfig types.GitspaceConfig, infra *types.Infrastructure, opts InfraEventOpts, ) error { infraProviderEntity, err := i.getConfigFromResource(ctx, gitspaceConfig.InfraProviderResource) if err != nil { return err } infraProvider, err := i.getInfraProvider(infraProviderEntity.Type) if err != nil { return err } switch eventType { case enum.InfraEventProvision: if infraProvider.ProvisioningType() == enum.InfraProvisioningTypeNew { return i.provisionNewInfrastructure(ctx, infraProvider, infraProviderEntity.Type, gitspaceConfig, opts.RequiredGitspacePorts) } return i.provisionExistingInfrastructure(ctx, infraProvider, gitspaceConfig, opts.RequiredGitspacePorts) case enum.InfraEventDeprovision: if infraProvider.ProvisioningType() == enum.InfraProvisioningTypeNew { return i.deprovisionNewInfrastructure(ctx, infraProvider, gitspaceConfig, *infra, opts.CanDeleteUserData) } return infraProvider.Deprovision(ctx, *infra, opts.CanDeleteUserData) case enum.InfraEventCleanup: return infraProvider.CleanupInstanceResources(ctx, *infra) case enum.InfraEventStop: return infraProvider.Stop(ctx, *infra) default: return fmt.Errorf("unsupported event type: %s", eventType) } } func (i InfraProvisioner) provisionNewInfrastructure( ctx context.Context, infraProvider infraprovider.InfraProvider, infraProviderType enum.InfraProviderType, gitspaceConfig types.GitspaceConfig, requiredGitspacePorts []types.GitspacePort, ) error { // Logic for new provisioning... infraProvisionedLatest, _ := i.infraProvisionedStore.FindLatestByGitspaceInstanceID( ctx, gitspaceConfig.GitspaceInstance.ID) if infraProvisionedLatest != nil && infraProvisionedLatest.InfraStatus == enum.InfraStatusPending && time.Since(time.UnixMilli(infraProvisionedLatest.Updated)).Milliseconds() < (10*60*1000) { return fmt.Errorf("there is already infra provisioning in pending state %d", infraProvisionedLatest.ID) } else if infraProvisionedLatest != nil { infraProvisionedLatest.InfraStatus = enum.InfraStatusUnknown err := i.infraProvisionedStore.Update(ctx, infraProvisionedLatest) if err != nil { return fmt.Errorf("could not update Infra Provisioned entity: %w", err) } } infraProviderResource := gitspaceConfig.InfraProviderResource allParams, configMetadata, err := i.getAllParamsFromDB(ctx, infraProviderResource, infraProvider) if err != nil { return fmt.Errorf("could not get all params from DB while provisioning: %w", err) } err = infraProvider.ValidateParams(allParams) if err != nil { return fmt.Errorf("invalid provisioning params %v: %w", infraProviderResource.Metadata, err) } now := time.Now() paramsBytes, err := serializeInfraProviderParams(allParams) if err != nil { return err } infrastructure := types.Infrastructure{ Identifier: gitspaceConfig.GitspaceInstance.Identifier, SpaceID: gitspaceConfig.SpaceID, SpacePath: gitspaceConfig.SpacePath, GitspaceConfigIdentifier: gitspaceConfig.Identifier, GitspaceInstanceIdentifier: gitspaceConfig.GitspaceInstance.Identifier, ProviderType: infraProviderType, InputParameters: allParams, ConfigMetadata: configMetadata, } responseMetadata, err := json.Marshal(infrastructure) if err != nil { return fmt.Errorf("unable to marshal infra object %+v: %w", responseMetadata, err) } responseMetaDataJSON := string(responseMetadata) infraProvisioned := &types.InfraProvisioned{ GitspaceInstanceID: gitspaceConfig.GitspaceInstance.ID, InfraProviderType: infraProviderType, InfraProviderResourceID: infraProviderResource.ID, Created: now.UnixMilli(), Updated: now.UnixMilli(), InputParams: paramsBytes, InfraStatus: enum.InfraStatusPending, SpaceID: gitspaceConfig.SpaceID, ResponseMetadata: &responseMetaDataJSON, } err = i.infraProvisionedStore.Create(ctx, infraProvisioned) if err != nil { return fmt.Errorf("unable to create infraProvisioned entry for %d", gitspaceConfig.GitspaceInstance.ID) } agentPort := i.config.AgentPort err = infraProvider.Provision( ctx, gitspaceConfig.SpaceID, gitspaceConfig.SpacePath, gitspaceConfig.Identifier, gitspaceConfig.GitspaceInstance.Identifier, agentPort, requiredGitspacePorts, allParams, ) if err != nil { infraProvisioned.InfraStatus = enum.InfraStatusUnknown infraProvisioned.Updated = time.Now().UnixMilli() updateErr := i.infraProvisionedStore.Update(ctx, infraProvisioned) if updateErr != nil { log.Err(updateErr).Msgf("unable to update infraProvisioned Entry for %d", infraProvisioned.ID) } return fmt.Errorf( "unable to trigger provision infrastructure for gitspaceConfigIdentifier %v: %w", gitspaceConfig.Identifier, err, ) } return nil } func (i InfraProvisioner) provisionExistingInfrastructure( ctx context.Context, infraProvider infraprovider.InfraProvider, gitspaceConfig types.GitspaceConfig, requiredGitspacePorts []types.GitspacePort, ) error { allParams, _, err := i.getAllParamsFromDB(ctx, gitspaceConfig.InfraProviderResource, infraProvider) if err != nil { return fmt.Errorf("could not get all params from DB while provisioning: %w", err) } err = infraProvider.ValidateParams(allParams) if err != nil { return fmt.Errorf("invalid provisioning params %v: %w", gitspaceConfig.InfraProviderResource.Metadata, err) } err = infraProvider.Provision( ctx, gitspaceConfig.SpaceID, gitspaceConfig.SpacePath, gitspaceConfig.Identifier, gitspaceConfig.GitspaceInstance.Identifier, 0, // NOTE: Agent port is not required for provisioning type Existing. requiredGitspacePorts, allParams, ) if err != nil { return fmt.Errorf( "unable to trigger provision infrastructure for gitspaceConfigIdentifier %v: %w", gitspaceConfig.Identifier, err, ) } return nil } func (i InfraProvisioner) deprovisionNewInfrastructure( ctx context.Context, infraProvider infraprovider.InfraProvider, gitspaceConfig types.GitspaceConfig, infra types.Infrastructure, canDeleteUserData bool, ) error { infraProvisionedLatest, err := i.infraProvisionedStore.FindLatestByGitspaceInstanceID( ctx, gitspaceConfig.GitspaceInstance.ID) if err != nil { return fmt.Errorf( "could not find latest infra provisioned entity for instance %d: %w", gitspaceConfig.GitspaceInstance.ID, err) } if infraProvisionedLatest.InfraStatus == enum.InfraStatusDestroyed { return nil } err = infraProvider.Deprovision(ctx, infra, canDeleteUserData) if err != nil { return fmt.Errorf("unable to trigger deprovision infra %+v: %w", infra, err) } return err } func serializeInfraProviderParams(in []types.InfraProviderParameter) (string, error) { output, err := json.Marshal(in) if err != nil { return "", fmt.Errorf("unable to marshal infra provider params: %w", err) } return string(output), nil }