package api import ( "context" "fmt" "net/url" "os" "path" "syscall" "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{} myOverlay, err := overlay.Get(name) if err == nil { if files, err := myOverlay.GetFiles(); err == nil { o.Files = files } o.Site = myOverlay.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 _, err := overlay.Get(input.Name); err != nil { 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"` Perms os.FileMode `json:"perms"` Uid uint32 `json:"uid"` Gid uint32 `json:"gid"` rendered bool } func (of *OverlayFile) FullPath() string { if myOverlay, err := overlay.Get(of.Overlay); err != nil { return "" } else { return myOverlay.File(of.Path) } } func (of *OverlayFile) Exists() bool { if myOverlay, err := overlay.Get(of.Overlay); err != nil { return false } else { return myOverlay.Exists() && util.IsFile(of.FullPath()) } } func (of *OverlayFile) readContents() (string, error) { fullPath := of.FullPath() f, err := os.ReadFile(fullPath) if err != nil { wwlog.Warn("os.ReadFile err %w", err) return "", err } // Populate the permissions, uid, and gid. s, err := os.Stat(fullPath) if err != nil { wwlog.Warn("os.Stat err %w", err) return "", err } fileMode := s.Mode() of.Perms = fileMode & os.ModePerm sys := s.Sys() of.Uid = sys.(*syscall.Stat_t).Uid of.Gid = sys.(*syscall.Stat_t).Gid 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) if _, err := overlay.Get(input.Name); err == nil { // existing overlay, return a conflict error return status.Wrap(fmt.Errorf("%s overlay already exists", input.Name), status.AlreadyExists) } newOverlay, err := overlay.Create(input.Name) if 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 or in-use 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) overlay_, err := overlay.Get(input.Name) if err != nil { return err } if overlay_.IsDistributionOverlay() { return status.Wrap(fmt.Errorf("distribution overlay: %s", overlay_.Name()), status.InvalidArgument) } if !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) } } } if err := overlay_.Delete(input.Force); err != nil { return status.Wrap(fmt.Errorf("failed to remove overlay: %v", err), status.Code(409)) } *output = *NewOverlayResponse(input.Name) output.Site = overlay_.IsSiteOverlay() return nil }) u.SetTitle("Delete an overlay") u.SetDescription("Delete an overlay.") u.SetTags("Overlay") return u } func deleteOverlayFile() usecase.Interactor { type deleteOverlayFileInput struct { Name string `path:"name" required:"true" description:"Name of overlay to delete a file from"` Path string `query:"path" required:"true" description:"Path to file to delete from an overlay"` Force bool `query:"force" default:"false" description:"Whether to forcefully delete an overlay file, default:'false'"` Cleanup bool `query:"cleanup" default:"false" description:"Whether to cleanup empty parent directories, default:'false'"` } u := usecase.NewInteractor(func(ctx context.Context, input deleteOverlayFileInput, output *OverlayResponse) error { wwlog.Debug("api.deleteOverlayFile(Name:%v, Path:%v)", input.Name, input.Path) 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 overlay_, err := overlay.Get(input.Name); err != nil { return err } else { if err := overlay_.DeleteFile(relPath, input.Force, input.Cleanup); err != nil { return fmt.Errorf("unable to delete overlay file %v: %v: %w", input.Name, relPath, err) } } } *output = *NewOverlayResponse(input.Name) return nil }) u.SetTitle("Delete a file from an overlay") u.SetDescription("Delete a file from an overlay from the overlay name and file path") u.SetTags("Overlay") return u } func addOverlayFile() usecase.Interactor { type createOverlayFileInput struct { Name string `path:"name" required:"true" description:"Name of overlay to add a file to"` Path string `query:"path" required:"true" description:"Path to file to create in the overlay"` Content string `json:"content" required:"true" description:"Content of the file to create"` IfNoneMatch string `header:"If-None-Match" description:"Set to '*' to indicate that the file should only be added if it does not already exist"` } u := usecase.NewInteractor(func(ctx context.Context, input createOverlayFileInput, output *OverlayResponse) error { wwlog.Debug("api.addOverlayFile(Name:%v, Path:%v, IfNoneMatch: %v)", input.Name, input.Path, input.IfNoneMatch) 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 { overlay_, err := overlay.Get(input.Name) if err != nil { if _, err := overlay.Create(input.Name); err != nil { return err } } if err := overlay_.AddFile(relPath, []byte(input.Content), true, input.IfNoneMatch != "*"); err != nil { return fmt.Errorf("unable to add overlay file %v: %v: %w", input.Name, relPath, err) } *output = *NewOverlayResponse(input.Name) return nil } }) u.SetTitle("Add a file to an overlay") u.SetDescription("Add a file to an overlay from the overlay name and file path.") u.SetTags("Overlay") return u }