mirror of https://github.com/containers/podman.git
Merge pull request #19696 from Luap99/api-stream-format
api docs: document stream format
This commit is contained in:
commit
8bda49608f
|
@ -608,7 +608,7 @@ func (r *ConmonOCIRuntime) HTTPAttach(ctr *Container, req *http.Request, w http.
|
||||||
|
|
||||||
hijackDone <- true
|
hijackDone <- true
|
||||||
|
|
||||||
writeHijackHeader(req, httpBuf)
|
writeHijackHeader(req, httpBuf, isTerminal)
|
||||||
|
|
||||||
// Force a flush after the header is written.
|
// Force a flush after the header is written.
|
||||||
if err := httpBuf.Flush(); err != nil {
|
if err := httpBuf.Flush(); err != nil {
|
||||||
|
|
|
@ -569,7 +569,7 @@ func attachExecHTTP(c *Container, sessionID string, r *http.Request, w http.Resp
|
||||||
hijackDone <- true
|
hijackDone <- true
|
||||||
|
|
||||||
// Write a header to let the client know what happened
|
// Write a header to let the client know what happened
|
||||||
writeHijackHeader(r, httpBuf)
|
writeHijackHeader(r, httpBuf, isTerminal)
|
||||||
|
|
||||||
// Force a flush after the header is written.
|
// Force a flush after the header is written.
|
||||||
if err := httpBuf.Flush(); err != nil {
|
if err := httpBuf.Flush(); err != nil {
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/containers/common/libnetwork/types"
|
"github.com/containers/common/libnetwork/types"
|
||||||
"github.com/containers/common/pkg/config"
|
"github.com/containers/common/pkg/config"
|
||||||
"github.com/containers/podman/v4/libpod/define"
|
"github.com/containers/podman/v4/libpod/define"
|
||||||
|
"github.com/containers/podman/v4/pkg/api/handlers/utils/apiutil"
|
||||||
spec "github.com/opencontainers/runtime-spec/specs-go"
|
spec "github.com/opencontainers/runtime-spec/specs-go"
|
||||||
"github.com/opencontainers/selinux/go-selinux/label"
|
"github.com/opencontainers/selinux/go-selinux/label"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
@ -182,22 +183,36 @@ func makeHTTPAttachHeader(stream byte, length uint32) []byte {
|
||||||
|
|
||||||
// writeHijackHeader writes a header appropriate for the type of HTTP Hijack
|
// writeHijackHeader writes a header appropriate for the type of HTTP Hijack
|
||||||
// that occurred in a hijacked HTTP connection used for attach.
|
// that occurred in a hijacked HTTP connection used for attach.
|
||||||
func writeHijackHeader(r *http.Request, conn io.Writer) {
|
func writeHijackHeader(r *http.Request, conn io.Writer, tty bool) {
|
||||||
// AttachHeader is the literal header sent for upgraded/hijacked connections for
|
// AttachHeader is the literal header sent for upgraded/hijacked connections for
|
||||||
// attach, sourced from Docker at:
|
// attach, sourced from Docker at:
|
||||||
// https://raw.githubusercontent.com/moby/moby/b95fad8e51bd064be4f4e58a996924f343846c85/api/server/router/container/container_routes.go
|
// https://raw.githubusercontent.com/moby/moby/b95fad8e51bd064be4f4e58a996924f343846c85/api/server/router/container/container_routes.go
|
||||||
// Using literally to ensure compatibility with existing clients.
|
// Using literally to ensure compatibility with existing clients.
|
||||||
|
|
||||||
|
// New docker API uses a different header for the non tty case.
|
||||||
|
// Lets do the same for libpod. Only do this for the new api versions to not break older clients.
|
||||||
|
header := "application/vnd.docker.raw-stream"
|
||||||
|
if !tty {
|
||||||
|
version := "4.7.0"
|
||||||
|
if !apiutil.IsLibpodRequest(r) {
|
||||||
|
version = "1.42.0" // docker only used two digest "1.42" but our semver lib needs the extra .0 to work
|
||||||
|
}
|
||||||
|
if _, err := apiutil.SupportedVersion(r, ">= "+version); err == nil {
|
||||||
|
header = "application/vnd.docker.multiplexed-stream"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c := r.Header.Get("Connection")
|
c := r.Header.Get("Connection")
|
||||||
proto := r.Header.Get("Upgrade")
|
proto := r.Header.Get("Upgrade")
|
||||||
if len(proto) == 0 || !strings.EqualFold(c, "Upgrade") {
|
if len(proto) == 0 || !strings.EqualFold(c, "Upgrade") {
|
||||||
// OK - can't upgrade if not requested or protocol is not specified
|
// OK - can't upgrade if not requested or protocol is not specified
|
||||||
fmt.Fprintf(conn,
|
fmt.Fprintf(conn,
|
||||||
"HTTP/1.1 200 OK\r\nContent-Type: application/vnd.docker.raw-stream\r\n\r\n")
|
"HTTP/1.1 200 OK\r\nContent-Type: %s\r\n\r\n", header)
|
||||||
} else {
|
} else {
|
||||||
// Upgraded
|
// Upgraded
|
||||||
fmt.Fprintf(conn,
|
fmt.Fprintf(conn,
|
||||||
"HTTP/1.1 101 UPGRADED\r\nContent-Type: application/vnd.docker.raw-stream\r\nConnection: Upgrade\r\nUpgrade: %s\r\n\r\n",
|
"HTTP/1.1 101 UPGRADED\r\nContent-Type: %s\r\nConnection: Upgrade\r\nUpgrade: %s\r\n\r\n",
|
||||||
proto)
|
proto, header)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"github.com/containers/podman/v4/libpod"
|
"github.com/containers/podman/v4/libpod"
|
||||||
"github.com/containers/podman/v4/pkg/api/handlers"
|
"github.com/containers/podman/v4/pkg/api/handlers"
|
||||||
"github.com/containers/podman/v4/pkg/api/handlers/utils"
|
"github.com/containers/podman/v4/pkg/api/handlers/utils"
|
||||||
|
"github.com/containers/podman/v4/pkg/api/handlers/utils/apiutil"
|
||||||
api "github.com/containers/podman/v4/pkg/api/types"
|
api "github.com/containers/podman/v4/pkg/api/types"
|
||||||
"github.com/containers/podman/v4/pkg/auth"
|
"github.com/containers/podman/v4/pkg/auth"
|
||||||
"github.com/containers/podman/v4/pkg/channel"
|
"github.com/containers/podman/v4/pkg/channel"
|
||||||
|
@ -80,7 +81,7 @@ func ManifestCreate(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
status := http.StatusOK
|
status := http.StatusOK
|
||||||
if _, err := utils.SupportedVersion(r, "< 4.0.0"); err == utils.ErrVersionNotSupported {
|
if _, err := utils.SupportedVersion(r, "< 4.0.0"); err == apiutil.ErrVersionNotSupported {
|
||||||
status = http.StatusCreated
|
status = http.StatusCreated
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
package apiutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/blang/semver/v4"
|
||||||
|
"github.com/containers/podman/v4/version"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrVersionNotGiven returned when version not given by client
|
||||||
|
ErrVersionNotGiven = errors.New("version not given in URL path")
|
||||||
|
// ErrVersionNotSupported returned when given version is too old
|
||||||
|
ErrVersionNotSupported = errors.New("given version is not supported")
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsLibpodRequest returns true if the request related to a libpod endpoint
|
||||||
|
// (e.g., /v2/libpod/...).
|
||||||
|
func IsLibpodRequest(r *http.Request) bool {
|
||||||
|
split := strings.Split(r.URL.String(), "/")
|
||||||
|
return len(split) >= 3 && split[2] == "libpod"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportedVersion validates that the version provided by client is included in the given condition
|
||||||
|
// https://github.com/blang/semver#ranges provides the details for writing conditions
|
||||||
|
// If a version is not given in URL path, ErrVersionNotGiven is returned
|
||||||
|
func SupportedVersion(r *http.Request, condition string) (semver.Version, error) {
|
||||||
|
version := semver.Version{}
|
||||||
|
val, ok := mux.Vars(r)["version"]
|
||||||
|
if !ok {
|
||||||
|
return version, ErrVersionNotGiven
|
||||||
|
}
|
||||||
|
safeVal, err := url.PathUnescape(val)
|
||||||
|
if err != nil {
|
||||||
|
return version, fmt.Errorf("unable to unescape given API version: %q: %w", val, err)
|
||||||
|
}
|
||||||
|
version, err = semver.ParseTolerant(safeVal)
|
||||||
|
if err != nil {
|
||||||
|
return version, fmt.Errorf("unable to parse given API version: %q from %q: %w", safeVal, val, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inRange, err := semver.ParseRange(condition)
|
||||||
|
if err != nil {
|
||||||
|
return version, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if inRange(version) {
|
||||||
|
return version, nil
|
||||||
|
}
|
||||||
|
return version, ErrVersionNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportedVersionWithDefaults validates that the version provided by client valid is supported by server
|
||||||
|
// minimal API version <= client path version <= maximum API version focused on the endpoint tree from URL
|
||||||
|
func SupportedVersionWithDefaults(r *http.Request) (semver.Version, error) {
|
||||||
|
tree := version.Compat
|
||||||
|
if IsLibpodRequest(r) {
|
||||||
|
tree = version.Libpod
|
||||||
|
}
|
||||||
|
|
||||||
|
return SupportedVersion(r,
|
||||||
|
fmt.Sprintf(">=%s <=%s", version.APIVersion[tree][version.MinimalAPI].String(),
|
||||||
|
version.APIVersion[tree][version.CurrentAPI].String()))
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
package apiutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/containers/podman/v4/version"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSupportedVersion(t *testing.T) {
|
||||||
|
req, err := http.NewRequest(http.MethodGet,
|
||||||
|
fmt.Sprintf("/v%s/libpod/testing/versions", version.APIVersion[version.Libpod][version.CurrentAPI]),
|
||||||
|
nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"version": version.APIVersion[version.Libpod][version.CurrentAPI].String()})
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, err := SupportedVersionWithDefaults(r)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, ErrVersionNotGiven): // for compat endpoints version optional
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
fmt.Fprint(w, err.Error())
|
||||||
|
case errors.Is(err, ErrVersionNotSupported): // version given but not supported
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
fmt.Fprint(w, err.Error())
|
||||||
|
case err != nil:
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
fmt.Fprint(w, err.Error())
|
||||||
|
default: // all good
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
fmt.Fprint(w, "OK")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != http.StatusOK {
|
||||||
|
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||||
|
status, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the response body is what we expect.
|
||||||
|
expected := `OK`
|
||||||
|
if rr.Body.String() != expected {
|
||||||
|
t.Errorf("handler returned unexpected body: got %q want %q",
|
||||||
|
rr.Body.String(), expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnsupportedVersion(t *testing.T) {
|
||||||
|
version := "999.999.999"
|
||||||
|
req, err := http.NewRequest(http.MethodGet,
|
||||||
|
fmt.Sprintf("/v%s/libpod/testing/versions", version),
|
||||||
|
nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"version": version})
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, err := SupportedVersionWithDefaults(r)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, ErrVersionNotGiven): // for compat endpoints version optional
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
fmt.Fprint(w, err.Error())
|
||||||
|
case errors.Is(err, ErrVersionNotSupported): // version given but not supported
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
fmt.Fprint(w, err.Error())
|
||||||
|
case err != nil:
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
fmt.Fprint(w, err.Error())
|
||||||
|
default: // all good
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
fmt.Fprint(w, "OK")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != http.StatusBadRequest {
|
||||||
|
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||||
|
status, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the response body is what we expect.
|
||||||
|
expected := ErrVersionNotSupported.Error()
|
||||||
|
if rr.Body.String() != expected {
|
||||||
|
t.Errorf("handler returned unexpected body: got %q want %q",
|
||||||
|
rr.Body.String(), expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEqualVersion(t *testing.T) {
|
||||||
|
version := "1.30.0"
|
||||||
|
req, err := http.NewRequest(http.MethodGet,
|
||||||
|
fmt.Sprintf("/v%s/libpod/testing/versions", version),
|
||||||
|
nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"version": version})
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, err := SupportedVersion(r, "=="+version)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, ErrVersionNotGiven): // for compat endpoints version optional
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
fmt.Fprint(w, err.Error())
|
||||||
|
case errors.Is(err, ErrVersionNotSupported): // version given but not supported
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
fmt.Fprint(w, err.Error())
|
||||||
|
case err != nil:
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
fmt.Fprint(w, err.Error())
|
||||||
|
default: // all good
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
fmt.Fprint(w, "OK")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
|
||||||
|
if status := rr.Code; status != http.StatusOK {
|
||||||
|
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||||
|
status, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the response body is what we expect.
|
||||||
|
expected := http.StatusText(http.StatusOK)
|
||||||
|
if rr.Body.String() != expected {
|
||||||
|
t.Errorf("handler returned unexpected body: got %q want %q",
|
||||||
|
rr.Body.String(), expected)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,79 +1,34 @@
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/blang/semver/v4"
|
"github.com/blang/semver/v4"
|
||||||
"github.com/containers/podman/v4/version"
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/gorilla/schema"
|
"github.com/gorilla/schema"
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/containers/podman/v4/pkg/api/handlers/utils/apiutil"
|
||||||
api "github.com/containers/podman/v4/pkg/api/types"
|
api "github.com/containers/podman/v4/pkg/api/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrVersionNotGiven returned when version not given by client
|
|
||||||
ErrVersionNotGiven = errors.New("version not given in URL path")
|
|
||||||
// ErrVersionNotSupported returned when given version is too old
|
|
||||||
ErrVersionNotSupported = errors.New("given version is not supported")
|
|
||||||
)
|
|
||||||
|
|
||||||
// IsLibpodRequest returns true if the request related to a libpod endpoint
|
// IsLibpodRequest returns true if the request related to a libpod endpoint
|
||||||
// (e.g., /v2/libpod/...).
|
// (e.g., /v2/libpod/...).
|
||||||
func IsLibpodRequest(r *http.Request) bool {
|
func IsLibpodRequest(r *http.Request) bool {
|
||||||
split := strings.Split(r.URL.String(), "/")
|
return apiutil.IsLibpodRequest(r)
|
||||||
return len(split) >= 3 && split[2] == "libpod"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SupportedVersion validates that the version provided by client is included in the given condition
|
// SupportedVersion validates that the version provided by client is included in the given condition
|
||||||
// https://github.com/blang/semver#ranges provides the details for writing conditions
|
// https://github.com/blang/semver#ranges provides the details for writing conditions
|
||||||
// If a version is not given in URL path, ErrVersionNotGiven is returned
|
// If a version is not given in URL path, ErrVersionNotGiven is returned
|
||||||
func SupportedVersion(r *http.Request, condition string) (semver.Version, error) {
|
func SupportedVersion(r *http.Request, condition string) (semver.Version, error) {
|
||||||
version := semver.Version{}
|
return apiutil.SupportedVersion(r, condition)
|
||||||
val, ok := mux.Vars(r)["version"]
|
|
||||||
if !ok {
|
|
||||||
return version, ErrVersionNotGiven
|
|
||||||
}
|
|
||||||
safeVal, err := url.PathUnescape(val)
|
|
||||||
if err != nil {
|
|
||||||
return version, fmt.Errorf("unable to unescape given API version: %q: %w", val, err)
|
|
||||||
}
|
|
||||||
version, err = semver.ParseTolerant(safeVal)
|
|
||||||
if err != nil {
|
|
||||||
return version, fmt.Errorf("unable to parse given API version: %q from %q: %w", safeVal, val, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
inRange, err := semver.ParseRange(condition)
|
|
||||||
if err != nil {
|
|
||||||
return version, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if inRange(version) {
|
|
||||||
return version, nil
|
|
||||||
}
|
|
||||||
return version, ErrVersionNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
// SupportedVersionWithDefaults validates that the version provided by client valid is supported by server
|
|
||||||
// minimal API version <= client path version <= maximum API version focused on the endpoint tree from URL
|
|
||||||
func SupportedVersionWithDefaults(r *http.Request) (semver.Version, error) {
|
|
||||||
tree := version.Compat
|
|
||||||
if IsLibpodRequest(r) {
|
|
||||||
tree = version.Libpod
|
|
||||||
}
|
|
||||||
|
|
||||||
return SupportedVersion(r,
|
|
||||||
fmt.Sprintf(">=%s <=%s", version.APIVersion[tree][version.MinimalAPI].String(),
|
|
||||||
version.APIVersion[tree][version.CurrentAPI].String()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteResponse encodes the given value as JSON or string and renders it for http client
|
// WriteResponse encodes the given value as JSON or string and renders it for http client
|
||||||
|
|
|
@ -1,144 +1,9 @@
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/containers/podman/v4/version"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSupportedVersion(t *testing.T) {
|
|
||||||
req, err := http.NewRequest(http.MethodGet,
|
|
||||||
fmt.Sprintf("/v%s/libpod/testing/versions", version.APIVersion[version.Libpod][version.CurrentAPI]),
|
|
||||||
nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
req = mux.SetURLVars(req, map[string]string{"version": version.APIVersion[version.Libpod][version.CurrentAPI].String()})
|
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
_, err := SupportedVersionWithDefaults(r)
|
|
||||||
switch {
|
|
||||||
case errors.Is(err, ErrVersionNotGiven): // for compat endpoints version optional
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
fmt.Fprint(w, err.Error())
|
|
||||||
case errors.Is(err, ErrVersionNotSupported): // version given but not supported
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
fmt.Fprint(w, err.Error())
|
|
||||||
case err != nil:
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
fmt.Fprint(w, err.Error())
|
|
||||||
default: // all good
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
fmt.Fprint(w, "OK")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
handler.ServeHTTP(rr, req)
|
|
||||||
|
|
||||||
if status := rr.Code; status != http.StatusOK {
|
|
||||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
|
||||||
status, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the response body is what we expect.
|
|
||||||
expected := `OK`
|
|
||||||
if rr.Body.String() != expected {
|
|
||||||
t.Errorf("handler returned unexpected body: got %q want %q",
|
|
||||||
rr.Body.String(), expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUnsupportedVersion(t *testing.T) {
|
|
||||||
version := "999.999.999"
|
|
||||||
req, err := http.NewRequest(http.MethodGet,
|
|
||||||
fmt.Sprintf("/v%s/libpod/testing/versions", version),
|
|
||||||
nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
req = mux.SetURLVars(req, map[string]string{"version": version})
|
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
_, err := SupportedVersionWithDefaults(r)
|
|
||||||
switch {
|
|
||||||
case errors.Is(err, ErrVersionNotGiven): // for compat endpoints version optional
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
fmt.Fprint(w, err.Error())
|
|
||||||
case errors.Is(err, ErrVersionNotSupported): // version given but not supported
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
fmt.Fprint(w, err.Error())
|
|
||||||
case err != nil:
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
fmt.Fprint(w, err.Error())
|
|
||||||
default: // all good
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
fmt.Fprint(w, "OK")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
handler.ServeHTTP(rr, req)
|
|
||||||
|
|
||||||
if status := rr.Code; status != http.StatusBadRequest {
|
|
||||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
|
||||||
status, http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the response body is what we expect.
|
|
||||||
expected := ErrVersionNotSupported.Error()
|
|
||||||
if rr.Body.String() != expected {
|
|
||||||
t.Errorf("handler returned unexpected body: got %q want %q",
|
|
||||||
rr.Body.String(), expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEqualVersion(t *testing.T) {
|
|
||||||
version := "1.30.0"
|
|
||||||
req, err := http.NewRequest(http.MethodGet,
|
|
||||||
fmt.Sprintf("/v%s/libpod/testing/versions", version),
|
|
||||||
nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
req = mux.SetURLVars(req, map[string]string{"version": version})
|
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
|
||||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
_, err := SupportedVersion(r, "=="+version)
|
|
||||||
switch {
|
|
||||||
case errors.Is(err, ErrVersionNotGiven): // for compat endpoints version optional
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
fmt.Fprint(w, err.Error())
|
|
||||||
case errors.Is(err, ErrVersionNotSupported): // version given but not supported
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
fmt.Fprint(w, err.Error())
|
|
||||||
case err != nil:
|
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
|
||||||
fmt.Fprint(w, err.Error())
|
|
||||||
default: // all good
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
fmt.Fprint(w, "OK")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
handler.ServeHTTP(rr, req)
|
|
||||||
|
|
||||||
if status := rr.Code; status != http.StatusOK {
|
|
||||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
|
||||||
status, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the response body is what we expect.
|
|
||||||
expected := http.StatusText(http.StatusOK)
|
|
||||||
if rr.Body.String() != expected {
|
|
||||||
t.Errorf("handler returned unexpected body: got %q want %q",
|
|
||||||
rr.Body.String(), expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestErrorEncoderFuncOmit(t *testing.T) {
|
func TestErrorEncoderFuncOmit(t *testing.T) {
|
||||||
data, err := json.Marshal(struct {
|
data, err := json.Marshal(struct {
|
||||||
Err error `json:"err,omitempty"`
|
Err error `json:"err,omitempty"`
|
||||||
|
|
|
@ -526,7 +526,12 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error {
|
||||||
// tags:
|
// tags:
|
||||||
// - containers (compat)
|
// - containers (compat)
|
||||||
// summary: Attach to a container
|
// summary: Attach to a container
|
||||||
// description: Hijacks the connection to forward the container's standard streams to the client.
|
// description: |
|
||||||
|
// Attach to a container to read its output or send it input. You can attach
|
||||||
|
// to the same container multiple times and you can reattach to containers
|
||||||
|
// that have been detached.
|
||||||
|
//
|
||||||
|
// It uses the same stream format as docker, see the libpod attach endpoint for a description of the format.
|
||||||
// parameters:
|
// parameters:
|
||||||
// - in: path
|
// - in: path
|
||||||
// name: name
|
// name: name
|
||||||
|
@ -964,7 +969,10 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error {
|
||||||
// tags:
|
// tags:
|
||||||
// - containers
|
// - containers
|
||||||
// summary: Get container logs
|
// summary: Get container logs
|
||||||
// description: Get stdout and stderr logs from a container.
|
// description: |
|
||||||
|
// Get stdout and stderr logs from a container.
|
||||||
|
//
|
||||||
|
// The stream format is the same as described in the attach endpoint.
|
||||||
// parameters:
|
// parameters:
|
||||||
// - in: path
|
// - in: path
|
||||||
// name: name
|
// name: name
|
||||||
|
@ -1319,7 +1327,94 @@ func (s *APIServer) registerContainersHandlers(r *mux.Router) error {
|
||||||
// tags:
|
// tags:
|
||||||
// - containers
|
// - containers
|
||||||
// summary: Attach to a container
|
// summary: Attach to a container
|
||||||
// description: Hijacks the connection to forward the container's standard streams to the client.
|
// description: |
|
||||||
|
// Attach to a container to read its output or send it input. You can attach
|
||||||
|
// to the same container multiple times and you can reattach to containers
|
||||||
|
// that have been detached.
|
||||||
|
//
|
||||||
|
// ### Hijacking
|
||||||
|
//
|
||||||
|
// This endpoint hijacks the HTTP connection to transport `stdin`, `stdout`,
|
||||||
|
// and `stderr` on the same socket.
|
||||||
|
//
|
||||||
|
// This is the response from the service for an attach request:
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// HTTP/1.1 200 OK
|
||||||
|
// Content-Type: application/vnd.docker.raw-stream
|
||||||
|
//
|
||||||
|
// [STREAM]
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// After the headers and two new lines, the TCP connection can now be used
|
||||||
|
// for raw, bidirectional communication between the client and server.
|
||||||
|
//
|
||||||
|
// To inform potential proxies about connection hijacking, the client
|
||||||
|
// can also optionally send connection upgrade headers.
|
||||||
|
//
|
||||||
|
// For example, the client sends this request to upgrade the connection:
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// POST /v4.6.0/libpod/containers/16253994b7c4/attach?stream=1&stdout=1 HTTP/1.1
|
||||||
|
// Upgrade: tcp
|
||||||
|
// Connection: Upgrade
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// The service will respond with a `101 UPGRADED` response, and will
|
||||||
|
// similarly follow with the raw stream:
|
||||||
|
//
|
||||||
|
// ```
|
||||||
|
// HTTP/1.1 101 UPGRADED
|
||||||
|
// Content-Type: application/vnd.docker.raw-stream
|
||||||
|
// Connection: Upgrade
|
||||||
|
// Upgrade: tcp
|
||||||
|
//
|
||||||
|
// [STREAM]
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// ### Stream format
|
||||||
|
//
|
||||||
|
// When the TTY setting is disabled for the container,
|
||||||
|
// the HTTP Content-Type header is set to application/vnd.docker.multiplexed-stream
|
||||||
|
// (starting with v4.7.0, previously application/vnd.docker.raw-stream was always used)
|
||||||
|
// and the stream over the hijacked connected is multiplexed to separate out
|
||||||
|
// `stdout` and `stderr`. The stream consists of a series of frames, each
|
||||||
|
// containing a header and a payload.
|
||||||
|
//
|
||||||
|
// The header contains the information about the output stream type and the size of
|
||||||
|
// the payload.
|
||||||
|
// It is encoded on the first eight bytes like this:
|
||||||
|
//
|
||||||
|
// ```go
|
||||||
|
// header := [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4}
|
||||||
|
// ```
|
||||||
|
//
|
||||||
|
// `STREAM_TYPE` can be:
|
||||||
|
//
|
||||||
|
// - 0: `stdin` (is written on `stdout`)
|
||||||
|
// - 1: `stdout`
|
||||||
|
// - 2: `stderr`
|
||||||
|
//
|
||||||
|
// `SIZE1, SIZE2, SIZE3, SIZE4` are the four bytes of the `uint32` size
|
||||||
|
// encoded as big endian.
|
||||||
|
//
|
||||||
|
// Following the header is the payload, which contains the specified number of
|
||||||
|
// bytes as written in the size.
|
||||||
|
//
|
||||||
|
// The simplest way to implement this protocol is the following:
|
||||||
|
//
|
||||||
|
// 1. Read 8 bytes.
|
||||||
|
// 2. Choose `stdout` or `stderr` depending on the first byte.
|
||||||
|
// 3. Extract the frame size from the last four bytes.
|
||||||
|
// 4. Read the extracted size and output it on the correct output.
|
||||||
|
// 5. Goto 1.
|
||||||
|
//
|
||||||
|
// ### Stream format when using a TTY
|
||||||
|
//
|
||||||
|
// When the TTY setting is enabled for the container,
|
||||||
|
// the stream is not multiplexed. The data exchanged over the hijacked
|
||||||
|
// connection is simply the raw data from the process PTY and client's
|
||||||
|
// `stdin`.
|
||||||
// parameters:
|
// parameters:
|
||||||
// - in: path
|
// - in: path
|
||||||
// name: name
|
// name: name
|
||||||
|
|
|
@ -254,7 +254,9 @@ func (s *APIServer) registerExecHandlers(r *mux.Router) error {
|
||||||
// tags:
|
// tags:
|
||||||
// - exec
|
// - exec
|
||||||
// summary: Start an exec instance
|
// summary: Start an exec instance
|
||||||
// description: Starts a previously set up exec instance. If detach is true, this endpoint returns immediately after starting the command. Otherwise, it sets up an interactive session with the command.
|
// description: |
|
||||||
|
// Starts a previously set up exec instance. If detach is true, this endpoint returns immediately after starting the command.
|
||||||
|
// Otherwise, it sets up an interactive session with the command. The stream format is the same as the attach endpoint.
|
||||||
// parameters:
|
// parameters:
|
||||||
// - in: path
|
// - in: path
|
||||||
// name: id
|
// name: id
|
||||||
|
|
|
@ -30,6 +30,21 @@ podman run --rm -d --replace --name foo $IMAGE sh -c "echo $mytext;sleep 42"
|
||||||
# Looks like it is missing the required 0 bytes from the message, why?
|
# Looks like it is missing the required 0 bytes from the message, why?
|
||||||
t POST "containers/foo/attach?logs=true&stream=false" 200 \
|
t POST "containers/foo/attach?logs=true&stream=false" 200 \
|
||||||
$'\001\031'$mytext
|
$'\001\031'$mytext
|
||||||
|
|
||||||
|
# check old docker header
|
||||||
|
response_headers=$(cat "$WORKDIR/curl.headers.out")
|
||||||
|
like "$response_headers" ".*Content-Type: application/vnd\.docker\.raw-stream.*" "vnd.docker.raw-stream docker v1.40"
|
||||||
|
# check new vnd.docker.multiplexed-stream header
|
||||||
|
t POST "/v1.42/containers/foo/attach?logs=true&stream=false" 200
|
||||||
|
response_headers=$(cat "$WORKDIR/curl.headers.out")
|
||||||
|
like "$response_headers" ".*Content-Type: application/vnd\.docker\.multiplexed-stream.*" "vnd.docker.multiplexed-stream docker v1.42"
|
||||||
|
t POST "/v4.6.0/libpod/containers/foo/attach?logs=true&stream=false" 200
|
||||||
|
response_headers=$(cat "$WORKDIR/curl.headers.out")
|
||||||
|
like "$response_headers" ".*Content-Type: application/vnd\.docker\.raw-stream.*" "vnd.docker.raw-stream libpod v4.6.0"
|
||||||
|
t POST "/v4.7.0/libpod/containers/foo/attach?logs=true&stream=false" 200
|
||||||
|
response_headers=$(cat "$WORKDIR/curl.headers.out")
|
||||||
|
like "$response_headers" ".*Content-Type: application/vnd\.docker\.multiplexed-stream.*" "vnd.docker.multiplexed-stream libpod v4.7.0"
|
||||||
|
|
||||||
t POST "containers/foo/kill" 204
|
t POST "containers/foo/kill" 204
|
||||||
|
|
||||||
podman run --replace --name=foo -v /tmp:/tmp $IMAGE true
|
podman run --replace --name=foo -v /tmp:/tmp $IMAGE true
|
||||||
|
|
Loading…
Reference in New Issue