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}