debian-forge-composer/internal/blueprintapi/blueprint_editor.go
robojerk 4eeaa43c39
Some checks failed
Tests / 🛃 Unit tests (push) Failing after 13s
Tests / 🗄 DB tests (push) Failing after 19s
Tests / 🐍 Lint python scripts (push) Failing after 1s
Tests / ⌨ Golang Lint (push) Failing after 1s
Tests / 📦 Packit config lint (push) Failing after 1s
Tests / 🔍 Check source preparation (push) Failing after 1s
Tests / 🔍 Check for valid snapshot urls (push) Failing after 1s
Tests / 🔍 Check for missing or unused runner repos (push) Failing after 1s
Tests / 🐚 Shellcheck (push) Failing after 1s
Tests / 📦 RPMlint (push) Failing after 1s
Tests / Gitlab CI trigger helper (push) Failing after 1s
Tests / 🎀 kube-linter (push) Failing after 1s
Tests / 🧹 cloud-cleaner-is-enabled (push) Successful in 3s
Tests / 🔍 Check spec file osbuild/images dependencies (push) Failing after 1s
did stuff
2025-08-26 10:34:42 -07:00

441 lines
13 KiB
Go

package blueprintapi
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/labstack/echo/v4"
"github.com/sirupsen/logrus"
)
type BlueprintEditor struct {
store BlueprintStore
logger *logrus.Logger
templates map[string]BlueprintTemplate
}
type BlueprintStore interface {
SaveBlueprint(blueprint *Blueprint) error
GetBlueprint(id string) (*Blueprint, error)
ListBlueprints() ([]*Blueprint, error)
DeleteBlueprint(id string) error
ValidateBlueprint(blueprint *Blueprint) error
}
type Blueprint struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
Variant string `json:"variant"`
Architecture string `json:"architecture"`
Packages BlueprintPackages `json:"packages"`
Users []BlueprintUser `json:"users"`
Groups []BlueprintGroup `json:"groups"`
Services []BlueprintService `json:"services"`
Files []BlueprintFile `json:"files"`
Customizations BlueprintCustomizations `json:"customizations"`
Created time.Time `json:"created"`
Modified time.Time `json:"modified"`
Tags []string `json:"tags"`
Metadata map[string]interface{} `json:"metadata"`
}
type BlueprintPackages struct {
Include []string `json:"include"`
Exclude []string `json:"exclude"`
Groups []string `json:"groups"`
}
type BlueprintUser struct {
Name string `json:"name"`
Description string `json:"description"`
Password string `json:"password,omitempty"`
Key string `json:"key,omitempty"`
Home string `json:"home"`
Shell string `json:"shell"`
Groups []string `json:"groups"`
UID int `json:"uid"`
GID int `json:"gid"`
}
type BlueprintGroup struct {
Name string `json:"name"`
Description string `json:"description"`
GID int `json:"gid"`
}
type BlueprintService struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
Masked bool `json:"masked"`
}
type BlueprintFile struct {
Path string `json:"path"`
User string `json:"user"`
Group string `json:"group"`
Mode string `json:"mode"`
Data string `json:"data"`
EnsureParents bool `json:"ensure_parents"`
}
type BlueprintCustomizations struct {
Hostname string `json:"hostname"`
Kernel BlueprintKernel `json:"kernel"`
Timezone string `json:"timezone"`
Locale string `json:"locale"`
Firewall BlueprintFirewall `json:"firewall"`
SSH BlueprintSSH `json:"ssh"`
}
type BlueprintKernel struct {
Name string `json:"name"`
Append string `json:"append"`
Remove string `json:"remove"`
}
type BlueprintFirewall struct {
Services []string `json:"services"`
Ports []string `json:"ports"`
}
type BlueprintSSH struct {
KeyFile string `json:"key_file"`
User string `json:"user"`
}
type BlueprintTemplate struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Category string `json:"category"`
Tags []string `json:"tags"`
Blueprint *Blueprint `json:"blueprint"`
Popularity int `json:"popularity"`
}
type BlueprintValidationResult struct {
Valid bool `json:"valid"`
Errors []string `json:"errors"`
Warnings []string `json:"warnings"`
}
func NewBlueprintEditor(store BlueprintStore, logger *logrus.Logger) *BlueprintEditor {
editor := &BlueprintEditor{
store: store,
logger: logger,
templates: make(map[string]BlueprintTemplate),
}
// Initialize default templates
editor.initializeTemplates()
return editor
}
func (be *BlueprintEditor) initializeTemplates() {
// Minimal Debian template
minimalTemplate := BlueprintTemplate{
ID: "debian-minimal",
Name: "Minimal Debian",
Description: "Minimal Debian system without desktop environment",
Category: "minimal",
Tags: []string{"minimal", "server", "debian"},
Popularity: 100,
Blueprint: &Blueprint{
Name: "debian-minimal",
Description: "Minimal Debian system",
Version: "1.0.0",
Variant: "bookworm",
Architecture: "amd64",
Packages: BlueprintPackages{
Include: []string{"task-minimal"},
Exclude: []string{},
Groups: []string{},
},
Users: []BlueprintUser{
{
Name: "debian",
Description: "Default user",
Home: "/home/debian",
Shell: "/bin/bash",
Groups: []string{"users"},
},
},
Customizations: BlueprintCustomizations{
Hostname: "debian-minimal",
Timezone: "UTC",
Locale: "en_US.UTF-8",
},
},
}
// GNOME Desktop template
gnomeTemplate := BlueprintTemplate{
ID: "debian-gnome",
Name: "Debian GNOME",
Description: "Debian with GNOME desktop environment",
Category: "desktop",
Tags: []string{"desktop", "gnome", "debian"},
Popularity: 90,
Blueprint: &Blueprint{
Name: "debian-gnome",
Description: "Debian with GNOME desktop",
Version: "1.0.0",
Variant: "bookworm",
Architecture: "amd64",
Packages: BlueprintPackages{
Include: []string{"task-gnome-desktop", "gnome-core"},
Exclude: []string{},
Groups: []string{},
},
Users: []BlueprintUser{
{
Name: "debian",
Description: "Default user",
Home: "/home/debian",
Shell: "/bin/bash",
Groups: []string{"users", "sudo"},
},
},
Customizations: BlueprintCustomizations{
Hostname: "debian-gnome",
Timezone: "UTC",
Locale: "en_US.UTF-8",
},
},
}
be.templates["debian-minimal"] = minimalTemplate
be.templates["debian-gnome"] = gnomeTemplate
}
func (be *BlueprintEditor) RegisterRoutes(e *echo.Echo) {
// Blueprint CRUD operations
e.GET("/api/v1/blueprints", be.ListBlueprints)
e.POST("/api/v1/blueprints", be.CreateBlueprint)
e.GET("/api/v1/blueprints/:id", be.GetBlueprint)
e.PUT("/api/v1/blueprints/:id", be.UpdateBlueprint)
e.DELETE("/api/v1/blueprints/:id", be.DeleteBlueprint)
// Blueprint validation
e.POST("/api/v1/blueprints/validate", be.ValidateBlueprint)
// Blueprint templates
e.GET("/api/v1/blueprint-templates", be.ListTemplates)
e.GET("/api/v1/blueprint-templates/:id", be.GetTemplate)
e.POST("/api/v1/blueprint-templates/:id/instantiate", be.InstantiateTemplate)
// Blueprint import/export
e.POST("/api/v1/blueprints/import", be.ImportBlueprint)
e.GET("/api/v1/blueprints/:id/export", be.ExportBlueprint)
}
func (be *BlueprintEditor) ListBlueprints(c echo.Context) error {
blueprints, err := be.store.ListBlueprints()
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to list blueprints: %v", err))
}
return c.JSON(http.StatusOK, blueprints)
}
func (be *BlueprintEditor) CreateBlueprint(c echo.Context) error {
var blueprint Blueprint
if err := c.Bind(&blueprint); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid blueprint data: %v", err))
}
// Validate blueprint
if err := be.store.ValidateBlueprint(&blueprint); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("blueprint validation failed: %v", err))
}
// Set timestamps
now := time.Now()
blueprint.Created = now
blueprint.Modified = now
// Save blueprint
if err := be.store.SaveBlueprint(&blueprint); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to save blueprint: %v", err))
}
be.logger.Infof("Created blueprint: %s", blueprint.ID)
return c.JSON(http.StatusCreated, blueprint)
}
func (be *BlueprintEditor) GetBlueprint(c echo.Context) error {
id := c.Param("id")
blueprint, err := be.store.GetBlueprint(id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("blueprint not found: %v", err))
}
return c.JSON(http.StatusOK, blueprint)
}
func (be *BlueprintEditor) UpdateBlueprint(c echo.Context) error {
id := c.Param("id")
// Get existing blueprint
existing, err := be.store.GetBlueprint(id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("blueprint not found: %v", err))
}
// Bind update data
var update Blueprint
if err := c.Bind(&update); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid update data: %v", err))
}
// Update fields
update.ID = existing.ID
update.Created = existing.Created
update.Modified = time.Now()
// Validate updated blueprint
if err := be.store.ValidateBlueprint(&update); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("blueprint validation failed: %v", err))
}
// Save updated blueprint
if err := be.store.SaveBlueprint(&update); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to save blueprint: %v", err))
}
be.logger.Infof("Updated blueprint: %s", id)
return c.JSON(http.StatusOK, update)
}
func (be *BlueprintEditor) DeleteBlueprint(c echo.Context) error {
id := c.Param("id")
if err := be.store.DeleteBlueprint(id); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to delete blueprint: %v", err))
}
be.logger.Infof("Deleted blueprint: %s", id)
return c.NoContent(http.StatusNoContent)
}
func (be *BlueprintEditor) ValidateBlueprint(c echo.Context) error {
var blueprint Blueprint
if err := c.Bind(&blueprint); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid blueprint data: %v", err))
}
result := BlueprintValidationResult{
Valid: true,
Errors: []string{},
Warnings: []string{},
}
// Validate blueprint
if err := be.store.ValidateBlueprint(&blueprint); err != nil {
result.Valid = false
result.Errors = append(result.Errors, err.Error())
}
// Additional validation logic can be added here
return c.JSON(http.StatusOK, result)
}
func (be *BlueprintEditor) ListTemplates(c echo.Context) error {
var templates []BlueprintTemplate
for _, template := range be.templates {
templates = append(templates, template)
}
return c.JSON(http.StatusOK, templates)
}
func (be *BlueprintEditor) GetTemplate(c echo.Context) error {
id := c.Param("id")
template, exists := be.templates[id]
if !exists {
return echo.NewHTTPError(http.StatusNotFound, "template not found")
}
return c.JSON(http.StatusOK, template)
}
func (be *BlueprintEditor) InstantiateTemplate(c echo.Context) error {
id := c.Param("id")
template, exists := be.templates[id]
if !exists {
return echo.NewHTTPError(http.StatusNotFound, "template not found")
}
// Create new blueprint from template
blueprint := *template.Blueprint
blueprint.ID = "" // Will be generated
blueprint.Created = time.Now()
blueprint.Modified = time.Now()
// Save new blueprint
if err := be.store.SaveBlueprint(&blueprint); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to save blueprint: %v", err))
}
be.logger.Infof("Instantiated template %s as blueprint: %s", id, blueprint.ID)
return c.JSON(http.StatusCreated, blueprint)
}
func (be *BlueprintEditor) ImportBlueprint(c echo.Context) error {
file, err := c.FormFile("blueprint")
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "no blueprint file provided")
}
// Read file content
src, err := file.Open()
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to open file: %v", err))
}
defer src.Close()
// Parse blueprint
var blueprint Blueprint
if err := json.NewDecoder(src).Decode(&blueprint); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid blueprint format: %v", err))
}
// Validate and save
if err := be.store.ValidateBlueprint(&blueprint); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("blueprint validation failed: %v", err))
}
blueprint.Created = time.Now()
blueprint.Modified = time.Now()
if err := be.store.SaveBlueprint(&blueprint); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to save blueprint: %v", err))
}
be.logger.Infof("Imported blueprint: %s", blueprint.ID)
return c.JSON(http.StatusCreated, blueprint)
}
func (be *BlueprintEditor) ExportBlueprint(c echo.Context) error {
id := c.Param("id")
blueprint, err := be.store.GetBlueprint(id)
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("blueprint not found: %v", err))
}
// Set content type and headers for download
c.Response().Header().Set("Content-Type", "application/json")
c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.json\"", blueprint.Name))
return c.JSON(http.StatusOK, blueprint)
}