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