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