diff --git a/.gitignore b/.gitignore index 644fa2bf..cc54511b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ .DS_Store .vscode /dist/* +vendor/ diff --git a/examples/pod_exec/README.md b/examples/pod_exec/README.md new file mode 100644 index 00000000..6fb45016 --- /dev/null +++ b/examples/pod_exec/README.md @@ -0,0 +1,33 @@ +## Execute commands from the running container + +This directory contains the example of how to run a command inside a running container by buildin function `ExecInPod` provided by e2e framework. + +### How to use `ExecInPod` function? + +First of all there should be a pod with a proper container, which has `Running` status to execute commands from it. To meet status condition within a test either `wait.For()` or `resources.Watch()` may be used. + +To invoke a function it is required to pass the following parameters: + +| Param | Type | Description | +|----------------|---------------|-----------------------------------------| +| namespaceName | string | Namespace name, where the pod is running | +| podName | string | Pod name | +| containerName | string | Container name | +| command | [] string | Command to be executed in container | +| stdout | *bytes.Buffer | Buffer pointer to read from in case of successful command execution | +| stderr | *bytes.Buffer | Buffer pointer to read from in case a command failed | + +### What does this test do? + +1. Create a Kind cluster with a random name with `exectest-` as the cluster name prefix. +2. Create a custom namespace with `my-ns` as the prefix. +3. Create nginx deployment with one replica. +4. Wait for the deployment to be `Available`. +5. Use curl to request the main page of Wikipedia.org by `ExecInPod` function. +6. Check is a status code equals 200. + +### How to run the tests + +```bash +go test -v . +``` diff --git a/examples/pod_exec/envtest_test.go b/examples/pod_exec/envtest_test.go new file mode 100644 index 00000000..e021b8ff --- /dev/null +++ b/examples/pod_exec/envtest_test.go @@ -0,0 +1,100 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package execpod + +import ( + "bytes" + "context" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/e2e-framework/klient/wait" + "sigs.k8s.io/e2e-framework/klient/wait/conditions" + "strings" + "testing" + "time" + + "sigs.k8s.io/e2e-framework/pkg/envconf" + "sigs.k8s.io/e2e-framework/pkg/features" +) + +func TestExecPod(t *testing.T) { + deploymentName := "test-deployment" + containerName := "curl" + feature := features.New("Call external service"). + Setup(func(ctx context.Context, t *testing.T, c *envconf.Config) context.Context { + deployment := newDeployment(c.Namespace(), deploymentName, 1, containerName) + client, err := c.NewClient() + if err != nil { + t.Fatal(err) + } + if err := client.Resources().Create(ctx, deployment); err != nil { + t.Fatal(err) + } + err = wait.For(conditions.New(client.Resources()).DeploymentConditionMatch(deployment, appsv1.DeploymentAvailable, v1.ConditionTrue), wait.WithTimeout(time.Minute*5)) + if err != nil { + t.Fatal(err) + } + + return ctx + }). + Assess("check connectivity to wikipedia.org main page", func(ctx context.Context, t *testing.T, c *envconf.Config) context.Context { + + client, err := c.NewClient() + if err != nil { + t.Fatal(err) + } + + pods := &corev1.PodList{} + err = client.Resources(c.Namespace()).List(context.TODO(), pods) + if err != nil || pods.Items == nil { + t.Error("error while getting pods", err) + } + var stdout, stderr bytes.Buffer + podName := pods.Items[0].Name + command := []string{"curl", "-I", "https://en.wikipedia.org/wiki/Main_Page"} + + if err := client.Resources().ExecInPod(c.Namespace(), podName, containerName, command, &stdout, &stderr); err != nil { + t.Log(stderr.String()) + t.Fatal(err) + } + + httpStatus := strings.Split(stdout.String(), "\n")[0] + if !strings.Contains(httpStatus, "200") { + t.Fatal("Couldn't connect to en.wikipedia.org") + } + return ctx + }).Feature() + testEnv.Test(t, feature) +} +func newDeployment(namespace string, name string, replicas int32, containerName string) *appsv1.Deployment { + labels := map[string]string{"app": "pod-exec"} + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: labels}, + Spec: v1.PodSpec{Containers: []v1.Container{{Name: containerName, Image: "nginx"}}}, + }, + }, + } +} diff --git a/examples/pod_exec/main_test.go b/examples/pod_exec/main_test.go new file mode 100644 index 00000000..242eea03 --- /dev/null +++ b/examples/pod_exec/main_test.go @@ -0,0 +1,51 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package execpod + +import ( + "os" + "testing" + + "sigs.k8s.io/e2e-framework/pkg/env" + "sigs.k8s.io/e2e-framework/pkg/envconf" + "sigs.k8s.io/e2e-framework/pkg/envfuncs" +) + +var ( + testEnv env.Environment + kindClusterName string + namespace string +) + +func TestMain(m *testing.M) { + cfg, _ := envconf.NewFromFlags() + testEnv = env.NewWithConfig(cfg) + kindClusterName = envconf.RandomName("exectest-", 16) + namespace = envconf.RandomName("my-ns", 10) + + testEnv.Setup( + envfuncs.CreateKindCluster(kindClusterName), + envfuncs.CreateNamespace(namespace), + ) + + testEnv.Finish( + envfuncs.DeleteNamespace(namespace), + envfuncs.DestroyKindCluster(kindClusterName), + ) + + os.Exit(testEnv.Run(m)) +} diff --git a/examples/wait_for_resources/wait_test.go b/examples/wait_for_resources/wait_test.go index ef244c4b..a2a3771d 100644 --- a/examples/wait_for_resources/wait_test.go +++ b/examples/wait_for_resources/wait_test.go @@ -37,7 +37,7 @@ func TestWaitForResources(t *testing.T) { depFeature := features.New("appsv1/deployment").WithLabel("env", "dev"). Setup(func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context { // create a deployment - deployment := newDeployment(cfg.Namespace(), "test-deployment", 10) + deployment := newDeployment(cfg.Namespace(), "test-deployment", 8) client, err := cfg.NewClient() if err != nil { t.Fatal(err) @@ -59,7 +59,7 @@ func TestWaitForResources(t *testing.T) { err = wait.For(conditions.New(client.Resources()).ResourceMatch(&dep, func(object k8s.Object) bool { d := object.(*appsv1.Deployment) return float64(d.Status.ReadyReplicas)/float64(*d.Spec.Replicas) >= 0.50 - }), wait.WithTimeout(time.Minute*1)) + }), wait.WithTimeout(time.Minute*2)) if err != nil { t.Fatal(err) } diff --git a/go.mod b/go.mod index b2805972..ab0835d1 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.6 // indirect + github.com/moby/spdystream v0.2.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect diff --git a/go.sum b/go.sum index b3da6556..49fe4e5e 100644 --- a/go.sum +++ b/go.sum @@ -50,6 +50,7 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -67,6 +68,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= @@ -208,6 +210,7 @@ github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= diff --git a/klient/k8s/resources/resources.go b/klient/k8s/resources/resources.go index 6b7ac125..45f2635a 100644 --- a/klient/k8s/resources/resources.go +++ b/klient/k8s/resources/resources.go @@ -17,10 +17,15 @@ limitations under the License. package resources import ( + "bytes" "context" "errors" "time" + v1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/remotecommand" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/scheme" @@ -204,3 +209,42 @@ func (r *Resources) Watch(object k8s.ObjectList, opts ...ListOption) *watcher.Ev Cfg: r.GetConfig(), } } + +func (r *Resources) ExecInPod(namespaceName, podName, containerName string, command []string, stdout, stderr *bytes.Buffer) error { + clientset, err := kubernetes.NewForConfig(r.config) + if err != nil { + return err + } + + req := clientset.CoreV1().RESTClient().Post(). + Resource("pods"). + Name(podName). + Namespace(namespaceName). + SubResource("exec") + newScheme := runtime.NewScheme() + if err := v1.AddToScheme(newScheme); err != nil { + return err + } + parameterCodec := runtime.NewParameterCodec(newScheme) + req.VersionedParams(&v1.PodExecOptions{ + Container: containerName, + Command: command, + Stdout: true, + Stderr: true, + }, parameterCodec) + + exec, err := remotecommand.NewSPDYExecutor(r.config, "POST", req.URL()) + if err != nil { + panic(err) + } + + err = exec.Stream(remotecommand.StreamOptions{ + Stdout: stdout, + Stderr: stderr, + }) + if err != nil { + return err + } + + return nil +} diff --git a/klient/k8s/resources/resources_test.go b/klient/k8s/resources/resources_test.go index 166614d4..c0b041c5 100644 --- a/klient/k8s/resources/resources_test.go +++ b/klient/k8s/resources/resources_test.go @@ -17,12 +17,15 @@ limitations under the License. package resources import ( + "bytes" "context" "encoding/json" + "strings" "testing" "time" "github.com/vladimirvivien/gexe" + "k8s.io/apimachinery/pkg/labels" log "k8s.io/klog/v2" "sigs.k8s.io/e2e-framework/klient/k8s/resources/testdata/projectExample" @@ -280,3 +283,69 @@ func TestGetCRDs(t *testing.T) { t.Error("error while listing custom resources", err) } } + +func TestExecInPod(t *testing.T) { + res, err := New(cfg) + containerName := "nginx" + if err != nil { + t.Fatalf("Error initiating runtime controller: %v", err) + } + namespace := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-exec-ns"}} + err = res.Create(context.TODO(), namespace) + if err != nil { + t.Fatalf("Error while creating namespace resource: %v", err) + } + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "test-exec", Namespace: namespace.Name}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: containerName, Image: "nginx"}}}, + } + + err = res.Create(context.TODO(), pod) + if err != nil { + t.Error("Error while creating pod resource", err) + } + + pods := &corev1.PodList{} + err = res.List(context.TODO(), pods) + if err != nil { + t.Error("error while getting pods", err) + } + if pods.Items == nil { + t.Error("error while getting the list of pods", err) + } + var stdout, stderr bytes.Buffer + + addWait := make(chan struct{}) + onAddfunc := func(obj interface{}) { + addWait <- struct{}{} + } + w := res.Watch(&corev1.PodList{}, WithFieldSelector(labels.FormatLabels( + map[string]string{ + "metadata.name": pod.Name, + "metadata.namespace": namespace.Name, + "status.phase": "Running", + }))). + WithAddFunc(onAddfunc) + + if err = w.Start(ctx); err != nil { + t.Fatal(err) + } + + select { + case <-time.After(300 * time.Second): + t.Error("Add callback not called") + case <-addWait: + close(addWait) + } + + if err := res.ExecInPod(namespace.Name, pod.Name, containerName, []string{"printenv"}, &stdout, &stderr); err != nil { + t.Log(stderr.String()) + t.Fatal(err) + } + + hostName := "HOSTNAME=" + pod.Name + if !strings.Contains(stdout.String(), hostName) { + t.Fatal("Couldn't find proper env") + } +}