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