Add a new OpenAPI v3 REST API to warewulfd at /api

Co-authored-by: jason yang <jasonyangshadow@gmail.com>

Signed-off-by: Jonathon Anderson <janderson@ciq.com>
This commit is contained in:
Jonathon Anderson
2025-03-02 13:32:41 -07:00
parent bf0fc8bb27
commit 4005afac4c
42 changed files with 2730 additions and 345 deletions

View File

@@ -0,0 +1,234 @@
package api
import (
"context"
"fmt"
"net/url"
"os"
"path"
"github.com/swaggest/usecase"
"github.com/swaggest/usecase/status"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/warewulf/warewulf/internal/pkg/overlay"
"github.com/warewulf/warewulf/internal/pkg/util"
"github.com/warewulf/warewulf/internal/pkg/wwlog"
)
type OverlayResponse struct {
Files []string `json:"files"`
Site bool `json:"site"`
}
func NewOverlayResponse(name string) *OverlayResponse {
o := new(OverlayResponse)
o.Files = []string{}
if files, err := overlay.OverlayGetFiles(name); err == nil {
o.Files = files
}
o.Site = overlay.GetOverlay(name).IsSiteOverlay()
return o
}
func getOverlays() usecase.Interactor {
u := usecase.NewInteractor(func(ctx context.Context, _ struct{}, output *map[string]*OverlayResponse) error {
wwlog.Debug("api.getOverlays()")
m := make(map[string]*OverlayResponse)
names := overlay.FindOverlays()
for _, name := range names {
m[name] = NewOverlayResponse(name)
}
*output = m
return nil
})
u.SetTitle("Get overlays")
u.SetDescription("Get all overlays.")
u.SetTags("Overlay")
return u
}
func getOverlayByName() usecase.Interactor {
type getOverlayByNameInput struct {
Name string `path:"name" required:"true" description:"Name of overlay to get"`
}
u := usecase.NewInteractor(func(ctx context.Context, input getOverlayByNameInput, output *OverlayResponse) error {
wwlog.Debug("api.getOverlayByName(Name:%v)", input.Name)
if !overlay.GetOverlay(input.Name).Exists() {
return status.Wrap(fmt.Errorf("overlay not found: %v", input.Name), status.NotFound)
} else {
*output = *NewOverlayResponse(input.Name)
return nil
}
})
u.SetTitle("Get an overlay")
u.SetDescription("Get an overlay by its name.")
u.SetTags("Overlay")
return u
}
type OverlayFile struct {
Overlay string `json:"overlay"`
Path string `json:"path"`
Contents string `json:"contents"`
rendered bool
}
func (of *OverlayFile) FullPath() string {
return path.Join(overlay.GetOverlay(of.Overlay).Rootfs(), of.Path)
}
func (of *OverlayFile) Exists() bool {
return overlay.GetOverlay(of.Overlay).Exists() && util.IsFile(of.FullPath())
}
func (of *OverlayFile) readContents() (string, error) {
f, err := os.ReadFile(of.FullPath())
return string(f), err
}
func (of *OverlayFile) renderContents(nodeName string) (string, error) {
if !(path.Ext(of.Path) == ".ww") {
return "", fmt.Errorf("'%s' does not end with '.ww'", of.Path)
}
if of.rendered {
return "", fmt.Errorf("already rendered")
}
registry, regErr := node.New()
if regErr != nil {
return "", regErr
}
renderNode, nodeErr := registry.GetNode(nodeName)
if nodeErr != nil {
return "", nodeErr
}
allNodes, allNodesErr := registry.FindAllNodes()
if allNodesErr != nil {
return "", allNodesErr
}
tstruct, structErr := overlay.InitStruct(of.Overlay, renderNode, allNodes)
if structErr != nil {
return "", structErr
}
tstruct.BuildSource = of.Path
buffer, _, _, renderErr := overlay.RenderTemplateFile(of.FullPath(), tstruct)
if renderErr != nil {
return "", renderErr
}
return buffer.String(), nil
}
func NewOverlayFile(name string, path string, renderNodeName string) (*OverlayFile, error) {
of := new(OverlayFile)
of.Overlay = name
of.Path = path
if renderNodeName == "" {
if contents, err := of.readContents(); err != nil {
return of, err
} else {
of.Contents = contents
}
} else {
if contents, err := of.renderContents(renderNodeName); err != nil {
return of, err
} else {
of.Contents = contents
}
}
return of, nil
}
func getOverlayFile() usecase.Interactor {
type getOverlayFileInput struct {
Name string `path:"name" required:"true" description:"Name of overlay to get a file from"`
Path string `query:"path" required:"true" description:"Path to file to get from an overlay"`
Node string `query:"render" description:"ID of node to render a template for"`
}
u := usecase.NewInteractor(func(ctx context.Context, input getOverlayFileInput, output *OverlayFile) error {
wwlog.Debug("api.getOverlayFile(Name:%v, Path:%v, Node:%v)", input.Name, input.Path, input.Node)
if input.Path == "" {
return status.Wrap(fmt.Errorf("must specify a path"), status.InvalidArgument)
}
if relPath, err := url.QueryUnescape(input.Path); err != nil {
return fmt.Errorf("failed to decode path: %v: %w", input.Path, err)
} else {
if overlayFile, err := NewOverlayFile(input.Name, relPath, input.Node); err != nil {
return fmt.Errorf("unable to read overlay file %v: %v: %w", input.Name, relPath, err)
} else {
*output = *overlayFile
return nil
}
}
})
u.SetTitle("Get a file from an overlay")
u.SetDescription("Get a file from an overlay from the overlay name and file path, optionally rendered for a given node.")
u.SetTags("Overlay")
return u
}
func createOverlay() usecase.Interactor {
type createOverlayInput struct {
Name string `path:"name" required:"true" description:"Name of overlay to create"`
}
u := usecase.NewInteractor(func(ctx context.Context, input createOverlayInput, output *OverlayResponse) error {
wwlog.Debug("api.createOverlay(Name:%v)", input.Name)
newOverlay := overlay.GetSiteOverlay(input.Name)
if err := newOverlay.Create(); err != nil {
return err
}
*output = *NewOverlayResponse(newOverlay.Name())
return nil
})
u.SetTitle("Create an overlay")
u.SetDescription("Create an overlay.")
u.SetTags("Overlay")
return u
}
func deleteOverlay() usecase.Interactor {
type deleteOverlayInput struct {
Name string `path:"name" required:"true" description:"Name of overlay to delete"`
Force bool `query:"force" default:"false" description:"Whether to delete a non-empty overlay, default:'false'"`
}
u := usecase.NewInteractor(func(ctx context.Context, input deleteOverlayInput, output *OverlayResponse) error {
wwlog.Debug("api.deleteOverlay(Name:%v, Force:%v)", input.Name, input.Force)
if registry, err := node.New(); err != nil {
return err
} else {
nodesCount := len(registry.ListNodesUsingOverlay(input.Name))
profilesCount := len(registry.ListProfilesUsingOverlay(input.Name))
if nodesCount > 0 || profilesCount > 0 {
return status.Wrap(fmt.Errorf(
"overlay '%s' is in use by %v nodes and %v profiles", input.Name, nodesCount, profilesCount),
status.InvalidArgument)
}
}
*output = *NewOverlayResponse(input.Name)
overlay_ := overlay.GetSiteOverlay(input.Name)
if input.Force {
if err := os.RemoveAll(overlay_.Path()); err != nil {
return err
}
} else {
if err := os.Remove(overlay_.Path()); err != nil {
return err
}
}
return nil
})
u.SetTitle("Delete an overlay")
u.SetDescription("Delete an overlay.")
u.SetTags("Overlay")
return u
}