diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 5ce6fbd5ef4..d6f501985f2 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1666,63 +1666,73 @@ }, { "ImportPath": "github.com/onsi/gomega", - "Comment": "v1.0-115-g9ed8da1", - "Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1" + "Comment": "v1.0-122-gd59fa0a", + "Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3" }, { "ImportPath": "github.com/onsi/gomega/format", - "Comment": "v1.0-115-g9ed8da1", - "Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1" + "Comment": "v1.0-122-gd59fa0a", + "Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3" + }, + { + "ImportPath": "github.com/onsi/gomega/gstruct", + "Comment": "v1.0-122-gd59fa0a", + "Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3" + }, + { + "ImportPath": "github.com/onsi/gomega/gstruct/errors", + "Comment": "v1.0-122-gd59fa0a", + "Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3" }, { "ImportPath": "github.com/onsi/gomega/internal/assertion", - "Comment": "v1.0-115-g9ed8da1", - "Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1" + "Comment": "v1.0-122-gd59fa0a", + "Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3" }, { "ImportPath": "github.com/onsi/gomega/internal/asyncassertion", - "Comment": "v1.0-115-g9ed8da1", - "Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1" + "Comment": "v1.0-122-gd59fa0a", + "Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3" }, { "ImportPath": "github.com/onsi/gomega/internal/oraclematcher", - "Comment": "v1.0-115-g9ed8da1", - "Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1" + "Comment": "v1.0-122-gd59fa0a", + "Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3" }, { "ImportPath": "github.com/onsi/gomega/internal/testingtsupport", - "Comment": "v1.0-115-g9ed8da1", - "Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1" + "Comment": "v1.0-122-gd59fa0a", + "Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3" }, { "ImportPath": "github.com/onsi/gomega/matchers", - "Comment": "v1.0-115-g9ed8da1", - "Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1" + "Comment": "v1.0-122-gd59fa0a", + "Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3" }, { "ImportPath": "github.com/onsi/gomega/matchers/support/goraph/bipartitegraph", - "Comment": "v1.0-115-g9ed8da1", - "Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1" + "Comment": "v1.0-122-gd59fa0a", + "Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3" }, { "ImportPath": "github.com/onsi/gomega/matchers/support/goraph/edge", - "Comment": "v1.0-115-g9ed8da1", - "Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1" + "Comment": "v1.0-122-gd59fa0a", + "Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3" }, { "ImportPath": "github.com/onsi/gomega/matchers/support/goraph/node", - "Comment": "v1.0-115-g9ed8da1", - "Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1" + "Comment": "v1.0-122-gd59fa0a", + "Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3" }, { "ImportPath": "github.com/onsi/gomega/matchers/support/goraph/util", - "Comment": "v1.0-115-g9ed8da1", - "Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1" + "Comment": "v1.0-122-gd59fa0a", + "Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3" }, { "ImportPath": "github.com/onsi/gomega/types", - "Comment": "v1.0-115-g9ed8da1", - "Rev": "9ed8da19f2156b87a803a8fdf6d126f627a12db1" + "Comment": "v1.0-122-gd59fa0a", + "Rev": "d59fa0ac68bb5dd932ee8d24eed631cdd519efc3" }, { "ImportPath": "github.com/opencontainers/runc/libcontainer", diff --git a/Godeps/LICENSES b/Godeps/LICENSES index 3c22d1d831d..df98fd04f9b 100644 --- a/Godeps/LICENSES +++ b/Godeps/LICENSES @@ -53508,6 +53508,62 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================================ +================================================================================ += vendor/github.com/onsi/gomega/gstruct licensed under: = + +Copyright (c) 2013-2014 Onsi Fakhouri + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + += vendor/github.com/onsi/gomega/LICENSE 570603114d52313cb86c0206401c9af7 - +================================================================================ + + +================================================================================ += vendor/github.com/onsi/gomega/gstruct/errors licensed under: = + +Copyright (c) 2013-2014 Onsi Fakhouri + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + += vendor/github.com/onsi/gomega/LICENSE 570603114d52313cb86c0206401c9af7 - +================================================================================ + + ================================================================================ = vendor/github.com/onsi/gomega/internal/assertion licensed under: = diff --git a/test/e2e_node/kubelet_test.go b/test/e2e_node/kubelet_test.go index 9202e1e0797..cc150e7aab2 100644 --- a/test/e2e_node/kubelet_test.go +++ b/test/e2e_node/kubelet_test.go @@ -18,21 +18,14 @@ package e2e_node import ( "bytes" - "encoding/json" "fmt" - "io/ioutil" - "net/http" - "strings" "time" "k8s.io/kubernetes/pkg/api" apiUnversioned "k8s.io/kubernetes/pkg/api/unversioned" - "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/stats" - "k8s.io/kubernetes/pkg/util/sets" "k8s.io/kubernetes/pkg/util/uuid" "k8s.io/kubernetes/test/e2e/framework" - "github.com/davecgh/go-spew/spew" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) @@ -158,322 +151,4 @@ var _ = framework.KubeDescribe("Kubelet", func() { }, time.Minute, time.Second*4).Should(Equal("sh: can't create /file: Read-only file system\n")) }) }) - Describe("metrics api", func() { - Context("when querying /stats/summary", func() { - It("it should report resource usage through the stats api", func() { - podNamePrefix := "stats-busybox-" + string(uuid.NewUUID()) - volumeNamePrefix := "test-empty-dir" - podNames, volumes := createSummaryTestPods(f.PodClient(), podNamePrefix, 2, volumeNamePrefix) - By("Returning stats summary") - summary := stats.Summary{} - Eventually(func() error { - resp, err := http.Get(*kubeletAddress + "/stats/summary") - if err != nil { - return fmt.Errorf("Failed to get /stats/summary - %v", err) - } - contentsBytes, err := ioutil.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("Failed to read /stats/summary - %+v", resp) - } - contents := string(contentsBytes) - decoder := json.NewDecoder(strings.NewReader(contents)) - err = decoder.Decode(&summary) - if err != nil { - return fmt.Errorf("Failed to parse /stats/summary to go struct: %+v", resp) - } - missingPods := podsMissingFromSummary(summary, podNames) - if missingPods.Len() != 0 { - return fmt.Errorf("expected pods not found. Following pods are missing - %v", missingPods) - } - missingVolumes := volumesMissingFromSummary(summary, volumes) - if missingVolumes.Len() != 0 { - return fmt.Errorf("expected volumes not found. Following volumes are missing - %v", missingVolumes) - } - if err := testSummaryMetrics(summary, podNamePrefix); err != nil { - return err - } - return nil - }, 5*time.Minute, time.Second*4).Should(BeNil()) - }) - }) - }) }) - -const ( - containerSuffix = "-c" -) - -func createSummaryTestPods(podClient *framework.PodClient, podNamePrefix string, count int, volumeNamePrefix string) (sets.String, sets.String) { - podNames := sets.NewString() - volumes := sets.NewString(volumeNamePrefix) - for i := 0; i < count; i++ { - podNames.Insert(fmt.Sprintf("%s%v", podNamePrefix, i)) - } - - var pods []*api.Pod - for _, podName := range podNames.List() { - pods = append(pods, &api.Pod{ - ObjectMeta: api.ObjectMeta{ - Name: podName, - }, - Spec: api.PodSpec{ - // Don't restart the Pod since it is expected to exit - RestartPolicy: api.RestartPolicyNever, - Containers: []api.Container{ - { - Image: "gcr.io/google_containers/busybox:1.24", - Command: []string{"sh", "-c", "while true; do echo 'hello world' | tee /test-empty-dir-mnt/file ; sleep 1; done"}, - Name: podName + containerSuffix, - VolumeMounts: []api.VolumeMount{ - {MountPath: "/test-empty-dir-mnt", Name: volumeNamePrefix}, - }, - }, - }, - SecurityContext: &api.PodSecurityContext{ - SELinuxOptions: &api.SELinuxOptions{ - Level: "s0", - }, - }, - Volumes: []api.Volume{ - // TODO: Test secret volumes - // TODO: Test hostpath volumes - {Name: volumeNamePrefix, VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, - }, - }, - }) - } - podClient.CreateBatch(pods) - - return podNames, volumes -} - -// Returns pods missing from summary. -func podsMissingFromSummary(s stats.Summary, expectedPods sets.String) sets.String { - expectedPods = sets.StringKeySet(expectedPods) - for _, pod := range s.Pods { - if expectedPods.Has(pod.PodRef.Name) { - expectedPods.Delete(pod.PodRef.Name) - } - } - return expectedPods -} - -// Returns volumes missing from summary. -func volumesMissingFromSummary(s stats.Summary, expectedVolumes sets.String) sets.String { - for _, pod := range s.Pods { - expectedPodVolumes := sets.StringKeySet(expectedVolumes) - for _, vs := range pod.VolumeStats { - if expectedPodVolumes.Has(vs.Name) { - expectedPodVolumes.Delete(vs.Name) - } - } - if expectedPodVolumes.Len() != 0 { - return expectedPodVolumes - } - } - return sets.NewString() -} - -func testSummaryMetrics(s stats.Summary, podNamePrefix string) error { - const ( - nonNilValue = "expected %q to not be nil" - nonZeroValue = "expected %q to not be zero" - ) - if s.Node.NodeName != framework.TestContext.NodeName { - return fmt.Errorf("unexpected node name - %q", s.Node.NodeName) - } - if s.Node.CPU.UsageCoreNanoSeconds == nil { - return fmt.Errorf(nonNilValue, "cpu instantaneous") - } - if *s.Node.CPU.UsageCoreNanoSeconds == 0 { - return fmt.Errorf(nonZeroValue, "cpu instantaneous") - } - if s.Node.Memory.UsageBytes == nil { - return fmt.Errorf(nonNilValue, "memory") - } - if *s.Node.Memory.UsageBytes == 0 { - return fmt.Errorf(nonZeroValue, "memory") - } - if s.Node.Memory.WorkingSetBytes == nil { - return fmt.Errorf(nonNilValue, "memory working set") - } - if *s.Node.Memory.WorkingSetBytes == 0 { - return fmt.Errorf(nonZeroValue, "memory working set") - } - if s.Node.Fs.AvailableBytes == nil { - return fmt.Errorf(nonNilValue, "memory working set") - } - if *s.Node.Fs.AvailableBytes == 0 { - return fmt.Errorf(nonZeroValue, "node Fs available") - } - if s.Node.Fs.CapacityBytes == nil { - return fmt.Errorf(nonNilValue, "node fs capacity") - } - if *s.Node.Fs.CapacityBytes == 0 { - return fmt.Errorf(nonZeroValue, "node fs capacity") - } - if s.Node.Fs.UsedBytes == nil { - return fmt.Errorf(nonNilValue, "node fs used") - } - if *s.Node.Fs.UsedBytes == 0 { - return fmt.Errorf(nonZeroValue, "node fs used") - } - - if s.Node.Runtime == nil { - return fmt.Errorf(nonNilValue, "node runtime") - } - if s.Node.Runtime.ImageFs == nil { - return fmt.Errorf(nonNilValue, "runtime image Fs") - } - if s.Node.Runtime.ImageFs.AvailableBytes == nil { - return fmt.Errorf(nonNilValue, "runtime image Fs available") - } - if *s.Node.Runtime.ImageFs.AvailableBytes == 0 { - return fmt.Errorf(nonZeroValue, "runtime image Fs available") - } - if s.Node.Runtime.ImageFs.CapacityBytes == nil { - return fmt.Errorf(nonNilValue, "runtime image Fs capacity") - } - if *s.Node.Runtime.ImageFs.CapacityBytes == 0 { - return fmt.Errorf(nonZeroValue, "runtime image Fs capacity") - } - if s.Node.Runtime.ImageFs.UsedBytes == nil { - return fmt.Errorf(nonNilValue, "runtime image Fs usage") - } - if *s.Node.Runtime.ImageFs.UsedBytes == 0 { - return fmt.Errorf(nonZeroValue, "runtime image Fs usage") - } - sysContainers := map[string]stats.ContainerStats{} - for _, container := range s.Node.SystemContainers { - sysContainers[container.Name] = container - if err := expectContainerStatsNotEmpty(&container); err != nil { - return err - } - } - if _, exists := sysContainers["kubelet"]; !exists { - return fmt.Errorf("expected metrics for kubelet") - } - if _, exists := sysContainers["runtime"]; !exists { - return fmt.Errorf("expected metrics for runtime") - } - // Verify Pods Stats are present - podsList := []string{} - By("Having resources for pods") - for _, pod := range s.Pods { - if !strings.HasPrefix(pod.PodRef.Name, podNamePrefix) { - // Ignore pods created outside this test - continue - } - - podsList = append(podsList, pod.PodRef.Name) - - if len(pod.Containers) != 1 { - return fmt.Errorf("expected only one container") - } - container := pod.Containers[0] - - if container.Name != (pod.PodRef.Name + containerSuffix) { - return fmt.Errorf("unexpected container name - %q", container.Name) - } - - if err := expectContainerStatsNotEmpty(&container); err != nil { - return err - } - - // emptydir volume - foundExpectedVolume := false - for _, vs := range pod.VolumeStats { - if *vs.CapacityBytes == 0 { - return fmt.Errorf(nonZeroValue, "volume capacity") - } - if *vs.AvailableBytes == 0 { - return fmt.Errorf(nonZeroValue, "volume available") - } - if *vs.UsedBytes == 0 { - return fmt.Errorf(nonZeroValue, "volume used") - } - if vs.Name == "test-empty-dir" { - foundExpectedVolume = true - } - } - if !foundExpectedVolume { - return fmt.Errorf("expected 'test-empty-dir' volume") - } - - // fs usage (not for system containers) - if container.Rootfs == nil { - return fmt.Errorf(nonNilValue+" - "+spew.Sdump(container), "container root fs") - } - if container.Rootfs.AvailableBytes == nil { - return fmt.Errorf(nonNilValue+" - "+spew.Sdump(container), "container root fs available") - } - if *container.Rootfs.AvailableBytes == 0 { - return fmt.Errorf(nonZeroValue+" - "+spew.Sdump(container), "container root fs available") - } - if container.Rootfs.CapacityBytes == nil { - return fmt.Errorf(nonNilValue+" - "+spew.Sdump(container), "container root fs capacity") - } - if *container.Rootfs.CapacityBytes == 0 { - return fmt.Errorf(nonZeroValue+" - "+spew.Sdump(container), "container root fs capacity") - } - if container.Rootfs.UsedBytes == nil { - return fmt.Errorf(nonNilValue+" - "+spew.Sdump(container), "container root fs usage") - } - if *container.Rootfs.UsedBytes == 0 { - return fmt.Errorf(nonZeroValue+" - "+spew.Sdump(container), "container root fs usage") - } - if container.Logs == nil { - return fmt.Errorf(nonNilValue+" - "+spew.Sdump(container), "container logs") - } - if container.Logs.AvailableBytes == nil { - return fmt.Errorf(nonNilValue+" - "+spew.Sdump(container), "container logs available") - } - if *container.Logs.AvailableBytes == 0 { - return fmt.Errorf(nonZeroValue+" - "+spew.Sdump(container), "container logs available") - } - if container.Logs.CapacityBytes == nil { - return fmt.Errorf(nonNilValue+" - "+spew.Sdump(container), "container logs capacity") - } - if *container.Logs.CapacityBytes == 0 { - return fmt.Errorf(nonZeroValue+" - "+spew.Sdump(container), "container logs capacity") - } - if container.Logs.UsedBytes == nil { - return fmt.Errorf(nonNilValue+" - "+spew.Sdump(container), "container logs usage") - } - if *container.Logs.UsedBytes == 0 { - return fmt.Errorf(nonZeroValue+" - "+spew.Sdump(container), "container logs usage") - } - } - return nil -} - -func expectContainerStatsNotEmpty(container *stats.ContainerStats) error { - // TODO: Test Network - - if container.CPU == nil { - return fmt.Errorf("expected container cpu to be not nil - %q", spew.Sdump(container)) - } - if container.CPU.UsageCoreNanoSeconds == nil { - return fmt.Errorf("expected container cpu instantaneous usage to be not nil - %q", spew.Sdump(container)) - } - if *container.CPU.UsageCoreNanoSeconds == 0 { - return fmt.Errorf("expected container cpu instantaneous usage to be non zero - %q", spew.Sdump(container)) - } - - if container.Memory == nil { - return fmt.Errorf("expected container memory to be not nil - %q", spew.Sdump(container)) - } - if container.Memory.UsageBytes == nil { - return fmt.Errorf("expected container memory usage to be not nil - %q", spew.Sdump(container)) - } - if *container.Memory.UsageBytes == 0 { - return fmt.Errorf("expected container memory usage to be non zero - %q", spew.Sdump(container)) - } - if container.Memory.WorkingSetBytes == nil { - return fmt.Errorf("expected container memory working set to be not nil - %q", spew.Sdump(container)) - } - if *container.Memory.WorkingSetBytes == 0 { - return fmt.Errorf("expected container memory working set to be non zero - %q", spew.Sdump(container)) - } - return nil -} diff --git a/test/e2e_node/summary_test.go b/test/e2e_node/summary_test.go new file mode 100644 index 00000000000..5b375e58245 --- /dev/null +++ b/test/e2e_node/summary_test.go @@ -0,0 +1,279 @@ +/* +Copyright 2016 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 e2e_node + +import ( + "fmt" + "time" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/resource" + "k8s.io/kubernetes/pkg/kubelet/api/v1alpha1/stats" + "k8s.io/kubernetes/test/e2e/framework" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gstruct" + "github.com/onsi/gomega/types" +) + +// TODO(timstclair): Move this test out of the flaky suite once it has demonstrated stability. +var _ = framework.KubeDescribe("Summary API [Flaky]", func() { + f := framework.NewDefaultFramework("summary-test") + Context("when querying /stats/summary", func() { + It("should report resource usage through the stats api", func() { + const pod0 = "stats-busybox-0" + const pod1 = "stats-busybox-1" + + By("Creating test pods") + createSummaryTestPods(f, pod0, pod1) + // Wait for cAdvisor to collect 2 stats points + time.Sleep(15 * time.Second) + + // Setup expectations. + const ( + kb = 1000 + mb = 1000 * kb + gb = 1000 * mb + tb = 1000 * gb + + maxStartAge = time.Hour * 24 * 365 // 1 year + maxStatsAge = time.Minute + ) + fsCapacityBounds := bounded(100*mb, 100*gb) + // Expectations for system containers. + sysContExpectations := gstruct.MatchAllFields(gstruct.Fields{ + "Name": gstruct.Ignore(), + "StartTime": recent(maxStartAge), + "CPU": ptrMatchAllFields(gstruct.Fields{ + "Time": recent(maxStatsAge), + "UsageNanoCores": bounded(100000, 2E9), + "UsageCoreNanoSeconds": bounded(10000000, 1E15), + }), + "Memory": ptrMatchAllFields(gstruct.Fields{ + "Time": recent(maxStatsAge), + // We don't limit system container memory. + "AvailableBytes": BeNil(), + "UsageBytes": bounded(5*mb, 1*gb), + "WorkingSetBytes": bounded(5*mb, 1*gb), + "RSSBytes": bounded(5*mb, 1*gb), + "PageFaults": bounded(1000, 1E9), + "MajorPageFaults": bounded(0, 100000), + }), + // TODO(#31999): Don't report FS stats for system containers. + "Rootfs": gstruct.Ignore(), + "Logs": gstruct.Ignore(), + "UserDefinedMetrics": BeEmpty(), + }) + // Expectations for pods. + podExpectations := gstruct.MatchAllFields(gstruct.Fields{ + "PodRef": gstruct.Ignore(), + "StartTime": recent(maxStartAge), + "Containers": gstruct.MatchAllElements(summaryObjectID, gstruct.Elements{ + "busybox-container": gstruct.MatchAllFields(gstruct.Fields{ + "Name": Equal("busybox-container"), + "StartTime": recent(maxStartAge), + "CPU": ptrMatchAllFields(gstruct.Fields{ + "Time": recent(maxStatsAge), + "UsageNanoCores": bounded(100000, 100000000), + "UsageCoreNanoSeconds": bounded(10000000, 1000000000), + }), + "Memory": ptrMatchAllFields(gstruct.Fields{ + "Time": recent(maxStatsAge), + "AvailableBytes": bounded(1*mb, 10*mb), + "UsageBytes": bounded(10*kb, mb), + "WorkingSetBytes": bounded(10*kb, mb), + "RSSBytes": bounded(1*kb, mb), + "PageFaults": bounded(100, 100000), + "MajorPageFaults": bounded(0, 10), + }), + "Rootfs": ptrMatchAllFields(gstruct.Fields{ + "AvailableBytes": fsCapacityBounds, + "CapacityBytes": fsCapacityBounds, + "UsedBytes": bounded(kb, 10*mb), + "InodesFree": bounded(1E4, 1E8), + "Inodes": bounded(1E4, 1E8), + }), + "Logs": ptrMatchAllFields(gstruct.Fields{ + "AvailableBytes": fsCapacityBounds, + "CapacityBytes": fsCapacityBounds, + "UsedBytes": bounded(kb, 10*mb), + "InodesFree": bounded(1E4, 1E8), + "Inodes": bounded(1E4, 1E8), + }), + "UserDefinedMetrics": BeEmpty(), + }), + }), + "Network": ptrMatchAllFields(gstruct.Fields{ + "Time": recent(maxStatsAge), + "RxBytes": bounded(10, 10*mb), + "RxErrors": bounded(0, 1000), + "TxBytes": bounded(10, 10*mb), + "TxErrors": bounded(0, 1000), + }), + "VolumeStats": gstruct.MatchAllElements(summaryObjectID, gstruct.Elements{ + "test-empty-dir": gstruct.MatchAllFields(gstruct.Fields{ + "Name": Equal("test-empty-dir"), + "FsStats": gstruct.MatchAllFields(gstruct.Fields{ + "AvailableBytes": fsCapacityBounds, + "CapacityBytes": fsCapacityBounds, + "UsedBytes": bounded(kb, 1*mb), + // Inodes are not reported for Volumes. + "InodesFree": BeNil(), + "Inodes": BeNil(), + }), + }), + }), + }) + matchExpectations := ptrMatchAllFields(gstruct.Fields{ + "Node": gstruct.MatchAllFields(gstruct.Fields{ + "NodeName": Equal(framework.TestContext.NodeName), + "StartTime": recent(maxStartAge), + "SystemContainers": gstruct.MatchElements(summaryObjectID, gstruct.IgnoreExtras, gstruct.Elements{ + "kubelet": sysContExpectations, + "runtime": sysContExpectations, + }), + "CPU": ptrMatchAllFields(gstruct.Fields{ + "Time": recent(maxStatsAge), + "UsageNanoCores": bounded(100E3, 2E9), + "UsageCoreNanoSeconds": bounded(1E9, 1E15), + }), + "Memory": ptrMatchAllFields(gstruct.Fields{ + "Time": recent(maxStatsAge), + "AvailableBytes": bounded(100*mb, 100*gb), + "UsageBytes": bounded(10*mb, 10*gb), + "WorkingSetBytes": bounded(10*mb, 10*gb), + "RSSBytes": bounded(1*kb, 1*gb), + "PageFaults": bounded(1000, 1E9), + "MajorPageFaults": bounded(0, 100000), + }), + // TODO(#28407): Handle non-eth0 network interface names. + "Network": Or(BeNil(), ptrMatchAllFields(gstruct.Fields{ + "Time": recent(maxStatsAge), + "RxBytes": bounded(1*mb, 100*gb), + "RxErrors": bounded(0, 100000), + "TxBytes": bounded(10*kb, 10*gb), + "TxErrors": bounded(0, 100000), + })), + "Fs": ptrMatchAllFields(gstruct.Fields{ + "AvailableBytes": fsCapacityBounds, + "CapacityBytes": fsCapacityBounds, + "UsedBytes": bounded(kb, 10*gb), + "InodesFree": bounded(1E4, 1E8), + "Inodes": bounded(1E4, 1E8), + }), + "Runtime": ptrMatchAllFields(gstruct.Fields{ + "ImageFs": ptrMatchAllFields(gstruct.Fields{ + "AvailableBytes": fsCapacityBounds, + "CapacityBytes": fsCapacityBounds, + "UsedBytes": bounded(kb, 10*gb), + "InodesFree": bounded(1E4, 1E8), + "Inodes": bounded(1E4, 1E8), + }), + }), + }), + // Ignore extra pods since the tests run in parallel. + "Pods": gstruct.MatchElements(summaryObjectID, gstruct.IgnoreExtras, gstruct.Elements{ + fmt.Sprintf("%s::%s", f.Namespace.Name, pod0): podExpectations, + fmt.Sprintf("%s::%s", f.Namespace.Name, pod1): podExpectations, + }), + }) + + By("Validating /stats/summary") + // Give pods a minute to actually start up. + Eventually(getNodeSummary, 1*time.Minute, 15*time.Second).Should(matchExpectations) + // Then the summary should match the expectations a few more times. + Consistently(getNodeSummary, 30*time.Second, 15*time.Second).Should(matchExpectations) + }) + }) +}) + +func createSummaryTestPods(f *framework.Framework, names ...string) { + pods := make([]*api.Pod, 0, len(names)) + for _, name := range names { + pods = append(pods, &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: name, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyAlways, + Containers: []api.Container{ + { + Name: "busybox-container", + Image: "gcr.io/google_containers/busybox:1.24", + Command: []string{"sh", "-c", "ping -c 1 google.com; while true; do echo 'hello world' | tee /test-empty-dir-mnt/file ; sleep 1; done"}, + Resources: api.ResourceRequirements{ + Limits: api.ResourceList{ + // Must set memory limit to get MemoryStats.AvailableBytes + api.ResourceMemory: resource.MustParse("10M"), + }, + }, + VolumeMounts: []api.VolumeMount{ + {MountPath: "/test-empty-dir-mnt", Name: "test-empty-dir"}, + }, + }, + }, + SecurityContext: &api.PodSecurityContext{ + SELinuxOptions: &api.SELinuxOptions{ + Level: "s0", + }, + }, + Volumes: []api.Volume{ + // TODO(#28393): Test secret volumes + // TODO(#28394): Test hostpath volumes + {Name: "test-empty-dir", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + }, + }, + }) + } + f.PodClient().CreateBatch(pods) +} + +// Mapping function for gstruct.MatchAllElements +func summaryObjectID(element interface{}) string { + switch el := element.(type) { + case stats.PodStats: + return fmt.Sprintf("%s::%s", el.PodRef.Namespace, el.PodRef.Name) + case stats.ContainerStats: + return el.Name + case stats.VolumeStats: + return el.Name + case stats.UserDefinedMetric: + return el.Name + default: + framework.Failf("Unknown type: %T", el) + return "???" + } +} + +// Convenience functions for common matcher combinations. +func ptrMatchAllFields(fields gstruct.Fields) types.GomegaMatcher { + return gstruct.PointTo(gstruct.MatchAllFields(fields)) +} + +func bounded(lower, upper interface{}) types.GomegaMatcher { + return gstruct.PointTo(And( + BeNumerically(">=", lower), + BeNumerically("<=", upper))) +} + +func recent(d time.Duration) types.GomegaMatcher { + return And( + BeTemporally(">=", time.Now().Add(-d)), + // Now() is the test start time, not the match time, so permit a few extra minutes. + BeTemporally("<", time.Now().Add(2*time.Minute))) +} diff --git a/vendor/github.com/onsi/gomega/README.md b/vendor/github.com/onsi/gomega/README.md index c8255919225..d1add5ba0c4 100644 --- a/vendor/github.com/onsi/gomega/README.md +++ b/vendor/github.com/onsi/gomega/README.md @@ -10,6 +10,10 @@ To discuss Gomega and get updates, join the [google group](https://groups.google Learn more about Ginkgo [here](http://onsi.github.io/ginkgo/) +## Community Matchers + +A collection of community matchers is available on the [wiki](https://github.com/onsi/gomega/wiki). + ## License Gomega is MIT-Licensed diff --git a/vendor/github.com/onsi/gomega/format/format.go b/vendor/github.com/onsi/gomega/format/format.go index 226b8925dda..06355d945bc 100644 --- a/vendor/github.com/onsi/gomega/format/format.go +++ b/vendor/github.com/onsi/gomega/format/format.go @@ -7,6 +7,7 @@ import ( "fmt" "reflect" "strings" + "strconv" ) // Use MaxDepth to set the maximum recursion depth when printing deeply nested objects @@ -143,9 +144,6 @@ func formatValue(value reflect.Value, indentation uint) string { case reflect.Ptr: return formatValue(value.Elem(), indentation) case reflect.Slice: - if value.Type().Elem().Kind() == reflect.Uint8 { - return formatString(value.Bytes(), indentation) - } return formatSlice(value, indentation) case reflect.String: return formatString(value.String(), indentation) @@ -189,6 +187,10 @@ func formatString(object interface{}, indentation uint) string { } func formatSlice(v reflect.Value, indentation uint) string { + if v.Kind() == reflect.Slice && v.Type().Elem().Kind() == reflect.Uint8 && isPrintableString(string(v.Bytes())){ + return formatString(v.Bytes(), indentation) + } + l := v.Len() result := make([]string, l) longest := 0 @@ -261,3 +263,15 @@ func isNilValue(a reflect.Value) bool { return false } + +/* +Returns true when the string is entirely made of printable runes, false otherwise. +*/ +func isPrintableString(str string) bool { + for _, runeValue := range str { + if !strconv.IsPrint(runeValue) { + return false + } + } + return true +} diff --git a/vendor/github.com/onsi/gomega/gstruct/elements.go b/vendor/github.com/onsi/gomega/gstruct/elements.go new file mode 100644 index 00000000000..9b6b134649b --- /dev/null +++ b/vendor/github.com/onsi/gomega/gstruct/elements.go @@ -0,0 +1,141 @@ +package gstruct + +import ( + "errors" + "fmt" + "reflect" + "runtime/debug" + + "github.com/onsi/gomega/format" + errorsutil "github.com/onsi/gomega/gstruct/errors" + "github.com/onsi/gomega/types" +) + +//MatchAllElements succeeds if every element of a slice matches the element matcher it maps to +//through the id function, and every element matcher is matched. +// Expect([]string{"a", "b"}).To(MatchAllElements(idFn, matchers.Elements{ +// "a": BeEqual("a"), +// "b": BeEqual("b"), +// }) +func MatchAllElements(identifier Identifier, elements Elements) types.GomegaMatcher { + return &ElementsMatcher{ + Identifier: identifier, + Elements: elements, + } +} + +//MatchElements succeeds if each element of a slice matches the element matcher it maps to +//through the id function. It can ignore extra elements and/or missing elements. +// Expect([]string{"a", "c"}).To(MatchElements(idFn, IgnoreMissing|IgnoreExtra, matchers.Elements{ +// "a": BeEqual("a") +// "b": BeEqual("b"), +// }) +func MatchElements(identifier Identifier, options Options, elements Elements) types.GomegaMatcher { + return &ElementsMatcher{ + Identifier: identifier, + Elements: elements, + IgnoreExtras: options&IgnoreExtras != 0, + IgnoreMissing: options&IgnoreMissing != 0, + } +} + +// ElementsMatcher is a NestingMatcher that applies custom matchers to each element of a slice mapped +// by the Identifier function. +// TODO: Extend this to work with arrays & maps (map the key) as well. +type ElementsMatcher struct { + // Matchers for each element. + Elements Elements + // Function mapping an element to the string key identifying its matcher. + Identifier Identifier + + // Whether to ignore extra elements or consider it an error. + IgnoreExtras bool + // Whether to ignore missing elements or consider it an error. + IgnoreMissing bool + + // State. + failures []error +} + +// Element ID to matcher. +type Elements map[string]types.GomegaMatcher + +// Function for identifying (mapping) elements. +type Identifier func(element interface{}) string + +func (m *ElementsMatcher) Match(actual interface{}) (success bool, err error) { + if reflect.TypeOf(actual).Kind() != reflect.Slice { + return false, fmt.Errorf("%v is type %T, expected slice", actual, actual) + } + + m.failures = m.matchElements(actual) + if len(m.failures) > 0 { + return false, nil + } + return true, nil +} + +func (m *ElementsMatcher) matchElements(actual interface{}) (errs []error) { + // Provide more useful error messages in the case of a panic. + defer func() { + if err := recover(); err != nil { + errs = append(errs, fmt.Errorf("panic checking %+v: %v\n%s", actual, err, debug.Stack())) + } + }() + + val := reflect.ValueOf(actual) + elements := map[string]bool{} + for i := 0; i < val.Len(); i++ { + element := val.Index(i).Interface() + id := m.Identifier(element) + // TODO: Add options to ignore & match duplicates. + if elements[id] { + errs = append(errs, fmt.Errorf("found duplicate element ID %s", id)) + continue + } + elements[id] = true + + matcher, expected := m.Elements[id] + if !expected { + if !m.IgnoreExtras { + errs = append(errs, fmt.Errorf("unexpected element %s", id)) + } + continue + } + + match, err := matcher.Match(element) + if match { + continue + } + + if err == nil { + if nesting, ok := matcher.(errorsutil.NestingMatcher); ok { + err = errorsutil.AggregateError(nesting.Failures()) + } else { + err = errors.New(matcher.FailureMessage(element)) + } + } + errs = append(errs, errorsutil.Nest(fmt.Sprintf("[%s]", id), err)) + } + + for id := range m.Elements { + if !elements[id] && !m.IgnoreMissing { + errs = append(errs, fmt.Errorf("missing expected element %s", id)) + } + } + + return errs +} + +func (m *ElementsMatcher) FailureMessage(actual interface{}) (message string) { + failure := errorsutil.AggregateError(m.failures) + return format.Message(actual, fmt.Sprintf("to match elements: %v", failure)) +} + +func (m *ElementsMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to match elements") +} + +func (m *ElementsMatcher) Failures() []error { + return m.failures +} diff --git a/vendor/github.com/onsi/gomega/gstruct/errors/nested_types.go b/vendor/github.com/onsi/gomega/gstruct/errors/nested_types.go new file mode 100644 index 00000000000..188492b212f --- /dev/null +++ b/vendor/github.com/onsi/gomega/gstruct/errors/nested_types.go @@ -0,0 +1,72 @@ +package errors + +import ( + "fmt" + "strings" + + "github.com/onsi/gomega/types" +) + +// A stateful matcher that nests other matchers within it and preserves the error types of the +// nested matcher failures. +type NestingMatcher interface { + types.GomegaMatcher + + // Returns the failures of nested matchers. + Failures() []error +} + +// An error type for labeling errors on deeply nested matchers. +type NestedError struct { + Path string + Err error +} + +func (e *NestedError) Error() string { + // Indent Errors. + indented := strings.Replace(e.Err.Error(), "\n", "\n\t", -1) + return fmt.Sprintf("%s:\n\t%v", e.Path, indented) +} + +// Create a NestedError with the given path. +// If err is a NestedError, prepend the path to it. +// If err is an AggregateError, recursively Nest each error. +func Nest(path string, err error) error { + if ag, ok := err.(AggregateError); ok { + var errs AggregateError + for _, e := range ag { + errs = append(errs, Nest(path, e)) + } + return errs + } + if ne, ok := err.(*NestedError); ok { + return &NestedError{ + Path: path + ne.Path, + Err: ne.Err, + } + } + return &NestedError{ + Path: path, + Err: err, + } +} + +// An error type for treating multiple errors as a single error. +type AggregateError []error + +// Error is part of the error interface. +func (err AggregateError) Error() string { + if len(err) == 0 { + // This should never happen, really. + return "" + } + if len(err) == 1 { + return err[0].Error() + } + result := fmt.Sprintf("[%s", err[0].Error()) + for i := 1; i < len(err); i++ { + result += fmt.Sprintf(", %s", err[i].Error()) + } + result += "]" + return result +} diff --git a/vendor/github.com/onsi/gomega/gstruct/fields.go b/vendor/github.com/onsi/gomega/gstruct/fields.go new file mode 100644 index 00000000000..f3c1575511d --- /dev/null +++ b/vendor/github.com/onsi/gomega/gstruct/fields.go @@ -0,0 +1,141 @@ +package gstruct + +import ( + "errors" + "fmt" + "reflect" + "runtime/debug" + "strings" + + "github.com/onsi/gomega/format" + errorsutil "github.com/onsi/gomega/gstruct/errors" + "github.com/onsi/gomega/types" +) + +//MatchAllFields succeeds if every field of a struct matches the field matcher associated with +//it, and every element matcher is matched. +// Expect([]string{"a", "b"}).To(MatchAllFields(idFn, gstruct.Fields{ +// "a": BeEqual("a"), +// "b": BeEqual("b"), +// }) +func MatchAllFields(fields Fields) types.GomegaMatcher { + return &FieldsMatcher{ + Fields: fields, + } +} + +//MatchFields succeeds if each element of a struct matches the field matcher associated with +//it. It can ignore extra fields and/or missing fields. +// Expect([]string{"a", "c"}).To(MatchFields(idFn, IgnoreMissing|IgnoreExtra, gstruct.Fields{ +// "a": BeEqual("a") +// "b": BeEqual("b"), +// }) +func MatchFields(options Options, fields Fields) types.GomegaMatcher { + return &FieldsMatcher{ + Fields: fields, + IgnoreExtras: options&IgnoreExtras != 0, + IgnoreMissing: options&IgnoreMissing != 0, + } +} + +type FieldsMatcher struct { + // Matchers for each field. + Fields Fields + + // Whether to ignore extra elements or consider it an error. + IgnoreExtras bool + // Whether to ignore missing elements or consider it an error. + IgnoreMissing bool + + // State. + failures []error +} + +// Field name to matcher. +type Fields map[string]types.GomegaMatcher + +func (m *FieldsMatcher) Match(actual interface{}) (success bool, err error) { + if reflect.TypeOf(actual).Kind() != reflect.Struct { + return false, fmt.Errorf("%v is type %T, expected struct", actual, actual) + } + + m.failures = m.matchFields(actual) + if len(m.failures) > 0 { + return false, nil + } + return true, nil +} + +func (m *FieldsMatcher) matchFields(actual interface{}) (errs []error) { + val := reflect.ValueOf(actual) + typ := val.Type() + fields := map[string]bool{} + for i := 0; i < val.NumField(); i++ { + fieldName := typ.Field(i).Name + fields[fieldName] = true + + err := func() (err error) { + // This test relies heavily on reflect, which tends to panic. + // Recover here to provide more useful error messages in that case. + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic checking %+v: %v\n%s", actual, r, debug.Stack()) + } + }() + + matcher, expected := m.Fields[fieldName] + if !expected { + if !m.IgnoreExtras { + return fmt.Errorf("unexpected field %s: %+v", fieldName, actual) + } + return nil + } + + var field interface{} + if val.Field(i).IsValid() { + field = val.Field(i).Interface() + } else { + field = reflect.Zero(typ.Field(i).Type) + } + + match, err := matcher.Match(field) + if err != nil { + return err + } else if !match { + if nesting, ok := matcher.(errorsutil.NestingMatcher); ok { + return errorsutil.AggregateError(nesting.Failures()) + } + return errors.New(matcher.FailureMessage(field)) + } + return nil + }() + if err != nil { + errs = append(errs, errorsutil.Nest("."+fieldName, err)) + } + } + + for field := range m.Fields { + if !fields[field] && !m.IgnoreMissing { + errs = append(errs, fmt.Errorf("missing expected field %s", field)) + } + } + + return errs +} + +func (m *FieldsMatcher) FailureMessage(actual interface{}) (message string) { + failures := make([]string, len(m.failures)) + for i := range m.failures { + failures[i] = m.failures[i].Error() + } + return format.Message(reflect.TypeOf(actual).Name(), + fmt.Sprintf("to match fields: {\n%v\n}\n", strings.Join(failures, "\n"))) +} + +func (m *FieldsMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to match fields") +} + +func (m *FieldsMatcher) Failures() []error { + return m.failures +} diff --git a/vendor/github.com/onsi/gomega/gstruct/ignore.go b/vendor/github.com/onsi/gomega/gstruct/ignore.go new file mode 100644 index 00000000000..0365f32ad17 --- /dev/null +++ b/vendor/github.com/onsi/gomega/gstruct/ignore.go @@ -0,0 +1,37 @@ +package gstruct + +import ( + "github.com/onsi/gomega/types" +) + +//Ignore ignores the actual value and always succeeds. +// Expect(nil).To(Ignore()) +// Expect(true).To(Ignore()) +func Ignore() types.GomegaMatcher { + return &IgnoreMatcher{true} +} + +//Reject ignores the actual value and always fails. It can be used in conjunction with IgnoreMissing +//to catch problematic elements, or to verify tests are running. +// Expect(nil).NotTo(Reject()) +// Expect(true).NotTo(Reject()) +func Reject() types.GomegaMatcher { + return &IgnoreMatcher{false} +} + +// A matcher that either always succeeds or always fails. +type IgnoreMatcher struct { + Succeed bool +} + +func (m *IgnoreMatcher) Match(actual interface{}) (bool, error) { + return m.Succeed, nil +} + +func (m *IgnoreMatcher) FailureMessage(_ interface{}) (message string) { + return "Unconditional failure" +} + +func (m *IgnoreMatcher) NegatedFailureMessage(_ interface{}) (message string) { + return "Unconditional success" +} diff --git a/vendor/github.com/onsi/gomega/gstruct/pointer.go b/vendor/github.com/onsi/gomega/gstruct/pointer.go new file mode 100644 index 00000000000..0a2f35de312 --- /dev/null +++ b/vendor/github.com/onsi/gomega/gstruct/pointer.go @@ -0,0 +1,56 @@ +package gstruct + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/format" + "github.com/onsi/gomega/types" +) + +//PointTo applies the given matcher to the value pointed to by actual. It fails if the pointer is +//nil. +// actual := 5 +// Expect(&actual).To(PointTo(Equal(5))) +func PointTo(matcher types.GomegaMatcher) types.GomegaMatcher { + return &PointerMatcher{ + Matcher: matcher, + } +} + +type PointerMatcher struct { + Matcher types.GomegaMatcher + + // Failure message. + failure string +} + +func (m *PointerMatcher) Match(actual interface{}) (bool, error) { + val := reflect.ValueOf(actual) + + // return error if actual type is not a pointer + if val.Kind() != reflect.Ptr { + return false, fmt.Errorf("PointerMatcher expects a pointer but we have '%s'", val.Kind()) + } + + if !val.IsValid() || val.IsNil() { + m.failure = format.Message(actual, "not to be ") + return false, nil + } + + // Forward the value. + elem := val.Elem().Interface() + match, err := m.Matcher.Match(elem) + if !match { + m.failure = m.Matcher.FailureMessage(elem) + } + return match, err +} + +func (m *PointerMatcher) FailureMessage(_ interface{}) (message string) { + return m.failure +} + +func (m *PointerMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return m.Matcher.NegatedFailureMessage(actual) +} diff --git a/vendor/github.com/onsi/gomega/gstruct/types.go b/vendor/github.com/onsi/gomega/gstruct/types.go new file mode 100644 index 00000000000..0b7a1246b9e --- /dev/null +++ b/vendor/github.com/onsi/gomega/gstruct/types.go @@ -0,0 +1,11 @@ +package gstruct + +//Options is the type for options passed to some matchers. +type Options int + +const ( + //IgnoreExtras tells the matcher to ignore extra elements or fields, rather than triggering a failure. + IgnoreExtras Options = 1 << iota + //IgnoreMissing tells the matcher to ignore missing elements or fields, rather than triggering a failure. + IgnoreMissing +)