Files
warewulf/internal/pkg/warewulfd/api/image.go
Jonathon Anderson 3c895dd941 Refactor gRPC-based image functions
Signed-off-by: Jonathon Anderson <janderson@ciq.com>
2026-03-22 01:02:03 -06:00

200 lines
6.0 KiB
Go

package api
import (
"context"
"fmt"
"strings"
"github.com/swaggest/usecase"
"github.com/swaggest/usecase/status"
"github.com/warewulf/warewulf/internal/pkg/image"
"github.com/warewulf/warewulf/internal/pkg/kernel"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/warewulf/warewulf/internal/pkg/warewulfd"
"github.com/warewulf/warewulf/internal/pkg/wwlog"
)
type Image struct {
Kernels []string `json:"kernels"`
Size int `json:"size"`
BuildTime int64 `json:"buildtime"`
Writable bool `json:"writable"`
}
func NewImage(name string) *Image {
c := new(Image)
c.Kernels = []string{}
for _, k := range kernel.FindKernels(name) {
c.Kernels = append(c.Kernels, k.Path)
}
c.Size = image.ImageSize(name)
modTime := image.ImageModTime(name)
if modTime.IsZero() {
c.BuildTime = 0
} else {
c.BuildTime = modTime.Unix()
}
c.Writable = image.IsWriteAble(name)
return c
}
func getImages() usecase.Interactor {
u := usecase.NewInteractor(func(ctx context.Context, _ struct{}, output *map[string]*Image) error {
wwlog.Debug("api.getImages()")
m := make(map[string]*Image)
if names, err := image.ListSources(); err != nil {
return err
} else {
for _, name := range names {
m[name] = NewImage(name)
}
*output = m
return nil
}
})
u.SetTitle("Get images")
u.SetDescription("Get all OS images")
u.SetTags("Image")
return u
}
func getImageByName() usecase.Interactor {
type getImageByNameInput struct {
Name string `path:"name" required:"true" description:"Name of image to add"`
}
u := usecase.NewInteractor(func(ctx context.Context, input getImageByNameInput, output *Image) error {
wwlog.Debug("api.getImageByName(Name:%v)", input.Name)
if !image.ValidSource(input.Name) {
return status.Wrap(fmt.Errorf("image not found: %v", input.Name), status.NotFound)
} else {
*output = *NewImage(input.Name)
return nil
}
})
u.SetTitle("Get an image")
u.SetDescription("Get an OS image by its name")
u.SetTags("Image")
return u
}
func importImage() usecase.Interactor {
type importImageInput struct {
Name string `path:"name" required:"true" description:"Name of image to import"`
URI string `json:"uri" required:"true" description:"OCI registry URI to import image definition from"`
NoHttps bool `json:"nohttps" default:"false" description:"Use http, rather than https, to communicate with the registry, default:'false'"`
User string `json:"user" description:"Username for the registry, if needed"`
Password string `json:"password" description:"Password for the registry, if needed"`
}
u := usecase.NewInteractor(func(ctx context.Context, input importImageInput, output *Image) error {
wwlog.Debug("api.importImage(Name:%v, URI:%v, NoHttps:%v, User:%v, Password:[redacted])",
input.Name, input.URI, input.NoHttps, input.User)
if !strings.HasPrefix(input.URI, "docker://") {
return status.Wrap(fmt.Errorf("missing docker:// prefix: %s", input.URI), status.InvalidArgument)
}
if !image.ValidName(input.Name) {
return status.Wrap(fmt.Errorf("name contains illegal characters: %s", input.Name), status.InvalidArgument)
}
if sctx, err := image.GetSystemContext(input.NoHttps, input.User, input.Password, ""); err != nil {
return err
} else {
if err := image.ImportDocker(input.URI, input.Name, sctx); err != nil {
return err
}
*output = *NewImage(input.Name)
return nil
}
})
u.SetTitle("Import an image")
u.SetDescription("Import an OS image from an OCI registry")
u.SetTags("Image")
return u
}
func deleteImage() usecase.Interactor {
type deleteImageInput struct {
Name string `path:"name" required:"true" description:"Name of image to delete"`
}
u := usecase.NewInteractor(func(ctx context.Context, input deleteImageInput, output *Image) error {
wwlog.Debug("api.deleteImage(Name:%v)", input.Name)
if image.ValidSource(input.Name) {
*output = *NewImage(input.Name)
}
if registry, err := node.New(); err != nil {
return err
} else {
nodesCount := len(registry.ListNodesUsingImage(input.Name))
profilesCount := len(registry.ListProfilesUsingImage(input.Name))
if nodesCount > 0 || profilesCount > 0 {
return status.Wrap(fmt.Errorf(
"image '%s' is in use by %v nodes and %v profiles", input.Name, nodesCount, profilesCount),
status.InvalidArgument)
}
}
return image.Delete(input.Name)
})
u.SetTitle("Delete an image")
u.SetDescription("Delete an existing OS image")
u.SetTags("Image")
return u
}
func updateImage() usecase.Interactor {
type renameImageInput struct {
Name string `path:"name" required:"true" description:"Name of image to update"`
NewName string `json:"name" description:"New name to rename the image to"`
Build bool `query:"build" default:"true" description:"Build the OS image after renaming, default:'true'"`
}
u := usecase.NewInteractor(func(ctx context.Context, input renameImageInput, output *Image) error {
wwlog.Debug("api.updateImage(Name:%v, NewName:%v, Build:%v)", input.Name, input.NewName, input.Build)
name := input.Name
if input.NewName != "" {
if err := image.Rename(input.Name, input.NewName, input.Build); err != nil {
return err
}
name = input.NewName
warewulfd.Reload()
}
*output = *NewImage(name)
return nil
})
u.SetTitle("Update or rename an image")
u.SetDescription("Update or rename an existing OS image")
u.SetTags("Image")
return u
}
func buildImage() usecase.Interactor {
type buildImageInput struct {
Name string `path:"name" required:"true" description:"Name of image to build"`
Force bool `query:"force" default:"false" description:"Build the OS image even if it appears unnecessary, default:'false'"`
}
u := usecase.NewInteractor(func(ctx context.Context, input buildImageInput, output *Image) error {
wwlog.Debug("api.buildImage(Name:%v, Force:%v)", input.Name, input.Force)
if err := image.Build(input.Name, input.Force); err != nil {
return err
}
*output = *NewImage(input.Name)
return nil
})
u.SetTitle("Build an image")
u.SetDescription("Build an OS image")
u.SetTags("Image")
return u
}