Files
podman/pkg/api/handlers/libpod/containers.go
Jan Kaluza 701aade262 Add --env and --unsetenv to podman update.
The --env is used to add new environment variable to container or
override the existing one. The --unsetenv is used to remove
the environment variable.

It is done by sharing "env" and "unsetenv" flags between both
"update" and "create" commands and later handling these flags
in the "update" command handler.

The list of environment variables to add/remove is stored
in newly added variables in the ContainerUpdateOptions.

The Container.Update API call is refactored to take
the ContainerUpdateOptions as an input to limit the number of its
arguments.

The Env and UnsetEnv lists are later handled using the envLib
package and the Container is updated.

The remote API is also extended to handle Env and EnvUnset.

Fixes: #24875

Signed-off-by: Jan Kaluza <jkaluza@redhat.com>
2025-03-21 13:15:44 +01:00

497 lines
15 KiB
Go

//go:build !remote
package libpod
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"strings"
"github.com/containers/podman/v5/libpod"
"github.com/containers/podman/v5/libpod/define"
"github.com/containers/podman/v5/pkg/api/handlers"
"github.com/containers/podman/v5/pkg/api/handlers/compat"
"github.com/containers/podman/v5/pkg/api/handlers/utils"
api "github.com/containers/podman/v5/pkg/api/types"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/domain/infra/abi"
"github.com/containers/podman/v5/pkg/specgenutil"
"github.com/containers/podman/v5/pkg/util"
"github.com/gorilla/schema"
"github.com/sirupsen/logrus"
)
func ContainerExists(w http.ResponseWriter, r *http.Request) {
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
// Now use the ABI implementation to prevent us from having duplicate
// code.
containerEngine := abi.ContainerEngine{Libpod: runtime}
name := utils.GetName(r)
query := struct {
External bool `schema:"external"`
}{
// override any golang type defaults
}
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
return
}
options := entities.ContainerExistsOptions{
External: query.External,
}
report, err := containerEngine.ContainerExists(r.Context(), name, options)
if err != nil {
if errors.Is(err, define.ErrNoSuchCtr) {
utils.ContainerNotFound(w, name, err)
return
}
utils.InternalServerError(w, err)
return
}
if report.Value {
utils.WriteResponse(w, http.StatusNoContent, "")
} else {
utils.ContainerNotFound(w, name, define.ErrNoSuchCtr)
}
}
func ListContainers(w http.ResponseWriter, r *http.Request) {
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
query := struct {
All bool `schema:"all"`
External bool `schema:"external"`
Last int `schema:"last"` // alias for limit
Limit int `schema:"limit"`
Namespace bool `schema:"namespace"`
Size bool `schema:"size"`
Sync bool `schema:"sync"`
}{
// override any golang type defaults
}
filterMap, err := util.PrepareFilters(r)
if err != nil {
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("failed to decode filter parameters for %s: %w", r.URL.String(), err))
return
}
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
return
}
limit := query.Limit
// Support `last` as an alias for `limit`. While Podman uses --last in
// the CLI, the API is using `limit`. As we first used `last` in the
// API as well, we decided to go with aliasing to prevent any
// regression. See github.com/containers/podman/issues/6413.
if _, found := r.URL.Query()["last"]; found {
logrus.Info("List containers: received `last` parameter - overwriting `limit`")
limit = query.Last
}
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
// Now use the ABI implementation to prevent us from having duplicate
// code.
containerEngine := abi.ContainerEngine{Libpod: runtime}
opts := entities.ContainerListOptions{
All: query.All,
External: query.External,
Filters: *filterMap,
Last: limit,
Namespace: query.Namespace,
// Always return Pod, should not be part of the API.
// https://github.com/containers/podman/pull/7223
Pod: true,
Size: query.Size,
Sync: query.Sync,
}
pss, err := containerEngine.ContainerList(r.Context(), opts)
if err != nil {
utils.InternalServerError(w, err)
return
}
utils.WriteResponse(w, http.StatusOK, pss)
}
func GetContainer(w http.ResponseWriter, r *http.Request) {
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
query := struct {
Size bool `schema:"size"`
}{
// override any golang type defaults
}
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
return
}
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
name := utils.GetName(r)
container, err := runtime.LookupContainer(name)
if err != nil {
utils.ContainerNotFound(w, name, err)
return
}
data, err := container.Inspect(query.Size)
if err != nil {
utils.InternalServerError(w, err)
return
}
// if client request old v4 payload we should return v4 compatible json
if _, err := utils.SupportedVersion(r, ">=5.0.0"); err != nil {
data.Config.V4PodmanCompatMarshal = true
}
utils.WriteResponse(w, http.StatusOK, data)
}
func WaitContainer(w http.ResponseWriter, r *http.Request) {
utils.WaitContainerLibpod(w, r)
}
func UnmountContainer(w http.ResponseWriter, r *http.Request) {
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
name := utils.GetName(r)
conn, err := runtime.LookupContainer(name)
if err != nil {
utils.ContainerNotFound(w, name, err)
return
}
// TODO In future it might be an improvement that libpod unmount return a
// "container not mounted" error so we can surface that to the endpoint user
if err := conn.Unmount(false); err != nil {
utils.InternalServerError(w, err)
return
}
utils.WriteResponse(w, http.StatusNoContent, "")
}
func MountContainer(w http.ResponseWriter, r *http.Request) {
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
name := utils.GetName(r)
conn, err := runtime.LookupContainer(name)
if err != nil {
utils.ContainerNotFound(w, name, err)
return
}
m, err := conn.Mount()
if err != nil {
utils.InternalServerError(w, err)
return
}
utils.WriteResponse(w, http.StatusOK, m)
}
func ShowMountedContainers(w http.ResponseWriter, r *http.Request) {
response := make(map[string]string)
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
conns, err := runtime.GetAllContainers()
if err != nil {
utils.InternalServerError(w, err)
return
}
for _, conn := range conns {
mounted, mountPoint, err := conn.Mounted()
if err != nil {
utils.InternalServerError(w, err)
return
}
if !mounted {
continue
}
response[conn.ID()] = mountPoint
}
utils.WriteResponse(w, http.StatusOK, response)
}
func Checkpoint(w http.ResponseWriter, r *http.Request) {
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
containerEngine := abi.ContainerEngine{Libpod: runtime}
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
query := struct {
Keep bool `schema:"keep"`
LeaveRunning bool `schema:"leaveRunning"`
TCPEstablished bool `schema:"tcpEstablished"`
Export bool `schema:"export"`
IgnoreRootFS bool `schema:"ignoreRootFS"`
PrintStats bool `schema:"printStats"`
PreCheckpoint bool `schema:"preCheckpoint"`
WithPrevious bool `schema:"withPrevious"`
FileLocks bool `schema:"fileLocks"`
CreateImage string `schema:"createImage"`
}{
// override any golang type defaults
}
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
return
}
name := utils.GetName(r)
if _, err := runtime.LookupContainer(name); err != nil {
utils.ContainerNotFound(w, name, err)
return
}
names := []string{name}
options := entities.CheckpointOptions{
Keep: query.Keep,
LeaveRunning: query.LeaveRunning,
TCPEstablished: query.TCPEstablished,
IgnoreRootFS: query.IgnoreRootFS,
PrintStats: query.PrintStats,
PreCheckPoint: query.PreCheckpoint,
WithPrevious: query.WithPrevious,
FileLocks: query.FileLocks,
CreateImage: query.CreateImage,
}
if query.Export {
f, err := os.CreateTemp("", "checkpoint")
if err != nil {
utils.InternalServerError(w, err)
return
}
defer os.Remove(f.Name())
if err := f.Close(); err != nil {
utils.InternalServerError(w, err)
return
}
options.Export = f.Name()
}
reports, err := containerEngine.ContainerCheckpoint(r.Context(), names, options)
if err != nil {
utils.InternalServerError(w, err)
return
}
if len(reports) != 1 {
utils.InternalServerError(w, fmt.Errorf("expected 1 restore report but got %d", len(reports)))
return
}
if reports[0].Err != nil {
utils.InternalServerError(w, reports[0].Err)
return
}
if !query.Export {
utils.WriteResponse(w, http.StatusOK, reports[0])
return
}
f, err := os.Open(options.Export)
if err != nil {
utils.InternalServerError(w, err)
return
}
defer f.Close()
utils.WriteResponse(w, http.StatusOK, f)
}
func Restore(w http.ResponseWriter, r *http.Request) {
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
containerEngine := abi.ContainerEngine{Libpod: runtime}
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
query := struct {
Keep bool `schema:"keep"`
TCPEstablished bool `schema:"tcpEstablished"`
Import bool `schema:"import"`
Name string `schema:"name"`
IgnoreRootFS bool `schema:"ignoreRootFS"`
IgnoreVolumes bool `schema:"ignoreVolumes"`
IgnoreStaticIP bool `schema:"ignoreStaticIP"`
IgnoreStaticMAC bool `schema:"ignoreStaticMAC"`
PrintStats bool `schema:"printStats"`
FileLocks bool `schema:"fileLocks"`
PublishPorts string `schema:"publishPorts"`
Pod string `schema:"pod"`
}{
// override any golang type defaults
}
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
return
}
options := entities.RestoreOptions{
Name: query.Name,
Keep: query.Keep,
TCPEstablished: query.TCPEstablished,
IgnoreRootFS: query.IgnoreRootFS,
IgnoreVolumes: query.IgnoreVolumes,
IgnoreStaticIP: query.IgnoreStaticIP,
IgnoreStaticMAC: query.IgnoreStaticMAC,
PrintStats: query.PrintStats,
FileLocks: query.FileLocks,
PublishPorts: strings.Fields(query.PublishPorts),
Pod: query.Pod,
}
var names []string
if query.Import {
t, err := os.CreateTemp("", "restore")
if err != nil {
utils.InternalServerError(w, err)
return
}
defer os.Remove(t.Name())
if err := compat.SaveFromBody(t, r); err != nil {
utils.InternalServerError(w, err)
return
}
options.Import = t.Name()
} else {
name := utils.GetName(r)
if _, err := runtime.LookupContainer(name); err != nil {
// If container was not found, check if this is a checkpoint image
ir := abi.ImageEngine{Libpod: runtime}
report, err := ir.Exists(r.Context(), name)
if err != nil {
utils.Error(w, http.StatusNotFound, fmt.Errorf("failed to find container or checkpoint image %s: %w", name, err))
return
}
if !report.Value {
utils.Error(w, http.StatusNotFound, fmt.Errorf("failed to find container or checkpoint image %s", name))
return
}
}
names = []string{name}
}
reports, err := containerEngine.ContainerRestore(r.Context(), names, options)
if err != nil {
utils.InternalServerError(w, err)
return
}
if len(reports) != 1 {
utils.InternalServerError(w, fmt.Errorf("expected 1 restore report but got %d", len(reports)))
return
}
if reports[0].Err != nil {
utils.InternalServerError(w, reports[0].Err)
return
}
utils.WriteResponse(w, http.StatusOK, reports[0])
}
func InitContainer(w http.ResponseWriter, r *http.Request) {
name := utils.GetName(r)
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
ctr, err := runtime.LookupContainer(name)
if err != nil {
utils.ContainerNotFound(w, name, err)
return
}
err = ctr.Init(r.Context(), ctr.PodID() != "")
if errors.Is(err, define.ErrCtrStateInvalid) {
utils.Error(w, http.StatusNotModified, err)
return
}
if err != nil {
utils.InternalServerError(w, err)
return
}
utils.WriteResponse(w, http.StatusNoContent, "")
}
func UpdateContainer(w http.ResponseWriter, r *http.Request) {
name := utils.GetName(r)
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
decoder := utils.GetDecoder(r)
query := struct {
RestartPolicy string `schema:"restartPolicy"`
RestartRetries uint `schema:"restartRetries"`
}{
// override any golang type defaults
}
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
return
}
ctr, err := runtime.LookupContainer(name)
if err != nil {
utils.ContainerNotFound(w, name, err)
return
}
var restartPolicy *string
var restartRetries *uint
if query.RestartPolicy != "" {
restartPolicy = &query.RestartPolicy
if query.RestartPolicy == define.RestartPolicyOnFailure {
restartRetries = &query.RestartRetries
} else if query.RestartRetries != 0 {
utils.Error(w, http.StatusBadRequest, errors.New("cannot set restart retries unless restart policy is on-failure"))
return
}
} else if query.RestartRetries != 0 {
utils.Error(w, http.StatusBadRequest, errors.New("cannot set restart retries unless restart policy is set"))
return
}
options := &handlers.UpdateEntities{}
if err := json.NewDecoder(r.Body).Decode(&options); err != nil {
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("decode(): %w", err))
return
}
resourceLimits, err := specgenutil.UpdateMajorAndMinorNumbers(&options.LinuxResources, &options.UpdateContainerDevicesLimits)
if err != nil {
utils.InternalServerError(w, err)
return
}
updateOptions := &entities.ContainerUpdateOptions{
Resources: resourceLimits,
ChangedHealthCheckConfiguration: &options.UpdateHealthCheckConfig,
RestartPolicy: restartPolicy,
RestartRetries: restartRetries,
Env: options.Env,
UnsetEnv: options.UnsetEnv,
}
err = ctr.Update(updateOptions)
if err != nil {
utils.InternalServerError(w, err)
return
}
utils.WriteResponse(w, http.StatusCreated, ctr.ID())
}
func ShouldRestart(w http.ResponseWriter, r *http.Request) {
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
// Now use the ABI implementation to prevent us from having duplicate
// code.
containerEngine := abi.ContainerEngine{Libpod: runtime}
name := utils.GetName(r)
report, err := containerEngine.ShouldRestart(r.Context(), name)
if err != nil {
if errors.Is(err, define.ErrNoSuchCtr) {
utils.ContainerNotFound(w, name, err)
return
}
utils.InternalServerError(w, err)
return
}
if report.Value {
utils.WriteResponse(w, http.StatusNoContent, "")
} else {
utils.ContainerNotFound(w, name, define.ErrNoSuchCtr)
}
}