mirror of
https://github.com/podman-container-tools/podman.git
synced 2026-08-05 00:15:44 +00:00
Quadlet installation code refactoring
- Cleanup the code to install quadlets - Fix `podman quadlet install` output message (see https://github.com/podman-container-tools/podman/pull/28335#discussion_r3310372000) - Update libpod quadlet endpoint documentation Signed-off-by: Mario Loriedo <mario.loriedo@gmail.com>
This commit is contained in:
parent
5cc79ac325
commit
d1e2692069
5 changed files with 249 additions and 173 deletions
|
|
@ -84,8 +84,9 @@ func (s *APIServer) registerQuadletHandlers(r *mux.Router) error {
|
|||
// summary: Install quadlet files
|
||||
// description: |
|
||||
// Install one or more files for a quadlet application. Each request should contain a single quadlet file
|
||||
// and optionally more files such as containerfile, kube yaml or configuration files. Supports both tar
|
||||
// archives and multipart form data uploads.
|
||||
// and optionally more files such as containerfile, kube yaml or configuration files. When additional
|
||||
// files are passed, the application query parameter should be specified. Supports both tar archives and
|
||||
// multipart form data uploads.
|
||||
// consumes:
|
||||
// - application/x-tar
|
||||
// - multipart/form-data
|
||||
|
|
@ -93,6 +94,12 @@ func (s *APIServer) registerQuadletHandlers(r *mux.Router) error {
|
|||
// - application/json
|
||||
// parameters:
|
||||
// - in: query
|
||||
// name: application
|
||||
// type: string
|
||||
// description: |
|
||||
// Group quadlet and associated files in a directory with the application name.
|
||||
// Required when additional files are passed.
|
||||
// - in: query
|
||||
// name: replace
|
||||
// type: boolean
|
||||
// default: false
|
||||
|
|
|
|||
|
|
@ -27,19 +27,23 @@ import (
|
|||
|
||||
// Install one or more Quadlet files
|
||||
func (ic *ContainerEngine) QuadletInstall(ctx context.Context, pathsOrURLs []string, options entities.QuadletInstallOptions) (*entities.QuadletInstallReport, error) {
|
||||
// Is systemd available to the current user?
|
||||
// We cannot proceed if not.
|
||||
// Fail if quadlet files list is empty
|
||||
if len(pathsOrURLs) == 0 {
|
||||
return nil, fmt.Errorf("at least one quadlet file path needed")
|
||||
}
|
||||
|
||||
// Fail if systemd isn't available to the current user
|
||||
conn, err := systemd.ConnectToDBUS()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connecting to systemd dbus: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Fail if the Quadlet binary cannot be found
|
||||
cfg, err := config.Default()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to load default config: %w", err)
|
||||
}
|
||||
|
||||
// Is Quadlet installed? No point if not.
|
||||
quadletPath, err := cfg.FindHelperBinary("quadlet", true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot stat Quadlet generator, Quadlet may not be installed: %w", err)
|
||||
|
|
@ -51,157 +55,129 @@ func (ic *ContainerEngine) QuadletInstall(ctx context.Context, pathsOrURLs []str
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot stat Quadlet generator, Quadlet may not be installed: %w", err)
|
||||
}
|
||||
|
||||
if !quadletStat.Mode().IsRegular() || quadletStat.Mode()&0o100 == 0 {
|
||||
return nil, fmt.Errorf("no valid Quadlet binary installed to %q, unable to use Quadlet", quadletPath)
|
||||
}
|
||||
|
||||
// Set installDir (quadlets target directory)
|
||||
installDir := systemdquadlet.GetInstallUnitDirPath(rootless.IsRootless())
|
||||
|
||||
if len(options.Application) > 0 {
|
||||
// Prevent path traversal by validating the user input "Application"
|
||||
err := validateApplicationName(installDir, options.Application)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid application name: %w", err)
|
||||
}
|
||||
|
||||
installDir = filepath.Join(installDir, options.Application)
|
||||
}
|
||||
|
||||
logrus.Debugf("Going to install Quadlet to directory %s", installDir)
|
||||
|
||||
if err := os.MkdirAll(installDir, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("unable to create Quadlet install path %s: %w", installDir, err)
|
||||
}
|
||||
|
||||
type qpaths struct {
|
||||
src string
|
||||
dst string
|
||||
}
|
||||
var quadletPaths []qpaths
|
||||
var quadletURLs, nestedQuadletPaths []string
|
||||
firstArg := pathsOrURLs[0]
|
||||
switch {
|
||||
case isFolder(firstArg):
|
||||
if options.Application == "" {
|
||||
return nil, fmt.Errorf("application name cannot be empty when installing from directory")
|
||||
}
|
||||
nestedQuadletPaths, err = findNestedQuadlets(firstArg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed finding quadlet files in folder %q: %w", firstArg, err)
|
||||
}
|
||||
case isURL(firstArg):
|
||||
quadletURLs = append(quadletURLs, firstArg)
|
||||
default:
|
||||
quadletPaths = append(quadletPaths, qpaths{firstArg, filepath.Join(installDir, filepath.Base(firstArg))})
|
||||
}
|
||||
otherArgs := pathsOrURLs[1:]
|
||||
for _, pathOrURL := range otherArgs {
|
||||
if isURL(pathOrURL) {
|
||||
quadletURLs = append(quadletURLs, pathOrURL)
|
||||
} else {
|
||||
quadletPaths = append(quadletPaths, qpaths{pathOrURL, filepath.Join(installDir, filepath.Base(pathOrURL))})
|
||||
}
|
||||
}
|
||||
|
||||
// Process the quadlet lists
|
||||
installReport := entities.QuadletInstallReport{
|
||||
InstalledQuadlets: make(map[string]string),
|
||||
QuadletErrors: make(map[string]error),
|
||||
}
|
||||
|
||||
paths := pathsOrURLs
|
||||
if len(pathsOrURLs) > 0 && !strings.HasPrefix(pathsOrURLs[0], "http://") && !strings.HasPrefix(pathsOrURLs[0], "https://") {
|
||||
// Check if first path is dir, this is an APP
|
||||
info, err := os.Stat(pathsOrURLs[0])
|
||||
for _, nestedPath := range nestedQuadletPaths {
|
||||
// `nestedQuadletPaths` are files under folder
|
||||
// `firstArg` or one of its subfolders. These files
|
||||
// need to be installed under folder `installDir`.
|
||||
// For example file `firstArg + "foo/bar"` needs
|
||||
// to be installed in `installDir + "foo/bar".
|
||||
// For this reason we need to get the relative
|
||||
// path ("foo/bar") and pass it to `installQuadlet`
|
||||
nestedPathRel, err := filepath.Rel(firstArg, nestedPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to stat Quadlet path %s: %w", pathsOrURLs[0], err)
|
||||
}
|
||||
if info.IsDir() {
|
||||
if len(options.Application) == 0 {
|
||||
return nil, fmt.Errorf("application name cannot be empty when installing from directory")
|
||||
}
|
||||
|
||||
// If it's a directory, then read all files and add it to paths
|
||||
entries, err := os.ReadDir(pathsOrURLs[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read Quadlet dir %s: %w", pathsOrURLs[0], err)
|
||||
}
|
||||
redoPaths := make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
redoPaths = append(redoPaths, filepath.Join(pathsOrURLs[0], entry.Name()))
|
||||
}
|
||||
redoPaths = append(redoPaths, pathsOrURLs[1:]...)
|
||||
paths = redoPaths
|
||||
} else if !systemdquadlet.IsExtSupported(pathsOrURLs[0]) &&
|
||||
filepath.Ext(pathsOrURLs[0]) != ".quadlets" {
|
||||
return nil, fmt.Errorf("%q is not a supported Quadlet file type", filepath.Ext(pathsOrURLs[0]))
|
||||
installReport.QuadletErrors[nestedPath] = err
|
||||
continue
|
||||
}
|
||||
quadletPaths = append(quadletPaths, qpaths{nestedPath, filepath.Join(installDir, nestedPathRel)})
|
||||
}
|
||||
|
||||
// Loop over all given URLs
|
||||
for _, toInstall := range paths {
|
||||
// Loop over the URLs
|
||||
for _, quadletURL := range quadletURLs {
|
||||
installedPath, err := installQuadletFromURL(ctx, ic, quadletURL, installDir, options.Replace)
|
||||
if err != nil {
|
||||
installReport.QuadletErrors[quadletURL] = err
|
||||
continue
|
||||
}
|
||||
installReport.InstalledQuadlets[quadletURL] = installedPath
|
||||
}
|
||||
// Loop over the paths
|
||||
for _, quadletPath := range quadletPaths {
|
||||
err = fileutils.Exists(quadletPath.src)
|
||||
if err != nil {
|
||||
installReport.QuadletErrors[quadletPath.src] = err
|
||||
continue
|
||||
}
|
||||
quadletExt := filepath.Ext(quadletPath.src)
|
||||
// Check if this file is a .quadlets file
|
||||
switch {
|
||||
case strings.HasPrefix(toInstall, "http://") || strings.HasPrefix(toInstall, "https://"):
|
||||
r, err := http.Get(toInstall)
|
||||
case quadletExt == ".quadlets":
|
||||
// Parse the multi-quadlet file
|
||||
sections, err := parseMultiQuadletFile(quadletPath.src)
|
||||
if err != nil {
|
||||
installReport.QuadletErrors[toInstall] = fmt.Errorf("unable to download URL %s: %w", toInstall, err)
|
||||
installReport.QuadletErrors[quadletPath.src] = err
|
||||
continue
|
||||
}
|
||||
defer r.Body.Close()
|
||||
quadletFileName, err := getFileName(r, toInstall)
|
||||
if err != nil {
|
||||
installReport.QuadletErrors[toInstall] = fmt.Errorf("unable to get file name from url %s: %w", toInstall, err)
|
||||
continue
|
||||
}
|
||||
// It's a URL. Pull to temporary file.
|
||||
tmpFile, err := os.CreateTemp("", quadletFileName)
|
||||
if err != nil {
|
||||
installReport.QuadletErrors[toInstall] = fmt.Errorf("unable to create temporary file to download URL %s: %w", toInstall, err)
|
||||
continue
|
||||
}
|
||||
defer func() {
|
||||
tmpFile.Close()
|
||||
if err := os.Remove(tmpFile.Name()); err != nil {
|
||||
logrus.Errorf("unable to remove temporary file %q: %v", tmpFile.Name(), err)
|
||||
// The sections installation folder can be different
|
||||
// than the root `installDir`. For example if the .quadlets
|
||||
// file is part of an application and is in a subdirectory.
|
||||
sectionsDestDir := filepath.Dir(quadletPath.dst)
|
||||
// Install each quadlet section as a separate file
|
||||
for _, section := range sections {
|
||||
installedPath, err := installMultiQuadletSection(ctx, ic, section, sectionsDestDir, options.Replace)
|
||||
if err != nil {
|
||||
installReport.QuadletErrors[quadletPath.src] = fmt.Errorf("unable to install multi-quadlet section %w", err)
|
||||
continue
|
||||
}
|
||||
}()
|
||||
_, err = io.Copy(tmpFile, r.Body)
|
||||
// Record the installation (use a unique key for each section)
|
||||
sectionKey := fmt.Sprintf("%s#%s", quadletPath.src, filepath.Base(installedPath))
|
||||
installReport.InstalledQuadlets[sectionKey] = installedPath
|
||||
}
|
||||
case systemdquadlet.IsExtSupported(quadletPath.src) ||
|
||||
options.Application != "":
|
||||
// If quadletPath is a single file with a supported extension, or
|
||||
// if it isn't but it's part of an application, execute the original logic
|
||||
installedPath, err := ic.installQuadlet(ctx, quadletPath.src, quadletPath.dst, options.Replace)
|
||||
if err != nil {
|
||||
installReport.QuadletErrors[toInstall] = fmt.Errorf("populating temporary file: %w", err)
|
||||
installReport.QuadletErrors[quadletPath.src] = err
|
||||
continue
|
||||
}
|
||||
installedPath, err := ic.installQuadlet(ctx, tmpFile.Name(), quadletFileName, installDir, options.Replace)
|
||||
if err != nil {
|
||||
installReport.QuadletErrors[toInstall] = err
|
||||
continue
|
||||
}
|
||||
installReport.InstalledQuadlets[toInstall] = installedPath
|
||||
installReport.InstalledQuadlets[quadletPath.src] = installedPath
|
||||
default:
|
||||
err := fileutils.Exists(toInstall)
|
||||
if err != nil {
|
||||
installReport.QuadletErrors[toInstall] = err
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this file has a supported extension or is a .quadlets file
|
||||
isQuadletsFile := filepath.Ext(toInstall) == ".quadlets"
|
||||
|
||||
if isQuadletsFile {
|
||||
// Parse the multi-quadlet file
|
||||
quadlets, err := parseMultiQuadletFile(toInstall)
|
||||
if err != nil {
|
||||
installReport.QuadletErrors[toInstall] = err
|
||||
continue
|
||||
}
|
||||
|
||||
// Install each quadlet section as a separate file
|
||||
for _, quadlet := range quadlets {
|
||||
// Create a temporary file for this quadlet section
|
||||
tmpFile, err := os.CreateTemp("", quadlet.name+"*"+quadlet.extension)
|
||||
if err != nil {
|
||||
installReport.QuadletErrors[toInstall] = fmt.Errorf("unable to create temporary file for quadlet section %s: %w", quadlet.name, err)
|
||||
continue
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
// Write the quadlet content to the temporary file
|
||||
_, err = tmpFile.WriteString(quadlet.content)
|
||||
tmpFile.Close()
|
||||
if err != nil {
|
||||
installReport.QuadletErrors[toInstall] = fmt.Errorf("unable to write quadlet section %s to temporary file: %w", quadlet.name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Install the quadlet from the temporary file
|
||||
destName := quadlet.name + quadlet.extension
|
||||
installedPath, err := ic.installQuadlet(ctx, tmpFile.Name(), destName, installDir, options.Replace)
|
||||
if err != nil {
|
||||
installReport.QuadletErrors[toInstall] = fmt.Errorf("unable to install quadlet section %s: %w", destName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Record the installation (use a unique key for each section)
|
||||
sectionKey := fmt.Sprintf("%s#%s", toInstall, destName)
|
||||
installReport.InstalledQuadlets[sectionKey] = installedPath
|
||||
}
|
||||
} else {
|
||||
// If toInstall is a single file with a supported extension, execute the original logic
|
||||
installedPath, err := ic.installQuadlet(ctx, toInstall, filepath.Base(toInstall), installDir, options.Replace)
|
||||
if err != nil {
|
||||
installReport.QuadletErrors[toInstall] = err
|
||||
continue
|
||||
}
|
||||
installReport.InstalledQuadlets[toInstall] = installedPath
|
||||
}
|
||||
installReport.QuadletErrors[quadletPath.src] = fmt.Errorf("unsupported quadlet extension (%q)", quadletExt)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -215,6 +191,90 @@ func (ic *ContainerEngine) QuadletInstall(ctx context.Context, pathsOrURLs []str
|
|||
return &installReport, nil
|
||||
}
|
||||
|
||||
func installQuadletFromURL(ctx context.Context, ic *ContainerEngine, quadletURL string, installDir string, replace bool) (string, error) {
|
||||
r, err := http.Get(quadletURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to download URL %s: %w", quadletURL, err)
|
||||
}
|
||||
defer r.Body.Close()
|
||||
quadletFileName, err := getFileName(r, quadletURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to get file name from url %s: %w", quadletURL, err)
|
||||
}
|
||||
// It's a URL. Pull to temporary file.
|
||||
tmpFile, err := os.CreateTemp("", quadletFileName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to create temporary file to download URL %s: %w", quadletURL, err)
|
||||
}
|
||||
defer func() {
|
||||
tmpFile.Close()
|
||||
if err := os.Remove(tmpFile.Name()); err != nil {
|
||||
logrus.Errorf("unable to remove temporary file %q: %v", tmpFile.Name(), err)
|
||||
}
|
||||
}()
|
||||
_, err = io.Copy(tmpFile, r.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("populating temporary file: %w", err)
|
||||
}
|
||||
return ic.installQuadlet(ctx, tmpFile.Name(), filepath.Join(installDir, quadletFileName), replace)
|
||||
}
|
||||
|
||||
func installMultiQuadletSection(ctx context.Context, ic *ContainerEngine, section quadletSection, installDir string, replace bool) (string, error) {
|
||||
tmpFile, err := os.CreateTemp("", section.name+"*"+section.extension)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to create temporary file for quadlet section %s: %w", section.name, err)
|
||||
}
|
||||
defer func() {
|
||||
tmpFile.Close()
|
||||
if err := os.Remove(tmpFile.Name()); err != nil {
|
||||
logrus.Errorf("unable to remove temporary file %q: %v", tmpFile.Name(), err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Write the quadlet content to the temporary file
|
||||
_, err = tmpFile.WriteString(section.content)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to write quadlet section %s to temporary file: %w", section.name, err)
|
||||
}
|
||||
// Install the quadlet from the temporary file
|
||||
destName := section.name + section.extension
|
||||
return ic.installQuadlet(ctx, tmpFile.Name(), filepath.Join(installDir, destName), replace)
|
||||
}
|
||||
|
||||
func isFolder(s string) bool {
|
||||
if isURL(s) {
|
||||
return false
|
||||
}
|
||||
info, err := os.Stat(s)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return info.IsDir()
|
||||
}
|
||||
|
||||
func isURL(s string) bool {
|
||||
return strings.HasPrefix(s, "http://") ||
|
||||
strings.HasPrefix(s, "https://")
|
||||
}
|
||||
|
||||
func findNestedQuadlets(folderPath string) ([]string, error) {
|
||||
// If it's a directory, then read all files and add it to paths
|
||||
quadletPaths := make([]string, 0)
|
||||
err := filepath.WalkDir(folderPath, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read Quadlet dir %s: %w", path, err)
|
||||
}
|
||||
if !d.IsDir() {
|
||||
quadletPaths = append(quadletPaths, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return quadletPaths, nil
|
||||
}
|
||||
|
||||
// Extracts file name from Content-Disposition or URL
|
||||
func getFileName(resp *http.Response, fileURL string) (string, error) {
|
||||
// Try to get filename from Content-Disposition header
|
||||
|
|
@ -240,7 +300,7 @@ func getFileName(resp *http.Response, fileURL string) (string, error) {
|
|||
// Perform some minimal validation, but not much.
|
||||
// We can't know about a lot of problems without running the Quadlet binary, which we
|
||||
// only want to do once.
|
||||
func (ic *ContainerEngine) installQuadlet(ctx context.Context, path, destName, installDir string, replace bool) (string, error) {
|
||||
func (ic *ContainerEngine) installQuadlet(ctx context.Context, srcPath, destPath string, replace bool) (string, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", fmt.Errorf("context cancelled: %w", ctx.Err())
|
||||
|
|
@ -248,57 +308,31 @@ func (ic *ContainerEngine) installQuadlet(ctx context.Context, path, destName, i
|
|||
}
|
||||
|
||||
// First, validate that the source path exists and is a file
|
||||
stat, err := os.Stat(path)
|
||||
_, err := os.Stat(srcPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("quadlet to install %q does not exist or cannot be read: %w", path, err)
|
||||
}
|
||||
if stat.IsDir() {
|
||||
dirs, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, d := range dirs {
|
||||
nInstallDir := filepath.Join(installDir, destName)
|
||||
err := os.MkdirAll(nInstallDir, 0o755)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
_, err = ic.installQuadlet(
|
||||
ctx,
|
||||
filepath.Join(path, d.Name()), // path
|
||||
d.Name(), // destName
|
||||
nInstallDir, // installDir
|
||||
replace)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return path, nil
|
||||
return "", fmt.Errorf("quadlet to install %q does not exist or cannot be read: %w", srcPath, err)
|
||||
}
|
||||
|
||||
finalPath := filepath.Join(installDir, filepath.Base(filepath.Clean(path)))
|
||||
if destName != "" {
|
||||
finalPath = filepath.Join(installDir, destName)
|
||||
// Second, create the destPath folder as it may not exist yet
|
||||
if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil {
|
||||
return "", fmt.Errorf("unable to create Quadlet install path %s: %w", destPath, err)
|
||||
}
|
||||
|
||||
var destFile *os.File
|
||||
var tempPath string
|
||||
|
||||
if !replace {
|
||||
var err error
|
||||
// O_EXCL ensures we fail if the file already exists (avoids TOCTOU race)
|
||||
destFile, err = os.OpenFile(finalPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o644)
|
||||
destFile, err = os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o644)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrExist) {
|
||||
return "", fmt.Errorf("a Quadlet with name %s already exists, refusing to overwrite", filepath.Base(finalPath))
|
||||
return "", fmt.Errorf("a Quadlet with name %s already exists, refusing to overwrite", filepath.Base(destPath))
|
||||
}
|
||||
return "", fmt.Errorf("unable to open file %s: %w", finalPath, err)
|
||||
return "", fmt.Errorf("unable to open file %s: %w", destPath, err)
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
destFile, err = os.CreateTemp(filepath.Dir(finalPath), ".quadlet-install-*")
|
||||
destFile, err = os.CreateTemp(filepath.Dir(destPath), ".quadlet-install-*")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to create temp file: %w", err)
|
||||
}
|
||||
|
|
@ -314,7 +348,7 @@ func (ic *ContainerEngine) installQuadlet(ctx context.Context, path, destName, i
|
|||
}
|
||||
}()
|
||||
|
||||
srcFile, err := os.Open(path)
|
||||
srcFile, err := os.Open(srcPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to open file: %w", err)
|
||||
}
|
||||
|
|
@ -322,7 +356,7 @@ func (ic *ContainerEngine) installQuadlet(ctx context.Context, path, destName, i
|
|||
|
||||
err = fileutils.ReflinkOrCopy(srcFile, destFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("unable to copy file from %s to %s: %w", path, finalPath, err)
|
||||
return "", fmt.Errorf("unable to copy file from %s to %s: %w", srcPath, destPath, err)
|
||||
}
|
||||
|
||||
// Close before rename to flush writes; nil out to prevent double-close in defer
|
||||
|
|
@ -336,12 +370,12 @@ func (ic *ContainerEngine) installQuadlet(ctx context.Context, path, destName, i
|
|||
return "", fmt.Errorf("unable to set permissions on temp file: %w", err)
|
||||
}
|
||||
|
||||
if err := os.Rename(tempPath, finalPath); err != nil {
|
||||
return "", fmt.Errorf("unable to rename temp file to %s: %w", finalPath, err)
|
||||
if err := os.Rename(tempPath, destPath); err != nil {
|
||||
return "", fmt.Errorf("unable to rename temp file to %s: %w", destPath, err)
|
||||
}
|
||||
tempPath = ""
|
||||
}
|
||||
return finalPath, nil
|
||||
return destPath, nil
|
||||
}
|
||||
|
||||
// quadletSection represents a single quadlet extracted from a multi-quadlet file
|
||||
|
|
|
|||
|
|
@ -281,27 +281,27 @@ EOF
|
|||
echo "$quadlet_6_content" > "$TMPD/$quadlet_6"
|
||||
echo "$containerfile_2_content" > "$TMPD/$containerfile_2"
|
||||
|
||||
t POST "libpod/quadlets" --form="quadlet_6=@$TMPD/$quadlet_6" --form="containerfile_2=@$TMPD/$containerfile_2" 200
|
||||
t POST "libpod/quadlets?application=foo" --form="quadlet_6=@$TMPD/$quadlet_6" --form="containerfile_2=@$TMPD/$containerfile_2" 200
|
||||
|
||||
t GET "libpod/quadlets/$quadlet_6/file" 200
|
||||
is "$output" "$quadlet_6_content" "quadlet-6 should be installed"
|
||||
is "$(cat "$quadlet_install_dir/$containerfile_2")" "$containerfile_2_content" "containerfile_2 should be installed"
|
||||
is "$(cat "$quadlet_install_dir/foo/$containerfile_2")" "$containerfile_2_content" "containerfile_2 should be installed"
|
||||
|
||||
# update with no replace and check nothing changed
|
||||
echo "$quadlet_6_updated_content" > "$TMPD/$quadlet_6"
|
||||
echo "$containerfile_2_updated_content" > "$TMPD/$containerfile_2"
|
||||
t POST "libpod/quadlets" --form="quadlet_6=@$TMPD/$quadlet_6" --form="containerfile_2=@$TMPD/$containerfile_2" 400
|
||||
t POST "libpod/quadlets?application=foo" --form="quadlet_6=@$TMPD/$quadlet_6" --form="containerfile_2=@$TMPD/$containerfile_2" 400
|
||||
|
||||
t GET "libpod/quadlets/$quadlet_6/file" 200
|
||||
is "$output" "$quadlet_6_content" "quadlet-6 should not be updated"
|
||||
is "$(cat "$quadlet_install_dir/$containerfile_2")" "$containerfile_2_content" "containerfile_2 should not be updated"
|
||||
is "$(cat "$quadlet_install_dir/foo/$containerfile_2")" "$containerfile_2_content" "containerfile_2 should not be updated"
|
||||
|
||||
# replace
|
||||
t POST "libpod/quadlets?replace=true" --form="quadlet_6=@$TMPD/$quadlet_6" --form="containerfile_2=@$TMPD/$containerfile_2" 200
|
||||
t POST "libpod/quadlets?application=foo&replace=true" --form="quadlet_6=@$TMPD/$quadlet_6" --form="containerfile_2=@$TMPD/$containerfile_2" 200
|
||||
|
||||
t GET "libpod/quadlets/$quadlet_6/file" 200
|
||||
is "$output" "$quadlet_6_updated_content" "quadlet-6 should be updated"
|
||||
is "$(cat "$quadlet_install_dir/$containerfile_2")" "$containerfile_2_updated_content" "containerfile_2 should be updated"
|
||||
is "$(cat "$quadlet_install_dir/foo/$containerfile_2")" "$containerfile_2_updated_content" "containerfile_2 should be updated"
|
||||
|
||||
|
||||
# Scenario: install and remove quadlets as application
|
||||
|
|
|
|||
|
|
@ -260,6 +260,14 @@ EOF
|
|||
Image=$IMAGE
|
||||
Environment=FOO1=foo1
|
||||
Exec=sh -c "echo STARTED NGINX; trap 'exit' SIGTERM; while :; do sleep 0.1; done"
|
||||
EOF
|
||||
|
||||
mkdir $quadlet_dir/sub
|
||||
cat > $quadlet_dir/sub/nginxsub.container <<EOF
|
||||
[Container]
|
||||
Image=$IMAGE
|
||||
Environment=FOO2=foo2
|
||||
Exec=sh -c "echo STARTED NGINX SUB; trap 'exit' SIGTERM; while :; do sleep 0.1; done"
|
||||
EOF
|
||||
|
||||
# Without --application should fail
|
||||
|
|
@ -268,18 +276,21 @@ EOF
|
|||
|
||||
# Test quadlet install with directory
|
||||
run_podman quadlet install --application=foo $quadlet_dir
|
||||
assert "$output" =~ "nginxsub.container" "install should list nginxsub that is in a subfolder"
|
||||
|
||||
# Test quadlet list to verify all containers were installed
|
||||
run_podman quadlet list
|
||||
assert "$output" =~ "alpine1.container" "list should contain alpine1.container"
|
||||
assert "$output" =~ "alpine2.container" "list should contain alpine2.container"
|
||||
assert "$output" =~ "nginx.container" "list should contain nginx.container"
|
||||
assert "$output" =~ "nginxsub.container" "list should contain nginxsub.container"
|
||||
|
||||
# Test quadlet list with filter for alpine containers
|
||||
run_podman quadlet list --filter name=alpine*
|
||||
assert "$output" =~ "alpine1.container" "filtered list should contain alpine1.container"
|
||||
assert "$output" =~ "alpine2.container" "filtered list should contain alpine2.container"
|
||||
assert "$output" !~ "nginx.container" "filtered list should not contain nginx.container"
|
||||
assert "$output" !~ "nginxsub.container" "filtered list should not contain nginxsub.container"
|
||||
|
||||
# Test quadlet print for each container
|
||||
run_podman quadlet print alpine1.container
|
||||
|
|
@ -291,6 +302,9 @@ EOF
|
|||
run_podman quadlet print nginx.container
|
||||
assert "$output" =~ "Environment=FOO1=foo1" "print should contain environment for nginx container"
|
||||
|
||||
run_podman quadlet print nginxsub.container
|
||||
assert "$output" =~ "Environment=FOO2=foo2" "print should contain environment for nginxsub container"
|
||||
|
||||
# Test quadlet rm using one quadlet file name without recursive (should fail)
|
||||
run_podman 125 quadlet rm "alpine1.container"
|
||||
assert "$output" =~ "recursive option is not set" "rm application without --recursive must fail"
|
||||
|
|
@ -401,7 +415,7 @@ EOF
|
|||
run_podman quadlet rm --recursive mount-test.container
|
||||
|
||||
# Verify the test.txt file should not exists in $install_dir
|
||||
if [[ -f "$install_dir/test.txt" ]]; then
|
||||
if [[ -f "$install_dir/bar/test.txt" ]]; then
|
||||
die "test.txt file should not exist in install directory $install_dir after removal"
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -260,6 +260,7 @@ EOF
|
|||
local api_name="api-server_$(random_string)"
|
||||
local cache_name="cache_$(random_string)"
|
||||
local network_name="app-network_$(random_string)"
|
||||
local nested_server_name="nested-server_$(random_string)"
|
||||
|
||||
# Create an individual container quadlet file
|
||||
cat > "$app_dir/${frontend_name}.container" <<EOF
|
||||
|
|
@ -307,18 +308,30 @@ debug=true
|
|||
port=3000
|
||||
EOF
|
||||
|
||||
# Create a nested .quadlets file
|
||||
mkdir "$app_dir/a"
|
||||
cat > "$app_dir/a/backend_$(random_string).quadlets" <<EOF
|
||||
# FileName=$nested_server_name
|
||||
[Container]
|
||||
Image=$IMAGE
|
||||
ContainerName=nested-server-$(random_string)
|
||||
PublishPort=8080:8080
|
||||
EOF
|
||||
|
||||
|
||||
# Install the directory
|
||||
run_podman quadlet install "$app_dir" --application=$app_name
|
||||
|
||||
# Verify all quadlets were installed (2 individual + 3 from .quadlets file = 5 total)
|
||||
# Verify all quadlets were installed (2 individual + 3 from .quadlets file + 1 from nested .quadlet file = 6 total)
|
||||
assert "$output" =~ "${frontend_name}.container" "install output should contain ${frontend_name}.container"
|
||||
assert "$output" =~ "${data_name}.volume" "install output should contain ${data_name}.volume"
|
||||
assert "$output" =~ "${api_name}.container" "install output should contain ${api_name}.container"
|
||||
assert "$output" =~ "${cache_name}.volume" "install output should contain ${cache_name}.volume"
|
||||
assert "$output" =~ "${network_name}.network" "install output should contain ${network_name}.network"
|
||||
assert "$output" =~ "${nested_server_name}.container" "install output should contain ${nested_server_name}.container"
|
||||
|
||||
# Count lines in output (should be 6 lines: 5 quadlets + 1 asset file)
|
||||
assert "${#lines[@]}" -eq 6 "install output should contain exactly six lines"
|
||||
# Count lines in output (should be 7 lines: 6 quadlets + 1 asset file)
|
||||
assert "${#lines[@]}" -eq 7 "install output should contain exactly seven lines"
|
||||
|
||||
# Verify all files exist on disk
|
||||
[[ -f "$install_dir/$app_name/${frontend_name}.container" ]] || die "${frontend_name}.container should exist on disk"
|
||||
|
|
@ -327,6 +340,7 @@ EOF
|
|||
[[ -f "$install_dir/$app_name/${cache_name}.volume" ]] || die "${cache_name}.volume should exist on disk"
|
||||
[[ -f "$install_dir/$app_name/${network_name}.network" ]] || die "${network_name}.network should exist on disk"
|
||||
[[ -f "$install_dir/$app_name/app.conf" ]] || die "app.conf should exist on disk"
|
||||
[[ -f "$install_dir/$app_name/a/${nested_server_name}.container" ]] || die "a/${nested_server_name}.container should exist on disk"
|
||||
|
||||
# Test quadlet list to verify all quadlets show the same app name
|
||||
run_podman quadlet list
|
||||
|
|
@ -335,6 +349,7 @@ EOF
|
|||
local api_line=$(echo "$output" | grep "${api_name}.container")
|
||||
local cache_line=$(echo "$output" | grep "${cache_name}.volume")
|
||||
local network_line=$(echo "$output" | grep "${network_name}.network")
|
||||
local nested_server_line=$(echo "$output" | grep "${nested_server_name}.container")
|
||||
|
||||
# Verify content of individual quadlet files
|
||||
run cat "$install_dir/$app_name/${frontend_name}.container"
|
||||
|
|
@ -349,6 +364,10 @@ EOF
|
|||
assert "$output" =~ "\\[Network\\]" "network file should contain [Network] section"
|
||||
assert "$output" =~ "Subnet=192.168.1.0/24" "network file should contain correct subnet"
|
||||
|
||||
run cat "$install_dir/$app_name/a/${nested_server_name}.container"
|
||||
assert "$output" =~ "\\[Container\\]" "nested-server container file should contain [Container] section"
|
||||
assert "$output" =~ "ContainerName=nested-server-" "nested-server container file should contain correct name prefix"
|
||||
|
||||
# Test that removing one quadlet removes the entire application
|
||||
run_podman quadlet rm $app_name --recursive
|
||||
|
||||
|
|
@ -359,6 +378,7 @@ EOF
|
|||
assert "$output" !~ "${api_name}.container" "${api_name}.container should also be removed as part of same app"
|
||||
assert "$output" !~ "${cache_name}.volume" "${cache_name}.volume should also be removed as part of same app"
|
||||
assert "$output" !~ "${network_name}.network" "${network_name}.network should also be removed as part of same app"
|
||||
assert "$output" !~ "${nested_server_name}.container" "${nested_server_name}.container should also be removed as part of same app"
|
||||
|
||||
# All individual files should be removed
|
||||
[[ ! -f "$install_dir/$app_name/${frontend_name}.container" ]] || die "${frontend_name}.container should be removed"
|
||||
|
|
@ -367,4 +387,5 @@ EOF
|
|||
[[ ! -f "$install_dir/$app_name/${cache_name}.volume" ]] || die "${cache_name}.volume should be removed"
|
||||
[[ ! -f "$install_dir/$app_name/${network_name}.network" ]] || die "${network_name}.network should be removed"
|
||||
[[ ! -f "$install_dir/$app_name/app.conf" ]] || die "app.conf should be removed"
|
||||
[[ ! -f "$install_dir/$app_name/a/${nested_server_name}.container" ]] || die "${nested_server_name}.container should be removed"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue