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}