diff --git a/cmd/podman/auto-update.go b/cmd/podman/auto-update.go index 35fc96661e..ff58fd2a5f 100644 --- a/cmd/podman/auto-update.go +++ b/cmd/podman/auto-update.go @@ -31,7 +31,6 @@ var ( 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, diff --git a/hack/swagger-check b/hack/swagger-check index b4481f5bbc..d09e3ddcb1 100755 --- a/hack/swagger-check +++ b/hack/swagger-check @@ -343,6 +343,11 @@ sub operation_name { $main = 'image'; $action = $1; } + # Top-level autoupdate endpoint + elsif ($main eq 'autoupdate') { + $main = 'autoupdate'; + $action = ''; + } # Top-level system endpoints elsif ($main =~ /^(auth|event|info|version)$/) { $main = 'system'; diff --git a/pkg/api/handlers/libpod/autoupdate.go b/pkg/api/handlers/libpod/autoupdate.go new file mode 100644 index 0000000000..ed643326ab --- /dev/null +++ b/pkg/api/handlers/libpod/autoupdate.go @@ -0,0 +1,79 @@ +//go:build !remote + +package libpod + +import ( + "fmt" + "net/http" + + "github.com/containers/podman/v5/libpod" + "github.com/containers/podman/v5/pkg/api/handlers" + "github.com/containers/podman/v5/pkg/api/handlers/utils" + api "github.com/containers/podman/v5/pkg/api/types" + "github.com/containers/podman/v5/pkg/auth" + "github.com/containers/podman/v5/pkg/domain/entities" + "github.com/containers/podman/v5/pkg/domain/infra/abi" + "github.com/containers/podman/v5/pkg/errorhandling" + "github.com/gorilla/schema" + "go.podman.io/image/v5/types" +) + +func AutoUpdate(w http.ResponseWriter, r *http.Request) { + decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) + runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) + + query := struct { + DryRun bool `schema:"dryRun"` + Rollback bool `schema:"rollback"` + TLSVerify types.OptionalBool `schema:"tlsVerify"` + }{} + + if err := decoder.Decode(&query, r.URL.Query()); err != nil { + utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err)) + return + } + + _, authfile, err := auth.GetCredentials(r) + if err != nil { + utils.Error(w, http.StatusBadRequest, err) + return + } + defer auth.RemoveAuthfile(authfile) + + containerEngine := abi.ContainerEngine{Libpod: runtime} + + options := entities.AutoUpdateOptions{ + Authfile: authfile, + DryRun: query.DryRun, + Rollback: query.Rollback, + InsecureSkipTLSVerify: types.OptionalBoolUndefined, + } + + // If TLS verification is explicitly specified (True or False) in the query, + // set the InsecureSkipTLSVerify option accordingly. + // If TLSVerify was not set in the query, OptionalBoolUndefined is used and + // handled later based off the target registry configuration. + switch query.TLSVerify { + case types.OptionalBoolTrue: + options.InsecureSkipTLSVerify = types.NewOptionalBool(false) + case types.OptionalBoolFalse: + options.InsecureSkipTLSVerify = types.NewOptionalBool(true) + case types.OptionalBoolUndefined: + // If the user doesn't define TLSVerify in the query, do nothing and pass + // it to the backend code to handle. + default: // Should never happen + panic("Unexpected handling occurred for TLSVerify") + } + + autoUpdateReports, autoUpdateFailures := containerEngine.AutoUpdate(r.Context(), options) + if autoUpdateReports == nil { + if err := errorhandling.JoinErrors(autoUpdateFailures); err != nil { + utils.Error(w, http.StatusInternalServerError, err) + return + } + } + + reports := handlers.LibpodAutoUpdateReports{Reports: autoUpdateReports, Errors: errorhandling.ErrorsToStrings(autoUpdateFailures)} + + utils.WriteResponse(w, http.StatusOK, reports) +} diff --git a/pkg/api/handlers/swagger/responses.go b/pkg/api/handlers/swagger/responses.go index 7b349f26ad..655ff35f96 100644 --- a/pkg/api/handlers/swagger/responses.go +++ b/pkg/api/handlers/swagger/responses.go @@ -520,3 +520,10 @@ type artifactPushResponse struct { // in:body Body entities.ArtifactPushReport } + +// Auto Update +// swagger:response +type autoupdateResponse struct { + // in:body + Body handlers.LibpodAutoUpdateReports +} diff --git a/pkg/api/handlers/types.go b/pkg/api/handlers/types.go index 6a79103b4a..c4a5ff0749 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -188,3 +188,10 @@ type ExecStartConfig struct { type ExecRemoveConfig struct { Force bool `json:"Force"` } + +// LibpodAutoUpdateReport is the return type for auto update via the rest api. +type LibpodAutoUpdateReports struct { + Reports []*entities.AutoUpdateReport + // Auto update returns data and possible errors. + Errors []string +} diff --git a/pkg/api/server/register_autoupdate.go b/pkg/api/server/register_autoupdate.go new file mode 100644 index 0000000000..61f875225c --- /dev/null +++ b/pkg/api/server/register_autoupdate.go @@ -0,0 +1,52 @@ +//go:build !remote + +package server + +import ( + "net/http" + + "github.com/containers/podman/v5/pkg/api/handlers/libpod" + "github.com/gorilla/mux" +) + +func (s *APIServer) registerAutoUpdateHandlers(r *mux.Router) error { + // swagger:operation POST /libpod/autoupdate libpod AutoupdateLibpod + // --- + // tags: + // - autoupdate + // summary: Auto update + // description: | + // 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. + // parameters: + // - in: query + // name: authfile + // type: string + // description: Authfile to use when contacting registries. + // - in: query + // name: dryRun + // type: boolean + // description: Only check for but do not perform any update. If an update is pending, it will be indicated in the Updated field. + // - in: query + // name: rollback + // type: boolean + // description: If restarting the service with the new image failed, restart it another time with the previous image. + // - in: query + // name: tlsVerify + // type: boolean + // default: true + // description: Require HTTPS and verify signatures when contacting registries. + // produces: + // - application/json + // responses: + // 200: + // $ref: "#/responses/autoupdateResponse" + // 500: + // $ref: '#/responses/internalError' + r.HandleFunc(VersionedPath("/libpod/autoupdate"), s.APIHandler(libpod.AutoUpdate)).Methods(http.MethodPost) + return nil +} diff --git a/pkg/api/server/server.go b/pkg/api/server/server.go index aff00e830c..1ed17673f6 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -133,6 +133,7 @@ func newServer(runtime *libpod.Runtime, listener net.Listener, opts entities.Ser server.registerAuthHandlers, server.registerArtifactHandlers, server.registerArchiveHandlers, + server.registerAutoUpdateHandlers, server.registerContainersHandlers, server.registerDistributionHandlers, server.registerEventsHandlers, diff --git a/pkg/api/tags.yaml b/pkg/api/tags.yaml index 3df18bd88e..427aa6ef64 100644 --- a/pkg/api/tags.yaml +++ b/pkg/api/tags.yaml @@ -1,6 +1,8 @@ tags: - name: artifacts description: Actions related to artifacts + - name: autoupdate + description: Actions related to auto update - name: containers description: Actions related to containers - name: exec diff --git a/pkg/bindings/auto-update/auto-update.go b/pkg/bindings/auto-update/auto-update.go new file mode 100644 index 0000000000..6aed9d4422 --- /dev/null +++ b/pkg/bindings/auto-update/auto-update.go @@ -0,0 +1,54 @@ +package autoupdate + +import ( + "context" + "net/http" + "strconv" + + "github.com/containers/podman/v5/pkg/api/handlers" + "github.com/containers/podman/v5/pkg/auth" + "github.com/containers/podman/v5/pkg/bindings" + "github.com/containers/podman/v5/pkg/domain/entities" + "github.com/containers/podman/v5/pkg/errorhandling" + imageTypes "go.podman.io/image/v5/types" +) + +func AutoUpdate(ctx context.Context, options *AutoUpdateOptions) ([]*entities.AutoUpdateReport, []error) { + conn, err := bindings.GetClient(ctx) + if err != nil { + return nil, []error{err} + } + if options == nil { + options = new(AutoUpdateOptions) + } + + params, err := options.ToParams() + if err != nil { + return nil, []error{err} + } + // InsecureSkipTLSVerify is special. We need to delete the param added by + // ToParams() and change the key and flip the bool + if options.InsecureSkipTLSVerify != nil { + params.Del("SkipTLSVerify") + params.Set("tlsVerify", strconv.FormatBool(!options.GetInsecureSkipTLSVerify())) + } + + header, err := auth.MakeXRegistryAuthHeader(&imageTypes.SystemContext{AuthFilePath: options.GetAuthfile()}, "", "") + if err != nil { + return nil, []error{err} + } + + response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/autoupdate", params, header) + if err != nil { + return nil, []error{err} + } + defer response.Body.Close() + + var reports handlers.LibpodAutoUpdateReports + + if err := response.Process(&reports); err != nil { + return nil, []error{err} + } + + return reports.Reports, errorhandling.StringsToErrors(reports.Errors) +} diff --git a/pkg/bindings/auto-update/types.go b/pkg/bindings/auto-update/types.go new file mode 100644 index 0000000000..b31c54f0f0 --- /dev/null +++ b/pkg/bindings/auto-update/types.go @@ -0,0 +1,19 @@ +package autoupdate + +// AutoUpdateOptions are the options for running auto-update +// +//go:generate go run ../generator/generator.go AutoUpdateOptions +type AutoUpdateOptions struct { + // Authfile to use when contacting registries. + Authfile *string + // Only check for but do not perform any update. If an update is + // pending, it will be indicated in the Updated field of + // AutoUpdateReport. + DryRun *bool + // If restarting the service with the new image failed, restart it + // another time with the previous image. + Rollback *bool + // Allow contacting registries over HTTP, or HTTPS with failed TLS + // verification. Note that this does not affect other TLS connections. + InsecureSkipTLSVerify *bool +} diff --git a/pkg/bindings/auto-update/types_autoupdate_options.go b/pkg/bindings/auto-update/types_autoupdate_options.go new file mode 100644 index 0000000000..78a17c7479 --- /dev/null +++ b/pkg/bindings/auto-update/types_autoupdate_options.go @@ -0,0 +1,78 @@ +// Code generated by go generate; DO NOT EDIT. +package autoupdate + +import ( + "net/url" + + "github.com/containers/podman/v5/pkg/bindings/internal/util" +) + +// Changed returns true if named field has been set +func (o *AutoUpdateOptions) Changed(fieldName string) bool { + return util.Changed(o, fieldName) +} + +// ToParams formats struct fields to be passed to API service +func (o *AutoUpdateOptions) ToParams() (url.Values, error) { + return util.ToParams(o) +} + +// WithAuthfile set field Authfile to given value +func (o *AutoUpdateOptions) WithAuthfile(value string) *AutoUpdateOptions { + o.Authfile = &value + return o +} + +// GetAuthfile returns value of field Authfile +func (o *AutoUpdateOptions) GetAuthfile() string { + if o.Authfile == nil { + var z string + return z + } + return *o.Authfile +} + +// WithDryRun set field DryRun to given value +func (o *AutoUpdateOptions) WithDryRun(value bool) *AutoUpdateOptions { + o.DryRun = &value + return o +} + +// GetDryRun returns value of field DryRun +func (o *AutoUpdateOptions) GetDryRun() bool { + if o.DryRun == nil { + var z bool + return z + } + return *o.DryRun +} + +// WithRollback set field Rollback to given value +func (o *AutoUpdateOptions) WithRollback(value bool) *AutoUpdateOptions { + o.Rollback = &value + return o +} + +// GetRollback returns value of field Rollback +func (o *AutoUpdateOptions) GetRollback() bool { + if o.Rollback == nil { + var z bool + return z + } + return *o.Rollback +} + +// WithInsecureSkipTLSVerify set field InsecureSkipTLSVerify to given value +func (o *AutoUpdateOptions) WithInsecureSkipTLSVerify(value bool) *AutoUpdateOptions { + o.InsecureSkipTLSVerify = &value + return o +} + +// GetInsecureSkipTLSVerify returns value of field InsecureSkipTLSVerify +func (o *AutoUpdateOptions) GetInsecureSkipTLSVerify() bool { + if o.InsecureSkipTLSVerify == nil { + var z bool + return z + } + return *o.InsecureSkipTLSVerify +} diff --git a/pkg/domain/infra/tunnel/auto-update.go b/pkg/domain/infra/tunnel/auto-update.go index 322c04c226..9c9c663b87 100644 --- a/pkg/domain/infra/tunnel/auto-update.go +++ b/pkg/domain/infra/tunnel/auto-update.go @@ -2,11 +2,21 @@ package tunnel import ( "context" - "errors" + autoupdate "github.com/containers/podman/v5/pkg/bindings/auto-update" "github.com/containers/podman/v5/pkg/domain/entities" + "go.podman.io/image/v5/types" ) -func (ic *ContainerEngine) AutoUpdate(_ context.Context, _ entities.AutoUpdateOptions) ([]*entities.AutoUpdateReport, []error) { - return nil, []error{errors.New("not implemented")} +func (ic *ContainerEngine) AutoUpdate(_ context.Context, opts entities.AutoUpdateOptions) ([]*entities.AutoUpdateReport, []error) { + options := new(autoupdate.AutoUpdateOptions).WithAuthfile(opts.Authfile).WithDryRun(opts.DryRun).WithRollback(opts.Rollback) + if s := opts.InsecureSkipTLSVerify; s != types.OptionalBoolUndefined { + if s == types.OptionalBoolTrue { + options.WithInsecureSkipTLSVerify(true) + } else { + options.WithInsecureSkipTLSVerify(false) + } + } + + return autoupdate.AutoUpdate(ic.ClientCtx, options) } diff --git a/test/system/255-auto-update.bats b/test/system/255-auto-update.bats index e916b90cbc..94779964a8 100644 --- a/test/system/255-auto-update.bats +++ b/test/system/255-auto-update.bats @@ -11,7 +11,6 @@ load helpers.systemd export SNAME_FILE function setup() { - skip_if_remote "systemd tests are meaningless over remote" basic_setup SNAME_FILE=${PODMAN_TMPDIR}/services @@ -93,7 +92,14 @@ function generate_service() { run_podman create $extraArgs --name $cname $label $target_img $command - (cd $UNIT_DIR; run_podman generate systemd --new --files --name $requires $cname) + # podman-remote will give warning saying the generated units should be placed on the remote system + if ! is_remote; then + (cd $UNIT_DIR; run_podman generate systemd --new --files --name $requires $cname) + else + + (cd $UNIT_DIR; run_podman 0+w generate systemd --new --files --name $requires $cname) + fi + echo "container-$cname" >> $SNAME_FILE run_podman rm -t 0 -f $cname @@ -165,7 +171,13 @@ function _confirm_update() { archive=$PODMAN_TMPDIR/archive.tar run_podman save -o $archive $IMAGE run_podman 125 create --label io.containers.autoupdate=registry docker-archive:$archive - is "$output" ".*Error: auto updates require the docker image transport but image is of transport \"docker-archive\"" + + # Remote gives different error message + if ! is_remote; then + is "$output" ".*Error: auto updates require the docker image transport but image is of transport \"docker-archive\"" + else + is "$output" ".*Error: unsupported transport docker-archive in \"docker-archive:$archive\": only docker transport is supported" + fi run_podman rmi $shortname } @@ -200,7 +212,12 @@ function _confirm_update() { since=$(date --iso-8601=seconds) run_podman auto-update --rollback=false --format "{{.Unit}},{{.Image}},{{.Updated}},{{.Policy}}" - is "$output" "Trying to pull.*" "Image is updated." + + # Trying to pull is not logged for podman-remote + if ! is_remote; then + is "$output" "Trying to pull.*" "Image is updated." + fi + is "$output" ".*container-$ctr_parent.service,quay.io/libpod/alpine:latest,true,registry.*" "Image is updated." run_podman events --filter type=system --since $since --stream=false is "$output" ".* system auto-update" @@ -239,7 +256,12 @@ function _confirm_update() { containerID="$output" run_podman auto-update --format "{{.Unit}},{{.Image}},{{.Updated}},{{.Policy}}" - is "$output" "Trying to pull.*" "Image is updated." + + # Trying to pull is not logged for podman-remote + if ! is_remote; then + is "$output" "Trying to pull.*" "Image is updated." + fi + is "$output" ".*container-$cname.service,$image:latest,rolled back,registry.*" "Image has been rolled back." run_podman container inspect --format "{{.Image}}" $cname @@ -386,8 +408,11 @@ EOF update_log=$output assert "$update_log" =~ '.*Error: auto-updating container "[0-9a-f]{64}": invalid auto-update policy.*' "invalid policy setup" - local n_updated=$(grep -c 'Trying to pull' <<<"$update_log") - is "$n_updated" "2" "Number of images updated from registry." + # Trying to pull is not logged for podman-remote + if ! is_remote; then + local n_updated=$(grep -c 'Trying to pull' <<<"$update_log") + is "$n_updated" "2" "Number of images updated from registry." + fi for cname in "${!expect_update[@]}"; do is "$update_log" ".*$cname.*" "container with auto-update policy image updated" @@ -473,6 +498,8 @@ EOF } @test "podman-kube@.service template with rollback" { + skip_if_remote "tests depend on podman kube play --service-container which does not work with podman-remote" + # sdnotify fails with runc 1.0.0-3-dev2 on Ubuntu. Let's just # assume that we work only with crun, nothing else. # [copied from 260-sdnotify.bats] @@ -587,7 +614,15 @@ EOF # cd into the unit dir to generate the two files. pushd "$UNIT_DIR" - run_podman generate systemd --name --new --files $podname + + # podman-remote will give warning saying the generated units should be placed on the remote system + if ! is_remote; then + run_podman generate systemd --name --new --files $podname + else + + run_podman 0+w generate systemd --name --new --files $podname + fi + is "$output" ".*$podunit.*" is "$output" ".*$ctrunit.*" popd @@ -635,6 +670,8 @@ EOF } @test "podman-auto-update --authfile" { + skip_if_remote "tests depend on start_registry which does not work with podman-remote" + # Test the three supported ways of using authfiles with auto updates # 1) Passed via --authfile CLI flag # 2) Passed via the REGISTRY_AUTH_FILE env variable