mirror of https://github.com/containers/podman.git
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:
parent
a77ac5be83
commit
0ce234425a
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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{}
|
||||
|
|
Loading…
Reference in New Issue