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 plugin
6
7import (
8 "encoding/json"
9 "fmt"
10 "path/filepath"
11 "reflect"
12 "regexp"
13 "slices"
14 "strings"
15
16 "github.com/goccy/go-yaml"
17 "github.com/hashicorp/go-getter"
18 "github.com/samber/oops"
19 "github.com/tetratelabs/wazero"
20 "gitroot.dev/libs/golang/glob"
21 pluginLib "gitroot.dev/libs/golang/plugin/model"
22 "gitroot.dev/server/logger"
23 "gitroot.dev/server/user"
24)
25
26type Plugin struct {
27 Url string `yaml:",omitempty"`
28 Checksum string `yaml:",omitempty"`
29 Name string
30 Version string
31 Active bool
32 compiledModule wazero.CompiledModule
33 commiter *user.Commiter
34 Run []PluginRun
35}
36
37func (p Plugin) Log() string {
38 return p.Name
39}
40
41type PluginRun struct {
42 pluginLib.PluginRun `yaml:",inline"`
43 glob *glob.Glob
44 write PluginWrite
45}
46
47type PluginWrite struct {
48 git []PluginWriteRight
49 web []PluginWriteRight
50 exec []PluginExecRight
51 callFunc []pluginLib.PluginCallFuncRight
52}
53
54type PluginWriteRight struct {
55 pluginLib.PluginWriteRight
56 glob *glob.Glob
57}
58
59type PluginExecRight struct {
60 pluginLib.PluginExecRight
61 regexp *regexp.Regexp
62}
63
64func pluginLibToPlugin(run []pluginLib.PluginRun) ([]PluginRun, error) {
65 p := make([]PluginRun, len(run))
66 for i, pl := range run {
67 g, err := glob.NewGlob(pl.Path)
68 if err != nil {
69 return nil, oops.Wrapf(err, "can't glob %s", pl.Path)
70 }
71 pluginWrite := PluginWrite{
72 git: make([]PluginWriteRight, len(pl.Write.Git)),
73 web: make([]PluginWriteRight, len(pl.Write.Web)),
74 exec: make([]PluginExecRight, len(pl.Write.Exec)),
75 callFunc: pl.Write.CallFunc,
76 }
77 for j, pw := range pl.Write.Git {
78 g, err := glob.NewGlob(pw.Path)
79 if err != nil {
80 return nil, oops.Wrapf(err, "can't glob %s", pw.Path)
81 }
82 pluginWrite.git[j] = PluginWriteRight{PluginWriteRight: pw, glob: g}
83 }
84 for j, pw := range pl.Write.Web {
85 g, err := glob.NewGlob(pw.Path)
86 if err != nil {
87 return nil, oops.Wrapf(err, "can't glob %s", pw.Path)
88 }
89 pluginWrite.web[j] = PluginWriteRight{PluginWriteRight: pw, glob: g}
90 }
91 for j, pw := range pl.Write.Exec {
92 g, err := regexp.Compile(pw.Command)
93 if err != nil {
94 return nil, oops.Wrapf(err, "can't regexp %s", pw.Command)
95 }
96 pluginWrite.exec[j] = PluginExecRight{PluginExecRight: pw, regexp: g}
97 }
98 p[i] = PluginRun{PluginRun: pl, glob: g, write: pluginWrite}
99 }
100 return p, nil
101}
102
103func ParsePlugins(fileContent []byte, withRun bool) ([]Plugin, error) {
104 pluginsRaw := make([]Plugin, 0)
105 if err := yaml.Unmarshal(fileContent, &pluginsRaw); err != nil {
106 return nil, oops.Wrapf(err, "can't parse yaml config")
107 }
108
109 plugins := make([]Plugin, len(pluginsRaw))
110 for i, plugin := range pluginsRaw {
111 plugins[i] = plugin.DetermineNameAndVersionFromUrl()
112 }
113 if withRun {
114 for i, plugin := range plugins {
115 for j, pluginRun := range plugin.Run {
116 g, err := glob.NewGlob(pluginRun.Path)
117 if err != nil {
118 return nil, oops.Wrapf(err, "can't glob %s", pluginRun.Path)
119 }
120 plugins[i].Run[j].glob = g
121 for h, pluginWrite := range pluginRun.Write.Git {
122 g, err = glob.NewGlob(pluginWrite.Path)
123 if err != nil {
124 return nil, oops.Wrapf(err, "can't glob git %s", pluginRun.Path)
125 }
126 if plugins[i].Run[j].write.git == nil {
127 plugins[i].Run[j].write.git = make([]PluginWriteRight, len(pluginRun.Write.Git))
128 }
129 plugins[i].Run[j].write.git[h] = PluginWriteRight{PluginWriteRight: pluginWrite, glob: g}
130 }
131 for h, pluginWrite := range pluginRun.Write.Web {
132 g, err = glob.NewGlob(pluginWrite.Path)
133 if err != nil {
134 return nil, oops.Wrapf(err, "can't glob git %s", pluginRun.Path)
135 }
136 if plugins[i].Run[j].write.web == nil {
137 plugins[i].Run[j].write.web = make([]PluginWriteRight, len(pluginRun.Write.Web))
138 }
139 plugins[i].Run[j].write.web[h] = PluginWriteRight{PluginWriteRight: pluginWrite, glob: g}
140 }
141 for h, pluginWrite := range pluginRun.Write.Exec {
142 g, err := regexp.Compile(pluginWrite.Command)
143 if err != nil {
144 return nil, oops.Wrapf(err, "can't regexp %s", pluginWrite.Command)
145 }
146 if plugins[i].Run[j].write.exec == nil {
147 plugins[i].Run[j].write.exec = make([]PluginExecRight, len(pluginRun.Write.Exec))
148 }
149 plugins[i].Run[j].write.exec[h] = PluginExecRight{PluginExecRight: pluginWrite, regexp: g}
150 }
151 plugins[i].Run[j].write.callFunc = pluginRun.Write.CallFunc
152 }
153 }
154 }
155 return plugins, nil
156}
157
158func (p Plugin) SetActive(active bool) Plugin {
159 return Plugin{
160 Url: p.Url,
161 Checksum: p.Checksum,
162 Name: p.Name,
163 Version: p.Version,
164 Active: active,
165 compiledModule: p.compiledModule,
166 commiter: p.commiter,
167 Run: p.Run,
168 }
169}
170
171func (p Plugin) SetRun(run []PluginRun) Plugin {
172 return Plugin{
173 Url: p.Url,
174 Checksum: p.Checksum,
175 Name: p.Name,
176 Version: p.Version,
177 Active: p.Active,
178 compiledModule: p.compiledModule,
179 commiter: p.commiter,
180 Run: run,
181 }
182}
183
184func (p Plugin) UpdateVersion(version string) Plugin {
185 return Plugin{
186 Url: p.Url,
187 Checksum: p.Checksum,
188 Name: p.Name,
189 Version: version,
190 Active: p.Active,
191 compiledModule: p.compiledModule,
192 commiter: p.commiter,
193 Run: p.Run,
194 }
195}
196
197func (p Plugin) OverrideWith(other Plugin) Plugin {
198 copy := Plugin{
199 Url: p.Url,
200 Checksum: p.Checksum,
201 Name: p.Name,
202 Version: p.Version,
203 Active: p.Active,
204 compiledModule: p.compiledModule,
205 commiter: p.commiter,
206 Run: p.Run,
207 }
208 if copy.Url == "" {
209 copy.Url = other.Url
210 }
211 if copy.Checksum == "" {
212 copy.Checksum = other.Checksum
213 }
214 if copy.Name == "" {
215 copy.Name = other.Name
216 }
217 if copy.Version == "" {
218 copy.Version = other.Version
219 }
220 if !copy.Active {
221 copy.Active = other.Active
222 }
223 if len(copy.Run) == 0 {
224 copy.Run = other.Run
225 }
226 if copy.compiledModule == nil {
227 copy.compiledModule = other.compiledModule
228 }
229 if copy.commiter == nil || copy.commiter.Signer == nil {
230 copy.commiter = other.commiter
231 }
232 return copy
233}
234
235func (p Plugin) Commiter() *user.Commiter {
236 return p.commiter
237}
238
239func (p Plugin) DetermineNameAndVersionFromUrl() Plugin {
240 if p.Url != "" { //if child repo no url
241 _, versions := filepath.Split(p.Url)
242 nameVersion, _, _ := strings.Cut(versions, ".wasm")
243 name, version, found := strings.Cut(nameVersion, "-")
244 if !found && p.Name == "" {
245 name = p.Url
246 version = "unknown"
247 }
248 return p.OverrideWith(Plugin{Name: name}).SetActive(p.Active).UpdateVersion(version)
249 }
250 return p
251}
252
253func (p Plugin) uuid() string {
254 return fmt.Sprintf("%s-%s", p.Name, p.Version)
255}
256
257func (pr PluginRun) Marshal() ([]byte, error) {
258 return json.Marshal(pr.Configuration)
259}
260
261func (m *Manager) PathWasm(p Plugin) string {
262 return filepath.Join(m.conf.GetDirPathDataPlugin(p.Name), fmt.Sprintf("%s-%s.wasm", p.Name, p.Version))
263}
264
265func (m *Manager) Install(p Plugin) error {
266 url := p.Url
267 if p.Checksum != "" {
268 url = fmt.Sprintf("%s?checksum=%s", url, p.Checksum)
269 }
270 path := m.PathWasm(p)
271 m.logger.Debug("install plugin", logger.NewLoggerPair("url", url), logger.NewLoggerPair("path", path))
272 httpGetter := &getter.HttpGetter{
273 Netrc: true,
274 }
275 err := getter.GetFile(path, url, getter.WithGetters(map[string]getter.Getter{
276 "file": &getter.FileGetter{Copy: true},
277 "http": httpGetter,
278 "https": httpGetter,
279 }))
280 return err
281}
282
283func Equal(aPlugin, bPlugin Plugin) bool {
284 return aPlugin.Name == bPlugin.Name &&
285 aPlugin.Active == bPlugin.Active &&
286 aPlugin.Version == bPlugin.Version &&
287 equalPluginRun(aPlugin.Run, bPlugin.Run)
288}
289
290func equalPluginRun(aPlugin, bPlugin []PluginRun) bool {
291 if len(aPlugin) == len(bPlugin) {
292 for i, w := range aPlugin {
293 isEqual := w.Path == bPlugin[i].Path &&
294 slices.Equal(w.Branch, bPlugin[i].Branch) &&
295 slices.Equal(w.When, bPlugin[i].When) &&
296 equalPluginWrite(w.Write, bPlugin[i].Write) &&
297 reflect.DeepEqual(w.Configuration, bPlugin[i].Configuration)
298 if !isEqual {
299 return false
300 }
301 }
302 return true
303 }
304 return false
305}
306
307func equalPluginWrite(aPlugin, bPlugin pluginLib.PluginWrite) bool {
308 if len(aPlugin.Git) == len(bPlugin.Git) {
309 for i, w := range aPlugin.Git {
310 if w.Path != bPlugin.Git[i].Path || !slices.Equal(w.Can, bPlugin.Git[i].Can) {
311 return false
312 }
313 }
314 return true
315 }
316 return false
317}