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