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:
fixomatic-ctrl 2024-09-27 15:40:55 +02:00 committed by GitHub
parent 514d25d53b
commit 1dd90dbe20
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 273 additions and 2 deletions

View File

@ -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

View File

@ -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())
})
}

View File

@ -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.

View 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