75424a5f25
Build / build (push) Successful in 10m42s
Adds a new Stacks feature: upload/edit docker-compose YAML, deploy as atomic units, browse revisions, roll back, and stream logs. Backend in internal/stack + internal/api/stacks.go, persistent storage in internal/store/stacks.go. Stacks pages (list, new, detail) use a modern Forge aesthetic — Instrument Serif display type, JetBrains Mono for meta/code, indigo ember accents, dot-grid hero, registration marks on hover, terminal panel for logs. Palette is sourced from the app's existing design tokens so the feature remains consistent with the rest of Tinyforge. Fonts self-hosted via @fontsource/instrument-serif and @fontsource/jetbrains-mono to satisfy the strict CSP.
53 lines
1.7 KiB
Go
53 lines
1.7 KiB
Go
package stack
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// ComposeSpec is a minimal, lenient representation of a compose file.
|
|
// We only decode fields we need for validation + label-based proxy routing;
|
|
// everything else is preserved as-is and passed to `docker compose`.
|
|
type ComposeSpec struct {
|
|
Version string `yaml:"version,omitempty"`
|
|
Services map[string]ServiceSpec `yaml:"services"`
|
|
}
|
|
|
|
// ServiceSpec captures the subset of compose service fields we inspect.
|
|
type ServiceSpec struct {
|
|
Image string `yaml:"image,omitempty"`
|
|
Ports []any `yaml:"ports,omitempty"`
|
|
Labels map[string]string `yaml:"labels,omitempty"`
|
|
Privileged bool `yaml:"privileged,omitempty"`
|
|
}
|
|
|
|
// Parse decodes YAML into a ComposeSpec. Returns a descriptive error on failure.
|
|
func Parse(yamlText string) (ComposeSpec, error) {
|
|
var spec ComposeSpec
|
|
if err := yaml.Unmarshal([]byte(yamlText), &spec); err != nil {
|
|
return ComposeSpec{}, fmt.Errorf("invalid yaml: %w", err)
|
|
}
|
|
if len(spec.Services) == 0 {
|
|
return ComposeSpec{}, fmt.Errorf("compose file has no services")
|
|
}
|
|
return spec, nil
|
|
}
|
|
|
|
// Validate enforces Tinyforge-level constraints beyond compose schema validity.
|
|
// Current rules:
|
|
// - No service may set `privileged: true`.
|
|
// - Every service must declare an image (compose supports build: too, but
|
|
// Tinyforge v1 disallows building from context to avoid arbitrary-code exec).
|
|
func Validate(spec ComposeSpec) error {
|
|
for name, svc := range spec.Services {
|
|
if svc.Privileged {
|
|
return fmt.Errorf("service %q: privileged mode is not allowed", name)
|
|
}
|
|
if svc.Image == "" {
|
|
return fmt.Errorf("service %q: image is required (build contexts not supported)", name)
|
|
}
|
|
}
|
|
return nil
|
|
}
|