Files
odo/pkg/podman/podman.go
Armel Soro c217741cc7 Add a timeout when initializing the Podman client (broken Podman should not affect odo dev on cluster) (#6808)
* Add test highlighting the issue and setting the expectations

* Add a timeout of 1s to the 'podman version' command

This command is called at dependency injection time to initialize a (nil-able) Podman client,
even if users won't use Podman at all.
As discussed, this command is supposed to be
quite fast to return, hence this timeout of 1 second.

Initially, we were using cmd.Output to get the command output,
but as reported in [1], cmd.Output does not respect the context timeout.
This explains the workaround of reading from both stdout and stderr pipes,
*and* relying on cmd.Wait() to close those pipes properly when the program exits
(either as expected or when the timeout is reached).

[1] https://github.com/golang/go/issues/57129

Co-authored-by: Philippe Martin <phmartin@redhat.com>

* Log the errors returned at dependency injection time when the optional Kubernetes/Podman clients could not be initialized

This helps debug such potential issues instead of swallowing the errors.

* Make the timeout configurable via the 'PODMAN_CMD_INIT_TIMEOUT' env var

This will allow setting a different value for environments like
in GitHub where the Podman client would take slightly more time to return
(I guess because of we are running a lot of Podman commands in parallel?).

* Increase the timeout for Podman tests to an arbitrary value of 10s

Some tests did not pass because the Podman client did not
initialize in 1s; I guess because we are running a lot of Podman commands in parallel?
This should hopefully improve this situation.

* fixup! Add a timeout of 1s to the 'podman version' command

---------

Co-authored-by: Philippe Martin <phmartin@redhat.com>
2023-05-11 15:34:34 -04:00

248 lines
6.4 KiB
Go

package podman
import (
"bufio"
"context"
"fmt"
"os/exec"
"strings"
"time"
envcontext "github.com/redhat-developer/odo/pkg/config/context"
"github.com/redhat-developer/odo/pkg/platform"
corev1 "k8s.io/api/core/v1"
jsonserializer "k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/klog"
"k8s.io/kubectl/pkg/scheme"
)
type PodmanCli struct {
podmanCmd string
podmanCmdInitTimeout time.Duration
containerRunGlobalExtraArgs []string
containerRunExtraArgs []string
}
var _ Client = (*PodmanCli)(nil)
var _ platform.Client = (*PodmanCli)(nil)
// NewPodmanCli returns a new podman client, or nil if the podman command is not accessible in the system
func NewPodmanCli(ctx context.Context) (*PodmanCli, error) {
// Check if podman is available in the system
cli := &PodmanCli{
podmanCmd: envcontext.GetEnvConfig(ctx).PodmanCmd,
podmanCmdInitTimeout: envcontext.GetEnvConfig(ctx).PodmanCmdInitTimeout,
containerRunGlobalExtraArgs: envcontext.GetEnvConfig(ctx).OdoContainerBackendGlobalArgs,
containerRunExtraArgs: envcontext.GetEnvConfig(ctx).OdoContainerRunArgs,
}
version, err := cli.Version(ctx)
if err != nil {
return nil, err
}
if version.Client == nil {
return nil, fmt.Errorf("executable %q not recognized as podman client", cli.podmanCmd)
}
return cli, nil
}
func (o *PodmanCli) PlayKube(pod *corev1.Pod) error {
serializer := jsonserializer.NewSerializerWithOptions(
jsonserializer.SimpleMetaFactory{},
scheme.Scheme,
scheme.Scheme,
jsonserializer.SerializerOptions{
Yaml: true,
},
)
// +3 because of "play kube -"
args := make([]string, 0, len(o.containerRunGlobalExtraArgs)+len(o.containerRunExtraArgs)+3)
args = append(args, o.containerRunGlobalExtraArgs...)
args = append(args, "play", "kube")
args = append(args, o.containerRunExtraArgs...)
args = append(args, "-")
cmd := exec.Command(o.podmanCmd, args...)
klog.V(3).Infof("executing %v", cmd.Args)
stdin, err := cmd.StdinPipe()
if err != nil {
return err
}
stdout, err := cmd.StdoutPipe()
if err != nil {
return err
}
cmd.Stderr = cmd.Stdout
if err = cmd.Start(); err != nil {
return err
}
if klog.V(4) {
var sb strings.Builder
_ = serializer.Encode(pod, &sb)
klog.Infof("Pod spec to play: \n---\n%s\n---\n", sb.String())
}
err = serializer.Encode(pod, stdin)
if err != nil {
return err
}
stdin.Close()
var podmanOut string
go func() {
for {
tmp := make([]byte, 1024)
_, err = stdout.Read(tmp)
podmanOut += string(tmp)
klog.V(4).Info(string(tmp))
if err != nil {
break
}
}
}()
if err = cmd.Wait(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
err = fmt.Errorf("%s: %s\nComplete Podman output:\n%s", err, string(exiterr.Stderr), podmanOut)
}
return err
}
return nil
}
func (o *PodmanCli) KubeGenerate(name string) (*corev1.Pod, error) {
serializer := jsonserializer.NewSerializerWithOptions(
jsonserializer.SimpleMetaFactory{},
scheme.Scheme,
scheme.Scheme,
jsonserializer.SerializerOptions{
Yaml: true,
},
)
cmd := exec.Command(o.podmanCmd, append(o.containerRunGlobalExtraArgs, "generate", "kube", name)...)
klog.V(3).Infof("executing %v", cmd.Args)
resultBytes, err := cmd.Output()
if err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
err = fmt.Errorf("%s: %s", err, string(exiterr.Stderr))
}
return nil, err
}
var pod corev1.Pod
_, _, err = serializer.Decode(resultBytes, nil, &pod)
if err != nil {
return nil, err
}
return &pod, nil
}
func (o *PodmanCli) PodStop(podname string) error {
cmd := exec.Command(o.podmanCmd, append(o.containerRunGlobalExtraArgs, "pod", "stop", podname)...)
klog.V(3).Infof("executing %v", cmd.Args)
out, err := cmd.Output()
if err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
err = fmt.Errorf("%s: %s", err, string(exiterr.Stderr))
}
return err
}
klog.V(4).Infof("Stopped pod %s", string(out))
return nil
}
func (o *PodmanCli) PodRm(podname string) error {
cmd := exec.Command(o.podmanCmd, append(o.containerRunGlobalExtraArgs, "pod", "rm", podname)...)
klog.V(3).Infof("executing %v", cmd.Args)
out, err := cmd.Output()
if err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
err = fmt.Errorf("%s: %s", err, string(exiterr.Stderr))
}
return err
}
klog.V(4).Infof("Deleted pod %s", string(out))
return nil
}
func (o *PodmanCli) PodLs() (map[string]bool, error) {
cmd := exec.Command(o.podmanCmd, append(o.containerRunGlobalExtraArgs, "pod", "list", "--format", "{{.Name}}", "--noheading")...)
klog.V(3).Infof("executing %v", cmd.Args)
out, err := cmd.Output()
if err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
err = fmt.Errorf("%s: %s", err, string(exiterr.Stderr))
}
return nil, err
}
return SplitLinesAsSet(string(out)), nil
}
func (o *PodmanCli) VolumeRm(volumeName string) error {
cmd := exec.Command(o.podmanCmd, append(o.containerRunGlobalExtraArgs, "volume", "rm", volumeName)...)
klog.V(3).Infof("executing %v", cmd.Args)
out, err := cmd.Output()
if err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
err = fmt.Errorf("%s: %s", err, string(exiterr.Stderr))
}
return err
}
klog.V(4).Infof("Deleted volume %s", string(out))
return nil
}
func (o *PodmanCli) VolumeLs() (map[string]bool, error) {
cmd := exec.Command(o.podmanCmd, append(o.containerRunGlobalExtraArgs, "volume", "ls", "--format", "{{.Name}}", "--noheading")...)
klog.V(3).Infof("executing %v", cmd.Args)
out, err := cmd.Output()
if err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
err = fmt.Errorf("%s: %s", err, string(exiterr.Stderr))
}
return nil, err
}
return SplitLinesAsSet(string(out)), nil
}
func (o *PodmanCli) CleanupPodResources(pod *corev1.Pod, cleanupVolumes bool) error {
err := o.PodStop(pod.GetName())
if err != nil {
return err
}
err = o.PodRm(pod.GetName())
if err != nil {
return err
}
if !cleanupVolumes {
return nil
}
for _, volume := range pod.Spec.Volumes {
if volume.PersistentVolumeClaim == nil {
continue
}
volumeName := volume.PersistentVolumeClaim.ClaimName
klog.V(3).Infof("deleting podman volume %q", volumeName)
err = o.VolumeRm(volumeName)
if err != nil {
return err
}
}
return nil
}
func SplitLinesAsSet(s string) map[string]bool {
lines := map[string]bool{}
sc := bufio.NewScanner(strings.NewReader(s))
for sc.Scan() {
lines[sc.Text()] = true
}
return lines
}