Skip to content

Commit

Permalink
feat(libpod): support kube play tar content-type (#24015)
Browse files Browse the repository at this point in the history
feat(libpod): support kube play tar content-type

Signed-off-by: fixomatic-ctrl <[email protected]>
  • Loading branch information
fixomatic-ctrl authored Sep 27, 2024
1 parent 514d25d commit 1dd90db
Show file tree
Hide file tree
Showing 4 changed files with 273 additions and 2 deletions.
91 changes: 90 additions & 1 deletion pkg/api/handlers/libpod/kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -110,14 +195,18 @@ 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)
}
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
Expand Down
70 changes: 70 additions & 0 deletions pkg/api/handlers/libpod/kube_test.go
Original file line number Diff line number Diff line change
@@ -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())
})
}
46 changes: 45 additions & 1 deletion pkg/api/server/register_kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
68 changes: 68 additions & 0 deletions test/apiv2/80-kube.at
Original file line number Diff line number Diff line change
Expand Up @@ -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

1 comment on commit 1dd90db

@packit-as-a-service
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

podman-next COPR build failed. @containers/packit-build please check.

Please sign in to comment.