HTTP PUT is supposed to be idempotent, so PUT for an existing node or profile should replace the node or profile, not throw an error. Generating an error on duplicate can now be requested using the If-None-Match header with a `*` value. Signed-off-by: Jonathon Anderson <janderson@ciq.com>
200 lines
6.5 KiB
Go
200 lines
6.5 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"dario.cat/mergo"
|
|
"github.com/swaggest/usecase"
|
|
"github.com/swaggest/usecase/status"
|
|
"github.com/warewulf/warewulf/internal/pkg/image"
|
|
"github.com/warewulf/warewulf/internal/pkg/node"
|
|
"github.com/warewulf/warewulf/internal/pkg/overlay"
|
|
"github.com/warewulf/warewulf/internal/pkg/warewulfd"
|
|
"github.com/warewulf/warewulf/internal/pkg/wwlog"
|
|
)
|
|
|
|
func getProfiles() usecase.Interactor {
|
|
u := usecase.NewInteractor(func(ctx context.Context, _ struct{}, output *map[string]*node.Profile) error {
|
|
wwlog.Debug("api.getProfiles()")
|
|
if registry, err := node.New(); err != nil {
|
|
return err
|
|
} else {
|
|
*output = registry.NodeProfiles
|
|
return nil
|
|
}
|
|
})
|
|
u.SetTitle("Get profiles")
|
|
u.SetDescription("Get all node profiles.")
|
|
u.SetTags("Profile")
|
|
return u
|
|
}
|
|
|
|
func getProfileByID() usecase.Interactor {
|
|
type getProfileByIDInput struct {
|
|
ID string `path:"id" required:"true" description:"ID of profile to get"`
|
|
}
|
|
|
|
u := usecase.NewInteractor(func(ctx context.Context, input getProfileByIDInput, output *node.Profile) error {
|
|
wwlog.Debug("api.getProfileByID(ID:%v)", input.ID)
|
|
if registry, err := node.New(); err != nil {
|
|
return err
|
|
} else {
|
|
if profile, err := registry.GetProfile(input.ID); err != nil {
|
|
return status.Wrap(fmt.Errorf("profile not found: %v (%v)", input.ID, err), status.NotFound)
|
|
} else {
|
|
*output = profile
|
|
return nil
|
|
}
|
|
}
|
|
})
|
|
u.SetTitle("Get a profile")
|
|
u.SetDescription("Get a node profile by its ID.")
|
|
u.SetTags("Profile")
|
|
u.SetExpectedErrors(status.NotFound)
|
|
return u
|
|
}
|
|
|
|
func addProfile() usecase.Interactor {
|
|
type addProfileInput struct {
|
|
ID string `path:"id" required:"true" description:"ID of profile to add"`
|
|
Profile node.Profile `json:"profile" required:"true" description:"Field values in JSON format for added profile"`
|
|
IfNoneMatch string `header:"If-None-Match" description:"Set to '*' to indicate that the profile should only be created if it does not already exist"`
|
|
}
|
|
|
|
u := usecase.NewInteractor(func(ctx context.Context, input addProfileInput, output *node.Profile) error {
|
|
wwlog.Debug("api.addProfile(ID:%v, Profile:%+v)", input.ID, input.Profile)
|
|
if registry, err := node.New(); err != nil {
|
|
return err
|
|
} else {
|
|
if input.IfNoneMatch == "*" {
|
|
if _, ok := registry.NodeProfiles[input.ID]; ok {
|
|
return status.Wrap(fmt.Errorf("profile '%s' already exists", input.ID), status.InvalidArgument)
|
|
}
|
|
}
|
|
for _, profile := range input.Profile.Profiles {
|
|
if _, ok := registry.NodeProfiles[profile]; !ok {
|
|
return status.Wrap(fmt.Errorf("profile '%s' does not exist", profile), status.InvalidArgument)
|
|
}
|
|
}
|
|
if input.Profile.ImageName != "" && !image.ValidSource(input.Profile.ImageName) {
|
|
return status.Wrap(fmt.Errorf("image '%s' does not exist", input.Profile.ImageName), status.InvalidArgument)
|
|
}
|
|
for _, overlay_ := range input.Profile.SystemOverlay {
|
|
if !overlay.GetOverlay(overlay_).Exists() {
|
|
return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument)
|
|
}
|
|
}
|
|
for _, overlay_ := range input.Profile.RuntimeOverlay {
|
|
if !overlay.GetOverlay(overlay_).Exists() {
|
|
return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument)
|
|
}
|
|
}
|
|
registry.NodeProfiles[input.ID] = &input.Profile
|
|
if err := registry.Persist(); err != nil {
|
|
return err
|
|
}
|
|
warewulfd.Reload()
|
|
*output = *(registry.NodeProfiles[input.ID])
|
|
return nil
|
|
}
|
|
})
|
|
u.SetTitle("Add a profile")
|
|
u.SetDescription("Add a new node profile.")
|
|
u.SetTags("Profile")
|
|
|
|
return u
|
|
}
|
|
|
|
func updateProfile() usecase.Interactor {
|
|
type updateProfileInput struct {
|
|
ID string `path:"id" required:"true" description:"ID of profile to update"`
|
|
Profile node.Profile `json:"profile" required:"true" description:"Field values in JSON format to update on profile"`
|
|
}
|
|
|
|
u := usecase.NewInteractor(func(ctx context.Context, input updateProfileInput, output *node.Profile) error {
|
|
wwlog.Debug("api.updateProfile(ID:%v, Profile:%+v)", input.ID, input.Profile)
|
|
if registry, err := node.New(); err != nil {
|
|
return err
|
|
} else {
|
|
for _, profile := range input.Profile.Profiles {
|
|
if _, ok := registry.NodeProfiles[profile]; !ok {
|
|
return status.Wrap(fmt.Errorf("profile '%s' does not exist", profile), status.InvalidArgument)
|
|
}
|
|
}
|
|
if input.Profile.ImageName != "" && !image.ValidSource(input.Profile.ImageName) {
|
|
return status.Wrap(fmt.Errorf("image '%s' does not exist", input.Profile.ImageName), status.InvalidArgument)
|
|
}
|
|
for _, overlay_ := range input.Profile.SystemOverlay {
|
|
if !overlay.GetOverlay(overlay_).Exists() {
|
|
return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument)
|
|
}
|
|
}
|
|
for _, overlay_ := range input.Profile.RuntimeOverlay {
|
|
if !overlay.GetOverlay(overlay_).Exists() {
|
|
return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument)
|
|
}
|
|
}
|
|
if profilePtr, err := registry.GetProfilePtr(input.ID); err != nil {
|
|
return status.Wrap(err, status.NotFound)
|
|
} else {
|
|
if err := mergo.MergeWithOverwrite(profilePtr, &input.Profile); err != nil {
|
|
return err
|
|
}
|
|
if err := registry.Persist(); err != nil {
|
|
return err
|
|
}
|
|
warewulfd.Reload()
|
|
*output = *profilePtr
|
|
return nil
|
|
}
|
|
}
|
|
})
|
|
u.SetTitle("Update a profile")
|
|
u.SetDescription("Update an existing node profile.")
|
|
u.SetTags("Profile")
|
|
|
|
return u
|
|
}
|
|
|
|
func deleteProfile() usecase.Interactor {
|
|
type deleteProfileInput struct {
|
|
ID string `path:"id" required:"true" description:"ID of profile to delete"`
|
|
}
|
|
|
|
u := usecase.NewInteractor(func(ctx context.Context, input deleteProfileInput, output *node.Profile) error {
|
|
wwlog.Debug("api.deleteProfile(ID:%v)", input.ID)
|
|
if registry, err := node.New(); err != nil {
|
|
return err
|
|
} else {
|
|
if profile, ok := registry.NodeProfiles[input.ID]; ok {
|
|
*output = *profile
|
|
}
|
|
|
|
nodesCount := len(registry.ListNodesUsingProfile(input.ID))
|
|
profilesCount := len(registry.ListProfilesUsingProfile(input.ID))
|
|
if nodesCount > 0 || profilesCount > 0 {
|
|
return status.Wrap(fmt.Errorf(
|
|
"profile '%s' is in use by %v nodes and %v profiles", input.ID, nodesCount, profilesCount),
|
|
status.InvalidArgument)
|
|
}
|
|
|
|
if err := registry.DelProfile(input.ID); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := registry.Persist(); err != nil {
|
|
return err
|
|
}
|
|
|
|
warewulfd.Reload()
|
|
return nil
|
|
}
|
|
})
|
|
u.SetTitle("Delete a profile")
|
|
u.SetDescription("Delete an existing node profile.")
|
|
u.SetTags("Profile")
|
|
|
|
return u
|
|
}
|