feat(libpod): support kube play tar content-type (#24015)
feat(libpod): support kube play tar content-type Signed-off-by: fixomatic-ctrl <180758136+fixomatic-ctrl@users.noreply.github.com>
This commit is contained in:
parent
514d25d53b
commit
1dd90dbe20
|
@ -3,9 +3,16 @@
|
|||
package libpod
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/containers/storage/pkg/archive"
|
||||
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/containers/podman/v5/libpod"
|
||||
|
@ -15,9 +22,86 @@ import (
|
|||
"github.com/containers/podman/v5/pkg/domain/entities"
|
||||
"github.com/containers/podman/v5/pkg/domain/infra/abi"
|
||||
"github.com/gorilla/schema"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ExtractPlayReader provide an io.Reader given a http.Request object
|
||||
// the function will extract the Content-Type header, if not provided, the body will be returned
|
||||
// of the header define a text format (json, yaml or text) it will also return the body
|
||||
// if the Content-Type is tar, we extract the content to the anchorDir and try to read the `play.yaml` file
|
||||
func extractPlayReader(anchorDir string, r *http.Request) (io.Reader, error) {
|
||||
hdr, found := r.Header["Content-Type"]
|
||||
|
||||
// If Content-Type is not specific we use the body
|
||||
if !found || len(hdr) == 0 {
|
||||
return r.Body, nil
|
||||
}
|
||||
|
||||
var reader io.Reader
|
||||
switch hdr[0] {
|
||||
// backward compatibility
|
||||
case "text/plain":
|
||||
fallthrough
|
||||
case "application/json":
|
||||
fallthrough
|
||||
case "application/yaml":
|
||||
fallthrough
|
||||
case "application/text":
|
||||
fallthrough
|
||||
case "application/x-yaml":
|
||||
reader = r.Body
|
||||
case "application/x-tar":
|
||||
// un-tar the content
|
||||
err := archive.Untar(r.Body, anchorDir, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check for play.yaml
|
||||
path := filepath.Join(anchorDir, "play.yaml")
|
||||
// open the play.yaml file
|
||||
f, err := os.Open(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, fmt.Errorf("file not found: tar missing play.yaml file at root")
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
reader = f
|
||||
default:
|
||||
return nil, fmt.Errorf("Content-Type: %s is not supported. Should be \"application/x-tar\"", hdr[0])
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bytes.NewReader(data), nil
|
||||
}
|
||||
|
||||
func KubePlay(w http.ResponseWriter, r *http.Request) {
|
||||
// create a tmp directory
|
||||
contextDirectory, err := os.MkdirTemp("", "libpod_kube")
|
||||
if err != nil {
|
||||
utils.InternalServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// cleanup the tmp directory
|
||||
defer func() {
|
||||
err := os.RemoveAll(contextDirectory)
|
||||
if err != nil {
|
||||
logrus.Warn(fmt.Errorf("failed to remove libpod_kube tmp directory %q: %w", contextDirectory, err))
|
||||
}
|
||||
}()
|
||||
|
||||
// extract the reader
|
||||
reader, err := extractPlayReader(contextDirectory, r)
|
||||
if err != nil {
|
||||
utils.InternalServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
|
||||
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
|
||||
query := struct {
|
||||
|
@ -37,6 +121,7 @@ func KubePlay(w http.ResponseWriter, r *http.Request) {
|
|||
TLSVerify bool `schema:"tlsVerify"`
|
||||
Userns string `schema:"userns"`
|
||||
Wait bool `schema:"wait"`
|
||||
Build bool `schema:"build"`
|
||||
}{
|
||||
TLSVerify: true,
|
||||
Start: true,
|
||||
|
@ -110,6 +195,10 @@ func KubePlay(w http.ResponseWriter, r *http.Request) {
|
|||
Username: username,
|
||||
Userns: query.Userns,
|
||||
Wait: query.Wait,
|
||||
ContextDir: contextDirectory,
|
||||
}
|
||||
if _, found := r.URL.Query()["build"]; found {
|
||||
options.Build = types.NewOptionalBool(query.Build)
|
||||
}
|
||||
if _, found := r.URL.Query()["tlsVerify"]; found {
|
||||
options.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
|
||||
|
@ -117,7 +206,7 @@ func KubePlay(w http.ResponseWriter, r *http.Request) {
|
|||
if _, found := r.URL.Query()["start"]; found {
|
||||
options.Start = types.NewOptionalBool(query.Start)
|
||||
}
|
||||
report, err := containerEngine.PlayKube(r.Context(), r.Body, options)
|
||||
report, err := containerEngine.PlayKube(r.Context(), reader, options)
|
||||
if err != nil {
|
||||
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("playing YAML file: %w", err))
|
||||
return
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
//go:build !remote
|
||||
|
||||
package libpod
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestExtractPlayReader(t *testing.T) {
|
||||
// Setup temporary directory for testing purposes
|
||||
tempDir := t.TempDir()
|
||||
|
||||
t.Run("Content-Type not provided - should return body", func(t *testing.T) {
|
||||
req := &http.Request{
|
||||
Body: io.NopCloser(strings.NewReader("test body content")),
|
||||
}
|
||||
|
||||
reader, err := extractPlayReader(tempDir, req)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Read from the returned reader
|
||||
data, err := io.ReadAll(reader)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test body content", string(data))
|
||||
})
|
||||
|
||||
t.Run("Supported content types (json/yaml/text) - should return body", func(t *testing.T) {
|
||||
supportedTypes := []string{
|
||||
"application/json",
|
||||
"application/yaml",
|
||||
"application/text",
|
||||
"application/x-yaml",
|
||||
}
|
||||
|
||||
for _, contentType := range supportedTypes {
|
||||
req := &http.Request{
|
||||
Header: map[string][]string{
|
||||
"Content-Type": {contentType},
|
||||
},
|
||||
Body: io.NopCloser(strings.NewReader("test body content")),
|
||||
}
|
||||
|
||||
reader, err := extractPlayReader(tempDir, req)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Read from the returned reader
|
||||
data, err := io.ReadAll(reader)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test body content", string(data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Unsupported content type - should return error", func(t *testing.T) {
|
||||
req := &http.Request{
|
||||
Header: map[string][]string{
|
||||
"Content-Type": {"application/unsupported"},
|
||||
},
|
||||
Body: io.NopCloser(strings.NewReader("test body content")),
|
||||
}
|
||||
|
||||
_, err := extractPlayReader(tempDir, req)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Content-Type: application/unsupported is not supported. Should be \"application/x-tar\"", err.Error())
|
||||
})
|
||||
}
|
|
@ -16,8 +16,48 @@ func (s *APIServer) registerKubeHandlers(r *mux.Router) error {
|
|||
// - containers
|
||||
// - pods
|
||||
// summary: Play a Kubernetes YAML file.
|
||||
// description: Create and run pods based on a Kubernetes YAML file (pod or service kind).
|
||||
// description: |
|
||||
// Create and run pods based on a Kubernetes YAML file.
|
||||
//
|
||||
// ### Content-Type
|
||||
//
|
||||
// Then endpoint support two Content-Type
|
||||
// - `plain/text` for yaml format
|
||||
// - `application/x-tar` for sending context(s) required for building images
|
||||
//
|
||||
// #### Tar format
|
||||
//
|
||||
// The tar format must contain a `play.yaml` file at the root that will be used.
|
||||
// If the file format requires context to build an image, it uses the image name and
|
||||
// check for corresponding folder.
|
||||
//
|
||||
// For example, the client sends a tar file with the following structure:
|
||||
//
|
||||
// ```
|
||||
// └── content.tar
|
||||
// ├── play.yaml
|
||||
// └── foobar/
|
||||
// └── Containerfile
|
||||
// ```
|
||||
//
|
||||
// The `play.yaml` is the following, the `foobar` image means we are looking for a context with this name.
|
||||
// ```
|
||||
// apiVersion: v1
|
||||
// kind: Pod
|
||||
// metadata:
|
||||
// name: demo-build-remote
|
||||
// spec:
|
||||
// containers:
|
||||
// - name: container
|
||||
// image: foobar
|
||||
// ```
|
||||
//
|
||||
// parameters:
|
||||
// - in: header
|
||||
// name: Content-Type
|
||||
// type: string
|
||||
// default: plain/text
|
||||
// enum: ["plain/text", "application/x-tar"]
|
||||
// - in: query
|
||||
// name: annotations
|
||||
// type: string
|
||||
|
@ -99,6 +139,10 @@ func (s *APIServer) registerKubeHandlers(r *mux.Router) error {
|
|||
// type: boolean
|
||||
// default: false
|
||||
// description: Clean up all objects created when a SIGTERM is received or pods exit.
|
||||
// - in: query
|
||||
// name: build
|
||||
// type: boolean
|
||||
// description: Build the images with corresponding context.
|
||||
// - in: body
|
||||
// name: request
|
||||
// description: Kubernetes YAML file.
|
||||
|
|
|
@ -78,4 +78,72 @@ t DELETE libpod/play/kube $YAML 200 \
|
|||
|
||||
rm -rf $TMPD
|
||||
|
||||
# check kube play works when uploading body as a tar
|
||||
TMPD=$(mktemp -d podman-apiv2-test-kube.XXXXXX)
|
||||
KUBE_PLAY_TAR="${TMPD}/kubeplay.tar"
|
||||
cat > $TMPD/play.yaml << EOF
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: demo-tar-remote
|
||||
spec:
|
||||
containers:
|
||||
- name: container
|
||||
image: ${IMAGE}
|
||||
EOF
|
||||
# Tar the content of the tmp folder
|
||||
tar --format=posix -C $TMPD -cvf ${KUBE_PLAY_TAR} play.yaml &> /dev/null
|
||||
t POST "libpod/play/kube" $KUBE_PLAY_TAR 200 \
|
||||
.Pods[0].ID~[0-9a-f]\\{64\\} \
|
||||
.Pods[0].ContainerErrors=null \
|
||||
.Pods[0].Containers[0]~[0-9a-f]\\{64\\}
|
||||
# Cleanup
|
||||
t DELETE libpod/kube/play $TMPD/play.yaml 200 \
|
||||
.StopReport[0].Id~[0-9a-f]\\{64\\} \
|
||||
.RmReport[0].Id~[0-9a-f]\\{64\\}
|
||||
rm -rf $TMPD
|
||||
|
||||
# check kube play is capable of building the image when uploading body as a tar
|
||||
|
||||
TMPD=$(mktemp -d podman-apiv2-test-kube-build.XXXXXX)
|
||||
KUBE_PLAY_TAR="${TMPD}/kubeplay.tar"
|
||||
# Generate an unique label value
|
||||
LABEL_VALUE="foo-$(date +%s)"
|
||||
cat > $TMPD/play.yaml << EOF
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: demo-build-remote
|
||||
spec:
|
||||
containers:
|
||||
- name: container
|
||||
image: barfoo
|
||||
EOF
|
||||
mkdir $TMPD/barfoo
|
||||
cat > $TMPD/barfoo/Containerfile << EOF
|
||||
FROM ${IMAGE}
|
||||
LABEL bar="${LABEL_VALUE}"
|
||||
EOF
|
||||
|
||||
tar --format=posix -C $TMPD -cvf ${KUBE_PLAY_TAR} . &> /dev/null
|
||||
|
||||
t POST "libpod/play/kube?build=true" $KUBE_PLAY_TAR 200 \
|
||||
.Pods[0].ID~[0-9a-f]\\{64\\} \
|
||||
.Pods[0].ContainerErrors=null \
|
||||
.Pods[0].Containers[0]~[0-9a-f]\\{64\\}
|
||||
|
||||
# Get the container id created
|
||||
cid=$(jq -r '.Pods[0].Containers[0]' <<<"$output")
|
||||
|
||||
# Ensure the image build has the label defined in the Containerfile
|
||||
t GET containers/$cid/json 200 \
|
||||
.Config.Labels.bar="${LABEL_VALUE}"
|
||||
|
||||
# Cleanup
|
||||
t DELETE "libpod/kube/play" $TMPD/play.yaml 200 \
|
||||
.StopReport[0].Id~[0-9a-f]\\{64\\} \
|
||||
.RmReport[0].Id~[0-9a-f]\\{64\\}
|
||||
|
||||
rm -rf $TMPD
|
||||
|
||||
# vim: filetype=sh
|
||||
|
|
Loading…
Reference in New Issue