Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

api docs: document stream format #19696

Merged
merged 3 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion libpod/oci_conmon_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ func (r *ConmonOCIRuntime) HTTPAttach(ctr *Container, req *http.Request, w http.

hijackDone <- true

writeHijackHeader(req, httpBuf)
writeHijackHeader(req, httpBuf, isTerminal)

// Force a flush after the header is written.
if err := httpBuf.Flush(); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion libpod/oci_conmon_exec_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ func attachExecHTTP(c *Container, sessionID string, r *http.Request, w http.Resp
hijackDone <- true

// 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.
if err := httpBuf.Flush(); err != nil {
Expand Down
23 changes: 19 additions & 4 deletions libpod/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/containers/common/libnetwork/types"
"github.com/containers/common/pkg/config"
"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"
"github.com/opencontainers/selinux/go-selinux/label"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -182,22 +183,36 @@ func makeHTTPAttachHeader(stream byte, length uint32) []byte {

// writeHijackHeader writes a header appropriate for the type of HTTP Hijack
// 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
// attach, sourced from Docker at:
// https://raw.githubusercontent.com/moby/moby/b95fad8e51bd064be4f4e58a996924f343846c85/api/server/router/container/container_routes.go
// 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")
proto := r.Header.Get("Upgrade")
if len(proto) == 0 || !strings.EqualFold(c, "Upgrade") {
// OK - can't upgrade if not requested or protocol is not specified
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 {
// Upgraded
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",
proto)
"HTTP/1.1 101 UPGRADED\r\nContent-Type: %s\r\nConnection: Upgrade\r\nUpgrade: %s\r\n\r\n",
proto, header)
}
}

Expand Down
3 changes: 2 additions & 1 deletion pkg/api/handlers/libpod/manifests.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/containers/podman/v4/libpod"
"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/apiutil"
api "github.com/containers/podman/v4/pkg/api/types"
"github.com/containers/podman/v4/pkg/auth"
"github.com/containers/podman/v4/pkg/channel"
Expand Down Expand Up @@ -80,7 +81,7 @@ func ManifestCreate(w http.ResponseWriter, r *http.Request) {
}

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
}

Expand Down
69 changes: 69 additions & 0 deletions pkg/api/handlers/utils/apiutil/apiutil.go
Original file line number Diff line number Diff line change
@@ -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()))
}
140 changes: 140 additions & 0 deletions pkg/api/handlers/utils/apiutil/apiutil_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
51 changes: 3 additions & 48 deletions pkg/api/handlers/utils/handler.go
Original file line number Diff line number Diff line change
@@ -1,79 +1,34 @@
package utils

import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"unsafe"

"github.com/blang/semver/v4"
"github.com/containers/podman/v4/version"
"github.com/gorilla/mux"
"github.com/gorilla/schema"
jsoniter "github.com/json-iterator/go"
"github.com/sirupsen/logrus"

"github.com/containers/podman/v4/pkg/api/handlers/utils/apiutil"
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
// (e.g., /v2/libpod/...).
func IsLibpodRequest(r *http.Request) bool {
split := strings.Split(r.URL.String(), "/")
return len(split) >= 3 && split[2] == "libpod"
return apiutil.IsLibpodRequest(r)
}

// 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()))
return apiutil.SupportedVersion(r, condition)
}

// WriteResponse encodes the given value as JSON or string and renders it for http client
Expand Down
Loading