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