Some checks failed
Deploy Website / deploy (push) Successful in 2m7s
Publish Edge Images / images (apps/console/Dockerfile, barkstack) (push) Successful in 12m9s
Publish Edge Images / images (services/pawsql/Dockerfile, pawsql) (push) Successful in 13m7s
Test and Release Barkstack / test (push) Failing after 2m32s
Verify Platform / verify (push) Failing after 48s
Verify Platform / images (apps/console/Dockerfile, console) (push) Has been skipped
Verify Platform / images (services/pawsql/Dockerfile, pawsql) (push) Has been skipped
Verify Platform / images (services/treatvault/Dockerfile, treatvault) (push) Has been skipped
Test and Release Barkstack / release (push) Has been skipped
Publish Edge Images / images (services/treatvault/Dockerfile, treatvault) (push) Successful in 8m50s
AGENT.md now points at .ai/guidelines, so the guidelines move into the repository and the reference resolves on a fresh clone. Completes the exported doc comments in the console manifest and registry left from the lint pass, and ignores local scratch files (.DS_Store, sdkscratch). The root Barkfile's include files (Barkfile.campbellwireless, Barkfile.ruckstack) stay local on purpose: they are personal infrastructure and Barkfile.campbellwireless currently does not parse (a treatvault block inside a project block, which the parser rejects).
115 lines
3.9 KiB
Go
115 lines
3.9 KiB
Go
package console
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"path"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// PluginAPIVersion is the manifest API version this console server accepts.
|
|
const PluginAPIVersion = "barkstack.dev/ui/v1"
|
|
|
|
var (
|
|
pluginIDPattern = regexp.MustCompile(`^[a-z][a-z0-9-]*$`)
|
|
elementPattern = regexp.MustCompile(`^[a-z][a-z0-9]*(?:-[a-z0-9]+)+$`)
|
|
)
|
|
|
|
// PluginEndpoint is one statically configured plugin backend.
|
|
type PluginEndpoint struct {
|
|
ID string
|
|
BaseURL string
|
|
}
|
|
|
|
// NavigationItem is one navigation entry a plugin contributes to the
|
|
// console sidebar.
|
|
type NavigationItem struct {
|
|
Label string `json:"label"`
|
|
Path string `json:"path"`
|
|
Icon string `json:"icon,omitempty"`
|
|
}
|
|
|
|
// PluginManifest is the manifest document a plugin serves at its manifest
|
|
// endpoint.
|
|
type PluginManifest struct {
|
|
APIVersion string `json:"apiVersion"`
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description,omitempty"`
|
|
Icon string `json:"icon,omitempty"`
|
|
Mount string `json:"mount"`
|
|
Entry string `json:"entry,omitempty"`
|
|
Element string `json:"element,omitempty"`
|
|
Navigation []NavigationItem `json:"navigation"`
|
|
Available bool `json:"available"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// ValidateManifest checks a fetched manifest against the Barkfile-configured
|
|
// plugin ID and the naming rules for mounts, entries, and elements.
|
|
func ValidateManifest(configuredID string, manifest PluginManifest) error {
|
|
if manifest.APIVersion != PluginAPIVersion {
|
|
return fmt.Errorf("unsupported API version %q", manifest.APIVersion)
|
|
}
|
|
if !pluginIDPattern.MatchString(manifest.ID) {
|
|
return fmt.Errorf("invalid plugin ID %q", manifest.ID)
|
|
}
|
|
if manifest.ID != configuredID {
|
|
return fmt.Errorf("configured plugin ID %q does not match manifest ID %q", configuredID, manifest.ID)
|
|
}
|
|
if manifest.Name == "" {
|
|
return fmt.Errorf("plugin name is required")
|
|
}
|
|
if !validConsolePath(manifest.Mount) || manifest.Mount == "/" {
|
|
return fmt.Errorf("invalid mount path %q", manifest.Mount)
|
|
}
|
|
entry, err := url.Parse(manifest.Entry)
|
|
if err != nil || entry.Scheme != "" || entry.Host != "" || entry.User != nil || entry.RawQuery != "" || entry.Fragment != "" || !strings.HasPrefix(entry.Path, "/barkstack/ui/") || path.Clean(entry.Path) != entry.Path {
|
|
return fmt.Errorf("invalid plugin entry path %q", manifest.Entry)
|
|
}
|
|
if !elementPattern.MatchString(manifest.Element) {
|
|
return fmt.Errorf("invalid custom-element name %q", manifest.Element)
|
|
}
|
|
if len(manifest.Navigation) == 0 {
|
|
return fmt.Errorf("at least one navigation item is required")
|
|
}
|
|
for index, item := range manifest.Navigation {
|
|
if item.Label == "" {
|
|
return fmt.Errorf("navigation item %d has no label", index)
|
|
}
|
|
if !validConsolePath(item.Path) || (item.Path != manifest.Mount && !strings.HasPrefix(item.Path, manifest.Mount+"/")) {
|
|
return fmt.Errorf("navigation path %q is outside mount %q", item.Path, manifest.Mount)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validConsolePath(value string) bool {
|
|
if value == "" || !strings.HasPrefix(value, "/") || strings.HasPrefix(value, "//") || strings.Contains(value, "\\") {
|
|
return false
|
|
}
|
|
parsed, err := url.Parse(value)
|
|
return err == nil && parsed.Path == value && parsed.RawQuery == "" && parsed.Fragment == "" && path.Clean(value) == value
|
|
}
|
|
|
|
func publicManifest(manifest PluginManifest) PluginManifest {
|
|
manifest.Entry = "/plugins/" + manifest.ID + strings.TrimPrefix(manifest.Entry, "/barkstack")
|
|
manifest.Available = true
|
|
manifest.Error = ""
|
|
return manifest
|
|
}
|
|
|
|
func unavailableManifest(id string) PluginManifest {
|
|
name := strings.ToUpper(id[:1]) + id[1:]
|
|
return PluginManifest{
|
|
APIVersion: PluginAPIVersion,
|
|
ID: id,
|
|
Name: name,
|
|
Mount: "/" + id,
|
|
Navigation: []NavigationItem{{Label: name, Path: "/" + id}},
|
|
Available: false,
|
|
Error: "Service is unreachable from Barkstack.",
|
|
}
|
|
}
|