play kube: Allow the user to import the contents of a tar file into a volume

Add a new annotation to allow the user to point to a local tar file
If the annotation is present, import the file's content into the volume
Add a flag to PlayKubeOptions to note remote requests
Fail when trying to import volume content in remote requests
Add the annotation to the documentation
Add an E2E test to the new annotation

Signed-off-by: Ygal Blum <ygal.blum@gmail.com>
This commit is contained in:
Ygal Blum 2022-09-28 09:59:36 +03:00
parent a77ac5be83
commit 0ce234425a
7 changed files with 165 additions and 0 deletions

View File

@ -44,6 +44,9 @@ A Kubernetes PersistentVolumeClaim represents a Podman named volume. Only the Pe
- volume.podman.io/uid
- volume.podman.io/gid
- volume.podman.io/mount-options
- volume.podman.io/import-source
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:

View File

@ -280,3 +280,7 @@ func (v *Volume) Unmount() error {
defer v.lock.Unlock()
return v.unmount(false)
}
func (v *Volume) NeedsMount() bool {
return v.needsMount()
}

View File

@ -93,6 +93,7 @@ func KubePlay(w http.ResponseWriter, r *http.Request) {
LogOptions: query.LogOptions,
StaticIPs: staticIPs,
StaticMACs: staticMACs,
IsRemote: true,
}
if _, found := r.URL.Query()["tlsVerify"]; found {
options.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)

View File

@ -58,6 +58,8 @@ type PlayKubeOptions struct {
ServiceContainer bool
// Userns - define the user namespace to use.
Userns string
// IsRemote - was the request triggered by running podman-remote
IsRemote bool
}
// PlayKubePod represents a single pod and associated containers created by play kube

View File

@ -18,6 +18,7 @@ import (
"github.com/containers/common/pkg/config"
"github.com/containers/common/pkg/secrets"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v4/cmd/podman/parse"
"github.com/containers/podman/v4/libpod"
"github.com/containers/podman/v4/libpod/define"
"github.com/containers/podman/v4/pkg/domain/entities"
@ -29,6 +30,7 @@ import (
"github.com/containers/podman/v4/pkg/specgenutil"
"github.com/containers/podman/v4/pkg/systemd/notifyproxy"
"github.com/containers/podman/v4/pkg/util"
"github.com/containers/podman/v4/utils"
"github.com/coreos/go-systemd/v22/daemon"
"github.com/ghodss/yaml"
"github.com/opencontainers/go-digest"
@ -233,6 +235,19 @@ func (ic *ContainerEngine) PlayKube(ctx context.Context, body io.Reader, options
return nil, fmt.Errorf("unable to read YAML as Kube PersistentVolumeClaim: %w", err)
}
for name, val := range options.Annotations {
if pvcYAML.Annotations == nil {
pvcYAML.Annotations = make(map[string]string)
}
pvcYAML.Annotations[name] = val
}
if options.IsRemote {
if _, ok := pvcYAML.Annotations[util.VolumeImportSourceAnnotation]; ok {
return nil, fmt.Errorf("importing volumes is not supported for remote requests")
}
}
r, err := ic.playKubePVC(ctx, &pvcYAML)
if err != nil {
return nil, err
@ -859,6 +874,7 @@ func (ic *ContainerEngine) playKubePVC(ctx context.Context, pvcYAML *v1.Persiste
// Get pvc annotations and create remaining podman volume options if available.
// These are podman volume options that do not match any of the persistent volume claim
// attributes, so they can be configured using annotations since they will not affect k8s.
var importFrom string
for k, v := range pvcYAML.Annotations {
switch k {
case util.VolumeDriverAnnotation:
@ -883,16 +899,45 @@ func (ic *ContainerEngine) playKubePVC(ctx context.Context, pvcYAML *v1.Persiste
opts["GID"] = v
case util.VolumeMountOptsAnnotation:
opts["o"] = v
case util.VolumeImportSourceAnnotation:
importFrom = v
}
}
volOptions = append(volOptions, libpod.WithVolumeOptions(opts))
// Validate the file and open it before creating the volume for fast-fail
var tarFile *os.File
if len(importFrom) > 0 {
err := parse.ValidateFileName(importFrom)
if err != nil {
return nil, err
}
// open tar file
tarFile, err = os.Open(importFrom)
if err != nil {
return nil, err
}
defer tarFile.Close()
}
// Create volume.
vol, err := ic.Libpod.NewVolume(ctx, volOptions...)
if err != nil {
return nil, err
}
if tarFile != nil {
err = ic.importVolume(ctx, vol, tarFile)
if err != nil {
// Remove the volume to avoid partial success
if rmErr := ic.Libpod.RemoveVolume(ctx, vol, true, nil); rmErr != nil {
logrus.Debug(rmErr)
}
return nil, err
}
}
report.Volumes = append(report.Volumes, entities.PlayKubeVolume{
Name: vol.Name(),
})
@ -900,6 +945,42 @@ func (ic *ContainerEngine) playKubePVC(ctx context.Context, pvcYAML *v1.Persiste
return &report, nil
}
func (ic *ContainerEngine) importVolume(ctx context.Context, vol *libpod.Volume, tarFile *os.File) error {
volumeConfig, err := vol.Config()
if err != nil {
return err
}
mountPoint := volumeConfig.MountPoint
if len(mountPoint) == 0 {
return errors.New("volume is not mounted anywhere on host")
}
driver := volumeConfig.Driver
volumeOptions := volumeConfig.Options
volumeMountStatus, err := ic.VolumeMounted(ctx, vol.Name())
if err != nil {
return err
}
// Check if volume needs a mount and export only if volume is mounted
if vol.NeedsMount() && !volumeMountStatus.Value {
return fmt.Errorf("volume needs to be mounted but is not mounted on %s", mountPoint)
}
// Check if volume is using `local` driver and has mount options type other than tmpfs
if len(driver) == 0 || driver == define.VolumeDriverLocal {
if mountOptionType, ok := volumeOptions["type"]; ok {
if mountOptionType != "tmpfs" && !volumeMountStatus.Value {
return fmt.Errorf("volume is using a driver %s and volume is not mounted on %s", driver, mountPoint)
}
}
}
// dont care if volume is mounted or not we are gonna import everything to mountPoint
return utils.UntarToFileSystem(mountPoint, tarFile, nil)
}
// readConfigMapFromFile returns a kubernetes configMap obtained from --configmap flag
func readConfigMapFromFile(r io.Reader) (v1.ConfigMap, error) {
var cm v1.ConfigMap

View File

@ -13,4 +13,6 @@ const (
VolumeGIDAnnotation = "volume.podman.io/gid"
// Kube annotation for podman volume mount options.
VolumeMountOptsAnnotation = "volume.podman.io/mount-options"
// Kube annotation for podman volume import source.
VolumeImportSourceAnnotation = "volume.podman.io/import-source"
)

View File

@ -3,6 +3,7 @@ package integration
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net"
"net/url"
@ -19,6 +20,7 @@ import (
"github.com/containers/podman/v4/pkg/bindings/play"
"github.com/containers/podman/v4/pkg/util"
. "github.com/containers/podman/v4/test/utils"
"github.com/containers/podman/v4/utils"
"github.com/containers/storage/pkg/stringid"
"github.com/google/uuid"
. "github.com/onsi/ginkgo"
@ -1326,6 +1328,36 @@ func milliCPUToQuota(milliCPU string) int {
return milli * defaultCPUPeriod
}
func createSourceTarFile(fileName, fileContent, tarFilePath string) error {
dir, err := os.MkdirTemp("", "podmanTest")
if err != nil {
return err
}
file, err := os.Create(filepath.Join(dir, fileName))
if err != nil {
return err
}
_, err = file.Write([]byte(fileContent))
if err != nil {
return err
}
err = file.Close()
if err != nil {
return err
}
tarFile, err := os.Create(tarFilePath)
if err != nil {
return err
}
defer tarFile.Close()
return utils.TarToFilesystem(dir, tarFile)
}
var _ = Describe("Podman play kube", func() {
var (
tempdir string
@ -3075,6 +3107,46 @@ o: {{ .Options.o }}`})
Expect(inspect.OutputToString()).To(ContainSubstring("o: " + volOpts))
})
It("podman play kube persistentVolumeClaim with source", func() {
fileName := "data"
expectedFileContent := "Test"
tarFilePath := filepath.Join(os.TempDir(), "podmanVolumeSource.tgz")
err := createSourceTarFile(fileName, expectedFileContent, tarFilePath)
Expect(err).To(BeNil())
volName := "myVolWithStorage"
pvc := getPVC(withPVCName(volName),
withPVCAnnotations(util.VolumeImportSourceAnnotation, tarFilePath),
)
err = generateKubeYaml("persistentVolumeClaim", pvc, kubeYaml)
Expect(err).To(BeNil())
kube := podmanTest.Podman([]string{"play", "kube", kubeYaml})
kube.WaitWithDefaultTimeout()
if IsRemote() {
Expect(kube).Error()
Expect(kube.ErrorToString()).To(ContainSubstring("importing volumes is not supported for remote requests"))
return
}
Expect(kube).Should(Exit(0))
inspect := podmanTest.Podman([]string{"inspect", volName, "--format", `
{
"Name": "{{ .Name }}",
"Mountpoint": "{{ .Mountpoint }}"
}`})
inspect.WaitWithDefaultTimeout()
Expect(inspect).Should(Exit(0))
mp := make(map[string]string)
err = json.Unmarshal([]byte(inspect.OutputToString()), &mp)
Expect(err).To(BeNil())
Expect(mp["Name"]).To(Equal(volName))
files, err := os.ReadDir(mp["Mountpoint"])
Expect(err).To(BeNil())
Expect(len(files)).To(Equal(1))
Expect(files[0].Name()).To(Equal(fileName))
})
// Multi doc related tests
It("podman play kube multi doc yaml with persistentVolumeClaim, service and deployment", func() {
yamlDocs := []string{}