Skip to content

Commit

Permalink
feat: kube play build remote support
Browse files Browse the repository at this point in the history
Signed-off-by: fixomatic-ctrl <[email protected]>
  • Loading branch information
fixomatic-ctrl committed Sep 11, 2024
1 parent b1efc50 commit de28900
Show file tree
Hide file tree
Showing 9 changed files with 610 additions and 273 deletions.
165 changes: 158 additions & 7 deletions cmd/podman/kube/play.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ package kube

import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"

v1 "github.com/containers/podman/v5/pkg/k8s.io/api/core/v1"
"sigs.k8s.io/yaml"

buildahParse "github.com/containers/buildah/pkg/parse"
"github.com/containers/common/pkg/auth"
"github.com/containers/common/pkg/completion"
Expand Down Expand Up @@ -174,6 +179,13 @@ func playFlags(cmd *cobra.Command) {
flags.BoolVar(&playOptions.UseLongAnnotations, noTruncFlagName, false, "Use annotations that are not truncated to the Kubernetes maximum length of 63 characters")
_ = flags.MarkHidden(noTruncFlagName)

buildFlagName := "build"
flags.BoolVar(&playOptions.BuildCLI, buildFlagName, false, "Build all images in a YAML (given Containerfiles exist)")

contextDirFlagName := "context-dir"
flags.StringVar(&playOptions.ContextDir, contextDirFlagName, "", "Path to top level of context directory")
_ = cmd.RegisterFlagCompletionFunc(contextDirFlagName, completion.AutocompleteDefault)

if !registry.IsRemote() {
certDirFlagName := "cert-dir"
flags.StringVar(&playOptions.CertDir, certDirFlagName, "", "`Pathname` of a directory containing TLS certificates and keys")
Expand All @@ -183,13 +195,6 @@ func playFlags(cmd *cobra.Command) {
flags.StringVar(&playOptions.SeccompProfileRoot, seccompProfileRootFlagName, defaultSeccompRoot, "Directory path for seccomp profiles")
_ = cmd.RegisterFlagCompletionFunc(seccompProfileRootFlagName, completion.AutocompleteDefault)

buildFlagName := "build"
flags.BoolVar(&playOptions.BuildCLI, buildFlagName, false, "Build all images in a YAML (given Containerfiles exist)")

contextDirFlagName := "context-dir"
flags.StringVar(&playOptions.ContextDir, contextDirFlagName, "", "Path to top level of context directory")
_ = cmd.RegisterFlagCompletionFunc(contextDirFlagName, completion.AutocompleteDefault)

flags.StringVar(&playOptions.SignaturePolicy, "signature-policy", "", "`Pathname` of signature policy file (not usually used)")

_ = flags.MarkHidden("signature-policy")
Expand Down Expand Up @@ -279,6 +284,22 @@ func play(cmd *cobra.Command, args []string) error {
return err
}

if registry.IsRemote() && playOptions.Build == types.OptionalBoolTrue {
var cwd string
if playOptions.ContextDir != "" {
cwd = playOptions.ContextDir
} else {
cwd = filepath.Dir(args[0])
}

result, err := kubeBuild(registry.ImageEngine(), registry.Context(), reader, cwd)
if err != nil {
return err
}

reader = result
}

if playOptions.Down {
return teardown(reader, entities.PlayKubeDownOptions{Force: playOptions.Force})
}
Expand Down Expand Up @@ -358,6 +379,136 @@ func play(cmd *cobra.Command, args []string) error {
return nil
}

// Concatenate and create a bytes.Reader
func bytesArrayToReader(yamlArray [][]byte) *bytes.Reader {
// Use a buffer to concatenate the byte slices
var buffer bytes.Buffer

// Loop through each []byte and add it to the buffer
for i, yamlDoc := range yamlArray {
buffer.Write(yamlDoc)

// Add YAML document separator between documents, except after the last one
if i < len(yamlArray)-1 {
buffer.WriteString("\n---\n")
}
}

// Return a bytes.Reader from the buffer
return bytes.NewReader(buffer.Bytes())
}

func kubeBuild(imageEngine entities.ImageEngine, context context.Context, body *bytes.Reader, contextDir string) (*bytes.Reader, error) {
content, err := io.ReadAll(body)
if err != nil {
return nil, err
}
if len(content) == 0 {
return nil, errors.New("yaml file provided is empty, cannot apply to a cluster")
}

// Split the yaml file
documentList, err := util.SplitMultiDocYAML(content)
if err != nil {
return nil, err
}

// sort kube kinds
documentList, err = util.SortKubeKinds(documentList)
if err != nil {
return nil, fmt.Errorf("unable to sort kube kinds: %w", err)
}

output := make([][]byte, len(documentList))

for i, document := range documentList {
kind, err := util.GetKubeKind(document)
if err != nil {
return nil, fmt.Errorf("unable to read as kube YAML: %w", err)
}

// ignore non-pod kind
if kind != "Pod" {
output[i] = document
continue
}

var podYAML v1.Pod
if err := yaml.Unmarshal(document, &podYAML); err != nil {
return nil, fmt.Errorf("unable to read YAML as Kube Pod: %w", err)
}

pod, err := podBuild(imageEngine, context, podYAML, contextDir)
if err != nil {
return nil, err
}

// convert the pod object to bytes
podMarshaled, err := yaml.Marshal(&pod)
if err != nil {
return nil, err
}

output[i] = podMarshaled
}

return bytesArrayToReader(output), nil
}

func podBuild(imageEngine entities.ImageEngine, context context.Context, pod v1.Pod, contextDir string) (*v1.Pod, error) {
for i := range pod.Spec.Containers {
// get the corresponding image
buildFile, err := util.GetBuildFile(pod.Spec.Containers[i].Image, contextDir)
if err != nil {
return nil, err
}

found, err := imageEngine.Exists(
context,
pod.Spec.Containers[i].Image,
)
if err != nil {
return nil, err
}

if len(buildFile) == 0 {
continue
}

if found.Value {
reports, _, err := imageEngine.Inspect(context, []string{pod.Spec.Containers[i].Image}, entities.InspectOptions{})
if err != nil {
return nil, err
}
if len(reports) == 0 {
return nil, fmt.Errorf("image %s not found in %s", pod.Spec.Containers[i].Image, contextDir)
}
// overwrite the image id as container image
pod.Spec.Containers[i].Image = reports[0].ID
} else {
buildOpts := new(entities.BuildOptions)
buildOpts.Output = pod.Spec.Containers[i].Image
buildOpts.SystemContext = playOptions.SystemContext
buildOpts.ContextDirectory = filepath.Dir(buildFile)

build, err := imageEngine.Build(
context,
[]string{buildFile},
*buildOpts,
)

if err != nil {
return nil, err
}

// overwrite the image id as container image
pod.Spec.Containers[i].Image = build.ID
}
}

return &pod, nil
}

func playKube(cmd *cobra.Command, args []string) error {
return play(cmd, args)
}
Expand Down
101 changes: 101 additions & 0 deletions cmd/podman/kube/play_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package kube

import (
"context"
"io"
"testing"

v1 "github.com/containers/podman/v5/pkg/k8s.io/api/core/v1"
"github.com/stretchr/testify/assert"
)

func TestPodBuild(t *testing.T) {
tests := []struct {
name string
pod v1.Pod
contextDir string
expectError bool
expectedErrorMsg string
expectedImages []string
}{
{
"pod without containers should no raise any error",
v1.Pod{
Spec: v1.PodSpec{
Containers: []v1.Container{},
},
},
"",
false,
"",
[]string{},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx := context.Background()

pod, err := podBuild(nil, ctx, test.pod, test.contextDir)
if test.expectError {
assert.Error(t, err)
assert.Contains(t, err.Error(), test.expectedErrorMsg)
} else {
assert.NoError(t, err)
for i, container := range pod.Spec.Containers {
assert.Equal(t, test.expectedImages[i], container.Image)
}
}
})
}
}

// TestBytesArrayToReader tests the bytesArrayToReader function
func TestBytesArrayToReader(t *testing.T) {
tests := []struct {
name string
input [][]byte
expected string
}{
{
name: "single document",
input: [][]byte{
[]byte("document1"),
},
expected: "document1",
},
{
name: "two documents",
input: [][]byte{
[]byte("document1"),
[]byte("document2"),
},
expected: "document1\n---\ndocument2",
},
{
name: "three documents",
input: [][]byte{
[]byte("document1"),
[]byte("document2"),
[]byte("document3"),
},
expected: "document1\n---\ndocument2\n---\ndocument3",
},
{
name: "empty input",
input: [][]byte{},
expected: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reader := bytesArrayToReader(tt.input)
result, err := io.ReadAll(reader)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
assert.Equal(t, tt.expected, string(result))
})
}
}
8 changes: 4 additions & 4 deletions docs/source/markdown/podman-kube-play.1.md.in
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ A Kubernetes PersistentVolumeClaim represents a Podman named volume. Only the Pe

Use `volume.podman.io/import-source` to import the contents of the tarball (.tar, .tar.gz, .tgz, .bzip, .tar.xz, .txz) specified in the annotation's value into the created Podman volume

Kube play is capable of building images on the fly given the correct directory layout and Containerfiles. This
option is not available for remote clients, including Mac and Windows (excluding WSL2) machines, yet. Consider the following excerpt from a YAML file:
Kube play is capable of building images on the fly given the correct directory layout and Containerfiles.
Consider the following excerpt from a YAML file:
```
apiVersion: v1
kind: Pod
Expand Down Expand Up @@ -178,7 +178,7 @@ An image can be automatically mounted into a container if the annotation `io.pod

#### **--build**

Build images even if they are found in the local storage. Use `--build=false` to completely disable builds. (This option is not available with the remote Podman client)
Build images even if they are found in the local storage. Use `--build=false` to completely disable builds.

Note: You can also override the default isolation type by setting the BUILDAH_ISOLATION environment variable. export BUILDAH_ISOLATION=oci. See podman-build.1.md for more information.

Expand All @@ -193,7 +193,7 @@ The YAML file may be in a multi-doc YAML format. But, it must container only con

#### **--context-dir**=*path*

Use *path* as the build context directory for each image. Requires --build option be true. (This option is not available with the remote Podman client)
Use *path* as the build context directory for each image. Requires --build option be true.

@@option creds

Expand Down
8 changes: 5 additions & 3 deletions pkg/domain/infra/abi/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"os"
"strings"

"github.com/containers/podman/v5/pkg/util"

"github.com/containers/podman/v5/pkg/domain/entities"
k8sAPI "github.com/containers/podman/v5/pkg/k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
Expand All @@ -30,13 +32,13 @@ func (ic *ContainerEngine) KubeApply(ctx context.Context, body io.Reader, option
}

// Split the yaml file
documentList, err := splitMultiDocYAML(content)
documentList, err := util.SplitMultiDocYAML(content)
if err != nil {
return err
}

// Sort the kube kinds
documentList, err = sortKubeKinds(documentList)
documentList, err = util.SortKubeKinds(documentList)
if err != nil {
return fmt.Errorf("unable to sort kube kinds: %w", err)
}
Expand All @@ -60,7 +62,7 @@ func (ic *ContainerEngine) KubeApply(ctx context.Context, body io.Reader, option
}

for _, document := range documentList {
kind, err := getKubeKind(document)
kind, err := util.GetKubeKind(document)
if err != nil {
return fmt.Errorf("unable to read kube YAML: %w", err)
}
Expand Down
Loading

0 comments on commit de28900

Please sign in to comment.