diff options
Diffstat (limited to 'pkg/bindings')
24 files changed, 365 insertions, 176 deletions
diff --git a/pkg/bindings/connection.go b/pkg/bindings/connection.go index b994a5857..6d7b052b7 100644 --- a/pkg/bindings/connection.go +++ b/pkg/bindings/connection.go @@ -14,11 +14,9 @@ import ( "time" "github.com/blang/semver/v4" - "github.com/containers/podman/v4/pkg/terminal" + "github.com/containers/common/pkg/ssh" "github.com/containers/podman/v4/version" "github.com/sirupsen/logrus" - "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/agent" ) type APIResponse struct { @@ -74,8 +72,7 @@ func NewConnection(ctx context.Context, uri string) (context.Context, error) { // or ssh://<user>@<host>[:port]/run/podman/podman.sock?secure=True func NewConnectionWithIdentity(ctx context.Context, uri string, identity string) (context.Context, error) { var ( - err error - secure bool + err error ) if v, found := os.LookupEnv("CONTAINER_HOST"); found && uri == "" { uri = v @@ -85,11 +82,6 @@ func NewConnectionWithIdentity(ctx context.Context, uri string, identity string) identity = v } - passPhrase := "" - if v, found := os.LookupEnv("CONTAINER_PASSPHRASE"); found { - passPhrase = v - } - _url, err := url.Parse(uri) if err != nil { return nil, fmt.Errorf("value of CONTAINER_HOST is not a valid url: %s: %w", uri, err) @@ -99,11 +91,26 @@ func NewConnectionWithIdentity(ctx context.Context, uri string, identity string) var connection Connection switch _url.Scheme { case "ssh": - secure, err = strconv.ParseBool(_url.Query().Get("secure")) + port, err := strconv.Atoi(_url.Port()) if err != nil { - secure = false + return nil, err } - connection, err = sshClient(_url, secure, passPhrase, identity) + conn, err := ssh.Dial(&ssh.ConnectionDialOptions{ + Host: uri, + Identity: identity, + User: _url.User, + Port: port, + }, "golang") + if err != nil { + return nil, err + } + connection = Connection{URI: _url} + connection.Client = &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + return ssh.DialNet(conn, "unix", _url) + }, + }} case "unix": if !strings.HasPrefix(uri, "unix:///") { // autofix unix://path_element vs unix:///path_element @@ -184,124 +191,6 @@ func pingNewConnection(ctx context.Context) (*semver.Version, error) { return nil, fmt.Errorf("ping response was %d", response.StatusCode) } -func sshClient(_url *url.URL, secure bool, passPhrase string, identity string) (Connection, error) { - // if you modify the authmethods or their conditionals, you will also need to make similar - // changes in the client (currently cmd/podman/system/connection/add getUDS). - - var signers []ssh.Signer // order Signers are appended to this list determines which key is presented to server - - if len(identity) > 0 { - s, err := terminal.PublicKey(identity, []byte(passPhrase)) - if err != nil { - return Connection{}, fmt.Errorf("failed to parse identity %q: %w", identity, err) - } - - signers = append(signers, s) - logrus.Debugf("SSH Ident Key %q %s %s", identity, ssh.FingerprintSHA256(s.PublicKey()), s.PublicKey().Type()) - } - - if sock, found := os.LookupEnv("SSH_AUTH_SOCK"); found { - logrus.Debugf("Found SSH_AUTH_SOCK %q, ssh-agent signer(s) enabled", sock) - - c, err := net.Dial("unix", sock) - if err != nil { - return Connection{}, err - } - - agentSigners, err := agent.NewClient(c).Signers() - if err != nil { - return Connection{}, err - } - signers = append(signers, agentSigners...) - - if logrus.IsLevelEnabled(logrus.DebugLevel) { - for _, s := range agentSigners { - logrus.Debugf("SSH Agent Key %s %s", ssh.FingerprintSHA256(s.PublicKey()), s.PublicKey().Type()) - } - } - } - - var authMethods []ssh.AuthMethod - if len(signers) > 0 { - var dedup = make(map[string]ssh.Signer) - // Dedup signers based on fingerprint, ssh-agent keys override CONTAINER_SSHKEY - for _, s := range signers { - fp := ssh.FingerprintSHA256(s.PublicKey()) - if _, found := dedup[fp]; found { - logrus.Debugf("Dedup SSH Key %s %s", ssh.FingerprintSHA256(s.PublicKey()), s.PublicKey().Type()) - } - dedup[fp] = s - } - - var uniq []ssh.Signer - for _, s := range dedup { - uniq = append(uniq, s) - } - authMethods = append(authMethods, ssh.PublicKeysCallback(func() ([]ssh.Signer, error) { - return uniq, nil - })) - } - - if pw, found := _url.User.Password(); found { - authMethods = append(authMethods, ssh.Password(pw)) - } - - if len(authMethods) == 0 { - callback := func() (string, error) { - pass, err := terminal.ReadPassword("Login password:") - return string(pass), err - } - authMethods = append(authMethods, ssh.PasswordCallback(callback)) - } - - port := _url.Port() - if port == "" { - port = "22" - } - - callback := ssh.InsecureIgnoreHostKey() - if secure { - host := _url.Hostname() - if port != "22" { - host = fmt.Sprintf("[%s]:%s", host, port) - } - key := terminal.HostKey(host) - if key != nil { - callback = ssh.FixedHostKey(key) - } - } - - bastion, err := ssh.Dial("tcp", - net.JoinHostPort(_url.Hostname(), port), - &ssh.ClientConfig{ - User: _url.User.Username(), - Auth: authMethods, - HostKeyCallback: callback, - HostKeyAlgorithms: []string{ - ssh.KeyAlgoRSA, - ssh.KeyAlgoDSA, - ssh.KeyAlgoECDSA256, - ssh.KeyAlgoECDSA384, - ssh.KeyAlgoECDSA521, - ssh.KeyAlgoED25519, - }, - Timeout: 5 * time.Second, - }, - ) - if err != nil { - return Connection{}, fmt.Errorf("connection to bastion host (%s) failed: %w", _url.String(), err) - } - - connection := Connection{URI: _url} - connection.Client = &http.Client{ - Transport: &http.Transport{ - DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { - return bastion.Dial("unix", _url.Path) - }, - }} - return connection, nil -} - func unixClient(_url *url.URL) Connection { connection := Connection{URI: _url} connection.Client = &http.Client{ diff --git a/pkg/bindings/containers/checkpoint.go b/pkg/bindings/containers/checkpoint.go index bcb944488..8c072f588 100644 --- a/pkg/bindings/containers/checkpoint.go +++ b/pkg/bindings/containers/checkpoint.go @@ -39,7 +39,7 @@ func Checkpoint(ctx context.Context, nameOrID string, options *CheckpointOptions } defer response.Body.Close() - if !export { + if response.StatusCode != http.StatusOK || !export { return &report, response.Process(&report) } diff --git a/pkg/bindings/containers/exec.go b/pkg/bindings/containers/exec.go index 3d19fb812..c5c8760ed 100644 --- a/pkg/bindings/containers/exec.go +++ b/pkg/bindings/containers/exec.go @@ -33,7 +33,7 @@ func ExecCreate(ctx context.Context, nameOrID string, config *handlers.ExecCreat requestJSON, err := json.Marshal(config) if err != nil { - return "", fmt.Errorf("error marshalling exec config to JSON: %w", err) + return "", fmt.Errorf("marshalling exec config to JSON: %w", err) } jsonReader := strings.NewReader(string(requestJSON)) diff --git a/pkg/bindings/containers/update.go b/pkg/bindings/containers/update.go new file mode 100644 index 000000000..7cda7c306 --- /dev/null +++ b/pkg/bindings/containers/update.go @@ -0,0 +1,31 @@ +package containers + +import ( + "context" + "net/http" + "strings" + + "github.com/containers/podman/v4/pkg/bindings" + "github.com/containers/podman/v4/pkg/domain/entities" + jsoniter "github.com/json-iterator/go" +) + +func Update(ctx context.Context, options *entities.ContainerUpdateOptions) (string, error) { + conn, err := bindings.GetClient(ctx) + if err != nil { + return "", err + } + + resources, err := jsoniter.MarshalToString(options.Specgen.ResourceLimits) + if err != nil { + return "", err + } + stringReader := strings.NewReader(resources) + response, err := conn.DoRequest(ctx, stringReader, http.MethodPost, "/containers/%s/update", nil, nil, options.NameOrID) + if err != nil { + return "", err + } + defer response.Body.Close() + + return options.NameOrID, response.Process(nil) +} diff --git a/pkg/bindings/generate/types.go b/pkg/bindings/generate/types.go index 25c398c8b..31b43897c 100644 --- a/pkg/bindings/generate/types.go +++ b/pkg/bindings/generate/types.go @@ -38,4 +38,6 @@ type SystemdOptions struct { After *[]string // Requires - systemd requires list for the container or pods Requires *[]string + // AdditionalEnvVariables - Sets environment variables to a systemd unit file + AdditionalEnvVariables *[]string } diff --git a/pkg/bindings/generate/types_systemd_options.go b/pkg/bindings/generate/types_systemd_options.go index 4d436945b..3aec33a54 100644 --- a/pkg/bindings/generate/types_systemd_options.go +++ b/pkg/bindings/generate/types_systemd_options.go @@ -226,3 +226,18 @@ func (o *SystemdOptions) GetRequires() []string { } return *o.Requires } + +// WithAdditionalEnvVariables set field AdditionalEnvVariables to given value +func (o *SystemdOptions) WithAdditionalEnvVariables(value []string) *SystemdOptions { + o.AdditionalEnvVariables = &value + return o +} + +// GetAdditionalEnvVariables returns value of field AdditionalEnvVariables +func (o *SystemdOptions) GetAdditionalEnvVariables() []string { + if o.AdditionalEnvVariables == nil { + var z []string + return z + } + return *o.AdditionalEnvVariables +} diff --git a/pkg/bindings/images/build.go b/pkg/bindings/images/build.go index 6883585e2..ef875c9eb 100644 --- a/pkg/bindings/images/build.go +++ b/pkg/bindings/images/build.go @@ -88,6 +88,13 @@ func Build(ctx context.Context, containerFiles []string, options entities.BuildO } params.Set("additionalbuildcontexts", string(additionalBuildContextMap)) } + if options.IDMappingOptions != nil { + idmappingsOptions, err := jsoniter.Marshal(options.IDMappingOptions) + if err != nil { + return nil, err + } + params.Set("idmappingoptions", string(idmappingsOptions)) + } if buildArgs := options.Args; len(buildArgs) > 0 { bArgs, err := jsoniter.MarshalToString(buildArgs) if err != nil { @@ -224,6 +231,15 @@ func Build(ctx context.Context, containerFiles []string, options entities.BuildO if len(options.Manifest) > 0 { params.Set("manifest", options.Manifest) } + if options.CacheFrom != nil { + params.Set("cachefrom", options.CacheFrom.String()) + } + if options.CacheTo != nil { + params.Set("cacheto", options.CacheTo.String()) + } + if int64(options.CacheTTL) != 0 { + params.Set("cachettl", options.CacheTTL.String()) + } if memSwap := options.CommonBuildOpts.MemorySwap; memSwap > 0 { params.Set("memswap", strconv.Itoa(int(memSwap))) } @@ -574,7 +590,7 @@ func Build(ctx context.Context, containerFiles []string, options entities.BuildO func nTar(excludes []string, sources ...string) (io.ReadCloser, error) { pm, err := fileutils.NewPatternMatcher(excludes) if err != nil { - return nil, fmt.Errorf("error processing excludes list %v: %w", excludes, err) + return nil, fmt.Errorf("processing excludes list %v: %w", excludes, err) } if len(sources) == 0 { @@ -623,7 +639,7 @@ func nTar(excludes []string, sources ...string) (io.ReadCloser, error) { excluded, err := pm.Matches(name) //nolint:staticcheck if err != nil { - return fmt.Errorf("error checking if %q is excluded: %w", name, err) + return fmt.Errorf("checking if %q is excluded: %w", name, err) } if excluded { // Note: filepath.SkipDir is not possible to use given .dockerignore semantics. @@ -726,7 +742,7 @@ func parseDockerignore(root string) ([]string, error) { var dockerIgnoreErr error ignore, dockerIgnoreErr = ioutil.ReadFile(filepath.Join(root, ".dockerignore")) if dockerIgnoreErr != nil && !os.IsNotExist(dockerIgnoreErr) { - return nil, fmt.Errorf("error reading .containerignore: '%s': %w", root, err) + return nil, err } } rawexcludes := strings.Split(string(ignore), "\n") diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go index bb7867c4e..ea7d445db 100644 --- a/pkg/bindings/images/images.go +++ b/pkg/bindings/images/images.go @@ -282,9 +282,9 @@ func Search(ctx context.Context, term string, options *SearchOptions) ([]entitie } params.Set("term", term) - // Note: we have to verify if skipped is false. + // SkipTLSVerify is special. It's not being serialized by ToParams() + // because we need to flip the boolean. if options.SkipTLSVerify != nil { - params.Del("SkipTLSVerify") params.Set("tlsVerify", strconv.FormatBool(!options.GetSkipTLSVerify())) } diff --git a/pkg/bindings/images/pull.go b/pkg/bindings/images/pull.go index 1a4aa3038..8caf45c0e 100644 --- a/pkg/bindings/images/pull.go +++ b/pkg/bindings/images/pull.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/http" "os" "strconv" @@ -36,9 +35,9 @@ func Pull(ctx context.Context, rawImage string, options *PullOptions) ([]string, } params.Set("reference", rawImage) + // SkipTLSVerify is special. It's not being serialized by ToParams() + // because we need to flip the boolean. if options.SkipTLSVerify != nil { - params.Del("SkipTLSVerify") - // Note: we have to verify if skipped is false. params.Set("tlsVerify", strconv.FormatBool(!options.GetSkipTLSVerify())) } @@ -57,10 +56,14 @@ func Pull(ctx context.Context, rawImage string, options *PullOptions) ([]string, return nil, response.Process(err) } - // Historically pull writes status to stderr - stderr := io.Writer(os.Stderr) + var writer io.Writer if options.GetQuiet() { - stderr = ioutil.Discard + writer = io.Discard + } else if progressWriter := options.GetProgressWriter(); progressWriter != nil { + writer = progressWriter + } else { + // Historically push writes status to stderr + writer = os.Stderr } dec := json.NewDecoder(response.Body) @@ -84,7 +87,7 @@ func Pull(ctx context.Context, rawImage string, options *PullOptions) ([]string, switch { case report.Stream != "": - fmt.Fprint(stderr, report.Stream) + fmt.Fprint(writer, report.Stream) case report.Error != "": pullErrors = append(pullErrors, errors.New(report.Error)) case len(report.Images) > 0: diff --git a/pkg/bindings/images/push.go b/pkg/bindings/images/push.go index 8db3726e6..0e1309e91 100644 --- a/pkg/bindings/images/push.go +++ b/pkg/bindings/images/push.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/http" "os" "strconv" @@ -39,10 +38,9 @@ func Push(ctx context.Context, source string, destination string, options *PushO if err != nil { return err } - // SkipTLSVerify is special. We need to delete the param added by - // toparams and change the key and flip the bool + // SkipTLSVerify is special. It's not being serialized by ToParams() + // because we need to flip the boolean. if options.SkipTLSVerify != nil { - params.Del("SkipTLSVerify") params.Set("tlsVerify", strconv.FormatBool(!options.GetSkipTLSVerify())) } params.Set("destination", destination) @@ -58,10 +56,14 @@ func Push(ctx context.Context, source string, destination string, options *PushO return response.Process(err) } - // Historically push writes status to stderr - writer := io.Writer(os.Stderr) + var writer io.Writer if options.GetQuiet() { - writer = ioutil.Discard + writer = io.Discard + } else if progressWriter := options.GetProgressWriter(); progressWriter != nil { + writer = progressWriter + } else { + // Historically push writes status to stderr + writer = os.Stderr } dec := json.NewDecoder(response.Body) diff --git a/pkg/bindings/images/types.go b/pkg/bindings/images/types.go index 0664afc1b..f8630926e 100644 --- a/pkg/bindings/images/types.go +++ b/pkg/bindings/images/types.go @@ -1,6 +1,8 @@ package images import ( + "io" + buildahDefine "github.com/containers/buildah/define" ) @@ -15,6 +17,8 @@ type RemoveOptions struct { Ignore *bool // Confirms if given name is a manifest list and removes it, otherwise returns error. LookupManifest *bool + // Does not remove dangling parent images + NoPrune *bool } //go:generate go run ../generator/generator.go DiffOptions @@ -129,8 +133,12 @@ type PushOptions struct { Format *string // Password for authenticating against the registry. Password *string + // ProgressWriter is a writer where push progress are sent. + // Since API handler for image push is quiet by default, WithQuiet(false) is necessary for + // the writer to receive progress messages. + ProgressWriter *io.Writer `schema:"-"` // SkipTLSVerify to skip HTTPS and certificate verification. - SkipTLSVerify *bool + SkipTLSVerify *bool `schema:"-"` // RemoveSignatures Discard any pre-existing signatures in the image. RemoveSignatures *bool // Username for authenticating against the registry. @@ -150,7 +158,7 @@ type SearchOptions struct { // Limit the number of results. Limit *int // SkipTLSVerify to skip HTTPS and certificate verification. - SkipTLSVerify *bool + SkipTLSVerify *bool `schema:"-"` // ListTags search the available tags of the repository ListTags *bool } @@ -174,11 +182,13 @@ type PullOptions struct { Policy *string // Password for authenticating against the registry. Password *string + // ProgressWriter is a writer where pull progress are sent. + ProgressWriter *io.Writer `schema:"-"` // Quiet can be specified to suppress pull progress when pulling. Ignored // for remote calls. Quiet *bool // SkipTLSVerify to skip HTTPS and certificate verification. - SkipTLSVerify *bool + SkipTLSVerify *bool `schema:"-"` // Username for authenticating against the registry. Username *string // Variant will overwrite the local variant for image pulls. diff --git a/pkg/bindings/images/types_pull_options.go b/pkg/bindings/images/types_pull_options.go index 4cd525185..c1a88fd9e 100644 --- a/pkg/bindings/images/types_pull_options.go +++ b/pkg/bindings/images/types_pull_options.go @@ -2,6 +2,7 @@ package images import ( + "io" "net/url" "github.com/containers/podman/v4/pkg/bindings/internal/util" @@ -107,6 +108,21 @@ func (o *PullOptions) GetPassword() string { return *o.Password } +// WithProgressWriter set field ProgressWriter to given value +func (o *PullOptions) WithProgressWriter(value io.Writer) *PullOptions { + o.ProgressWriter = &value + return o +} + +// GetProgressWriter returns value of field ProgressWriter +func (o *PullOptions) GetProgressWriter() io.Writer { + if o.ProgressWriter == nil { + var z io.Writer + return z + } + return *o.ProgressWriter +} + // WithQuiet set field Quiet to given value func (o *PullOptions) WithQuiet(value bool) *PullOptions { o.Quiet = &value diff --git a/pkg/bindings/images/types_push_options.go b/pkg/bindings/images/types_push_options.go index 1ae031824..817d873f8 100644 --- a/pkg/bindings/images/types_push_options.go +++ b/pkg/bindings/images/types_push_options.go @@ -2,6 +2,7 @@ package images import ( + "io" "net/url" "github.com/containers/podman/v4/pkg/bindings/internal/util" @@ -107,6 +108,21 @@ func (o *PushOptions) GetPassword() string { return *o.Password } +// WithProgressWriter set field ProgressWriter to given value +func (o *PushOptions) WithProgressWriter(value io.Writer) *PushOptions { + o.ProgressWriter = &value + return o +} + +// GetProgressWriter returns value of field ProgressWriter +func (o *PushOptions) GetProgressWriter() io.Writer { + if o.ProgressWriter == nil { + var z io.Writer + return z + } + return *o.ProgressWriter +} + // WithSkipTLSVerify set field SkipTLSVerify to given value func (o *PushOptions) WithSkipTLSVerify(value bool) *PushOptions { o.SkipTLSVerify = &value diff --git a/pkg/bindings/images/types_remove_options.go b/pkg/bindings/images/types_remove_options.go index 559ebcfd5..8972ac93c 100644 --- a/pkg/bindings/images/types_remove_options.go +++ b/pkg/bindings/images/types_remove_options.go @@ -76,3 +76,18 @@ func (o *RemoveOptions) GetLookupManifest() bool { } return *o.LookupManifest } + +// WithNoPrune set field NoPrune to given value +func (o *RemoveOptions) WithNoPrune(value bool) *RemoveOptions { + o.NoPrune = &value + return o +} + +// GetNoPrune returns value of field NoPrune +func (o *RemoveOptions) GetNoPrune() bool { + if o.NoPrune == nil { + var z bool + return z + } + return *o.NoPrune +} diff --git a/pkg/bindings/internal/util/util.go b/pkg/bindings/internal/util/util.go index f8f99d6c1..52ce14738 100644 --- a/pkg/bindings/internal/util/util.go +++ b/pkg/bindings/internal/util/util.go @@ -74,6 +74,9 @@ func ToParams(o interface{}) (url.Values, error) { } paramName := fieldName if pn, ok := sType.Field(i).Tag.Lookup("schema"); ok { + if pn == "-" { + continue + } paramName = pn } switch { diff --git a/pkg/bindings/kube/kube.go b/pkg/bindings/kube/kube.go index b9cc0efa7..1b9f888ef 100644 --- a/pkg/bindings/kube/kube.go +++ b/pkg/bindings/kube/kube.go @@ -10,6 +10,7 @@ import ( "github.com/containers/image/v5/types" "github.com/containers/podman/v4/pkg/auth" "github.com/containers/podman/v4/pkg/bindings" + "github.com/containers/podman/v4/pkg/bindings/generate" "github.com/containers/podman/v4/pkg/domain/entities" "github.com/sirupsen/logrus" ) @@ -39,8 +40,10 @@ func PlayWithBody(ctx context.Context, body io.Reader, options *PlayOptions) (*e if err != nil { return nil, err } + // SkipTLSVerify is special. It's not being serialized by ToParams() + // because we need to flip the boolean. if options.SkipTLSVerify != nil { - params.Set("tlsVerify", strconv.FormatBool(options.GetSkipTLSVerify())) + params.Set("tlsVerify", strconv.FormatBool(!options.GetSkipTLSVerify())) } if options.Start != nil { params.Set("start", strconv.FormatBool(options.GetStart())) @@ -51,7 +54,7 @@ func PlayWithBody(ctx context.Context, body io.Reader, options *PlayOptions) (*e return nil, err } - response, err := conn.DoRequest(ctx, body, http.MethodPost, "/kube/play", params, header) + response, err := conn.DoRequest(ctx, body, http.MethodPost, "/play/kube", params, header) if err != nil { return nil, err } @@ -85,7 +88,7 @@ func DownWithBody(ctx context.Context, body io.Reader) (*entities.KubePlayReport return nil, err } - response, err := conn.DoRequest(ctx, body, http.MethodDelete, "/kube/play", nil, nil) + response, err := conn.DoRequest(ctx, body, http.MethodDelete, "/play/kube", nil, nil) if err != nil { return nil, err } @@ -94,3 +97,8 @@ func DownWithBody(ctx context.Context, body io.Reader) (*entities.KubePlayReport } return &report, nil } + +// Kube generate Kubernetes YAML (v1 specification) +func Generate(ctx context.Context, nameOrIDs []string, options generate.KubeOptions) (*entities.GenerateKubeReport, error) { + return generate.Kube(ctx, nameOrIDs, &options) +} diff --git a/pkg/bindings/kube/types.go b/pkg/bindings/kube/types.go index 783d1912a..279a9f8f3 100644 --- a/pkg/bindings/kube/types.go +++ b/pkg/bindings/kube/types.go @@ -27,7 +27,7 @@ type PlayOptions struct { SignaturePolicy *string // SkipTLSVerify - skip https and certificate validation when // contacting container registries. - SkipTLSVerify *bool + SkipTLSVerify *bool `schema:"-"` // SeccompProfileRoot - path to a directory containing seccomp // profiles. SeccompProfileRoot *string diff --git a/pkg/bindings/manifests/manifests.go b/pkg/bindings/manifests/manifests.go index 80153c4b4..752366937 100644 --- a/pkg/bindings/manifests/manifests.go +++ b/pkg/bindings/manifests/manifests.go @@ -2,10 +2,13 @@ package manifests import ( "context" + "encoding/json" "errors" "fmt" + "io" "io/ioutil" "net/http" + "os" "strconv" "strings" @@ -142,7 +145,6 @@ func Delete(ctx context.Context, name string) (*entities.ManifestRemoveReport, e // the name will be used instead. If the optional all boolean is specified, all images specified // in the list will be pushed as well. func Push(ctx context.Context, name, destination string, options *images.PushOptions) (string, error) { - var idr entities.IDResponse if options == nil { options = new(images.PushOptions) } @@ -163,10 +165,9 @@ func Push(ctx context.Context, name, destination string, options *images.PushOpt if err != nil { return "", err } - // SkipTLSVerify is special. We need to delete the param added by - // ToParams() and change the key and flip the bool + // SkipTLSVerify is special. It's not being serialized by ToParams() + // because we need to flip the boolean. if options.SkipTLSVerify != nil { - params.Del("SkipTLSVerify") params.Set("tlsVerify", strconv.FormatBool(!options.GetSkipTLSVerify())) } @@ -176,7 +177,46 @@ func Push(ctx context.Context, name, destination string, options *images.PushOpt } defer response.Body.Close() - return idr.ID, response.Process(&idr) + if !response.IsSuccess() { + return "", response.Process(err) + } + + var writer io.Writer + if options.GetQuiet() { + writer = io.Discard + } else if progressWriter := options.GetProgressWriter(); progressWriter != nil { + writer = progressWriter + } else { + // Historically push writes status to stderr + writer = os.Stderr + } + + dec := json.NewDecoder(response.Body) + for { + var report entities.ManifestPushReport + if err := dec.Decode(&report); err != nil { + return "", err + } + + select { + case <-response.Request.Context().Done(): + break + default: + // non-blocking select + } + + switch { + case report.ID != "": + return report.ID, nil + case report.Stream != "": + fmt.Fprint(writer, report.Stream) + case report.Error != "": + // There can only be one error. + return "", errors.New(report.Error) + default: + return "", fmt.Errorf("failed to parse push results stream, unexpected input: %v", report) + } + } } // Modify modifies the given manifest list using options and the optional list of images @@ -205,10 +245,9 @@ func Modify(ctx context.Context, name string, images []string, options *ModifyOp if err != nil { return "", err } - // SkipTLSVerify is special. We need to delete the param added by - // ToParams() and change the key and flip the bool + // SkipTLSVerify is special. It's not being serialized by ToParams() + // because we need to flip the boolean. if options.SkipTLSVerify != nil { - params.Del("SkipTLSVerify") params.Set("tlsVerify", strconv.FormatBool(!options.GetSkipTLSVerify())) } diff --git a/pkg/bindings/manifests/types.go b/pkg/bindings/manifests/types.go index e23ef798d..fec3f9d13 100644 --- a/pkg/bindings/manifests/types.go +++ b/pkg/bindings/manifests/types.go @@ -8,7 +8,8 @@ type InspectOptions struct { //go:generate go run ../generator/generator.go CreateOptions // CreateOptions are optional options for creating manifests type CreateOptions struct { - All *bool + All *bool + Amend *bool } //go:generate go run ../generator/generator.go ExistsOptions @@ -31,7 +32,7 @@ type AddOptions struct { Authfile *string Password *string Username *string - SkipTLSVerify *bool + SkipTLSVerify *bool `schema:"-"` } //go:generate go run ../generator/generator.go RemoveOptions @@ -59,5 +60,5 @@ type ModifyOptions struct { Authfile *string Password *string Username *string - SkipTLSVerify *bool + SkipTLSVerify *bool `schema:"-"` } diff --git a/pkg/bindings/manifests/types_create_options.go b/pkg/bindings/manifests/types_create_options.go index 960332a82..09942c00a 100644 --- a/pkg/bindings/manifests/types_create_options.go +++ b/pkg/bindings/manifests/types_create_options.go @@ -31,3 +31,18 @@ func (o *CreateOptions) GetAll() bool { } return *o.All } + +// WithAmend set field Amend to given value +func (o *CreateOptions) WithAmend(value bool) *CreateOptions { + o.Amend = &value + return o +} + +// GetAmend returns value of field Amend +func (o *CreateOptions) GetAmend() bool { + if o.Amend == nil { + var z bool + return z + } + return *o.Amend +} diff --git a/pkg/bindings/system/system.go b/pkg/bindings/system/system.go index dae80384b..733b2cb5c 100644 --- a/pkg/bindings/system/system.go +++ b/pkg/bindings/system/system.go @@ -36,8 +36,9 @@ func Events(ctx context.Context, eventChan chan entities.Event, cancelChan chan if cancelChan != nil { go func() { <-cancelChan - err = response.Body.Close() - logrus.Errorf("Unable to close event response body: %v", err) + if err := response.Body.Close(); err != nil { + logrus.Errorf("Unable to close event response body: %v", err) + } }() } diff --git a/pkg/bindings/test/images_test.go b/pkg/bindings/test/images_test.go index 8f76ce456..53c5a1e83 100644 --- a/pkg/bindings/test/images_test.go +++ b/pkg/bindings/test/images_test.go @@ -1,11 +1,14 @@ package bindings_test import ( + "bytes" + "fmt" "net/http" "os" "path/filepath" "time" + podmanRegistry "github.com/containers/podman/v4/hack/podman-registry-go" "github.com/containers/podman/v4/pkg/bindings" "github.com/containers/podman/v4/pkg/bindings/containers" "github.com/containers/podman/v4/pkg/bindings/images" @@ -362,9 +365,14 @@ var _ = Describe("Podman images", func() { It("Image Pull", func() { rawImage := "docker.io/library/busybox:latest" - pulledImages, err := images.Pull(bt.conn, rawImage, nil) + var writer bytes.Buffer + pullOpts := new(images.PullOptions).WithProgressWriter(&writer) + pulledImages, err := images.Pull(bt.conn, rawImage, pullOpts) Expect(err).NotTo(HaveOccurred()) Expect(len(pulledImages)).To(Equal(1)) + output := writer.String() + Expect(output).To(ContainSubstring("Trying to pull ")) + Expect(output).To(ContainSubstring("Getting image source signatures")) exists, err := images.Exists(bt.conn, rawImage, nil) Expect(err).NotTo(HaveOccurred()) @@ -379,6 +387,22 @@ var _ = Describe("Podman images", func() { Expect(err).To(HaveOccurred()) }) + It("Image Push", func() { + registry, err := podmanRegistry.Start() + Expect(err).To(BeNil()) + + var writer bytes.Buffer + pushOpts := new(images.PushOptions).WithUsername(registry.User).WithPassword(registry.Password).WithSkipTLSVerify(true).WithProgressWriter(&writer).WithQuiet(false) + err = images.Push(bt.conn, alpine.name, fmt.Sprintf("localhost:%s/test:latest", registry.Port), pushOpts) + Expect(err).ToNot(HaveOccurred()) + + output := writer.String() + Expect(output).To(ContainSubstring("Copying blob ")) + Expect(output).To(ContainSubstring("Copying config ")) + Expect(output).To(ContainSubstring("Writing manifest to image destination")) + Expect(output).To(ContainSubstring("Storing signatures")) + }) + It("Build no options", func() { results, err := images.Build(bt.conn, []string{"fixture/Containerfile"}, entities.BuildOptions{}) Expect(err).ToNot(HaveOccurred()) diff --git a/pkg/bindings/test/manifests_test.go b/pkg/bindings/test/manifests_test.go index 6a34ef5a6..d6749f920 100644 --- a/pkg/bindings/test/manifests_test.go +++ b/pkg/bindings/test/manifests_test.go @@ -1,9 +1,12 @@ package bindings_test import ( + "bytes" + "fmt" "net/http" "time" + podmanRegistry "github.com/containers/podman/v4/hack/podman-registry-go" "github.com/containers/podman/v4/pkg/bindings" "github.com/containers/podman/v4/pkg/bindings/images" "github.com/containers/podman/v4/pkg/bindings/manifests" @@ -12,7 +15,7 @@ import ( "github.com/onsi/gomega/gexec" ) -var _ = Describe("podman manifest", func() { +var _ = Describe("Podman manifests", func() { var ( bt *bindingTest s *gexec.Session @@ -172,7 +175,21 @@ var _ = Describe("podman manifest", func() { Expect(list.Manifests[0].Platform.OS).To(Equal("foo")) }) - It("push manifest", func() { - Skip("TODO: implement test for manifest push to registry") + It("Manifest Push", func() { + registry, err := podmanRegistry.Start() + Expect(err).To(BeNil()) + + name := "quay.io/libpod/foobar:latest" + _, err = manifests.Create(bt.conn, name, []string{alpine.name}, nil) + Expect(err).ToNot(HaveOccurred()) + + var writer bytes.Buffer + pushOpts := new(images.PushOptions).WithUsername(registry.User).WithPassword(registry.Password).WithAll(true).WithSkipTLSVerify(true).WithProgressWriter(&writer).WithQuiet(false) + _, err = manifests.Push(bt.conn, name, fmt.Sprintf("localhost:%s/test:latest", registry.Port), pushOpts) + Expect(err).ToNot(HaveOccurred()) + + output := writer.String() + Expect(output).To(ContainSubstring("Writing manifest list to image destination")) + Expect(output).To(ContainSubstring("Storing list signatures")) }) }) diff --git a/pkg/bindings/test/types_test.go b/pkg/bindings/test/types_test.go new file mode 100644 index 000000000..bc98c8b7d --- /dev/null +++ b/pkg/bindings/test/types_test.go @@ -0,0 +1,66 @@ +package bindings_test + +import ( + "bytes" + + "github.com/containers/podman/v4/pkg/bindings/images" + "github.com/containers/podman/v4/pkg/bindings/kube" + "github.com/containers/podman/v4/pkg/bindings/manifests" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Binding types", func() { + It("serialize image pull options", func() { + var writer bytes.Buffer + opts := new(images.PullOptions).WithOS("foo").WithProgressWriter(&writer).WithSkipTLSVerify(true) + params, err := opts.ToParams() + Expect(err).ToNot(HaveOccurred()) + Expect(params.Get("os")).To(Equal("foo")) + Expect(params.Has("progresswriter")).To(BeFalse()) + Expect(params.Has("skiptlsverify")).To(BeFalse()) + }) + + It("serialize image push options", func() { + var writer bytes.Buffer + opts := new(images.PushOptions).WithAll(true).WithProgressWriter(&writer).WithSkipTLSVerify(true) + params, err := opts.ToParams() + Expect(err).ToNot(HaveOccurred()) + Expect(params.Get("all")).To(Equal("true")) + Expect(params.Has("progresswriter")).To(BeFalse()) + Expect(params.Has("skiptlsverify")).To(BeFalse()) + }) + + It("serialize image search options", func() { + opts := new(images.SearchOptions).WithLimit(123).WithSkipTLSVerify(true) + params, err := opts.ToParams() + Expect(err).ToNot(HaveOccurred()) + Expect(params.Get("limit")).To(Equal("123")) + Expect(params.Has("skiptlsverify")).To(BeFalse()) + }) + + It("serialize manifest modify options", func() { + opts := new(manifests.ModifyOptions).WithOS("foo").WithSkipTLSVerify(true) + params, err := opts.ToParams() + Expect(err).ToNot(HaveOccurred()) + Expect(params.Get("os")).To(Equal("foo")) + Expect(params.Has("skiptlsverify")).To(BeFalse()) + }) + + It("serialize manifest add options", func() { + opts := new(manifests.AddOptions).WithAll(true).WithOS("foo").WithSkipTLSVerify(true) + params, err := opts.ToParams() + Expect(err).ToNot(HaveOccurred()) + Expect(params.Get("all")).To(Equal("true")) + Expect(params.Get("os")).To(Equal("foo")) + Expect(params.Has("skiptlsverify")).To(BeFalse()) + }) + + It("serialize kube play options", func() { + opts := new(kube.PlayOptions).WithQuiet(true).WithSkipTLSVerify(true) + params, err := opts.ToParams() + Expect(err).ToNot(HaveOccurred()) + Expect(params.Get("quiet")).To(Equal("true")) + Expect(params.Has("skiptlsverify")).To(BeFalse()) + }) +}) |