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