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
441 lines
13 KiB
Go
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)
|
|
}
|