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}