GitRoot

Craft your forge, Build your project, Grow your community freely
  1// SPDX-FileCopyrightText: 2025 Romain Maneschi <romain@gitroot.dev>
  2//
  3// SPDX-License-Identifier: EUPL-1.2
  4
  5package exec
  6
  7import (
  8	"context"
  9	"fmt"
 10	"io"
 11	"os"
 12	"path/filepath"
 13	"time"
 14
 15	"github.com/samber/oops"
 16	pluginLib "gitroot.dev/libs/golang/plugin/model"
 17	"gitroot.dev/server/configuration"
 18	"gitroot.dev/server/logger"
 19	"gitroot.dev/server/repository"
 20	"gitroot.dev/server/user"
 21)
 22
 23const (
 24	APP_DIR         = "app"
 25	JOB_CONTEXT_DIR = "jobContext"
 26	LOGS_DIR        = "logs"      //if you change that think to change in app/executor/main.go
 27	ARTIFACTS_DIR   = "artifacts" //if you change that think to change in app/executor/main.go
 28	CACHE_DIR       = "cache"
 29)
 30
 31type executor interface {
 32	name() string
 33	prepareTempDir(dir string) error
 34	exec(dir string, exec pluginLib.Exec, projectPluginCacheDir string) (*pluginLib.ExecStatus, error)
 35}
 36
 37type manager struct {
 38	logger   *logger.Logger
 39	ctx      context.Context
 40	conf     needConf
 41	executor executor
 42}
 43
 44type needConf interface {
 45	GetExecConf() configuration.ExecConf
 46	PathCacheProject(repoName string, pluginName string) string
 47}
 48
 49type needUser interface {
 50	RootCommiter() *user.Commiter
 51}
 52
 53func NewManager(ctx context.Context, conf needConf, userManager needUser) *manager {
 54	log := logger.NewLoggerCtx(logger.EXEC, ctx)
 55	var exe executor = NewNone(log)
 56	c := conf.GetExecConf()
 57	if c.BareMetal.Enabled {
 58		exe = NewBareMetal(log)
 59	} else if c.Bwrap.Enabled {
 60		exe = NewBwrap(log, conf.GetExecConf().Bwrap)
 61	} else if c.Container.Enabled {
 62		exe = NewContainer(log, conf.GetExecConf().Container)
 63	} else if c.Ssh.Enabled {
 64		exe = NewSsh(log, conf.GetExecConf().Ssh, userManager)
 65	} else {
 66		log.Warn("executor not found will use none")
 67	}
 68	m := &manager{
 69		logger:   log,
 70		ctx:      ctx,
 71		conf:     conf,
 72		executor: exe,
 73	}
 74	return m
 75}
 76
 77func (m *manager) prepareTempDir(project *repository.GitRootRepository, branch string) (string, string, error) {
 78	dir := os.TempDir()
 79	pipelineDirName := fmt.Sprintf("%s-%s-%d", project.Name(), branch, time.Now().Nanosecond())
 80	tempDir := filepath.Join(dir, "pipelines-gitroot", pipelineDirName)
 81
 82	err := os.MkdirAll(tempDir, os.ModePerm)
 83	if err != nil {
 84		return "", "", err
 85	}
 86
 87	err = project.Worktree(filepath.Join(tempDir, APP_DIR), branch)
 88	if err != nil {
 89		return "", "", err
 90	}
 91	return tempDir, pipelineDirName, nil
 92}
 93
 94func (m *manager) Exec(project *repository.GitRootRepository, branch string, pluginName string, exec pluginLib.Exec) (*pluginLib.ExecStatus, error) {
 95	m.logger.Info("will exec", logger.NewLoggerPair("executor", m.executor.name()), logger.NewLoggerPair("project", project.Name()), logger.NewLoggerPair("branch", branch))
 96	tempDir, pipelineDirName, err := m.prepareTempDir(project, branch)
 97	if err != nil {
 98		return nil, oops.Wrapf(err, "prepareTempDir")
 99	}
100	projectPluginCacheDir := m.conf.PathCacheProject(project.Name(), pluginName)
101	err = m.executor.prepareTempDir(tempDir)
102	if err != nil {
103		return nil, oops.With("executor", m.executor.name()).Wrapf(err, "executor.prepareTempDir")
104	}
105	execStatus, err := m.executor.exec(tempDir, exec, projectPluginCacheDir)
106	if err != nil {
107		return nil, oops.With("executor", m.executor.name()).Wrapf(err, "executor.exec")
108	}
109	for i, execStatu := range execStatus.CmdsLogs {
110		if execStatu != "" {
111			execStatus.CmdsLogs[i] = filepath.Join(pipelineDirName, execStatu)
112		}
113	}
114	for i, artifact := range execStatus.Artifacts {
115		if artifact != "" {
116			execStatus.Artifacts[i] = filepath.Join(pipelineDirName, artifact)
117		}
118	}
119
120	distArtifacts := filepath.Join(projectPluginCacheDir, pipelineDirName, ARTIFACTS_DIR)
121	copyDir(filepath.Join(tempDir, JOB_CONTEXT_DIR, ARTIFACTS_DIR), distArtifacts)
122	distLogs := filepath.Join(projectPluginCacheDir, pipelineDirName, LOGS_DIR)
123	copyDir(filepath.Join(tempDir, JOB_CONTEXT_DIR, LOGS_DIR), distLogs)
124	return execStatus, nil
125}
126
127// from https://gistlib.com/go/copy-a-directory-in-go
128func copyDir(src, dst string) error {
129	// get properties of source dir
130	srcInfo, err := os.Stat(src)
131	if err != nil {
132		return err
133	}
134
135	// create destination dir
136	err = os.MkdirAll(dst, srcInfo.Mode())
137	if err != nil {
138		return err
139	}
140
141	// get contents of the source dir
142	entries, err := os.ReadDir(src)
143	if err != nil {
144		return err
145	}
146
147	// copy each file/dir in the source dir to destination dir
148	for _, entry := range entries {
149		srcPath := src + "/" + entry.Name()
150		dstPath := dst + "/" + entry.Name()
151
152		// recursively copy a directory
153		if entry.IsDir() {
154			err = copyDir(srcPath, dstPath)
155			if err != nil {
156				return err
157			}
158		} else {
159			// perform copy operation on a file
160			err = copyFile(srcPath, dstPath)
161			if err != nil {
162				return err
163			}
164		}
165	}
166	return nil
167}
168
169// from https://gistlib.com/go/copy-a-directory-in-go
170func copyFile(src, dst string) error {
171	sourceFile, err := os.Open(src)
172	if err != nil {
173		return err
174	}
175	defer sourceFile.Close()
176
177	destFile, err := os.Create(dst)
178	if err != nil {
179		return err
180	}
181	defer destFile.Close()
182
183	_, err = io.Copy(destFile, sourceFile)
184	if err == nil {
185		sourceInfo, err := os.Stat(src)
186		if err != nil {
187			err = os.Chmod(dst, sourceInfo.Mode())
188		}
189
190	}
191	return err
192}