summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--cmd/podman/containers/rm.go9
-rw-r--r--cmd/podman/parse/net.go17
-rw-r--r--cmd/podman/pods/ps.go1
-rwxr-xr-xcontrib/cirrus/ext_svc_check.sh21
-rw-r--r--contrib/cirrus/lib.sh5
-rwxr-xr-xcontrib/cirrus/runner.sh4
-rw-r--r--docs/play_kube_support.md152
-rw-r--r--docs/source/markdown/podman-kill.1.md2
-rw-r--r--libpod/container.go3
-rw-r--r--libpod/container_api.go3
-rw-r--r--libpod/container_config.go1
-rw-r--r--libpod/container_exec.go2
-rw-r--r--libpod/container_internal_linux.go7
-rw-r--r--libpod/plugin/volume_api.go7
-rw-r--r--libpod/pod_api.go9
-rw-r--r--libpod/runtime.go3
-rw-r--r--test/e2e/kill_test.go20
-rw-r--r--troubleshooting.md14
19 files changed, 230 insertions, 52 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e9f40dffe..271c130c9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -150,7 +150,7 @@ Regardless of the type of PR, all PRs should include:
* additional testcases. Ideally, they should fail w/o your code change applied.
(With a few exceptions, CI hooks will block your PR unless your change
includes files named `*_test.go` or under the `test/` subdirectory. To
- bypass this block, include the string `[NO TESTS NEEDED]` in your
+ bypass this block, include the string `[NO NEW TESTS NEEDED]` in your
commit message).
* documentation changes.
diff --git a/cmd/podman/containers/rm.go b/cmd/podman/containers/rm.go
index 85fa138a9..bcbe86947 100644
--- a/cmd/podman/containers/rm.go
+++ b/cmd/podman/containers/rm.go
@@ -123,9 +123,7 @@ func rm(cmd *cobra.Command, args []string) error {
// removeContainers will set the exit code according to the `podman-rm` man
// page.
func removeContainers(namesOrIDs []string, rmOptions entities.RmOptions, setExit bool) error {
- var (
- errs utils.OutputErrors
- )
+ var errs utils.OutputErrors
responses, err := registry.ContainerEngine().ContainerRm(context.Background(), namesOrIDs, rmOptions)
if err != nil {
if setExit {
@@ -135,8 +133,9 @@ func removeContainers(namesOrIDs []string, rmOptions entities.RmOptions, setExit
}
for _, r := range responses {
if r.Err != nil {
- // TODO this will not work with the remote client
- if errors.Cause(err) == define.ErrWillDeadlock {
+ // When using the API, errors.Cause(err) will never equal constant define.ErrWillDeadLock
+ if errors.Cause(r.Err) == define.ErrWillDeadlock ||
+ errors.Cause(r.Err).Error() == define.ErrWillDeadlock.Error() {
logrus.Errorf("Potential deadlock detected - please run 'podman system renumber' to resolve")
}
if setExit {
diff --git a/cmd/podman/parse/net.go b/cmd/podman/parse/net.go
index 870690db3..b616e1029 100644
--- a/cmd/podman/parse/net.go
+++ b/cmd/podman/parse/net.go
@@ -18,6 +18,8 @@ import (
const (
Protocol_TCP Protocol = 0
Protocol_UDP Protocol = 1
+ LabelType string = "label"
+ ENVType string = "env"
)
type Protocol int32
@@ -89,9 +91,7 @@ func GetAllLabels(labelFile, inputLabels []string) (map[string]string, error) {
// There's an argument that we SHOULD be doing that parsing for
// all environment variables, even those sourced from files, but
// that would require a substantial rework.
- if err := parseEnvFile(labels, file); err != nil {
- // FIXME: parseEnvFile is using parseEnv, so we need to add extra
- // logic for labels.
+ if err := parseEnvOrLabelFile(labels, file, LabelType); err != nil {
return nil, err
}
}
@@ -109,7 +109,7 @@ func GetAllLabels(labelFile, inputLabels []string) (map[string]string, error) {
return labels, nil
}
-func parseEnv(env map[string]string, line string) error {
+func parseEnvOrLabel(env map[string]string, line, configType string) error {
data := strings.SplitN(line, "=", 2)
// catch invalid variables such as "=" or "=A"
@@ -137,7 +137,7 @@ func parseEnv(env map[string]string, line string) error {
env[part[0]] = part[1]
}
}
- } else {
+ } else if configType == ENVType {
// if only a pass-through variable is given, clean it up.
if val, ok := os.LookupEnv(name); ok {
env[name] = val
@@ -147,8 +147,9 @@ func parseEnv(env map[string]string, line string) error {
return nil
}
-// parseEnvFile reads a file with environment variables enumerated by lines
-func parseEnvFile(env map[string]string, filename string) error {
+// parseEnvOrLabelFile reads a file with environment variables enumerated by lines
+// configType should be set to either "label" or "env" based on what type is being parsed
+func parseEnvOrLabelFile(envOrLabel map[string]string, filename, configType string) error {
fh, err := os.Open(filename)
if err != nil {
return err
@@ -161,7 +162,7 @@ func parseEnvFile(env map[string]string, filename string) error {
line := strings.TrimLeft(scanner.Text(), whiteSpaces)
// line is not empty, and not starting with '#'
if len(line) > 0 && !strings.HasPrefix(line, "#") {
- if err := parseEnv(env, line); err != nil {
+ if err := parseEnvOrLabel(envOrLabel, line, configType); err != nil {
return err
}
}
diff --git a/cmd/podman/pods/ps.go b/cmd/podman/pods/ps.go
index a89448275..aa42e1983 100644
--- a/cmd/podman/pods/ps.go
+++ b/cmd/podman/pods/ps.go
@@ -49,7 +49,6 @@ func init() {
flags.BoolVar(&psInput.CtrNames, "ctr-names", false, "Display the container names")
flags.BoolVar(&psInput.CtrIds, "ctr-ids", false, "Display the container UUIDs. If no-trunc is not set they will be truncated")
flags.BoolVar(&psInput.CtrStatus, "ctr-status", false, "Display the container status")
- // TODO should we make this a [] ?
filterFlagName := "filter"
flags.StringSliceVarP(&inputFilters, filterFlagName, "f", []string{}, "Filter output based on conditions given")
diff --git a/contrib/cirrus/ext_svc_check.sh b/contrib/cirrus/ext_svc_check.sh
index 92ac4e93a..146919c39 100755
--- a/contrib/cirrus/ext_svc_check.sh
+++ b/contrib/cirrus/ext_svc_check.sh
@@ -25,6 +25,23 @@ cat ${CIRRUS_WORKING_DIR}/${SCRIPT_BASE}/required_host_ports.txt | \
fi
done
-# TODO: Pull images required during testing into /dev/null
+# Verify we can pull metadata from a few key testing images on quay.io
+# in the 'libpod' namespace. This is mostly aimed at validating the
+# quay.io service is up and responsive. Images were hand-picked with
+# egrep -ro 'quay.io/libpod/.+:latest' test | sort -u
+TEST_IMGS=(\
+ alpine:latest
+ busybox:latest
+ alpine_labels:latest
+ alpine_nginx:latest
+ alpine_healthcheck:latest
+ badhealthcheck:latest
+ cirros:latest
+)
-# TODO: Refresh DNF package-cache into /dev/null
+echo "Checking quay.io test image accessibility"
+for testimg in "${TEST_IMGS[@]}"; do
+ fqin="quay.io/libpod/$testimg"
+ echo " $fqin"
+ skopeo inspect --retry-times 5 "docker://$fqin" | jq . > /dev/null
+done
diff --git a/contrib/cirrus/lib.sh b/contrib/cirrus/lib.sh
index 5d3e43c50..724f7c3d5 100644
--- a/contrib/cirrus/lib.sh
+++ b/contrib/cirrus/lib.sh
@@ -225,11 +225,6 @@ use_netavark() {
export NETWORK_BACKEND=netavark # needed for install_test_configs()
msg "Removing any/all CNI configuration"
rm -rvf /etc/cni/net.d/*
-
- # TODO: Remove this when netavark/aardvark-dns development slows down
- warn "Updating netavark/aardvark-dns to avoid frequent VM image rebuilds"
- # N/B: This is coming from updates-testing repo in F36
- lilto dnf update -y netavark aardvark-dns
}
# Remove all files provided by the distro version of podman.
diff --git a/contrib/cirrus/runner.sh b/contrib/cirrus/runner.sh
index 83a81bd0a..c871f1f54 100755
--- a/contrib/cirrus/runner.sh
+++ b/contrib/cirrus/runner.sh
@@ -303,10 +303,6 @@ function _run_altbuild() {
}
function _run_release() {
- # TODO: These tests should come from code external to the podman repo.
- # to allow test-changes (and re-runs) in the case of a correctable test
- # flaw or flake at release tag-push time. For now, the test is here
- # given its simplicity.
msg "podman info:"
bin/podman info
diff --git a/docs/play_kube_support.md b/docs/play_kube_support.md
new file mode 100644
index 000000000..cf983bc04
--- /dev/null
+++ b/docs/play_kube_support.md
@@ -0,0 +1,152 @@
+# Podman Play Kube Support
+
+This document outlines the kube yaml fields that are currently supported by the **podman play kube** command.
+
+Note: **N/A** means that the option cannot be supported in a single-node Podman environment.
+
+## Pod Fields
+
+| Field | Support |
+|---------------------------------------------------|---------|
+| containers | ✅ |
+| initContainers | ✅ |
+| imagePullSecrets | |
+| enableServiceLinks | |
+| os<nolink>.name | |
+| volumes | |
+| nodeSelector | N/A |
+| nodeName | N/A |
+| affinity.nodeAffinity | N/A |
+| affinity.podAffinity | N/A |
+| affinity.podAntiAffinity | N/A |
+| tolerations.key | N/A |
+| tolerations.operator | N/A |
+| tolerations.effect | N/A |
+| tolerations.tolerationSeconds | N/A |
+| schedulerName | N/A |
+| runtimeClassName | |
+| priorityClassName | |
+| priority | |
+| topologySpreadConstraints.maxSkew | N/A |
+| topologySpreadConstraints.topologyKey | N/A |
+| topologySpreadConstraints.whenUnsatisfiable | N/A |
+| topologySpreadConstraints.labelSelector | N/A |
+| topologySpreadConstraints.minDomains | N/A |
+| restartPolicy | ✅ |
+| terminationGracePeriod | |
+| activeDeadlineSeconds | |
+| readinessGates.conditionType | |
+| hostname | ✅ |
+| setHostnameAsFQDN | |
+| subdomain | |
+| hostAliases.hostnames | ✅ |
+| hostAliases.ip | ✅ |
+| dnsConfig.nameservers | ✅ |
+| dnsConfig<nolink>.options.name | ✅ |
+| dnsConfig.options.value | ✅ |
+| dnsConfig.searches | ✅ |
+| dnsPolicy | |
+| hostNetwork | ✅ |
+| hostPID | |
+| hostIPC | |
+| shareProcessNamespace | ✅ |
+| serviceAccountName | |
+| automountServiceAccountToken | |
+| securityContext.runAsUser | |
+| securityContext.runAsNonRoot | |
+| securityContext.runAsGroup | |
+| securityContext.supplementalGroups | |
+| securityContext.fsGroup | |
+| securityContext.fsGroupChangePolicy | |
+| securityContext.seccompProfile.type | |
+| securityContext.seccompProfile.localhostProfile | |
+| securityContext.seLinuxOptions.level | |
+| securityContext.seLinuxOptions.role | |
+| securityContext.seLinuxOptions.type | |
+| securityContext.seLinuxOptions.user | |
+| securityContext<nolink>.sysctls.name | |
+| securityContext.sysctls.value | |
+| securityContext.windowsOptions.gmsaCredentialSpec | |
+| securityContext.windowsOptions.hostProcess | |
+| securityContext.windowsOptions.runAsUserName | |
+
+## Container Fields
+
+| Field | Support |
+|---------------------------------------------------|---------|
+| name | ✅ |
+| image | ✅ |
+| imagePullPolicy | ✅ |
+| command | ✅ |
+| args | ✅ |
+| workingDir | ✅ |
+| ports.containerPort | ✅ |
+| ports.hostIP | ✅ |
+| ports.hostPort | ✅ |
+| ports<nolink>.name | ✅ |
+| ports.protocol | ✅ |
+| env<nolink>.name | ✅ |
+| env.value | ✅ |
+| env.valueFrom.configMapKeyRef.key | ✅ |
+| env<nolink>.valueFrom.configMapKeyRef.name | ✅ |
+| env.valueFrom.configMapKeyRef.optional | ✅ |
+| env.valueFrom.fieldRef | ✅ |
+| env.valueFrom.resourceFieldRef | ✅ |
+| env.valueFrom.secretKeyRef.key | ✅ |
+| env<nolink>.valueFrom.secretKeyRef.name | ✅ |
+| env.valueFrom.secretKeyRef.optional | ✅ |
+| envFrom<nolink>.configMapRef.name | ✅ |
+| envFrom.configMapRef.optional | ✅ |
+| envFrom.prefix | |
+| envFrom<nolink>.secretRef.name | ✅ |
+| envFrom.secretRef.optional | ✅ |
+| volumeMounts.mountPath | ✅ |
+| volumeMounts<nolink>.name | ✅ |
+| volumeMounts.mountPropagation | |
+| volumeMounts.readOnly | ✅ |
+| volumeMounts.subPath | |
+| volumeMounts.subPathExpr | |
+| volumeDevices.devicePath | |
+| volumeDevices<nolink>.name | |
+| resources.limits | ✅ |
+| resources.requests | ✅ |
+| lifecycle.postStart | |
+| lifecycle.preStop | |
+| terminationMessagePath | |
+| terminationMessagePolicy | |
+| livenessProbe | ✅ |
+| readinessProbe | |
+| startupProbe | |
+| securityContext.runAsUser | ✅ |
+| securityContext.runAsNonRoot | |
+| securityContext.runAsGroup | ✅ |
+| securityContext.readOnlyRootFilesystem | ✅ |
+| securityContext.procMount | |
+| securityContext.privileged | ✅ |
+| securityContext.allowPrivilegeEscalation | ✅ |
+| securityContext.capabilities.add | ✅ |
+| securityContext.capabilities.drop | ✅ |
+| securityContext.seccompProfile.type | |
+| securityContext.seccompProfile.localhostProfile | |
+| securityContext.seLinuxOptions.level | ✅ |
+| securityContext.seLinuxOptions.role | ✅ |
+| securityContext.seLinuxOptions.type | ✅ |
+| securityContext.seLinuxOptions.user | ✅ |
+| securityContext.windowsOptions.gmsaCredentialSpec | |
+| securityContext.windowsOptions.hostProcess | |
+| securityContext.windowsOptions.runAsUserName | |
+| stdin | |
+| stdinOnce | |
+| tty | |
+
+## PersistentVolumeClaim Fields
+
+| Field | Support |
+|--------------------|---------|
+| volumeName | |
+| storageClassName | ✅ |
+| volumeMode | |
+| accessModes | ✅ |
+| selector | |
+| resources.limits | |
+| resources.requests | ✅ |
diff --git a/docs/source/markdown/podman-kill.1.md b/docs/source/markdown/podman-kill.1.md
index 35ca9f74f..a4f80ac81 100644
--- a/docs/source/markdown/podman-kill.1.md
+++ b/docs/source/markdown/podman-kill.1.md
@@ -14,7 +14,7 @@ The main process inside each container specified will be sent SIGKILL, or any si
## OPTIONS
#### **--all**, **-a**
-Signal all running containers. This does not include paused containers.
+Signal all running and paused containers.
#### **--cidfile**
diff --git a/libpod/container.go b/libpod/container.go
index 64b4453fb..04a4ae64a 100644
--- a/libpod/container.go
+++ b/libpod/container.go
@@ -1331,8 +1331,7 @@ func (c *Container) getNetworkStatus() map[string]types.StatusBlock {
}
c.state.NetworkStatus = result
_ = c.save()
- // TODO remove debug for final version
- logrus.Debugf("converted old network result to new result %v", result)
+
return result
}
return nil
diff --git a/libpod/container_api.go b/libpod/container_api.go
index a6fcf709d..0fab36bdc 100644
--- a/libpod/container_api.go
+++ b/libpod/container_api.go
@@ -202,9 +202,8 @@ func (c *Container) Kill(signal uint) error {
}
}
- // TODO: Is killing a paused container OK?
switch c.state.State {
- case define.ContainerStateRunning, define.ContainerStateStopping:
+ case define.ContainerStateRunning, define.ContainerStateStopping, define.ContainerStatePaused:
// Note that killing containers in "stopping" state is okay.
// In that state, the Podman is waiting for the runtime to
// stop the container and if that is taking too long, a user
diff --git a/libpod/container_config.go b/libpod/container_config.go
index 3e85ad4d5..ae3bc5865 100644
--- a/libpod/container_config.go
+++ b/libpod/container_config.go
@@ -372,7 +372,6 @@ type ContainerMiscConfig struct {
// restart the container. Used only if RestartPolicy is set to
// "on-failure".
RestartRetries uint `json:"restart_retries,omitempty"`
- // TODO log options for log drivers
// PostConfigureNetNS needed when a user namespace is created by an OCI runtime
// if the network namespace is created before the user namespace it will be
// owned by the wrong user namespace.
diff --git a/libpod/container_exec.go b/libpod/container_exec.go
index c05e7fd94..1e8fce4da 100644
--- a/libpod/container_exec.go
+++ b/libpod/container_exec.go
@@ -279,8 +279,6 @@ func (c *Container) ExecStart(sessionID string) error {
// ExecStartAndAttach starts and attaches to an exec session in a container.
// newSize resizes the tty to this size before the process is started, must be nil if the exec session has no tty
-// TODO: Should we include detach keys in the signature to allow override?
-// TODO: How do we handle AttachStdin/AttachStdout/AttachStderr?
func (c *Container) ExecStartAndAttach(sessionID string, streams *define.AttachStreams, newSize *define.TerminalSize) error {
if !c.batched {
c.lock.Lock()
diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go
index 298eb1947..e19d75deb 100644
--- a/libpod/container_internal_linux.go
+++ b/libpod/container_internal_linux.go
@@ -1091,7 +1091,6 @@ func (c *Container) addNamespaceContainer(g *generate.Generator, ns LinuxNS, ctr
g.AddProcessEnv("HOSTNAME", hostname)
}
- // TODO need unlocked version of this for use in pods
nsPath, err := nsCtr.NamespacePath(ns)
if err != nil {
return err
@@ -3230,10 +3229,8 @@ func (c *Container) fixVolumePermissions(v *ContainerNamedVolume) error {
return err
}
- // TODO: For now, I've disabled chowning volumes owned by non-Podman
- // drivers. This may be safe, but it's really going to be a case-by-case
- // thing, I think - safest to leave disabled now and re-enable later if
- // there is a demand.
+ // Volumes owned by a volume driver are not chowned - we don't want to
+ // mess with a mount not managed by us.
if vol.state.NeedsChown && !vol.UsesVolumeDriver() {
vol.state.NeedsChown = false
diff --git a/libpod/plugin/volume_api.go b/libpod/plugin/volume_api.go
index a6d66a034..2818e70c1 100644
--- a/libpod/plugin/volume_api.go
+++ b/libpod/plugin/volume_api.go
@@ -22,9 +22,6 @@ import (
var json = jsoniter.ConfigCompatibleWithStandardLibrary
-// TODO: We should add syntax for specifying plugins to containers.conf, and
-// support for loading based on that.
-
// Copied from docker/go-plugins-helpers/volume/api.go - not exported, so we
// need to do this to get at them.
// These are well-established paths that should not change unless the plugin API
@@ -185,8 +182,7 @@ func (p *VolumePlugin) getURI() string {
}
// Verify the plugin is still available.
-// TODO: Do we want to ping with an HTTP request? There's no ping endpoint so
-// we'd need to hit Activate or Capabilities?
+// Does not actually ping the API, just verifies that the socket still exists.
func (p *VolumePlugin) verifyReachable() error {
if _, err := os.Stat(p.SocketPath); err != nil {
if os.IsNotExist(err) {
@@ -307,7 +303,6 @@ func (p *VolumePlugin) ListVolumes() ([]*volume.Volume, error) {
return nil, err
}
- // TODO: Can probably unify response reading under a helper
volumeRespBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrapf(err, "error reading response body from volume plugin %s", p.Name)
diff --git a/libpod/pod_api.go b/libpod/pod_api.go
index eede896a9..1c1e15984 100644
--- a/libpod/pod_api.go
+++ b/libpod/pod_api.go
@@ -152,8 +152,8 @@ func (p *Pod) stopWithTimeout(ctx context.Context, cleanup bool, timeout int) (m
return nil, err
}
- // TODO: There may be cases where it makes sense to order stops based on
- // dependencies. Should we bother with this?
+ // Stopping pods is not ordered by dependency. We haven't seen any case
+ // where this would actually matter.
ctrErrChan := make(map[string]<-chan error)
@@ -162,8 +162,9 @@ func (p *Pod) stopWithTimeout(ctx context.Context, cleanup bool, timeout int) (m
c := ctr
logrus.Debugf("Adding parallel job to stop container %s", c.ID())
retChan := parallel.Enqueue(ctx, func() error {
- // TODO: Might be better to batch stop and cleanup
- // together?
+ // Can't batch these without forcing Stop() to hold the
+ // lock for the full duration of the timeout.
+ // We probably don't want to do that.
if timeout > -1 {
if err := c.StopWithTimeout(uint(timeout)); err != nil {
return err
diff --git a/libpod/runtime.go b/libpod/runtime.go
index 00fa2fe88..e268c2d17 100644
--- a/libpod/runtime.go
+++ b/libpod/runtime.go
@@ -412,7 +412,6 @@ func makeRuntime(runtime *Runtime) (retErr error) {
return err
}
runtime.eventer = eventer
- // TODO: events for libimage
// Set up containers/image
if runtime.imageContext == nil {
@@ -517,8 +516,6 @@ func makeRuntime(runtime *Runtime) (retErr error) {
}
// Acquire the lock and hold it until we return
// This ensures that no two processes will be in runtime.refresh at once
- // TODO: we can't close the FD in this lock, so we should keep it around
- // and use it to lock important operations
aliveLock.Lock()
doRefresh := false
defer func() {
diff --git a/test/e2e/kill_test.go b/test/e2e/kill_test.go
index 552a7c15d..2a9a86729 100644
--- a/test/e2e/kill_test.go
+++ b/test/e2e/kill_test.go
@@ -128,6 +128,26 @@ var _ = Describe("Podman kill", func() {
Expect(podmanTest.NumberOfContainersRunning()).To(Equal(0))
})
+ It("podman kill paused container", func() {
+ ctrName := "testctr"
+ session := podmanTest.RunTopContainer(ctrName)
+ session.WaitWithDefaultTimeout()
+ Expect(session).Should(Exit(0))
+
+ pause := podmanTest.Podman([]string{"pause", ctrName})
+ pause.WaitWithDefaultTimeout()
+ Expect(pause).Should(Exit(0))
+
+ kill := podmanTest.Podman([]string{"kill", ctrName})
+ kill.WaitWithDefaultTimeout()
+ Expect(kill).Should(Exit(0))
+
+ inspect := podmanTest.Podman([]string{"inspect", "-f", "{{.State.Status}}", ctrName})
+ inspect.WaitWithDefaultTimeout()
+ Expect(inspect).Should(Exit(0))
+ Expect(inspect.OutputToString()).To(Or(Equal("stopped"), Equal("exited")))
+ })
+
It("podman kill --cidfile", func() {
tmpDir, err := ioutil.TempDir("", "")
Expect(err).To(BeNil())
diff --git a/troubleshooting.md b/troubleshooting.md
index cf554654b..6e7f5cf26 100644
--- a/troubleshooting.md
+++ b/troubleshooting.md
@@ -1217,3 +1217,17 @@ WARN[0000] Can't stat lower layer "/var/lib/containers/storage/overlay/l/7HS76F2
It is the user responsibility to make sure images in an additional
store are not deleted while being used by containers in another
store.
+
+### 36) Syncing bugfixes for podman-remote or setups using Podman API
+
+After upgrading Podman to a newer version an issue with the earlier version of Podman still presents itself while using podman-remote.
+
+#### Symptom
+
+While running podman remote commands with the most updated Podman, issues that were fixed in a prior version of Podman can arise either on the Podman client side or the Podman server side.
+
+#### Solution
+
+When upgrading Podman to a particular version for the required fixes, users often make the mistake of only upgrading the Podman client. However, suppose a setup uses `podman-remote` or uses a client that communicates with the Podman server on a remote machine via the REST API. In that case, it is required to upgrade both the Podman client and the Podman server running on the remote machine. Both the Podman client and server must be upgraded to the same version.
+
+Example: If a particular bug was fixed in `v4.1.0` then The Podman client` must have version `v4.1.0` as well the Podman server must have version `v4.1.0`.