drone/internal/services/exporter/repository.go
2023-09-16 22:24:11 +00:00

227 lines
6.2 KiB
Go

// Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.
package exporter
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"time"
"github.com/harness/gitness/encrypt"
"github.com/harness/gitness/internal/api/controller/repo"
"github.com/harness/gitness/internal/sse"
"github.com/harness/gitness/types/enum"
"github.com/rs/zerolog/log"
"github.com/harness/gitness/gitrpc"
"github.com/harness/gitness/internal/services/job"
"github.com/harness/gitness/internal/store"
gitnessurl "github.com/harness/gitness/internal/url"
"github.com/harness/gitness/types"
)
var (
// ErrNotFound is returned if no export data was found.
ErrNotFound = errors.New("export not found")
)
type Repository struct {
urlProvider *gitnessurl.Provider
git gitrpc.Interface
repoStore store.RepoStore
scheduler *job.Scheduler
encrypter encrypt.Encrypter
sseStreamer sse.Streamer
}
type Input struct {
UID string `json:"uid"`
ID int64 `json:"id"`
Description string `json:"description"`
IsPublic bool `json:"is_public"`
HarnessCodeInfo HarnessCodeInfo `json:"harness_code_info"`
}
type HarnessCodeInfo struct {
AccountId string `json:"account_id"`
ProjectIdentifier string `json:"project_identifier"`
OrgIdentifier string `json:"org_identifier"`
Token string `json:"token"`
}
var _ job.Handler = (*Repository)(nil)
const (
exportJobMaxRetries = 1
exportJobMaxDuration = 45 * time.Minute
exportRepoJobUid = "export_repo_%d"
exportSpaceJobUid = "export_space_%d"
)
const jobType = "repository_export"
func (r *Repository) Register(executor *job.Executor) error {
return executor.Register(jobType, r)
}
func (r *Repository) RunManyForSpace(
ctx context.Context,
spaceId int64,
repos []*types.Repository,
harnessCodeInfo *HarnessCodeInfo,
) error {
jobGroupId := getJobGroupId(spaceId)
jobDefinitions := make([]job.Definition, len(repos))
for i, repository := range repos {
repoJobData := Input{
UID: repository.UID,
ID: repository.ID,
Description: repository.Description,
IsPublic: repository.IsPublic,
HarnessCodeInfo: *harnessCodeInfo,
}
data, err := json.Marshal(repoJobData)
if err != nil {
return fmt.Errorf("failed to marshal job input json: %w", err)
}
strData := strings.TrimSpace(string(data))
encryptedData, err := r.encrypter.Encrypt(strData)
if err != nil {
return fmt.Errorf("failed to encrypt job input: %w", err)
}
jobUID := fmt.Sprintf(exportRepoJobUid, repository.ID)
jobDefinitions[i] = job.Definition{
UID: jobUID,
Type: jobType,
MaxRetries: exportJobMaxRetries,
Timeout: exportJobMaxDuration,
Data: base64.StdEncoding.EncodeToString(encryptedData),
}
}
return r.scheduler.RunJobs(ctx, jobGroupId, jobDefinitions)
}
func getJobGroupId(spaceId int64) string {
return fmt.Sprintf(exportSpaceJobUid, spaceId)
}
// Handle is repository export background job handler.
func (r *Repository) Handle(ctx context.Context, data string, _ job.ProgressReporter) (string, error) {
input, err := r.getJobInput(data)
if err != nil {
return "", err
}
harnessCodeInfo := input.HarnessCodeInfo
client, err := newHarnessCodeClient(r.urlProvider.GetHarnessCodeInternalUrl(), harnessCodeInfo.AccountId, harnessCodeInfo.OrgIdentifier, harnessCodeInfo.ProjectIdentifier, harnessCodeInfo.Token)
if err != nil {
return "", err
}
repository, err := r.repoStore.Find(ctx, input.ID)
if err != nil {
return "", err
}
remoteRepo, err := client.CreateRepo(ctx, repo.CreateInput{
UID: repository.UID,
DefaultBranch: repository.DefaultBranch,
Description: repository.Description,
IsPublic: repository.IsPublic,
Readme: false,
License: "",
GitIgnore: "",
})
if err != nil {
r.publishSSE(ctx, repository)
return "", err
}
urlWithToken, err := modifyUrl(remoteRepo.GitURL, harnessCodeInfo.Token)
if err != nil {
return "", err
}
err = r.git.PushRemote(ctx, &gitrpc.PushRemoteParams{
ReadParams: gitrpc.ReadParams{RepoUID: repository.GitUID},
RemoteUrl: urlWithToken,
})
if err != nil && !strings.Contains(err.Error(), "empty") {
errDelete := client.DeleteRepo(ctx, remoteRepo.UID)
if errDelete != nil {
log.Ctx(ctx).Err(errDelete).Msgf("failed to delete repo '%s' on harness", remoteRepo.UID)
}
r.publishSSE(ctx, repository)
return "", err
}
log.Ctx(ctx).Info().Msgf("completed exporting repository '%s' to harness", repository.UID)
r.publishSSE(ctx, repository)
return "", nil
}
func (r *Repository) publishSSE(ctx context.Context, repository *types.Repository) {
err := r.sseStreamer.Publish(ctx, repository.ParentID, enum.SSETypeRepositoryExportCompleted, repository)
if err != nil {
log.Ctx(ctx).Warn().Err(err).Msg("failed to publish export completion SSE")
}
}
func (r *Repository) getJobInput(data string) (Input, error) {
encrypted, err := base64.StdEncoding.DecodeString(data)
if err != nil {
return Input{}, fmt.Errorf("failed to base64 decode job input: %w", err)
}
decrypted, err := r.encrypter.Decrypt(encrypted)
if err != nil {
return Input{}, fmt.Errorf("failed to decrypt job input: %w", err)
}
var input Input
err = json.NewDecoder(strings.NewReader(decrypted)).Decode(&input)
if err != nil {
return Input{}, fmt.Errorf("failed to unmarshal job input json: %w", err)
}
return input, nil
}
func (r *Repository) GetProgressForSpace(ctx context.Context, spaceID int64) ([]types.JobProgress, error) {
spaceId := getJobGroupId(spaceID)
progress, err := r.scheduler.GetJobProgressForGroup(ctx, spaceId)
if err != nil {
return nil, fmt.Errorf("failed to get job progress for group: %w", err)
}
if len(progress) == 0 {
return nil, ErrNotFound
}
return progress, nil
}
func modifyUrl(u string, token string) (string, error) {
parsedUrl, err := url.Parse(u)
if err != nil {
fmt.Println("Error parsing URL:", err)
return "", err
}
// Set the username and password in the URL
parsedUrl.User = url.UserPassword("token", token)
return parsedUrl.String(), nil
}