Compare commits

...

No commits in common. "master" and "main" have entirely different histories.
master ... main

10237 changed files with 2719284 additions and 1 deletions

23
.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# ---> Go
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work

3
CODE-OF-CONDUCT.md Normal file
View File

@ -0,0 +1,3 @@
## The Podman Project Community Code of Conduct
The Podman project follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md).

421
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,421 @@
![PODMAN logo](https://raw.githubusercontent.com/containers/common/main/logos/podman-logo-full-vert.png)
# Contributing to Podman
We'd love to have you join the community!
Below summarizes the processes that we follow.
## Topics
* [Reporting Issues](#reporting-issues)
* [Working On Issues](#working-on-issues)
* [Contributing to Podman](#contributing-to-podman)
* [Continuous Integration](#continuous-integration) [![Build Status](https://api.cirrus-ci.com/github/containers/podman.svg)](https://cirrus-ci.com/github/containers/podman/main)
* [Submitting Pull Requests](#submitting-pull-requests)
* [Communications](#communications)
## Reporting Issues
Before reporting an issue, check our backlog of [open issues](https://github.com/containers/podman/issues) to see if someone else has already reported it.
If so, feel free to add your scenario, or additional information, to the discussion.
Or simply "subscribe" to it to be notified when it is updated.
Please do not add comments like "+1" or "I have this issue as well" without adding any new information.
Instead, please add a thumbs-up emoji to the original report.
Note: Older closed issues/PRs are automatically locked.
If you have a similar problem please open a new issue instead of commenting.
If you find a new issue with the project we'd love to hear about it!
The most important aspect of a bug report is that it includes enough information for us to reproduce it.
To make this easier, there are three types of issue templates you can use.
* If you have a bug to report, please use *Bug Report* template.
* If you have an idea to propose, please use the *Feature Request* template.
* If your issue is something else, please use the default *Blank issue* template.
Please include as much detail as possible, including all requested fields in the template.
Not having all requested information - for example, a full `podman info` - makes it much harder to find and fix issues.
A reproducer is the best thing you can include.
Reproducers make finding and fixing issues much easier for maintainers.
The easier it is for us to reproduce a bug, the faster it'll be fixed!
Please don't include any private/sensitive information in your issue!
Security issues should NOT be reported via Github and should instead be reported via the process described [here](https://github.com/containers/common/blob/main/SECURITY.md).
## Working On Issues
Once you have decided to contribute to Podman by working on an issue, check our backlog of [open issues](https://github.com/containers/podman/issues) looking for any that are unassigned.
If you want to work on a specific issue that is already assigned but does not appear to be actively being worked on, please ping the assignee in the issue and ask if you can take over.
If they do not respond after several days, you can notify a maintainer to have the issue reassigned.
When working on an issue, please assign it to yourself.
You can use the `/assign` bot command in a comment on an issue to assign it to yourself.
If you lack permissions to do so, you can ping the `@containers/podman-maintainers` group to have a maintainer set you as assignee.
If you are a maintainer of Podman project, please following the [instructions](https://github.com/containers/podman/blob/main/TRIAGE.md) to triage new issues.
## Contributing to Podman
This section describes how to make a contribution to Podman.
These instructions are geared towards using a Linux development machine, which is required for doing development on the Podman backend.
Development for the Windows and Mac clients can also be done on those operating systems.
Check out these instructions for building the Podman client on [MacOSX](./build_osx.md) or [Windows](./build_windows.md).
### Prepare your environment
Read the [install documentation to see how to install dependencies](https://podman.io/getting-started/installation#build-and-run-dependencies).
The install documentation will illustrate the following steps:
- Installation of required libraries and tools
- Installing Podman from source
The minimum version of Golang required to build Podman is contained in [go.mod](https://github.com/containers/podman/blob/main/go.mod#L5).
You will need to make sure your system's Go compiler is at least this version using the `go version` command.
### Fork and clone Podman
First, you need to fork this project on GitHub.
Then clone your fork locally:
```shell
$ git clone git@github.com:<you>/podman
$ cd ./podman/
```
### Using the Makefile
Podman uses a Makefile for common actions such as compiling Podman, building the documentation, and linting.
You can list available actions by using:
```shell
$ make help
Usage: make <target>
...output...
```
### Install required tools
Makefile allow you to install needed development tools (e.g. the linter):
```shell
$ make install.tools
```
### Building binaries
To build Podman binaries, you can run `make binaries`.
Built binaries will be placed in the `bin/` directory.
You can manually test to verify that Podman is working by running the binaries.
For further reading about branching [you can read this document](https://herve.beraud.io/containers/linux/podman/isolate/environment/2019/02/06/how-to-hack-on-podman.html).
### Building docs
To build Podman's manpages, you can run `make docs`.
Built documentation will be placed in the `docs/build/man` directory.
Markdown versions can be viewed in the `docs/source/markdown` directory.
Files suffixed with `.in` are preliminary versions that are compiled into the final markdown files.
## Libraries
Podman uses a large amount of vendored library code, contained in the `vendor/` directory.
This code is included in the Podman repository, but is actually maintained elsewhere.
Pull requests that change the vendor/ directory directly will not be accepted.
Instead, changes should be submitted to the original package (defined by the path in `vendor/`; for example, `vendor/github.com/containers/storage` is the [containers/storage library](https://github.com/containers/storage/).
Once the changes have been merged into the original package, Podman's vendor directory can be updated by using `go get` on the appropriate version of the package, then running `make vendor` or `make vendor-in-container`.
## Codebase structure
Description about important directories in our repository is found [here](./docs/CODE_STRUCTURE.md).
## Testing
Podman provides an extensive suite of regression tests in the `test/` directory.
There is a [readme](https://github.com/containers/podman/blob/main/test/README.md) file available with details about the tests and how to run them.
All pull requests should be accompanied by test changes covering the changes in the PR.
Pull requests without tests will receive additional scrutiny from maintainers and may be blocked from merging unless tests are added.
Maintainers will decide if tests are not necessary during review.
### Types of Tests
There are several types of tests run by Podman's upstream CI.
* Build testing (including cross-build tests, and testing to verify each commit in a PR builds on its own)
* Go format/lint checking
* Unit testing
* Integration testing (run on several operating systems, both root and rootless)
* System testing (again, run on several operating systems, root and rootless)
* API testing (validates the Podman REST API)
* Machine testing (validates `podman machine` on Windows and Mac hosts)
Changes will usually only need to be tested in one of these.
For example, if you were to make a change to `podman run`, you could test this in either the system tests or the integration tests.
It is not necessary to test a single change in multiple places.
### Go Format and lint
All code changes must pass ``make validatepr``.
### Integration Tests
Our primary means of performing integration testing for Podman is with the [Ginkgo](https://github.com/onsi/ginkgo) BDD testing framework.
This allows us to use native Golang to perform our tests and there is a strong affiliation between Ginkgo and the Go test framework.
Adequate test cases are expected to be provided with PRs.
For details on how to run the tests for Podman in your test environment, see the testing [README.md](test/README.md).
The integration tests are located in the `test/e2e/` directory.
### System Tests
The system tests are written in Bash using the BATS framework.
They provide less comprehensive coverage than the integration tests.
They are intended to validate Podman builds before they are shipped by distributions.
The system tests are located in the `test/system/` directory.
## Documentation
Make sure to update the documentation if needed.
Podman is primarily documented via its manpages, which are located under `docs/source/markdown`.
There are a number of automated tests to make sure the manpages are up to date.
These tests run on all submitted pull requests.
Full details on working with the manpages can be found in the [README](https://github.com/containers/podman/blob/main/docs/README.md) for the docs.
Podman also provides Swagger documentation for the REST API.
Swagger is generated from comments on registered handlers located in the `pkg/api/server/` directory.
All API changes should update these Swagger comments to ensure the documentation remains accurate.
## Submitting Pull Requests
No Pull Request (PR) is too small!
Typos, additional comments in the code, new test cases, bug fixes, new features, more documentation, ... it's all welcome!
While bug fixes can first be identified via an "issue" in Github, that is not required.
It's ok to just open up a PR with the fix, but make sure you include the same information you would have included in an issue - like how to reproduce it.
PRs for new features should include some background on what use cases the new code is trying to address.
When possible and when it makes sense, try to break-up larger PRs into smaller ones - it's easier to review smaller code changes.
But only if those smaller ones make sense as stand-alone PRs.
Pull requests should be submitted to the main branch of the Podman repository. Bug fixes may be cherry-picked or back-ported
to Podman release branches but must first be merged upstream. Maintainers reserve the right to not accept any pull requests
to any release branches.
Regardless of the type of PR, all PRs should include:
* Well-documented code changes, both through comments in the code itself and high-quality commit messages.
* Additional tests. 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. Repo
admins may bypass this restriction by setting the 'No New Tests' GitHub
label on the PR).
* Documentation updates to reflect the changes made in the pull request.
Squash your commits into logical pieces of work that might want to be reviewed separate from the rest of the PRs.
Squashing down to just one commit is also acceptable since in the end the entire PR will be reviewed anyway.
When in doubt, squash.
When your PR fixes an issue, please note that by including `Fixes: #00000` in the commit description.
More details on this are below, in the "Describe your changes in Commit Messages" section.
The Podman repo follows a two-ack policy for merges.
PRs will be approved by an [approver][owners] listed in [`OWNERS`](OWNERS).
They will then be merged by a repo owner.
Two reviews are required for a pull request to merge.
### Describe your Changes in Commit Messages
Describe your problem.
Whether your patch is a one-line bug fix or 5000 lines of a new feature, there must be an underlying problem that motivated you to do this work.
Convince the reviewer that there is a problem worth fixing and that it makes sense for them to read past the first paragraph.
Describe user-visible impact.
Straight up crashes and lockups are pretty convincing, but not all bugs are that blatant.
Even if the problem was spotted during code review, describe the impact you think it can have on users.
Keep in mind that the majority of users run packages provided by distributions, so include anything that could help route your change downstream.
Quantify optimizations and trade-offs.
If you claim improvements in performance, memory consumption, stack footprint, or binary size, include
numbers that back them up.
But also describe non-obvious costs.
Optimizations usually arent free but trade-offs between CPU, memory, and readability; or, when it comes to heuristics, between different workloads.
Describe the expected downsides of your optimization so that the reviewer can weigh costs against
benefits.
Once the problem is established, describe what you are actually doing about it in technical detail.
Its important to describe the change in plain English for the reviewer to verify that the code is behaving as you intend it to.
Solve only one problem per patch.
If your description starts to get long, thats a sign that you probably need to split up your patch.
If the patch fixes a logged bug entry, refer to that bug entry by number or URL.
If the patch follows from a mailing list discussion, give a URL to the mailing list archive.
Please format these lines as `Fixes:` followed by the URL or, for Github bugs, the bug number preceded by a #.
For example:
```
Fixes: #00000
Fixes: https://github.com/containers/common/issues/00000
Fixes: https://issues.redhat.com/browse/RHEL-00000
Fixes: RHEL-00000
```
However, try to make your explanation understandable without external resources.
In addition to giving a URL to a mailing list archive or bug, summarize the relevant points of the discussion that led to the patch as submitted.
If you want to refer to a specific commit, dont just refer to the SHA-1 ID of the commit.
Please also include the oneline summary of the commit, to make it easier for reviewers to know what it is about. If the commit was merged in Github, referring to a Github PR number is also a good option, as that will retain all discussion from development, and makes including a summary less critical.
Examples:
```
Commit f641c2d9384e ("fix bug in rm -fa parallel deletes") [...]
PR #00000
```
When referring to a commit by SHA, you should also be sure to use at least the first twelve characters of the SHA-1 ID.
The Podman repository holds a lot of objects, making collisions with shorter IDs a real possibility.
Bear in mind that, even if there is no collision with your six-character ID now, that condition may change five years from now.
The following git config settings can be used to add a pretty format for outputting the above style in the git log or git show commands:
```
[core]
abbrev = 12
[pretty]
fixes = Fixes: %h (\"%s\")
```
### Sign your PRs
The sign-off is a line at the end of the explanation for the patch.
Your signature certifies that you wrote the patch or otherwise have the right to pass it on as an open-source patch.
The rules are simple: if you can certify the below (from [developercertificate.org](https://developercertificate.org/)):
```
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
660 York Street, Suite 102,
San Francisco, CA 94110 USA
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
Then you just add a line to every git commit message:
Signed-off-by: Joe Smith <joe.smith@email.com>
Use your real name (sorry, no pseudonyms or anonymous contributions).
If you set your `user.name` and `user.email` git configs, you can sign your commit automatically with `git commit -s`.
### Reviewing PRs
If you are a maintainer of Podman project, please following the [guidelines](https://github.com/containers/podman/blob/main/REVIEWING.md) on how to review a PR.
### Continuous Integration
All pull requests automatically run Podman's test suite.
The tests have been configured such that only tests relevant to the code changed will be run.
For example, a documentation-only PR with no code changes will run a substantially reduced set of tests.
To force a PR to run all tests, you can include the string `[CI:ALL]` in the PR title, but this is almost never necessary.
There is always additional complexity added by automation, and so it sometimes can fail for any number of reasons.
This includes post-merge testing on all branches, which you may occasionally see [red bars on the status graph](https://cirrus-ci.com/github/containers/podman/main).
Most notably, the tests will occasionally flake.
If you see a single test on your PR has failed, and you do not believe it is caused by your changes, you can rerun the tests.
If you lack permissions to rerun the tests, please ping the maintainers using the `@containers/podman-maintainers` group and request that the failing test be rerun.
If you see multiple test failures, you may wish to check the status graph mentioned above.
When the graph shows mostly green bars on the right, it's a good indication the main branch is currently stable.
Alternating red/green bars is indicative of a testing "flake", and should be examined (anybody can do this):
* *One or a small handful of tests, on a single task, (i.e. specific distro/version)
where all others ran successfully:* Frequently the cause is networking or a brief
external service outage. The failed tasks may simply be re-run by pressing the
corresponding button on the task details page.
* *Multiple tasks failing*: Logically this should be due to some shared/common element.
If that element is identifiable as a networking or external service (e.g. packaging
repository outage), a re-run should be attempted.
* *All tasks are failing*: If a common element is **not** identifiable as
temporary (i.e. container registry outage), please seek assistance via
[the methods below](#communications) as this may be early indication of
a more serious problem.
In the (hopefully) rare case there are multiple, contiguous red bars, this is
a ***very bad*** sign. It means additional merges are occurring despite an uncorrected
or persistently faulty condition. This risks additional bugs being introduced
and further complication of necessary corrective measures. Most likely people
are aware and working on this, but it doesn't hurt [to confirm and/or try and help
if possible.](#communications).
NOTE: Jobs triggered by Packit are not merge blockers and should be considered of secondary importance.
Contributors and maintainers should feel free to ignore failure status on such jobs.
## Communications
If you need help, you can contact the maintainers using the channels mentioned in Podman's [communications](https://github.com/containers/podman/blob/main/README.md#communications) document.
For discussions around issues/bugs and features, you can use the GitHub
[issues](https://github.com/containers/podman/issues)
and
[PRs](https://github.com/containers/podman/pulls)
tracking system.
### Bot Interactions
The primary human-interface is through comments in pull-requests.
Some of these are outlined below, along with their meaning and intended usage.
Some of them require the comment author hold special privileges on the github repository.
Others can be used by anyone.
* ``/close``: Closes an issue or PR.
* ``/approve``: Mark a PR as appropriate to the project, and as close to meeting
met all the contribution criteria above. Adds the *approved* label, marking
it as ready for review and possible future merging.
* ``/lgtm``: A literal "Stamp of approval", signaling okay-to-merge. This causes
the bot to ad the *lgtm* label, then attempt a merge. In other words - Never,
ever, ever comment ``/lgtm``, unless a PR has actually, really, been fully
reviewed. The bot isn't too smart about these things, and could merge
unintentionally. Instead, just write ``LGTM``, or
spell it out.
* ``/hold`` and ``/unhold``: Override the automatic handling of a request. Either
put it on hold (no handling) or remove the hold (normal handling).
* ``[ci skip]``: [Adding `[ci skip]` within the HEAD commit](https://cirrus-ci.org/guide/writing-tasks/#conditional-task-execution)
will cause Cirrus CI to ***NOT*** execute tests for the PR or after merge. This
is useful in only one instance: Your changes are absolutely not exercised by
any test. For example, documentation changes. ***IMPORTANT NOTE*** **Other
automation may interpret the lack of test results as "PASSED" and unintentional
merge a PR. Consider also using `/hold` in a comment, to add additional
protection.**
[The complete list may be found on the command-help page.](https://prow.k8s.io/command-help)
However, not all commands are implemented for this repository.
If in doubt, ask a maintainer.

111
DISTRO_PACKAGE.md Normal file
View File

@ -0,0 +1,111 @@
# Podman Packaging
This document is intended for Podman *packagers*: those very few individuals
responsible for building and shipping Podman on Linux distributions.
Document verified accurate as of Podman 5.2, 2024-10-16.
## Building Podman
This document assumes you are able to build executables up to and
including `make install`.
See [Building from Source](https://podman.io/docs/installation#building-from-source)
on podman.io for possibly-outdated instructions.
## Package contents
Everything installed by `make install`, obviously.
Upstream splits Podman into multiple subpackages and we encourage you
to consider doing likewise: some users may not want `podman-remote`
or `-machine` or the test suite.
The best starting point is the
[RPM spec file](https://github.com/containers/podman/blob/main/rpm/podman.spec).
This illustrates the subpackage breakdown as well as top-level dependencies.
## Dependencies
Podman requires a *runtime*, a *runtime monitor*, a *pause process*,
and *networking tools*. In Fedora, some of these requirements are indirectly
specified via [containers-common](https://github.com/containers/common);
the nested tree looks like this:
```
Podman
├── Requires: catatonit
├── Requires: conmon
└── Requires: containers-common-extra
├── Requires: crun
├── Requires: netavark
└── Requires: passt
```
### Runtime: crun
The only runtime supported upstream is [crun](https://github.com/containers/crun),
but different distros may wish to offer other options to their users. Your package
must, directly or indirectly, list a runtime prerequisite.
Heads up: you may end up being responsible for packaging this runtime, or at the
very least working closely with the package maintainer. The best starting point
for crun is its
[RPM spec file](https://github.com/containers/crun/blob/main/rpm/crun.spec).
### Pause process: catatonit
The pause process serves as a container `init`, reaping PIDs and handling signals.
As of this writing, Podman uses an external tool,
[catatonit](https://github.com/openSUSE/catatonit). This may be subject
to change in future Podman versions.
If you need to package catatonit, a good starting point might be its
[Fedora specfile](https://src.fedoraproject.org/rpms/catatonit/blob/rawhide/f/catatonit.spec).
### Runtime Monitor: conmon
The only working monitor is [conmon](https://github.com/containers/conmon).
There is a Rust implementation in the works,
[conmon-rs](https://github.com/containers/conmon-rs), but efforts
to make it work with Podman have stalled for years.
Heads up: you may end up being responsible for packaging conmon.
The best starting point is its
[RPM spec file](https://github.com/containers/conmon/blob/main/rpm/conmon.spec).
### Networking Tools: netavark, aardvark-dns, passt
Networking differs between *root* and *rootless*: [passt](https://passt.top/)
(also referred to as "pasta") is only needed for rootless.
[netavark](https://github.com/containers/netavark/) and
[aardvark-dns](https://github.com/containers/aardvark-dns/)
are needed for both root and rootless podman.
Heads up: you will probably end up being responsible for packaging
at least some of these. The best starting points are their respective
RPM spec files:
[netavark](https://github.com/containers/netavark/blob/main/rpm/netavark.spec),
[aardvark-dns](https://github.com/containers/aardvark-dns/blob/main/rpm/aardvark-dns.spec).
Netavark and aardvark-dns must be packaged in lockstep down
to the major-minor level: version `X.Y` of either is only
guaranteed to work with `X.Y` of the other. If you are responsible
for packaging these, make sure you set up interpackage dependencies
appropriately to prevent version mismatches between them.
## Metapackage: containers-common
This package provides config files, man pages, and (at the
packaging level) dependencies. There are good reasons for
keeping this as a separate package, the most important one
being that `buildah` and `skopeo` rely on this same content.
Also important is the ability for individual distros to
fine-tune config settings and dependencies.
You will probably be responsible for packaging this.
The best starting point is its
[RPM spec file](https://github.com/containers/common/blob/main/rpm/containers-common.spec).

54
DOWNLOADS.md Normal file
View File

@ -0,0 +1,54 @@
![PODMAN logo](https://raw.githubusercontent.com/containers/common/main/logos/podman-logo-full-vert.png)
# Downloads
## Latest signed/official
[The latest Podman release version is always available on the GitHub releases
page](https://github.com/containers/podman/releases/latest). These are official,
signed, sealed, and blessed artifacts intended for general use. Though for
super-serious production use, please utilize the pre-packaged podman provided
by your OS/Distro vendor.
## CI Artifacts
If you're looking for something even more bleeding-edge, esp. for testing
purposes and/or in other CI systems. There are several permalinks available
depending on how much you want to download. Everything inside has at least
gone through and passed CI testing. However, **they are all unsigned**, and
frequently changing. Perfectly fine for non-production testing but please
don't take them beyond that.
* [Giant artifacts
archive](https://api.cirrus-ci.com/v1/artifact/github/containers/podman/Artifacts/binary.zip)
containing every binary produced in CI from the most recent successful run.
*Warning*: This file is pretty large, expect a 700+MB download. However,
it's guaranteed to contain everything, where as the items below can change
or become unavailable due to somebody forgetting to update this doc.
<!--
WARNING: The items linked below all come from scripts in the `artifacts_task`
map of `.cirrus.yml`. When adding or updating any item below, please ensure it
matches corresponding changes in the artifacts task.
-->
* Raw dynamically linked ELF (x86_64) binaries for [podman](https://api.cirrus-ci.com/v1/artifact/github/containers/podman/Artifacts/binary/podman)
, [podman-remote](https://api.cirrus-ci.com/v1/artifact/github/containers/podman/Artifacts/binary/podman-remote)
, [quadlet](https://api.cirrus-ci.com/v1/artifact/github/containers/podman/Artifacts/binary/quadlet)
, and
[rootlessport](https://api.cirrus-ci.com/v1/artifact/github/containers/podman/Artifacts/binary/rootlessport) -
Built on the latest supported Fedora release.
* MacOS
[universal](https://api.cirrus-ci.com/v1/artifact/github/containers/podman/Artifacts/binary/podman-installer-macos-universal.pkg)
,
[x86_64](https://api.cirrus-ci.com/v1/artifact/github/containers/podman/Artifacts/binary/podman-installer-macos-amd64.pkg)
, and
[arm64](https://api.cirrus-ci.com/v1/artifact/github/containers/podman/Artifacts/binary/podman-installer-macos-arm64.pkg)
installation packages. Again, these are **not** signed, so expect warnings if you try to install them.
There's also binary release *ZIP-files* for
[darwin_amd64](https://api.cirrus-ci.com/v1/artifact/github/containers/podman/Artifacts/binary/podman-remote-release-darwin_amd64.zip)
and
[darwin_arm64](https://api.cirrus-ci.com/v1/artifact/github/containers/podman/Artifacts/binary/podman-remote-release-darwin_arm64.zip).
* Windows [podman-remote](https://api.cirrus-ci.com/v1/artifact/github/containers/podman/Artifacts/binary/podman-remote-release-windows_amd64.zip) for x86_64 only.

212
GOVERNANCE.md Normal file
View File

@ -0,0 +1,212 @@
# Project Governance
* [Contributor Ladder](#contributor-ladder-template)
* [Contributor](#contributor)
* [Reviewer](#reviewer)
* [Maintainer](#maintainer)
* [Core Maintainer](#core-maintainer)
* [Community Manager](#community-manager)
* [Emeritus Maintainer](#emeritus-maintainer)
* [Maintainers File](#maintainers-file)
* [Inactivity](#inactivity)
* [Involuntary Removal](#involuntary-removal-or-demotion)
* [Stepping Down/Emeritus Process](#stepping-downemeritus-process)
* [Updates to this Document](#updates-to-this-document)
* [Contact](#contact)
# Podman Project
This document defines the governance of the Podman Project, including its subprojects. It defines the various roles our maintainers fill, how to become a maintainer, and how project-level decisions are made.
The Podman project currently consists of the Podman project (the repository containing this file) and two subprojects:
* [Buildah](https://github.com/containers/buildah)
* [Skopeo](https://github.com/containers/skopeo/)
# Contributor Ladder
The Podman project has a number of maintainer roles arranged in a ladder. Each role is a rung on the ladder, with different responsibilities and privileges. Community members generally start at the first levels of the "ladder" and advance as their involvement in the project grows. Our project members are happy to help you advance along the contributor ladder. At all levels, contributors are required to follow the CNCF Code of Conduct (COC).
Each of the project member roles below is organized into lists of three types of things.
* "Responsibilities" functions of a member
* "Requirements" qualifications of a member
* "Privileges" entitlements of member
### Contributor
Description: A Contributor supports the project and adds value to it. Contributions need not be code. People at the Contributor level may be new contributors, or they may only contribute occasionally.
* Responsibilities include:
* Follow the CNCF CoC
* Follow the project contributing guide
* Requirements (one or several of the below):
* Report and sometimes resolve issues against any of the projects repositories
* Occasionally submit PRs against any of the projects repositories
* Contribute to project documentation, including the manpages, tutorials, and Podman.io
* Attend community meetings when reasonable
* Answer questions from other community members on the mailing list, Slack, Matrix, and other communication channels
* Assist in triaging issues, following the [issue triage guide](./TRIAGE.md)
* Assist in reviewing pull requests, including testing patches when applicable
* Test release candidates and provide feedback
* Promote the project in public
* Help run the project infrastructure
* Privileges:
* Invitations to contributor events
* Eligible to become a Reviewer
### Reviewer
Description: A Reviewer has responsibility for the triage of issues and review of pull requests on the Podman project or a subproject, consisting of one or more of the Git repositories that form the project. They are collectively responsible, with other Reviewers, for reviewing changes to the repository or repositories and indicating whether those changes are ready to merge. They have a track record of contribution and review in the project.
Reviewers have all the rights and responsibilities of a Contributor, plus:
* Responsibilities include:
* Regular contribution of pull requests to the Podman project or its subprojects
* Triage of GitHub issues on the Podman project or its subprojects
* Regularly fixing GitHub issues on the Podman project or its subprojects
* Following the [reviewing guide](./REVIEWING.md) and [issue triage guide](./TRIAGE.md)
* A sustained high level of pull request reviews on the Podman project or one of its subprojects
* Assisting new Contributors in their interactions with the project
* Helping other contributors become reviewers
* Requirements:
* Has a proven record of good-faith contributions to the project as a Contributor for a period of at least 6 months. The time requirement may be overridden by a supermajority (66%) vote of Maintainers and Core Maintainers.
* Has participated in pull request review and/or issue triage on the project for at least 6 months. The time requirement may be overridden by a supermajority (66%) vote of Maintainers and Core Maintainers.
* Is supportive of new and occasional contributors and helps get useful PRs in shape to merge
* Additional privileges:
* Has rights to approve pull requests in the Podman project or a subproject, marking them as ready for a Maintainer to review and merge
* Can recommend and review other contributors to become Reviewers
* Has permissions to change labels on Github to aid in triage
In repositories using an OWNERS file, Reviewers are listed as Reviewers in that file.
#### The process of becoming a Reviewer is:
1. The contributor must be sponsored by a Maintainer. That sponsor will open a PR against the appropriate repository, which adds the nominee to the [MAINTAINERS.md](./MAINTAINERS.md) file as a reviewer.
2. The contributor will add a comment to the pull request indicating their willingness to assume the responsibilities of a Reviewer.
3. At least two Maintainers of the repository must concur to merge the PR.
### Maintainer
Description: Maintainers are established contributors with deep technical knowledge of the Podman project and/or one of its subprojects. Maintainers are granted the authority to merge pull requests, and are expected to participate in making decisions about the strategy and priorities of the project. Maintainers are responsible for code review and merging in a single repository or subproject. It is possible to become Maintainer of additional repositories or subprojects, but each additional repository or project will require a separate application and vote. They are able to participate in all maintainer activities, including Core Maintainer meetings, but do not have a vote at Core Maintainer meetings.
In repositories using an OWNERS file, Maintainers are listed as Approvers in that file.
A Maintainer must meet the responsibilities and requirements of a Reviewer, plus:
* Responsibilities include:
* Sustained high level of reviews of pull requests to the project or subproject, with a goal of one or more a week when averaged across the year.
* Merging pull requests which pass review
* Mentoring new Reviewers
* Participating in CNCF maintainer activities for the projects they are maintainers of
* Assisting Core Maintainers in determining strategy and policy for the project
* Participating in, and leading, community meetings
* Requirements
* Experience as a Reviewer for at least 6 months, or status as an Emeritus Maintainer. The time requirement may be overridden by a supermajority (66%) vote of Maintainers and Core Maintainers.
* Demonstrates a broad knowledge of the project or one or more of its subprojects
* Is able to exercise judgment for the good of the project, independent of their employer, friends, or team
* Mentors contributors, reviewers, and new maintainers
* Collaborates with other Maintainers to work on complex contributions
* Can commit to maintaining a high level of contribution to the project or one of its subprojects
* Additional privileges:
* Represent the project in public as a senior project member
* Represent the project in interactions with the CNCF
* Have a voice, but not a vote, in Core Maintainer decision-making meetings
#### Process of becoming a maintainer:
1. A current reviewer must be sponsored by a Maintainer of the repository in question or a Core Maintainer. The Maintainer or Core Maintainer will open a PR against the repository and add the nominee as a Maintainer in the [MAINTAINERS.md](./MAINTAINERS.md) file. The need for a sponsor is removed for Emeritus Maintainers, who may open this pull request themselves.
2. The nominee will add a comment to the PR confirming that they agree to all requirements and responsibilities of becoming a Maintainer.
3. A majority of the current Maintainers of the repository or subproject (including Core Maintainers) must then approve the PR. The need for a majority is removed for Emeritus Maintainers, who require only 2 current Maintainers or Core Maintainers to approve their return.
### Core Maintainer
Description: As the Podman project is composed of a number of subprojects, most maintainers will not have full knowledge of the full project and all its technical aspects. Those that do are eligible to become Core Maintainers, responsible for decisions affecting the entire project. Core Maintainers may act as a maintainer in all repositories and subprojects of the Podman Project. It is recognized that fulfilling all responsibilities of a maintainer on all project repositories is an excessive time commitment, so Core Maintainers are encouraged to choose one repository to specialize in and to spend most of their time working in that repository. Core Maintainers are encouraged to assist other repositories that require additional reviews as time allows, and should make an effort to review pull requests in other repositories that will affect multiple repositories (especially ones that will effect the repository they have chosen to specialize in).
* Responsibilities include:
* All responsibilities of a maintainer on a single repository
* Determining strategy and policy for the project
* Requirements
* Experience as a Maintainer for at least 3 months
* Demonstrates a broad knowledge of all components, repositories, and subprojects of the Podman project.
* Is able to exercise judgment for the good of the project, independent of their employer, friends, or team
* Mentors new Maintainers and Core Maintainers
* Able to make decisions and contributions affecting the whole project, including multiple subprojects and repositories
* Can commit to maintaining a high level of contribution to the project as a whole
* Additional privileges:
* Merge privileges on all repositories in the project
* Represent the project in public as a senior project member
* Represent the project in interactions with the CNCF
* Have a vote in Core Maintainer decision-making meetings
#### Process of becoming a Core Maintainer:
1. A current maintainer must be sponsored by Core Maintainer. The Core Maintainer will open a PR against the main Podman repository and add the nominee as a Core Maintainer in the [MAINTAINERS.md](./MAINTAINERS.md) file.
2. The nominee will add a comment to the PR confirming that they agree to all requirements and responsibilities of becoming a Core Maintainer.
3. A majority of the current Core Maintainers must then approve the PR.
4. If, for some reason, all existing members are inactive according to the Inactivity policy below or there are no Core Maintainers due to resignations, a supermajority (66%) vote of maintainers can bypass this process and approve new Core Maintainers directly.
### Community Manager
Description: Community managers are responsible for the projects community interactions, including project social media, website maintenance, gathering metrics, managing the new contributor process, ensuring documentation is easy to use and welcoming to new users, and managing the projects interactions with the CNCF. This is a nontechnical role, and as such does not require technical contribution to the project.
* Responsibilities include:
* Participating in CNCF maintainer activities
* Arranging, participating in, and leading, community meetings
* Managing the project website and gathering associated metrics
* Managing the projects social media accounts and mailing lists and gathering associated metrics
* Creating and publishing minutes from Core Maintainer meetings
* Requirements
* Sustained high level of contribution to the community, including attending and engaging in community meetings, contributions to the website, and contributions to documentation, for at least six months
* Is able to exercise judgment for the good of the project, independent of their employer, friends, or team
* Can commit to maintaining a high level of contribution to the project's community, website, and social media presence
* Advocates for the community in Maintainer and Core Maintainer meetings
* Additional privileges:
* Represent the project in public
* Represent the project in interactions with the CNCF
* Have a voice, but not a vote, in Core Maintainer decision-making meetings
#### Process of becoming a Community Manager:
1. Community Managers must be sponsored by a Core Maintainer. The Core Maintainer will open a PR against the main Podman repository and add the nominee as a Community Manager in the [MAINTAINERS.md](./MAINTAINERS.md) file.
2. The nominee will add a comment to the PR confirming that they agree to all requirements and responsibilities of becoming a Community Manager.
3. A majority of the current Core Maintainers must then approve the PR.
### Emeritus Maintainer
Emeritus Maintainers are former Maintainers or Core Maintainers whose status has lapsed, either voluntarily or through inactivity. We recognize that these former maintainers still have valuable experience and insights, and maintain Emeritus status as a way of recognizing this. Emeritus Maintainer also offers a fast-tracked path to becoming a Maintainer again, should the contributor wish to return to the project.
Emeritus Maintainers have no responsibilities or requirements beyond those of an ordinary Contributor.
#### Process of becoming an Emeritus Maintainer:
1. A current Maintainer or Core Maintainer may voluntarily resign from their position by making a pull request changing their role in the OWNERS file. They may choose to remove themselves entirely or to change their role to Emeritus Maintainer.
2. Maintainers and Core Maintainers removed due to the Inactivity policy below may be moved to Emeritus Status.
---
# Maintainers File
The definitive source of truth for maintainers of this repository is the local [MAINTAINERS.md](./MAINTAINERS.md) file. The [MAINTAINERS.md](./MAINTAINERS.md) file in the main Podman repository is used for project-spanning roles, including Core Maintainer and Community Manager. Some repositories in the project will also have a local [OWNERS](./OWNERS) file, which the CI system uses to map users to roles. Any changes to the [OWNERS](./OWNERS) file must make a corresponding change to the [MAINTAINERS.md](./MAINTAINERS.md) file to ensure that file remains up to date. Most changes to [MAINTAINERS.md](./MAINTAINERS.md) will require a change to the repositorys [OWNERS](./OWNERS) file (e.g., adding a Reviewer), but some will not (e.g., promoting a Maintainer to a Core Maintainer, which comes with no additional CI-related privileges).
---
# Inactivity
* Inactivity is measured by one or more of the following:
* Periods of no contribution of code, pull request review, or participation in issue triage for longer than 12 months
* Periods of no communication for longer than 3 months
* Consequences of being inactive include:
* Involuntary removal or demotion
* Being asked to move to Emeritus status
---
# Involuntary Removal or Demotion
Involuntary removal/demotion of a contributor happens when responsibilities and requirements aren't being met. This may include repeated patterns of inactivity, an extended period of inactivity, a period of failing to meet the requirements of your role, and/or a violation of the Code of Conduct. This process is important because it protects the community and its deliverables while also opening up opportunities for new contributors to step in.
Involuntary removal or demotion of Maintainers and Reviewers is handled through a vote by a majority of the current Maintainers. Core Maintainers may be involuntarily removed by a majority vote of current Core Maintainers or, if all Core Maintainers have stepped down or are inactive according to the inactivity policy, by a supermajority (66%) vote of maintainers.
---
# Stepping Down/Emeritus Process
If and when contributors' commitment levels change, contributors can consider stepping down (moving down the contributor ladder) vs moving to emeritus status (completely stepping away from the project).
Maintainers and Reviewers should contact the Maintainers about changing to Emeritus status, or reducing your contributor level. Core Maintainers should contact other Core Maintainers.
---
# Updates to this document
Updates to this Governance document require approval from a supermajority (66%) vote of the Core Maintainers.
# Contact
* For inquiries, please reach out to:
* [Tom Sweeney, Community Manager](tsweeney@redhat.com)

123
ISSUE.md Normal file
View File

@ -0,0 +1,123 @@
# Issue reporting on Podman
The Podman team cares for our users and our communities. When someone has a problem with
Podman and takes the time to report an [issue](https://github.com/containers/podman/issues)
on our Github, we deeply appreciate the effort. We want to help. Consider reading
our [support](SUPPORT.md) document prior to submitting an issue as well.
## Considerations when reporting an issue upstream
### Where to report your issue
If you are running Podman acquired from a Linux distribution and that Linux distribution has a
bug reporting mechanism, then please report the bug there. To report an issue on our
github repository, use the issue tab and click [New Issue](https://github.com/containers/podman/issues/new/choose)
### Development or latest version
We view this Github repository as an upstream repository for development of the latest
of Podman in the main branch.
When reporting an issue, it should ideally be for the main branch of our repository. Please make
an effort to reproduce the issue using the main branch whenever possible. An issue can also
be written against the latest released version of Podman.
The term "latest version" refers to our mainline development tree or the
[latest release](https://github.com/containers/podman/releases/latest).
### Bugs vs features
A bug is when something in Podman is not working as it should or has been described. A
feature or enhancement is when you would like Podman to behave differently.
### Use the issue template when reporting bugs
When you report an issue on the upstream repository, be sure to fill out the entire template.
You must provide the required `podman info` wherever possible as it helps us diagnose
your report. If possible, always provide a _reliable reproducer_. This is extremely
helpful for us during triage and bug fixing. Good examples of a reliable repoducer are:
* Provide precise Podman commands
* Use generic images (like fedora/alpine/debian) where possible to reduce the chance your
container images was a contributor. Abstracting away from the functional purpose helps
diagnoses and reduces noise for us.
* If using the RESTFUL API, providing curl commands as a repoducer is preferred. Be sure
to provide the same data (or sample data) for things like POST.
* Not requiring the use of a third party tool to reproduce the problem
### Look through existing issues before reporting a new issue
Managing issues is a time consuming processes for maintainers. You can save us time by
making sure the issue you want to report has not already been reported. It is appropriate
to comment on the existing issue with relevant information.
### Why was my issue report closed
Issues filed upstream may be closed by a maintainer for the following reasons:
* A fix for the issue has been merged into the main branch of our upstream
repository. It is possible that the bug was already fixed upstream as well.
* The reported issue is a duplicate.
* The issue is reported against a [distribution that has a bug reporting mechanism](#where-to-report-your-issue)
or paid support.
* The issue was reported using an [older version](#development-or-latest-version) of Podman.
* A maintainer determines the code is working as designed.
* The issue has become [stale](#-stale) and reporters are not responding.
* We were unable to reproduce the issue, or there was insufficient information to reproduce the issue.
* One or more maintainers have decided a feature will not be implemented or an issue will not be fixed.
#### Definitions
[**stale**](https://github.com/containers/podman/issues?q=is%3Aopen+is%3Aissue+sort%3Acreated-asc+label%3Astale-issue): open, but no activity in the last thirty days.
**crickets**: closed due to lack of response from reporting party.
[**jetsam**](https://github.com/containers/podman/issues?q=is%3Aissue+label%3Ajetsam+is%3Aclosed): closed without being implemented. A deliberate decision made in recognition of human limitations.
#### Process
In order of judgment, from least to most.
##### &rarr; stale
Issues are marked with the label *stale-issue* by a [github action](https://github.com/containers/podman/blob/main/.github/workflows/stale.yml) that runs daily at 00:00 UT. This also triggers an email alert to subscribers on that issue.
Judgment: typically a team member will skim the issue, then decide whether to:
* remove the label; or
* close the issue (see below); or
* do nothing.
This is informal: there is no guarantee that anyone will actually do this.
##### &rarr; crickets
Typically done by a team member after receiving a *stale-issue* email.
Judgment:
* there is not enough information to act on the issue; and
* someone on the team has asked the reporter for more details (like NEEDINFO); and
* the reporter has not responded.
There is no actual *crickets* label. There is no automated way to
find issues that have been closed for this reason.
##### &rarr; jetsam
Last-resort closing of an issue that will not be worked on.
Factors:
* issue has remained open for over sixty days; and
* reporter is responsive, and still wishes to have the issue addressed (as does the team).
Judgment:
* the issue is too difficult or complicated or hard to track down.
* decision should be made by two or more team members, with discussion in the issue/PR itself.
When such an issue is closed, team member should apply the *jetsam* label.

201
LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

45
MAINTAINERS.md Normal file
View File

@ -0,0 +1,45 @@
# Podman Maintainers
[GOVERNANCE.md](https://github.com/containers/podman/blob/main/GOVERNANCE.md)
describes the Podman project's governance and the Project Roles used below.
Please note that this file only includes Podman's Maintainers and Reviewers.
Maintainers and Reviewers for the Skopeo and Buildah projects are found in their respective repository's MAINTAINERS.md files.
## Maintainers
| Maintainer | GitHub ID | Project Roles | Affiliation |
|-------------------|----------------------------------------------------------|----------------------------------|----------------------------------------------|
| Brent Baude | [baude](https://github.com/baude) | Core Maintainer | [Red Hat](https://github.com/RedHatOfficial) |
| Nalin Dahyabhai | [nalind](https://github.com/nalind) | Core Maintainer | [Red Hat](https://github.com/RedHatOfficial) |
| Matthew Heon | [mheon](https://github.com/mheon) | Core Maintainer | [Red Hat](https://github.com/RedHatOfficial) |
| Paul Holzinger | [Luap99](https://github.com/Luap99) | Core Maintainer | [Red Hat](https://github.com/RedHatOfficial) |
| Giuseppe Scrivano | [giuseppe](https://github.com/giuseppe) | Core Maintainer | [Red Hat](https://github.com/RedHatOfficial) |
| Miloslav Trmač | [mtrmac](https://github.com/mtrmac) | Core Maintainer | [Red Hat](https://github.com/RedHatOfficial) |
| Mohan Boddu | [mohanboddu](https://github.com/mohanboddu) | Community Manager | [Red Hat](https://github.com/RedHatOfficial) |
| Neil Smith | [actionmancan](https://github.com/actionmancan) | Community Manager | [Red Hat](https://github.com/RedHatOfficial) |
| Tom Sweeney | [TomSweeneyRedHat](https://github.com/TomSweeneyRedHat/) | Maintainer and Community Manager | [Red Hat](https://github.com/RedHatOfficial) |
| Ygal Blum | [ygalblum](https://github.com/ygalblum) | Maintainer | [Red Hat](https://github.com/RedHatOfficial) |
| Ashley Cui | [ashley-cui](https://github.com/ashley-cui) | Maintainer | [Red Hat](https://github.com/RedHatOfficial) |
| Mario Loriedo | [l0rd](https://github.com/l0rd/) | Maintainer | [Red Hat](https://github.com/RedHatOfficial) |
| Lokesh Mandvekar | [lsm5](https://github.com/lsm5) | Maintainer | [Red Hat](https://github.com/RedHatOfficial) |
| Jake Correnti | [jakecorrenti](https://github.com/jakecorrenti) | Reviewer | [Red Hat](https://github.com/RedHatOfficial) |
| Jason Greene | [n1hility](https://github.com/n1hility) | Reviewer | [Red Hat](https://github.com/RedHatOfficial) |
| Jhon Honce | [jwhonce](https://github.com/jwhonce) | Reviewer | [Red Hat](https://github.com/RedHatOfficial) |
| Jan Kaluza | [jankaluza](https://github.com/jankaluza) | Reviewer | [Red Hat](https://github.com/RedHatOfficial) |
| Craig Loewen | [craigloewen-msft](https://github.com/craigloewen-msft) | Reviewer | [Microsoft](https://github.com/microsoft) |
| Urvashi Mohnani | [umohnani8](https://github.com/umohnani8) | Reviewer | [Red Hat](https://github.com/RedHatOfficial) |
| Aditya Rajan | [flouthoc](https://github.com/flouthoc) | Reviewer | [Red Hat](https://github.com/RedHatOfficial) |
| Jan Rodák | [Honny1](https://github.com/Honny1) | Reviewer | [Red Hat](https://github.com/RedHatOfficial) |
| Valentin Rothberg | [vrothberg](https://github.com/vrothberg) | Reviewer | [Red Hat](https://github.com/RedHatOfficial) |
| Lewis Roy | [ninja-quokka](https://github.com/ninja-quokka) | Reviewer | [Red Hat](https://github.com/RedHatOfficial) |
| Nicola Sella | [inknos](https://github.com/inknos) | Reviewer | [Red Hat](https://github.com/RedHatOfficial) |
| Dan Walsh | [rhatdan](https://github.com/rhatdan) | Reviewer | [Red Hat](https://github.com/RedHatOfficial) |
## Alumni
None at present
## Credits
The structure of this document was based off of the equivalent one in the [CRI-O Project](https://github.com/cri-o/cri-o/blob/main/MAINTAINERS.md).

1089
Makefile Normal file

File diff suppressed because it is too large Load Diff

34
OWNERS Normal file
View File

@ -0,0 +1,34 @@
approvers:
- Luap99
- TomSweeneyRedHat
- ashley-cui
- baude
- giuseppe
- l0rd
- lsm5
- mheon
- mtrmac
- nalind
- ygalblum
reviewers:
- Luap99
- TomSweeneyRedHat
- baude
- flouthoc
- giuseppe
- Honny1
- inknos
- jakecorrenti
- jankaluza
- jwhonce
- l0rd
- lsm5
- mheon
- mtrmac
- n1hility
- nalind
- ninja-quokka
- rhatdan
- umohnani8
- vrothberg
- ygalblum

204
README.md
View File

@ -1 +1,203 @@
# Podman Build Project
![PODMAN logo](https://raw.githubusercontent.com/containers/common/main/logos/podman-logo-full-vert.png)
# Podman: A tool for managing OCI containers and pods
![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)
![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/containers/podman)
[![Go Report Card](https://goreportcard.com/badge/github.com/containers/libpod)](https://goreportcard.com/report/github.com/containers/libpod)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/10499/badge)](https://www.bestpractices.dev/projects/10499)
<br/>
Podman (the POD MANager) is a tool for managing containers and images, volumes mounted into those containers, and pods made from groups of containers.
Podman runs containers on Linux, but can also be used on Mac and Windows systems using a Podman-managed virtual machine.
Podman is based on libpod, a library for container lifecycle management that is also contained in this repository. The libpod library provides APIs for managing containers, pods, container images, and volumes.
Podman releases a new major or minor release 4 times a year, during the second week of February, May, August, and November. Patch releases are more frequent and may occur at any time to get bugfixes out to users. All releases are PGP signed. Public keys of members of the team approved to make releases are located [here](https://github.com/containers/release-keys/tree/main/podman).
* Continuous Integration:
* [![Build Status](https://api.cirrus-ci.com/github/containers/podman.svg)](https://cirrus-ci.com/github/containers/podman/main)
* [GoDoc: ![GoDoc](https://godoc.org/github.com/containers/podman/libpod?status.svg)](https://godoc.org/github.com/containers/podman/libpod)
* [Downloads](DOWNLOADS.md)
## Overview and scope
At a high level, the scope of Podman and libpod is the following:
* Support for multiple container image formats, including OCI and Docker images.
* Full management of those images, including pulling from various sources (including trust and verification), creating (built via Containerfile or Dockerfile or committed from a container), and pushing to registries and other storage backends.
* Full management of container lifecycle, including creation (both from an image and from an exploded root filesystem), running, checkpointing and restoring (via CRIU), and removal.
* Full management of container networking, using Netavark.
* Support for pods, groups of containers that share resources and are managed together.
* Support for running containers and pods without root or other elevated privileges.
* Resource isolation of containers and pods.
* Support for a Docker-compatible CLI interface, which can both run containers locally and on remote systems.
* No manager daemon, for improved security and lower resource utilization at idle.
* Support for a REST API providing both a Docker-compatible interface and an improved interface exposing advanced Podman functionality.
* Support for running on Windows and Mac via virtual machines run by `podman machine`.
## Roadmap
The future of Podman feature development can be found in its **[roadmap](ROADMAP.md)**.
## Communications
If you think you've identified a security issue in the project, please *DO NOT* report the issue publicly via the GitHub issue tracker, mailing list, or IRC.
Instead, send an email with as many details as possible to `security@lists.podman.io`. This is a private mailing list for the core maintainers.
For general questions and discussion, please use Podman's
[channels](https://podman.io/community/#slack-irc-matrix-and-discord).
For discussions around issues/bugs and features, you can use the GitHub
[issues](https://github.com/containers/podman/issues)
and
[PRs](https://github.com/containers/podman/pulls)
tracking system.
There is also a [mailing list](https://lists.podman.io/archives/) at `lists.podman.io`.
You can subscribe by sending a message to `podman-join@lists.podman.io` with the subject `subscribe`.
## Rootless
Podman can be easily run as a normal user, without requiring a setuid binary.
When run without root, Podman containers use user namespaces to set root in the container to the user running Podman.
Rootless Podman runs locked-down containers with no privileges that the user running the container does not have.
Some of these restrictions can be lifted (via `--privileged`, for example), but rootless containers will never have more privileges than the user that launched them.
If you run Podman as your user and mount in `/etc/passwd` from the host, you still won't be able to change it, since your user doesn't have permission to do so.
Almost all normal Podman functionality is available, though there are some [shortcomings](https://github.com/containers/podman/blob/main/rootless.md).
Any recent Podman release should be able to run rootless without any additional configuration, though your operating system may require some additional configuration detailed in the [install guide](https://podman.io/getting-started/installation).
A little configuration by an administrator is required before rootless Podman can be used, the necessary setup is documented [here](https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md).
## Podman Desktop
[Podman Desktop](https://podman-desktop.io/) provides a local development environment for Podman and Kubernetes on Linux, Windows, and Mac machines.
It is a full-featured desktop UI frontend for Podman which uses the `podman machine` backend on non-Linux operating systems to run containers.
It supports full container lifecycle management (building, pulling, and pushing images, creating and managing containers, creating and managing pods, and working with Kubernetes YAML).
The project develops on [GitHub](https://github.com/containers/podman-desktop) and contributions are welcome.
## Out of scope
* Specialized signing and pushing of images to various storage backends.
See [Skopeo](https://github.com/containers/skopeo/) for those tasks.
* Support for the Kubernetes CRI interface for container management.
The [CRI-O](https://github.com/cri-o/cri-o) daemon specializes in that.
## OCI Projects Plans
Podman uses OCI projects and best of breed libraries for different aspects:
- Runtime: We use the [OCI runtime tools](https://github.com/opencontainers/runtime-tools) to generate OCI runtime configurations that can be used with any OCI-compliant runtime, like [crun](https://github.com/containers/crun/) and [runc](https://github.com/opencontainers/runc/).
- Images: Image management uses the [containers/image](https://github.com/containers/image) library.
- Storage: Container and image storage is managed by [containers/storage](https://github.com/containers/storage).
- Networking: Networking support through use of [Netavark](https://github.com/containers/netavark) and [Aardvark](https://github.com/containers/aardvark-dns). Rootless networking is handled via [pasta](https://passt.top/passt) or [slirp4netns](https://github.com/rootless-containers/slirp4netns).
- Builds: Builds are supported via [Buildah](https://github.com/containers/buildah).
- Conmon: [Conmon](https://github.com/containers/conmon) is a tool for monitoring OCI runtimes, used by both Podman and CRI-O.
- Seccomp: A unified [Seccomp](https://github.com/containers/common/blob/main/pkg/seccomp/seccomp.json) policy for Podman, Buildah, and CRI-O.
## Podman Information for Developers
For blogs, release announcements and more, please checkout the [podman.io](https://podman.io) website!
**[Installation notes](install.md)**
Information on how to install Podman in your environment.
**[OCI Hooks Support](https://github.com/containers/common/blob/main/pkg/hooks/README.md)**
Information on how Podman configures [OCI Hooks][spec-hooks] to run when launching a container.
**[Podman API](https://docs.podman.io/en/latest/_static/api.html)**
Documentation on the Podman REST API.
**[Podman Commands](https://podman.readthedocs.io/en/latest/Commands.html)**
A list of the Podman commands with links to their man pages and in many cases videos
showing the commands in use.
**[Podman Container Images](https://github.com/containers/image_build/blob/main/podman/README.md)**
Information on the Podman Container Images found on [quay.io](https://quay.io/podman/stable).
**[Podman Troubleshooting Guide](troubleshooting.md)**
A list of common issues and solutions for Podman.
**[Podman Usage Transfer](transfer.md)**
Useful information for ops and dev transfer as it relates to infrastructure that utilizes Podman. This page
includes tables showing Docker commands and their Podman equivalent commands.
**[Tutorials](docs/tutorials)**
Tutorials on using Podman.
**[Remote Client](https://github.com/containers/podman/blob/main/docs/tutorials/remote_client.md)**
A brief how-to on using the Podman remote client.
**[Basic Setup and Use of Podman in a Rootless environment](https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md)**
A tutorial showing the setup and configuration necessary to run Rootless Podman.
**[Release Notes](RELEASE_NOTES.md)**
Release notes for recent Podman versions.
**[Contributing](CONTRIBUTING.md)**
Information about contributing to this project.
[spec-hooks]: https://github.com/opencontainers/runtime-spec/blob/v1.0.2/config.md#posix-platform-hooks
## Buildah and Podman relationship
Buildah and Podman are two complementary open-source projects that are
available on most Linux platforms and both projects reside at
[GitHub.com](https://github.com) with Buildah
[here](https://github.com/containers/buildah) and Podman
[here](https://github.com/containers/podman). Both, Buildah and Podman are
command line tools that work on Open Container Initiative (OCI) images and
containers. The two projects differentiate in their specialization.
Buildah specializes in building OCI images. Buildah's commands replicate all
of the commands that are found in a Dockerfile. This allows building images
with and without Dockerfiles while not requiring any root privileges.
Buildahs ultimate goal is to provide a lower-level coreutils interface to
build images. The flexibility of building images without Dockerfiles allows
for the integration of other scripting languages into the build process.
Buildah follows a simple fork-exec model and does not run as a daemon
but it is based on a comprehensive API in golang, which can be vendored
into other tools.
Podman specializes in all of the commands and functions that help you to maintain and modify
OCI images, such as pulling and tagging. It also allows you to create, run, and maintain those containers
created from those images. For building container images via Dockerfiles, Podman uses Buildah's
golang API and can be installed independently from Buildah.
A major difference between Podman and Buildah is their concept of a container. Podman
allows users to create "traditional containers" where the intent of these containers is
to be long lived. While Buildah containers are really just created to allow content
to be added back to the container image. An easy way to think of it is the
`buildah run` command emulates the RUN command in a Dockerfile while the `podman run`
command emulates the `docker run` command in functionality. Because of this and their underlying
storage differences, you can not see Podman containers from within Buildah or vice versa.
In short, Buildah is an efficient way to create OCI images while Podman allows
you to manage and maintain those images and containers in a production environment using
familiar container cli commands. For more details, see the
[Container Tools Guide](https://github.com/containers/buildah/tree/main/docs/containertools).
## Podman Hello
```
$ podman run quay.io/podman/hello
Trying to pull quay.io/podman/hello:latest...
Getting image source signatures
Copying blob a6b3126f3807 done
Copying config 25c667d086 done
Writing manifest to image destination
Storing signatures
!... Hello Podman World ...!
.--"--.
/ - - \
/ (O) (O) \
~~~| -=(,Y,)=- |
.---. /` \ |~~
~/ o o \~~~~.----. ~~
| =(X)= |~ / (O (O) \
~~~~~~~ ~| =(Y_)=- |
~~~~ ~~~| U |~~
Project: https://github.com/containers/podman
Website: https://podman.io
Documents: https://docs.podman.io
Twitter: @Podman_io
```

4536
RELEASE_NOTES.md Normal file

File diff suppressed because it is too large Load Diff

267
RELEASE_PROCESS.md Normal file
View File

@ -0,0 +1,267 @@
# Podman Releases
## Overview
Podman (and podman-remote) versioning is mostly based on [semantic-versioning
standards](https://semver.org).
Significant versions
are tagged, including *release candidates* (`rc`).
All relevant **minor** releases (`vX.Y`) have their own branches. The **latest**
development efforts occur on the *main* branch. Branches with a
*rhel* suffix are use for long-term support of downstream RHEL releases.
## Release workflow expectations
* You have push access to the [upstream podman repository](https://github.com/containers/podman.git), and the upstream [podman-machine-os repository](https://github.com/containers/podman-machine-os)
* You understand all basic `git` operations and concepts, like creating commits,
local vs. remote branches, rebasing, and conflict resolution.
* You have access to your public and private *GPG* keys. They should also be documented on our [release keys repo](https://github.com/containers/release-keys).
* You have reliable internet access (i.e. not the public WiFi link at McDonalds)
* Other podman maintainers are online/available for assistance if needed.
* For a **major** release, you have 4-8 hours of time available, most of which will
be dedicated to writing release notes.
* For a **minor** or **patch** release, you have 2-4 hours of time available
(minimum depends largely on the speed/reliability of automated testing)
* You will announce the release on the proper platforms
(i.e. Podman blog, Twitter, Mastodon Podman and Podman-Desktop mailing lists)
# Release cadence
Upstream major or minor releases occur the 2nd week of February, May, August, November.
Branching and RC's may start several weeks beforehand.
Patch releases occur as-needed.
# Releases
## Major (***X***.y.z) release
These releases always begin from *main*, and are contained in a branch
named with the **major** and **minor** version. **Major** release branches
begin in a *release candidate* phase, with prospective release tags being
created with an `-rc` suffix. There may be multiple *release candidate*
tags before the final/official **major** version is tagged and released.
## Significant minor (x.**Y**.z) and patch (x.y.**Z**) releases
Significant **minor** and **patch** level releases are normally
branched from *main*, but there are occasional exceptions.
Additionally, these branches may be named with `-rhel` (or another)
suffix to signify a specialized purpose. For example, `-rhel` indicates
a release intended for downstream *RHEL* consumption.
## Unreleased Milestones
Non-release versions may occasionally appear tagged on a branch, without
the typical (major) receive media postings or artifact distribution. For
example, as required for the (separate) RHEL release process. Otherwise
these tags are simply milestones of reference purposes and may
generally be safely ignored.
## Process
***Note:*** This is intended as a guideline, and generalized process.
Not all steps are applicable in all situations. Not all steps are
spelled with complete minutiae.
1. Create a new upstream release branch (if none already exist).
1. Check if a release branch is needed. All major and minor releases should be branched before RC1.
Patch releases typically already have a branch created.
Branching ensures all changes are curated before inclusion in the
release, and no new features land after the *release-candidate* phases
are complete.
1. Ensure your local clone is fully up to date with the remote upstream
(`git remote update`). Switch to this branch (`git checkout upstream/main`).
1. Make a new local branch for the release based on *main*. For example,
`git checkout -b vX.Y`. Where `X.Y` represent the complete release
version-name, including any suffix (if any) like `-rhel`. ***DO NOT***
include any `-rc` suffix in the branch name.
1. Push the new branch otherwise unmodified (`git push upstream vX.Y`).
1. Check if a release branch is needed on the `podman-machine-os` repo.
If so, repeat above steps for `podman-machine-os`.
1. Back on the podman repo, automation will begin executing on the branch immediately.
Because the repository allows out-of-sequence PR merging, it is possible that
merge order introduced bugs/defects. To establish a clean
baseline, observe the initial CI run on the branch for any unexpected
failures. This can be done by going directly to
`https://cirrus-ci.com/github/containers/podman/vX.Y`
1. If there are CI test or automation boops that need fixing on the branch,
attend to them using normal PR process (to *main* first, then backport
changes to the new branch). Ideally, CI should be "green" on the new
branch before proceeding.
1. Create a new branch-verification Cirrus-Cron entry.
1. This is to ensure CI's VM image timestamps are refreshed. Without this,
the VM images ***will*** be permanently pruned after 60 days of inactivity
and are hard/impossible to re-create accurately.
1. Go to
[https://cirrus-ci.com/github/containers/podman](https://cirrus-ci.com/github/containers/podman)
and press the "gear" (Repository Settings) button on the top-right.
1. At the bottom of the settings page is a table of cron-job names, branches,
schedule, and recent status. Below that is an editable new-entry line.
1. Set the new job's `name` and `branch` to the name of new release branch.
1. Set the `expression` using the form `X X X ? * 1-6` where 'X' is a number
between 0-23 and not already taken by another job in the table. The 1-hour
interval is used because it takes about that long for the job to run.
1. Add the new job by pressing the `+` button on the right-side of the
new-entry line.
1. Create a new local working-branch to develop the release PR
1. Ensure your local clone is fully up to
date with the remote upstream (`git remote update`).
1. Create a local working branch based on `upstream/main` or the correct upstream branch.
Example: `git checkout -b bump_vX.Y.Z --no-track upstream/vX.Y`
1. Compile release notes.
1. Ensure any/all intended PR's are completed and merged prior to any
processing of release notes.
1. Find all commits since the last release. There is a script, `/hack/branch_commits.rb`
that is helpful for finding all commits in one branch, but not in another,
accounting for cherry-picks. Commits in base branch that are not in
the old branch will be reported. `ruby branch_commits.rb upstream/main upstream/vX.Y`
Keep this list open/available for reference as you edit.
1. Edit `RELEASE_NOTES.md`
* Add/update the version-section of with sub-sections for *Features*
(new functionality), *Changes* (Altered podman behaviors),
*Bugfixes* (self-explanatory), *API* (All related features,
changes, and bugfixes), and *Misc* (include any **major**
library bumps, e.g. `c/buildah`, `c/storage`, `c/common`, etc).
* Use your merge-bot reference PR-listing to examine each PR in turn,
adding an entry for it into the appropriate section.
* Use the list of commits to find the PR that the commit came from.
Write a release note if needed.
* Use the release note field in the PR as a guideline.
It may be helpful but also may need rewording for consistency.
Some PR's with a release note field may not need one, and some PR's
without a release note field may need one.
* Be sure to link any issue the PR fixed.
* Do not include any PRs that are only documentation or test/automation
changes.
* Do not include any PRs that fix bugs which we introduced due to
new features/enhancements. In other words, if it was working, broke, then
got fixed, there's no need to mention those items.
1. Commit the `RELEASE_NOTES.md` changes, using the description
`Create release notes for vX.Y.Z` (where `X`, `Y`, and `Z` are the
actual version numbers).
1. Open a Release Notes PR, or include this commit with the version bump PR.
1. Update version numbers and push tag
1. Edit `version/rawversion/version.go` and bump the `Version` value to the new
release version. If there were API changes, also bump `APIVersion` value.
Make sure to also bump the version in the swagger.yaml `pkg/api/server/docs.go`
For major and minor versions also add the new branch name to
`docs/source/Reference.rst` to show the new swagger version on docs.podman.io.
1. Commit this and sign the commit (`git commit -a -s -S`). The commit message
should be `Bump to vX.Y.Z` (using the actual version numbers).
1. Push this single change to your GitHub fork, and make a new PR,
**being careful** to select the proper release branch as its base.
1. Wait for all automated tests pass (including on an RC-branch PR). Re-running
and/or updating code as needed.
1. In the PR, under the *Checks* tab, locate and clock on the Cirrus-CI
task `Optional Release Test`. In the right-hand window pane, click
the `trigger` button and wait for the test to go green. *This is a
critical step* which confirms the commit is worthy of becoming a release.
1. In the PR, under the *Checks* tab, a GitHub actions [task](https://github.com/containers/podman/actions/workflows/machine-os-pr.yml) will run.
This action opens a PR on the [podman-machine-os repo](https://github.com/containers/podman-machine-os), which builds VM images for the release. The action will also link the `podman-machine-os` pr in a comment on the podman PR
This action also automatically applies the `do-not-merge/wait-machine-image-build` to the Podman PR, which blocks merging until VM images are built and published.
1. Go to the `podman-machine-os` bump pr, by clicking the link in the comment, or by finding it in the [podman-machine-os repo](https://github.com/containers/podman-machine-os/pulls).
1. Wait for automation to finish running
1. Once you are sure that there will be no more force pushes on the Podman release PR, merge the `podman-machine-os` bump PR
1. Tag the `podman-machine-os` bump commit with the same version as the podman release. (git tag -s -m 'vX.Y.Z' vX.Y.Z)
1. Push the tag.
1. The tag will automatically trigger a Cirrus task, named “Publish Image”,
to publish the release images. It will push the images to Quay and cut a release on the `podman-machine-os` repo. Wait for this task to complete. You can monitor the task on the [Cirrus CI dashboard](https://cirrus-ci.com/github/containers/podman-machine-os)
1. Return to the Podman repo
1. The `do-not-merge/wait-podman-machine-os` label should be automatically
un-set once the `podman-machine-os` release is finished.
1. Wait for all other PR checks to pass.
1. Wait for other maintainers to merge the PR.
1. Tag the `Bump to vX.Y.Z` commit as a release by running
`git tag -s -m 'vX.Y.Z' vX.Y.Z $HASH` where `$HASH` is specified explicitly and carefully, to avoid (basically) unfixable accidents
(if they are pushed).
1. **Note:** This is the last point where any test-failures can be addressed
by code changes. After pushing the new version-tag upstream, no further
changes can be made to the code without lots of unpleasant efforts. Please
seek assistance if needed, before proceeding.
1. Assuming the "Bump to ..." PR merged successfully, and you're **really**
confident the correct commit has been tagged, push it with
`git push upstream vX.Y.Z`
1. Monitor release automation
1. After the tag is pushed, the release GitHub action should run.
This action creates the GitHub release from the pushed tag,
and automatically builds and uploads the binaries and installers to the release.
1. The following artifacts should be attached to the release:
* podman-installer-macos-amd64.pkg
* podman-installer-macos-arm64.pkg
* podman-installer-macos-universal.pkg
* podman-installer-windows-amd64.exe
* podman-installer-windows-arm64.exe
* podman-remote-release-darwin_amd64.zip
* podman-remote-release-darwin_arm64.zip
* podman-remote-release-windows_amd64.zip
* podman-remote-release-windows_arm64.zip
* podman-remote-static-linux_amd64.tar.gz
* podman-remote-static-linux_arm64.tar.gz
* shasums
1. An email should have been sent to the [podman](mailto:podman@lists.podman.io) mailing list.
Keep an eye on it make sure the email went through to the list.
1. The release action will also bump the Podman version on podman.io. It will open a PR if a non-rc latest version is released. Go to the [podman.io](https://github.com/containers/podman.io) repo and merge the PR opened by this action, if needed.
1. After the tag is pushed, an action to bump to -dev will run. A PR will be opened for this bump. Merge this PR if needed.
1. Locate, Verify release testing is proceeding
1. When the tag was pushed, an automated build was created. Locate this
by starting from
`https://github.com/containers/podman/tags` and finding the recent entry
for the pushed tag. Under the tag name will be a timestamp and abbrieviated
commit hash, for example `<> 5b2585f`. Click the commit-hash link.
1. In the upper-left most corner, just to the left of the "Bump to vX.Y"
text, will be a small status icon (Yellow circle, Red "X", or green check).
Click this, to open a small pop-up/overlay window listing all the status
checks.
1. In the small pop-up/overlay window, press the "Details" link on one of the
Cirrus-CI status check entries (doesn't matter which one).
1. On the following page, in the lower-right pane, will be a "View more details
on Cirrus CI" link, click this.
1. A Cirrus-CI task details page will open, click the button labeled
"View All Tasks".
1. Keep this page open to monitor its progress and for use in future steps.
1. Update Cirrus-CI cron job list
1. After any Major or significant minor (esp. `-rhel`) releases, it's critical to
maintain the Cirrus-CI cron job list. This applies to all containers-org repos,
not just podman.
1. Access the repo. settings WebUI by navigating to
`https://cirrus-ci.com/github/containers/<repo name>`
and clicking the gear-icon in the upper-right.
1. For minor (i.e. **NOT** `-rhel`) releases, (e.x. `vX.Y`), the previous release
should be removed from rotation (e.x. `vX.<Y-1>`) assuming it's no longer supported.
Simply click the trash-can icon to the right of the job definition.
1. For `-rhel` releases, these are tied to products with specific EOL dates. They should
*never* be disabled unless you (and a buddy) are *absolutely* certain the product is EOL
and will *never* ever see another backport (CVE or otherwise).
1. On the settings page, pick a "less used" time-slot based on the currently defined
jobs. For example, if three jobs specify `12 12 12 ? * 1-6`, choose another. Any
spec. `H`/`M`/`S` value between 12 and 22 is acceptable (e.x. `22 22 22 ? * 1-6`).
The point is to not overload the clouds with CI jobs.
1. Following the pattern of the already defined jobs, at the bottom of the settings
page add a new entry. The "Name" should reflect the version number, the "Branch"
is simply the newly created release branch name (must be exact), and the "Expression"
is the time slot you selected (copy-paste).
1. Click the "+" button next to the new-job row you just filled out.
1. Announce the release
1. For major and minor releases, write a blog post and publish it to blogs.podman.io
Highlight key features and important changes or fixes. Link to the GitHub release.
Make sure the blog post is properly tagged with the Announcement, Release, and Podman tags,
and any other appropriate tags.
1. Tweet the release. Make a Mastodon post about the release.
1. RC's can also be announced if needed.

122
REVIEWING.md Normal file
View File

@ -0,0 +1,122 @@
# Reviewing Pull Requests
This document contains general principles for how to perform code reviews in the Podman repository.
It does not aim to be a complete guide to how to perform code review, but rather to provide general guidance on how code reviews should be performed.
This document is aimed at Reviewers, Maintainers, and Core Maintainers (see [GOVERNANCE.md](./GOVERNANCE.md) for definitions of these roles), but these guidelines should be followed by all who wish to review code in the Podman project's GitHub repositories, even those who are not currently a maintainer.
## How are reviews performed
The Podman project aims to ensure that all PRs are reviewed by at least 2 people prior to merge, at least one of which must be a repository Maintainer.
There are some exceptions to this: Updates to libraries (including Go vendor updates) that pass CI cleanly and require no code changes may be merged by a maintainer without further review.
All code merged must pass CI.
## What should you review?
We encourage review of all PRs, even those not in the area of expertise of the reviewer.
Even if you are not fully familiar with the code in question, you can still notice basic mistakes.
Feel free to ask questions about how areas of the code work to help familiarize yourself during reviews.
If you finish a review and do not feel like you adequately understood the code to approve it for merge, tag an expert in that area of the code to perform a further review.
Timely PR reviews are important - contributors can become discouraged if a PR is neglected by maintainers.
All Maintainers and Reviewers should try to review new pull requests in their repositories once a day to ensure this.
When you review a PR with failing tests, please check to see if those tests failed due to known flakes.
If so, please restart the failed tests.
Many repositories in the Podman project can only have their tests restarted by project members, not the submitter, so regular attention to test failures by reviewers is important.
## Things to Check
### Breaking Changes
The Podman project aims to present a stable API for its users.
Breaking changes to the project's Command Line Interfaces or public APIs (the Podman REST API and its associated bindings) must only be made in approved breaking change releases - Podman 6.0, 7.0, etc.
Individual repositories should identify what parts of the repository are considered to be their stable API.
Breaking changes can include renaming an option without retaining the original name as an alias, removing an option entirely, or changing how an option works in a way that does not ensure backwards compatibility.
Periods when it is acceptable to merge breaking changes will be widely announced.
If it is not one of those periods, reviewers should be on the lookout for breaking changes.
PRs with breaking changes should not be merged.
Please guide the contributor in how to make the change in a non-breaking fashion.
If this is not possible, deferring the PR to the next breaking change window or closing it entirely is appropriate.
### Commit Messages and Hygiene
Good commit messages are essential for understanding why a change was made in the future.
Reviewers should check each commit of a PR to ensure that it has an appropriate commit message which fully and accurately explains the change and why it is being made.
This should remain true after changes to the PR due to code review, so please re-review commit messages before merging code, to ensure they are still accurate.
Pull requests fixing a specific issue must include a `Fixes: #xxxxx` line in the commit message.
Full details on the `Fixes` line can be found in the [Contributor's Guide](./CONTRIBUTING.md).
Reviewers are responsible for enforcing the guidelines in that document.
Each commit in a PR should be self-contained and have a clear and distinct purpose.
If this is not true, please encourage the contributor to squash their commits using `git rebase -i` until it is true.
### Disagreements between reviewers
We do not expect all reviewers to be of the same opinion during code review.
If you see another reviewer requesting changes to a PR you do not agree with, it is perfectly acceptable to comment to that effect.
Disagreements between maintainers can generally be worked out during PR review through comments.
If this is not possible, other maintainers can be called on to give their opinions and determine a way forward.
We encourage such disagreements and discussions, so long as they remain respectful and are done with the goal of resolving the dispute.
If a decision cannot be reached, the issue may be put to a vote, in which all Maintainers of the repository in question and all Core Maintainers can vote.
Being respectful includes respect for the contributor's time.
If there is a disagreement, please do not make the contributor make repeated changes until an agreement on how to proceed is reached.
Also, please attempt to reach an agreement quickly, so the PR can be merged in a timely fashion.
### Language Version Updates
Most repositories in the Podman project are written in Go, and as such target a specific version of Go in their `go.mod`.
For example:
```
$ cat go.mod | grep 'go 1.'
go 1.22.8
```
Changing this value affects what versions of Go can build the project, and as such increasing it can prevent some distributions with older Golang versions from building Podman.
For all branches except the main branch, Go version should remain static unless there is an extremely good reason to change it (for example, a CVE fix requires pulling in a new version of a library that needs a newer Golang version).
PRs into a non-main branch which change the supported Go version should be modified to not require such a change, or rejected if that is not possible.
Changing supported Go version in the main branch is allowed, but not encouraged.
### Tests
Please check tests added by the PR.
If a PR has no new tests, determine if this is actually appropriate - has new functionality been added which should have been tested?
If the PR does have tests, check to see if they are reasonably comprehensive.
The Podman Project does not have code coverage standards at present, but we aim to ensure that all new functionality is tested.
Tests for bugs and new functionality should, generally speaking, fail when run against Podman without the patch applied.
Reviewers are encouraged to check this when reviewing a pull request.
### Documentation
All changes to public-facing APIs (e.g. the Podman REST API) and CLI should be appropriately documented.
API changes require Swagger documentation.
CLI changes should be documented in the Manpages.
Please validate that PRs making such changes include appropriate documentation.
Many repositories in the Podman project will enforce this via CI check, but you should still review the contents of the documentation to make sure they are appropriate and complete.
## Things to Avoid
### Bikeshedding and excessively critical reviews
Please avoid bikeshedding during reviews.
Trivial changes that do not affect the ultimate functionality of the PR - for example, unnecessary renaming of variables, or small changes to code style or formatting - should not block merge of a PR.
It is acceptable to make such comments, but they should be marked as nice to have changes.
Exceptions can be made with documentation, as ensuring correctness and clarity in documentation is very important.
Ensuring our documentation is free of typos and obvious grammatical issues is not bikeshedding and is allowed.
### Asking for unrelated changes
Please do not request that a contributor make significant changes to code their PR did not touch.
If you are reviewing and find problems in pre-existing code in a file that the PR changed, you should not require that the contributor change this code as well.
Asking if they are willing to do so is fine, but do not block the merge of the PR if they are unwilling.
Some changes are OK to ask for - for example, asking a contributor to refactor existing code very similar to something being added to prevent code duplication.
However, larger changes that would substantially increase the size of the PR should be avoided.
Reviewers should use their best judgement to balance respect for the contributor's time and the code hygiene of the project.
If a change is too large to be reasonably asked for, consider asking the contributor to add a comment with a "TODO" or "FIXME" to the area that needs changing (or making a PR yourself with such a comment).

61
ROADMAP.md Normal file
View File

@ -0,0 +1,61 @@
![PODMAN logo](https://raw.githubusercontent.com/containers/common/main/logos/podman-logo-full-vert.png)
# Podman Roadmap
The Podman development team reviews feature requests from its various stakeholders for consideration
quarterly. Podman maintainers then prioritize these features. Top features are then assigned to
one or more engineers.
## Future feature considerations
The following features are of general importantance to Podman. While these features have no timeline
associated with them yet, they will likely be on future quarterly milestones.
* Further improvements to `podman machine` to better support Podman Desktop and other developer usecases.
- Smoother upgrade process for Podman machine operating system (OS) images
- Convergence of WSL technologies with other providers including its OS
* Remote client support for OCI artifacts and its RESTFUL API
* Integration of composefs
* Ongoing work around partial pull support (zstd:chunked)
* Improved support for the BuildKit API.
* Performance and stability improvements.
* Reductions to the size of the Podman binary.
## Milestones and commitments by quarter
This section is a historical account of what features were prioritized by quarter. Results of the prioritization will be added at start of each quarter (Jan, Apr, July, Oct).
### 2025 Q2 ####
#### Releases ####
- [ ] Podman 5.5
- [ ] Fully automated Podman releases
#### Features ####
- [ ] Windows ARM64 installer
- [ ] Add support for artifacts in RESTFUL service
- [ ] Reduce binary size of Podman
- [ ] Add remote client support for artifacts
- [ ] Add support for newer Docker API versions to RESTFUL service
- [ ] Replace Podman pause image with a rootfs
#### CNCF ####
- [ ] Add and adhere to Governance model
### 2025 Q1 ####
#### Releases ####
- [x] Podman 5.4
- [x] Podman release automation
#### Features ####
- [x] Artifact add --append
- [x] Artifact extract
- [x] Artifact add --options
- [x] Mount OCI artifacts inside containers
- [x] Determine strategy for configuration files when remote
#### CNCF ####
- [x] Create Maintainers file
- [x] Create Governance documentation

3
SECURITY.md Normal file
View File

@ -0,0 +1,3 @@
## Security and Disclosure Information Policy for the Podman Project
The Podman Project follows the [Security and Disclosure Information Policy](https://github.com/containers/common/blob/main/SECURITY.md) for the Containers Projects.

45
SUPPORT.md Normal file
View File

@ -0,0 +1,45 @@
# Upstream support of Podman
This Github repository is for the upstream development of Podman and the latest version
of Podman.
The term "latest version" refers to our mainline development tree or the
[latest release](https://github.com/containers/podman/releases/latest).
## Expectations on support
The Podman maintainers provide a "best effort" for the support of Podman. If are using
Podman from a Linux distribution, please use the Linux distribution's mechanism as support
unless you are willing to reproduce problems on the main branch of our upstream code.
## Operating System and Hardware
Podman is run on a bevy of operating systems and hardware. Upstream development cannot
possibly support all the combinations and custom environments of our users.
All pull requests (new code) to Podman go through automated testing on the following
combinations:
### Native Podman
| Architecture | Operating System | Distribution |
| :--- | :--- | :--- |
| x86_64 | Linux | Debian (latest) |
| x86_64 | Linux | Fedora (latest) |
| ARM64 | Linux | Fedora (latest) |
### Podman Machine
| Architecture | Operating System | Machine Provider |
| :--- | :--- | :--- |
| x86_64 | Windows 2022 | WSL |
| x86_64 | Windows 2022 | HyperV |
| ARM64 | MacOS | AppleHV |
| ARM64 | MacOS | Libkrun |
For Linux, we test the latest versions of Fedora and Debian.
Operating systems and hardware outside our automated testing is considered "best effort".
In many cases, we are unable to test, triage, and develop for combinations outside what
our automated testing covers. For example, Podman on Intel-based Macs.

50
TRIAGE.md Normal file
View File

@ -0,0 +1,50 @@
# Triaging of Podman issues
To manage new GitHub issues, maintainers perform issue triage on a regular basis and categorize the issues based on priority, type of issue, and other factors.
This process includes:
1. Ensure the issue is relevant to the correct repository (i.e. build issues go to buildah repo, podman desktop issues go to Podman Desktop repo, etc) and transfer as needed.
2. Categorize issues by type and assign its associated label ([see below](#labels)) and “traiged” label. If the issue is a bug and it is of high impact, please assign a high-impact label.
3. Assign high-impact issues to either themselves or a [core maintainer](https://github.com/containers/podman/blob/main/OWNERS#L1).
4. If [essential information is lacking](#checks-for-triaging), request it from the submitter and apply the 'needs-info' label.
5. Once all the necessary information is gathered, the maintainer will assign the high-impact label if needed and removes the needs-info label
6. Check our [issue closing policy](https://github.com/containers/podman/blob/main/ISSUE.md#why-was-my-issue-report-closed) and close the new issue if it matches the listed criteria.
## Checks for triaging
While triaging, the maintainer has to look for the following information in the issue and ask the reporter for any missing information.
### Bugs:
1. Check what version of Podman, the distro, and any pertinent environmental notes the reporter is experiencing the problem on. This should come in the form of podman info as the issue template states.
2. If the issue is distribution specific, then suggest in the comment that it should also be brought to the attention of the distribution and close the issue.
3. If the reporter is not using the latest (or very near latest) version of Podman, the reporter should be asked to verify this still exists in main or at least in the latest release. The triager can also verify this.
4. Check if there is a good reproducer that preferably reproduces on the latest Podman version
5. Any other missing information that could help with debugging.
6. Check for similar issues and act accordingly
7. If the issue is related to Brew. Chocolatey or another package manager, suggest the reporter to use the latest binaries on the release page
### Features:
1. Check if the feature is already added to the newer Podman releases, if it is, add the appropriate suggestion and close the issue.
2. Check if the feature is reasonable and is under the projects scope
3. Check if the feature is clear and ask for any missing information.
### High Impact Bug Definition
1. An issue that impacts multiple users
2. An issue that is encountered on each run of Podman
3. An issue that breaks basic Podman functionality like `podman run` or `podman build`
4. A regression caused by new release
## Labels:
1. network
2. quadlet
3. machine
4. kube
5. storage
6. build
7. windows
8. macos
9. documentation
10. pasta
11. remote
12. compose
13. regression

BIN
bin/podman Executable file

Binary file not shown.

BIN
bin/podman-remote Executable file

Binary file not shown.

BIN
bin/podman-testing Executable file

Binary file not shown.

1
bin/podmansh Symbolic link
View File

@ -0,0 +1 @@
podman

BIN
bin/quadlet Executable file

Binary file not shown.

BIN
bin/rootlessport Executable file

Binary file not shown.

69
build_osx.md Normal file
View File

@ -0,0 +1,69 @@
# Building the Podman client on macOS
The following describes the process for building the Podman client on macOS.
## Install brew
Podman requires brew -- a package manager for macOS. This will allow additional packages to be installed that are
needed by Podman. See the [brew project page](https://brew.sh/) for installation instructions.
## Install build dependencies
Podman requires some software from brew to be able to build. This can be done using brew from a macOS terminal:
```
$ brew install go go-md2man
```
## Obtain Podman source code
You can obtain the latest source code for Podman from its github repository.
```
$ git clone https://github.com/containers/podman go/src/github.com/containers/podman
```
## Build client
After completing the preparatory steps of obtaining the Podman source code and installing its dependencies, the client
can now be built.
```
$ cd go/src/github.com/containers/podman
$ make podman-remote
$ mv bin/darwin/podman bin/podman
```
The binary will be located in bin/
```
$ ls -l bin/
```
### Using gvproxy from homebrew, with podman from git
Recent podman builds depend on a `gvproxy` binary which comes from [containers/gvisor-tap-vsock](https://github.com/containers/gvisor-tap-vsock). A common development scenario may be using the podman desktop app as a baseline, with a development
binary of `podman` you build from git. To ensure that the podman you build here can find the gvproxy installed from podman desktop, use:
`make podman-remote HELPER_BINARIES_DIR=/opt/podman/bin`
(Also note that because the `Makefile` rules do not correctly invalidate the binary when this variable changes,
so if you already have a build you'll need to `rm bin/darwin/podman` first if you have an existing build).
Alternatively, you can set `helper_binaries_dir=` in `~/.config/containers/containers.conf`.
### Building docs
If you would like to build the docs associated with Podman on macOS:
```
$ make podman-remote-darwin-docs
$ ls docs/build/remote/darwin
```
To install and view these manpages:
```
$ cp -a docs/build/remote/darwin/* /usr/share/man/man1
$ man podman
```
## Using the client
To learn how to use the Podman client, refer to its
[tutorial](https://github.com/containers/podman/blob/main/docs/tutorials/remote_client.md).

514
build_windows.md Normal file
View File

@ -0,0 +1,514 @@
# Building the Podman client and client installer on Windows
The following describes the process for building and testing the Podman Windows
client (`podman.exe`) and the Podman Windows installer (`podman-setup.exe`) on
Windows.
## Topics
- [Requirements](#requirements)
- [OS requirements](#os-requirements)
- [Git and go](#git-and-go)
- [Pandoc](#pandoc)
- [.NET SDK](#net-sdk)
- [Virtualization Provider](#virtualization-provider)
- [WSL](#wsl)
- [Hyper-V](#hyper-v)
- [Get the source code](#get-the-source-code)
- [Allow local PowerShell scripts execution](#allow-local-powershell-scripts-execution)
- [Build and test the Podman client for Windows](#build-and-test-the-podman-client-for-windows)
- [Build the Podman client](#build-the-podman-client)
- [Download gvproxy.exe and win-sshproxy.exe](#download-gvproxyexe-and-win-sshproxyexe)
- [Create a configuration file (optional)](#create-a-configuration-file-optional)
- [Create and start a podman machine](#create-and-start-a-podman-machine)
- [Run a container using podman](#run-a-container-using-podman)
- [Build and test the Podman Windows installer](#build-and-test-the-podman-windows-installer)
- [Build the Windows installer](#build-the-windows-installer)
- [Test the Windows installer](#test-the-windows-installer)
- [Build and test the standalone `podman.msi` file](#build-and-test-the-standalone-podmanmsi-file)
- [Verify the installation](#verify-the-installation)
- [Uninstall and clean-up](#uninstall-and-clean-up)
- [Validate changes before submitting a PR](#validate-changes-before-submitting-a-pr)
- [winmake lint](#winmake-lint)
- [winmake validatepr](#winmake-validatepr)
## Requirements
### OS requirements
This documentation assumes one uses a Windows 10 or 11 development machine and a
PowerShell terminal.
### Git and go
To build Podman, the [git](https://gitforwindows.org/) and [go](https://go.dev)
tools are required. In case they are not yet installed, open a Windows
PowerShell terminal and run the following command (it assumes that
[winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/) is
installed):
```pwsh
winget install -e GoLang.Go Git.Git
```
:information_source: A terminal restart is advised for the `PATH` to be
reloaded. This can also be manually changed by configuring the `PATH`:
```pwsh
$env:Path += ";C:\Program Files\Go\bin\;C:\Program Files\Git\cmd\"
```
### Pandoc (optional)
[Pandoc](https://pandoc.org/) is used to generate Podman documentation. It is
used for building the documentation.
Pandoc can be installed from https://pandoc.org/installing.html. When performing
the Pandoc installation one, has to choose the option "Install for all users"
(to put the binaries into "Program Files" directory).
Alternatively, Podman documentation can be built using a container with the target
`docs-using-podman` in the `winmake.ps1` script.
```pwsh
.\winmake docs-using-podman
```
### .NET SDK
[.NET SDK](https://learn.microsoft.com/en-us/dotnet/core/sdk), version 6 or
later, is required to develop and build the Podman Windows installer. It's not
required for the Podman Windows client.
```pwsh
winget install -e Microsoft.DotNet.SDK.8
```
[WiX Toolset](https://wixtoolset.org) **v5**, distributed as a .NET SDK tool, is
used too and can be installed using `dotnet install`:
```pwsh
dotnet tool install --global wix
```
### Virtualization Provider
Running Podman on Windows requires a virtualization provider. The supported
providers are the
[Windows Subsystem for Linux (WSL)](https://learn.microsoft.com/en-us/windows/wsl/)
and
[Hyper-V](https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v).
At least one of those two is required to test podman on a local Windows machine.
#### WSL
WSL can be installed on Windows 10 and Windows 11, including Windows Home, with
the following command, from a PowerShell or Windows Command Prompt terminal in
**administrator mode**:
```pwsh
wsl --install
```
For more information refer to
[the official documentation](https://learn.microsoft.com/en-us/windows/wsl/).
#### Hyper-V
Hyper-V is an optional feature of Windows Enterprise, Pro, or Education (not
Home). It is available on Windows 10 and 11 only and
[has some particular requirements in terms of CPU and memory](https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v#check-requirements).
To enable it on a supported system, enter the following command:
```pwsh
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All
```
After running this command, a restart of the Windows machine is required.
:information_source: Configure the VM provider used by podman (Hyper-V or WSL)
in the file `%PROGRAMDATA%/containers/containers.conf`.
[More on that later](#create-a-configuration-file-optional).
## Get the source code
Open a Windows Terminal and run the following command:
```pwsh
git config --global core.autocrlf false
```
It configures git so that it does **not** automatically convert LF to CRLF. In
the Podman git repository, files are expected to use Unix LF rather than Windows
CRLF.
Then run the command to clone the Podman git repository:
```pwsh
git clone https://github.com/containers/podman
```
It creates the folder `podman` in the current directory and clones the Podman
git repository into it.
### Allow local PowerShell scripts execution
A developer can build the Podman client for Windows and the Windows installer
with the PowerShell script
[winmake.ps1](https://github.com/containers/podman/blob/main/winmake.ps1).
Windows sets the ExecutionPolicy to `Restricted` by default; running scripts is
prohibited. Determine the ExecutionPolicy on the machine with this command:
```pwsh
Get-ExecutionPolicy
```
If the command returns `Restricted`, the ExecutionPolicy should be changed to
`RemoteSigned`:
```pwsh
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```
This policy allows the execution of local PowerShell scripts, such as
`winmake.ps1`, for the current user.
## Build and test the Podman client for Windows
The following steps describe how to build the `podman.exe` binary from sources
and test it.
### Build the Podman client
Open a PowerShell terminal and move to Podman local git repository directory:
```pwsh
Set-Location .\podman
```
Build `podman.exe`
```
.\winmake.ps1 podman-remote
```
:information_source: Verify build's success by checking the content of the
`.\bin\windows` folder. Upon successful completion, the executable `podman.exe`
should be there:
```pwsh
Get-ChildItem .\bin\windows\
Directory: C:\Users\mario\Git\podman\bin\windows
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2/27/2024 11:59 AM 45408256 podman.exe
```
### Download gvproxy.exe and win-sshproxy.exe
[gvisor-tap-vsock](https://github.com/containers/gvisor-tap-vsock/) binaries
(`gvproxy-windowsgui.exe` and `win-sshproxy.exe`) are required to run the Podman
client on Windows. The executables are expected to be in the same folder as
`podman.exe`. The following command downloads the latest version in the
`.\bin\windows\` folder:
```pwsh
.\winmake.ps1 win-gvproxy
```
:information_source: To verify that the binaries have been downloaded
successfully, check the content of the .\bin\windows` folder.
```pwsh
Get-ChildItem .\bin\windows\
Directory: C:\Users\mario\Git\podman\bin\windows
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2/29/2024 12:10 PM 10946048 gvproxy.exe
-a---- 2/27/2024 11:59 AM 45408256 podman.exe
-a---- 2/29/2024 12:10 PM 4089856 win-sshproxy.exe
```
### Create a configuration file (optional)
To test some particular configurations of Podman, create a `containers.conf`
file:
```
New-Item -ItemType Directory $env:PROGRAMDATA\containers\
New-Item -ItemType File $env:PROGRAMDATA\containers\containers.conf
notepad $env:PROGRAMDATA\containers\containers.conf
```
For example, to test with Hyper-V as the virtualization provider, use the
following content:
```toml
[machine]
provider="hyperv"
```
Find the complete list of configuration options in the
[documentation](https://github.com/containers/common/blob/main/docs/containers.conf.5.md).
### Create and start a podman machine
Execute the following commands in a terminal to create a Podman machine:
```pwsh
.\bin\windows\podman.exe machine init
```
When `machine init` completes, run `machine start`:
```pwsh
.\bin\windows\podman.exe machine start
```
:information_source: If the virtualization provider is Hyperv-V, execute the
above commands in an administrator terminal.
### Run a container using podman
Use the locally built Podman client for Windows to run containers:
```pwsh
.\bin\windows\podman.exe run hello-world
```
To learn how to use the Podman client, refer to its
[tutorial](https://github.com/containers/podman/blob/main/docs/tutorials/remote_client.md).
## Build and test the Podman Windows installer
The Podman Windows installer (e.g., `podman-5.1.0-dev-setup.exe`) is a bundle
that includes an msi package (`podman.msi`). It's built using the
[WiX Toolset](https://wixtoolset.org/) and the
[PanelSwWixExtension](https://github.com/nirbar/PanelSwWixExtension/tree/master5)
WiX extension. The source code is in the folder `contrib\win-installer`.
### Build the Windows installer
To build the installation bundle, run the following command:
```pwsh
.\winmake.ps1 installer
```
:information_source: making `podman-remote`, `win-gvproxy`, and `docs` (or `docs-using-podman`) is
required before running this command.
Locate the installer in the `contrib\win-installer` folder (relative to checkout
root) with a name like `podman-5.2.0-dev-setup.exe`.
The `installer` target of `winmake.ps1` runs the script
`contrib\win-installer\build.ps1` that, in turns, executes:
- `dotnet build podman.wixproj`: builds `podman.msi` from the WiX source files `podman.wxs`,
`pages.wxs`, `podman-ui.wxs` and `welcome-install-dlg.wxs`.
- `dotnet build podman-setup.wixproj`: builds `podman-setup.exe` file from
[WiX Burn bundle](https://wixtoolset.org/docs/tools/burn/) `burn.wxs`.
### Test the Windows installer
Double-click on the Windows installer to run it. To get the installation logs
with debug information, running it via the command line is recommended:
```pwsh
contrib\win-installer\podman-5.1.0-dev-setup.exe /install /log podman-setup.log
```
It generates the files `podman-setup.log` and `podman-setup_000_Setup.log`,
which include detailed installation information, in the current directory.
Run it in `quiet` mode to automate the installation and avoid interacting with
the GUI. Open the terminal **as an administrator**, add the `/quiet` option, and
set the bundle variable `MachineProvider` (`wsl` or `hyperv`):
```pwsh
contrib\win-installer\podman-5.1.0-dev-setup.exe /install `
/log podman-setup.log /quiet `
MachineProvider=wsl
```
:information_source: If uninstallation fails, the installer may end up in an
inconsistent state. Podman results as uninstalled, but some install packages are
still tracked in the Windows registry and will affect further tentative to
re-install Podman. When this is the case, trying to re-install Podman results in
the installer returning zero (success) but no action is executed. The trailing
packages `GID` can be found in installation logs:
```
Detected related package: {<GID>}
```
To fix this problem remove the related packages:
```pwsh
msiexec /x "{<GID>}"
```
#### Run the Windows installer automated tests
The following command executes a number of tests of the windows installer. Running
it requires an administrator terminal.
```pwsh
.\winmake.ps1 installertest [wsl|hyperv]
```
### Build and test the standalone `podman.msi` file
Building and testing the standalone `podman.msi` package during development may
be useful. Even if this package is not published as a standalone file when
Podman is released (it's included in the `podman-setup.exe` bundle), it can be
faster to build and test that rather than the full bundle during the development
phase.
Run the command `dotnet build` to build the standalone `podman.msi` file:
```pwsh
Push-Location .\contrib\win-installer\
dotnet build podman.wixproj /property:DefineConstants="VERSION=9.9.9" -o .
Pop-Location
```
It creates the file `.\contrib\win-installer\en-US\podman.msi`. Test it using the
[Microsoft Standard Installer](https://learn.microsoft.com/en-us/windows/win32/msi/standard-installer-command-line-options)
command line tool:
```pwsh
msiexec /package contrib\win-installer\en-US\podman.msi /l*v podman-msi.log
```
To run it in quiet, non-interactive mode, open the terminal **as an
administrator**, add the `/quiet` option, and set the MSI property
`MACHINE_PROVIDER` (`wsl` or `hyperv`):
```pwsh
msiexec /package contrib\win-installer\en-US\podman.msi /l*v podman-msi.log /quiet MACHINE_PROVIDER=wsl
```
:information_source: `podman.msi` GUI dialogs, defined in the file
`contrib\win-installer\welcome-install-dlg.wxs`, are distinct from the installation bundle
`podman-setup.exe` GUI dialogs, defined in
`contrib\win-installer\podman-theme.xml`.
### Verify the installation
Inspect the msi installation log `podman-msi.log` (or
`podman-setup_000_Setup.log` if testing with the bundle) to verify that the
installation was successful:
```pwsh
Select-String -Path "podman-msi.log" -Pattern "Installation success or error status: 0"
```
These commands too are helpful to check the installation:
```pwsh
# Check the copy of the podman client in the Podman folder
Test-Path -Path "$ENV:PROGRAMFILES\RedHat\Podman\podman.exe"
# Check the generation of the podman configuration file
Test-Path -Path "$ENV:PROGRAMDATA\containers\containers.conf.d\99-podman-machine-provider.conf"
# Check that the installer configured the right provider
Get-Content "$ENV:PROGRAMDATA\containers\containers.conf.d\99-podman-machine-provider.conf" | Select -Skip 1 | ConvertFrom-StringData | % { $_.provider }
# Check the creation of the registry key
Test-Path -Path "HKLM:\SOFTWARE\Red Hat\Podman"
Get-ItemProperty "HKLM:\SOFTWARE\Red Hat\Podman" InstallDir
# Check the podman.exe is in the $PATH
$env:PATH | Select-String -Pattern "Podman"
```
:information_source: Podman CI uses script
`contrib\cirrus\win-installer-main.ps1`. Use it locally, too, to build and test
the installer:
```pwsh
$ENV:CONTAINERS_MACHINE_PROVIDER='wsl'; .\contrib\cirrus\win-installer-main.ps1
$ENV:CONTAINERS_MACHINE_PROVIDER='hyperv'; .\contrib\cirrus\win-installer-main.ps1
```
### Uninstall and clean-up
Podman can be uninstalled from the Windows Control Panel or running the
following command from a terminal **as an administrator**:
```pwsh
contrib\win-installer\podman-5.1.0-dev-setup.exe /uninstall /quiet /log podman-setup-uninstall.log
```
The uninstaller does not delete some folders. Clean them up manually:
```pwsh
$extraFolders = @(
"$ENV:PROGRAMDATA\containers\"
"$ENV:LOCALAPPDATA\containers\"
"$env:USERPROFILE.config\containers\"
"$env:USERPROFILE.local\share\containers\"
)
$extraFolders | ForEach-Object {Remove-Item -Recurse -Force $PSItem}
```
The following commands are helpful to verify that the uninstallation was
successful:
```pwsh
# Inspect the uninstallation log for a success message
Select-String -Path "podman-setup-uninstall_000_Setup.log" -Pattern "Removal success or error status: 0"
# Check that the uninstaller removed Podman resources
$foldersToCheck = @(
"$ENV:PROGRAMFILES\RedHat\Podman\podman.exe"
"HKLM:\SOFTWARE\Red Hat\Podman"
"$ENV:PROGRAMDATA\containers\"
"$env:USERPROFILE.config\containers\"
"$env:USERPROFILE.local\share\containers\"
"$ENV:LOCALAPPDATA\containers\"
"$ENV:PROGRAMDATA\containers\containers.conf.d\99-podman-machine-provider.conf"
)
$foldersToCheck | ForEach-Object {Test-Path -Path $PSItem}
```
## Validate changes before submitting a PR
The script `winmake.ps1` has a couple of targets to check the source code
statically. GitHub Pull request checks execute the same statical analysis. It is
highly recommended that you run them locally before submitting a PR.
### winmake lint
The `lint` target provides a fast validation target. It runs the following
tools:
- `golangci-lint`: runs go-specific linters configured in
[`.golangci.yml`](.golangci.yml)
- `pre-commit`: runs more linters configured in
[`.pre-commit-config.yaml`](.pre-commit-config.yaml)
:information_source: Install [golangci-lint](https://golangci-lint.run) and
[pre-commit](https://pre-commit.com) to run `winmake.ps1 lint`:
```pwsh
winget install -e golangci-lint.golangci-lint
winget install -e Python.Python.3.13
pip install pre-commit
```
### winmake validatepr
Target `validatepr` performs a more exhaustive validation but takes
significantly more time to complete. It uses `podman` to run the target
`.validatepr` of the [Linux `Makefile`](Makefile). It builds Podman for Linux,
MacOS and Windows and then performs the same checks as the `lint` target plus
many more.
:information_source: Create and start a Podman machine before running
`winmake.ps1 validatepr`. Configure the Podman machine with at least 4GB of
memory:
`podman machine init -m 4096`.

View File

@ -0,0 +1,245 @@
//go:build darwin
package main
import (
"bytes"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"syscall"
"text/template"
"github.com/containers/storage/pkg/fileutils"
"github.com/spf13/cobra"
)
const (
mode755 = 0755
mode644 = 0644
)
const launchConfig = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.github.containers.podman.helper-{{.User}}</string>
<key>ProgramArguments</key>
<array>
<string>{{.Program}}</string>
<string>service</string>
<string>{{.Target}}</string>
</array>
<key>inetdCompatibility</key>
<dict>
<key>Wait</key>
<false/>
</dict>
<key>UserName</key>
<string>root</string>
<key>Sockets</key>
<dict>
<key>Listeners</key>
<dict>
<key>SockFamily</key>
<string>Unix</string>
<key>SockPathName</key>
<string>/private/var/run/podman-helper-{{.User}}.socket</string>
<key>SockPathOwner</key>
<integer>{{.UID}}</integer>
<key>SockPathMode</key>
<!-- SockPathMode takes base 10 (384 = 0600) -->
<integer>384</integer>
<key>SockType</key>
<string>stream</string>
</dict>
</dict>
</dict>
</plist>
`
type launchParams struct {
Program string
User string
UID string
Target string
}
var installCmd = &cobra.Command{
Use: "install",
Short: "Install the podman helper agent",
Long: "Install the podman helper agent, which manages the /var/run/docker.sock link",
PreRun: silentUsage,
RunE: install,
}
func init() {
addPrefixFlag(installCmd)
rootCmd.AddCommand(installCmd)
}
func install(cmd *cobra.Command, args []string) error {
userName, uid, homeDir, err := getUser()
if err != nil {
return err
}
labelName := fmt.Sprintf("com.github.containers.podman.helper-%s.plist", userName)
fileName := filepath.Join("/Library", "LaunchDaemons", labelName)
if err := fileutils.Exists(fileName); err == nil || !errors.Is(err, fs.ErrNotExist) {
fmt.Fprintln(os.Stderr, "helper is already installed, skipping the install, uninstall first if you want to reinstall")
return nil
}
prog, err := installExecutable(userName)
if err != nil {
return err
}
target := filepath.Join(homeDir, ".local", "share", "containers", "podman", "machine", "podman.sock")
var buf bytes.Buffer
t := template.Must(template.New("launchdConfig").Parse(launchConfig))
err = t.Execute(&buf, launchParams{prog, userName, uid, target})
if err != nil {
return err
}
file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_EXCL, mode644)
if err != nil {
return fmt.Errorf("creating helper plist file: %w", err)
}
defer file.Close()
_, err = buf.WriteTo(file)
if err != nil {
return err
}
if err = runDetectErr("launchctl", "load", fileName); err != nil {
return fmt.Errorf("launchctl failed loading service: %w", err)
}
return nil
}
func restrictRecursive(targetDir string, until string) error {
for targetDir != until && len(targetDir) > 1 {
info, err := os.Lstat(targetDir)
if err != nil {
return err
}
if info.Mode()&fs.ModeSymlink != 0 {
return fmt.Errorf("symlinks not allowed in helper paths (remove them and rerun): %s", targetDir)
}
if err = os.Chown(targetDir, 0, 0); err != nil {
return fmt.Errorf("could not update ownership of helper path: %w", err)
}
if err = os.Chmod(targetDir, mode755|fs.ModeSticky); err != nil {
return fmt.Errorf("could not update permissions of helper path: %w", err)
}
targetDir = filepath.Dir(targetDir)
}
return nil
}
func verifyRootDeep(path string) error {
path = filepath.Clean(path)
current := "/"
segs := strings.Split(path, "/")
depth := 0
for i := 1; i < len(segs); i++ {
seg := segs[i]
current = filepath.Join(current, seg)
info, err := os.Lstat(current)
if err != nil {
return err
}
stat := info.Sys().(*syscall.Stat_t)
if stat.Uid != 0 {
return fmt.Errorf("installation target path must be solely owned by root: %s is not", current)
}
if info.Mode()&fs.ModeSymlink != 0 {
target, err := os.Readlink(current)
if err != nil {
return err
}
targetParts := strings.Split(target, "/")
segs = append(targetParts, segs[i+1:]...)
if depth++; depth > 1000 {
return errors.New("reached max recursion depth, link structure is cyclical or too complex")
}
if !filepath.IsAbs(target) {
current = filepath.Dir(current)
i = -1 // Start at 0
} else {
current = "/"
i = 0 // Skip empty first segment
}
}
}
return nil
}
func installExecutable(user string) (string, error) {
// Since the installed executable runs as root, as a precaution verify root ownership of
// the entire installation path, and utilize sticky + read-only perms for the helper path
// suffix. The goal is to help users harden against privilege escalation from loose
// filesystem permissions.
//
// Since userspace package management tools, such as brew, delegate management of system
// paths to standard unix users, the daemon executable is copied into a separate more
// restricted area of the filesystem.
if err := verifyRootDeep(installPrefix); err != nil {
return "", err
}
targetDir := filepath.Join(installPrefix, "podman", "helper", user)
if err := os.MkdirAll(targetDir, mode755); err != nil {
return "", fmt.Errorf("could not create helper directory structure: %w", err)
}
// Correct any incorrect perms on previously existing directories and verify no symlinks
if err := restrictRecursive(targetDir, installPrefix); err != nil {
return "", err
}
exec, err := os.Executable()
if err != nil {
return "", err
}
install := filepath.Join(targetDir, filepath.Base(exec))
return install, copyFile(install, exec, mode755)
}
func copyFile(dest string, source string, perms fs.FileMode) error {
in, err := os.Open(source)
if err != nil {
return err
}
defer in.Close()
out, err := os.OpenFile(dest, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, perms)
if err != nil {
return err
}
defer out.Close()
if _, err := io.Copy(out, in); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,148 @@
//go:build darwin
package main
import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"github.com/spf13/cobra"
)
const (
defaultPrefix = "/usr/local"
dockerSock = "/var/run/docker.sock"
)
var installPrefix string
var rootCmd = &cobra.Command{
Use: "podman-mac-helper",
Short: "A system helper to manage docker.sock",
Long: `podman-mac-helper is a system helper service and tool for managing docker.sock `,
CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true},
SilenceErrors: true,
}
// Note, this code is security sensitive since it runs under privilege.
// Limit actions to what is strictly necessary, and take appropriate
// safeguards
//
// After installation the service call is ran under launchd in a nowait
// inetd style fashion, so stdin, stdout, and stderr are all pointing to
// an accepted connection
//
// This service is installed once per user and will redirect
// /var/run/docker to the fixed user-assigned unix socket location.
//
// Control communication is restricted to each user specific service via
// unix file permissions
func main() {
if os.Geteuid() != 0 {
fmt.Printf("This command must be run as root via sudo or as a script\n")
os.Exit(1)
}
if err := rootCmd.Execute(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error())
os.Exit(1)
}
}
func getUserInfo(name string) (string, string, string, error) {
// We exec id instead of using user.Lookup to remain compat
// with CGO disabled.
cmd := exec.Command("/usr/bin/id", "-P", name)
output, err := cmd.StdoutPipe()
if err != nil {
return "", "", "", err
}
if err := cmd.Start(); err != nil {
return "", "", "", err
}
entry := readCapped(output)
elements := strings.Split(entry, ":")
if len(elements) < 9 || elements[0] != name {
return "", "", "", errors.New("could not look up user")
}
return elements[0], elements[2], elements[8], nil
}
func getUser() (string, string, string, error) {
name, found := os.LookupEnv("SUDO_USER")
if !found {
name, found = os.LookupEnv("USER")
if !found {
return "", "", "", errors.New("could not determine user")
}
}
_, uid, home, err := getUserInfo(name)
if err != nil {
return "", "", "", fmt.Errorf("could not look up user: %s", name)
}
id, err := strconv.Atoi(uid)
if err != nil {
return "", "", "", fmt.Errorf("invalid uid for user: %s", name)
}
if id == 0 {
return "", "", "", errors.New("unexpected root user")
}
return name, uid, home, nil
}
// Used for commands that don't return a proper exit code
func runDetectErr(name string, args ...string) error {
cmd := exec.Command(name, args...)
errReader, err := cmd.StderrPipe()
if err != nil {
return err
}
err = cmd.Start()
if err == nil {
errString := readCapped(errReader)
if len(errString) > 0 {
re := regexp.MustCompile(`\r?\n`)
err = errors.New(re.ReplaceAllString(errString, ": "))
}
}
if werr := cmd.Wait(); werr != nil {
err = werr
}
return err
}
func readCapped(reader io.Reader) string {
// Cap output
buffer := make([]byte, 2048)
n, _ := io.ReadFull(reader, buffer)
_, _ = io.Copy(io.Discard, reader)
if n > 0 {
return string(buffer[:n])
}
return ""
}
func addPrefixFlag(cmd *cobra.Command) {
cmd.Flags().StringVar(&installPrefix, "prefix", defaultPrefix, "Sets the install location prefix")
}
func silentUsage(cmd *cobra.Command, args []string) {
cmd.SilenceUsage = true
cmd.SilenceErrors = true
}

View File

@ -0,0 +1,84 @@
//go:build darwin
package main
import (
"fmt"
"io"
"io/fs"
"os"
"time"
"github.com/spf13/cobra"
)
const (
trigger = "GO\n"
fail = "NO"
success = "OK"
)
var serviceCmd = &cobra.Command{
Use: "service",
Short: "Service requests",
Long: "Service requests",
PreRun: silentUsage,
Run: serviceRun,
Hidden: true,
}
func init() {
rootCmd.AddCommand(serviceCmd)
}
func serviceRun(cmd *cobra.Command, args []string) {
info, err := os.Stdin.Stat()
if err != nil || info.Mode()&fs.ModeSocket == 0 {
fmt.Fprintln(os.Stderr, "This is an internal command that is not intended for standard terminal usage")
os.Exit(1)
}
os.Exit(service())
}
func service() int {
defer os.Stdout.Close()
defer os.Stdin.Close()
defer os.Stderr.Close()
if len(os.Args) < 3 {
fmt.Print(fail)
return 1
}
target := os.Args[2]
request := make(chan bool)
go func() {
buf := make([]byte, 3)
_, err := io.ReadFull(os.Stdin, buf)
request <- err == nil && string(buf) == trigger
}()
valid := false
select {
case valid = <-request:
case <-time.After(5 * time.Second):
}
if !valid {
fmt.Println(fail)
return 2
}
err := os.Remove(dockerSock)
if err == nil || os.IsNotExist(err) {
err = os.Symlink(target, dockerSock)
}
if err != nil {
fmt.Print(fail)
return 3
}
fmt.Print(success)
return 0
}

View File

@ -0,0 +1,91 @@
//go:build darwin
package main
import (
"errors"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"github.com/containers/storage/pkg/fileutils"
"github.com/spf13/cobra"
)
var uninstallCmd = &cobra.Command{
Use: "uninstall",
Short: "Uninstall the podman helper agent",
Long: "Uninstall the podman helper agent, which manages the /var/run/docker.sock link",
PreRun: silentUsage,
RunE: uninstall,
}
func init() {
addPrefixFlag(uninstallCmd)
rootCmd.AddCommand(uninstallCmd)
}
func uninstall(cmd *cobra.Command, args []string) error {
userName, _, homeDir, err := getUser()
if err != nil {
return err
}
labelName := fmt.Sprintf("com.github.containers.podman.helper-%s", userName)
fileName := filepath.Join("/Library", "LaunchDaemons", labelName+".plist")
if err = runDetectErr("launchctl", "unload", fileName); err != nil {
// Try removing the service by label in case the service is half uninstalled
if rerr := runDetectErr("launchctl", "remove", labelName); rerr != nil {
// Exit code 3 = no service to remove
if exitErr, ok := rerr.(*exec.ExitError); !ok || exitErr.ExitCode() != 3 {
fmt.Fprintf(os.Stderr, "Warning: service unloading failed: %s\n", err.Error())
fmt.Fprintf(os.Stderr, "Warning: remove also failed: %s\n", rerr.Error())
}
}
}
if err := os.Remove(fileName); err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("could not remove plist file: %s", fileName)
}
}
helperPath := filepath.Join(installPrefix, "podman", "helper", userName)
if err := os.RemoveAll(helperPath); err != nil {
return fmt.Errorf("could not remove helper binary path: %s", helperPath)
}
// Get the file information of dockerSock
if err := fileutils.Lexists(dockerSock); err != nil {
// If the error is due to the file not existing, return nil
if errors.Is(err, fs.ErrNotExist) {
return nil
}
// Return an error if unable to get the file information
return fmt.Errorf("could not stat dockerSock: %v", err)
}
if target, err := os.Readlink(dockerSock); err != nil {
// Return an error if unable to read the symlink
return fmt.Errorf("could not read dockerSock symlink: %v", err)
} else {
// Check if the target of the symlink matches the expected target
expectedTarget := filepath.Join(homeDir, ".local", "share", "containers", "podman", "machine", "podman.sock")
if target != expectedTarget {
// If the targets do not match, print the information and return with nothing left to do
fmt.Printf("dockerSock does not point to the expected target: %v\n", target)
return nil
}
// Attempt to remove dockerSock
if err := os.Remove(dockerSock); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("could not remove dockerSock file: %s", err)
}
}
}
return nil
}

View File

@ -0,0 +1,129 @@
//go:build !remote
package main
import (
"fmt"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/internal/domain/entities"
"github.com/spf13/cobra"
)
var (
createStorageLayerDescription = `Create an unmanaged layer in local storage.`
createStorageLayerCmd = &cobra.Command{
Use: "create-storage-layer [options]",
Args: validate.NoArgs,
Short: "Create an unmanaged layer",
Long: createStorageLayerDescription,
RunE: createStorageLayer,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman testing create-storage-layer`,
}
createStorageLayerOpts entities.CreateStorageLayerOptions
createLayerDescription = `Create an unused layer in local storage.`
createLayerCmd = &cobra.Command{
Use: "create-layer [options]",
Args: validate.NoArgs,
Short: "Create an unused layer",
Long: createLayerDescription,
RunE: createLayer,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman testing create-layer`,
}
createLayerOpts entities.CreateLayerOptions
createImageDescription = `Create an image in local storage.`
createImageCmd = &cobra.Command{
Use: "create-image [options]",
Args: validate.NoArgs,
Short: "Create an image",
Long: createImageDescription,
RunE: createImage,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman testing create-image`,
}
createImageOpts entities.CreateImageOptions
createContainerDescription = `Create a container in local storage.`
createContainerCmd = &cobra.Command{
Use: "create-container [options]",
Args: validate.NoArgs,
Short: "Create a container",
Long: createContainerDescription,
RunE: createContainer,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman testing create-container`,
}
createContainerOpts entities.CreateContainerOptions
)
func init() {
mainCmd.AddCommand(createStorageLayerCmd)
flags := createStorageLayerCmd.Flags()
flags.StringVarP(&createStorageLayerOpts.ID, "id", "i", "", "ID to assign the new layer (default random)")
flags.StringVarP(&createStorageLayerOpts.Parent, "parent", "p", "", "ID of parent of new layer (default none)")
mainCmd.AddCommand(createLayerCmd)
flags = createLayerCmd.Flags()
flags.StringVarP(&createLayerOpts.ID, "id", "i", "", "ID to assign the new layer (default random)")
flags.StringVarP(&createLayerOpts.Parent, "parent", "p", "", "ID of parent of new layer (default none)")
mainCmd.AddCommand(createImageCmd)
flags = createImageCmd.Flags()
flags.StringVarP(&createImageOpts.ID, "id", "i", "", "ID to assign the new image (default random)")
flags.StringVarP(&createImageOpts.Layer, "layer", "l", "", "ID of image's main layer (default none)")
mainCmd.AddCommand(createContainerCmd)
flags = createContainerCmd.Flags()
flags.StringVarP(&createContainerOpts.ID, "id", "i", "", "ID to assign the new container (default random)")
flags.StringVarP(&createContainerOpts.Image, "image", "b", "", "ID of containers's base image (default none)")
flags.StringVarP(&createContainerOpts.Layer, "layer", "l", "", "ID of containers's read-write layer (default none)")
}
func createStorageLayer(cmd *cobra.Command, args []string) error {
results, err := testingEngine.CreateStorageLayer(mainContext, createStorageLayerOpts)
if err != nil {
return err
}
fmt.Println(results.ID)
return nil
}
func createLayer(cmd *cobra.Command, args []string) error {
results, err := testingEngine.CreateLayer(mainContext, createLayerOpts)
if err != nil {
return err
}
fmt.Println(results.ID)
return nil
}
func createImage(cmd *cobra.Command, args []string) error {
results, err := testingEngine.CreateImage(mainContext, createImageOpts)
if err != nil {
return err
}
fmt.Println(results.ID)
return nil
}
func createContainer(cmd *cobra.Command, args []string) error {
results, err := testingEngine.CreateContainer(mainContext, createContainerOpts)
if err != nil {
return err
}
fmt.Println(results.ID)
return nil
}

407
cmd/podman-testing/data.go Normal file
View File

@ -0,0 +1,407 @@
//go:build !remote
package main
import (
"errors"
"os"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/internal/domain/entities"
"github.com/spf13/cobra"
)
var (
createLayerDataDescription = `Create data for a layer in local storage.`
createLayerDataCmd = &cobra.Command{
Use: "create-layer-data [options]",
Args: validate.NoArgs,
Short: "Create data for a layer",
Long: createLayerDataDescription,
RunE: createLayerData,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman testing create-layer-data`,
}
createLayerDataOpts entities.CreateLayerDataOptions
createLayerDataKey string
createLayerDataValue string
createLayerDataFile string
createImageDataDescription = `Create data for an image in local storage.`
createImageDataCmd = &cobra.Command{
Use: "create-image-data [options]",
Args: validate.NoArgs,
Short: "Create data for an image",
Long: createImageDataDescription,
RunE: createImageData,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman testing create-image-data`,
}
createImageDataOpts entities.CreateImageDataOptions
createImageDataKey string
createImageDataValue string
createImageDataFile string
createContainerDataDescription = `Create data for a container in local storage.`
createContainerDataCmd = &cobra.Command{
Use: "create-container-data [options]",
Args: validate.NoArgs,
Short: "Create data for a container",
Long: createContainerDataDescription,
RunE: createContainerData,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman testing create-container-data`,
}
createContainerDataOpts entities.CreateContainerDataOptions
createContainerDataKey string
createContainerDataValue string
createContainerDataFile string
modifyLayerDataDescription = `Modify data for a layer in local storage, corrupting it.`
modifyLayerDataCmd = &cobra.Command{
Use: "modify-layer-data [options]",
Args: validate.NoArgs,
Short: "Modify data for a layer",
Long: modifyLayerDataDescription,
RunE: modifyLayerData,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman testing modify-layer-data`,
}
modifyLayerDataOpts entities.ModifyLayerDataOptions
modifyLayerDataValue string
modifyLayerDataFile string
modifyImageDataDescription = `Modify data for an image in local storage, corrupting it.`
modifyImageDataCmd = &cobra.Command{
Use: "modify-image-data [options]",
Args: validate.NoArgs,
Short: "Modify data for an image",
Long: modifyImageDataDescription,
RunE: modifyImageData,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman testing modify-image-data`,
}
modifyImageDataOpts entities.ModifyImageDataOptions
modifyImageDataValue string
modifyImageDataFile string
modifyContainerDataDescription = `Modify data for a container in local storage, corrupting it.`
modifyContainerDataCmd = &cobra.Command{
Use: "modify-container-data [options]",
Args: validate.NoArgs,
Short: "Modify data for a container",
Long: modifyContainerDataDescription,
RunE: modifyContainerData,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman testing modify-container-data`,
}
modifyContainerDataOpts entities.ModifyContainerDataOptions
modifyContainerDataValue string
modifyContainerDataFile string
removeLayerDataDescription = `Remove data from a layer in local storage, corrupting it.`
removeLayerDataCmd = &cobra.Command{
Use: "remove-layer-data [options]",
Args: validate.NoArgs,
Short: "Remove data for a layer",
Long: removeLayerDataDescription,
RunE: removeLayerData,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman testing remove-layer-data`,
}
removeLayerDataOpts entities.RemoveLayerDataOptions
removeImageDataDescription = `Remove data from an image in local storage, corrupting it.`
removeImageDataCmd = &cobra.Command{
Use: "remove-image-data [options]",
Args: validate.NoArgs,
Short: "Remove data from an image",
Long: removeImageDataDescription,
RunE: removeImageData,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman testing remove-image-data`,
}
removeImageDataOpts entities.RemoveImageDataOptions
removeContainerDataDescription = `Remove data from a container in local storage, corrupting it.`
removeContainerDataCmd = &cobra.Command{
Use: "remove-container-data [options]",
Args: validate.NoArgs,
Short: "Remove data from a container",
Long: removeContainerDataDescription,
RunE: removeContainerData,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman testing remove-container-data`,
}
removeContainerDataOpts entities.RemoveContainerDataOptions
)
func init() {
mainCmd.AddCommand(createLayerDataCmd)
flags := createLayerDataCmd.Flags()
flags.StringVarP(&createLayerDataOpts.ID, "layer", "i", "", "ID of the layer")
flags.StringVarP(&createLayerDataKey, "key", "k", "", "Name of the data item")
flags.StringVarP(&createLayerDataValue, "value", "v", "", "Value of the data item")
flags.StringVarP(&createLayerDataFile, "file", "f", "", "File containing the data item")
mainCmd.AddCommand(createImageDataCmd)
flags = createImageDataCmd.Flags()
flags.StringVarP(&createImageDataOpts.ID, "image", "i", "", "ID of the image")
flags.StringVarP(&createImageDataKey, "key", "k", "", "Name of the data item")
flags.StringVarP(&createImageDataValue, "value", "v", "", "Value of the data item")
flags.StringVarP(&createImageDataFile, "file", "f", "", "File containing the data item")
mainCmd.AddCommand(createContainerDataCmd)
flags = createContainerDataCmd.Flags()
flags.StringVarP(&createContainerDataOpts.ID, "container", "i", "", "ID of the container")
flags.StringVarP(&createContainerDataKey, "key", "k", "", "Name of the data item")
flags.StringVarP(&createContainerDataValue, "value", "v", "", "Value of the data item")
flags.StringVarP(&createContainerDataFile, "file", "f", "", "File containing the data item")
mainCmd.AddCommand(modifyLayerDataCmd)
flags = modifyLayerDataCmd.Flags()
flags.StringVarP(&modifyLayerDataOpts.ID, "layer", "i", "", "ID of the layer")
flags.StringVarP(&modifyLayerDataOpts.Key, "key", "k", "", "Name of the data item")
flags.StringVarP(&modifyLayerDataValue, "value", "v", "", "Value of the data item")
flags.StringVarP(&modifyLayerDataFile, "file", "f", "", "File containing the data item")
mainCmd.AddCommand(modifyImageDataCmd)
flags = modifyImageDataCmd.Flags()
flags.StringVarP(&modifyImageDataOpts.ID, "image", "i", "", "ID of the image")
flags.StringVarP(&modifyImageDataOpts.Key, "key", "k", "", "Name of the data item")
flags.StringVarP(&modifyImageDataValue, "value", "v", "", "Value of the data item")
flags.StringVarP(&modifyImageDataFile, "file", "f", "", "File containing the data item")
mainCmd.AddCommand(modifyContainerDataCmd)
flags = modifyContainerDataCmd.Flags()
flags.StringVarP(&modifyContainerDataOpts.ID, "container", "i", "", "ID of the container")
flags.StringVarP(&modifyContainerDataOpts.Key, "key", "k", "", "Name of the data item")
flags.StringVarP(&modifyContainerDataValue, "value", "v", "", "Value of the data item")
flags.StringVarP(&modifyContainerDataFile, "file", "f", "", "File containing the data item")
mainCmd.AddCommand(removeLayerDataCmd)
flags = removeLayerDataCmd.Flags()
flags.StringVarP(&removeLayerDataOpts.ID, "layer", "i", "", "ID of the layer")
flags.StringVarP(&removeLayerDataOpts.Key, "key", "k", "", "Name of the data item")
mainCmd.AddCommand(removeImageDataCmd)
flags = removeImageDataCmd.Flags()
flags.StringVarP(&removeImageDataOpts.ID, "image", "i", "", "ID of the image")
flags.StringVarP(&removeImageDataOpts.Key, "key", "k", "", "Name of the data item")
mainCmd.AddCommand(removeContainerDataCmd)
flags = removeContainerDataCmd.Flags()
flags.StringVarP(&removeContainerDataOpts.ID, "container", "i", "", "ID of the container")
flags.StringVarP(&removeContainerDataOpts.Key, "key", "k", "", "Name of the data item")
}
func createLayerData(cmd *cobra.Command, args []string) error {
if createLayerDataOpts.ID == "" {
return errors.New("layer ID not specified")
}
if createLayerDataKey == "" {
return errors.New("layer data name not specified")
}
if createLayerDataValue == "" && createLayerDataFile == "" {
return errors.New("neither layer data value nor file specified")
}
createLayerDataOpts.Data = make(map[string][]byte)
if createLayerDataValue != "" {
createLayerDataOpts.Data[createLayerDataKey] = []byte(createLayerDataValue)
}
if createLayerDataFile != "" {
buf, err := os.ReadFile(createLayerDataFile)
if err != nil {
return err
}
createLayerDataOpts.Data[createLayerDataKey] = buf
}
_, err := testingEngine.CreateLayerData(mainContext, createLayerDataOpts)
if err != nil {
return err
}
return nil
}
func createImageData(cmd *cobra.Command, args []string) error {
if createImageDataOpts.ID == "" {
return errors.New("image ID not specified")
}
if createImageDataKey == "" {
return errors.New("image data name not specified")
}
if createImageDataValue == "" && createImageDataFile == "" {
return errors.New("neither image data value nor file specified")
}
createImageDataOpts.Data = make(map[string][]byte)
if createImageDataValue != "" {
createImageDataOpts.Data[createImageDataKey] = []byte(createImageDataValue)
}
if createImageDataFile != "" {
d, err := os.ReadFile(createImageDataFile)
if err != nil {
return err
}
createImageDataOpts.Data[createImageDataKey] = d
}
_, err := testingEngine.CreateImageData(mainContext, createImageDataOpts)
if err != nil {
return err
}
return nil
}
func createContainerData(cmd *cobra.Command, args []string) error {
if createContainerDataOpts.ID == "" {
return errors.New("container ID not specified")
}
if createContainerDataKey == "" {
return errors.New("container data name not specified")
}
if createContainerDataValue == "" && createContainerDataFile == "" {
return errors.New("neither container data value nor file specified")
}
createContainerDataOpts.Data = make(map[string][]byte)
if createContainerDataValue != "" {
createContainerDataOpts.Data[createContainerDataKey] = []byte(createContainerDataValue)
}
if createContainerDataFile != "" {
d, err := os.ReadFile(createContainerDataFile)
if err != nil {
return err
}
createContainerDataOpts.Data[createContainerDataKey] = d
}
_, err := testingEngine.CreateContainerData(mainContext, createContainerDataOpts)
if err != nil {
return err
}
return nil
}
func modifyLayerData(cmd *cobra.Command, args []string) error {
if modifyLayerDataOpts.ID == "" {
return errors.New("layer ID not specified")
}
if modifyLayerDataOpts.Key == "" {
return errors.New("layer data name not specified")
}
if modifyLayerDataValue == "" && modifyLayerDataFile == "" {
return errors.New("neither layer data value nor file specified")
}
modifyLayerDataOpts.Data = []byte(modifyLayerDataValue)
if modifyLayerDataFile != "" {
d, err := os.ReadFile(modifyLayerDataFile)
if err != nil {
return err
}
modifyLayerDataOpts.Data = d
}
_, err := testingEngine.ModifyLayerData(mainContext, modifyLayerDataOpts)
if err != nil {
return err
}
return nil
}
func modifyImageData(cmd *cobra.Command, args []string) error {
if modifyImageDataOpts.ID == "" {
return errors.New("image ID not specified")
}
if modifyImageDataOpts.Key == "" {
return errors.New("image data name not specified")
}
if modifyImageDataValue == "" && modifyImageDataFile == "" {
return errors.New("neither image data value nor file specified")
}
modifyImageDataOpts.Data = []byte(modifyImageDataValue)
if modifyImageDataFile != "" {
d, err := os.ReadFile(modifyImageDataFile)
if err != nil {
return err
}
modifyImageDataOpts.Data = d
}
_, err := testingEngine.ModifyImageData(mainContext, modifyImageDataOpts)
if err != nil {
return err
}
return nil
}
func modifyContainerData(cmd *cobra.Command, args []string) error {
if modifyContainerDataOpts.ID == "" {
return errors.New("container ID not specified")
}
if modifyContainerDataOpts.Key == "" {
return errors.New("container data name not specified")
}
if modifyContainerDataValue == "" && modifyContainerDataFile == "" {
return errors.New("neither container data value nor file specified")
}
modifyContainerDataOpts.Data = []byte(modifyContainerDataValue)
if modifyContainerDataFile != "" {
d, err := os.ReadFile(modifyContainerDataFile)
if err != nil {
return err
}
modifyContainerDataOpts.Data = d
}
_, err := testingEngine.ModifyContainerData(mainContext, modifyContainerDataOpts)
if err != nil {
return err
}
return nil
}
func removeLayerData(cmd *cobra.Command, args []string) error {
if removeLayerDataOpts.ID == "" {
return errors.New("layer ID not specified")
}
if removeLayerDataOpts.Key == "" {
return errors.New("layer data name not specified")
}
_, err := testingEngine.RemoveLayerData(mainContext, removeLayerDataOpts)
if err != nil {
return err
}
return nil
}
func removeImageData(cmd *cobra.Command, args []string) error {
if removeImageDataOpts.ID == "" {
return errors.New("image ID not specified")
}
if removeImageDataOpts.Key == "" {
return errors.New("image data name not specified")
}
_, err := testingEngine.RemoveImageData(mainContext, removeImageDataOpts)
if err != nil {
return err
}
return nil
}
func removeContainerData(cmd *cobra.Command, args []string) error {
if removeContainerDataOpts.ID == "" {
return errors.New("container ID not specified")
}
if removeContainerDataOpts.Key == "" {
return errors.New("container data name not specified")
}
_, err := testingEngine.RemoveContainerData(mainContext, removeContainerDataOpts)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,93 @@
//go:build !remote
package main
import (
"errors"
"os"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/internal/domain/entities"
"github.com/spf13/cobra"
)
var (
populateLayerDescription = `Populate a layer in local storage.`
populateLayerCmd = &cobra.Command{
Use: "populate-layer [options]",
Args: validate.NoArgs,
Short: "Populate a layer",
Long: populateLayerDescription,
RunE: populateLayer,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman testing populate-layer`,
}
populateLayerOpts entities.PopulateLayerOptions
populateLayerFile string
modifyLayerDescription = `Modify a layer in local storage, corrupting it.`
modifyLayerCmd = &cobra.Command{
Use: "modify-layer [options]",
Args: validate.NoArgs,
Short: "Modify the contents of a layer",
Long: modifyLayerDescription,
RunE: modifyLayer,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman testing modify-layer`,
}
modifyLayerOpts entities.ModifyLayerOptions
modifyLayerFile string
)
func init() {
mainCmd.AddCommand(populateLayerCmd)
flags := populateLayerCmd.Flags()
flags.StringVarP(&populateLayerOpts.ID, "layer", "l", "", "ID of layer to be populated")
flags.StringVarP(&populateLayerFile, "file", "f", "", "archive of contents to extract in layer")
mainCmd.AddCommand(modifyLayerCmd)
flags = modifyLayerCmd.Flags()
flags.StringVarP(&modifyLayerOpts.ID, "layer", "l", "", "ID of layer to be modified")
flags.StringVarP(&modifyLayerFile, "file", "f", "", "archive of contents to extract over layer")
}
func populateLayer(cmd *cobra.Command, args []string) error {
if populateLayerOpts.ID == "" {
return errors.New("layer ID not specified")
}
if populateLayerFile == "" {
return errors.New("layer contents file not specified")
}
buf, err := os.ReadFile(populateLayerFile)
if err != nil {
return err
}
populateLayerOpts.ContentsArchive = buf
_, err = testingEngine.PopulateLayer(mainContext, populateLayerOpts)
if err != nil {
return err
}
return nil
}
func modifyLayer(cmd *cobra.Command, args []string) error {
if modifyLayerOpts.ID == "" {
return errors.New("layer ID not specified")
}
if modifyLayerFile == "" {
return errors.New("layer contents file not specified")
}
buf, err := os.ReadFile(modifyLayerFile)
if err != nil {
return err
}
modifyLayerOpts.ContentsArchive = buf
_, err = testingEngine.ModifyLayer(mainContext, modifyLayerOpts)
if err != nil {
return err
}
return nil
}

130
cmd/podman-testing/main.go Normal file
View File

@ -0,0 +1,130 @@
//go:build !remote
package main
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"syscall"
"github.com/containers/common/pkg/config"
_ "github.com/containers/podman/v5/cmd/podman/completion"
ientities "github.com/containers/podman/v5/internal/domain/entities"
"github.com/containers/podman/v5/internal/domain/infra"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/storage"
"github.com/containers/storage/pkg/reexec"
"github.com/containers/storage/pkg/unshare"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
mainCmd = &cobra.Command{
Use: "podman-testing",
Long: "Assorted tools for use in testing podman",
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return before()
},
PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
return after()
},
SilenceUsage: true,
SilenceErrors: true,
}
mainContext = context.Background()
podmanConfig entities.PodmanConfig
globalStorageOptions storage.StoreOptions
globalLogLevel string
testingEngine ientities.TestingEngine
)
func init() {
podmanConfig.FlagSet = mainCmd.PersistentFlags()
fl := mainCmd.PersistentFlags()
fl.StringVar(&podmanConfig.DockerConfig, "docker-config", os.Getenv("DOCKER_CONFIG"), "path to .docker/config")
fl.StringVar(&globalLogLevel, "log-level", "warn", "logging level")
fl.StringVar(&podmanConfig.URI, "url", "", "URL to access Podman service")
fl.StringVar(&podmanConfig.RegistriesConf, "registries-conf", os.Getenv("REGISTRIES_CONF"), "path to registries.conf (REGISTRIES_CONF)")
}
func before() error {
if globalLogLevel != "" {
parsedLogLevel, err := logrus.ParseLevel(globalLogLevel)
if err != nil {
return fmt.Errorf("parsing log level %q: %w", globalLogLevel, err)
}
logrus.SetLevel(parsedLogLevel)
}
if err := storeBefore(); err != nil {
return fmt.Errorf("setting up storage: %w", err)
}
podmanConfig.EngineMode = engineMode
podmanConfig.Remote = podmanConfig.URI != ""
containersConf, err := config.Default()
if err != nil {
return fmt.Errorf("loading default configuration (may reference $CONTAINERS_CONF): %w", err)
}
podmanConfig.ContainersConfDefaultsRO = containersConf
containersConf, err = config.New(nil)
if err != nil {
return fmt.Errorf("loading default configuration (may reference $CONTAINERS_CONF): %w", err)
}
podmanConfig.ContainersConf = containersConf
podmanConfig.StorageDriver = globalStorageOptions.GraphDriverName
podmanConfig.GraphRoot = globalStorageOptions.GraphRoot
podmanConfig.Runroot = globalStorageOptions.RunRoot
podmanConfig.ImageStore = globalStorageOptions.ImageStore
podmanConfig.StorageOpts = globalStorageOptions.GraphDriverOptions
podmanConfig.TransientStore = globalStorageOptions.TransientStore
te, err := infra.NewTestingEngine(&podmanConfig)
if err != nil {
return fmt.Errorf("initializing libpod: %w", err)
}
testingEngine = te
return nil
}
func after() error {
if err := storeAfter(); err != nil {
return fmt.Errorf("shutting down storage: %w", err)
}
return nil
}
func main() {
if reexec.Init() {
// We were invoked with a different argv[0] indicating that we
// had a specific job to do as a subprocess, and it's done.
return
}
unshare.MaybeReexecUsingUserNamespace(false)
exitCode := 1
if err := mainCmd.Execute(); err != nil {
if logrus.IsLevelEnabled(logrus.TraceLevel) {
fmt.Fprintf(os.Stderr, "Error: %+v\n", err)
} else {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
}
var ee *exec.ExitError
if errors.As(err, &ee) {
if w, ok := ee.Sys().(syscall.WaitStatus); ok {
exitCode = w.ExitStatus()
}
}
} else {
exitCode = 0
}
os.Exit(exitCode)
}

View File

@ -0,0 +1,120 @@
//go:build !remote
package main
import (
"fmt"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/internal/domain/entities"
"github.com/spf13/cobra"
)
var (
removeStorageLayerDescription = `Remove an unmanaged layer in local storage, potentially corrupting it.`
removeStorageLayerCmd = &cobra.Command{
Use: "remove-storage-layer [options]",
Args: validate.NoArgs,
Short: "Remove an unmanaged layer",
Long: removeStorageLayerDescription,
RunE: removeStorageLayer,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman testing remove-storage-layer`,
}
removeStorageLayerOpts entities.RemoveStorageLayerOptions
removeLayerDescription = `Remove a layer in local storage, potentially corrupting it.`
removeLayerCmd = &cobra.Command{
Use: "remove-layer [options]",
Args: validate.NoArgs,
Short: "Remove a layer",
Long: removeLayerDescription,
RunE: removeLayer,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman testing remove-layer`,
}
removeLayerOpts entities.RemoveLayerOptions
removeImageDescription = `Remove an image in local storage, potentially corrupting it.`
removeImageCmd = &cobra.Command{
Use: "remove-image [options]",
Args: validate.NoArgs,
Short: "Remove an image",
Long: removeImageDescription,
RunE: removeImage,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman testing remove-image`,
}
removeImageOpts entities.RemoveImageOptions
removeContainerDescription = `Remove a container in local storage, potentially corrupting it.`
removeContainerCmd = &cobra.Command{
Use: "remove-container [options]",
Args: validate.NoArgs,
Short: "Remove an container",
Long: removeContainerDescription,
RunE: removeContainer,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman testing remove-container`,
}
removeContainerOpts entities.RemoveContainerOptions
)
func init() {
mainCmd.AddCommand(removeStorageLayerCmd)
flags := removeStorageLayerCmd.Flags()
flags.StringVarP(&removeStorageLayerOpts.ID, "layer", "i", "", "ID of the layer to remove")
mainCmd.AddCommand(removeLayerCmd)
flags = removeLayerCmd.Flags()
flags.StringVarP(&removeLayerOpts.ID, "layer", "i", "", "ID of the layer to remove")
mainCmd.AddCommand(removeImageCmd)
flags = removeImageCmd.Flags()
flags.StringVarP(&removeImageOpts.ID, "image", "i", "", "ID of the image to remove")
mainCmd.AddCommand(removeContainerCmd)
flags = removeContainerCmd.Flags()
flags.StringVarP(&removeContainerOpts.ID, "container", "i", "", "ID of the container to remove")
}
func removeStorageLayer(cmd *cobra.Command, args []string) error {
results, err := testingEngine.RemoveStorageLayer(mainContext, removeStorageLayerOpts)
if err != nil {
return err
}
fmt.Println(results.ID)
return nil
}
func removeLayer(cmd *cobra.Command, args []string) error {
results, err := testingEngine.RemoveLayer(mainContext, removeLayerOpts)
if err != nil {
return err
}
fmt.Println(results.ID)
return nil
}
func removeImage(cmd *cobra.Command, args []string) error {
results, err := testingEngine.RemoveImage(mainContext, removeImageOpts)
if err != nil {
return err
}
fmt.Println(results.ID)
return nil
}
func removeContainer(cmd *cobra.Command, args []string) error {
results, err := testingEngine.RemoveContainer(mainContext, removeContainerOpts)
if err != nil {
return err
}
fmt.Println(results.ID)
return nil
}

View File

@ -0,0 +1,64 @@
//go:build (linux || freebsd) && !remote
package main
import (
"fmt"
"os"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/storage"
"github.com/containers/storage/types"
)
var (
globalStore storage.Store
engineMode = entities.ABIMode
)
func init() {
if defaultStoreOptions, err := storage.DefaultStoreOptions(); err == nil {
globalStorageOptions = defaultStoreOptions
}
if storageConf, ok := os.LookupEnv("CONTAINERS_STORAGE_CONF"); ok {
options := globalStorageOptions
if types.ReloadConfigurationFileIfNeeded(storageConf, &options) == nil {
globalStorageOptions = options
}
}
fl := mainCmd.PersistentFlags()
fl.StringVar(&globalStorageOptions.GraphDriverName, "storage-driver", "", "storage driver used to manage images and containers")
fl.StringVar(&globalStorageOptions.GraphRoot, "root", "", "where images and containers will be stored")
fl.StringVar(&globalStorageOptions.RunRoot, "runroot", "", "where volatile state information will be stored")
fl.StringArrayVar(&globalStorageOptions.GraphDriverOptions, "storage-opt", nil, "storage driver options")
fl.StringVar(&globalStorageOptions.ImageStore, "imagestore", "", "where to store just some parts of images")
fl.BoolVar(&globalStorageOptions.TransientStore, "transient-store", false, "enable transient container storage")
}
func storeBefore() error {
defaultStoreOptions, err := storage.DefaultStoreOptions()
if err != nil {
fmt.Fprintf(os.Stderr, "selecting storage options: %v", err)
return nil
}
globalStorageOptions = defaultStoreOptions
store, err := storage.GetStore(globalStorageOptions)
if err != nil {
return err
}
globalStore = store
if podmanConfig.URI != "" {
engineMode = entities.TunnelMode
} else {
engineMode = entities.ABIMode
}
return nil
}
func storeAfter() error {
if globalStore != nil {
_, err := globalStore.Shutdown(false)
return err
}
return nil
}

View File

@ -0,0 +1,72 @@
//go:build windows
package main
import (
"bytes"
"fmt"
"github.com/sirupsen/logrus"
"golang.org/x/sys/windows/svc/eventlog"
)
// Logrus hook that delegates to windows event log
type EventLogHook struct {
events *eventlog.Log
}
type LogFormat struct {
name string
}
func (f *LogFormat) Format(entry *logrus.Entry) ([]byte, error) {
var b *bytes.Buffer
if entry.Buffer != nil {
b = entry.Buffer
} else {
b = &bytes.Buffer{}
}
fmt.Fprintf(b, "[%-5s] %s: %s", entry.Level.String(), f.name, entry.Message)
for key, value := range entry.Data {
fmt.Fprintf(b, " {%s = %s}", key, value)
}
b.WriteByte('\n')
return b.Bytes(), nil
}
func NewEventHook(events *eventlog.Log, name string) *EventLogHook {
logrus.SetFormatter(&LogFormat{name})
return &EventLogHook{events}
}
func (hook *EventLogHook) Fire(entry *logrus.Entry) error {
line, err := entry.String()
if err != nil {
return err
}
switch entry.Level {
case logrus.PanicLevel:
return hook.events.Error(1002, line)
case logrus.FatalLevel:
return hook.events.Error(1001, line)
case logrus.ErrorLevel:
return hook.events.Error(1000, line)
case logrus.WarnLevel:
return hook.events.Warning(1000, line)
case logrus.InfoLevel:
return hook.events.Info(1000, line)
case logrus.DebugLevel, logrus.TraceLevel:
return hook.events.Info(1001, line)
default:
return nil
}
}
func (hook *EventLogHook) Levels() []logrus.Level {
return logrus.AllLevels
}

View File

@ -0,0 +1,102 @@
//go:build windows
package main
import (
"fmt"
"os"
"path"
"syscall"
"time"
"unsafe"
"github.com/containers/podman/v5/pkg/machine/wsl/wutil"
"github.com/sirupsen/logrus"
"golang.org/x/sys/windows/svc/eventlog"
)
const (
MB_ICONWARNING = 0x00000030
MB_OK = 0x00000000
MB_DEFBUTTON1 = 0x00000000
)
const KernelWarning = "WSL Kernel installation did not complete successfully. " +
"Podman machine will attempt to install this at a later time. " +
"You can also manually complete the installation using the " +
"\"wsl --update\" command."
func setupLogging(name string) (*eventlog.Log, error) {
// Reuse the Built-in .NET Runtime Source so that we do not
// have to provide a message table and modify the system
// event configuration
log, err := eventlog.Open(".NET Runtime")
if err != nil {
return nil, err
}
logrus.AddHook(NewEventHook(log, name))
logrus.SetLevel(logrus.InfoLevel)
return log, nil
}
func installWslKernel() error {
logrus.Info("Installing WSL Kernel update")
var (
err error
)
backoff := 500 * time.Millisecond
for i := 1; i < 6; i++ {
err = wutil.SilentExec("--update")
if err == nil {
break
}
// In case of unusual circumstances (e.g. race with installer actions)
// retry a few times
logrus.Warn("An error occurred attempting the WSL Kernel update, retrying...")
time.Sleep(backoff)
backoff *= 2
}
if err != nil {
err = fmt.Errorf("could not install WSL Kernel: %w", err)
}
return err
}
// Creates an "warn" style pop-up window
func warn(title string, caption string) int {
format := MB_ICONWARNING | MB_OK | MB_DEFBUTTON1
user32 := syscall.NewLazyDLL("user32.dll")
captionPtr, _ := syscall.UTF16PtrFromString(caption)
titlePtr, _ := syscall.UTF16PtrFromString(title)
ret, _, _ := user32.NewProc("MessageBoxW").Call(
uintptr(0),
uintptr(unsafe.Pointer(captionPtr)),
uintptr(unsafe.Pointer(titlePtr)),
uintptr(format))
return int(ret)
}
func main() {
args := os.Args
_, _ = setupLogging(path.Base(args[0]))
if wutil.IsWSLInstalled() {
// nothing to do
logrus.Info("WSL Kernel already installed")
return
}
result := installWslKernel()
if result != nil {
logrus.Error(result.Error())
_ = warn("Podman Setup", KernelWarning)
}
logrus.Info("WSL Kernel update successful")
}

119
cmd/podman/README.md Normal file
View File

@ -0,0 +1,119 @@
# Podman CLI
The following is an example of how to add a new primary command (`manifest`) and a sub-command (`inspect`) to the Podman CLI.
This is example code, the production code has additional error checking and the business logic provided.
See items below for details on building, installing, contributing to Podman:
- [Readme](README.md)
- [Contributing](../../CONTRIBUTING.md)
- [Podman Usage](../../transfer.md)
- [Trouble Shooting](../../troubleshooting.md)
- [Code Of Conduct](../../CODE-OF-CONDUCT.md)
## Adding a new command `podman manifest`
```shell script
$ mkdir -p $GOPATH/src/github.com/containers/podman/cmd/podman/manifest
```
Create the file ```$GOPATH/src/github.com/containers/podman/cmd/podman/manifest/manifest.go```
```go
package manifest
import (
"github.com/containers/podman/cmd/podman/registry"
"github.com/containers/podman/cmd/podman/validate"
"github.com/containers/podman/pkg/domain/entities"
"github.com/spf13/cobra"
)
var (
// podman _manifests_
manifestCmd = &cobra.Command{
Use: "manifest",
Short: "Manage manifests",
Args: cobra.ExactArgs(1),
Long: "Manage manifests",
Example: "podman manifest IMAGE",
TraverseChildren: true,
RunE: validate.SubCommandExists, // Report error if there is no sub command given
}
)
func init() {
// Subscribe command to podman
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: manifestCmd,
})
}
```
To "wire" in the `manifest` command, edit the file ```$GOPATH/src/github.com/containers/podman/cmd/podman/main.go``` to add:
```go
package main
import _ "github.com/containers/podman/cmd/podman/manifest"
```
## Adding a new sub command `podman manifest list`
Create the file ```$GOPATH/src/github.com/containers/podman/cmd/podman/manifest/inspect.go```
```go
package manifest
import (
"github.com/containers/podman/cmd/podman/registry"
"github.com/containers/podman/pkg/domain/entities"
"github.com/spf13/cobra"
)
var (
// podman manifests _inspect_
inspectCmd = &cobra.Command{
Use: "inspect IMAGE",
Short: "Display manifest from image",
Long: "Displays the low-level information on a manifest identified by image name or ID",
RunE: inspect,
Annotations: map[string]string{
// Add this annotation if this command cannot be run rootless
// registry.ParentNSRequired: "",
},
Example: "podman manifest inspect DEADBEEF",
}
)
func init() {
// Subscribe inspect sub command to manifest command
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: inspectCmd,
// The parent command to proceed this command on the CLI
Parent: manifestCmd,
})
// This is where you would configure the cobra flags using inspectCmd.Flags()
}
// Business logic: cmd is inspectCmd, args is the positional arguments from os.Args
func inspect(cmd *cobra.Command, args []string) error {
// Business logic using registry.ImageEngine()
// Do not pull from libpod directly use the domain objects and types
return nil
}
```
## Helper functions
The complete set can be found in the `validate` package, here are some examples:
- `cobra.Command{ Args: validate.NoArgs }` used when the command does not accept errors
- `cobra.Command{ Args: validate.IdOrLatestArgs }` used to ensure either a list of ids given or the --latest flag
- `cobra.Command{ RunE: validate.SubCommandExists }` used to validate a subcommand given to a command
- `validate.ChoiceValue` used to create a `pflag.Value` that validate user input against a provided slice of values. For example:
```go
flags := cobraCommand.Flags()
created := validate.ChoiceValue(&opts.Sort, "command", "created", "id", "image", "names", "runningfor", "size", "status")
flags.Var(created, "sort", "Sort output by: "+created.Choices())
```
## Adding CLI flags
When adding adding a new cli option that accepts a string array, there are two options to choose from: `StringSlice()` and `StringArray()`.
They differ slightly in their behavior: `StringSlice()` allows the values to be comma separated so `--opt v1,v2 --opt v3` results in
`[]string{"v1", "v2", "v3"}`, while `StringArray()` would result in `[]string{"v1,v2", "v3"}`. Thus it is impossible to use values with comma in `StringSlice()`, which makes it unsuitable for flags that accept arbitrary values such as file paths as example. Also, because `StringSlice()` uses the csv lib to parse the values, it has special escaping rules for things like quotes, see https://github.com/containers/podman/issues/20064 for an example of how complicated things can get because of this.
Thus use `StringSlice()` only when the option accepts predefined values that do not contain special characters, for example `--cap-add` and `--cap-drop` are a good example for this. Using `--cap-add NET_ADMIN,NET_RAW` is equal to `--cap-add NET_ADMIN --cap-add NET_RAW` so it is better suited to save some typing for users.
When in doubt always choose `StringArray()` over `StringSlice()`.

View File

@ -0,0 +1,94 @@
package artifact
import (
"fmt"
"path/filepath"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/domain/utils"
"github.com/spf13/cobra"
)
var (
addCmd = &cobra.Command{
Use: "add [options] ARTIFACT PATH [...PATH]",
Short: "Add an OCI artifact to the local store",
Long: "Add an OCI artifact to the local store from the local filesystem",
RunE: add,
Args: cobra.MinimumNArgs(2),
ValidArgsFunction: common.AutocompleteArtifactAdd,
Example: `podman artifact add quay.io/myimage/myartifact:latest /tmp/foobar.txt
podman artifact add --file-type text/yaml quay.io/myimage/myartifact:latest /tmp/foobar.yaml
podman artifact add --append quay.io/myimage/myartifact:latest /tmp/foobar.tar.gz`,
}
)
// AddOptionsWrapper wraps entities.ArtifactsAddOptions and prevents leaking
// CLI-only fields into the API types.
type AddOptionsWrapper struct {
entities.ArtifactAddOptions
AnnotationsCLI []string // CLI only
}
var addOpts AddOptionsWrapper
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: addCmd,
Parent: artifactCmd,
})
flags := addCmd.Flags()
annotationFlagName := "annotation"
flags.StringArrayVar(&addOpts.AnnotationsCLI, annotationFlagName, nil, "set an `annotation` for the specified files of artifact")
_ = addCmd.RegisterFlagCompletionFunc(annotationFlagName, completion.AutocompleteNone)
addMIMETypeFlagName := "type"
flags.StringVar(&addOpts.ArtifactMIMEType, addMIMETypeFlagName, "", "Use type to describe an artifact")
_ = addCmd.RegisterFlagCompletionFunc(addMIMETypeFlagName, completion.AutocompleteNone)
appendFlagName := "append"
flags.BoolVarP(&addOpts.Append, appendFlagName, "a", false, "Append files to an existing artifact")
fileMIMETypeFlagName := "file-type"
flags.StringVarP(&addOpts.FileMIMEType, fileMIMETypeFlagName, "", "", "Set file type to use for the artifact (layer)")
_ = addCmd.RegisterFlagCompletionFunc(fileMIMETypeFlagName, completion.AutocompleteNone)
}
func add(cmd *cobra.Command, args []string) error {
artifactName := args[0]
blobs := args[1:]
annots, err := utils.ParseAnnotations(addOpts.AnnotationsCLI)
if err != nil {
return err
}
opts := entities.ArtifactAddOptions{
Annotations: annots,
ArtifactMIMEType: addOpts.ArtifactMIMEType,
Append: addOpts.Append,
FileMIMEType: addOpts.FileMIMEType,
}
artifactBlobs := make([]entities.ArtifactBlob, 0, len(blobs))
for _, blobPath := range blobs {
artifactBlob := entities.ArtifactBlob{
BlobFilePath: blobPath,
FileName: filepath.Base(blobPath),
}
artifactBlobs = append(artifactBlobs, artifactBlob)
}
report, err := registry.ImageEngine().ArtifactAdd(registry.Context(), artifactName, artifactBlobs, opts)
if err != nil {
return err
}
fmt.Println(report.ArtifactDigest.Encoded())
return nil
}

View File

@ -0,0 +1,21 @@
package artifact
import (
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/spf13/cobra"
)
// Command: podman _artifact_
var artifactCmd = &cobra.Command{
Use: "artifact",
Short: "Manage OCI artifacts",
Long: "Manage OCI artifacts",
RunE: validate.SubCommandExists,
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: artifactCmd,
})
}

View File

@ -0,0 +1,51 @@
package artifact
import (
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
)
var (
extractCmd = &cobra.Command{
Use: "extract [options] ARTIFACT PATH",
Short: "Extract an OCI artifact to a local path",
Long: "Extract the blobs of an OCI artifact to a local file or directory",
RunE: extract,
Args: cobra.ExactArgs(2),
ValidArgsFunction: common.AutocompleteArtifactAdd,
Example: `podman artifact Extract quay.io/myimage/myartifact:latest /tmp/foobar.txt
podman artifact Extract quay.io/myimage/myartifact:latest /home/paul/mydir`,
}
)
var (
extractOpts entities.ArtifactExtractOptions
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: extractCmd,
Parent: artifactCmd,
})
flags := extractCmd.Flags()
digestFlagName := "digest"
flags.StringVar(&extractOpts.Digest, digestFlagName, "", "Only extract blob with the given digest")
_ = extractCmd.RegisterFlagCompletionFunc(digestFlagName, completion.AutocompleteNone)
titleFlagName := "title"
flags.StringVar(&extractOpts.Title, titleFlagName, "", "Only extract blob with the given title")
_ = extractCmd.RegisterFlagCompletionFunc(titleFlagName, completion.AutocompleteNone)
}
func extract(cmd *cobra.Command, args []string) error {
err := registry.ImageEngine().ArtifactExtract(registry.Context(), args[0], args[1], extractOpts)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,50 @@
package artifact
import (
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/utils"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
)
var (
inspectCmd = &cobra.Command{
Use: "inspect [ARTIFACT...]",
Short: "Inspect an OCI artifact",
Long: "Provide details on an OCI artifact",
RunE: inspect,
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: common.AutocompleteArtifacts,
Example: `podman artifact inspect quay.io/myimage/myartifact:latest`,
}
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: inspectCmd,
Parent: artifactCmd,
})
// TODO When things firm up on inspect looks, we can do a format implementation
// flags := inspectCmd.Flags()
// formatFlagName := "format"
// flags.StringVar(&inspectFlag.format, formatFlagName, "", "Format volume output using JSON or a Go template")
// This is something we wanted to do but did not seem important enough for initial PR
// remoteFlagName := "remote"
// flags.BoolVar(&inspectFlag.remote, remoteFlagName, false, "Inspect the image on a container image registry")
// TODO When the inspect structure has been defined, we need to uncomment and redirect this. Reminder, this
// will also need to be reflected in the podman-artifact-inspect man page
// _ = inspectCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(&machine.InspectInfo{}))
}
func inspect(cmd *cobra.Command, args []string) error {
artifactOptions := entities.ArtifactInspectOptions{}
inspectData, err := registry.ImageEngine().ArtifactInspect(registry.Context(), args[0], artifactOptions)
if err != nil {
return err
}
return utils.PrintGenericJSON(inspectData)
}

142
cmd/podman/artifact/list.go Normal file
View File

@ -0,0 +1,142 @@
package artifact
import (
"fmt"
"os"
"github.com/containers/common/pkg/completion"
"github.com/containers/common/pkg/report"
"github.com/containers/image/v5/docker/reference"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/docker/go-units"
"github.com/spf13/cobra"
)
var (
listCmd = &cobra.Command{
Use: "ls [options]",
Aliases: []string{"list"},
Short: "List OCI artifacts",
Long: "List OCI artifacts in local store",
RunE: list,
Args: validate.NoArgs,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman artifact ls`,
}
listFlag = listFlagType{}
)
type listFlagType struct {
format string
noHeading bool
noTrunc bool
}
type artifactListOutput struct {
Digest string
Repository string
Size string
Tag string
}
var (
defaultArtifactListOutputFormat = "{{range .}}{{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.Size}}\n{{end -}}"
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: listCmd,
Parent: artifactCmd,
})
flags := listCmd.Flags()
formatFlagName := "format"
flags.StringVar(&listFlag.format, formatFlagName, defaultArtifactListOutputFormat, "Format volume output using JSON or a Go template")
_ = listCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(&artifactListOutput{}))
flags.BoolVarP(&listFlag.noHeading, "noheading", "n", false, "Do not print column headings")
flags.BoolVar(&listFlag.noTrunc, "no-trunc", false, "Do not truncate output")
}
func list(cmd *cobra.Command, _ []string) error {
reports, err := registry.ImageEngine().ArtifactList(registry.Context(), entities.ArtifactListOptions{})
if err != nil {
return err
}
return outputTemplate(cmd, reports)
}
func outputTemplate(cmd *cobra.Command, lrs []*entities.ArtifactListReport) error {
var err error
artifacts := make([]artifactListOutput, 0)
for _, lr := range lrs {
var (
tag string
)
artifactName, err := lr.Artifact.GetName()
if err != nil {
return err
}
repo, err := reference.Parse(artifactName)
if err != nil {
return err
}
named, ok := repo.(reference.Named)
if !ok {
return fmt.Errorf("%q is an invalid artifact name", artifactName)
}
if tagged, ok := named.(reference.Tagged); ok {
tag = tagged.Tag()
}
// Note: Right now we only support things that are single manifests
// We should certainly expand this support for things like arch, etc
// as we move on
artifactDigest, err := lr.Artifact.GetDigest()
if err != nil {
return err
}
artifactHash := artifactDigest.Encoded()[0:12]
// If the user does not want truncated hashes
if listFlag.noTrunc {
artifactHash = artifactDigest.Encoded()
}
artifacts = append(artifacts, artifactListOutput{
Digest: artifactHash,
Repository: named.Name(),
Size: units.HumanSize(float64(lr.Artifact.TotalSizeBytes())),
Tag: tag,
})
}
headers := report.Headers(artifactListOutput{}, map[string]string{
"REPOSITORY": "REPOSITORY",
"Tag": "TAG",
"Size": "SIZE",
"Digest": "DIGEST",
})
rpt := report.New(os.Stdout, cmd.Name())
defer rpt.Flush()
switch {
case cmd.Flag("format").Changed:
rpt, err = rpt.Parse(report.OriginUser, listFlag.format)
default:
rpt, err = rpt.Parse(report.OriginPodman, listFlag.format)
}
if err != nil {
return err
}
if rpt.RenderHeaders && !listFlag.noHeading {
if err := rpt.Execute(headers); err != nil {
return fmt.Errorf("failed to write report column headers: %w", err)
}
}
return rpt.Execute(artifacts)
}

139
cmd/podman/artifact/pull.go Normal file
View File

@ -0,0 +1,139 @@
package artifact
import (
"fmt"
"os"
"github.com/containers/buildah/pkg/cli"
"github.com/containers/common/pkg/auth"
"github.com/containers/common/pkg/completion"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/util"
"github.com/spf13/cobra"
)
// pullOptionsWrapper wraps entities.ImagePullOptions and prevents leaking
// CLI-only fields into the API types.
type pullOptionsWrapper struct {
entities.ArtifactPullOptions
TLSVerifyCLI bool // CLI only
CredentialsCLI string
DecryptionKeys []string
}
var (
pullOptions = pullOptionsWrapper{}
pullDescription = `Pulls an artifact from a registry and stores it locally.`
pullCmd = &cobra.Command{
Use: "pull [options] ARTIFACT",
Short: "Pull an OCI artifact",
Long: pullDescription,
RunE: artifactPull,
Args: cobra.ExactArgs(1),
ValidArgsFunction: common.AutocompleteArtifacts,
Example: `podman artifact pull quay.io/myimage/myartifact:latest`,
}
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: pullCmd,
Parent: artifactCmd,
})
pullFlags(pullCmd)
}
// pullFlags set the flags for the pull command.
func pullFlags(cmd *cobra.Command) {
flags := cmd.Flags()
credsFlagName := "creds"
flags.StringVar(&pullOptions.CredentialsCLI, credsFlagName, "", "`Credentials` (USERNAME:PASSWORD) to use for authenticating to a registry")
_ = cmd.RegisterFlagCompletionFunc(credsFlagName, completion.AutocompleteNone)
flags.BoolVarP(&pullOptions.Quiet, "quiet", "q", false, "Suppress output information when pulling images")
flags.BoolVar(&pullOptions.TLSVerifyCLI, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries")
authfileFlagName := "authfile"
flags.StringVar(&pullOptions.AuthFilePath, authfileFlagName, auth.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override")
_ = cmd.RegisterFlagCompletionFunc(authfileFlagName, completion.AutocompleteDefault)
decryptionKeysFlagName := "decryption-key"
flags.StringArrayVar(&pullOptions.DecryptionKeys, decryptionKeysFlagName, nil, "Key needed to decrypt the image (e.g. /path/to/key.pem)")
_ = cmd.RegisterFlagCompletionFunc(decryptionKeysFlagName, completion.AutocompleteDefault)
retryFlagName := "retry"
flags.Uint(retryFlagName, registry.RetryDefault(), "number of times to retry in case of failure when performing pull")
_ = cmd.RegisterFlagCompletionFunc(retryFlagName, completion.AutocompleteNone)
retryDelayFlagName := "retry-delay"
flags.String(retryDelayFlagName, registry.RetryDelayDefault(), "delay between retries in case of pull failures")
_ = cmd.RegisterFlagCompletionFunc(retryDelayFlagName, completion.AutocompleteNone)
if registry.IsRemote() {
_ = flags.MarkHidden(decryptionKeysFlagName)
} else {
certDirFlagName := "cert-dir"
flags.StringVar(&pullOptions.CertDirPath, certDirFlagName, "", "`Pathname` of a directory containing TLS certificates and keys")
_ = cmd.RegisterFlagCompletionFunc(certDirFlagName, completion.AutocompleteDefault)
}
}
func artifactPull(cmd *cobra.Command, args []string) error {
// TLS verification in c/image is controlled via a `types.OptionalBool`
// which allows for distinguishing among set-true, set-false, unspecified
// which is important to implement a sane way of dealing with defaults of
// boolean CLI flags.
if cmd.Flags().Changed("tls-verify") {
pullOptions.InsecureSkipTLSVerify = types.NewOptionalBool(!pullOptions.TLSVerifyCLI)
}
if cmd.Flags().Changed("retry") {
retry, err := cmd.Flags().GetUint("retry")
if err != nil {
return err
}
pullOptions.MaxRetries = &retry
}
if cmd.Flags().Changed("retry-delay") {
val, err := cmd.Flags().GetString("retry-delay")
if err != nil {
return err
}
pullOptions.RetryDelay = val
}
if cmd.Flags().Changed("authfile") {
if err := auth.CheckAuthFile(pullOptions.AuthFilePath); err != nil {
return err
}
}
if pullOptions.CredentialsCLI != "" {
creds, err := util.ParseRegistryCreds(pullOptions.CredentialsCLI)
if err != nil {
return err
}
pullOptions.Username = creds.Username
pullOptions.Password = creds.Password
}
decConfig, err := cli.DecryptConfig(pullOptions.DecryptionKeys)
if err != nil {
return fmt.Errorf("unable to obtain decryption config: %w", err)
}
pullOptions.OciDecryptConfig = decConfig
if !pullOptions.Quiet {
pullOptions.Writer = os.Stdout
}
_, err = registry.ImageEngine().ArtifactPull(registry.Context(), args[0], pullOptions.ArtifactPullOptions)
return err
}

228
cmd/podman/artifact/push.go Normal file
View File

@ -0,0 +1,228 @@
package artifact
import (
"fmt"
"os"
"github.com/containers/buildah/pkg/cli"
"github.com/containers/common/pkg/auth"
"github.com/containers/common/pkg/completion"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/util"
"github.com/spf13/cobra"
)
// pushOptionsWrapper wraps entities.ImagepushOptions and prevents leaking
// CLI-only fields into the API types.
type pushOptionsWrapper struct {
entities.ArtifactPushOptions
TLSVerifyCLI bool // CLI only
CredentialsCLI string
SignPassphraseFileCLI string
SignBySigstoreParamFileCLI string
EncryptionKeys []string
EncryptLayers []int
DigestFile string
}
var (
pushOptions = pushOptionsWrapper{}
pushDescription = `Push an OCI artifact from local storage to an image registry`
pushCmd = &cobra.Command{
Use: "push [options] ARTIFACT.",
Short: "Push an OCI artifact",
Long: pushDescription,
RunE: artifactPush,
Args: cobra.ExactArgs(1),
ValidArgsFunction: common.AutocompleteArtifacts,
Example: `podman artifact push quay.io/myimage/myartifact:latest`,
}
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: pushCmd,
Parent: artifactCmd,
})
pushFlags(pushCmd)
}
// pullFlags set the flags for the pull command.
func pushFlags(cmd *cobra.Command) {
flags := cmd.Flags()
// For now default All flag to true, for pushing of manifest lists
pushOptions.All = true
authfileFlagName := "authfile"
flags.StringVar(&pushOptions.Authfile, authfileFlagName, auth.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override")
_ = cmd.RegisterFlagCompletionFunc(authfileFlagName, completion.AutocompleteDefault)
certDirFlagName := "cert-dir"
flags.StringVar(&pushOptions.CertDir, certDirFlagName, "", "Path to a directory containing TLS certificates and keys")
_ = cmd.RegisterFlagCompletionFunc(certDirFlagName, completion.AutocompleteDefault)
// This is a flag I didn't wire up but could be considered
// flags.BoolVar(&pushOptions.Compress, "compress", false, "Compress tarball image layers when pushing to a directory using the 'dir' transport. (default is same compression type as source)")
credsFlagName := "creds"
flags.StringVar(&pushOptions.CredentialsCLI, credsFlagName, "", "`Credentials` (USERNAME:PASSWORD) to use for authenticating to a registry")
_ = cmd.RegisterFlagCompletionFunc(credsFlagName, completion.AutocompleteNone)
digestfileFlagName := "digestfile"
flags.StringVar(&pushOptions.DigestFile, digestfileFlagName, "", "Write the digest of the pushed image to the specified file")
_ = cmd.RegisterFlagCompletionFunc(digestfileFlagName, completion.AutocompleteDefault)
flags.BoolVarP(&pushOptions.Quiet, "quiet", "q", false, "Suppress output information when pushing images")
retryFlagName := "retry"
flags.Uint(retryFlagName, registry.RetryDefault(), "number of times to retry in case of failure when performing push")
_ = cmd.RegisterFlagCompletionFunc(retryFlagName, completion.AutocompleteNone)
retryDelayFlagName := "retry-delay"
flags.String(retryDelayFlagName, registry.RetryDelayDefault(), "delay between retries in case of push failures")
_ = cmd.RegisterFlagCompletionFunc(retryDelayFlagName, completion.AutocompleteNone)
signByFlagName := "sign-by"
flags.StringVar(&pushOptions.SignBy, signByFlagName, "", "Add a signature at the destination using the specified key")
_ = cmd.RegisterFlagCompletionFunc(signByFlagName, completion.AutocompleteNone)
signBySigstoreFlagName := "sign-by-sigstore"
flags.StringVar(&pushOptions.SignBySigstoreParamFileCLI, signBySigstoreFlagName, "", "Sign the image using a sigstore parameter file at `PATH`")
_ = cmd.RegisterFlagCompletionFunc(signBySigstoreFlagName, completion.AutocompleteDefault)
signBySigstorePrivateKeyFlagName := "sign-by-sigstore-private-key"
flags.StringVar(&pushOptions.SignBySigstorePrivateKeyFile, signBySigstorePrivateKeyFlagName, "", "Sign the image using a sigstore private key at `PATH`")
_ = cmd.RegisterFlagCompletionFunc(signBySigstorePrivateKeyFlagName, completion.AutocompleteDefault)
signPassphraseFileFlagName := "sign-passphrase-file"
flags.StringVar(&pushOptions.SignPassphraseFileCLI, signPassphraseFileFlagName, "", "Read a passphrase for signing an image from `PATH`")
_ = cmd.RegisterFlagCompletionFunc(signPassphraseFileFlagName, completion.AutocompleteDefault)
flags.BoolVar(&pushOptions.TLSVerifyCLI, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries")
// TODO I think these two can be removed?
/*
compFormat := "compression-format"
flags.StringVar(&pushOptions.CompressionFormat, compFormat, compressionFormat(), "compression format to use")
_ = cmd.RegisterFlagCompletionFunc(compFormat, common.AutocompleteCompressionFormat)
compLevel := "compression-level"
flags.Int(compLevel, compressionLevel(), "compression level to use")
_ = cmd.RegisterFlagCompletionFunc(compLevel, completion.AutocompleteNone)
*/
// Potential options that could be wired up if deemed necessary
// encryptionKeysFlagName := "encryption-key"
// flags.StringArrayVar(&pushOptions.EncryptionKeys, encryptionKeysFlagName, nil, "Key with the encryption protocol to use to encrypt the image (e.g. jwe:/path/to/key.pem)")
// _ = cmd.RegisterFlagCompletionFunc(encryptionKeysFlagName, completion.AutocompleteDefault)
// encryptLayersFlagName := "encrypt-layer"
// flags.IntSliceVar(&pushOptions.EncryptLayers, encryptLayersFlagName, nil, "Layers to encrypt, 0-indexed layer indices with support for negative indexing (e.g. 0 is the first layer, -1 is the last layer). If not defined, will encrypt all layers if encryption-key flag is specified")
// _ = cmd.RegisterFlagCompletionFunc(encryptLayersFlagName, completion.AutocompleteDefault)
if registry.IsRemote() {
_ = flags.MarkHidden("cert-dir")
_ = flags.MarkHidden("compress")
_ = flags.MarkHidden("quiet")
_ = flags.MarkHidden(signByFlagName)
_ = flags.MarkHidden(signBySigstoreFlagName)
_ = flags.MarkHidden(signBySigstorePrivateKeyFlagName)
_ = flags.MarkHidden(signPassphraseFileFlagName)
} else {
signaturePolicyFlagName := "signature-policy"
flags.StringVar(&pushOptions.SignaturePolicy, signaturePolicyFlagName, "", "Path to a signature-policy file")
_ = flags.MarkHidden(signaturePolicyFlagName)
}
}
func artifactPush(cmd *cobra.Command, args []string) error {
source := args[0]
// Should we just make destination == origin ?
// destination := args[len(args)-1]
// TLS verification in c/image is controlled via a `types.OptionalBool`
// which allows for distinguishing among set-true, set-false, unspecified
// which is important to implement a sane way of dealing with defaults of
// boolean CLI flags.
if cmd.Flags().Changed("tls-verify") {
pushOptions.SkipTLSVerify = types.NewOptionalBool(!pushOptions.TLSVerifyCLI)
}
if cmd.Flags().Changed("authfile") {
if err := auth.CheckAuthFile(pushOptions.Authfile); err != nil {
return err
}
}
if pushOptions.CredentialsCLI != "" {
creds, err := util.ParseRegistryCreds(pushOptions.CredentialsCLI)
if err != nil {
return err
}
pushOptions.Username = creds.Username
pushOptions.Password = creds.Password
}
if !pushOptions.Quiet {
pushOptions.Writer = os.Stderr
}
signingCleanup, err := common.PrepareSigning(&pushOptions.ImagePushOptions,
pushOptions.SignPassphraseFileCLI, pushOptions.SignBySigstoreParamFileCLI)
if err != nil {
return err
}
defer signingCleanup()
encConfig, encLayers, err := cli.EncryptConfig(pushOptions.EncryptionKeys, pushOptions.EncryptLayers)
if err != nil {
return fmt.Errorf("unable to obtain encryption config: %w", err)
}
pushOptions.OciEncryptConfig = encConfig
pushOptions.OciEncryptLayers = encLayers
if cmd.Flags().Changed("retry") {
retry, err := cmd.Flags().GetUint("retry")
if err != nil {
return err
}
pushOptions.Retry = &retry
}
if cmd.Flags().Changed("retry-delay") {
val, err := cmd.Flags().GetString("retry-delay")
if err != nil {
return err
}
pushOptions.RetryDelay = val
}
// TODO If not compression options are supported, we do not need the following
/*
if cmd.Flags().Changed("compression-level") {
val, err := cmd.Flags().GetInt("compression-level")
if err != nil {
return err
}
pushOptions.CompressionLevel = &val
}
if cmd.Flags().Changed("compression-format") {
if !cmd.Flags().Changed("force-compression") {
// If `compression-format` is set and no value for `--force-compression`
// is selected then defaults to `true`.
pushOptions.ForceCompressionFormat = true
}
}
*/
_, err = registry.ImageEngine().ArtifactPush(registry.Context(), source, pushOptions.ArtifactPushOptions)
return err
}

77
cmd/podman/artifact/rm.go Normal file
View File

@ -0,0 +1,77 @@
package artifact
import (
"errors"
"fmt"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
)
var (
rmCmd = &cobra.Command{
Use: "rm [options] ARTIFACT",
Short: "Remove an OCI artifact",
Long: "Remove an OCI artifact from local storage",
RunE: rm,
Aliases: []string{"remove"},
Args: func(cmd *cobra.Command, args []string) error { //nolint: gocritic
return checkAllAndArgs(cmd, args)
},
ValidArgsFunction: common.AutocompleteArtifacts,
Example: `podman artifact rm quay.io/myimage/myartifact:latest
podman artifact rm -a`,
}
rmOptions = entities.ArtifactRemoveOptions{}
)
func rmFlags(cmd *cobra.Command) {
flags := cmd.Flags()
flags.BoolVarP(&rmOptions.All, "all", "a", false, "Remove all artifacts")
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: rmCmd,
Parent: artifactCmd,
})
rmFlags(rmCmd)
}
func rm(cmd *cobra.Command, args []string) error {
var nameOrID string
if len(args) > 0 {
nameOrID = args[0]
}
artifactRemoveReport, err := registry.ImageEngine().ArtifactRm(registry.Context(), nameOrID, rmOptions)
if err != nil {
return err
}
for _, d := range artifactRemoveReport.ArtifactDigests {
fmt.Println(d.Encoded())
}
return nil
}
// checkAllAndArgs takes a cobra command and args and checks if
// all is used, then no args can be passed. note: this was created
// as an unexported local func for now and could be moved to pkg
// validate. if we add "--latest" to the command, then perhaps
// one of the existing plg validate funcs would be appropriate.
func checkAllAndArgs(c *cobra.Command, args []string) error {
all, _ := c.Flags().GetBool("all")
if all && len(args) > 0 {
return fmt.Errorf("when using the --all switch, you may not pass any artifact names or digests")
}
if !all {
if len(args) < 1 {
return errors.New("a single artifact name or digest must be specified")
}
if len(args) > 1 {
return errors.New("too many arguments: only accepts one artifact name or digest ")
}
}
return nil
}

149
cmd/podman/auto-update.go Normal file
View File

@ -0,0 +1,149 @@
package main
import (
"encoding/json"
"fmt"
"os"
"github.com/containers/common/pkg/auth"
"github.com/containers/common/pkg/completion"
"github.com/containers/common/pkg/report"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/errorhandling"
"github.com/spf13/cobra"
)
type cliAutoUpdateOptions struct {
entities.AutoUpdateOptions
format string
tlsVerify bool
}
var (
autoUpdateOptions = cliAutoUpdateOptions{}
autoUpdateDescription = `Auto update containers according to their auto-update policy.
Auto-update policies are specified with the "io.containers.autoupdate" label.
Containers are expected to run in systemd units created with "podman-generate-systemd --new",
or similar units that create new containers in order to run the updated images.
Please refer to the podman-auto-update(1) man page for details.`
autoUpdateCommand = &cobra.Command{
Annotations: map[string]string{registry.EngineMode: registry.ABIMode},
Use: "auto-update [options]",
Short: "Auto update containers according to their auto-update policy",
Long: autoUpdateDescription,
RunE: autoUpdate,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman auto-update
podman auto-update --authfile ~/authfile.json`,
}
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: autoUpdateCommand,
})
flags := autoUpdateCommand.Flags()
authfileFlagName := "authfile"
flags.StringVar(&autoUpdateOptions.Authfile, authfileFlagName, auth.GetDefaultAuthFile(), "Path to the authentication file. Use REGISTRY_AUTH_FILE environment variable to override")
_ = autoUpdateCommand.RegisterFlagCompletionFunc(authfileFlagName, completion.AutocompleteDefault)
flags.BoolVar(&autoUpdateOptions.DryRun, "dry-run", false, "Check for pending updates")
flags.BoolVar(&autoUpdateOptions.Rollback, "rollback", true, "Rollback to previous image if update fails")
flags.StringVar(&autoUpdateOptions.format, "format", "", "Change the output format to JSON or a Go template")
_ = autoUpdateCommand.RegisterFlagCompletionFunc("format", common.AutocompleteFormat(&autoUpdateOutput{}))
flags.BoolVarP(&autoUpdateOptions.tlsVerify, "tls-verify", "", true, "Require HTTPS and verify certificates when contacting registries")
}
func autoUpdate(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
// Backwards compat. System tests expect this error string.
return fmt.Errorf("`%s` takes no arguments", cmd.CommandPath())
}
if cmd.Flags().Changed("authfile") {
if err := auth.CheckAuthFile(autoUpdateOptions.Authfile); err != nil {
return err
}
}
if cmd.Flags().Changed("tls-verify") {
autoUpdateOptions.InsecureSkipTLSVerify = types.NewOptionalBool(!autoUpdateOptions.tlsVerify)
}
allReports, failures := registry.ContainerEngine().AutoUpdate(registry.Context(), autoUpdateOptions.AutoUpdateOptions)
if allReports == nil {
return errorhandling.JoinErrors(failures)
}
if err := writeTemplate(allReports, autoUpdateOptions.format); err != nil {
failures = append(failures, err)
}
return errorhandling.JoinErrors(failures)
}
type autoUpdateOutput struct {
Unit string
Container string
ContainerName string
ContainerID string
Image string
Policy string
Updated string
}
func reportsToOutput(allReports []*entities.AutoUpdateReport) []autoUpdateOutput {
output := make([]autoUpdateOutput, len(allReports))
for i, r := range allReports {
output[i] = autoUpdateOutput{
Unit: r.SystemdUnit,
Container: fmt.Sprintf("%s (%s)", r.ContainerID[:12], r.ContainerName),
ContainerName: r.ContainerName,
ContainerID: r.ContainerID,
Image: r.ImageName,
Policy: r.Policy,
Updated: r.Updated,
}
}
return output
}
func writeTemplate(allReports []*entities.AutoUpdateReport, inputFormat string) error {
rpt := report.New(os.Stdout, "auto-update")
defer rpt.Flush()
output := reportsToOutput(allReports)
var err error
switch inputFormat {
case "":
format := "{{range . }}\t{{.Unit}}\t{{.Container}}\t{{.Image}}\t{{.Policy}}\t{{.Updated}}\n{{end -}}"
rpt, err = rpt.Parse(report.OriginPodman, format)
case "json":
prettyJSON, err := json.MarshalIndent(output, "", " ")
if err != nil {
return err
}
fmt.Println(string(prettyJSON))
return nil
default:
rpt, err = rpt.Parse(report.OriginUser, inputFormat)
}
if err != nil {
return err
}
if rpt.RenderHeaders {
headers := report.Headers(autoUpdateOutput{}, nil)
if err := rpt.Execute(headers); err != nil {
return err
}
}
return rpt.Execute(output)
}

27
cmd/podman/client.go Normal file
View File

@ -0,0 +1,27 @@
package main
import "github.com/containers/podman/v5/libpod/define"
type clientInfo struct {
OSArch string `json:"OS"`
Provider string `json:"provider"`
Version string `json:"version"`
BuildOrigin string `json:"buildOrigin,omitempty" yaml:",omitempty"`
}
func getClientInfo() (*clientInfo, error) {
p, err := getProvider()
if err != nil {
return nil, err
}
vinfo, err := define.GetVersion()
if err != nil {
return nil, err
}
return &clientInfo{
OSArch: vinfo.OsArch,
Provider: p,
Version: vinfo.Version,
BuildOrigin: vinfo.BuildOrigin,
}, nil
}

View File

@ -0,0 +1,15 @@
//go:build amd64 || arm64
package main
import (
"github.com/containers/podman/v5/pkg/machine/provider"
)
func getProvider() (string, error) {
p, err := provider.Get()
if err != nil {
return "", err
}
return p.VMType().String(), nil
}

View File

@ -0,0 +1,7 @@
//go:build !amd64 && !arm64
package main
func getProvider() (string, error) {
return "", nil
}

689
cmd/podman/common/build.go Normal file
View File

@ -0,0 +1,689 @@
package common
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
buildahDefine "github.com/containers/buildah/define"
buildahCLI "github.com/containers/buildah/pkg/cli"
"github.com/containers/buildah/pkg/parse"
buildahUtil "github.com/containers/buildah/pkg/util"
"github.com/containers/common/pkg/auth"
"github.com/containers/common/pkg/completion"
"github.com/containers/common/pkg/config"
"github.com/containers/image/v5/docker/reference"
"github.com/containers/image/v5/types"
encconfig "github.com/containers/ocicrypt/config"
enchelpers "github.com/containers/ocicrypt/helpers"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/utils"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/env"
"github.com/openshift/imagebuilder"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// BuildFlagsWrapper are local to cmd/ as the build code is using Buildah-internal
// types. Hence, after parsing, we are converting buildFlagsWrapper to the entities'
// options which essentially embed the Buildah types.
type BuildFlagsWrapper struct {
// Buildah stuff first
buildahCLI.BudResults
buildahCLI.LayerResults
buildahCLI.FromAndBudResults
buildahCLI.NameSpaceResults
buildahCLI.UserNSResults
// SquashAll squashes all layers into a single layer.
SquashAll bool
// Cleanup removes built images from remote connections on success
Cleanup bool
}
// FarmBuildHiddenFlags are the flags hidden from the farm build command because they are either not
// supported or don't make sense in the farm build use case
var FarmBuildHiddenFlags = []string{"arch", "all-platforms", "compress", "cw", "disable-content-trust",
"logsplit", "manifest", "os", "output", "platform", "sign-by", "signature-policy", "stdin",
"variant"}
func DefineBuildFlags(cmd *cobra.Command, buildOpts *BuildFlagsWrapper, isFarmBuild bool) {
flags := cmd.Flags()
// buildx build --load ignored, but added for compliance
flags.Bool("load", false, "buildx --load")
_ = flags.MarkHidden("load")
// buildx build --progress ignored, but added for compliance
flags.String("progress", "auto", "buildx --progress")
_ = flags.MarkHidden("progress")
// Podman flags
flags.BoolVarP(&buildOpts.SquashAll, "squash-all", "", false, "Squash all layers into a single layer")
// Bud flags
budFlags := buildahCLI.GetBudFlags(&buildOpts.BudResults)
// --pull flag
flag := budFlags.Lookup("pull")
flag.DefValue = "missing"
if err := flag.Value.Set("missing"); err != nil {
logrus.Errorf("Unable to set --pull to 'missing': %v", err)
}
flag.Usage = `Pull image policy ("always"|"missing"|"never"|"newer")`
flags.AddFlagSet(&budFlags)
// Add the completion functions
budCompletions := buildahCLI.GetBudFlagsCompletions()
completion.CompleteCommandFlags(cmd, budCompletions)
// Layer flags
layerFlags := buildahCLI.GetLayerFlags(&buildOpts.LayerResults)
// --layers flag
flag = layerFlags.Lookup("layers")
useLayersVal := useLayers()
buildOpts.Layers = useLayersVal == "true"
if err := flag.Value.Set(useLayersVal); err != nil {
logrus.Errorf("Unable to set --layers to %v: %v", useLayersVal, err)
}
flag.DefValue = useLayersVal
// --force-rm flag
flag = layerFlags.Lookup("force-rm")
if err := flag.Value.Set("true"); err != nil {
logrus.Errorf("Unable to set --force-rm to true: %v", err)
}
flag.DefValue = "true"
flags.AddFlagSet(&layerFlags)
// FromAndBud flags
fromAndBudFlags, err := buildahCLI.GetFromAndBudFlags(&buildOpts.FromAndBudResults, &buildOpts.UserNSResults, &buildOpts.NameSpaceResults)
if err != nil {
logrus.Errorf("Setting up build flags: %v", err)
os.Exit(1)
}
flags.AddFlagSet(&fromAndBudFlags)
// Add the completion functions
fromAndBudFlagsCompletions := buildahCLI.GetFromAndBudFlagsCompletions()
completion.CompleteCommandFlags(cmd, fromAndBudFlagsCompletions)
flags.SetNormalizeFunc(buildahCLI.AliasFlags)
if registry.IsRemote() {
// Unset the isolation default as we never want to send this over the API
// as it can be wrong (root vs rootless).
_ = flags.Lookup("isolation").Value.Set("")
_ = flags.MarkHidden("disable-content-trust")
_ = flags.MarkHidden("sign-by")
_ = flags.MarkHidden("signature-policy")
_ = flags.MarkHidden("compress")
_ = flags.MarkHidden("output")
_ = flags.MarkHidden("logsplit")
_ = flags.MarkHidden("cw")
// Support for farm build in podman-remote
if !isFarmBuild {
_ = flags.MarkHidden("tls-verify")
}
}
if isFarmBuild {
for _, f := range FarmBuildHiddenFlags {
_ = flags.MarkHidden(f)
}
}
}
func ParseBuildOpts(cmd *cobra.Command, args []string, buildOpts *BuildFlagsWrapper) (*entities.BuildOptions, error) {
if cmd.Flags().Changed("squash-all") && cmd.Flags().Changed("squash") {
return nil, errors.New("cannot specify --squash-all with --squash")
}
if cmd.Flag("output").Changed && registry.IsRemote() {
return nil, errors.New("'--output' option is not supported in remote mode")
}
if buildOpts.Network == "none" {
if cmd.Flag("dns").Changed {
return nil, errors.New("the --dns option cannot be used with --network=none")
}
if cmd.Flag("dns-option").Changed {
return nil, errors.New("the --dns-option option cannot be used with --network=none")
}
if cmd.Flag("dns-search").Changed {
return nil, errors.New("the --dns-search option cannot be used with --network=none")
}
}
if cmd.Flag("network").Changed {
if buildOpts.Network != "host" && buildOpts.Isolation == buildahDefine.IsolationChroot.String() {
return nil, fmt.Errorf("cannot set --network other than host with --isolation %s", buildOpts.Isolation)
}
}
// Extract container files from the CLI (i.e., --file/-f) first.
var containerFiles []string
for _, f := range buildOpts.File {
if f == "-" {
if len(args) == 0 {
args = append(args, "-")
} else {
containerFiles = append(containerFiles, "/dev/stdin")
}
} else {
containerFiles = append(containerFiles, f)
}
}
// Determine context directory.
var (
contextDir string
apiBuildOpts entities.BuildOptions
)
if len(args) > 0 {
// The context directory could be a URL. Try to handle that.
tempDir, subDir, err := buildahDefine.TempDirForURL("", "buildah", args[0])
if err != nil {
return nil, fmt.Errorf("prepping temporary context directory: %w", err)
}
if tempDir != "" {
apiBuildOpts.TmpDirToClose = tempDir
contextDir = filepath.Join(tempDir, subDir)
} else {
// Nope, it was local. Use it as is.
absDir, err := filepath.Abs(args[0])
if err != nil {
return nil, fmt.Errorf("determining path to directory %q: %w", args[0], err)
}
contextDir = absDir
}
} else {
// No context directory or URL was specified. Try to use the home of
// the first locally-available Containerfile.
for i := range containerFiles {
if isURL(containerFiles[i]) {
continue
}
absFile, err := filepath.Abs(containerFiles[i])
if err != nil {
return nil, fmt.Errorf("determining path to file %q: %w", containerFiles[i], err)
}
contextDir = filepath.Dir(absFile)
containerFiles[i] = absFile
break
}
}
if contextDir == "" {
return nil, errors.New("no context directory and no Containerfile specified")
}
if !utils.IsDir(contextDir) {
return nil, fmt.Errorf("context must be a directory: %q", contextDir)
}
if len(containerFiles) == 0 {
switch {
case utils.FileExists(filepath.Join(contextDir, "Containerfile")):
if utils.IsDir(filepath.Join(contextDir, "Containerfile")) {
return nil, fmt.Errorf("containerfile: cannot be path or directory")
}
containerFiles = append(containerFiles, filepath.Join(contextDir, "Containerfile"))
case utils.FileExists(filepath.Join(contextDir, "Dockerfile")):
if utils.IsDir(filepath.Join(contextDir, "Dockerfile")) {
return nil, fmt.Errorf("dockerfile: cannot be path or directory")
}
containerFiles = append(containerFiles, filepath.Join(contextDir, "Dockerfile"))
default:
return nil, fmt.Errorf("no Containerfile or Dockerfile specified or found in context directory, %s: %w", contextDir, syscall.ENOENT)
}
}
if err := areContainerfilesValid(contextDir, containerFiles); err != nil {
return nil, err
}
var logFile *os.File
if cmd.Flag("logfile").Changed {
var err error
logFile, err = os.OpenFile(buildOpts.Logfile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
if err != nil {
return nil, err
}
apiBuildOpts.LogFileToClose = logFile
}
buildahDefineOpts, err := buildFlagsWrapperToOptions(cmd, contextDir, buildOpts, logFile, buildOpts.Layers, buildOpts.Squash)
if err != nil {
return nil, err
}
apiBuildOpts.BuildOptions = *buildahDefineOpts
apiBuildOpts.ContainerFiles = containerFiles
apiBuildOpts.Authfile = buildOpts.Authfile
return &apiBuildOpts, err
}
// buildFlagsWrapperToOptions converts the local build flags to the build options used
// in the API which embed Buildah types used across the build code. Doing the
// conversion here prevents the API from doing that (redundantly).
//
// TODO: this code should really be in Buildah.
func buildFlagsWrapperToOptions(c *cobra.Command, contextDir string, flags *BuildFlagsWrapper, logfile *os.File, layers, squash bool) (*buildahDefine.BuildOptions, error) {
output := ""
tags := []string{}
if c.Flag("tag").Changed {
tags = flags.Tag
if len(tags) > 0 {
output = tags[0]
tags = tags[1:]
}
}
if c.Flags().Changed("authfile") {
if err := auth.CheckAuthFile(flags.Authfile); err != nil {
return nil, err
}
}
commonOpts, err := parse.CommonBuildOptions(c)
if err != nil {
return nil, err
}
pullFlagsCount := 0
if c.Flag("pull").Changed {
pullFlagsCount++
}
if c.Flag("pull-always").Changed {
pullFlagsCount++
}
if c.Flag("pull-never").Changed {
pullFlagsCount++
}
if pullFlagsCount > 1 {
return nil, errors.New("can only set one of 'pull' or 'pull-always' or 'pull-never'")
}
// Allow for --pull, --pull=true, --pull=false, --pull=never, --pull=always
// --pull-always and --pull-never. The --pull-never and --pull-always options
// will not be documented.
pullPolicy := buildahDefine.PullIfMissing
if c.Flags().Changed("pull") && strings.EqualFold(strings.TrimSpace(flags.Pull), "true") {
pullPolicy = buildahDefine.PullAlways
}
if flags.PullAlways || strings.EqualFold(strings.TrimSpace(flags.Pull), "always") {
pullPolicy = buildahDefine.PullAlways
}
if flags.PullNever ||
strings.EqualFold(strings.TrimSpace(flags.Pull), "false") ||
strings.EqualFold(strings.TrimSpace(flags.Pull), "never") {
pullPolicy = buildahDefine.PullNever
}
var cleanTmpFile bool
flags.Authfile, cleanTmpFile = buildahUtil.MirrorToTempFileIfPathIsDescriptor(flags.Authfile)
if cleanTmpFile {
defer os.Remove(flags.Authfile)
}
args := make(map[string]string)
if c.Flag("build-arg-file").Changed {
for _, argfile := range flags.BuildArgFile {
fargs, err := env.ParseFile(argfile)
if err != nil {
return nil, err
}
for name, val := range fargs {
args[name] = val
}
}
}
if c.Flag("build-arg").Changed {
for _, arg := range flags.BuildArg {
key, val, hasVal := strings.Cut(arg, "=")
if hasVal {
args[key] = val
} else {
// check if the env is set in the local environment and use that value if it is
if val, present := os.LookupEnv(key); present {
args[key] = val
} else {
delete(args, key)
}
}
}
}
flags.Layers = layers
// `buildah bud --layers=false` acts like `docker build --squash` does.
// That is all of the new layers created during the build process are
// condensed into one, any layers present prior to this build are
// retained without condensing. `buildah bud --squash` squashes both
// new and old layers down into one. Translate Podman commands into
// Buildah. Squash invoked, retain old layers, squash new layers into
// one.
if c.Flags().Changed("squash") && squash {
flags.Squash = false
flags.Layers = false
}
// Squash-all invoked, squash both new and old layers into one.
if c.Flags().Changed("squash-all") {
flags.Squash = true
if !c.Flags().Changed("layers") {
// Buildah supports using layers and --squash together
// after https://github.com/containers/buildah/pull/3674
// so podman must honor if user wants to still use layers
// with --squash-all.
flags.Layers = false
}
}
var stdin io.Reader
if flags.Stdin {
stdin = os.Stdin
}
var stdout, stderr, reporter *os.File
stdout = os.Stdout
stderr = os.Stderr
reporter = os.Stderr
if logfile != nil {
logrus.SetOutput(logfile)
stdout = logfile
stderr = logfile
reporter = logfile
}
nsValues, networkPolicy, err := parse.NamespaceOptions(c)
if err != nil {
return nil, err
}
compression := buildahDefine.Gzip
if flags.DisableCompression {
compression = buildahDefine.Uncompressed
}
isolation := buildahDefine.IsolationDefault
// Only parse the isolation when it is actually needed as we do not want to send a wrong default
// to the server in the remote case (root vs rootless).
if flags.Isolation != "" {
isolation, err = parse.IsolationOption(flags.Isolation)
if err != nil {
return nil, err
}
}
usernsOption, idmappingOptions, err := parse.IDMappingOptions(c, isolation)
if err != nil {
return nil, err
}
nsValues = append(nsValues, usernsOption...)
systemContext, err := parse.SystemContextFromOptions(c)
if err != nil {
return nil, err
}
var format string
flags.Format = strings.ToLower(flags.Format)
switch {
case strings.HasPrefix(flags.Format, buildahDefine.OCI):
format = buildahDefine.OCIv1ImageManifest
case strings.HasPrefix(flags.Format, buildahDefine.DOCKER):
format = buildahDefine.Dockerv2ImageManifest
default:
return nil, fmt.Errorf("unrecognized image type %q", flags.Format)
}
runtimeFlags := []string{}
for _, arg := range flags.RuntimeFlags {
runtimeFlags = append(runtimeFlags, "--"+arg)
}
podmanConfig := registry.PodmanConfig()
for _, arg := range podmanConfig.RuntimeFlags {
runtimeFlags = append(runtimeFlags, "--"+arg)
}
if podmanConfig.ContainersConf.Engine.CgroupManager == config.SystemdCgroupsManager {
runtimeFlags = append(runtimeFlags, "--systemd-cgroup")
}
platforms, err := parse.PlatformsFromOptions(c)
if err != nil {
return nil, err
}
decConfig, err := getDecryptConfig(flags.DecryptionKeys)
if err != nil {
return nil, fmt.Errorf("unable to obtain decrypt config: %w", err)
}
additionalBuildContext := make(map[string]*buildahDefine.AdditionalBuildContext)
if c.Flag("build-context").Changed {
for _, contextString := range flags.BuildContext {
key, val, hasVal := strings.Cut(contextString, "=")
if hasVal {
parseAdditionalBuildContext, err := parse.GetAdditionalBuildContext(val)
if err != nil {
return nil, fmt.Errorf("while parsing additional build context: %w", err)
}
additionalBuildContext[key] = &parseAdditionalBuildContext
} else {
return nil, fmt.Errorf("while parsing additional build context: %s, accepts value in the form of key=value", contextString)
}
}
}
var cacheTo []reference.Named
var cacheFrom []reference.Named
if c.Flag("cache-to").Changed {
cacheTo, err = parse.RepoNamesToNamedReferences(flags.CacheTo)
if err != nil {
return nil, fmt.Errorf("unable to parse value provided `%s` to --cache-to: %w", flags.CacheTo, err)
}
}
if c.Flag("cache-from").Changed {
cacheFrom, err = parse.RepoNamesToNamedReferences(flags.CacheFrom)
if err != nil {
return nil, fmt.Errorf("unable to parse value provided `%s` to --cache-from: %w", flags.CacheTo, err)
}
}
var cacheTTL time.Duration
if c.Flag("cache-ttl").Changed {
cacheTTL, err = time.ParseDuration(flags.CacheTTL)
if err != nil {
return nil, fmt.Errorf("unable to parse value provided %q as --cache-ttl: %w", flags.CacheTTL, err)
}
}
var confidentialWorkloadOptions buildahDefine.ConfidentialWorkloadOptions
if c.Flag("cw").Changed {
confidentialWorkloadOptions, err = parse.GetConfidentialWorkloadOptions(flags.CWOptions)
if err != nil {
return nil, err
}
}
retryDelay := 2 * time.Second
if flags.RetryDelay != "" {
retryDelay, err = time.ParseDuration(flags.RetryDelay)
if err != nil {
return nil, fmt.Errorf("unable to parse value provided %q as --retry-delay: %w", flags.RetryDelay, err)
}
}
opts := buildahDefine.BuildOptions{
AddCapabilities: flags.CapAdd,
AdditionalTags: tags,
AdditionalBuildContexts: additionalBuildContext,
AllPlatforms: flags.AllPlatforms,
Annotations: flags.Annotation,
Args: args,
BlobDirectory: flags.BlobCache,
BuildOutputs: flags.BuildOutputs,
CacheFrom: cacheFrom,
CacheTo: cacheTo,
CacheTTL: cacheTTL,
ConfidentialWorkload: confidentialWorkloadOptions,
CommonBuildOpts: commonOpts,
Compression: compression,
ConfigureNetwork: networkPolicy,
ContextDirectory: contextDir,
CPPFlags: flags.CPPFlags,
DefaultMountsFilePath: podmanConfig.ContainersConfDefaultsRO.Containers.DefaultMountsFile,
Devices: flags.Devices,
DropCapabilities: flags.CapDrop,
Envs: buildahCLI.LookupEnvVarReferences(flags.Envs, os.Environ()),
Err: stderr,
ForceRmIntermediateCtrs: flags.ForceRm,
From: flags.From,
GroupAdd: flags.GroupAdd,
IDMappingOptions: idmappingOptions,
In: stdin,
Isolation: isolation,
Jobs: &flags.Jobs,
Labels: flags.Label,
LayerLabels: flags.LayerLabel,
Layers: flags.Layers,
LogRusage: flags.LogRusage,
LogFile: flags.Logfile,
LogSplitByPlatform: flags.LogSplitByPlatform,
Manifest: flags.Manifest,
MaxPullPushRetries: flags.Retry,
NamespaceOptions: nsValues,
NoCache: flags.NoCache,
OSFeatures: flags.OSFeatures,
OSVersion: flags.OSVersion,
OciDecryptConfig: decConfig,
Out: stdout,
Output: output,
OutputFormat: format,
Platforms: platforms,
PullPolicy: pullPolicy,
PullPushRetryDelay: retryDelay,
Quiet: flags.Quiet,
RemoveIntermediateCtrs: flags.Rm,
ReportWriter: reporter,
RewriteTimestamp: flags.RewriteTimestamp,
Runtime: podmanConfig.RuntimePath,
RuntimeArgs: runtimeFlags,
RusageLogFile: flags.RusageLogFile,
SignBy: flags.SignBy,
SignaturePolicyPath: flags.SignaturePolicy,
Squash: flags.Squash,
SystemContext: systemContext,
Target: flags.Target,
TransientMounts: flags.Volumes,
UnsetEnvs: flags.UnsetEnvs,
UnsetLabels: flags.UnsetLabels,
UnsetAnnotations: flags.UnsetAnnotations,
}
if c.Flag("created-annotation").Changed {
opts.CreatedAnnotation = types.NewOptionalBool(flags.CreatedAnnotation)
}
if c.Flag("compat-volumes").Changed {
opts.CompatVolumes = types.NewOptionalBool(flags.CompatVolumes)
}
if c.Flag("inherit-labels").Changed {
opts.InheritLabels = types.NewOptionalBool(flags.InheritLabels)
}
if c.Flag("inherit-annotations").Changed {
opts.InheritAnnotations = types.NewOptionalBool(flags.InheritAnnotations)
}
if flags.IgnoreFile != "" {
excludes, err := imagebuilder.ParseIgnore(flags.IgnoreFile)
if err != nil {
return nil, fmt.Errorf("unable to parse ignore file: %w", err)
}
opts.Excludes = excludes
}
if flags.SourceDateEpoch != "" { // could be explicitly specified, or passed via the environment, tricking .Changed()
sde, err := strconv.ParseInt(flags.SourceDateEpoch, 10, 64)
if err != nil {
return nil, fmt.Errorf("parsing source-date-epoch value %q: %w", flags.SourceDateEpoch, err)
}
sourceDateEpoch := time.Unix(sde, 0).UTC()
opts.SourceDateEpoch = &sourceDateEpoch
}
if c.Flag("timestamp").Changed {
timestamp := time.Unix(flags.Timestamp, 0).UTC()
opts.Timestamp = &timestamp
}
if c.Flag("skip-unused-stages").Changed {
opts.SkipUnusedStages = types.NewOptionalBool(flags.SkipUnusedStages)
}
return &opts, nil
}
// useLayers returns false if BUILDAH_LAYERS is set to "0" or "false"
// otherwise it returns true
func useLayers() string {
layers := os.Getenv("BUILDAH_LAYERS")
if strings.ToLower(layers) == "false" || layers == "0" {
return "false"
}
return "true"
}
func getDecryptConfig(decryptionKeys []string) (*encconfig.DecryptConfig, error) {
decConfig := &encconfig.DecryptConfig{}
if len(decryptionKeys) > 0 {
// decryption
dcc, err := enchelpers.CreateCryptoConfig([]string{}, decryptionKeys)
if err != nil {
return nil, fmt.Errorf("invalid decryption keys: %w", err)
}
cc := encconfig.CombineCryptoConfigs([]encconfig.CryptoConfig{dcc})
decConfig = cc.DecryptConfig
}
return decConfig, nil
}
func areContainerfilesValid(contextDir string, containerFiles []string) error {
for _, f := range containerFiles {
if isURL(f) || f == "/dev/stdin" {
continue
}
// Because currently podman runs the test/bud.bats tests under the buildah project in CI,
// the following error messages need to be consistent with buildah; otherwise, the podman CI will fail.
// See: https://github.com/containers/buildah/blob/4c781b59b49d66e07324566555339888113eb7e2/imagebuildah/build.go#L139-L141
// https://github.com/containers/buildah/blob/4c781b59b49d66e07324566555339888113eb7e2/tests/bud.bats#L3474-L3479
if utils.IsDir(f) {
return fmt.Errorf("containerfile: %q cannot be path to a directory", f)
}
// If the file is not found, try again with context directory prepended (if not prepended yet)
// Ref: https://github.com/containers/buildah/blob/4c781b59b49d66e07324566555339888113eb7e2/imagebuildah/build.go#L125-L135
if utils.FileExists(f) {
continue
}
if !strings.HasPrefix(f, contextDir) {
if utils.FileExists(filepath.Join(contextDir, f)) {
continue
}
}
return fmt.Errorf("the specified Containerfile or Dockerfile does not exist, %s: %w", f, syscall.ENOENT)
}
return nil
}
func isURL(s string) bool {
return strings.HasPrefix(s, "http://") ||
strings.HasPrefix(s, "https://") ||
strings.HasPrefix(s, "git://") ||
strings.HasPrefix(s, "github.com/")
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,205 @@
package common_test
import (
"testing"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)
//nolint:recvcheck // We like to test mixed pointer receiver and non-pointer receiver
type Car struct {
Brand string
Stats struct {
HP *int
Displacement int
}
Extras map[string]Extra
// also ensure it will work with pointers
Extras2 map[string]*Extra
}
type Extra struct {
Name1 string
Name2 string
}
type Anonymous struct {
Hello string
// The name should match the testStruct Name below. This is used to make
// sure the logic uses the actual struct fields before the embedded ones.
Name struct {
Suffix string
Prefix string
}
}
// The name should match the testStruct Age name below.
func (a Anonymous) Age() int {
return 0
}
func (c Car) Type() string {
return ""
}
// Note: It is important that this function is *Car and the Type one is just Car.
// The reflect logic behaves differently for these cases so we have to test both.
func (c *Car) Color() string {
return ""
}
// This is for reflect testing required.
//
//nolint:unused
func (c Car) internal() int {
return 0
}
func (c Car) TwoOut() (string, string) {
return "", ""
}
func (c Car) Struct() Car {
return Car{}
}
func TestAutocompleteFormat(t *testing.T) {
testStruct := struct {
Name string
Age int
Car *Car
Car2 *Car
*Anonymous
private int
}{}
testStruct.Car = &Car{}
tests := []struct {
name string
toComplete string
expected []string
}{
{
"empty completion",
"",
[]string{"json"},
},
{
"json completion",
"json",
[]string{"json"},
},
{
"invalid completion",
"blahblah",
nil,
},
{
"invalid completion",
"{{",
nil,
},
{
"invalid completion",
"{{ ",
nil,
},
{
"invalid completion",
"{{ ..",
[]string{},
},
{
"fist level struct field name",
"{{.",
[]string{"{{.Name}}", "{{.Age}}", "{{.Car.", "{{.Car2.", "{{.Anonymous.", "{{.Hello}}"},
},
{
"fist level struct field name",
"{{ .",
[]string{"{{ .Name}}", "{{ .Age}}", "{{ .Car.", "{{ .Car2.", "{{ .Anonymous.", "{{ .Hello}}"},
},
{
"fist level struct field name",
"{{ .N",
[]string{"{{ .Name}}"},
},
{
"second level struct field name",
"{{ .Car.",
[]string{"{{ .Car.Color}}", "{{ .Car.Struct.", "{{ .Car.Type}}", "{{ .Car.Brand}}", "{{ .Car.Stats.", "{{ .Car.Extras.", "{{ .Car.Extras2."},
},
{
"second level struct field name",
"{{ .Car.B",
[]string{"{{ .Car.Brand}}"},
},
{
"second level nil struct field name",
"{{ .Car2.",
[]string{"{{ .Car2.Color}}", "{{ .Car2.Struct.", "{{ .Car2.Type}}", "{{ .Car2.Brand}}", "{{ .Car2.Stats.", "{{ .Car2.Extras.", "{{ .Car2.Extras2."},
},
{
"three level struct field name",
"{{ .Car.Stats.",
[]string{"{{ .Car.Stats.HP}}", "{{ .Car.Stats.Displacement}}"},
},
{
"three level struct field name",
"{{ .Car.Stats.D",
[]string{"{{ .Car.Stats.Displacement}}"},
},
{
"second level struct field name",
"{{ .Car.B",
[]string{"{{ .Car.Brand}}"},
},
{
"invalid field name",
"{{ .Ca.B",
[]string{},
},
{
"map key names don't work",
"{{ .Car.Extras.",
[]string{},
},
{
"map values work",
"{{ .Car.Extras.somekey.",
[]string{"{{ .Car.Extras.somekey.Name1}}", "{{ .Car.Extras.somekey.Name2}}"},
},
{
"map values work with ptr",
"{{ .Car.Extras2.somekey.",
[]string{"{{ .Car.Extras2.somekey.Name1}}", "{{ .Car.Extras2.somekey.Name2}}"},
},
{
"two variables struct field name",
"{{ .Car.Brand }} {{ .Car.",
[]string{"{{ .Car.Brand }} {{ .Car.Color}}", "{{ .Car.Brand }} {{ .Car.Struct.", "{{ .Car.Brand }} {{ .Car.Type}}",
"{{ .Car.Brand }} {{ .Car.Brand}}", "{{ .Car.Brand }} {{ .Car.Stats.", "{{ .Car.Brand }} {{ .Car.Extras.",
"{{ .Car.Brand }} {{ .Car.Extras2."},
},
{
"only dot without variable",
".",
nil,
},
{
"access embedded nil struct field",
"{{.Hello.",
[]string{},
},
}
for _, test := range tests {
completion, directive := common.AutocompleteFormat(&testStruct)(nil, nil, test.toComplete)
// directive should always be greater than ShellCompDirectiveNoFileComp
assert.GreaterOrEqual(t, directive, cobra.ShellCompDirectiveNoFileComp, "unexpected ShellCompDirective")
assert.Equal(t, test.expected, completion, test.name)
}
}

1099
cmd/podman/common/create.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,91 @@
package common
import (
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/libpod/define"
"github.com/containers/podman/v5/pkg/domain/entities"
)
func ulimits() []string {
if !registry.IsRemote() {
return podmanConfig.ContainersConfDefaultsRO.Ulimits()
}
return nil
}
func cgroupConfig() string {
if !registry.IsRemote() {
return podmanConfig.ContainersConfDefaultsRO.Cgroups()
}
return ""
}
func devices() []string {
if !registry.IsRemote() {
return podmanConfig.ContainersConfDefaultsRO.Devices()
}
return nil
}
func Env() []string {
if !registry.IsRemote() {
return podmanConfig.ContainersConfDefaultsRO.Env()
}
return nil
}
func pidsLimit() int64 {
if !registry.IsRemote() {
return podmanConfig.ContainersConfDefaultsRO.PidsLimit()
}
return -1
}
func policy() string {
if !registry.IsRemote() {
return podmanConfig.ContainersConfDefaultsRO.Engine.PullPolicy
}
return ""
}
func shmSize() string {
if !registry.IsRemote() {
return podmanConfig.ContainersConfDefaultsRO.ShmSize()
}
return ""
}
func volumes() []string {
if !registry.IsRemote() {
return podmanConfig.ContainersConfDefaultsRO.Volumes()
}
return nil
}
func LogDriver() string {
if !registry.IsRemote() {
return podmanConfig.ContainersConfDefaultsRO.Containers.LogDriver
}
return ""
}
// DefineCreateDefaults is used to initialize ctr create options before flag initialization
func DefineCreateDefaults(opts *entities.ContainerCreateOptions) {
opts.LogDriver = LogDriver()
opts.CgroupsMode = cgroupConfig()
opts.MemorySwappiness = -1
opts.ImageVolume = podmanConfig.ContainersConfDefaultsRO.Engine.ImageVolumeMode
opts.Pull = policy()
opts.ReadWriteTmpFS = true
opts.SdNotifyMode = define.SdNotifyModeContainer
opts.StopTimeout = podmanConfig.ContainersConfDefaultsRO.Engine.StopTimeout
opts.Systemd = "true"
opts.Timezone = podmanConfig.ContainersConfDefaultsRO.TZ()
opts.Umask = podmanConfig.ContainersConfDefaultsRO.Umask()
opts.Ulimit = ulimits()
opts.SeccompPolicy = "default"
opts.Volume = volumes()
opts.HealthLogDestination = define.DefaultHealthCheckLocalDestination
opts.HealthMaxLogCount = define.DefaultHealthMaxLogCount
opts.HealthMaxLogSize = define.DefaultHealthMaxLogSize
}

View File

@ -0,0 +1,53 @@
package common_test
import (
"reflect"
"strings"
"testing"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/stretchr/testify/assert"
)
func TestPodOptions(t *testing.T) {
entry := "/test1"
exampleOptions := entities.ContainerCreateOptions{CPUS: 5.5, CPUSetCPUs: "0-4", Entrypoint: &entry, Hostname: "foo", Name: "testing123", Volume: []string{"/fakeVol1", "/fakeVol2"}, Net: &entities.NetOptions{DNSSearch: []string{"search"}}, PID: "ns:/proc/self/ns"}
podOptions := entities.PodCreateOptions{}
err := common.ContainerToPodOptions(&exampleOptions, &podOptions)
assert.NoError(t, err)
cc := reflect.ValueOf(&exampleOptions).Elem()
pc := reflect.ValueOf(&podOptions).Elem()
pcType := reflect.TypeOf(podOptions)
for i := 0; i < pc.NumField(); i++ {
podField := pc.FieldByIndex([]int{i})
podType := pcType.Field(i)
for j := 0; j < cc.NumField(); j++ {
containerField := cc.FieldByIndex([]int{j})
containerType := reflect.TypeOf(exampleOptions).Field(j)
tagPod := strings.Split(podType.Tag.Get("json"), ",")[0]
tagContainer := strings.Split(containerType.Tag.Get("json"), ",")[0]
if tagPod == tagContainer && (tagPod != "" && tagContainer != "") {
areEqual := true
if containerField.Kind() == podField.Kind() {
switch containerField.Kind() {
case reflect.Slice:
for i, w := range containerField.Interface().([]string) {
areEqual = podField.Interface().([]string)[i] == w
}
case reflect.String:
areEqual = podField.String() == containerField.String()
case reflect.Bool:
areEqual = podField.Bool() == containerField.Bool()
case reflect.Ptr:
areEqual = reflect.DeepEqual(podField.Elem().Interface(), containerField.Elem().Interface())
}
}
assert.True(t, areEqual)
}
}
}
}

View File

@ -0,0 +1,10 @@
package common
import (
"github.com/containers/podman/v5/cmd/podman/registry"
)
var (
// Pull in configured json library
json = registry.JSONLibrary()
)

View File

@ -0,0 +1,16 @@
package common
const (
// AllType can be of type ImageType or ContainerType.
AllType = "all"
// ContainerType is the container type.
ContainerType = "container"
// ImageType is the image type.
ImageType = "image"
// NetworkType is the network type
NetworkType = "network"
// PodType is the pod type.
PodType = "pod"
// VolumeType is the volume type
VolumeType = "volume"
)

View File

@ -0,0 +1,309 @@
package common
import (
"errors"
"fmt"
"net"
"github.com/containers/common/libnetwork/types"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/parse"
"github.com/containers/podman/v5/libpod/define"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/specgen"
"github.com/containers/podman/v5/pkg/specgenutil"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
func DefineNetFlags(cmd *cobra.Command) {
netFlags := cmd.Flags()
addHostFlagName := "add-host"
netFlags.StringSlice(
addHostFlagName, []string{},
"Add a custom host-to-IP mapping (host:ip) (default [])",
)
_ = cmd.RegisterFlagCompletionFunc(addHostFlagName, completion.AutocompleteNone)
hostsFileFlagName := "hosts-file"
netFlags.String(
hostsFileFlagName, "",
`Base file to create the /etc/hosts file inside the container, or one of the special values. ("image"|"none")`,
)
_ = cmd.RegisterFlagCompletionFunc(hostsFileFlagName, AutocompleteHostsFile)
dnsFlagName := "dns"
netFlags.StringSlice(
dnsFlagName, podmanConfig.ContainersConf.DNSServers(),
"Set custom DNS servers",
)
_ = cmd.RegisterFlagCompletionFunc(dnsFlagName, completion.AutocompleteNone)
dnsOptFlagName := "dns-option"
netFlags.StringSlice(
dnsOptFlagName, podmanConfig.ContainersConf.DNSOptions(),
"Set custom DNS options",
)
_ = cmd.RegisterFlagCompletionFunc(dnsOptFlagName, completion.AutocompleteNone)
dnsSearchFlagName := "dns-search"
netFlags.StringSlice(
dnsSearchFlagName, podmanConfig.ContainersConf.DNSSearches(),
"Set custom DNS search domains",
)
_ = cmd.RegisterFlagCompletionFunc(dnsSearchFlagName, completion.AutocompleteNone)
ipFlagName := "ip"
netFlags.String(
ipFlagName, "",
"Specify a static IPv4 address for the container",
)
_ = cmd.RegisterFlagCompletionFunc(ipFlagName, completion.AutocompleteNone)
ip6FlagName := "ip6"
netFlags.String(
ip6FlagName, "",
"Specify a static IPv6 address for the container",
)
_ = cmd.RegisterFlagCompletionFunc(ip6FlagName, completion.AutocompleteNone)
macAddressFlagName := "mac-address"
netFlags.String(
macAddressFlagName, "",
"Container MAC address (e.g. 92:d0:c6:0a:29:33)",
)
_ = cmd.RegisterFlagCompletionFunc(macAddressFlagName, completion.AutocompleteNone)
networkFlagName := "network"
netFlags.StringArray(
networkFlagName, nil,
"Connect a container to a network",
)
_ = cmd.RegisterFlagCompletionFunc(networkFlagName, AutocompleteNetworkFlag)
networkAliasFlagName := "network-alias"
netFlags.StringSlice(
networkAliasFlagName, []string{},
"Add network-scoped alias for the container",
)
_ = cmd.RegisterFlagCompletionFunc(networkAliasFlagName, completion.AutocompleteNone)
publishFlagName := "publish"
netFlags.StringSliceP(
publishFlagName, "p", []string{},
"Publish a container's port, or a range of ports, to the host (default [])",
)
_ = cmd.RegisterFlagCompletionFunc(publishFlagName, completion.AutocompleteNone)
netFlags.Bool(
"no-hostname", false, "Do not create /etc/hostname within the container, instead use the version from the image",
)
netFlags.Bool(
"no-hosts", podmanConfig.ContainersConfDefaultsRO.Containers.NoHosts,
"Do not create /etc/hosts within the container, instead use the version from the image",
)
}
// NetFlagsToNetOptions parses the network flags for the given cmd.
func NetFlagsToNetOptions(opts *entities.NetOptions, flags pflag.FlagSet) (*entities.NetOptions, error) {
var (
err error
)
if opts == nil {
opts = &entities.NetOptions{}
}
if flags.Changed("add-host") {
opts.AddHosts, err = flags.GetStringSlice("add-host")
if err != nil {
return nil, err
}
// Verify the additional hosts are in correct format
for _, host := range opts.AddHosts {
if _, err := parse.ValidateExtraHost(host); err != nil {
return nil, err
}
}
}
if flags.Changed("hosts-file") {
opts.HostsFile, err = flags.GetString("hosts-file")
if err != nil {
return nil, err
}
}
if flags.Changed("dns") {
servers, err := flags.GetStringSlice("dns")
if err != nil {
return nil, err
}
for _, d := range servers {
if d == "none" {
opts.UseImageResolvConf = true
if len(servers) > 1 {
return nil, fmt.Errorf("%s is not allowed to be specified with other DNS ip addresses", d)
}
break
}
dns := net.ParseIP(d)
if dns == nil {
return nil, fmt.Errorf("%s is not an ip address", d)
}
opts.DNSServers = append(opts.DNSServers, dns)
}
}
if flags.Changed("dns-option") {
options, err := flags.GetStringSlice("dns-option")
if err != nil {
return nil, err
}
opts.DNSOptions = options
}
if flags.Changed("dns-search") {
dnsSearches, err := flags.GetStringSlice("dns-search")
if err != nil {
return nil, err
}
// Validate domains are good
for _, dom := range dnsSearches {
if dom == "." {
if len(dnsSearches) > 1 {
return nil, errors.New("cannot pass additional search domains when also specifying '.'")
}
continue
}
if _, err := parse.ValidateDomain(dom); err != nil {
return nil, err
}
}
opts.DNSSearch = dnsSearches
}
if flags.Changed("publish") {
inputPorts, err := flags.GetStringSlice("publish")
if err != nil {
return nil, err
}
if len(inputPorts) > 0 {
opts.PublishPorts, err = specgenutil.CreatePortBindings(inputPorts)
if err != nil {
return nil, err
}
}
}
opts.NoHostname, err = flags.GetBool("no-hostname")
if err != nil {
return nil, err
}
opts.NoHosts, err = flags.GetBool("no-hosts")
if err != nil {
return nil, err
}
// parse the network only when network was changed
// otherwise we send default to server so that the server
// can pick the correct default instead of the client
if flags.Changed("network") {
network, err := flags.GetStringArray("network")
if err != nil {
return nil, err
}
ns, networks, options, err := specgen.ParseNetworkFlag(network)
if err != nil {
return nil, err
}
opts.NetworkOptions = options
opts.Network = ns
opts.Networks = networks
}
if flags.Changed("ip") || flags.Changed("ip6") || flags.Changed("mac-address") || flags.Changed("network-alias") {
// if there is no network we add the default
if len(opts.Networks) == 0 {
opts.Networks = map[string]types.PerNetworkOptions{
"default": {},
}
}
for _, ipFlagName := range []string{"ip", "ip6"} {
ip, err := flags.GetString(ipFlagName)
if err != nil {
return nil, err
}
if ip != "" {
// if pod create --infra=false
if infra, err := flags.GetBool("infra"); err == nil && !infra {
return nil, fmt.Errorf("cannot set --%s without infra container: %w", ipFlagName, define.ErrInvalidArg)
}
staticIP := net.ParseIP(ip)
if staticIP == nil {
return nil, fmt.Errorf("%q is not an ip address", ip)
}
if !opts.Network.IsBridge() && !opts.Network.IsDefault() {
return nil, fmt.Errorf("--%s can only be set when the network mode is bridge: %w", ipFlagName, define.ErrInvalidArg)
}
if len(opts.Networks) != 1 {
return nil, fmt.Errorf("--%s can only be set for a single network: %w", ipFlagName, define.ErrInvalidArg)
}
for name, netOpts := range opts.Networks {
netOpts.StaticIPs = append(netOpts.StaticIPs, staticIP)
opts.Networks[name] = netOpts
}
}
}
m, err := flags.GetString("mac-address")
if err != nil {
return nil, err
}
if len(m) > 0 {
// if pod create --infra=false
if infra, err := flags.GetBool("infra"); err == nil && !infra {
return nil, fmt.Errorf("cannot set --mac without infra container: %w", define.ErrInvalidArg)
}
mac, err := net.ParseMAC(m)
if err != nil {
return nil, err
}
if !opts.Network.IsBridge() && !opts.Network.IsDefault() {
return nil, fmt.Errorf("--mac-address can only be set when the network mode is bridge: %w", define.ErrInvalidArg)
}
if len(opts.Networks) != 1 {
return nil, fmt.Errorf("--mac-address can only be set for a single network: %w", define.ErrInvalidArg)
}
for name, netOpts := range opts.Networks {
netOpts.StaticMAC = types.HardwareAddr(mac)
opts.Networks[name] = netOpts
}
}
aliases, err := flags.GetStringSlice("network-alias")
if err != nil {
return nil, err
}
if len(aliases) > 0 {
// if pod create --infra=false
if infra, err := flags.GetBool("infra"); err == nil && !infra {
return nil, fmt.Errorf("cannot set --network-alias without infra container: %w", define.ErrInvalidArg)
}
if !opts.Network.IsBridge() && !opts.Network.IsDefault() {
return nil, fmt.Errorf("--network-alias can only be set when the network mode is bridge: %w", define.ErrInvalidArg)
}
for name, netOpts := range opts.Networks {
netOpts.Aliases = aliases
opts.Networks[name] = netOpts
}
}
}
return opts, err
}

View File

@ -0,0 +1,24 @@
package common
import (
"os"
"github.com/containers/image/v5/types"
)
// SetRegistriesConfPath sets the registries.conf path for the specified context.
// NOTE: this is a verbatim copy from c/common/libimage which we're not using
// to prevent leaking c/storage into this file. Maybe this should go into c/image?
func SetRegistriesConfPath(systemContext *types.SystemContext) {
if systemContext.SystemRegistriesConfPath != "" {
return
}
if envOverride, ok := os.LookupEnv("CONTAINERS_REGISTRIES_CONF"); ok {
systemContext.SystemRegistriesConfPath = envOverride
return
}
if envOverride, ok := os.LookupEnv("REGISTRIES_CONFIG_PATH"); ok {
systemContext.SystemRegistriesConfPath = envOverride
return
}
}

66
cmd/podman/common/sign.go Normal file
View File

@ -0,0 +1,66 @@
package common
import (
"fmt"
"os"
"github.com/containers/common/pkg/ssh"
"github.com/containers/image/v5/pkg/cli"
"github.com/containers/image/v5/pkg/cli/sigstore"
"github.com/containers/image/v5/signature/signer"
"github.com/containers/podman/v5/pkg/domain/entities"
)
// PrepareSigning updates pushOpts.Signers, pushOpts.SignPassphrase and SignSigstorePrivateKeyPassphrase based on a --sign-passphrase-file
// value signPassphraseFile and a --sign-by-sigsstore value signBySigstoreParamFile, and validates pushOpts.Sign* consistency.
// It may interactively prompt for a passphrase if one is required and wasnt provided otherwise;
// or it may interactively trigger an OIDC authentication, using standard input/output, or even open a web browser.
// Returns a cleanup callback on success, which must be called when done.
func PrepareSigning(pushOpts *entities.ImagePushOptions,
signPassphraseFile, signBySigstoreParamFile string) (func(), error) {
// c/common/libimage.Image does allow creating both simple signing and sigstore signatures simultaneously,
// with independent passphrases, but that would make the CLI probably too confusing.
// For now, use the passphrase with either, but only one of them.
if signPassphraseFile != "" && pushOpts.SignBy != "" && pushOpts.SignBySigstorePrivateKeyFile != "" {
return nil, fmt.Errorf("only one of --sign-by and sign-by-sigstore-private-key can be used with --sign-passphrase-file")
}
var passphrase string
if signPassphraseFile != "" {
p, err := cli.ReadPassphraseFile(signPassphraseFile)
if err != nil {
return nil, err
}
passphrase = p
} else if pushOpts.SignBySigstorePrivateKeyFile != "" {
p := ssh.ReadPassphrase()
passphrase = string(p)
} // pushOpts.SignBy triggers a GPG-agent passphrase prompt, possibly using a more secure channel, so we usually shouldnt prompt ourselves if no passphrase was explicitly provided.
pushOpts.SignPassphrase = passphrase
pushOpts.SignSigstorePrivateKeyPassphrase = []byte(passphrase)
cleanup := signingCleanup{}
if signBySigstoreParamFile != "" {
signer, err := sigstore.NewSignerFromParameterFile(signBySigstoreParamFile, &sigstore.Options{
PrivateKeyPassphrasePrompt: cli.ReadPassphraseFile,
Stdin: os.Stdin,
Stdout: os.Stdout,
})
if err != nil {
return nil, err
}
pushOpts.Signers = append(pushOpts.Signers, signer)
cleanup.signers = append(cleanup.signers, signer)
}
return cleanup.cleanup, nil
}
// signingCleanup carries state for cleanup after PrepareSigning
type signingCleanup struct {
signers []*signer.Signer
}
func (c *signingCleanup) cleanup() {
for _, s := range c.signers {
s.Close()
}
}

19
cmd/podman/common/term.go Normal file
View File

@ -0,0 +1,19 @@
package common
import (
"os"
"golang.org/x/term"
)
// ClearScreen clears the screen and puts the cursor back to position 1,1
// Useful when printing output in an interval like podman stats.
// When the stdout is not a terminal this is a NOP.
func ClearScreen() {
// Only write escape sequences when the output is a terminal.
if term.IsTerminal(int(os.Stdout.Fd())) {
// terminal escape control sequence to clear screen ([2J)
// followed by putting the cursor to position 1,1 ([1;1H)
os.Stdout.WriteString("\033[2J\033[1;1H")
}
}

View File

@ -0,0 +1,93 @@
package completion
import (
"fmt"
"io"
"os"
"strings"
commonComp "github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/spf13/cobra"
)
const (
completionDescription = `Generate shell autocompletions.
Valid arguments are bash, zsh, fish, and powershell.
Please refer to the man page to see how you can load these completions.`
)
var (
file string
noDesc bool
shells = []string{"bash", "zsh", "fish", "powershell"}
completionCmd = &cobra.Command{
Use: fmt.Sprintf("completion [options] {%s}", strings.Join(shells, "|")),
Short: "Generate shell autocompletions",
Long: completionDescription,
ValidArgs: shells,
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
RunE: completion,
Example: `podman completion bash
podman completion zsh -f _podman
podman completion fish --no-desc`,
// don't show this command to users
Hidden: true,
}
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: completionCmd,
})
flags := completionCmd.Flags()
fileFlagName := "file"
flags.StringVarP(&file, fileFlagName, "f", "", "Output the completion to file rather than stdout.")
_ = completionCmd.RegisterFlagCompletionFunc(fileFlagName, commonComp.AutocompleteDefault)
flags.BoolVar(&noDesc, "no-desc", false, "Don't include descriptions in the completion output.")
}
func completion(cmd *cobra.Command, args []string) error {
var w io.Writer
if file != "" {
file, err := os.Create(file)
if err != nil {
return err
}
defer file.Close()
w = file
} else {
w = os.Stdout
}
var err error
switch args[0] {
case "bash":
err = cmd.Root().GenBashCompletionV2(w, !noDesc)
case "zsh":
if noDesc {
err = cmd.Root().GenZshCompletionNoDesc(w)
} else {
err = cmd.Root().GenZshCompletion(w)
}
case "fish":
err = cmd.Root().GenFishCompletion(w, !noDesc)
case "powershell":
if noDesc {
err = cmd.Root().GenPowerShellCompletion(w)
} else {
err = cmd.Root().GenPowerShellCompletionWithDesc(w)
}
}
if err != nil {
return err
}
_, err = io.WriteString(w, fmt.Sprintf(
"\n# This file is generated with %q; see: podman-completion(1)\n", cmd.CommandPath(),
))
return err
}

273
cmd/podman/compose.go Normal file
View File

@ -0,0 +1,273 @@
package main
import (
"errors"
"fmt"
"io"
"net/url"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"text/template"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/pkg/errorhandling"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
var composeCommand = &cobra.Command{
Use: "compose [options]",
Short: "Run compose workloads via an external provider such as docker-compose or podman-compose",
Long: `This command is a thin wrapper around an external compose provider such as docker-compose or podman-compose. This means that podman compose is executing another tool that implements the compose functionality but sets up the environment in a way to let the compose provider communicate transparently with the local Podman socket. The specified options as well the command and argument are passed directly to the compose provider.
The default compose providers are docker-compose and podman-compose. If installed, docker-compose takes precedence since it is the original implementation of the Compose specification and is widely used on the supported platforms (i.e., Linux, Mac OS, Windows).
If you want to change the default behavior or have a custom installation path for your provider of choice, please change the compose_providers field in containers.conf(5) to compose_providers = ["/path/to/provider"]. You may also set the PODMAN_COMPOSE_PROVIDER environment variable.`,
RunE: composeMain,
ValidArgsFunction: composeCompletion,
Example: `podman compose -f nginx.yaml up --detach
podman --log-level=debug compose -f many-images.yaml pull`,
DisableFlagParsing: true,
Annotations: map[string]string{registry.ParentNSRequired: ""}, // don't join user NS for SSH to work correctly
}
func init() {
// NOTE: we need to fully disable flag parsing and manually parse the
// flags in composeMain. cobra's FParseErrWhitelist will strip off
// unknown flags _before_ the first argument. So `--unknown argument`
// will show as `argument`.
registry.Commands = append(registry.Commands, registry.CliCommand{Command: composeCommand})
}
func composeCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
var stdout strings.Builder
args = append(args, toComplete)
args = append([]string{"__complete"}, args...)
if err := composeProviderExec(args, &stdout, io.Discard, false); err != nil {
// Ignore errors since some providers may not expose a __complete command.
return nil, cobra.ShellCompDirectiveError
}
var num int
output := strings.Split(strings.TrimRight(stdout.String(), "\n"), "\n")
if len(output) >= 1 {
if lastLine := output[len(output)-1]; strings.HasPrefix(lastLine, ":") {
var err error
if num, err = strconv.Atoi(lastLine[1:]); err != nil {
return nil, cobra.ShellCompDirectiveError
}
output = output[:len(output)-1]
}
}
return output, cobra.ShellCompDirective(num)
}
// composeProvider provides the name of or absolute path to the compose
// provider (i.e., the external binary such as docker-compose).
func composeProvider() (string, error) {
if value, ok := os.LookupEnv("PODMAN_COMPOSE_PROVIDER"); ok {
return value, nil
}
candidates := registry.PodmanConfig().ContainersConfDefaultsRO.Engine.ComposeProviders.Get()
if len(candidates) == 0 {
return "", errors.New("no compose provider specified, please refer to `man podman-compose` for details")
}
lookupErrors := make([]error, 0, len(candidates))
for _, candidate := range candidates {
path, err := exec.LookPath(os.ExpandEnv(candidate))
if err == nil {
// First specified provider "candidate" wins.
logrus.Debugf("Found compose provider %q", path)
return path, nil
}
logrus.Debugf("Error looking up compose provider %q: %v", candidate, err)
lookupErrors = append(lookupErrors, err)
}
return "", fmt.Errorf("looking up compose provider failed\n%v", errorhandling.JoinErrors(lookupErrors))
}
// composeDockerHost returns the value to be set in the DOCKER_HOST environment
// variable.
func composeDockerHost() (string, error) {
if value, ok := os.LookupEnv("DOCKER_HOST"); ok {
return value, nil
}
// For local clients (Linux/FreeBSD), use the default API
// address.
if !registry.IsRemote() {
return registry.DefaultAPIAddress(), nil
}
conf := registry.PodmanConfig()
if conf.URI == "" {
switch runtime.GOOS {
// If no default connection is set on Linux or FreeBSD,
// we just use the local socket by default - just as
// the remote client does.
case "linux", "freebsd":
return registry.DefaultAPIAddress(), nil
// If there is no default connection on Windows or Mac
// OS, we can safely assume that something went wrong.
// A `podman machine init` will set the connection.
default:
return "", fmt.Errorf("cannot connect to a socket or via SSH: no default connection found: consider running `podman machine init`")
}
}
parsedConnection, err := url.Parse(conf.URI)
if err != nil {
return "", fmt.Errorf("preparing connection to remote machine: %w", err)
}
// If the default connection does not point to a `podman
// machine`, we cannot use a local path and need to use SSH.
if !conf.MachineMode {
// Docker Compose v1 doesn't like paths for ssh, so we optimistically
// assume the presence of a Docker socket on the remote
// machine which is the case for podman machines.
if parsedConnection.Scheme == "ssh" {
return strings.TrimSuffix(conf.URI, parsedConnection.Path), nil
}
return conf.URI, nil
}
uri, err := getMachineConn(conf.URI, parsedConnection)
if err != nil {
return "", fmt.Errorf("get machine connection URI: %w", err)
}
return uri, nil
}
// composeEnv returns the compose-specific environment variables.
func composeEnv() ([]string, error) {
hostValue, err := composeDockerHost()
if err != nil {
return nil, err
}
return []string{
"DOCKER_HOST=" + hostValue,
// Podman doesn't support all buildkit features and since it's
// a continuous catch-up game, disable buildkit on the client
// side.
//
// See https://github.com/containers/podman/issues/18617#issuecomment-1600495841
"DOCKER_BUILDKIT=0",
// FIXME: DOCKER_CONFIG is limited by containers/podman/issues/18617
// and it remains unclear which default path should be set
// w.r.t. Docker compatibility and a smooth experience of podman-login
// working with podman-compose _by default_.
"DOCKER_CONFIG=" + os.Getenv("DOCKER_CONFIG"),
}, nil
}
// composeShouldLogWarning returns whether a notice on engine redirection should be piped to stderr regardless of logging configuration
func composeShouldLogWarning() (bool, error) {
if shouldWarnLogsEnv, ok := os.LookupEnv("PODMAN_COMPOSE_WARNING_LOGS"); ok {
if shouldWarnLogsEnvVal, err := strconv.ParseBool(shouldWarnLogsEnv); err == nil {
return shouldWarnLogsEnvVal, nil
} else if shouldWarnLogsEnv != "" {
return true, fmt.Errorf("PODMAN_COMPOSE_WARNING_LOGS should be a boolean: %w", err)
}
}
return registry.PodmanConfig().ContainersConfDefaultsRO.Engine.ComposeWarningLogs, nil
}
// underline uses ANSI codes to underline the specified string.
func underline(str string) string {
return "\033[4m" + str + "\033[0m"
}
// composeProviderExec executes the compose provider with the specified arguments.
func composeProviderExec(args []string, stdout io.Writer, stderr io.Writer, warn bool) error {
provider, err := composeProvider()
if err != nil {
return err
}
env, err := composeEnv()
if err != nil {
return err
}
if stdout == nil {
stdout = os.Stdout
}
if stderr == nil {
stderr = os.Stderr
}
cmd := exec.Command(provider, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = stdout
cmd.Stderr = stderr
cmd.Env = append(os.Environ(), env...)
logrus.Debugf("Executing compose provider (%s %s) with additional env %s", provider, strings.Join(args, " "), strings.Join(env, " "))
if warn {
fmt.Fprint(os.Stderr, underline(fmt.Sprintf(">>>> Executing external compose provider %q. Please see podman-compose(1) for how to disable this message. <<<<\n\n", provider)))
}
if err := cmd.Run(); err != nil {
// Make sure podman returns with the same exit code as the compose provider.
if exitErr, isExit := err.(*exec.ExitError); isExit {
registry.SetExitCode(exitErr.ExitCode())
}
// Format the error to make it explicit that error did not come
// from podman but from the executed compose provider.
return fmt.Errorf("executing %s %s: %w", provider, strings.Join(args, " "), err)
}
return nil
}
// composeHelp is a custom help function to display the help message of the
// configured compose-provider.
func composeHelp(cmd *cobra.Command) error {
tmpl, err := template.New("help_template").Parse(helpTemplate)
if err != nil {
return err
}
if err := tmpl.Execute(os.Stdout, cmd); err != nil {
return err
}
shouldLog, err := composeShouldLogWarning()
if err != nil {
return err
}
return composeProviderExec([]string{"--help"}, nil, nil, shouldLog)
}
// composeMain is the main function of the compose command.
func composeMain(cmd *cobra.Command, args []string) error {
// We have to manually parse the flags here to make sure all arguments
// after `podman compose [ARGS]` are passed to the compose provider.
// For now, we only look for the --help flag.
fs := pflag.NewFlagSet("args", pflag.ContinueOnError)
fs.ParseErrorsWhitelist.UnknownFlags = true
fs.SetInterspersed(false)
fs.BoolP("help", "h", false, "")
if err := fs.Parse(args); err != nil {
return fmt.Errorf("parsing arguments: %w", err)
}
if len(args) == 0 || fs.Lookup("help").Changed {
return composeHelp(cmd)
}
shouldLog, err := composeShouldLogWarning()
if err != nil {
return err
}
return composeProviderExec(args, nil, nil, shouldLog)
}

View File

@ -0,0 +1,59 @@
//go:build amd64 || arm64
package main
import (
"fmt"
"net/url"
"strconv"
"github.com/containers/podman/v5/pkg/machine/define"
"github.com/containers/podman/v5/pkg/machine/env"
"github.com/containers/podman/v5/pkg/machine/provider"
"github.com/containers/podman/v5/pkg/machine/vmconfigs"
)
func getMachineConn(connectionURI string, parsedConnection *url.URL) (string, error) {
machineProvider, err := provider.Get()
if err != nil {
return "", fmt.Errorf("getting machine provider: %w", err)
}
dirs, err := env.GetMachineDirs(machineProvider.VMType())
if err != nil {
return "", err
}
machineList, err := vmconfigs.LoadMachinesInDir(dirs)
if err != nil {
return "", fmt.Errorf("listing machines: %w", err)
}
// Now we know that the connection points to a machine and we
// can find the machine by looking for the one with the
// matching port.
connectionPort, err := strconv.Atoi(parsedConnection.Port())
if err != nil {
return "", fmt.Errorf("parsing connection port: %w", err)
}
for _, mc := range machineList {
if connectionPort != mc.SSH.Port {
continue
}
state, err := machineProvider.State(mc, false)
if err != nil {
return "", err
}
if state != define.Running {
return "", fmt.Errorf("machine %s is not running but in state %s", mc.Name, state)
}
podmanSocket, podmanPipe, err := mc.ConnectionInfo(machineProvider.VMType())
if err != nil {
return "", err
}
return extractConnectionString(podmanSocket, podmanPipe)
}
return "", fmt.Errorf("could not find a matching machine for connection %q", connectionURI)
}

View File

@ -0,0 +1,16 @@
//go:build (amd64 || arm64) && !windows
package main
import (
"errors"
"github.com/containers/podman/v5/pkg/machine/define"
)
func extractConnectionString(podmanSocket *define.VMFile, podmanPipe *define.VMFile) (string, error) {
if podmanSocket == nil {
return "", errors.New("socket of machine is not set")
}
return "unix://" + podmanSocket.Path, nil
}

View File

@ -0,0 +1,12 @@
//go:build !(amd64 || arm64)
package main
import (
"errors"
"net/url"
)
func getMachineConn(connection string, parsedConnection *url.URL) (string, error) {
return "", errors.New("podman machine not supported on this architecture")
}

View File

@ -0,0 +1,15 @@
package main
import (
"errors"
"path/filepath"
"github.com/containers/podman/v5/pkg/machine/define"
)
func extractConnectionString(podmanSocket *define.VMFile, podmanPipe *define.VMFile) (string, error) {
if podmanPipe == nil {
return "", errors.New("pipe of machine is not set")
}
return "npipe://" + filepath.ToSlash(podmanPipe.Path), nil
}

View File

@ -0,0 +1,88 @@
package containers
import (
"errors"
"os"
"strings"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
)
var (
attachDescription = "The podman attach command allows you to attach to a running container using the container's ID or name, either to view its ongoing output or to control it interactively."
attachCommand = &cobra.Command{
Use: "attach [options] CONTAINER",
Short: "Attach to a running container",
Long: attachDescription,
RunE: attach,
Args: validate.IDOrLatestArgs,
ValidArgsFunction: common.AutocompleteContainersRunning,
Example: `podman attach ctrID
podman attach 1234
podman attach --no-stdin foobar`,
}
containerAttachCommand = &cobra.Command{
Use: attachCommand.Use,
Short: attachCommand.Short,
Long: attachCommand.Long,
RunE: attachCommand.RunE,
Args: validate.IDOrLatestArgs,
ValidArgsFunction: attachCommand.ValidArgsFunction,
Example: `podman container attach ctrID
podman container attach 1234
podman container attach --no-stdin foobar`,
}
)
var (
attachOpts entities.AttachOptions
)
func attachFlags(cmd *cobra.Command) {
flags := cmd.Flags()
detachKeysFlagName := "detach-keys"
flags.StringVar(&attachOpts.DetachKeys, detachKeysFlagName, containerConfig.DetachKeys(), "Select the key sequence for detaching a container. Format is a single character `[a-Z]` or a comma separated sequence of `ctrl-<value>`, where `<value>` is one of: `a-z`, `@`, `^`, `[`, `\\`, `]`, `^` or `_`")
_ = cmd.RegisterFlagCompletionFunc(detachKeysFlagName, common.AutocompleteDetachKeys)
flags.BoolVar(&attachOpts.NoStdin, "no-stdin", false, "Do not attach STDIN. The default is false")
flags.BoolVar(&attachOpts.SigProxy, "sig-proxy", true, "Proxy received signals to the process")
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: attachCommand,
})
attachFlags(attachCommand)
validate.AddLatestFlag(attachCommand, &attachOpts.Latest)
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: containerAttachCommand,
Parent: containerCmd,
})
attachFlags(containerAttachCommand)
validate.AddLatestFlag(containerAttachCommand, &attachOpts.Latest)
}
func attach(cmd *cobra.Command, args []string) error {
if len(args) > 1 || (len(args) == 0 && !attachOpts.Latest) {
return errors.New("attach requires the name or id of one running container or the latest flag")
}
var name string
if len(args) > 0 {
name = strings.TrimPrefix(args[0], "/")
}
attachOpts.Stdin = os.Stdin
if attachOpts.NoStdin {
attachOpts.Stdin = nil
}
attachOpts.Stdout = os.Stdout
attachOpts.Stderr = os.Stderr
return registry.ContainerEngine().ContainerAttach(registry.Context(), name, attachOpts)
}

View File

@ -0,0 +1,156 @@
package containers
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/utils"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/pkg/criu"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/rootless"
"github.com/containers/storage/pkg/archive"
"github.com/spf13/cobra"
)
var (
checkpointDescription = `
podman container checkpoint
Checkpoints one or more running containers. The container name or ID can be used.
`
checkpointCommand = &cobra.Command{
Use: "checkpoint [options] CONTAINER [CONTAINER...]",
Short: "Checkpoint one or more containers",
Long: checkpointDescription,
RunE: checkpoint,
Args: func(cmd *cobra.Command, args []string) error {
return validate.CheckAllLatestAndIDFile(cmd, args, false, "")
},
ValidArgsFunction: common.AutocompleteContainersRunning,
Example: `podman container checkpoint --keep ctrID
podman container checkpoint --all
podman container checkpoint --leave-running ctrID`,
}
)
var checkpointOptions entities.CheckpointOptions
type checkpointStatistics struct {
PodmanDuration int64 `json:"podman_checkpoint_duration"`
ContainerStatistics []*entities.CheckpointReport `json:"container_statistics"`
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: checkpointCommand,
Parent: containerCmd,
})
flags := checkpointCommand.Flags()
flags.BoolVarP(&checkpointOptions.Keep, "keep", "k", false, "Keep all temporary checkpoint files")
flags.BoolVarP(&checkpointOptions.LeaveRunning, "leave-running", "R", false, "Leave the container running after writing checkpoint to disk")
flags.BoolVar(&checkpointOptions.TCPEstablished, "tcp-established", false, "Checkpoint a container with established TCP connections")
flags.BoolVar(&checkpointOptions.FileLocks, "file-locks", false, "Checkpoint a container with file locks")
flags.BoolVarP(&checkpointOptions.All, "all", "a", false, "Checkpoint all running containers")
exportFlagName := "export"
flags.StringVarP(&checkpointOptions.Export, exportFlagName, "e", "", "Export the checkpoint image to a tar.gz")
_ = checkpointCommand.RegisterFlagCompletionFunc(exportFlagName, completion.AutocompleteDefault)
flags.BoolVar(&checkpointOptions.IgnoreRootFS, "ignore-rootfs", false, "Do not include root file-system changes when exporting")
flags.BoolVar(&checkpointOptions.IgnoreVolumes, "ignore-volumes", false, "Do not export volumes associated with container")
flags.BoolVarP(&checkpointOptions.PreCheckPoint, "pre-checkpoint", "P", false, "Dump container's memory information only, leave the container running")
flags.BoolVar(&checkpointOptions.WithPrevious, "with-previous", false, "Checkpoint container with pre-checkpoint images")
createImageFlagName := "create-image"
flags.StringVarP(&checkpointOptions.CreateImage, createImageFlagName, "", "", "Create checkpoint image with specified name")
_ = checkpointCommand.RegisterFlagCompletionFunc(createImageFlagName, completion.AutocompleteNone)
flags.StringP("compress", "c", "zstd", "Select compression algorithm (gzip, none, zstd) for checkpoint archive.")
_ = checkpointCommand.RegisterFlagCompletionFunc("compress", common.AutocompleteCheckpointCompressType)
flags.BoolVar(
&checkpointOptions.PrintStats,
"print-stats",
false,
"Display checkpoint statistics",
)
validate.AddLatestFlag(checkpointCommand, &checkpointOptions.Latest)
}
func checkpoint(cmd *cobra.Command, args []string) error {
var errs utils.OutputErrors
args = utils.RemoveSlash(args)
podmanStart := time.Now()
if cmd.Flags().Changed("compress") {
if checkpointOptions.Export == "" {
return errors.New("--compress can only be used with --export")
}
compress, _ := cmd.Flags().GetString("compress")
switch strings.ToLower(compress) {
case "none":
checkpointOptions.Compression = archive.Uncompressed
case "gzip":
checkpointOptions.Compression = archive.Gzip
case "zstd":
checkpointOptions.Compression = archive.Zstd
default:
return fmt.Errorf("selected compression algorithm (%q) not supported. Please select one from: gzip, none, zstd", compress)
}
} else {
checkpointOptions.Compression = archive.Zstd
}
if rootless.IsRootless() {
return errors.New("checkpointing a container requires root")
}
if checkpointOptions.Export == "" && checkpointOptions.IgnoreRootFS {
return errors.New("--ignore-rootfs can only be used with --export")
}
if checkpointOptions.Export == "" && checkpointOptions.IgnoreVolumes {
return errors.New("--ignore-volumes can only be used with --export")
}
if checkpointOptions.WithPrevious && checkpointOptions.PreCheckPoint {
return errors.New("--with-previous can not be used with --pre-checkpoint")
}
if (checkpointOptions.WithPrevious || checkpointOptions.PreCheckPoint) && !criu.MemTrack() {
return errors.New("system (architecture/kernel/CRIU) does not support memory tracking")
}
responses, err := registry.ContainerEngine().ContainerCheckpoint(context.Background(), args, checkpointOptions)
if err != nil {
return err
}
podmanFinished := time.Now()
var statistics checkpointStatistics
for _, r := range responses {
switch {
case r.Err != nil:
errs = append(errs, r.Err)
case checkpointOptions.PrintStats:
statistics.ContainerStatistics = append(statistics.ContainerStatistics, r)
case r.RawInput != "":
fmt.Println(r.RawInput)
default:
fmt.Println(r.Id)
}
}
if checkpointOptions.PrintStats {
statistics.PodmanDuration = podmanFinished.Sub(podmanStart).Microseconds()
j, err := json.MarshalIndent(statistics, "", " ")
if err != nil {
return err
}
fmt.Println(string(j))
}
return errs.PrintErrors()
}

View File

@ -0,0 +1,108 @@
package containers
import (
"errors"
"fmt"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/utils"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
cleanupDescription = `
podman container cleanup
Cleans up mount points and network stacks on one or more containers from the host. The container name or ID can be used. This command is used internally when running containers, but can also be used if container cleanup has failed when a container exits.
`
cleanupCommand = &cobra.Command{
Annotations: map[string]string{registry.EngineMode: registry.ABIMode},
Use: "cleanup [options] CONTAINER [CONTAINER...]",
Short: "Clean up network and mountpoints of one or more containers",
Long: cleanupDescription,
RunE: cleanup,
Args: func(cmd *cobra.Command, args []string) error {
return validate.CheckAllLatestAndIDFile(cmd, args, false, "")
},
ValidArgsFunction: common.AutocompleteContainersExited,
Example: `podman container cleanup ctrID1 ctrID2 ctrID3
podman container cleanup --all`,
}
)
var (
cleanupOptions entities.ContainerCleanupOptions
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Parent: containerCmd,
Command: cleanupCommand,
})
flags := cleanupCommand.Flags()
flags.BoolVarP(&cleanupOptions.All, "all", "a", false, "Cleans up all containers")
execFlagName := "exec"
flags.StringVar(&cleanupOptions.Exec, execFlagName, "", "Clean up the given exec session instead of the container")
_ = cleanupCommand.RegisterFlagCompletionFunc(execFlagName, completion.AutocompleteNone)
flags.BoolVar(&cleanupOptions.Remove, "rm", false, "After cleanup, remove the container entirely")
flags.BoolVar(&cleanupOptions.RemoveImage, "rmi", false, "After cleanup, remove the image entirely")
stoppedOnlyFlag := "stopped-only"
flags.BoolVar(&cleanupOptions.StoppedOnly, stoppedOnlyFlag, false, "Only cleanup when the container is in the stopped state")
_ = flags.MarkHidden(stoppedOnlyFlag)
validate.AddLatestFlag(cleanupCommand, &cleanupOptions.Latest)
}
func cleanup(cmd *cobra.Command, args []string) error {
var (
errs utils.OutputErrors
)
if cleanupOptions.Exec != "" {
switch {
case cleanupOptions.All:
return errors.New("--all and --exec cannot be set together")
case len(args) > 1:
return errors.New("cannot use exec option when more than one container is given")
case cleanupOptions.RemoveImage:
return errors.New("--exec and --rmi cannot be set together")
}
}
responses, err := registry.ContainerEngine().ContainerCleanup(registry.Context(), args, cleanupOptions)
if err != nil {
// `podman container cleanup` is almost always run in the
// background. Our only way of relaying information to the user
// is via syslog.
// As such, we need to logrus.Errorf our errors to ensure they
// are properly printed if --syslog is set.
logrus.Errorf("Running container cleanup: %v", err)
return err
}
for _, r := range responses {
switch {
case r.RmErr != nil:
logrus.Errorf("Removing container: %v", r.RmErr)
errs = append(errs, r.RmErr)
case r.RmiErr != nil:
logrus.Errorf("Removing image: %v", r.RmiErr)
errs = append(errs, r.RmiErr)
case r.CleanErr != nil:
logrus.Errorf("Cleaning up container: %v", r.CleanErr)
errs = append(errs, r.CleanErr)
case r.RawInput != "":
fmt.Println(r.RawInput)
default:
fmt.Println(r.Id)
}
}
return errs.PrintErrors()
}

View File

@ -0,0 +1,86 @@
package containers
import (
"fmt"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/libpod/define"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
)
var (
cloneDescription = `Creates a copy of an existing container.`
containerCloneCommand = &cobra.Command{
Use: "clone [options] CONTAINER NAME IMAGE",
Short: "Clone an existing container",
Long: cloneDescription,
RunE: clone,
Args: cobra.RangeArgs(1, 3),
ValidArgsFunction: common.AutocompleteClone,
Example: `podman container clone container_name new_name image_name`,
}
)
var (
ctrClone entities.ContainerCloneOptions
)
func cloneFlags(cmd *cobra.Command) {
flags := cmd.Flags()
destroyFlagName := "destroy"
flags.BoolVar(&ctrClone.Destroy, destroyFlagName, false, "destroy the original container")
runFlagName := "run"
flags.BoolVar(&ctrClone.Run, runFlagName, false, "run the new container")
forceFlagName := "force"
flags.BoolVarP(&ctrClone.Force, forceFlagName, "f", false, "force the existing container to be destroyed")
common.DefineCreateDefaults(&ctrClone.CreateOpts)
common.DefineCreateFlags(cmd, &ctrClone.CreateOpts, entities.CloneMode)
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: containerCloneCommand,
Parent: containerCmd,
})
cloneFlags(containerCloneCommand)
}
func clone(cmd *cobra.Command, args []string) error {
switch len(args) {
case 0:
return fmt.Errorf("must specify at least 1 argument: %w", define.ErrInvalidArg)
case 2:
ctrClone.CreateOpts.Name = args[1]
case 3:
ctrClone.CreateOpts.Name = args[1]
ctrClone.Image = args[2]
if !cliVals.RootFS {
rawImageName := args[0]
name, err := pullImage(cmd, ctrClone.Image, &ctrClone.CreateOpts)
if err != nil {
return err
}
ctrClone.Image = name
ctrClone.RawImageName = rawImageName
}
}
if ctrClone.Force && !ctrClone.Destroy {
return fmt.Errorf("cannot set --force without --destroy: %w", define.ErrInvalidArg)
}
ctrClone.ID = args[0]
ctrClone.CreateOpts.IsClone = true
rep, err := registry.ContainerEngine().ContainerClone(registry.Context(), ctrClone)
if err != nil {
return err
}
fmt.Println(rep.Id)
return nil
}

View File

@ -0,0 +1,125 @@
package containers
import (
"context"
"fmt"
"os"
"strings"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
)
var (
commitDescription = `Create an image from a container's changes. Optionally tag the image created, set the author with the --author flag, set the commit message with the --message flag, and make changes to the instructions with the --change flag.`
commitCommand = &cobra.Command{
Use: "commit [options] CONTAINER [IMAGE]",
Short: "Create new image based on the changed container",
Long: commitDescription,
RunE: commit,
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: common.AutocompleteCommitCommand,
Example: `podman commit -q --message "committing container to image" reverent_golick image-committed
podman commit -q --author "firstName lastName" reverent_golick image-committed
podman commit -q --pause=false containerID image-committed
podman commit containerID`,
}
containerCommitCommand = &cobra.Command{
Args: commitCommand.Args,
Use: commitCommand.Use,
Short: commitCommand.Short,
Long: commitCommand.Long,
RunE: commitCommand.RunE,
ValidArgsFunction: commitCommand.ValidArgsFunction,
Example: `podman container commit -q --message "committing container to image" reverent_golick image-committed
podman container commit -q --author "firstName lastName" reverent_golick image-committed
podman container commit -q --pause=false containerID image-committed
podman container commit containerID`,
}
)
var (
commitOptions = entities.CommitOptions{
ImageName: "",
}
configFile, iidFile string
)
func commitFlags(cmd *cobra.Command) {
flags := cmd.Flags()
changeFlagName := "change"
flags.StringArrayVarP(&commitOptions.Changes, changeFlagName, "c", []string{}, "Apply the following possible instructions to the created image (default []): "+strings.Join(common.ChangeCmds, " | "))
_ = cmd.RegisterFlagCompletionFunc(changeFlagName, common.AutocompleteChangeInstructions)
configFileFlagName := "config"
flags.StringVar(&configFile, configFileFlagName, "", "`file` containing a container configuration to merge into the image")
_ = cmd.RegisterFlagCompletionFunc(configFileFlagName, completion.AutocompleteDefault)
formatFlagName := "format"
flags.StringVarP(&commitOptions.Format, formatFlagName, "f", "oci", "`Format` of the image manifest and metadata")
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteImageFormat)
iidFileFlagName := "iidfile"
flags.StringVarP(&iidFile, iidFileFlagName, "", "", "`file` to write the image ID to")
_ = cmd.RegisterFlagCompletionFunc(iidFileFlagName, completion.AutocompleteDefault)
messageFlagName := "message"
flags.StringVarP(&commitOptions.Message, messageFlagName, "m", "", "Set commit message for imported image")
_ = cmd.RegisterFlagCompletionFunc(messageFlagName, completion.AutocompleteNone)
authorFlagName := "author"
flags.StringVarP(&commitOptions.Author, authorFlagName, "a", "", "Set the author for the image committed")
_ = cmd.RegisterFlagCompletionFunc(authorFlagName, completion.AutocompleteNone)
flags.BoolVarP(&commitOptions.Pause, "pause", "p", false, "Pause container during commit")
flags.BoolVarP(&commitOptions.Quiet, "quiet", "q", false, "Suppress output")
flags.BoolVarP(&commitOptions.Squash, "squash", "s", false, "squash newly built layers into a single new layer")
flags.BoolVar(&commitOptions.IncludeVolumes, "include-volumes", false, "Include container volumes as image volumes")
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: commitCommand,
})
commitFlags(commitCommand)
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: containerCommitCommand,
Parent: containerCmd,
})
commitFlags(containerCommitCommand)
}
func commit(cmd *cobra.Command, args []string) error {
container := strings.TrimPrefix(args[0], "/")
if len(args) == 2 {
commitOptions.ImageName = args[1]
}
if !commitOptions.Quiet {
commitOptions.Writer = os.Stderr
}
if len(configFile) > 0 {
cfg, err := os.ReadFile(configFile)
if err != nil {
return fmt.Errorf("--config: %w", err)
}
commitOptions.Config = cfg
}
response, err := registry.ContainerEngine().ContainerCommit(context.Background(), container, commitOptions)
if err != nil {
return err
}
if len(iidFile) > 0 {
if err = os.WriteFile(iidFile, []byte(response.Id), 0644); err != nil {
return fmt.Errorf("failed to write image ID: %w", err)
}
}
fmt.Println(response.Id)
return nil
}

View File

@ -0,0 +1,29 @@
package containers
import (
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/spf13/cobra"
)
var (
// Pull in configured json library
json = registry.JSONLibrary()
// Command: podman _container_
containerCmd = &cobra.Command{
Use: "container",
Short: "Manage containers",
Long: "Manage containers",
TraverseChildren: true,
RunE: validate.SubCommandExists,
}
containerConfig = registry.PodmanConfig().ContainersConfDefaultsRO
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: containerCmd,
})
}

537
cmd/podman/containers/cp.go Normal file
View File

@ -0,0 +1,537 @@
package containers
import (
"fmt"
"io"
"os"
"os/user"
"path"
"path/filepath"
"strconv"
"strings"
"errors"
buildahCopiah "github.com/containers/buildah/copier"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/pkg/copy"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/errorhandling"
"github.com/containers/storage/pkg/archive"
"github.com/containers/storage/pkg/idtools"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
cpDescription = `Copy the contents of SRC_PATH to the DEST_PATH.
You can copy from the container's file system to the local machine or the reverse, from the local filesystem to the container. If "-" is specified for either the SRC_PATH or DEST_PATH, you can also stream a tar archive from STDIN or to STDOUT. The CONTAINER can be a running or stopped container. The SRC_PATH or DEST_PATH can be a file or a directory.
`
cpCommand = &cobra.Command{
Use: "cp [options] [CONTAINER:]SRC_PATH [CONTAINER:]DEST_PATH",
Short: "Copy files/folders between a container and the local filesystem",
Long: cpDescription,
Args: cobra.ExactArgs(2),
RunE: cp,
ValidArgsFunction: common.AutocompleteCpCommand,
Example: "podman cp [options] [CONTAINER:]SRC_PATH [CONTAINER:]DEST_PATH",
}
containerCpCommand = &cobra.Command{
Use: cpCommand.Use,
Short: cpCommand.Short,
Long: cpCommand.Long,
Args: cpCommand.Args,
RunE: cpCommand.RunE,
ValidArgsFunction: cpCommand.ValidArgsFunction,
Example: "podman container cp [CONTAINER:]SRC_PATH [CONTAINER:]DEST_PATH",
}
)
var (
cpOpts entities.ContainerCpOptions
chown bool
)
func cpFlags(cmd *cobra.Command) {
flags := cmd.Flags()
flags.BoolVar(&cpOpts.OverwriteDirNonDir, "overwrite", false, "Allow to overwrite directories with non-directories and vice versa")
flags.BoolVarP(&chown, "archive", "a", true, `Chown copied files to the primary uid/gid of the destination container.`)
// Deprecated flags (both are NOPs): exist for backwards compat
flags.BoolVar(&cpOpts.Extract, "extract", false, "Deprecated...")
_ = flags.MarkHidden("extract")
flags.BoolVar(&cpOpts.Pause, "pause", true, "Deprecated")
_ = flags.MarkHidden("pause")
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: cpCommand,
})
cpFlags(cpCommand)
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: containerCpCommand,
Parent: containerCmd,
})
cpFlags(containerCpCommand)
}
func cp(cmd *cobra.Command, args []string) error {
// Parse user input.
sourceContainerStr, sourcePath, destContainerStr, destPath, err := copy.ParseSourceAndDestination(args[0], args[1])
if err != nil {
return err
}
if len(sourceContainerStr) > 0 && len(destContainerStr) > 0 {
return copyContainerToContainer(sourceContainerStr, sourcePath, destContainerStr, destPath)
} else if len(sourceContainerStr) > 0 {
return copyFromContainer(sourceContainerStr, sourcePath, destPath)
}
return copyToContainer(destContainerStr, destPath, sourcePath)
}
// containerMustExist returns an error if the specified container does not
// exist.
func containerMustExist(container string) error {
exists, err := registry.ContainerEngine().ContainerExists(registry.Context(), container, entities.ContainerExistsOptions{})
if err != nil {
return err
}
if !exists.Value {
return fmt.Errorf("container %q does not exist", container)
}
return nil
}
// doCopy executes the two functions in parallel to copy data from A to B and
// joins the errors if any.
func doCopy(funcA func() error, funcB func() error) error {
errChan := make(chan error)
go func() {
errChan <- funcA()
}()
var copyErrors []error
copyErrors = append(copyErrors, funcB())
copyErrors = append(copyErrors, <-errChan)
return errorhandling.JoinErrors(copyErrors)
}
func copyContainerToContainer(sourceContainer string, sourcePath string, destContainer string, destPath string) error {
if err := containerMustExist(sourceContainer); err != nil {
return err
}
if err := containerMustExist(destContainer); err != nil {
return err
}
sourceContainerInfo, err := registry.ContainerEngine().ContainerStat(registry.Context(), sourceContainer, sourcePath)
if err != nil {
return fmt.Errorf("%q could not be found on container %s: %w", sourcePath, sourceContainer, err)
}
destContainerBaseName, destContainerInfo, destResolvedToParentDir, err := resolvePathOnDestinationContainer(destContainer, destPath, false)
if err != nil {
return err
}
if sourceContainerInfo.IsDir && !destContainerInfo.IsDir {
return errors.New("destination must be a directory when copying a directory")
}
sourceContainerTarget := sourceContainerInfo.LinkTarget
destContainerTarget := destContainerInfo.LinkTarget
if !destContainerInfo.IsDir {
destContainerTarget = path.Dir(destPath)
}
// If we copy a directory via the "." notation and the container path
// does not exist, we need to make sure that the destination on the
// container gets created; otherwise the contents of the source
// directory will be written to the destination's parent directory.
//
// Hence, whenever "." is the source and the destination does not
// exist, we copy the source's parent and let the copier package create
// the destination via the Rename option.
if destResolvedToParentDir && sourceContainerInfo.IsDir && path.Base(sourcePath) == "." {
sourceContainerTarget = path.Dir(sourceContainerTarget)
}
reader, writer := io.Pipe()
sourceContainerCopy := func() error {
defer writer.Close()
copyFunc, err := registry.ContainerEngine().ContainerCopyToArchive(registry.Context(), sourceContainer, sourceContainerTarget, writer)
if err != nil {
return err
}
if err := copyFunc(); err != nil {
return fmt.Errorf("copying from container: %w", err)
}
return nil
}
destContainerCopy := func() error {
defer reader.Close()
copyOptions := entities.CopyOptions{Chown: chown, NoOverwriteDirNonDir: !cpOpts.OverwriteDirNonDir}
if (!sourceContainerInfo.IsDir && !destContainerInfo.IsDir) || destResolvedToParentDir {
// If we're having a file-to-file copy, make sure to
// rename accordingly.
copyOptions.Rename = map[string]string{path.Base(sourceContainerTarget): destContainerBaseName}
}
copyFunc, err := registry.ContainerEngine().ContainerCopyFromArchive(registry.Context(), destContainer, destContainerTarget, reader, copyOptions)
if err != nil {
return err
}
if err := copyFunc(); err != nil {
return fmt.Errorf("copying to container: %w", err)
}
return nil
}
return doCopy(sourceContainerCopy, destContainerCopy)
}
// copyFromContainer copies from the containerPath on the container to hostPath.
func copyFromContainer(container string, containerPath string, hostPath string) error {
if err := containerMustExist(container); err != nil {
return err
}
isStdout := false
if hostPath == "-" {
isStdout = true
hostPath = os.Stdout.Name()
}
containerInfo, err := registry.ContainerEngine().ContainerStat(registry.Context(), container, containerPath)
if err != nil {
return fmt.Errorf("%q could not be found on container %s: %w", containerPath, container, err)
}
var hostBaseName string
var resolvedToHostParentDir bool
hostInfo, hostInfoErr := copy.ResolveHostPath(hostPath)
if hostInfoErr != nil {
if strings.HasSuffix(hostPath, "/") {
return fmt.Errorf("%q could not be found on the host: %w", hostPath, hostInfoErr)
}
// If it doesn't exist, then let's have a look at the parent dir.
parentDir := filepath.Dir(hostPath)
hostInfo, err = copy.ResolveHostPath(parentDir)
if err != nil {
return fmt.Errorf("%q could not be found on the host: %w", hostPath, hostInfoErr)
}
// If the specified path does not exist, we need to assume that
// it'll be created while copying. Hence, we use it as the
// base path.
hostBaseName = filepath.Base(hostPath)
resolvedToHostParentDir = true
} else {
// If the specified path exists on the host, we must use its
// base path as it may have changed due to symlink evaluations.
hostBaseName = filepath.Base(hostInfo.LinkTarget)
}
if !isStdout {
if err := validateFileInfo(hostInfo); err != nil {
return fmt.Errorf("invalid destination: %w", err)
}
}
// If we copy a directory via the "." notation and the host path does
// not exist, we need to make sure that the destination on the host
// gets created; otherwise the contents of the source directory will be
// written to the destination's parent directory.
//
// While we could cut it short on the host and do create the directory
// ourselves, we would run into problems trying to that the other way
// around when copying into a container. Instead, to keep both
// implementations symmetrical, we need to massage the code a bit to
// let Buildah's copier package create the destination.
//
// Hence, whenever "." is the source and the destination does not exist,
// we copy the source's parent and let the copier package create the
// destination via the Rename option.
containerTarget := containerInfo.LinkTarget
if resolvedToHostParentDir && containerInfo.IsDir && path.Base(containerTarget) == "." {
containerTarget = path.Dir(containerTarget)
}
if !isStdout && containerInfo.IsDir && !hostInfo.IsDir {
return errors.New("destination must be a directory when copying a directory")
}
reader, writer := io.Pipe()
hostCopy := func() error {
defer reader.Close()
if isStdout {
_, err := io.Copy(os.Stdout, reader)
return err
}
groot, err := user.Current()
if err != nil {
return err
}
// Set the {G,U}ID. Let's be tolerant towards the different
// operating systems and only log the errors, so we can debug
// if necessary.
idPair := idtools.IDPair{}
if i, err := strconv.Atoi(groot.Uid); err == nil {
idPair.UID = i
} else {
logrus.Debugf("Error converting UID %q to int: %v", groot.Uid, err)
}
if i, err := strconv.Atoi(groot.Gid); err == nil {
idPair.GID = i
} else {
logrus.Debugf("Error converting GID %q to int: %v", groot.Gid, err)
}
putOptions := buildahCopiah.PutOptions{
ChownDirs: &idPair,
ChownFiles: &idPair,
IgnoreDevices: true,
NoOverwriteDirNonDir: !cpOpts.OverwriteDirNonDir,
NoOverwriteNonDirDir: !cpOpts.OverwriteDirNonDir,
}
if (!containerInfo.IsDir && !hostInfo.IsDir) || resolvedToHostParentDir {
// If we're having a file-to-file copy, make sure to
// rename accordingly.
putOptions.Rename = map[string]string{path.Base(containerTarget): hostBaseName}
}
dir := hostInfo.LinkTarget
if !hostInfo.IsDir {
dir = filepath.Dir(dir)
}
if err := buildahCopiah.Put(dir, "", putOptions, reader); err != nil {
return fmt.Errorf("copying to host: %w", err)
}
return nil
}
containerCopy := func() error {
defer writer.Close()
copyFunc, err := registry.ContainerEngine().ContainerCopyToArchive(registry.Context(), container, containerTarget, writer)
if err != nil {
return err
}
if err := copyFunc(); err != nil {
return fmt.Errorf("copying from container: %w", err)
}
return nil
}
return doCopy(containerCopy, hostCopy)
}
// copyToContainer copies the hostPath to containerPath on the container.
func copyToContainer(container string, containerPath string, hostPath string) error {
if err := containerMustExist(container); err != nil {
return err
}
hostInfo := &copy.FileInfo{}
var err error
isStdin := false
if hostPath == "-" {
isStdin = true
} else {
// Make sure that host path exists if not copying from stdin.
hostInfo, err = copy.ResolveHostPath(hostPath)
if err != nil {
return fmt.Errorf("%q could not be found on the host: %w", hostPath, err)
}
}
containerBaseName, containerInfo, containerResolvedToParentDir, err := resolvePathOnDestinationContainer(container, containerPath, isStdin)
if err != nil {
return err
}
// If we copy a directory via the "." notation and the container path
// does not exist, we need to make sure that the destination on the
// container gets created; otherwise the contents of the source
// directory will be written to the destination's parent directory.
//
// Hence, whenever "." is the source and the destination does not
// exist, we copy the source's parent and let the copier package create
// the destination via the Rename option.
hostTarget := hostInfo.LinkTarget
if containerResolvedToParentDir && hostInfo.IsDir && filepath.Base(hostTarget) == "." {
hostTarget = filepath.Dir(hostTarget)
}
var stdinFile string
if isStdin {
if !containerInfo.IsDir {
return errors.New("destination must be a directory when copying from stdin")
}
// Copy from stdin to a temporary file *before* throwing it
// over the wire. This allows for proper client-side error
// reporting.
tmpFile, err := os.CreateTemp("", "")
if err != nil {
return err
}
defer os.Remove(tmpFile.Name())
_, err = io.Copy(tmpFile, os.Stdin)
if err != nil {
_ = tmpFile.Close()
return err
}
if err = tmpFile.Close(); err != nil {
return err
}
if !archive.IsArchivePath(tmpFile.Name()) {
return errors.New("source must be a (compressed) tar archive when copying from stdin")
}
stdinFile = tmpFile.Name()
}
if hostInfo.IsDir && !containerInfo.IsDir {
return errors.New("destination must be a directory when copying a directory")
}
reader, writer := io.Pipe()
hostCopy := func() error {
defer writer.Close()
if isStdin {
stream, err := os.Open(stdinFile)
if err != nil {
return err
}
defer stream.Close()
_, err = io.Copy(writer, stream)
return err
}
getOptions := buildahCopiah.GetOptions{
// Unless the specified path points to ".", we want to
// copy the base directory.
KeepDirectoryNames: hostInfo.IsDir && filepath.Base(hostTarget) != ".",
}
if (!hostInfo.IsDir && !containerInfo.IsDir) || containerResolvedToParentDir {
// If we're having a file-to-file copy, make sure to
// rename accordingly.
getOptions.Rename = map[string]string{filepath.Base(hostTarget): containerBaseName}
}
// On Windows, the root path needs to be <drive>:\, while otherwise
// it needs to be /. Combining filepath.VolumeName() + string(os.PathSeparator)
// gives us the correct path for the current OS.
if err := buildahCopiah.Get(filepath.VolumeName(hostTarget)+string(os.PathSeparator), "", getOptions, []string{hostTarget}, writer); err != nil {
return fmt.Errorf("copying from host: %w", err)
}
return nil
}
containerCopy := func() error {
defer reader.Close()
target := containerInfo.FileInfo.LinkTarget
if !containerInfo.IsDir {
target = path.Dir(target)
}
copyFunc, err := registry.ContainerEngine().ContainerCopyFromArchive(registry.Context(), container, target, reader, entities.CopyOptions{Chown: chown, NoOverwriteDirNonDir: !cpOpts.OverwriteDirNonDir})
if err != nil {
return err
}
if err := copyFunc(); err != nil {
return fmt.Errorf("copying to container: %w", err)
}
return nil
}
return doCopy(hostCopy, containerCopy)
}
// resolvePathOnDestinationContainer resolves the specified path on the
// container. If the path does not exist, it attempts to use the parent
// directory.
func resolvePathOnDestinationContainer(container string, containerPath string, isStdin bool) (baseName string, containerInfo *entities.ContainerStatReport, resolvedToParentDir bool, err error) {
containerInfo, err = registry.ContainerEngine().ContainerStat(registry.Context(), container, containerPath)
if err == nil {
baseName = path.Base(containerInfo.LinkTarget)
return //nolint: nilerr
}
if strings.HasSuffix(containerPath, "/") {
err = fmt.Errorf("%q could not be found on container %s: %w", containerPath, container, err)
return
}
if isStdin {
err = errors.New("destination must be a directory when copying from stdin")
return
}
// NOTE: containerInfo may actually be set. That happens when
// the container path is a symlink into nirvana. In that case,
// we must use the symlinked path instead.
parentPath := containerPath
if containerInfo != nil {
baseName = path.Base(containerInfo.LinkTarget)
parentPath = containerInfo.LinkTarget
} else {
baseName = path.Base(containerPath)
}
parentDir, err := containerParentDir(container, parentPath)
if err != nil {
err = fmt.Errorf("could not determine parent dir of %q on container %s: %w", parentPath, container, err)
return
}
containerInfo, err = registry.ContainerEngine().ContainerStat(registry.Context(), container, parentDir)
if err != nil {
err = fmt.Errorf("%q could not be found on container %s: %w", containerPath, container, err)
return
}
resolvedToParentDir = true
return baseName, containerInfo, resolvedToParentDir, nil
}
// containerParentDir returns the parent directory of the specified path on the
// container. If the path is relative, it will be resolved relative to the
// container's working directory (or "/" if the work dir isn't set).
func containerParentDir(container string, containerPath string) (string, error) {
// This is specifically a path in the (linux) container, so we need to intentionally use
// path instead of filepath to ensure we don't try to parse container paths using the
// host OS conventions.
if path.IsAbs(containerPath) {
return path.Dir(containerPath), nil
}
inspectData, _, err := registry.ContainerEngine().ContainerInspect(registry.Context(), []string{container}, entities.InspectOptions{})
if err != nil {
return "", err
}
if len(inspectData) != 1 {
return "", fmt.Errorf("inspecting container %q: expected 1 data item but got %d", container, len(inspectData))
}
workDir := path.Join("/", inspectData[0].Config.WorkingDir)
workDir = path.Join(workDir, containerPath)
return path.Dir(workDir), nil
}
// validateFileInfo returns an error if the specified FileInfo doesn't point to
// a directory or a regular file.
func validateFileInfo(info *copy.FileInfo) error {
if info.Mode.IsDir() || info.Mode.IsRegular() {
return nil
}
return fmt.Errorf("%q must be a directory or a regular file", info.LinkTarget)
}

View File

@ -0,0 +1,497 @@
package containers
import (
"context"
"errors"
"fmt"
"os"
"slices"
"strconv"
"strings"
"github.com/containers/buildah/pkg/cli"
"github.com/containers/common/pkg/auth"
"github.com/containers/common/pkg/config"
"github.com/containers/image/v5/transports/alltransports"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/utils"
"github.com/containers/podman/v5/libpod/define"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/specgen"
"github.com/containers/podman/v5/pkg/specgenutil"
"github.com/containers/podman/v5/pkg/util"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/term"
)
var (
createDescription = `Creates a new container from the given image or storage and prepares it for running the specified command.
The container ID is then printed to stdout. You can then start it at any time with the podman start <container_id> command. The container will be created with the initial state 'created'.`
createCommand = &cobra.Command{
Use: "create [options] IMAGE [COMMAND [ARG...]]",
Short: "Create but do not start a container",
Long: createDescription,
RunE: create,
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: common.AutocompleteCreateRun,
Example: `podman create alpine ls
podman create --annotation HELLO=WORLD alpine ls
podman create -t -i --name myctr alpine ls`,
}
containerCreateCommand = &cobra.Command{
Args: createCommand.Args,
Use: createCommand.Use,
Short: createCommand.Short,
Long: createCommand.Long,
RunE: createCommand.RunE,
ValidArgsFunction: createCommand.ValidArgsFunction,
Example: `podman container create alpine ls
podman container create --annotation HELLO=WORLD alpine ls
podman container create -t -i --name myctr alpine ls`,
}
)
var (
InitContainerType string
cliVals entities.ContainerCreateOptions
)
func createFlags(cmd *cobra.Command) {
flags := cmd.Flags()
initContainerFlagName := "init-ctr"
flags.StringVar(
&InitContainerType,
initContainerFlagName, "",
"Make this a pod init container.",
)
_ = cmd.RegisterFlagCompletionFunc(initContainerFlagName, common.AutocompleteInitCtr)
flags.SetInterspersed(false)
common.DefineCreateDefaults(&cliVals)
common.DefineCreateFlags(cmd, &cliVals, entities.CreateMode)
common.DefineNetFlags(cmd)
flags.SetNormalizeFunc(utils.AliasFlags)
if registry.IsRemote() {
if cliVals.IsInfra {
_ = flags.MarkHidden("infra-conmon-pidfile")
} else {
_ = flags.MarkHidden("conmon-pidfile")
}
_ = flags.MarkHidden("pidfile")
}
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: createCommand,
})
createFlags(createCommand)
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: containerCreateCommand,
Parent: containerCmd,
})
createFlags(containerCreateCommand)
}
func commonFlags(cmd *cobra.Command) error {
var err error
flags := cmd.Flags()
cliVals.Net, err = common.NetFlagsToNetOptions(nil, *flags)
if err != nil {
return err
}
if cmd.Flags().Changed("image-volume") {
cliVals.ImageVolume = cmd.Flag("image-volume").Value.String()
}
return nil
}
func create(cmd *cobra.Command, args []string) error {
if err := commonFlags(cmd); err != nil {
return err
}
// Check if initctr is used with --pod and the value is correct
if initctr := InitContainerType; cmd.Flags().Changed("init-ctr") {
if !cmd.Flags().Changed("pod") {
return errors.New("must specify pod value with init-ctr")
}
if !slices.Contains([]string{define.AlwaysInitContainer, define.OneShotInitContainer}, initctr) {
return fmt.Errorf("init-ctr value must be '%s' or '%s'", define.AlwaysInitContainer, define.OneShotInitContainer)
}
cliVals.InitContainerType = initctr
}
// TODO: v5.0 block users from setting restart policy for a container if the container is in a pod
cliVals, err := CreateInit(cmd, cliVals, false)
if err != nil {
return err
}
imageName := args[0]
rawImageName := ""
if !cliVals.RootFS {
rawImageName = args[0]
name, err := pullImage(cmd, args[0], &cliVals)
if err != nil {
return err
}
imageName = name
}
if cmd.Flags().Changed("authfile") {
if err := auth.CheckAuthFile(cliVals.Authfile); err != nil {
return err
}
}
s := specgen.NewSpecGenerator(imageName, cliVals.RootFS)
if err := specgenutil.FillOutSpecGen(s, &cliVals, args); err != nil {
return err
}
s.RawImageName = rawImageName
// Include the command used to create the container.
s.ContainerCreateCommand = os.Args
if err := createPodIfNecessary(cmd, s, cliVals.Net); err != nil {
return err
}
if cliVals.Replace {
if err := replaceContainer(cliVals.Name); err != nil {
return err
}
}
if s.HealthConfig == nil {
s.HealthConfig, err = common.GetHealthCheckOverrideConfig(cmd, &cliVals)
if err != nil {
return err
}
}
report, err := registry.ContainerEngine().ContainerCreate(registry.Context(), s)
if err != nil {
// if pod was created as part of run
// remove it in case ctr creation fails
if err := rmPodIfNecessary(cmd, s); err != nil {
if !errors.Is(err, define.ErrNoSuchPod) {
logrus.Error(err.Error())
}
}
return err
}
if cliVals.CIDFile != "" {
if err := util.CreateIDFile(cliVals.CIDFile, report.Id); err != nil {
return err
}
}
if cliVals.LogDriver != define.PassthroughLogging && cliVals.LogDriver != define.PassthroughTTYLogging {
fmt.Println(report.Id)
}
return nil
}
func replaceContainer(name string) error {
if len(name) == 0 {
return errors.New("cannot replace container without --name being set")
}
rmOptions := entities.RmOptions{
Force: true, // force stop & removal
Ignore: true, // ignore errors when a container doesn't exit
}
return removeContainers([]string{name}, rmOptions, false, true)
}
func createOrUpdateFlags(cmd *cobra.Command, vals *entities.ContainerCreateOptions) error {
if cmd.Flags().Changed("pids-limit") {
val := cmd.Flag("pids-limit").Value.String()
pidsLimit, err := strconv.ParseInt(val, 10, 32)
if err != nil {
return err
}
vals.PIDsLimit = &pidsLimit
}
return nil
}
func CreateInit(c *cobra.Command, vals entities.ContainerCreateOptions, isInfra bool) (entities.ContainerCreateOptions, error) {
if len(vals.UIDMap) > 0 || len(vals.GIDMap) > 0 || vals.SubUIDName != "" || vals.SubGIDName != "" {
if c.Flag("userns").Changed {
return vals, errors.New("--userns and --uidmap/--gidmap/--subuidname/--subgidname are mutually exclusive")
}
// force userns flag to "private"
vals.UserNS = "private"
} else {
vals.UserNS = c.Flag("userns").Value.String()
}
if c.Flag("kernel-memory") != nil && c.Flag("kernel-memory").Changed {
logrus.Warnf("The --kernel-memory flag is no longer supported. This flag is a noop.")
}
if cliVals.LogDriver == define.PassthroughLogging {
if term.IsTerminal(0) || term.IsTerminal(1) || term.IsTerminal(2) {
return vals, errors.New("the '--log-driver passthrough' option cannot be used on a TTY. If you really want it, use '--log-driver passthrough-tty'")
}
if registry.IsRemote() {
return vals, errors.New("the '--log-driver passthrough' option is not supported in remote mode")
}
}
if cliVals.LogDriver == define.PassthroughTTYLogging {
if registry.IsRemote() {
return vals, errors.New("the '--log-driver passthrough-tty' option is not supported in remote mode")
}
}
if !isInfra {
if c.Flag("cpu-period").Changed && c.Flag("cpus").Changed {
return vals, errors.New("--cpu-period and --cpus cannot be set together")
}
if c.Flag("cpu-quota").Changed && c.Flag("cpus").Changed {
return vals, errors.New("--cpu-quota and --cpus cannot be set together")
}
vals.IPC = c.Flag("ipc").Value.String()
vals.PID = c.Flag("pid").Value.String()
vals.CgroupNS = c.Flag("cgroupns").Value.String()
if c.Flags().Changed("group-add") {
groups := []string{}
for _, g := range cliVals.GroupAdd {
if g == "keep-groups" {
if len(cliVals.GroupAdd) > 1 {
return vals, errors.New("the '--group-add keep-groups' option is not allowed with any other --group-add options")
}
if registry.IsRemote() {
return vals, errors.New("the '--group-add keep-groups' option is not supported in remote mode")
}
vals.Annotation = append(vals.Annotation, fmt.Sprintf("%s=1", define.RunOCIKeepOriginalGroups))
} else {
groups = append(groups, g)
}
}
vals.GroupAdd = groups
}
if c.Flags().Changed("oom-score-adj") {
val, err := c.Flags().GetInt("oom-score-adj")
if err != nil {
return vals, err
}
vals.OOMScoreAdj = &val
}
if err := createOrUpdateFlags(c, &vals); err != nil {
return vals, err
}
if c.Flags().Changed("env") {
env, err := c.Flags().GetStringArray("env")
if err != nil {
return vals, fmt.Errorf("retrieve env flag: %w", err)
}
vals.Env = env
}
if c.Flag("cgroups").Changed && vals.CgroupsMode == "split" && registry.IsRemote() {
return vals, fmt.Errorf("the option --cgroups=%q is not supported in remote mode", vals.CgroupsMode)
}
if c.Flag("pod").Changed && !strings.HasPrefix(c.Flag("pod").Value.String(), "new:") && c.Flag("userns").Changed {
return vals, errors.New("--userns and --pod cannot be set together")
}
}
if c.Flag("shm-size").Changed {
vals.ShmSize = c.Flag("shm-size").Value.String()
}
if c.Flag("shm-size-systemd").Changed {
vals.ShmSizeSystemd = c.Flag("shm-size-systemd").Value.String()
}
if (c.Flag("dns").Changed || c.Flag("dns-option").Changed || c.Flag("dns-search").Changed) && vals.Net != nil && (vals.Net.Network.NSMode == specgen.NoNetwork || vals.Net.Network.IsContainer()) {
return vals, errors.New("conflicting options: dns and the network mode: " + string(vals.Net.Network.NSMode))
}
noHosts, err := c.Flags().GetBool("no-hosts")
if err != nil {
return vals, err
}
if noHosts && c.Flag("add-host").Changed {
return vals, errors.New("--no-hosts and --add-host cannot be set together")
}
if noHosts && c.Flag("hosts-file").Changed {
return vals, errors.New("--no-hosts and --hosts-file cannot be set together")
}
if !isInfra && c.Flag("entrypoint").Changed {
val := c.Flag("entrypoint").Value.String()
vals.Entrypoint = &val
}
// Docker-compatibility: the "-h" flag for run/create is reserved for
// the hostname (see https://github.com/containers/podman/issues/1367).
return vals, nil
}
// Pulls image if any also parses and populates OS, Arch and Variant in specified container create options
func pullImage(cmd *cobra.Command, imageName string, cliVals *entities.ContainerCreateOptions) (string, error) {
pullPolicy, err := config.ParsePullPolicy(cliVals.Pull)
if err != nil {
return "", err
}
if cliVals.Platform != "" || cliVals.Arch != "" || cliVals.OS != "" {
if cliVals.Platform != "" {
if cliVals.Arch != "" || cliVals.OS != "" {
return "", errors.New("--platform option can not be specified with --arch or --os")
}
OS, Arch, hasArch := strings.Cut(cliVals.Platform, "/")
cliVals.OS = OS
if hasArch {
cliVals.Arch = Arch
}
}
}
skipTLSVerify := types.OptionalBoolUndefined
if cliVals.TLSVerify.Present() {
skipTLSVerify = types.NewOptionalBool(!cliVals.TLSVerify.Value())
}
decConfig, err := cli.DecryptConfig(cliVals.DecryptionKeys)
if err != nil {
return "unable to obtain decryption config", err
}
pullOptions := entities.ImagePullOptions{
Authfile: cliVals.Authfile,
Quiet: cliVals.Quiet,
Arch: cliVals.Arch,
OS: cliVals.OS,
Variant: cliVals.Variant,
SignaturePolicy: cliVals.SignaturePolicy,
PullPolicy: pullPolicy,
SkipTLSVerify: skipTLSVerify,
OciDecryptConfig: decConfig,
}
if cmd.Flags().Changed("retry") {
retry, err := cmd.Flags().GetUint("retry")
if err != nil {
return "", err
}
pullOptions.Retry = &retry
}
if cmd.Flags().Changed("retry-delay") {
val, err := cmd.Flags().GetString("retry-delay")
if err != nil {
return "", err
}
pullOptions.RetryDelay = val
}
pullReport, pullErr := registry.ImageEngine().Pull(registry.Context(), imageName, pullOptions)
if pullErr != nil {
return "", pullErr
}
// Return the input name such that the image resolves to correct
// repo/tag in the backend (see #8082). Unless we're referring to
// the image via a transport.
if _, err := alltransports.ParseImageName(imageName); err == nil {
imageName = pullReport.Images[0]
}
return imageName, nil
}
func rmPodIfNecessary(cmd *cobra.Command, s *specgen.SpecGenerator) error {
if !strings.HasPrefix(cmd.Flag("pod").Value.String(), "new:") {
return nil
}
// errcheck not necessary since
// pod creation would've failed
podName := strings.Replace(s.Pod, "new:", "", 1)
_, err := registry.ContainerEngine().PodRm(context.Background(), []string{podName}, entities.PodRmOptions{})
return err
}
// createPodIfNecessary automatically creates a pod when requested. if the pod name
// has the form new:ID, the pod ID is created and the name in the spec generator is replaced
// with ID.
func createPodIfNecessary(cmd *cobra.Command, s *specgen.SpecGenerator, netOpts *entities.NetOptions) error {
if !strings.HasPrefix(s.Pod, "new:") {
return nil
}
podName := strings.Replace(s.Pod, "new:", "", 1)
if len(podName) < 1 {
return errors.New("new pod name must be at least one character")
}
var err error
uns := specgen.Namespace{NSMode: specgen.Default}
if cliVals.UserNS != "" {
uns, err = specgen.ParseUserNamespace(cliVals.UserNS)
if err != nil {
return err
}
}
createOptions := entities.PodCreateOptions{
Name: podName,
Infra: true,
Net: netOpts,
CreateCommand: os.Args,
Hostname: s.ContainerBasicConfig.Hostname,
Cpus: cliVals.CPUS,
CpusetCpus: cliVals.CPUSetCPUs,
Pid: cliVals.PID,
Userns: uns,
Restart: cliVals.Restart,
}
// Unset config values we passed to the pod to prevent them being used twice for the container and pod.
s.ContainerBasicConfig.Hostname = ""
s.ContainerNetworkConfig = specgen.ContainerNetworkConfig{}
s.Pod = podName
podSpec := entities.PodSpec{}
podGen := specgen.NewPodSpecGenerator()
podSpec.PodSpecGen = *podGen
podGen, err = entities.ToPodSpecGen(podSpec.PodSpecGen, &createOptions)
if err != nil {
return err
}
infraOpts := entities.NewInfraContainerCreateOptions()
infraOpts.Net = netOpts
infraOpts.Quiet = true
infraOpts.ReadOnly = true
infraOpts.ReadWriteTmpFS = false
infraOpts.Hostname, err = cmd.Flags().GetString("hostname")
if err != nil {
return err
}
podGen.InfraContainerSpec = specgen.NewSpecGenerator("", false)
podGen.InfraContainerSpec.NetworkOptions = podGen.NetworkOptions
err = specgenutil.FillOutSpecGen(podGen.InfraContainerSpec, &infraOpts, []string{})
if err != nil {
return err
}
podSpec.PodSpecGen = *podGen
_, err = registry.ContainerEngine().PodCreate(context.Background(), podSpec)
return err
}

View File

@ -0,0 +1,52 @@
package containers
import (
"errors"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/diff"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/libpod/define"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
)
var (
// podman container _diff_
diffCmd = &cobra.Command{
Use: "diff [options] CONTAINER [CONTAINER]",
Args: diff.ValidateContainerDiffArgs,
Short: "Inspect changes to the container's file systems",
Long: `Displays changes to the container filesystem's'. The container will be compared to its parent layer or the second argument when given.`,
RunE: diffRun,
ValidArgsFunction: common.AutocompleteContainers,
Example: `podman container diff myCtr
podman container diff -l --format json myCtr`,
}
diffOpts *entities.DiffOptions
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: diffCmd,
Parent: containerCmd,
})
diffOpts = new(entities.DiffOptions)
flags := diffCmd.Flags()
formatFlagName := "format"
flags.StringVar(&diffOpts.Format, formatFlagName, "", "Change the output format (json)")
_ = diffCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(nil))
validate.AddLatestFlag(diffCmd, &diffOpts.Latest)
}
func diffRun(cmd *cobra.Command, args []string) error {
if len(args) == 0 && !diffOpts.Latest {
return errors.New("container must be specified: podman container diff [options [...]] ID-NAME")
}
diffOpts.Type = define.DiffContainer
return diff.Diff(cmd, args, *diffOpts)
}

View File

@ -0,0 +1,250 @@
package containers
import (
"bufio"
"context"
"errors"
"fmt"
"os"
"strings"
"time"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/libpod/define"
"github.com/containers/podman/v5/pkg/domain/entities"
envLib "github.com/containers/podman/v5/pkg/env"
"github.com/containers/podman/v5/pkg/rootless"
"github.com/spf13/cobra"
)
var (
execDescription = `Execute the specified command inside a running container.
`
execCommand = &cobra.Command{
Use: "exec [options] CONTAINER COMMAND [ARG...]",
Short: "Run a process in a running container",
Long: execDescription,
RunE: exec,
ValidArgsFunction: common.AutocompleteExecCommand,
Example: `podman exec -it ctrID ls
podman exec -it -w /tmp myCtr pwd
podman exec --user root ctrID ls`,
}
containerExecCommand = &cobra.Command{
Use: execCommand.Use,
Short: execCommand.Short,
Long: execCommand.Long,
RunE: execCommand.RunE,
ValidArgsFunction: execCommand.ValidArgsFunction,
Example: `podman container exec -it ctrID ls
podman container exec -it -w /tmp myCtr pwd
podman container exec --user root ctrID ls`,
}
)
var (
envInput, envFile []string
execOpts entities.ExecOptions
execDetach bool
execCidFile string
)
func execFlags(cmd *cobra.Command) {
podmanConfig := registry.PodmanConfig()
flags := cmd.Flags()
flags.SetInterspersed(false)
flags.BoolVarP(&execDetach, "detach", "d", false, "Run the exec session in detached mode (backgrounded)")
detachKeysFlagName := "detach-keys"
flags.StringVar(&execOpts.DetachKeys, detachKeysFlagName, containerConfig.DetachKeys(), "Select the key sequence for detaching a container. Format is a single character [a-Z] or ctrl-<value> where <value> is one of: a-z, @, ^, [, , or _")
_ = cmd.RegisterFlagCompletionFunc(detachKeysFlagName, common.AutocompleteDetachKeys)
cidfileFlagName := "cidfile"
flags.StringVar(&execCidFile, cidfileFlagName, "", "File to read the container ID from")
_ = cmd.RegisterFlagCompletionFunc(cidfileFlagName, completion.AutocompleteDefault)
envFlagName := "env"
flags.StringArrayVarP(&envInput, envFlagName, "e", []string{}, "Set environment variables")
_ = cmd.RegisterFlagCompletionFunc(envFlagName, completion.AutocompleteNone)
envFileFlagName := "env-file"
flags.StringArrayVar(&envFile, envFileFlagName, []string{}, "Read in a file of environment variables")
_ = cmd.RegisterFlagCompletionFunc(envFileFlagName, completion.AutocompleteDefault)
flags.BoolVarP(&execOpts.Interactive, "interactive", "i", false, "Make STDIN available to the contained process")
flags.BoolVar(&execOpts.Privileged, "privileged", podmanConfig.ContainersConfDefaultsRO.Containers.Privileged, "Give the process extended Linux capabilities inside the container. The default is false")
flags.BoolVarP(&execOpts.Tty, "tty", "t", false, "Allocate a pseudo-TTY. The default is false")
userFlagName := "user"
flags.StringVarP(&execOpts.User, userFlagName, "u", "", "Sets the username or UID used and optionally the groupname or GID for the specified command")
_ = cmd.RegisterFlagCompletionFunc(userFlagName, common.AutocompleteUserFlag)
preserveFdsFlagName := "preserve-fds"
flags.UintVar(&execOpts.PreserveFDs, preserveFdsFlagName, 0, "Pass N additional file descriptors to the container")
_ = cmd.RegisterFlagCompletionFunc(preserveFdsFlagName, completion.AutocompleteNone)
preserveFdFlagName := "preserve-fd"
flags.UintSliceVar(&execOpts.PreserveFD, preserveFdFlagName, nil, "Pass a list of additional file descriptors to the container")
_ = cmd.RegisterFlagCompletionFunc(preserveFdFlagName, completion.AutocompleteNone)
workdirFlagName := "workdir"
flags.StringVarP(&execOpts.WorkDir, workdirFlagName, "w", "", "Working directory inside the container")
_ = cmd.RegisterFlagCompletionFunc(workdirFlagName, completion.AutocompleteDefault)
waitFlagName := "wait"
flags.Int32(waitFlagName, 0, "Total seconds to wait for container to start")
_ = flags.MarkHidden(waitFlagName)
if registry.IsRemote() {
_ = flags.MarkHidden("preserve-fds")
}
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: execCommand,
})
execFlags(execCommand)
validate.AddLatestFlag(execCommand, &execOpts.Latest)
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: containerExecCommand,
Parent: containerCmd,
})
execFlags(containerExecCommand)
validate.AddLatestFlag(containerExecCommand, &execOpts.Latest)
}
func exec(cmd *cobra.Command, args []string) error {
nameOrID, command, err := determineTargetCtrAndCmd(args, execOpts.Latest, execCidFile != "")
if err != nil {
return err
}
execOpts.Cmd = command
// Validate given environment variables
execOpts.Envs = make(map[string]string)
for _, f := range envFile {
fileEnv, err := envLib.ParseFile(f)
if err != nil {
return err
}
execOpts.Envs = envLib.Join(execOpts.Envs, fileEnv)
}
cliEnv, err := envLib.ParseSlice(envInput)
if err != nil {
return fmt.Errorf("parsing environment variables: %w", err)
}
execOpts.Envs = envLib.Join(execOpts.Envs, cliEnv)
for _, fd := range execOpts.PreserveFD {
if !rootless.IsFdInherited(int(fd)) {
return fmt.Errorf("file descriptor %d is not available - the preserve-fd option requires that file descriptors must be passed", fd)
}
}
for fd := 3; fd < int(3+execOpts.PreserveFDs); fd++ {
if !rootless.IsFdInherited(fd) {
return fmt.Errorf("file descriptor %d is not available - the preserve-fds option requires that file descriptors must be passed", fd)
}
}
if cmd.Flags().Changed("wait") {
seconds, err := cmd.Flags().GetInt32("wait")
if err != nil {
return err
}
if err := execWait(nameOrID, seconds); err != nil {
if errors.Is(err, define.ErrCanceled) {
return fmt.Errorf("timed out waiting for container: %s", nameOrID)
}
}
}
if !execDetach {
streams := define.AttachStreams{}
streams.OutputStream = os.Stdout
streams.ErrorStream = os.Stderr
if execOpts.Interactive {
streams.InputStream = bufio.NewReader(os.Stdin)
streams.AttachInput = true
}
streams.AttachOutput = true
streams.AttachError = true
exitCode, err := registry.ContainerEngine().ContainerExec(registry.Context(), nameOrID, execOpts, streams)
registry.SetExitCode(exitCode)
return err
}
id, err := registry.ContainerEngine().ContainerExecDetached(registry.Context(), nameOrID, execOpts)
if err != nil {
return err
}
fmt.Println(id)
return nil
}
// determineTargetCtrAndCmd determines which command exec should run in which container
func determineTargetCtrAndCmd(args []string, latestSpecified bool, execCidFileProvided bool) (string, []string, error) {
var nameOrID string
var command []string
if len(args) == 0 && !latestSpecified && !execCidFileProvided {
return "", nil, errors.New("exec requires the name or ID of a container or the --latest or --cidfile flag")
} else if latestSpecified && execCidFileProvided {
return "", nil, errors.New("--latest and --cidfile can not be used together")
}
command = args
if !latestSpecified {
if !execCidFileProvided {
// assume first arg to be name or ID
command = args[1:]
nameOrID = strings.TrimPrefix(args[0], "/")
} else {
content, err := os.ReadFile(execCidFile)
if err != nil {
return "", nil, fmt.Errorf("reading CIDFile: %w", err)
}
nameOrID = strings.Split(string(content), "\n")[0]
}
}
return nameOrID, command, nil
}
func execWait(ctr string, seconds int32) error {
maxDuration := time.Duration(seconds) * time.Second
interval := 100 * time.Millisecond
ctx, cancel := context.WithTimeout(registry.Context(), maxDuration)
defer cancel()
waitOptions.Conditions = []string{define.ContainerStateRunning.String()}
startTime := time.Now()
for time.Since(startTime) < maxDuration {
_, err := registry.ContainerEngine().ContainerWait(ctx, []string{ctr}, waitOptions)
if err == nil {
return nil
}
if !errors.Is(err, define.ErrNoSuchCtr) {
return err
}
interval *= 2
since := time.Since(startTime)
if since+interval > maxDuration {
interval = maxDuration - since
}
time.Sleep(interval)
}
return define.ErrCanceled
}

View File

@ -0,0 +1,53 @@
package containers
import (
"context"
"strings"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
)
var (
containerExistsDescription = `If the named container exists in local storage, podman container exists exits with 0, otherwise the exit code will be 1.`
existsCommand = &cobra.Command{
Use: "exists [options] CONTAINER",
Short: "Check if a container exists in local storage",
Long: containerExistsDescription,
Example: `podman container exists --external containerID
podman container exists myctr || podman run --name myctr [etc...]`,
RunE: exists,
Args: cobra.ExactArgs(1),
ValidArgsFunction: common.AutocompleteContainers,
}
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: existsCommand,
Parent: containerCmd,
})
flags := existsCommand.Flags()
flags.Bool("external", false, "Check external storage containers as well as Podman containers")
}
func exists(cmd *cobra.Command, args []string) error {
external, err := cmd.Flags().GetBool("external")
if err != nil {
return err
}
options := entities.ContainerExistsOptions{
External: external,
}
response, err := registry.ContainerEngine().ContainerExists(context.Background(), strings.TrimPrefix(args[0], "/"), options)
if err != nil {
return err
}
if !response.Value {
registry.SetExitCode(1)
}
return nil
}

View File

@ -0,0 +1,92 @@
package containers
import (
"context"
"errors"
"os"
"strings"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/parse"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
"golang.org/x/term"
)
var (
exportDescription = "Exports container's filesystem contents as a tar archive" +
" and saves it on the local machine."
exportCommand = &cobra.Command{
Use: "export [options] CONTAINER",
Short: "Export container's filesystem contents as a tar archive",
Long: exportDescription,
RunE: export,
Args: cobra.ExactArgs(1),
ValidArgsFunction: common.AutocompleteContainers,
Example: `podman export ctrID > myCtr.tar
podman export --output="myCtr.tar" ctrID`,
}
containerExportCommand = &cobra.Command{
Args: cobra.ExactArgs(1),
Use: exportCommand.Use,
Short: exportCommand.Short,
Long: exportCommand.Long,
RunE: exportCommand.RunE,
ValidArgsFunction: exportCommand.ValidArgsFunction,
Example: `podman container export ctrID > myCtr.tar
podman container export --output="myCtr.tar" ctrID`,
}
)
var (
exportOpts entities.ContainerExportOptions
outputFile string
)
func exportFlags(cmd *cobra.Command) {
flags := cmd.Flags()
outputFlagName := "output"
flags.StringVarP(&outputFile, outputFlagName, "o", "", "Write to a specified file (default: stdout, which must be redirected)")
_ = cmd.RegisterFlagCompletionFunc(outputFlagName, completion.AutocompleteDefault)
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: exportCommand,
})
exportFlags(exportCommand)
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: containerExportCommand,
Parent: containerCmd,
})
exportFlags(containerExportCommand)
}
func export(cmd *cobra.Command, args []string) error {
if len(outputFile) == 0 {
file := os.Stdout
if term.IsTerminal(int(file.Fd())) {
return errors.New("refusing to export to terminal. Use -o flag or redirect")
}
exportOpts.Output = file
} else {
if err := parse.ValidateFileName(outputFile); err != nil {
return err
}
// open file here with O_WRONLY since on MacOS it can fail to open /dev/stderr in read mode for example
// https://github.com/containers/podman/issues/16870
file, err := os.OpenFile(outputFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer file.Close()
exportOpts.Output = file
}
return registry.ContainerEngine().ContainerExport(context.Background(), strings.TrimPrefix(args[0], "/"), exportOpts)
}

View File

@ -0,0 +1,86 @@
package containers
import (
"fmt"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/utils"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
var (
initDescription = `Initialize one or more containers, creating the OCI spec and mounts for inspection. Container names or IDs can be used.`
initCommand = &cobra.Command{
Use: "init [options] CONTAINER [CONTAINER...]",
Short: "Initialize one or more containers",
Long: initDescription,
RunE: initContainer,
Args: func(cmd *cobra.Command, args []string) error {
return validate.CheckAllLatestAndIDFile(cmd, args, false, "")
},
ValidArgsFunction: common.AutocompleteContainersCreated,
Example: `podman init 3c45ef19d893
podman init test1`,
}
containerInitCommand = &cobra.Command{
Use: initCommand.Use,
Short: initCommand.Short,
Long: initCommand.Long,
RunE: initCommand.RunE,
Args: initCommand.Args,
ValidArgsFunction: initCommand.ValidArgsFunction,
Example: `podman container init 3c45ef19d893
podman container init test1`,
}
)
var (
initOptions entities.ContainerInitOptions
)
func initFlags(flags *pflag.FlagSet) {
flags.BoolVarP(&initOptions.All, "all", "a", false, "Initialize all containers")
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: initCommand,
})
flags := initCommand.Flags()
initFlags(flags)
validate.AddLatestFlag(initCommand, &initOptions.Latest)
registry.Commands = append(registry.Commands, registry.CliCommand{
Parent: containerCmd,
Command: containerInitCommand,
})
containerInitFlags := containerInitCommand.Flags()
initFlags(containerInitFlags)
validate.AddLatestFlag(containerInitCommand, &initOptions.Latest)
}
func initContainer(cmd *cobra.Command, args []string) error {
var errs utils.OutputErrors
args = utils.RemoveSlash(args)
report, err := registry.ContainerEngine().ContainerInit(registry.Context(), args, initOptions)
if err != nil {
return err
}
for _, r := range report {
switch {
case r.Err != nil:
errs = append(errs, r.Err)
case r.RawInput != "":
fmt.Println(r.RawInput)
default:
fmt.Println(r.Id)
}
}
return errs.PrintErrors()
}

View File

@ -0,0 +1,47 @@
package containers
import (
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/inspect"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/libpod/define"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
)
var (
// podman container _inspect_
inspectCmd = &cobra.Command{
Use: "inspect [options] CONTAINER [CONTAINER...]",
Short: "Display the configuration of a container",
Long: `Displays the low-level information on a container identified by name or ID.`,
RunE: inspectExec,
ValidArgsFunction: common.AutocompleteContainers,
Example: `podman container inspect myCtr
podman container inspect -l --format '{{.Id}} {{.Config.Labels}}'`,
}
inspectOpts *entities.InspectOptions
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: inspectCmd,
Parent: containerCmd,
})
inspectOpts = new(entities.InspectOptions)
flags := inspectCmd.Flags()
flags.BoolVarP(&inspectOpts.Size, "size", "s", false, "Display total file size")
formatFlagName := "format"
flags.StringVarP(&inspectOpts.Format, formatFlagName, "f", "json", "Format the output to a Go template or json")
_ = inspectCmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(&define.InspectContainerData{}))
validate.AddLatestFlag(inspectCmd, &inspectOpts.Latest)
}
func inspectExec(cmd *cobra.Command, args []string) error {
// Force container type
inspectOpts.Type = common.ContainerType
return inspect.Inspect(args, *inspectOpts)
}

View File

@ -0,0 +1,123 @@
package containers
import (
"context"
"errors"
"fmt"
"os"
"strings"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/utils"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/signal"
"github.com/spf13/cobra"
)
var (
killDescription = "The main process inside each container specified will be sent SIGKILL, or any signal specified with option --signal."
killCommand = &cobra.Command{
Use: "kill [options] CONTAINER [CONTAINER...]",
Short: "Kill one or more running containers with a specific signal",
Long: killDescription,
RunE: kill,
Args: func(cmd *cobra.Command, args []string) error {
return validate.CheckAllLatestAndIDFile(cmd, args, false, "cidfile")
},
ValidArgsFunction: common.AutocompleteContainersRunning,
Example: `podman kill mywebserver
podman kill 860a4b23
podman kill --signal TERM ctrID`,
}
containerKillCommand = &cobra.Command{
Args: func(cmd *cobra.Command, args []string) error {
return validate.CheckAllLatestAndIDFile(cmd, args, false, "cidfile")
},
Use: killCommand.Use,
Short: killCommand.Short,
Long: killCommand.Long,
RunE: killCommand.RunE,
ValidArgsFunction: killCommand.ValidArgsFunction,
Example: `podman container kill mywebserver
podman container kill 860a4b23
podman container kill --signal TERM ctrID`,
}
)
var (
killOptions = entities.KillOptions{}
killCidFiles = []string{}
)
func killFlags(cmd *cobra.Command) {
flags := cmd.Flags()
flags.BoolVarP(&killOptions.All, "all", "a", false, "Signal all running containers")
signalFlagName := "signal"
flags.StringVarP(&killOptions.Signal, signalFlagName, "s", "KILL", "Signal to send to the container")
_ = cmd.RegisterFlagCompletionFunc(signalFlagName, common.AutocompleteStopSignal)
cidfileFlagName := "cidfile"
flags.StringArrayVar(&killCidFiles, cidfileFlagName, nil, "Read the container ID from the file")
_ = cmd.RegisterFlagCompletionFunc(cidfileFlagName, completion.AutocompleteDefault)
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: killCommand,
})
killFlags(killCommand)
validate.AddLatestFlag(killCommand, &killOptions.Latest)
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: containerKillCommand,
Parent: containerCmd,
})
killFlags(containerKillCommand)
validate.AddLatestFlag(containerKillCommand, &killOptions.Latest)
}
func kill(_ *cobra.Command, args []string) error {
var (
err error
errs utils.OutputErrors
)
args = utils.RemoveSlash(args)
// Check if the signalString provided by the user is valid
// Invalid signals will return err
sig, err := signal.ParseSignalNameOrNumber(killOptions.Signal)
if err != nil {
return err
}
if sig < 1 || sig > 64 {
return errors.New("valid signals are 1 through 64")
}
for _, cidFile := range killCidFiles {
content, err := os.ReadFile(cidFile)
if err != nil {
return fmt.Errorf("reading CIDFile: %w", err)
}
id := strings.Split(string(content), "\n")[0]
args = append(args, id)
}
responses, err := registry.ContainerEngine().ContainerKill(context.Background(), args, killOptions)
if err != nil {
return err
}
for _, r := range responses {
switch {
case r.Err != nil:
errs = append(errs, r.Err)
case r.RawInput != "":
fmt.Println(r.RawInput)
default:
fmt.Println(r.Id)
}
}
return errs.PrintErrors()
}

View File

@ -0,0 +1,33 @@
package containers
import (
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/spf13/cobra"
)
var (
// podman container _list_
listCmd = &cobra.Command{
Use: "list [options]",
Aliases: []string{"ls"},
Args: validate.NoArgs,
Short: "List containers",
Long: "Prints out information about the containers",
RunE: ps,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman container list -a
podman container list -a --format "{{.ID}} {{.Image}} {{.Labels}} {{.Mounts}}"
podman container list --size --sort names`,
}
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: listCmd,
Parent: containerCmd,
})
listFlagSet(listCmd)
validate.AddLatestFlag(listCmd, &listOpts.Latest)
}

View File

@ -0,0 +1,145 @@
package containers
import (
"errors"
"fmt"
"os"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/utils"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/util"
"github.com/spf13/cobra"
)
// logsOptionsWrapper wraps entities.LogsOptions and prevents leaking
// CLI-only fields into the API types.
type logsOptionsWrapper struct {
entities.ContainerLogsOptions
SinceRaw string
UntilRaw string
}
var (
logsOptions logsOptionsWrapper
logsDescription = `Retrieves logs for one or more containers.
This does not guarantee execution order when combined with podman run (i.e., your run may not have generated any logs at the time you execute podman logs).
`
logsCommand = &cobra.Command{
Use: "logs [options] CONTAINER [CONTAINER...]",
Short: "Fetch the logs of one or more containers",
Long: logsDescription,
Args: func(cmd *cobra.Command, args []string) error {
switch {
case registry.IsRemote() && logsOptions.Latest:
return errors.New(cmd.Name() + " does not support 'latest' when run remotely")
case registry.IsRemote() && len(args) > 1:
return errors.New(cmd.Name() + " does not support multiple containers when run remotely")
case logsOptions.Latest && len(args) > 0:
return errors.New("--latest and containers cannot be used together")
case !logsOptions.Latest && len(args) < 1:
return errors.New("specify at least one container name or ID to log")
}
return nil
},
RunE: logs,
ValidArgsFunction: common.AutocompleteContainers,
Example: `podman logs ctrID
podman logs --names ctrID1 ctrID2
podman logs --tail 2 mywebserver
podman logs --follow=true --since 10m ctrID
podman logs mywebserver mydbserver`,
}
containerLogsCommand = &cobra.Command{
Use: logsCommand.Use,
Short: logsCommand.Short,
Long: logsCommand.Long,
Args: logsCommand.Args,
RunE: logsCommand.RunE,
ValidArgsFunction: logsCommand.ValidArgsFunction,
Example: `podman container logs ctrID
podman container logs --names ctrID1 ctrID2
podman container logs --color --names ctrID1 ctrID2
podman container logs --tail 2 mywebserver
podman container logs --follow=true --since 10m ctrID
podman container logs mywebserver mydbserver`,
}
)
func init() {
// if run remotely we only allow one container arg
if registry.IsRemote() {
logsCommand.Use = "logs [options] CONTAINER"
containerLogsCommand.Use = logsCommand.Use
}
// logs
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: logsCommand,
})
logsFlags(logsCommand)
validate.AddLatestFlag(logsCommand, &logsOptions.Latest)
// container logs
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: containerLogsCommand,
Parent: containerCmd,
})
logsFlags(containerLogsCommand)
validate.AddLatestFlag(containerLogsCommand, &logsOptions.Latest)
}
func logsFlags(cmd *cobra.Command) {
flags := cmd.Flags()
flags.BoolVar(&logsOptions.Details, "details", false, "Show extra details provided to the logs")
flags.BoolVarP(&logsOptions.Follow, "follow", "f", false, "Follow log output. The default is false")
sinceFlagName := "since"
flags.StringVar(&logsOptions.SinceRaw, sinceFlagName, "", "Show logs since TIMESTAMP")
_ = cmd.RegisterFlagCompletionFunc(sinceFlagName, completion.AutocompleteNone)
untilFlagName := "until"
flags.StringVar(&logsOptions.UntilRaw, untilFlagName, "", "Show logs until TIMESTAMP")
_ = cmd.RegisterFlagCompletionFunc(untilFlagName, completion.AutocompleteNone)
tailFlagName := "tail"
flags.Int64Var(&logsOptions.Tail, tailFlagName, -1, "Output the specified number of LINES at the end of the logs. Defaults to -1, which prints all lines")
_ = cmd.RegisterFlagCompletionFunc(tailFlagName, completion.AutocompleteNone)
flags.BoolVarP(&logsOptions.Timestamps, "timestamps", "t", false, "Output the timestamps in the log")
flags.BoolVarP(&logsOptions.Colors, "color", "", false, "Output the containers with different colors in the log.")
flags.BoolVarP(&logsOptions.Names, "names", "n", false, "Output the container name in the log")
_ = flags.MarkHidden("details")
}
func logs(_ *cobra.Command, args []string) error {
args = utils.RemoveSlash(args)
if logsOptions.SinceRaw != "" {
// parse time, error out if something is wrong
since, err := util.ParseInputTime(logsOptions.SinceRaw, true)
if err != nil {
return fmt.Errorf("parsing --since %q: %w", logsOptions.SinceRaw, err)
}
logsOptions.Since = since
}
if logsOptions.UntilRaw != "" {
// parse time, error out if something is wrong
until, err := util.ParseInputTime(logsOptions.UntilRaw, false)
if err != nil {
return fmt.Errorf("parsing --until %q: %w", logsOptions.UntilRaw, err)
}
logsOptions.Until = until
}
logsOptions.StdoutWriter = os.Stdout
logsOptions.StderrWriter = os.Stderr
return registry.ContainerEngine().ContainerLogs(registry.Context(), args, logsOptions.ContainerLogsOptions)
}

View File

@ -0,0 +1,164 @@
package containers
import (
"errors"
"fmt"
"os"
"github.com/containers/common/pkg/report"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/utils"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
)
var (
mountDescription = `podman mount
Lists all mounted containers mount points if no container is specified
podman mount CONTAINER-NAME-OR-ID
Mounts the specified container and outputs the mountpoint
`
mountCommand = &cobra.Command{
Annotations: map[string]string{
registry.UnshareNSRequired: "",
registry.ParentNSRequired: "",
registry.EngineMode: registry.ABIMode,
},
Use: "mount [options] [CONTAINER...]",
Short: "Mount a working container's root filesystem",
Long: mountDescription,
RunE: mount,
Args: func(cmd *cobra.Command, args []string) error {
return validate.CheckAllLatestAndIDFile(cmd, args, true, "")
},
ValidArgsFunction: common.AutocompleteContainers,
}
containerMountCommand = &cobra.Command{
Annotations: mountCommand.Annotations,
Use: mountCommand.Use,
Short: mountCommand.Short,
Long: mountCommand.Long,
RunE: mountCommand.RunE,
Args: mountCommand.Args,
ValidArgsFunction: mountCommand.ValidArgsFunction,
}
)
var (
mountOpts entities.ContainerMountOptions
)
func mountFlags(cmd *cobra.Command) {
flags := cmd.Flags()
flags.BoolVarP(&mountOpts.All, "all", "a", false, "Mount all containers")
formatFlagName := "format"
flags.StringVar(&mountOpts.Format, formatFlagName, "", "Print the mounted containers in specified format (json)")
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(nil))
flags.BoolVar(&mountOpts.NoTruncate, "no-trunc", false, "Do not truncate output")
flags.SetNormalizeFunc(utils.AliasFlags)
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: mountCommand,
})
mountFlags(mountCommand)
validate.AddLatestFlag(mountCommand, &mountOpts.Latest)
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: containerMountCommand,
Parent: containerCmd,
})
mountFlags(containerMountCommand)
validate.AddLatestFlag(containerMountCommand, &mountOpts.Latest)
}
func mount(cmd *cobra.Command, args []string) error {
if len(args) > 0 && mountOpts.Latest {
return errors.New("--latest and containers cannot be used together")
}
args = utils.RemoveSlash(args)
reports, err := registry.ContainerEngine().ContainerMount(registry.Context(), args, mountOpts)
if err != nil {
return err
}
if len(args) > 0 || mountOpts.Latest || mountOpts.All {
var errs utils.OutputErrors
for _, r := range reports {
if r.Err == nil {
fmt.Println(r.Path)
continue
}
errs = append(errs, r.Err)
}
return errs.PrintErrors()
}
switch {
case report.IsJSON(mountOpts.Format):
return printJSON(reports)
case mountOpts.Format == "":
break // print defaults
default:
return fmt.Errorf("unknown --format argument: %q", mountOpts.Format)
}
mrs := make([]mountReporter, 0, len(reports))
for _, r := range reports {
mrs = append(mrs, mountReporter{r})
}
rpt := report.New(os.Stdout, cmd.Name())
defer rpt.Flush()
rpt, err = rpt.Parse(report.OriginPodman, "{{range . }}{{.ID}}\t{{.Path}}\n{{end -}}")
if err != nil {
return err
}
return rpt.Execute(mrs)
}
func printJSON(reports []*entities.ContainerMountReport) error {
type jreport struct {
ID string `json:"id"`
Names []string
Mountpoint string `json:"mountpoint"`
}
jreports := make([]jreport, 0, len(reports))
for _, r := range reports {
jreports = append(jreports, jreport{
ID: r.Id,
Names: []string{r.Name},
Mountpoint: r.Path,
})
}
b, err := json.MarshalIndent(jreports, "", " ")
if err != nil {
return err
}
fmt.Println(string(b))
return nil
}
type mountReporter struct {
*entities.ContainerMountReport
}
func (m mountReporter) ID() string {
if mountOpts.NoTruncate {
return m.Id
}
return m.Id[0:12]
}

View File

@ -0,0 +1,127 @@
package containers
import (
"context"
"fmt"
"os"
"strings"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/utils"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
)
var (
pauseDescription = `Pauses one or more running containers. The container name or ID can be used.`
pauseCommand = &cobra.Command{
Use: "pause [options] CONTAINER [CONTAINER...]",
Short: "Pause all the processes in one or more containers",
Long: pauseDescription,
RunE: pause,
Args: func(cmd *cobra.Command, args []string) error {
return validate.CheckAllLatestAndIDFile(cmd, args, false, "cidfile")
},
ValidArgsFunction: common.AutocompleteContainersRunning,
Example: `podman pause mywebserver
podman pause 860a4b23
podman pause --all`,
}
containerPauseCommand = &cobra.Command{
Use: pauseCommand.Use,
Short: pauseCommand.Short,
Long: pauseCommand.Long,
RunE: pauseCommand.RunE,
Args: func(cmd *cobra.Command, args []string) error {
return validate.CheckAllLatestAndIDFile(cmd, args, false, "cidfile")
},
ValidArgsFunction: pauseCommand.ValidArgsFunction,
Example: `podman container pause mywebserver
podman container pause 860a4b23
podman container pause --all`,
}
)
var (
pauseOpts = entities.PauseUnPauseOptions{
Filters: make(map[string][]string),
}
pauseCidFiles = []string{}
)
func pauseFlags(cmd *cobra.Command) {
flags := cmd.Flags()
flags.BoolVarP(&pauseOpts.All, "all", "a", false, "Pause all running containers")
cidfileFlagName := "cidfile"
flags.StringArrayVar(&pauseCidFiles, cidfileFlagName, nil, "Read the container ID from the file")
_ = cmd.RegisterFlagCompletionFunc(cidfileFlagName, completion.AutocompleteDefault)
filterFlagName := "filter"
flags.StringArrayVarP(&filters, filterFlagName, "f", []string{}, "Filter output based on conditions given")
_ = cmd.RegisterFlagCompletionFunc(filterFlagName, common.AutocompletePsFilters)
if registry.IsRemote() {
_ = flags.MarkHidden("cidfile")
}
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: pauseCommand,
})
pauseFlags(pauseCommand)
validate.AddLatestFlag(pauseCommand, &pauseOpts.Latest)
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: containerPauseCommand,
Parent: containerCmd,
})
pauseFlags(containerPauseCommand)
validate.AddLatestFlag(containerPauseCommand, &pauseOpts.Latest)
}
func pause(cmd *cobra.Command, args []string) error {
var (
errs utils.OutputErrors
)
args = utils.RemoveSlash(args)
for _, cidFile := range pauseCidFiles {
content, err := os.ReadFile(cidFile)
if err != nil {
return fmt.Errorf("reading CIDFile: %w", err)
}
id := strings.Split(string(content), "\n")[0]
args = append(args, id)
}
for _, f := range filters {
fname, filter, hasFilter := strings.Cut(f, "=")
if !hasFilter {
return fmt.Errorf("invalid filter %q", f)
}
pauseOpts.Filters[fname] = append(pauseOpts.Filters[fname], filter)
}
responses, err := registry.ContainerEngine().ContainerPause(context.Background(), args, pauseOpts)
if err != nil {
return err
}
for _, r := range responses {
switch {
case r.Err != nil:
errs = append(errs, r.Err)
case r.RawInput != "":
fmt.Println(r.RawInput)
default:
fmt.Println(r.Id)
}
}
return errs.PrintErrors()
}

View File

@ -0,0 +1,155 @@
package containers
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
var (
portDescription = `List port mappings for the CONTAINER, or look up the public-facing port that is NAT-ed to the PRIVATE_PORT
`
portCommand = &cobra.Command{
Use: "port [options] CONTAINER [PORT]",
Short: "List port mappings or a specific mapping for the container",
Long: portDescription,
RunE: port,
Args: func(cmd *cobra.Command, args []string) error {
return validate.CheckAllLatestAndIDFile(cmd, args, true, "")
},
ValidArgsFunction: common.AutocompleteContainerOneArg,
Example: `podman port --all
podman port ctrID 80/tcp`,
}
containerPortCommand = &cobra.Command{
Use: "port [options] CONTAINER [PORT]",
Short: portCommand.Short,
Long: portDescription,
RunE: portCommand.RunE,
Args: func(cmd *cobra.Command, args []string) error {
return validate.CheckAllLatestAndIDFile(cmd, args, true, "")
},
ValidArgsFunction: portCommand.ValidArgsFunction,
Example: `podman container port --all
podman container port CTRID 80`,
}
)
var (
portOpts entities.ContainerPortOptions
)
func portFlags(flags *pflag.FlagSet) {
flags.BoolVarP(&portOpts.All, "all", "a", false, "Display port information for all containers")
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: portCommand,
})
portFlags(portCommand.Flags())
validate.AddLatestFlag(portCommand, &portOpts.Latest)
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: containerPortCommand,
Parent: containerCmd,
})
portFlags(containerPortCommand.Flags())
validate.AddLatestFlag(containerPortCommand, &portOpts.Latest)
}
func port(_ *cobra.Command, args []string) error {
var (
container string
err error
userPort uint16
userProto string
)
if len(args) == 0 && !portOpts.Latest && !portOpts.All {
return errors.New("you must supply a running container name or id")
}
if !portOpts.Latest && len(args) >= 1 {
container = strings.TrimPrefix(args[0], "/")
}
port := ""
if len(args) > 2 {
return errors.New("`port` accepts at most 2 arguments")
}
if len(args) > 1 && !portOpts.Latest {
port = args[1]
}
if len(args) == 1 && portOpts.Latest {
port = args[0]
}
if len(port) > 0 {
fields := strings.Split(port, "/")
if len(fields) > 2 || len(fields) < 1 {
return fmt.Errorf("port formats are port/protocol. '%s' is invalid", port)
}
if len(fields) == 1 {
fields = append(fields, "tcp")
}
portNum, err := strconv.ParseUint(fields[0], 10, 16)
if err != nil {
return err
}
userPort = uint16(portNum)
userProto = fields[1]
}
reports, err := registry.ContainerEngine().ContainerPort(registry.Context(), container, portOpts)
if err != nil {
return err
}
var found bool
// Iterate mappings
for _, report := range reports {
allPrefix := ""
if portOpts.All {
allPrefix = report.Id[:12] + "\t"
}
for _, v := range report.Ports {
hostIP := v.HostIP
// Set host IP to 0.0.0.0 if blank
if hostIP == "" {
hostIP = "0.0.0.0"
}
protocols := strings.Split(v.Protocol, ",")
for _, protocol := range protocols {
// If not searching by port or port/proto, then dump what we see
if port == "" {
for i := uint16(0); i < v.Range; i++ {
fmt.Printf("%s%d/%s -> %s:%d\n", allPrefix, v.ContainerPort+i, protocol, hostIP, v.HostPort+i)
}
continue
}
// check if the proto matches and if the port is in the range
// this is faster than looping over the range for no reason
if v.Protocol == userProto &&
v.ContainerPort <= userPort &&
v.ContainerPort+v.Range > userPort {
// we have to add the current range to the host port
hostPort := v.HostPort + userPort - v.ContainerPort
fmt.Printf("%s%s:%d\n", allPrefix, hostIP, hostPort)
found = true
break
}
}
}
if !found && port != "" {
return fmt.Errorf("failed to find published port %q", port)
}
}
return nil
}

View File

@ -0,0 +1,77 @@
package containers
import (
"bufio"
"context"
"fmt"
"os"
"strings"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/parse"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/utils"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
)
var (
pruneDescription = `podman container prune
Removes all non running containers`
pruneCommand = &cobra.Command{
Use: "prune [options]",
Short: "Remove all non running containers",
Long: pruneDescription,
RunE: prune,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman container prune`,
Args: validate.NoArgs,
}
force bool
filter = []string{}
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: pruneCommand,
Parent: containerCmd,
})
flags := pruneCommand.Flags()
flags.BoolVarP(&force, "force", "f", false, "Do not prompt for confirmation. The default is false")
filterFlagName := "filter"
flags.StringArrayVar(&filter, filterFlagName, []string{}, "Provide filter values (e.g. 'label=<key>=<value>')")
_ = pruneCommand.RegisterFlagCompletionFunc(filterFlagName, common.AutocompletePruneFilters)
}
func prune(cmd *cobra.Command, _ []string) error {
var (
pruneOptions = entities.ContainerPruneOptions{}
err error
)
if !force {
reader := bufio.NewReader(os.Stdin)
fmt.Println("WARNING! This will remove all non running containers.")
fmt.Print("Are you sure you want to continue? [y/N] ")
answer, err := reader.ReadString('\n')
if err != nil {
return err
}
if strings.ToLower(answer)[0] != 'y' {
return nil
}
}
pruneOptions.Filters, err = parse.FilterArgumentsIntoFilters(filter)
if err != nil {
return err
}
responses, err := registry.ContainerEngine().ContainerPrune(context.Background(), pruneOptions)
if err != nil {
return err
}
return utils.PrintContainerPruneResults(responses, false)
}

572
cmd/podman/containers/ps.go Normal file
View File

@ -0,0 +1,572 @@
package containers
import (
"cmp"
"errors"
"fmt"
"os"
"slices"
"strconv"
"strings"
"time"
"github.com/containers/common/libnetwork/types"
"github.com/containers/common/pkg/completion"
"github.com/containers/common/pkg/report"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/utils"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/docker/go-units"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
psDescription = "Prints out information about the containers"
psCommand = &cobra.Command{
Use: "ps [options]",
Short: "List containers",
Long: psDescription,
RunE: ps,
Args: validate.NoArgs,
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman ps -a
podman ps -a --format "{{.ID}} {{.Image}} {{.Labels}} {{.Mounts}}"
podman ps --size --sort names`,
}
psContainerCommand = &cobra.Command{
Use: psCommand.Use,
Short: psCommand.Short,
Long: psCommand.Long,
RunE: psCommand.RunE,
Args: psCommand.Args,
ValidArgsFunction: psCommand.ValidArgsFunction,
Example: strings.ReplaceAll(psCommand.Example, "podman ps", "podman container ps"),
}
)
var (
listOpts = entities.ContainerListOptions{
Filters: make(map[string][]string),
}
filters []string
noTrunc bool
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: psCommand,
})
listFlagSet(psCommand)
validate.AddLatestFlag(psCommand, &listOpts.Latest)
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: psContainerCommand,
Parent: containerCmd,
})
listFlagSet(psContainerCommand)
validate.AddLatestFlag(psContainerCommand, &listOpts.Latest)
}
func listFlagSet(cmd *cobra.Command) {
flags := cmd.Flags()
flags.BoolVarP(&listOpts.All, "all", "a", false, "Show all the containers, default is only running containers")
flags.BoolVar(&listOpts.External, "external", false, "Show containers in storage not controlled by Podman")
filterFlagName := "filter"
flags.StringArrayVarP(&filters, filterFlagName, "f", []string{}, "Filter output based on conditions given")
_ = cmd.RegisterFlagCompletionFunc(filterFlagName, common.AutocompletePsFilters)
formatFlagName := "format"
flags.StringVar(&listOpts.Format, formatFlagName, "", "Pretty-print containers to JSON or using a Go template")
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, common.AutocompleteFormat(&psReporter{}))
lastFlagName := "last"
flags.IntVarP(&listOpts.Last, lastFlagName, "n", -1, "Print the n last created containers (all states)")
_ = cmd.RegisterFlagCompletionFunc(lastFlagName, completion.AutocompleteNone)
flags.BoolVar(&listOpts.Namespace, "ns", false, "Display namespace information")
flags.BoolVar(&noTrunc, "no-trunc", false, "Display the extended information")
flags.BoolVarP(&listOpts.Pod, "pod", "p", false, "Print the ID and name of the pod the containers are associated with")
flags.BoolVarP(&listOpts.Quiet, "quiet", "q", false, "Print the numeric IDs of the containers only")
flags.Bool("noheading", false, "Do not print headers")
flags.BoolVarP(&listOpts.Size, "size", "s", false, "Display the total file sizes")
flags.BoolVar(&listOpts.Sync, "sync", false, "Sync container state with OCI runtime")
watchFlagName := "watch"
flags.UintVarP(&listOpts.Watch, watchFlagName, "w", 0, "Watch the ps output on an interval in seconds")
_ = cmd.RegisterFlagCompletionFunc(watchFlagName, completion.AutocompleteNone)
sort := validate.Value(&listOpts.Sort, "command", "created", "id", "image", "names", "runningfor", "size", "status")
sortFlagName := "sort"
flags.Var(sort, sortFlagName, "Sort output by: "+sort.Choices())
_ = cmd.RegisterFlagCompletionFunc(sortFlagName, common.AutocompletePsSort)
flags.SetNormalizeFunc(utils.AliasFlags)
}
func checkFlags(c *cobra.Command) error {
// latest, and last are mutually exclusive.
if listOpts.Last >= 0 && listOpts.Latest {
return errors.New("last and latest are mutually exclusive")
}
// Quiet conflicts with size and namespace and is overridden by a Go
// template.
if listOpts.Quiet {
if listOpts.Size || listOpts.Namespace {
return errors.New("quiet conflicts with size and namespace")
}
}
// Size and namespace conflict with each other
if listOpts.Size && listOpts.Namespace {
return errors.New("size and namespace options conflict")
}
if listOpts.Watch > 0 && listOpts.Latest {
return errors.New("the watch and latest flags cannot be used together")
}
podmanConfig := registry.PodmanConfig()
if podmanConfig.ContainersConf.Engine.Namespace != "" {
if c.Flag("storage").Changed && listOpts.External {
return errors.New("--namespace and --external flags can not both be set")
}
listOpts.External = false
}
return nil
}
func jsonOut(responses []entities.ListContainer) error {
type jsonFormat struct {
entities.ListContainer
Created int64
}
r := make([]jsonFormat, 0)
for _, con := range responses {
con.CreatedAt = units.HumanDuration(time.Since(con.Created)) + " ago"
con.Status = psReporter{con}.Status()
jf := jsonFormat{
ListContainer: con,
Created: con.Created.Unix(),
}
r = append(r, jf)
}
b, err := json.MarshalIndent(r, "", " ")
if err != nil {
return err
}
fmt.Println(string(b))
return nil
}
func quietOut(responses []entities.ListContainer) {
for _, r := range responses {
id := r.ID
if !noTrunc {
id = id[0:12]
}
fmt.Println(id)
}
}
func getResponses() ([]entities.ListContainer, error) {
responses, err := registry.ContainerEngine().ContainerList(registry.Context(), listOpts)
if err != nil {
return nil, err
}
if len(listOpts.Sort) > 0 {
responses, err = entities.SortPsOutput(listOpts.Sort, responses)
if err != nil {
return nil, err
}
}
return responses, nil
}
func ps(cmd *cobra.Command, _ []string) error {
if err := checkFlags(cmd); err != nil {
return err
}
if !listOpts.Pod {
listOpts.Pod = strings.Contains(listOpts.Format, ".PodName")
}
for _, f := range filters {
fname, filter, hasFilter := strings.Cut(f, "=")
if !hasFilter {
return fmt.Errorf("invalid filter %q", f)
}
listOpts.Filters[fname] = append(listOpts.Filters[fname], filter)
}
listContainers, err := getResponses()
if err != nil {
return err
}
if len(listOpts.Sort) > 0 {
listContainers, err = entities.SortPsOutput(listOpts.Sort, listContainers)
if err != nil {
return err
}
}
switch {
case report.IsJSON(listOpts.Format):
return jsonOut(listContainers)
case listOpts.Quiet && !cmd.Flags().Changed("format"):
quietOut(listContainers)
return nil
}
responses := make([]psReporter, 0, len(listContainers))
for _, r := range listContainers {
responses = append(responses, psReporter{r})
}
hdrs, format := createPsOut()
var origin report.Origin
noHeading, _ := cmd.Flags().GetBool("noheading")
if cmd.Flags().Changed("format") {
noHeading = noHeading || !report.HasTable(listOpts.Format)
format = listOpts.Format
origin = report.OriginUser
} else {
origin = report.OriginPodman
}
ns := strings.NewReplacer(".Namespaces.", ".")
format = ns.Replace(format)
rpt, err := report.New(os.Stdout, cmd.Name()).Parse(origin, format)
if err != nil {
return err
}
defer rpt.Flush()
headers := func() error { return nil }
if !noHeading {
headers = func() error {
return rpt.Execute(hdrs)
}
}
switch {
// Output table Watch > 0 will refresh screen
case listOpts.Watch > 0:
// responses will grow to the largest number of processes reported on, but will not thrash the gc
var responses []psReporter
for ; ; responses = responses[:0] {
ctnrs, err := getResponses()
if err != nil {
return err
}
for _, r := range ctnrs {
responses = append(responses, psReporter{r})
}
common.ClearScreen()
if err := headers(); err != nil {
return err
}
if err := rpt.Execute(responses); err != nil {
return err
}
if err := rpt.Flush(); err != nil {
// we usually do not care about Flush() failures but here do not loop if Flush() has failed
return err
}
time.Sleep(time.Duration(listOpts.Watch) * time.Second)
}
default:
if err := headers(); err != nil {
return err
}
if err := rpt.Execute(responses); err != nil {
return err
}
}
return nil
}
// cannot use report.Headers() as it doesn't support structures as fields
func createPsOut() ([]map[string]string, string) {
hdrs := report.Headers(psReporter{}, map[string]string{
"Cgroup": "cgroupns",
"CreatedHuman": "created",
"ID": "container id",
"IPC": "ipc",
"MNT": "mnt",
"NET": "net",
"Networks": "networks",
"PIDNS": "pidns",
"Pod": "pod id",
"PodName": "podname", // undo camelcase space break
"Restarts": "restarts",
"RunningFor": "running for",
"UTS": "uts",
"User": "userns",
})
var row string
if listOpts.Namespace {
row = "{{.ID}}\t{{.Names}}\t{{.Pid}}\t{{.Namespaces.Cgroup}}\t{{.Namespaces.IPC}}\t{{.Namespaces.MNT}}\t{{.Namespaces.NET}}\t{{.Namespaces.PIDNS}}\t{{.Namespaces.User}}\t{{.Namespaces.UTS}}"
} else {
row = "{{.ID}}\t{{.Image}}\t{{.Command}}\t{{.CreatedHuman}}\t{{.Status}}\t{{.Ports}}\t{{.Names}}"
if listOpts.Pod {
row += "\t{{.Pod}}\t{{.PodName}}"
}
if listOpts.Size {
row += "\t{{.Size}}"
}
}
return hdrs, "{{range .}}" + row + "\n{{end -}}"
}
type psReporter struct {
entities.ListContainer
}
// ImageID returns the ID of the container
func (l psReporter) ImageID() string {
if !noTrunc {
return l.ListContainer.ImageID[0:12]
}
return l.ListContainer.ImageID
}
// Label returns a map of the pod's labels
func (l psReporter) Label(name string) string {
return l.ListContainer.Labels[name]
}
// ID returns the ID of the container
func (l psReporter) ID() string {
if !noTrunc {
return l.ListContainer.ID[0:12]
}
return l.ListContainer.ID
}
// Pod returns the ID of the pod the container
// belongs to and appropriately truncates the ID
func (l psReporter) Pod() string {
if !noTrunc && len(l.ListContainer.Pod) > 0 {
return l.ListContainer.Pod[0:12]
}
return l.ListContainer.Pod
}
// Status returns the container status in the default ps output format.
func (l psReporter) Status() string {
var state string
switch l.ListContainer.State {
case "running":
t := units.HumanDuration(time.Since(time.Unix(l.StartedAt, 0)))
state = "Up " + t
case "exited", "stopped":
t := units.HumanDuration(time.Since(time.Unix(l.ExitedAt, 0)))
state = fmt.Sprintf("Exited (%d) %s ago", l.ExitCode, t)
default:
// Need to capitalize the first letter to match Docker.
// strings.Title is deprecated since go 1.18
// However for our use case it is still fine. The recommended replacement
// is adding about 400kb binary size so let's keep using this for now.
//nolint:staticcheck
state = strings.Title(l.ListContainer.State)
}
hc := l.ListContainer.Status
if hc != "" {
state += " (" + hc + ")"
}
return state
}
func (l psReporter) Restarts() string {
return strconv.Itoa(int(l.ListContainer.Restarts))
}
func (l psReporter) RunningFor() string {
return l.CreatedHuman()
}
// Command returns the container command in string format
func (l psReporter) Command() string {
command := strings.Join(l.ListContainer.Command, " ")
if !noTrunc {
if len(command) > 17 {
return command[0:17] + "..."
}
}
return command
}
// Size returns the rootfs and virtual sizes in human duration in
// and output form (string) suitable for ps
func (l psReporter) Size() string {
if l.ListContainer.Size == nil {
logrus.Errorf("Size format requires --size option")
return ""
}
virt := units.HumanSizeWithPrecision(float64(l.ListContainer.Size.RootFsSize), 3)
s := units.HumanSizeWithPrecision(float64(l.ListContainer.Size.RwSize), 3)
return fmt.Sprintf("%s (virtual %s)", s, virt)
}
// Names returns the container name in string format
func (l psReporter) Names() string {
return l.ListContainer.Names[0]
}
// Networks returns the container network names in string format
func (l psReporter) Networks() string {
return strings.Join(l.ListContainer.Networks, ",")
}
// Ports converts from Portmappings to the string form
// required by ps
func (l psReporter) Ports() string {
return portsToString(l.ListContainer.Ports, l.ListContainer.ExposedPorts)
}
// CreatedAt returns the container creation time in string format. podman
// and docker both return a timestamped value for createdat
func (l psReporter) CreatedAt() string {
return l.Created.String()
}
// CreatedHuman allows us to output the created time in human readable format
func (l psReporter) CreatedHuman() string {
return units.HumanDuration(time.Since(l.Created)) + " ago"
}
// Cgroup exposes .Namespaces.Cgroup
func (l psReporter) Cgroup() string {
return l.Namespaces.Cgroup
}
// IPC exposes .Namespaces.IPC
func (l psReporter) IPC() string {
return l.Namespaces.IPC
}
// MNT exposes .Namespaces.MNT
func (l psReporter) MNT() string {
return l.Namespaces.MNT
}
// NET exposes .Namespaces.NET
func (l psReporter) NET() string {
return l.Namespaces.NET
}
// PIDNS exposes .Namespaces.PIDNS
func (l psReporter) PIDNS() string {
return l.Namespaces.PIDNS
}
// User exposes .Namespaces.User
func (l psReporter) User() string {
return l.Namespaces.User
}
// UTS exposes .Namespaces.UTS
func (l psReporter) UTS() string {
return l.Namespaces.UTS
}
// portsToString converts the ports used to a string of the from "port1, port2"
// and also groups a continuous list of ports into a readable format.
// The format is IP:HostPort(-Range)->ContainerPort(-Range)/Proto
func portsToString(ports []types.PortMapping, exposedPorts map[uint16][]string) string {
if len(ports) == 0 && len(exposedPorts) == 0 {
return ""
}
portMap := make(map[string]struct{})
sb := &strings.Builder{}
for _, port := range ports {
hostIP := port.HostIP
if hostIP == "" {
hostIP = "0.0.0.0"
}
if port.Range > 1 {
fmt.Fprintf(sb, "%s:%d-%d->%d-%d/%s, ",
hostIP, port.HostPort, port.HostPort+port.Range-1,
port.ContainerPort, port.ContainerPort+port.Range-1, port.Protocol)
for i := range port.Range {
portMap[fmt.Sprintf("%d/%s", port.ContainerPort+i, port.Protocol)] = struct{}{}
}
} else {
fmt.Fprintf(sb, "%s:%d->%d/%s, ",
hostIP, port.HostPort,
port.ContainerPort, port.Protocol)
portMap[fmt.Sprintf("%d/%s", port.ContainerPort, port.Protocol)] = struct{}{}
}
}
// iterating a map is not deterministic so let's convert slice first and sort by protocol and port to make it deterministic
sortedPorts := make([]exposedPort, 0, len(exposedPorts))
for port, protocols := range exposedPorts {
for _, proto := range protocols {
sortedPorts = append(sortedPorts, exposedPort{num: port, protocol: proto})
}
}
slices.SortFunc(sortedPorts, func(a, b exposedPort) int {
protoCmp := cmp.Compare(a.protocol, b.protocol)
if protoCmp != 0 {
return protoCmp
}
return cmp.Compare(a.num, b.num)
})
var prevPort *exposedPort
for _, port := range sortedPorts {
// only if it was not published already so we do not have duplicates
if _, ok := portMap[fmt.Sprintf("%d/%s", port.num, port.protocol)]; ok {
continue
}
if prevPort != nil {
// if the prevPort is one below us we know it is a range, do not print it and just increase the range by one
if prevPort.protocol == port.protocol && prevPort.num == port.num-prevPort.portRange-1 {
prevPort.portRange++
continue
}
// the new port is not a range with the previous one so print it
printExposedPort(prevPort, sb)
}
prevPort = &port
}
// do not forget to print the last port
if prevPort != nil {
printExposedPort(prevPort, sb)
}
display := sb.String()
// make sure to trim the last ", " of the string
return display[:len(display)-2]
}
type exposedPort struct {
num uint16
protocol string
// portRange is 0 indexed
portRange uint16
}
func printExposedPort(port *exposedPort, sb *strings.Builder) {
// exposed ports do not have a host part and are just written as "NUM[-RANGE]/PROTO"
if port.portRange > 0 {
fmt.Fprintf(sb, "%d-%d/%s, ", port.num, port.num+port.portRange, port.protocol)
} else {
fmt.Fprintf(sb, "%d/%s, ", port.num, port.protocol)
}
}

View File

@ -0,0 +1,196 @@
package containers
import (
"testing"
"github.com/containers/common/libnetwork/types"
"github.com/stretchr/testify/assert"
)
func Test_portsToString(t *testing.T) {
tests := []struct {
name string
ports []types.PortMapping
exposedPorts map[uint16][]string
want string
}{
{
name: "no ports",
want: "",
},
{
name: "no ports empty map/slice",
ports: []types.PortMapping{},
exposedPorts: map[uint16][]string{},
want: "",
},
{
name: "single published port",
ports: []types.PortMapping{
{
ContainerPort: 80,
HostPort: 8080,
Protocol: "tcp",
Range: 1,
},
},
want: "0.0.0.0:8080->80/tcp",
},
{
name: "published port with host ip",
ports: []types.PortMapping{
{
ContainerPort: 80,
HostPort: 8080,
HostIP: "127.0.0.1",
Protocol: "tcp",
Range: 1,
},
},
want: "127.0.0.1:8080->80/tcp",
},
{
name: "published port with same exposed port",
ports: []types.PortMapping{
{
ContainerPort: 80,
HostPort: 8080,
Protocol: "tcp",
Range: 1,
},
},
exposedPorts: map[uint16][]string{
80: {"tcp"},
},
want: "0.0.0.0:8080->80/tcp",
},
{
name: "published port and exposed port with different protocol",
ports: []types.PortMapping{
{
ContainerPort: 80,
HostPort: 8080,
Protocol: "tcp",
Range: 1,
},
},
exposedPorts: map[uint16][]string{
80: {"udp"},
},
want: "0.0.0.0:8080->80/tcp, 80/udp",
},
{
name: "published port range",
ports: []types.PortMapping{
{
ContainerPort: 80,
HostPort: 8080,
Protocol: "tcp",
Range: 3,
},
},
want: "0.0.0.0:8080-8082->80-82/tcp",
},
{
name: "published port range and exposed port in that range",
ports: []types.PortMapping{
{
ContainerPort: 80,
HostPort: 8080,
Protocol: "tcp",
Range: 3,
},
},
exposedPorts: map[uint16][]string{
81: {"tcp"},
},
want: "0.0.0.0:8080-8082->80-82/tcp",
},
{
name: "two published ports",
ports: []types.PortMapping{
{
ContainerPort: 80,
HostPort: 8080,
Protocol: "tcp",
Range: 3,
},
{
ContainerPort: 80,
HostPort: 8080,
Protocol: "udp",
Range: 1,
},
},
want: "0.0.0.0:8080-8082->80-82/tcp, 0.0.0.0:8080->80/udp",
},
{
name: "exposed port",
exposedPorts: map[uint16][]string{
80: {"tcp"},
},
want: "80/tcp",
},
{
name: "exposed port multiple protocols",
exposedPorts: map[uint16][]string{
80: {"tcp", "udp"},
},
want: "80/tcp, 80/udp",
},
{
name: "exposed port range",
exposedPorts: map[uint16][]string{
80: {"tcp"},
81: {"tcp"},
82: {"tcp"},
},
want: "80-82/tcp",
},
{
name: "exposed port range with different protocols",
exposedPorts: map[uint16][]string{
80: {"tcp", "udp"},
81: {"tcp", "sctp"},
82: {"tcp", "udp"},
},
want: "81/sctp, 80-82/tcp, 80/udp, 82/udp",
},
{
name: "multiple exposed port ranges",
exposedPorts: map[uint16][]string{
80: {"tcp"},
81: {"tcp"},
82: {"tcp"},
// 83 missing to split the range
84: {"tcp"},
85: {"tcp"},
86: {"tcp"},
},
want: "80-82/tcp, 84-86/tcp",
},
{
name: "published port range partially overlaps with exposed port range",
ports: []types.PortMapping{
{
ContainerPort: 80,
HostPort: 8080,
Protocol: "tcp",
Range: 3,
},
},
exposedPorts: map[uint16][]string{
82: {"tcp"},
83: {"tcp"},
84: {"tcp"},
},
want: "0.0.0.0:8080-8082->80-82/tcp, 83-84/tcp",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := portsToString(tt.ports, tt.exposedPorts)
assert.Equal(t, tt.want, got)
})
}
}

View File

@ -0,0 +1,56 @@
package containers
import (
"errors"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/utils"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
)
var (
renameDescription = "The podman rename command allows you to rename an existing container"
renameCommand = &cobra.Command{
Use: "rename CONTAINER NAME",
Short: "Rename an existing container",
Long: renameDescription,
RunE: rename,
Args: cobra.ExactArgs(2),
ValidArgsFunction: common.AutocompleteContainerOneArg,
Example: "podman rename containerA newName",
}
containerRenameCommand = &cobra.Command{
Use: renameCommand.Use,
Short: renameCommand.Short,
Long: renameCommand.Long,
RunE: renameCommand.RunE,
Args: renameCommand.Args,
ValidArgsFunction: renameCommand.ValidArgsFunction,
Example: "podman container rename containerA newName",
}
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: renameCommand,
})
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: containerRenameCommand,
Parent: containerCmd,
})
}
func rename(cmd *cobra.Command, args []string) error {
if len(args) > 2 {
return errors.New("must provide at least two arguments to rename")
}
args = utils.RemoveSlash(args)
renameOpts := entities.ContainerRenameOptions{
NewName: args[1],
}
return registry.ContainerEngine().ContainerRename(registry.Context(), args[0], renameOpts)
}

View File

@ -0,0 +1,139 @@
package containers
import (
"context"
"fmt"
"os"
"strings"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/utils"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
)
var (
restartDescription = fmt.Sprintf(`Restarts one or more running containers. The container ID or name can be used.
A timeout before forcibly stopping can be set, but defaults to %d seconds.`, containerConfig.Engine.StopTimeout)
restartCommand = &cobra.Command{
Use: "restart [options] CONTAINER [CONTAINER...]",
Short: "Restart one or more containers",
Long: restartDescription,
RunE: restart,
Args: func(cmd *cobra.Command, args []string) error {
return validate.CheckAllLatestAndIDFile(cmd, args, false, "cidfile")
},
ValidArgsFunction: common.AutocompleteContainers,
Example: `podman restart ctrID
podman restart ctrID1 ctrID2`,
}
containerRestartCommand = &cobra.Command{
Use: restartCommand.Use,
Short: restartCommand.Short,
Long: restartCommand.Long,
RunE: restartCommand.RunE,
Args: restartCommand.Args,
ValidArgsFunction: restartCommand.ValidArgsFunction,
Example: `podman container restart ctrID
podman container restart ctrID1 ctrID2`,
}
)
var (
restartOpts = entities.RestartOptions{
Filters: make(map[string][]string),
}
restartCidFiles = []string{}
restartTimeout int
)
func restartFlags(cmd *cobra.Command) {
flags := cmd.Flags()
flags.BoolVarP(&restartOpts.All, "all", "a", false, "Restart all non-running containers")
flags.BoolVar(&restartOpts.Running, "running", false, "Restart only running containers")
cidfileFlagName := "cidfile"
flags.StringArrayVar(&restartCidFiles, cidfileFlagName, nil, "Read the container ID from the file")
_ = cmd.RegisterFlagCompletionFunc(cidfileFlagName, completion.AutocompleteDefault)
filterFlagName := "filter"
flags.StringArrayVarP(&filters, filterFlagName, "f", []string{}, "Filter output based on conditions given")
_ = cmd.RegisterFlagCompletionFunc(filterFlagName, common.AutocompletePsFilters)
timeFlagName := "time"
flags.IntVarP(&restartTimeout, timeFlagName, "t", int(containerConfig.Engine.StopTimeout), "Seconds to wait for stop before killing the container")
_ = cmd.RegisterFlagCompletionFunc(timeFlagName, completion.AutocompleteNone)
if registry.IsRemote() {
_ = flags.MarkHidden("cidfile")
}
flags.SetNormalizeFunc(utils.AliasFlags)
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: restartCommand,
})
restartFlags(restartCommand)
validate.AddLatestFlag(restartCommand, &restartOpts.Latest)
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: containerRestartCommand,
Parent: containerCmd,
})
restartFlags(containerRestartCommand)
validate.AddLatestFlag(containerRestartCommand, &restartOpts.Latest)
}
func restart(cmd *cobra.Command, args []string) error {
var (
errs utils.OutputErrors
)
args = utils.RemoveSlash(args)
if cmd.Flag("time").Changed {
timeout := uint(restartTimeout)
restartOpts.Timeout = &timeout
}
for _, cidFile := range restartCidFiles {
content, err := os.ReadFile(cidFile)
if err != nil {
return fmt.Errorf("reading CIDFile: %w", err)
}
id := strings.Split(string(content), "\n")[0]
args = append(args, id)
}
for _, f := range filters {
fname, filter, hasFilter := strings.Cut(f, "=")
if !hasFilter {
return fmt.Errorf("invalid filter %q", f)
}
restartOpts.Filters[fname] = append(restartOpts.Filters[fname], filter)
}
responses, err := registry.ContainerEngine().ContainerRestart(context.Background(), args, restartOpts)
if err != nil {
return err
}
for _, r := range responses {
switch {
case r.Err != nil:
errs = append(errs, r.Err)
case r.RawInput != "":
fmt.Println(r.RawInput)
default:
fmt.Println(r.Id)
}
}
return errs.PrintErrors()
}

View File

@ -0,0 +1,201 @@
package containers
import (
"context"
"fmt"
"time"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/utils"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/rootless"
"github.com/spf13/cobra"
)
var (
restoreDescription = `
podman container restore
Restores a container from a checkpoint. The container name or ID can be used.
`
restoreCommand = &cobra.Command{
Use: "restore [options] CONTAINER|IMAGE [CONTAINER|IMAGE...]",
Short: "Restore one or more containers from a checkpoint",
Long: restoreDescription,
RunE: restore,
Args: func(cmd *cobra.Command, args []string) error {
return validate.CheckAllLatestAndIDFile(cmd, args, true, "")
},
ValidArgsFunction: common.AutocompleteContainersAndImages,
Example: `podman container restore ctrID
podman container restore imageID
podman container restore --all`,
}
)
var restoreOptions entities.RestoreOptions
type restoreStatistics struct {
PodmanDuration int64 `json:"podman_restore_duration"`
ContainerStatistics []*entities.RestoreReport `json:"container_statistics"`
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: restoreCommand,
Parent: containerCmd,
})
flags := restoreCommand.Flags()
flags.BoolVarP(&restoreOptions.All, "all", "a", false, "Restore all checkpointed containers")
flags.BoolVarP(&restoreOptions.Keep, "keep", "k", false, "Keep all temporary checkpoint files")
flags.BoolVar(&restoreOptions.TCPEstablished, "tcp-established", false, "Restore a container with established TCP connections")
flags.BoolVar(&restoreOptions.FileLocks, "file-locks", false, "Restore a container with file locks")
importFlagName := "import"
flags.StringVarP(&restoreOptions.Import, importFlagName, "i", "", "Restore from exported checkpoint archive (tar.gz)")
_ = restoreCommand.RegisterFlagCompletionFunc(importFlagName, completion.AutocompleteDefault)
nameFlagName := "name"
flags.StringVarP(&restoreOptions.Name, nameFlagName, "n", "", "Specify new name for container restored from exported checkpoint (only works with image or --import)")
_ = restoreCommand.RegisterFlagCompletionFunc(nameFlagName, completion.AutocompleteNone)
importPreviousFlagName := "import-previous"
flags.StringVar(&restoreOptions.ImportPrevious, importPreviousFlagName, "", "Restore from exported pre-checkpoint archive (tar.gz)")
_ = restoreCommand.RegisterFlagCompletionFunc(importPreviousFlagName, completion.AutocompleteDefault)
flags.BoolVar(&restoreOptions.IgnoreRootFS, "ignore-rootfs", false, "Do not apply root file-system changes when importing from exported checkpoint")
flags.BoolVar(&restoreOptions.IgnoreStaticIP, "ignore-static-ip", false, "Ignore IP address set via --static-ip")
flags.BoolVar(&restoreOptions.IgnoreStaticMAC, "ignore-static-mac", false, "Ignore MAC address set via --mac-address")
flags.BoolVar(&restoreOptions.IgnoreVolumes, "ignore-volumes", false, "Do not export volumes associated with container")
flags.StringSliceP(
"publish", "p", []string{},
"Publish a container's port, or a range of ports, to the host (default [])",
)
_ = restoreCommand.RegisterFlagCompletionFunc("publish", completion.AutocompleteNone)
flags.StringVar(&restoreOptions.Pod, "pod", "", "Restore container into existing Pod (only works with image or --import)")
_ = restoreCommand.RegisterFlagCompletionFunc("pod", common.AutocompletePodsRunning)
flags.BoolVar(
&restoreOptions.PrintStats,
"print-stats",
false,
"Display restore statistics",
)
validate.AddLatestFlag(restoreCommand, &restoreOptions.Latest)
}
func restore(cmd *cobra.Command, args []string) error {
var (
e error
errs utils.OutputErrors
)
args = utils.RemoveSlash(args)
podmanStart := time.Now()
if rootless.IsRootless() {
return fmt.Errorf("restoring a container requires root")
}
// Check if the container exists (#15055)
exists := &entities.BoolReport{Value: false}
for _, ctr := range args {
exists, e = registry.ContainerEngine().ContainerExists(registry.Context(), ctr, entities.ContainerExistsOptions{})
if e != nil {
return e
}
if exists.Value {
break
}
}
if !exists.Value {
// Find out if this is an image
restoreOptions.CheckpointImage, e = utils.IsCheckpointImage(context.Background(), args)
if e != nil {
return e
}
}
notImport := !restoreOptions.CheckpointImage && restoreOptions.Import == ""
if notImport && restoreOptions.ImportPrevious != "" {
return fmt.Errorf("--import-previous can only be used with image or --import")
}
if notImport && restoreOptions.IgnoreRootFS {
return fmt.Errorf("--ignore-rootfs can only be used with image or --import")
}
if notImport && restoreOptions.IgnoreVolumes {
return fmt.Errorf("--ignore-volumes can only be used with image or --import")
}
if notImport && restoreOptions.Name != "" {
return fmt.Errorf("--name can only be used with image or --import")
}
if notImport && restoreOptions.Pod != "" {
return fmt.Errorf("--pod can only be used with image or --import")
}
if restoreOptions.Name != "" && restoreOptions.TCPEstablished {
return fmt.Errorf("--tcp-established cannot be used with --name")
}
inputPorts, err := cmd.Flags().GetStringSlice("publish")
if err != nil {
return err
}
restoreOptions.PublishPorts = inputPorts
argLen := len(args)
if restoreOptions.Import != "" {
if restoreOptions.All || restoreOptions.Latest {
return fmt.Errorf("cannot use --import with --all or --latest")
}
if argLen > 0 {
return fmt.Errorf("cannot use --import with positional arguments")
}
}
if (restoreOptions.All || restoreOptions.Latest) && argLen > 0 {
return fmt.Errorf("--all or --latest and containers cannot be used together")
}
if argLen < 1 && !restoreOptions.All && !restoreOptions.Latest && restoreOptions.Import == "" {
return fmt.Errorf("you must provide at least one name or id")
}
if argLen > 1 && restoreOptions.Name != "" {
return fmt.Errorf("--name can only be used with one checkpoint image")
}
responses, err := registry.ContainerEngine().ContainerRestore(context.Background(), args, restoreOptions)
if err != nil {
return err
}
podmanFinished := time.Now()
var statistics restoreStatistics
for _, r := range responses {
switch {
case r.Err != nil:
errs = append(errs, r.Err)
case restoreOptions.PrintStats:
statistics.ContainerStatistics = append(statistics.ContainerStatistics, r)
case r.RawInput != "":
fmt.Println(r.RawInput)
default:
fmt.Println(r.Id)
}
}
if restoreOptions.PrintStats {
statistics.PodmanDuration = podmanFinished.Sub(podmanStart).Microseconds()
j, err := json.MarshalIndent(statistics, "", " ")
if err != nil {
return err
}
fmt.Println(string(j))
}
return errs.PrintErrors()
}

188
cmd/podman/containers/rm.go Normal file
View File

@ -0,0 +1,188 @@
package containers
import (
"context"
"errors"
"fmt"
"os"
"strings"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/utils"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/libpod/define"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
rmDescription = `Removes one or more containers from the host. The container name or ID can be used.
Command does not remove images. Running or unusable containers will not be removed without the -f option.`
rmCommand = &cobra.Command{
Use: "rm [options] CONTAINER [CONTAINER...]",
Short: "Remove one or more containers",
Long: rmDescription,
RunE: rm,
Args: func(cmd *cobra.Command, args []string) error {
return validate.CheckAllLatestAndIDFile(cmd, args, false, "cidfile")
},
ValidArgsFunction: common.AutocompleteContainers,
Example: `podman rm ctrID
podman rm mywebserver myflaskserver 860a4b23
podman rm --force --all
podman rm -f c684f0d469f2`,
}
containerRmCommand = &cobra.Command{
Use: rmCommand.Use,
Short: rmCommand.Short,
Long: rmCommand.Long,
RunE: rmCommand.RunE,
Args: rmCommand.Args,
ValidArgsFunction: rmCommand.ValidArgsFunction,
Example: `podman container rm ctrID
podman container rm mywebserver myflaskserver 860a4b23
podman container rm --force --all
podman container rm -f c684f0d469f2`,
}
)
var (
rmOptions = entities.RmOptions{
Filters: make(map[string][]string),
}
rmCidFiles = []string{}
)
func rmFlags(cmd *cobra.Command) {
flags := cmd.Flags()
flags.BoolVarP(&rmOptions.All, "all", "a", false, "Remove all containers")
flags.BoolVarP(&rmOptions.Ignore, "ignore", "i", false, "Ignore errors when a specified container is missing")
flags.BoolVarP(&rmOptions.Force, "force", "f", false, "Force removal of a running or unusable container")
flags.BoolVar(&rmOptions.Depend, "depend", false, "Remove container and all containers that depend on the selected container")
timeFlagName := "time"
flags.IntVarP(&stopTimeout, timeFlagName, "t", int(containerConfig.Engine.StopTimeout), "Seconds to wait for stop before killing the container")
_ = cmd.RegisterFlagCompletionFunc(timeFlagName, completion.AutocompleteNone)
flags.BoolVarP(&rmOptions.Volumes, "volumes", "v", false, "Remove anonymous volumes associated with the container")
cidfileFlagName := "cidfile"
flags.StringArrayVar(&rmCidFiles, cidfileFlagName, nil, "Read the container ID from the file")
_ = cmd.RegisterFlagCompletionFunc(cidfileFlagName, completion.AutocompleteDefault)
filterFlagName := "filter"
flags.StringArrayVar(&filters, filterFlagName, []string{}, "Filter output based on conditions given")
_ = cmd.RegisterFlagCompletionFunc(filterFlagName, common.AutocompletePsFilters)
if !registry.IsRemote() {
// This option is deprecated, but needs to still exists for backwards compatibility
flags.Bool("storage", false, "Remove container from storage library")
_ = flags.MarkHidden("storage")
}
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: rmCommand,
})
rmFlags(rmCommand)
validate.AddLatestFlag(rmCommand, &rmOptions.Latest)
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: containerRmCommand,
Parent: containerCmd,
})
rmFlags(containerRmCommand)
validate.AddLatestFlag(containerRmCommand, &rmOptions.Latest)
}
func rm(cmd *cobra.Command, args []string) error {
if cmd.Flag("time").Changed {
if !rmOptions.Force {
return errors.New("--force option must be specified to use the --time option")
}
timeout := uint(stopTimeout)
rmOptions.Timeout = &timeout
}
for _, cidFile := range rmCidFiles {
content, err := os.ReadFile(cidFile)
if err != nil {
if rmOptions.Ignore && errors.Is(err, os.ErrNotExist) {
continue
}
return fmt.Errorf("reading CIDFile: %w", err)
}
id, _, _ := strings.Cut(string(content), "\n")
args = append(args, id)
}
for _, f := range filters {
fname, filter, hasFilter := strings.Cut(f, "=")
if !hasFilter {
return fmt.Errorf("invalid filter %q", f)
}
rmOptions.Filters[fname] = append(rmOptions.Filters[fname], filter)
}
if rmOptions.All {
logrus.Debug("--all is set: enforcing --depend=true")
rmOptions.Depend = true
}
if rmOptions.Force {
rmOptions.Ignore = true
}
return removeContainers(utils.RemoveSlash(args), rmOptions, true, false)
}
// removeContainers will remove the specified containers (names or IDs).
// Allows for sharing removal logic across commands. If setExit is set,
// removeContainers will set the exit code according to the `podman-rm` man
// page.
func removeContainers(namesOrIDs []string, rmOptions entities.RmOptions, setExit bool, quiet bool) error {
var errs utils.OutputErrors
responses, err := registry.ContainerEngine().ContainerRm(context.Background(), namesOrIDs, rmOptions)
if err != nil {
if setExit {
setExitCode(err)
}
return err
}
for _, r := range responses {
switch {
case r.Err != nil:
if errors.Is(r.Err, define.ErrWillDeadlock) {
logrus.Errorf("Potential deadlock detected - please run 'podman system renumber' to resolve")
}
if setExit {
setExitCode(r.Err)
}
errs = append(errs, r.Err)
case r.RawInput != "":
if !quiet {
fmt.Println(r.RawInput)
}
default:
if !quiet {
fmt.Println(r.Id)
}
}
}
return errs.PrintErrors()
}
func setExitCode(err error) {
// If error is set to no such container, do not reset
if registry.GetExitCode() == 1 {
return
}
if errors.Is(err, define.ErrNoSuchCtr) || strings.Contains(err.Error(), define.ErrNoSuchCtr.Error()) {
registry.SetExitCode(1)
} else if errors.Is(err, define.ErrCtrStateInvalid) || strings.Contains(err.Error(), define.ErrCtrStateInvalid.Error()) {
registry.SetExitCode(2)
}
}

View File

@ -0,0 +1,264 @@
package containers
import (
"errors"
"fmt"
"os"
"strings"
"github.com/containers/common/pkg/auth"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/utils"
"github.com/containers/podman/v5/libpod/define"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/rootless"
"github.com/containers/podman/v5/pkg/specgen"
"github.com/containers/podman/v5/pkg/specgenutil"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/term"
)
var (
runDescription = "Runs a command in a new container from the given image"
runCommand = &cobra.Command{
Args: cobra.MinimumNArgs(1),
Use: "run [options] IMAGE [COMMAND [ARG...]]",
Short: "Run a command in a new container",
Long: runDescription,
RunE: run,
ValidArgsFunction: common.AutocompleteCreateRun,
Example: `podman run imageID ls -alF /etc
podman run --network=host imageID dnf -y install java
podman run --volume /var/hostdir:/var/ctrdir -i -t fedora /bin/bash`,
}
containerRunCommand = &cobra.Command{
Args: cobra.MinimumNArgs(1),
Use: runCommand.Use,
Short: runCommand.Short,
Long: runCommand.Long,
RunE: runCommand.RunE,
ValidArgsFunction: runCommand.ValidArgsFunction,
Example: `podman container run imageID ls -alF /etc
podman container run --network=host imageID dnf -y install java
podman container run --volume /var/hostdir:/var/ctrdir -i -t fedora /bin/bash`,
}
)
var (
runOpts entities.ContainerRunOptions
runRmi bool
)
func runFlags(cmd *cobra.Command) {
flags := cmd.Flags()
flags.SetInterspersed(false)
common.DefineCreateDefaults(&cliVals)
common.DefineCreateFlags(cmd, &cliVals, entities.CreateMode)
common.DefineNetFlags(cmd)
flags.SetNormalizeFunc(utils.AliasFlags)
flags.BoolVar(&runOpts.SigProxy, "sig-proxy", true, "Proxy received signals to the process")
flags.BoolVar(&runRmi, "rmi", false, "Remove image unless used by other containers, implies --rm")
preserveFdsFlagName := "preserve-fds"
flags.UintVar(&runOpts.PreserveFDs, preserveFdsFlagName, 0, "Pass a number of additional file descriptors into the container")
_ = cmd.RegisterFlagCompletionFunc(preserveFdsFlagName, completion.AutocompleteNone)
preserveFdFlagName := "preserve-fd"
flags.UintSliceVar(&runOpts.PreserveFD, preserveFdFlagName, nil, "Pass a file descriptor into the container")
_ = cmd.RegisterFlagCompletionFunc(preserveFdFlagName, completion.AutocompleteNone)
flags.BoolVarP(&runOpts.Detach, "detach", "d", false, "Run container in background and print container ID")
detachKeysFlagName := "detach-keys"
flags.StringVar(&runOpts.DetachKeys, detachKeysFlagName, containerConfig.DetachKeys(), "Override the key sequence for detaching a container. Format is a single character `[a-Z]` or a comma separated sequence of `ctrl-<value>`, where `<value>` is one of: `a-cf`, `@`, `^`, `[`, `\\`, `]`, `^` or `_`")
_ = cmd.RegisterFlagCompletionFunc(detachKeysFlagName, common.AutocompleteDetachKeys)
passwdFlagName := "passwd"
flags.BoolVar(&runOpts.Passwd, passwdFlagName, true, "add entries to /etc/passwd and /etc/group")
if registry.IsRemote() {
_ = flags.MarkHidden(preserveFdsFlagName)
_ = flags.MarkHidden(preserveFdFlagName)
_ = flags.MarkHidden("conmon-pidfile")
_ = flags.MarkHidden("pidfile")
}
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: runCommand,
})
runFlags(runCommand)
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: containerRunCommand,
Parent: containerCmd,
})
runFlags(containerRunCommand)
}
func run(cmd *cobra.Command, args []string) error {
if err := commonFlags(cmd); err != nil {
return err
}
if runRmi {
if cmd.Flags().Changed("rm") && !cliVals.Rm {
return errors.New("the --rmi option does not work without --rm")
}
cliVals.Rm = true
}
// TODO: Breaking change should be made fatal in next major Release
if cliVals.TTY && cliVals.Interactive && !term.IsTerminal(int(os.Stdin.Fd())) {
logrus.Warnf("The input device is not a TTY. The --tty and --interactive flags might not work properly")
}
if cmd.Flags().Changed("authfile") {
if err := auth.CheckAuthFile(cliVals.Authfile); err != nil {
return err
}
}
runOpts.CIDFile = cliVals.CIDFile
runOpts.Rm = cliVals.Rm
cliVals, err := CreateInit(cmd, cliVals, false)
if err != nil {
return err
}
for _, fd := range runOpts.PreserveFD {
if !rootless.IsFdInherited(int(fd)) {
return fmt.Errorf("file descriptor %d is not available - the preserve-fd option requires that file descriptors must be passed", fd)
}
}
for fd := 3; fd < int(3+runOpts.PreserveFDs); fd++ {
if !rootless.IsFdInherited(fd) {
return fmt.Errorf("file descriptor %d is not available - the preserve-fds option requires that file descriptors must be passed", fd)
}
}
imageName := args[0]
rawImageName := ""
if !cliVals.RootFS {
rawImageName = args[0]
name, err := pullImage(cmd, args[0], &cliVals)
if err != nil {
return err
}
imageName = name
}
if cliVals.Replace {
if err := replaceContainer(cliVals.Name); err != nil {
return err
}
}
// First set the default streams before they get modified by any flags.
runOpts.OutputStream = os.Stdout
runOpts.InputStream = os.Stdin
runOpts.ErrorStream = os.Stderr
// If -i is not set, clear stdin
if !cliVals.Interactive {
runOpts.InputStream = nil
}
passthrough := cliVals.LogDriver == define.PassthroughLogging || cliVals.LogDriver == define.PassthroughTTYLogging
// If attach is set, clear stdin/stdout/stderr and only attach requested
if cmd.Flag("attach").Changed {
if passthrough {
return fmt.Errorf("cannot specify --attach with --log-driver=passthrough: %w", define.ErrInvalidArg)
}
runOpts.OutputStream = nil
runOpts.ErrorStream = nil
if !cliVals.Interactive {
runOpts.InputStream = nil
}
for _, stream := range cliVals.Attach {
switch strings.ToLower(stream) {
case "stdout":
runOpts.OutputStream = os.Stdout
case "stderr":
runOpts.ErrorStream = os.Stderr
case "stdin":
runOpts.InputStream = os.Stdin
default:
return fmt.Errorf("invalid stream %q for --attach - must be one of stdin, stdout, or stderr: %w", stream, define.ErrInvalidArg)
}
}
}
cliVals.PreserveFDs = runOpts.PreserveFDs
cliVals.PreserveFD = runOpts.PreserveFD
s := specgen.NewSpecGenerator(imageName, cliVals.RootFS)
if err := specgenutil.FillOutSpecGen(s, &cliVals, args); err != nil {
return err
}
s.RawImageName = rawImageName
// Include the command used to create the container.
s.ContainerCreateCommand = os.Args
s.ImageOS = cliVals.OS
s.ImageArch = cliVals.Arch
s.ImageVariant = cliVals.Variant
s.Passwd = &runOpts.Passwd
if runRmi {
s.RemoveImage = &runRmi
}
runOpts.Spec = s
if err := createPodIfNecessary(cmd, s, cliVals.Net); err != nil {
return err
}
if s.HealthConfig == nil {
s.HealthConfig, err = common.GetHealthCheckOverrideConfig(cmd, &cliVals)
if err != nil {
return err
}
}
report, err := registry.ContainerEngine().ContainerRun(registry.Context(), runOpts)
// report.ExitCode is set by ContainerRun even it returns an error
if report != nil {
registry.SetExitCode(report.ExitCode)
}
if err != nil {
// if pod was created as part of run
// remove it in case ctr creation fails
if err := rmPodIfNecessary(cmd, s); err != nil {
if !errors.Is(err, define.ErrNoSuchPod) {
logrus.Error(err.Error())
}
}
return err
}
if runOpts.Detach {
if !passthrough {
fmt.Println(report.Id)
}
return nil
}
if runRmi {
_, rmErrors := registry.ImageEngine().Remove(registry.Context(), []string{imageName}, entities.ImageRemoveOptions{Ignore: true})
for _, err := range rmErrors {
logrus.Warnf("Failed to remove image: %v", err)
}
}
return nil
}

View File

@ -0,0 +1,99 @@
package containers
import (
"context"
"strings"
"github.com/containers/common/pkg/auth"
"github.com/containers/common/pkg/completion"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// runlabelOptionsWrapper allows for combining API-only with CLI-only options
// and to convert between them.
type runlabelOptionsWrapper struct {
entities.ContainerRunlabelOptions
TLSVerifyCLI bool
}
var (
runlabelOptions = runlabelOptionsWrapper{}
runlabelDescription = "Executes a command as described by a container image label."
runlabelCommand = &cobra.Command{
Annotations: map[string]string{registry.EngineMode: registry.ABIMode},
Use: "runlabel [options] LABEL IMAGE [ARG...]",
Short: "Execute the command described by an image label",
Long: runlabelDescription,
RunE: runlabel,
Args: cobra.MinimumNArgs(2),
ValidArgsFunction: common.AutocompleteRunlabelCommand,
Example: `podman container runlabel run imageID
podman container runlabel install imageID arg1 arg2
podman container runlabel --display run myImage`,
}
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: runlabelCommand,
Parent: containerCmd,
})
flags := runlabelCommand.Flags()
authfileflagName := "authfile"
flags.StringVar(&runlabelOptions.Authfile, authfileflagName, auth.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override")
_ = runlabelCommand.RegisterFlagCompletionFunc(authfileflagName, completion.AutocompleteDefault)
certDirFlagName := "cert-dir"
flags.StringVar(&runlabelOptions.CertDir, certDirFlagName, "", "`Pathname` of a directory containing TLS certificates and keys")
_ = runlabelCommand.RegisterFlagCompletionFunc(certDirFlagName, completion.AutocompleteDefault)
credsFlagName := "creds"
flags.StringVar(&runlabelOptions.Credentials, credsFlagName, "", "`Credentials` (USERNAME:PASSWORD) to use for authenticating to a registry")
_ = runlabelCommand.RegisterFlagCompletionFunc(credsFlagName, completion.AutocompleteNone)
flags.BoolVar(&runlabelOptions.Display, "display", false, "Preview the command that the label would run")
nameFlagName := "name"
flags.StringVarP(&runlabelOptions.Name, nameFlagName, "n", "", "Assign a name to the container")
_ = runlabelCommand.RegisterFlagCompletionFunc(nameFlagName, completion.AutocompleteNone)
flags.StringVar(&runlabelOptions.Optional1, "opt1", "", "Optional parameter to pass for install")
flags.StringVar(&runlabelOptions.Optional2, "opt2", "", "Optional parameter to pass for install")
flags.StringVar(&runlabelOptions.Optional3, "opt3", "", "Optional parameter to pass for install")
flags.BoolVarP(&runlabelOptions.Pull, "pull", "p", true, "Pull the image if it does not exist locally prior to executing the label contents")
flags.BoolVarP(&runlabelOptions.Quiet, "quiet", "q", false, "Suppress output information when installing images")
flags.BoolVar(&runlabelOptions.Replace, "replace", false, "Replace existing container with a new one from the image")
flags.BoolVar(&runlabelOptions.TLSVerifyCLI, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries")
// Hide the optional flags.
_ = flags.MarkHidden("opt1")
_ = flags.MarkHidden("opt2")
_ = flags.MarkHidden("opt3")
_ = flags.MarkHidden("pull")
if !registry.IsRemote() {
flags.StringVar(&runlabelOptions.SignaturePolicy, "signature-policy", "", "`Pathname` of signature policy file (not usually used)")
_ = flags.MarkHidden("signature-policy")
}
if err := flags.MarkDeprecated("pull", "podman will pull if not found in local storage"); err != nil {
logrus.Error("unable to mark pull flag deprecated")
}
}
func runlabel(cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("tls-verify") {
runlabelOptions.SkipTLSVerify = types.NewOptionalBool(!runlabelOptions.TLSVerifyCLI)
}
if cmd.Flags().Changed("authfile") {
if err := auth.CheckAuthFile(runlabelOptions.Authfile); err != nil {
return err
}
}
return registry.ContainerEngine().ContainerRunlabel(context.Background(), strings.TrimPrefix(args[0], "/"), args[1], args[2:], runlabelOptions.ContainerRunlabelOptions)
}

View File

@ -0,0 +1,152 @@
package containers
import (
"errors"
"fmt"
"os"
"strings"
"github.com/containers/podman/v5/cmd/podman/common"
"github.com/containers/podman/v5/cmd/podman/registry"
"github.com/containers/podman/v5/cmd/podman/utils"
"github.com/containers/podman/v5/cmd/podman/validate"
"github.com/containers/podman/v5/libpod/define"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/spf13/cobra"
)
var (
startDescription = `Starts one or more containers. The container name or ID can be used.`
startCommand = &cobra.Command{
Use: "start [options] CONTAINER [CONTAINER...]",
Short: "Start one or more containers",
Long: startDescription,
RunE: start,
Args: validateStart,
ValidArgsFunction: common.AutocompleteContainersStartable,
Example: `podman start 860a4b231279 5421ab43b45
podman start --interactive --attach imageID`,
}
containerStartCommand = &cobra.Command{
Use: startCommand.Use,
Short: startCommand.Short,
Long: startCommand.Long,
RunE: startCommand.RunE,
Args: startCommand.Args,
ValidArgsFunction: startCommand.ValidArgsFunction,
Example: `podman container start 860a4b231279 5421ab43b45
podman container start --interactive --attach imageID`,
}
)
var (
startOptions = entities.ContainerStartOptions{
Filters: make(map[string][]string),
}
)
func startFlags(cmd *cobra.Command) {
flags := cmd.Flags()
flags.BoolVarP(&startOptions.Attach, "attach", "a", false, "Attach container's STDOUT and STDERR")
detachKeysFlagName := "detach-keys"
flags.StringVar(&startOptions.DetachKeys, detachKeysFlagName, containerConfig.DetachKeys(), "Select the key sequence for detaching a container. Format is a single character `[a-Z]` or a comma separated sequence of `ctrl-<value>`, where `<value>` is one of: `a-z`, `@`, `^`, `[`, `\\`, `]`, `^` or `_`")
_ = cmd.RegisterFlagCompletionFunc(detachKeysFlagName, common.AutocompleteDetachKeys)
flags.BoolVarP(&startOptions.Interactive, "interactive", "i", false, "Make STDIN available to the contained process")
flags.BoolVar(&startOptions.SigProxy, "sig-proxy", false, "Proxy received signals to the process (default true if attaching, false otherwise)")
filterFlagName := "filter"
flags.StringArrayVarP(&filters, filterFlagName, "f", []string{}, "Filter output based on conditions given")
_ = cmd.RegisterFlagCompletionFunc(filterFlagName, common.AutocompletePsFilters)
flags.BoolVar(&startOptions.All, "all", false, "Start all containers regardless of their state or configuration")
if registry.IsRemote() {
_ = flags.MarkHidden("sig-proxy")
}
}
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: startCommand,
})
startFlags(startCommand)
validate.AddLatestFlag(startCommand, &startOptions.Latest)
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: containerStartCommand,
Parent: containerCmd,
})
startFlags(containerStartCommand)
validate.AddLatestFlag(containerStartCommand, &startOptions.Latest)
}
func validateStart(cmd *cobra.Command, args []string) error {
if len(args) == 0 && !startOptions.Latest && !startOptions.All && len(filters) < 1 {
return errors.New("start requires at least one argument")
}
if startOptions.All && startOptions.Latest {
return errors.New("--all and --latest cannot be used together")
}
if len(args) > 0 && startOptions.Latest {
return errors.New("--latest and containers cannot be used together")
}
if len(args) > 1 && startOptions.Attach {
return errors.New("you cannot start and attach multiple containers at once")
}
if (len(args) > 0 || startOptions.Latest) && startOptions.All {
return errors.New("either start all containers or the container(s) provided in the arguments")
}
if startOptions.Attach && startOptions.All {
return errors.New("you cannot start and attach all containers at once")
}
return nil
}
func start(cmd *cobra.Command, args []string) error {
var errs utils.OutputErrors
sigProxy := startOptions.SigProxy || startOptions.Attach
if cmd.Flag("sig-proxy").Changed {
sigProxy = startOptions.SigProxy
}
startOptions.SigProxy = sigProxy
if sigProxy && !startOptions.Attach {
return fmt.Errorf("you cannot use sig-proxy without --attach: %w", define.ErrInvalidArg)
}
if startOptions.Attach {
startOptions.Stdin = os.Stdin
startOptions.Stderr = os.Stderr
startOptions.Stdout = os.Stdout
}
containers := utils.RemoveSlash(args)
for _, f := range filters {
fname, filter, hasFilter := strings.Cut(f, "=")
if !hasFilter {
return fmt.Errorf("invalid filter %q", f)
}
startOptions.Filters[fname] = append(startOptions.Filters[fname], filter)
}
responses, err := registry.ContainerEngine().ContainerStart(registry.Context(), containers, startOptions)
if err != nil {
return err
}
for _, r := range responses {
switch {
case r.Err != nil:
errs = append(errs, r.Err)
case startOptions.Attach:
// Implement the exitcode when the only one container is enabled attach
registry.SetExitCode(r.ExitCode)
case r.RawInput != "":
fmt.Println(r.RawInput)
default:
fmt.Println(r.Id)
}
}
return errs.PrintErrors()
}

Some files were not shown because too many files have changed in this diff Show More