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}