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