diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index 73f1adf651..54317288a8 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -1776,7 +1776,7 @@ func AutocompleteSDNotify(_ *cobra.Command, _ []string, _ string) ([]string, cob var containerStatuses = []string{"created", "running", "paused", "stopped", "exited", "unknown"} -var quadletStatuses = []string{"Not loaded", "active/running", "inactive/dead", "failed/failed", "activating/start", "deactivating/stop"} +var quadletStatuses = []string{entities.QuadletStatusNotLoaded, entities.QuadletStatusLoadedTemplate, "active/running", "inactive/dead", "failed/failed", "activating/start", "deactivating/stop"} // AutocompletePsFilters - Autocomplete ps filter options. func AutocompletePsFilters(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { diff --git a/docs/source/markdown/podman-quadlet-list.1.md.in b/docs/source/markdown/podman-quadlet-list.1.md.in index 576408c151..5ece71ab9a 100644 --- a/docs/source/markdown/podman-quadlet-list.1.md.in +++ b/docs/source/markdown/podman-quadlet-list.1.md.in @@ -23,8 +23,8 @@ Supported filters: | Filter | Description | |------------|--------------------------------------------------------------------------------------------------| | name | Filter by quadlet name | -| status | Filter by quadlet status. Valid values: `Not loaded`, `active/running`, `inactive/dead`, `failed/failed`, `activating/start`, `deactivating/stop` | | pod | Filter by the `Pod=` value (displays only for .container units) | +| status | Filter by quadlet status. Valid values: `Not loaded`, `loaded template`, `active/running`, `inactive/dead`, `failed/failed`, `activating/start`, `deactivating/stop` | #### **--format**=*format* @@ -38,7 +38,7 @@ Print results with a Go template. | .Name | Name of the Quadlet file | | .Path | Quadlet file path on disk | | .Pod | Pod quadlet file from `Pod=` in `[Container]` (empty if not set) | -| .Status | Quadlet status corresponding to systemd unit | +| .Status | Quadlet status corresponding to systemd unit (`Not loaded` and `loaded template` are from podman, other values are from systemd) | | .UnitName | Systemd unit name corresponding to quadlet | @@option noheading diff --git a/docs/source/markdown/podman-quadlet-rm.1.md.in b/docs/source/markdown/podman-quadlet-rm.1.md.in index ef1e5f9b98..139f388762 100644 --- a/docs/source/markdown/podman-quadlet-rm.1.md.in +++ b/docs/source/markdown/podman-quadlet-rm.1.md.in @@ -11,6 +11,8 @@ podman\-quadlet\-rm - Removes an installed quadlet Remove one or more installed Quadlets from the current user. Following command also takes application name as input and removes all the Quadlets which belongs to that specific application. +When the argument is uninstantiated template quadlet, this command removes the template quadlet file (e.g. `templateName@.container`) and the generated systemd template unit (e.g. `templateName@.service`). If there are running instances of that systemd template, the command fails if **--force** option is not set, and tries to stop the instances if **--force** option is set. + Note: If a quadlet is part of an application, removing that specific quadlet will remove the entire application. When a quadlet is installed from a directory, all files installed from that directory—including both quadlet and non-quadlet files—are considered part of a single application. @@ -23,7 +25,7 @@ Remove all Quadlets for the current user. #### **--force**, **-f** -Remove running Quadlets. +Remove running Quadlets (in case of uninstantiated template quadlets, stop its instances). #### **--ignore**, **-i** diff --git a/pkg/api/server/register_quadlets.go b/pkg/api/server/register_quadlets.go index d417771ef9..4263a4f33e 100644 --- a/pkg/api/server/register_quadlets.go +++ b/pkg/api/server/register_quadlets.go @@ -196,7 +196,7 @@ func (s *APIServer) registerQuadletHandlers(r *mux.Router) error { // name: force // type: boolean // default: false - // description: Remove running quadlet by stopping it first + // description: Remove running quadlets (in case of uninstantiated template quadlets, stop its instances). // - in: query // name: ignore // type: boolean diff --git a/pkg/domain/entities/quadlet.go b/pkg/domain/entities/quadlet.go index 94013e373e..3ce6fde884 100644 --- a/pkg/domain/entities/quadlet.go +++ b/pkg/domain/entities/quadlet.go @@ -28,6 +28,14 @@ type QuadletListOptions struct { Filters []string } +const ( + // Quadlet does not have a corresponding loaded systemd unit. + QuadletStatusNotLoaded = "Not loaded" + // Quadlet is a template loaded into systemd (there is no status + // to display in the same sense as for concrete units). + QuadletStatusLoadedTemplate = "loaded template" +) + // A ListQuadlet is a single Quadlet to be listed by `podman quadlet list` type ListQuadlet struct { // Name is the name of the Quadlet file @@ -37,8 +45,9 @@ type ListQuadlet struct { UnitName string // Path to the Quadlet on disk Path string - // What is the status of the Quadlet - if present in systemd, will be a - // systemd status, else will mention if the Quadlet has syntax errors + // What is the status of the Quadlet - either values from systemd + // (e.g. active/running), or podman-defined values "Not loaded" + // and "loaded template". Status string // If multiple quadlets were installed together they will belong // to common App. diff --git a/pkg/domain/infra/abi/quadlet_utils.go b/pkg/domain/infra/abi/quadlet_utils.go index 9a4a4c81c9..fe4d6efe56 100644 --- a/pkg/domain/infra/abi/quadlet_utils.go +++ b/pkg/domain/infra/abi/quadlet_utils.go @@ -116,7 +116,8 @@ func getAllQuadlets(ctx context.Context, conn *dbus.Conn) ([]*entities.ListQuadl // Get the root paths of all quadlets available to the current user quadletDirs := systemdquadlet.GetUnitDirs(rootless.IsRootless(), false) - allServiceNames := make([]string, 0) + concreteServiceNames := make([]string, 0) + templateServiceNames := make([]string, 0) // for every quadlet dir, let's get the quadlets for _, dir := range quadletDirs { @@ -154,36 +155,69 @@ func getAllQuadlets(ctx context.Context, conn *dbus.Conn) ([]*entities.ListQuadl report.Pod = pod } - allServiceNames = append(allServiceNames, serviceName) + if systemdquadlet.IsTemplateUnitFileName(serviceName) { + templateServiceNames = append(templateServiceNames, serviceName) + } else { + concreteServiceNames = append(concreteServiceNames, serviceName) + } partialReports[serviceName] = report } } - // Get status of all systemd units with given names. - statuses, err := conn.ListUnitsByNamesContext(ctx, allServiceNames) - if err != nil { - return nil, fmt.Errorf("querying systemd for unit status: %w", err) - } - if len(statuses) != len(allServiceNames) { - logrus.Warnf("Queried for %d services but received %d responses", len(allServiceNames), len(statuses)) + // Get status of concrete systemd units with given names. + if len(concreteServiceNames) > 0 { + statuses, err := conn.ListUnitsByNamesContext(ctx, concreteServiceNames) + if err != nil { + return nil, fmt.Errorf("querying systemd for unit status: %w", err) + } + if len(statuses) != len(concreteServiceNames) { + logrus.Warnf("Queried for %d services but received %d responses", len(concreteServiceNames), len(statuses)) + } + + for _, unitStatus := range statuses { + report, ok := partialReports[unitStatus.Name] + if !ok { + logrus.Errorf("Unexpected unit returned by systemd - was not searching for %s", unitStatus.Name) + } + logrus.Debugf("Unit %s has status %s %s %s", unitStatus.Name, unitStatus.LoadState, unitStatus.ActiveState, unitStatus.SubState) + report.UnitName = unitStatus.Name + + // Unit is not loaded + if unitStatus.LoadState != "loaded" { + report.Status = entities.QuadletStatusNotLoaded + } else { + report.Status = fmt.Sprintf("%s/%s", unitStatus.ActiveState, unitStatus.SubState) + } + reports = append(reports, &report) + delete(partialReports, unitStatus.Name) + } } - for _, unitStatus := range statuses { - report, ok := partialReports[unitStatus.Name] - if !ok { - logrus.Errorf("Unexpected unit returned by systemd - was not searching for %s", unitStatus.Name) + // Uninstantiated template units need to be handled separately, because + // ListUnitsBy* functions do not return anything for templates. + if len(templateServiceNames) > 0 { + unitFiles, err := conn.ListUnitFilesByPatternsContext(ctx, []string{}, templateServiceNames) + if err != nil { + return nil, fmt.Errorf("querying systemd for unit file: %w", err) } - logrus.Debugf("Unit %s has status %s %s %s", unitStatus.Name, unitStatus.LoadState, unitStatus.ActiveState, unitStatus.SubState) - report.UnitName = unitStatus.Name + unitFilesFound := make(map[string]struct{}) + for _, unitFile := range unitFiles { + unitFilesFound[filepath.Base(unitFile.Path)] = struct{}{} + } + for _, templateServiceName := range templateServiceNames { + report := partialReports[templateServiceName] - // Unit is not loaded - if unitStatus.LoadState != "loaded" { - report.Status = "Not loaded" - } else { - report.Status = fmt.Sprintf("%s/%s", unitStatus.ActiveState, unitStatus.SubState) + report.UnitName = templateServiceName + + if _, ok := unitFilesFound[templateServiceName]; ok { + report.Status = entities.QuadletStatusLoadedTemplate + } else { + report.Status = entities.QuadletStatusNotLoaded + } + + reports = append(reports, &report) + delete(partialReports, templateServiceName) } - reports = append(reports, &report) - delete(partialReports, unitStatus.Name) } // This should not happen. @@ -191,7 +225,7 @@ func getAllQuadlets(ctx context.Context, conn *dbus.Conn) ([]*entities.ListQuadl // We can find them with LoadState, as we do above. // Handle it anyways because it's easy enough to do. for _, report := range partialReports { - report.Status = "Not loaded" + report.Status = entities.QuadletStatusNotLoaded reports = append(reports, &report) } @@ -199,12 +233,7 @@ func getAllQuadlets(ctx context.Context, conn *dbus.Conn) ([]*entities.ListQuadl } func removeQuadlet(ctx context.Context, conn *dbus.Conn, quadlet *entities.ListQuadlet, force bool) error { - switch quadlet.Status { - case "Not loaded": - case "inactive/dead": - // Nothing to do here if it doesn't exist in systemd - break - case "active/running": + if quadlet.Status == "active/running" { if !force { return fmt.Errorf("quadlet %s is running and force is not set, refusing to remove: %w", quadlet.Name, define.ErrQuadletRunning) } @@ -221,10 +250,81 @@ func removeQuadlet(ctx context.Context, conn *dbus.Conn, quadlet *entities.ListQ return fmt.Errorf("unable to stop quadlet %s: %s", quadlet.Name, stopResult) } } + if quadlet.Status == entities.QuadletStatusLoadedTemplate { + if err := stopLoadedTemplateInstances(ctx, conn, quadlet, force); err != nil { + return err + } + } return os.Remove(quadlet.Path) } +// stopLoadedTemplateInstances handles the running instances of a template. +// If force is set, the instances are stopped and error is returned on failure. +// If force is not set, stopping is refused and error is returned on running instances. +func stopLoadedTemplateInstances(ctx context.Context, conn *dbus.Conn, quadlet *entities.ListQuadlet, force bool) error { + extension := filepath.Ext(quadlet.UnitName) + + // The regular expression in this pattern matches the template itself as well, but the + // ListUnitsBy* functions do not return anything for templates. + instancePattern := strings.TrimSuffix(quadlet.UnitName, extension) + "*" + extension + instances, err := conn.ListUnitsByPatternsContext(ctx, []string{}, []string{instancePattern}) + if err != nil { + return fmt.Errorf("querying systemd for instances of %q: %w", quadlet.UnitName, err) + } + + var runningInstanceErrors []error + for _, instanceStatus := range instances { + properties, err := conn.GetUnitPropertiesContext(ctx, instanceStatus.Name) + if err != nil { + logrus.Errorf("getting unit properties for %q: %v", instanceStatus.Name, err) + continue + } + + sourcePath, ok := properties["SourcePath"].(string) + if !ok || sourcePath == "" { + logrus.Warnf("source path not found for unit %q", instanceStatus.Name) + continue + } + + if filepath.Clean(sourcePath) != filepath.Clean(quadlet.Path) { + // Nothing to do here if the unit is not associated with this quadlet + continue + } + + if instanceStatus.LoadState != "loaded" { + // Nothing to do here if the instance is not loaded + continue + } + + if instanceStatus.ActiveState == "active" { + if !force { + runningInstanceErrors = append(runningInstanceErrors, fmt.Errorf("template %q has running instance %q and force is not set, refusing to remove: %w", quadlet.Name, instanceStatus.Name, define.ErrQuadletRunning)) + continue + } + logrus.Debugf("Going to stop systemd unit %q (Instance of template %q)", instanceStatus.Name, quadlet.Name) + + ch := make(chan string) + if _, err := conn.StopUnitContext(ctx, instanceStatus.Name, "replace", ch); err != nil { + runningInstanceErrors = append(runningInstanceErrors, fmt.Errorf("stopping instance %q of template %q: %w", instanceStatus.Name, quadlet.Name, err)) + continue + } + + logrus.Debugf("Waiting for systemd unit %q to stop", instanceStatus.Name) + stopResult := <-ch + if stopResult != "done" && stopResult != "skipped" { + runningInstanceErrors = append(runningInstanceErrors, fmt.Errorf("unable to stop instance %q of template %q: %q", instanceStatus.Name, quadlet.Name, stopResult)) + } + } + } + + if len(runningInstanceErrors) > 0 { + return errors.Join(runningInstanceErrors...) + } + + return nil +} + // getQuadletServiceNameAndUnit parses a Quadlet file and returns both the // generated systemd service name and the parsed unit file. func getQuadletServiceNameAndUnit(quadletPath string) (string, *parser.UnitFile, error) { diff --git a/pkg/systemd/quadlet/quadlet.go b/pkg/systemd/quadlet/quadlet.go index d83958c9dd..123c964e5a 100644 --- a/pkg/systemd/quadlet/quadlet.go +++ b/pkg/systemd/quadlet/quadlet.go @@ -943,8 +943,9 @@ func ConvertContainer(container *parser.UnitFile, unitsInfoMap map[string]*UnitI return service, warnings, nil } -func isTemplateUnit(unit *parser.UnitFile) bool { - base := strings.TrimSuffix(unit.Filename, filepath.Ext(unit.Filename)) +// Determine if the given file name belongs to an uninstantiated template unit. +func IsTemplateUnitFileName(fileName string) bool { + base := strings.TrimSuffix(fileName, filepath.Ext(fileName)) return strings.HasSuffix(base, "@") } @@ -2231,7 +2232,7 @@ func handlePod(quadletUnitFile, serviceUnitFile *parser.UnitFile, groupName stri // If we want to start the container with the pod, we add it to this list. // This creates corresponding Wants=/Before= statements in the pod service. // Do not add this for template units as dependency cannot be created for them. - if !isTemplateUnit(quadletUnitFile) && quadletUnitFile.LookupBooleanWithDefault(groupName, KeyStartWithPod, true) { + if !IsTemplateUnitFileName(quadletUnitFile.Filename) && quadletUnitFile.LookupBooleanWithDefault(groupName, KeyStartWithPod, true) { podInfo.ContainersToStart = append(podInfo.ContainersToStart, serviceUnitFile.Filename) } } diff --git a/test/system/253-podman-quadlet.bats b/test/system/253-podman-quadlet.bats index 3fc22237cb..0286fb3ec3 100644 --- a/test/system/253-podman-quadlet.bats +++ b/test/system/253-podman-quadlet.bats @@ -156,6 +156,70 @@ EOF run_podman quadlet rm $ctr_unit } +@test "quadlet verb - install, list, print, rm - template" { + # Determine the install directory path based on rootless/root + local install_dir + install_dir=$(get_quadlet_install_dir) + # Create a test quadlet file + local quadlet_name_without_extension="templated-quadlet@" + local quadlet_name=${quadlet_name_without_extension}.container + local quadlet_unit_name=${quadlet_name_without_extension}.service + local quadlet_instance_name=${quadlet_name_without_extension}instance.service + local quadlet_file=$PODMAN_TMPDIR/$quadlet_name + cat > "$quadlet_file" <