From 9c923c019e0db98a9133c0820650bf6000b3c747 Mon Sep 17 00:00:00 2001 From: Stefan Nienhuis Date: Thu, 17 Sep 2026 11:08:24 +0200 Subject: [PATCH] feat: Implement auto update support for podman-remote Signed-off-by: Stefan Nienhuis --- cmd/podman/auto-update.go | 1 - hack/swagger-check | 5 ++ pkg/api/handlers/libpod/autoupdate.go | 81 +++++++++++++++++++ pkg/api/handlers/swagger/responses.go | 7 ++ pkg/api/handlers/types.go | 7 ++ pkg/api/server/register_autoupdate.go | 52 ++++++++++++ pkg/api/server/server.go | 1 + pkg/api/tags.yaml | 2 + pkg/bindings/auto-update/auto-update.go | 54 +++++++++++++ pkg/bindings/auto-update/types.go | 19 +++++ .../auto-update/types_autoupdate_options.go | 78 ++++++++++++++++++ pkg/domain/infra/tunnel/auto-update.go | 16 +++- test/apiv2/37-autoupdate.at | 6 ++ 13 files changed, 325 insertions(+), 4 deletions(-) create mode 100644 pkg/api/handlers/libpod/autoupdate.go create mode 100644 pkg/api/server/register_autoupdate.go create mode 100644 pkg/bindings/auto-update/auto-update.go create mode 100644 pkg/bindings/auto-update/types.go create mode 100644 pkg/bindings/auto-update/types_autoupdate_options.go create mode 100644 test/apiv2/37-autoupdate.at diff --git a/cmd/podman/auto-update.go b/cmd/podman/auto-update.go index d6292ee013..f495029b80 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 0c9168db0c..50adf9d747 100755 --- a/hack/swagger-check +++ b/hack/swagger-check @@ -424,6 +424,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..249df7f8db --- /dev/null +++ b/pkg/api/handlers/libpod/autoupdate.go @@ -0,0 +1,81 @@ +//go:build !remote && (linux || freebsd) + +package libpod + +import ( + "fmt" + "net/http" + + "github.com/gorilla/schema" + "go.podman.io/image/v5/types" + "go.podman.io/podman/v6/libpod" + "go.podman.io/podman/v6/pkg/api/handlers" + "go.podman.io/podman/v6/pkg/api/handlers/utils" + api "go.podman.io/podman/v6/pkg/api/types" + "go.podman.io/podman/v6/pkg/auth" + "go.podman.io/podman/v6/pkg/domain/entities" + "go.podman.io/podman/v6/pkg/domain/infra/abi" + "go.podman.io/podman/v6/pkg/errorhandling" +) + +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"` + }{ + Rollback: true, + } + + 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 b309166a45..a92d9d3eb0 100644 --- a/pkg/api/handlers/swagger/responses.go +++ b/pkg/api/handlers/swagger/responses.go @@ -532,3 +532,10 @@ type quadletRemoveResponse struct { // in:body Body entities.QuadletRemoveReport } + +// 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 68166b90e9..0593e4e374 100644 --- a/pkg/api/handlers/types.go +++ b/pkg/api/handlers/types.go @@ -249,3 +249,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..c0da73f0da --- /dev/null +++ b/pkg/api/server/register_autoupdate.go @@ -0,0 +1,52 @@ +//go:build !remote + +package server + +import ( + "net/http" + + "github.com/gorilla/mux" + "go.podman.io/podman/v6/pkg/api/handlers/libpod" +) + +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 de861da32d..7f91585e8a 100644 --- a/pkg/api/server/server.go +++ b/pkg/api/server/server.go @@ -148,6 +148,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..9ca6d467fe --- /dev/null +++ b/pkg/bindings/auto-update/auto-update.go @@ -0,0 +1,54 @@ +package autoupdate + +import ( + "context" + "net/http" + "strconv" + + imageTypes "go.podman.io/image/v5/types" + "go.podman.io/podman/v6/pkg/api/handlers" + "go.podman.io/podman/v6/pkg/auth" + "go.podman.io/podman/v6/pkg/bindings" + "go.podman.io/podman/v6/pkg/domain/entities" + "go.podman.io/podman/v6/pkg/errorhandling" +) + +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..c5246aa7a0 --- /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" + + "go.podman.io/podman/v6/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 64721bdb29..16b0f62eb6 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" + "go.podman.io/image/v5/types" + autoupdate "go.podman.io/podman/v6/pkg/bindings/auto-update" "go.podman.io/podman/v6/pkg/domain/entities" ) -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/apiv2/37-autoupdate.at b/test/apiv2/37-autoupdate.at new file mode 100644 index 0000000000..9f8adec643 --- /dev/null +++ b/test/apiv2/37-autoupdate.at @@ -0,0 +1,6 @@ +# -*- sh -*- +# +# test 'autoupdate' endpoint +# + +t POST libpod/autoupdate 200