Merge pull request #106 from DirectXMan12/feature/resource-metrics

Resource Metrics API Support
This commit is contained in:
Solly Ross 2018-10-04 07:34:08 -07:00 committed by GitHub
commit 7a421bb91e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
281 changed files with 186573 additions and 7153 deletions

174
Gopkg.lock generated
View file

@ -254,6 +254,20 @@
revision = "20f1fb78b0740ba8c3cb143a61e86ba5c8669768" revision = "20f1fb78b0740ba8c3cb143a61e86ba5c8669768"
version = "v0.5.0" version = "v0.5.0"
[[projects]]
digest = "1:a1038ef593beb4771c8f0f9c26e8b00410acd800af5c6864651d9bf160ea1813"
name = "github.com/hpcloud/tail"
packages = [
".",
"ratelimiter",
"util",
"watch",
"winfile",
]
pruneopts = "UT"
revision = "a30252cb686a21eb2d0b98132633053ec2f7f1e5"
version = "v1.0.0"
[[projects]] [[projects]]
digest = "1:8eb1de8112c9924d59bf1d3e5c26f5eaa2bfc2a5fcbb92dc1c2e4546d695f277" digest = "1:8eb1de8112c9924d59bf1d3e5c26f5eaa2bfc2a5fcbb92dc1c2e4546d695f277"
name = "github.com/imdario/mergo" name = "github.com/imdario/mergo"
@ -279,20 +293,35 @@
version = "v1.1.5" version = "v1.1.5"
[[projects]] [[projects]]
digest = "1:46bdda4b93f3a1169365a10a9eef6cfba14fbb59a636f8562c6eba578afb48b8" digest = "1:cf71ca07e2ee5914c776d9eaec4a7f63f4ddb013d55e39ffd5689fe4cd2df861"
name = "github.com/kubernetes-incubator/custom-metrics-apiserver" name = "github.com/kubernetes-incubator/custom-metrics-apiserver"
packages = [ packages = [
"pkg/apiserver", "pkg/apiserver",
"pkg/apiserver/installer", "pkg/apiserver/installer",
"pkg/cmd",
"pkg/cmd/server", "pkg/cmd/server",
"pkg/dynamicmapper", "pkg/dynamicmapper",
"pkg/provider", "pkg/provider",
"pkg/provider/helpers",
"pkg/registry/custom_metrics", "pkg/registry/custom_metrics",
"pkg/registry/external_metrics", "pkg/registry/external_metrics",
] ]
pruneopts = "UT" pruneopts = "UT"
revision = "d8f23423aa1d0ff2bc9656da863d721725b3c68a" revision = "f54b0d6f31d8e0f0a2d7be372ddd837a2ef15d97"
version = "kubernetes-1.11.0-rc.1" version = "kubernetes-1.11.2"
[[projects]]
digest = "1:b15075296ef5f8edf0f101198deae645b12a81e89c54a132113e45172b9355b7"
name = "github.com/kubernetes-incubator/metrics-server"
packages = [
"pkg/apiserver/generic",
"pkg/provider",
"pkg/storage/nodemetrics",
"pkg/storage/podmetrics",
]
pruneopts = "UT"
revision = "6cd52610db69d662b3b7890b73df82b3b3601ab3"
version = "v0.3.1"
[[projects]] [[projects]]
branch = "master" branch = "master"
@ -330,6 +359,54 @@
revision = "4b7aa43c6742a2c18fdef89dd197aaae7dac7ccd" revision = "4b7aa43c6742a2c18fdef89dd197aaae7dac7ccd"
version = "1.0.1" version = "1.0.1"
[[projects]]
digest = "1:42e29deef12327a69123b9cb2cb45fee4af5c12c2a23c6e477338279a052703f"
name = "github.com/onsi/ginkgo"
packages = [
".",
"config",
"internal/codelocation",
"internal/containernode",
"internal/failer",
"internal/leafnodes",
"internal/remote",
"internal/spec",
"internal/spec_iterator",
"internal/specrunner",
"internal/suite",
"internal/testingtproxy",
"internal/writer",
"reporters",
"reporters/stenographer",
"reporters/stenographer/support/go-colorable",
"reporters/stenographer/support/go-isatty",
"types",
]
pruneopts = "UT"
revision = "3774a09d95489ccaa16032e0770d08ea77ba6184"
version = "v1.6.0"
[[projects]]
digest = "1:ab54eea8d482272009e9e4af07d4d9b5236c27b4d8c54a3f2c99d163be883eca"
name = "github.com/onsi/gomega"
packages = [
".",
"format",
"internal/assertion",
"internal/asyncassertion",
"internal/oraclematcher",
"internal/testingtsupport",
"matchers",
"matchers/support/goraph/bipartitegraph",
"matchers/support/goraph/edge",
"matchers/support/goraph/node",
"matchers/support/goraph/util",
"types",
]
pruneopts = "UT"
revision = "7615b9433f86a8bdf29709bf288bc4fd0636a369"
version = "v1.4.2"
[[projects]] [[projects]]
digest = "1:e5d0bd87abc2781d14e274807a470acd180f0499f8bf5bb18606e9ec22ad9de9" digest = "1:e5d0bd87abc2781d14e274807a470acd180f0499f8bf5bb18606e9ec22ad9de9"
name = "github.com/pborman/uuid" name = "github.com/pborman/uuid"
@ -354,14 +431,6 @@
revision = "5f041e8faa004a95c88a202771f4cc3e991971e6" revision = "5f041e8faa004a95c88a202771f4cc3e991971e6"
version = "v2.0.1" version = "v2.0.1"
[[projects]]
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
pruneopts = "UT"
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]] [[projects]]
digest = "1:b6221ec0f8903b556e127c449e7106b63e6867170c2d10a7c058623d086f2081" digest = "1:b6221ec0f8903b556e127c449e7106b63e6867170c2d10a7c058623d086f2081"
name = "github.com/prometheus/client_golang" name = "github.com/prometheus/client_golang"
@ -419,17 +488,6 @@
revision = "9a97c102cda95a86cec2345a6f09f55a939babf5" revision = "9a97c102cda95a86cec2345a6f09f55a939babf5"
version = "v1.0.2" version = "v1.0.2"
[[projects]]
digest = "1:c40d65817cdd41fac9aa7af8bed56927bb2d6d47e4fea566a74880f5c2b1c41e"
name = "github.com/stretchr/testify"
packages = [
"assert",
"require",
]
pruneopts = "UT"
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
version = "v1.2.2"
[[projects]] [[projects]]
digest = "1:03aa6e485e528acb119fb32901cf99582c380225fc7d5a02758e08b180cb56c3" digest = "1:03aa6e485e528acb119fb32901cf99582c380225fc7d5a02758e08b180cb56c3"
name = "github.com/ugorji/go" name = "github.com/ugorji/go"
@ -448,10 +506,13 @@
[[projects]] [[projects]]
branch = "master" branch = "master"
digest = "1:9812eb354c055268a95354e388d47021483712b842f32ebd4cbceb144cb18488" digest = "1:88058f781f1921d55d36bf6cf3ddafb894cf67031b52b805ad9838aa52d3419f"
name = "golang.org/x/net" name = "golang.org/x/net"
packages = [ packages = [
"context", "context",
"html",
"html/atom",
"html/charset",
"http/httpguts", "http/httpguts",
"http2", "http2",
"http2/hpack", "http2/hpack",
@ -475,17 +536,29 @@
revision = "90868a75fefd03942536221d7c0e2f84ec62a668" revision = "90868a75fefd03942536221d7c0e2f84ec62a668"
[[projects]] [[projects]]
digest = "1:0c56024909189aee3364b7f21a95a27459f718aa7c199a5c111c36cfffd9eaef" digest = "1:bb8277a2ca2bcad6ff7f413b939375924099be908cedd1314baa21ecd08df477"
name = "golang.org/x/text" name = "golang.org/x/text"
packages = [ packages = [
"collate", "collate",
"collate/build", "collate/build",
"encoding",
"encoding/charmap",
"encoding/htmlindex",
"encoding/internal",
"encoding/internal/identifier",
"encoding/japanese",
"encoding/korean",
"encoding/simplifiedchinese",
"encoding/traditionalchinese",
"encoding/unicode",
"internal/colltab", "internal/colltab",
"internal/gen", "internal/gen",
"internal/tag", "internal/tag",
"internal/triegen", "internal/triegen",
"internal/ucd", "internal/ucd",
"internal/utf8internal",
"language", "language",
"runes",
"secure/bidirule", "secure/bidirule",
"transform", "transform",
"unicode/bidi", "unicode/bidi",
@ -550,6 +623,15 @@
revision = "8dea3dc473e90c8179e519d91302d0597c0ca1d1" revision = "8dea3dc473e90c8179e519d91302d0597c0ca1d1"
version = "v1.15.0" version = "v1.15.0"
[[projects]]
digest = "1:abeb38ade3f32a92943e5be54f55ed6d6e3b6602761d74b4aab4c9dd45c18abd"
name = "gopkg.in/fsnotify.v1"
packages = ["."]
pruneopts = "UT"
revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9"
source = "https://github.com/fsnotify/fsnotify.git"
version = "v1.4.7"
[[projects]] [[projects]]
digest = "1:2d1fbdc6777e5408cabeb02bf336305e724b925ff4546ded0fa8715a7267922a" digest = "1:2d1fbdc6777e5408cabeb02bf336305e724b925ff4546ded0fa8715a7267922a"
name = "gopkg.in/inf.v0" name = "gopkg.in/inf.v0"
@ -566,6 +648,14 @@
revision = "a96e63847dc3c67d17befa69c303767e2f84e54f" revision = "a96e63847dc3c67d17befa69c303767e2f84e54f"
version = "v2.1" version = "v2.1"
[[projects]]
branch = "v1"
digest = "1:0caa92e17bc0b65a98c63e5bc76a9e844cd5e56493f8fdbb28fad101a16254d9"
name = "gopkg.in/tomb.v1"
packages = ["."]
pruneopts = "UT"
revision = "dd632973f1e7218eb1089048e0798ec9ae7dceb8"
[[projects]] [[projects]]
digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202" digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202"
name = "gopkg.in/yaml.v2" name = "gopkg.in/yaml.v2"
@ -575,6 +665,7 @@
version = "v2.2.1" version = "v2.2.1"
[[projects]] [[projects]]
branch = "release-1.11"
digest = "1:13f16734c26d843bc2accbc21dd49be71de99ad5fa0176726cdada80cc6859cb" digest = "1:13f16734c26d843bc2accbc21dd49be71de99ad5fa0176726cdada80cc6859cb"
name = "k8s.io/api" name = "k8s.io/api"
packages = [ packages = [
@ -611,9 +702,9 @@
] ]
pruneopts = "UT" pruneopts = "UT"
revision = "4e7be11eab3ffcfc1876898b8272df53785a9504" revision = "4e7be11eab3ffcfc1876898b8272df53785a9504"
version = "kubernetes-1.11.3"
[[projects]] [[projects]]
branch = "release-1.11"
digest = "1:3ac9d135ccf71b822fc64e13180a69a2a7df35c77327f731739619c41e8237a3" digest = "1:3ac9d135ccf71b822fc64e13180a69a2a7df35c77327f731739619c41e8237a3"
name = "k8s.io/apimachinery" name = "k8s.io/apimachinery"
packages = [ packages = [
@ -668,9 +759,9 @@
] ]
pruneopts = "UT" pruneopts = "UT"
revision = "def12e63c512da17043b4f0293f52d1006603d9f" revision = "def12e63c512da17043b4f0293f52d1006603d9f"
version = "kubernetes-1.11.3"
[[projects]] [[projects]]
branch = "release-1.11"
digest = "1:3d66c1286b541bc8be16799c7d55dc058197f984b3378d712a1f3c9d8323a439" digest = "1:3d66c1286b541bc8be16799c7d55dc058197f984b3378d712a1f3c9d8323a439"
name = "k8s.io/apiserver" name = "k8s.io/apiserver"
packages = [ packages = [
@ -765,10 +856,10 @@
"plugin/pkg/authorizer/webhook", "plugin/pkg/authorizer/webhook",
] ]
pruneopts = "UT" pruneopts = "UT"
revision = "386115dd78fde3efc2366cb381420a4dd0157348" revision = "d296c96c12b7d15d7fb5fea7a05fb165f8fd4014"
version = "kubernetes-1.11.3"
[[projects]] [[projects]]
branch = "release-8.0"
digest = "1:e0cfa2c6dd6483c1d9de73f972e1651c3b06591fb9ca39d2fac87e123693f005" digest = "1:e0cfa2c6dd6483c1d9de73f972e1651c3b06591fb9ca39d2fac87e123693f005"
name = "k8s.io/client-go" name = "k8s.io/client-go"
packages = [ packages = [
@ -900,8 +991,7 @@
"util/retry", "util/retry",
] ]
pruneopts = "UT" pruneopts = "UT"
revision = "2cefa64ff137e128daeddbd1775cd775708a05bf" revision = "f2f85107cac6fe04c30435ca0ac0c3318fd1b94c"
version = "kubernetes-1.11.3"
[[projects]] [[projects]]
branch = "master" branch = "master"
@ -918,7 +1008,8 @@
revision = "e3762e86a74c878ffed47484592986685639c2cd" revision = "e3762e86a74c878ffed47484592986685639c2cd"
[[projects]] [[projects]]
digest = "1:1663f8dc974bcb231bc7743999e60cd7a131844f11ad76561a2b0747f6060c9b" branch = "release-1.11"
digest = "1:593a35190e3fa5e35b749479345fe61adf1b4433150cae243401394c820b2095"
name = "k8s.io/metrics" name = "k8s.io/metrics"
packages = [ packages = [
"pkg/apis/custom_metrics", "pkg/apis/custom_metrics",
@ -927,24 +1018,28 @@
"pkg/apis/external_metrics", "pkg/apis/external_metrics",
"pkg/apis/external_metrics/install", "pkg/apis/external_metrics/install",
"pkg/apis/external_metrics/v1beta1", "pkg/apis/external_metrics/v1beta1",
"pkg/apis/metrics",
"pkg/apis/metrics/install",
"pkg/apis/metrics/v1beta1",
] ]
pruneopts = "UT" pruneopts = "UT"
revision = "9aa1ab0459f06be9d4f59b83c50a9a17a1358806" revision = "972ef826b8401c180b89cefc7457daa2d116daa9"
version = "kubernetes-1.11.3"
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
input-imports = [ input-imports = [
"github.com/golang/glog", "github.com/golang/glog",
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/cmd/server", "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/cmd",
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/dynamicmapper",
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider", "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider",
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider/helpers",
"github.com/kubernetes-incubator/metrics-server/pkg/apiserver/generic",
"github.com/kubernetes-incubator/metrics-server/pkg/provider",
"github.com/onsi/ginkgo",
"github.com/onsi/gomega",
"github.com/prometheus/client_golang/prometheus", "github.com/prometheus/client_golang/prometheus",
"github.com/prometheus/common/model", "github.com/prometheus/common/model",
"github.com/spf13/cobra", "github.com/spf13/cobra",
"github.com/stretchr/testify/assert",
"github.com/stretchr/testify/require",
"gopkg.in/yaml.v2", "gopkg.in/yaml.v2",
"k8s.io/api/core/v1", "k8s.io/api/core/v1",
"k8s.io/api/extensions/v1beta1", "k8s.io/api/extensions/v1beta1",
@ -952,19 +1047,18 @@
"k8s.io/apimachinery/pkg/api/meta", "k8s.io/apimachinery/pkg/api/meta",
"k8s.io/apimachinery/pkg/api/resource", "k8s.io/apimachinery/pkg/api/resource",
"k8s.io/apimachinery/pkg/apis/meta/v1", "k8s.io/apimachinery/pkg/apis/meta/v1",
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured",
"k8s.io/apimachinery/pkg/labels", "k8s.io/apimachinery/pkg/labels",
"k8s.io/apimachinery/pkg/runtime",
"k8s.io/apimachinery/pkg/runtime/schema", "k8s.io/apimachinery/pkg/runtime/schema",
"k8s.io/apimachinery/pkg/types",
"k8s.io/apimachinery/pkg/util/runtime", "k8s.io/apimachinery/pkg/util/runtime",
"k8s.io/apimachinery/pkg/util/wait", "k8s.io/apimachinery/pkg/util/wait",
"k8s.io/apiserver/pkg/util/logs", "k8s.io/apiserver/pkg/util/logs",
"k8s.io/client-go/discovery",
"k8s.io/client-go/dynamic", "k8s.io/client-go/dynamic",
"k8s.io/client-go/dynamic/fake", "k8s.io/client-go/dynamic/fake",
"k8s.io/client-go/rest", "k8s.io/client-go/rest",
"k8s.io/client-go/tools/clientcmd", "k8s.io/client-go/tools/clientcmd",
"k8s.io/metrics/pkg/apis/custom_metrics", "k8s.io/metrics/pkg/apis/custom_metrics",
"k8s.io/metrics/pkg/apis/metrics",
] ]
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

View file

@ -48,34 +48,53 @@
# Kubernetes incubator deps # Kubernetes incubator deps
[[constraint]] [[constraint]]
version = "kubernetes-1.11.0-rc.1"
name = "github.com/kubernetes-incubator/custom-metrics-apiserver" name = "github.com/kubernetes-incubator/custom-metrics-apiserver"
version = "kubernetes-1.11.2"
[[constraint]]
name = "github.com/kubernetes-incubator/metrics-server"
version = "v0.3.1"
# Core Kubernetes deps # Core Kubernetes deps
[[constraint]] [[constraint]]
name = "k8s.io/api" name = "k8s.io/api"
version = "kubernetes-1.11.3" branch = "release-1.11"
[[constraint]] [[constraint]]
name = "k8s.io/apimachinery" name = "k8s.io/apimachinery"
version = "kubernetes-1.11.3" branch = "release-1.11"
[[constraint]] [[constraint]]
name = "k8s.io/apiserver" name = "k8s.io/apiserver"
version = "kubernetes-1.11.3" branch = "release-1.11"
[[constraint]] [[constraint]]
name = "k8s.io/client-go" name = "k8s.io/client-go"
version = "kubernetes-1.11.3" branch = "release-8.0"
[[constraint]] [[constraint]]
name = "k8s.io/metrics" name = "k8s.io/metrics"
version = "kubernetes-1.11.3" branch = "release-1.11"
# messed up kubernetes dep
[[override]]
name = "github.com/json-iterator/go"
version = "1.1.5"
# Test deps # Test deps
[[constraint]] [[constraint]]
name = "github.com/stretchr/testify" name = "github.com/onsi/ginkgo"
version = "1.2.2" version = "1.6.0"
[[constraint]]
name = "github.com/onsi/gomega"
version = "1.4.2"
# For dependency below: Refer to issue https://github.com/golang/dep/issues/1799
[[override]]
name = "gopkg.in/fsnotify.v1"
source = "https://github.com/fsnotify/fsnotify.git"
version="v1.4.7"
[prune] [prune]
go-tests = true go-tests = true

View file

@ -18,26 +18,231 @@ package main
import ( import (
"flag" "flag"
"fmt"
"net/http"
"net/url"
"os" "os"
"runtime" "time"
"github.com/golang/glog"
basecmd "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/cmd"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider"
resmetrics "github.com/kubernetes-incubator/metrics-server/pkg/apiserver/generic"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/util/logs" "k8s.io/apiserver/pkg/util/logs"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"github.com/directxman12/k8s-prometheus-adapter/cmd/adapter/app" prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client"
mprom "github.com/directxman12/k8s-prometheus-adapter/pkg/client/metrics"
adaptercfg "github.com/directxman12/k8s-prometheus-adapter/pkg/config"
cmprov "github.com/directxman12/k8s-prometheus-adapter/pkg/custom-provider"
resprov "github.com/directxman12/k8s-prometheus-adapter/pkg/resourceprovider"
) )
type PrometheusAdapter struct {
basecmd.AdapterBase
// PrometheusURL is the URL describing how to connect to Prometheus. Query parameters configure connection options.
PrometheusURL string
// PrometheusAuthInCluster enables using the auth details from the in-cluster kubeconfig to connect to Prometheus
PrometheusAuthInCluster bool
// PrometheusAuthConf is the kubeconfig file that contains auth details used to connect to Prometheus
PrometheusAuthConf string
// AdapterConfigFile points to the file containing the metrics discovery configuration.
AdapterConfigFile string
// MetricsRelistInterval is the interval at which to relist the set of available metrics
MetricsRelistInterval time.Duration
metricsConfig *adaptercfg.MetricsDiscoveryConfig
}
func (cmd *PrometheusAdapter) makePromClient() (prom.Client, error) {
baseURL, err := url.Parse(cmd.PrometheusURL)
if err != nil {
return nil, fmt.Errorf("invalid Prometheus URL %q: %v", baseURL, err)
}
promHTTPClient, err := makeHTTPClient(cmd.PrometheusAuthInCluster, cmd.PrometheusAuthConf)
if err != nil {
return nil, err
}
genericPromClient := prom.NewGenericAPIClient(promHTTPClient, baseURL)
instrumentedGenericPromClient := mprom.InstrumentGenericAPIClient(genericPromClient, baseURL.String())
return prom.NewClientForAPI(instrumentedGenericPromClient), nil
}
func (cmd *PrometheusAdapter) addFlags() {
cmd.Flags().StringVar(&cmd.PrometheusURL, "prometheus-url", cmd.PrometheusURL,
"URL for connecting to Prometheus.")
cmd.Flags().BoolVar(&cmd.PrometheusAuthInCluster, "prometheus-auth-incluster", cmd.PrometheusAuthInCluster,
"use auth details from the in-cluster kubeconfig when connecting to prometheus.")
cmd.Flags().StringVar(&cmd.PrometheusAuthConf, "prometheus-auth-config", cmd.PrometheusAuthConf,
"kubeconfig file used to configure auth when connecting to Prometheus.")
cmd.Flags().StringVar(&cmd.AdapterConfigFile, "config", cmd.AdapterConfigFile,
"Configuration file containing details of how to transform between Prometheus metrics "+
"and custom metrics API resources")
cmd.Flags().DurationVar(&cmd.MetricsRelistInterval, "metrics-relist-interval", cmd.MetricsRelistInterval, ""+
"interval at which to re-list the set of all available metrics from Prometheus")
}
func (cmd *PrometheusAdapter) loadConfig() error {
// load metrics discovery configuration
if cmd.AdapterConfigFile == "" {
return fmt.Errorf("no metrics discovery configuration file specified (make sure to use --config)")
}
metricsConfig, err := adaptercfg.FromFile(cmd.AdapterConfigFile)
if err != nil {
return fmt.Errorf("unable to load metrics discovery configuration: %v", err)
}
cmd.metricsConfig = metricsConfig
return nil
}
func (cmd *PrometheusAdapter) makeProvider(promClient prom.Client, stopCh <-chan struct{}) (provider.CustomMetricsProvider, error) {
if len(cmd.metricsConfig.Rules) == 0 {
return nil, nil
}
// grab the mapper and dynamic client
mapper, err := cmd.RESTMapper()
if err != nil {
return nil, fmt.Errorf("unable to construct RESTMapper: %v", err)
}
dynClient, err := cmd.DynamicClient()
if err != nil {
return nil, fmt.Errorf("unable to construct Kubernetes client: %v", err)
}
// extract the namers
namers, err := cmprov.NamersFromConfig(cmd.metricsConfig, mapper)
if err != nil {
return nil, fmt.Errorf("unable to construct naming scheme from metrics rules: %v", err)
}
// construct the provider and start it
cmProvider, runner := cmprov.NewPrometheusProvider(mapper, dynClient, promClient, namers, cmd.MetricsRelistInterval)
runner.RunUntil(stopCh)
return cmProvider, nil
}
func (cmd *PrometheusAdapter) addResourceMetricsAPI(promClient prom.Client) error {
if cmd.metricsConfig.ResourceRules == nil {
// bail if we don't have rules for setting things up
return nil
}
mapper, err := cmd.RESTMapper()
if err != nil {
return err
}
provider, err := resprov.NewProvider(promClient, mapper, cmd.metricsConfig.ResourceRules)
if err != nil {
return fmt.Errorf("unable to construct resource metrics API provider: %v", err)
}
provCfg := &resmetrics.ProviderConfig{
Node: provider,
Pod: provider,
}
informers, err := cmd.Informers()
if err != nil {
return err
}
server, err := cmd.Server()
if err != nil {
return err
}
if err := resmetrics.InstallStorage(provCfg, informers.Core().V1(), server.GenericAPIServer); err != nil {
return err
}
return nil
}
func main() { func main() {
logs.InitLogs() logs.InitLogs()
defer logs.FlushLogs() defer logs.FlushLogs()
if len(os.Getenv("GOMAXPROCS")) == 0 { // set up flags
runtime.GOMAXPROCS(runtime.NumCPU()) cmd := &PrometheusAdapter{
PrometheusURL: "https://localhost",
MetricsRelistInterval: 10 * time.Minute,
}
cmd.Name = "prometheus-metrics-adapter"
cmd.addFlags()
cmd.Flags().AddGoFlagSet(flag.CommandLine) // make sure we get the glog flags
cmd.Flags().Parse(os.Args)
// make the prometheus client
promClient, err := cmd.makePromClient()
if err != nil {
glog.Fatalf("unable to construct Prometheus client: %v", err)
} }
cmd := app.NewCommandStartPrometheusAdapterServer(os.Stdout, os.Stderr, wait.NeverStop) // load the config
cmd.Flags().AddGoFlagSet(flag.CommandLine) if err := cmd.loadConfig(); err != nil {
if err := cmd.Execute(); err != nil { glog.Fatalf("unable to load metrics discovery config: %v", err)
panic(err) }
// construct the provider
cmProvider, err := cmd.makeProvider(promClient, wait.NeverStop)
if err != nil {
glog.Fatalf("unable to construct custom metrics provider: %v", err)
}
// attach the provider to the server, if it's needed
if cmProvider != nil {
cmd.WithCustomMetrics(cmProvider)
}
// attach resource metrics support, if it's needed
if err := cmd.addResourceMetricsAPI(promClient); err != nil {
glog.Fatalf("unable to install resource metrics API: %v", err)
}
// run the server
if err := cmd.Run(wait.NeverStop); err != nil {
glog.Fatalf("unable to run custom metrics adapter: %v", err)
} }
} }
// makeHTTPClient constructs an HTTP for connecting with the given auth options.
func makeHTTPClient(inClusterAuth bool, kubeConfigPath string) (*http.Client, error) {
// make sure we're not trying to use two different sources of auth
if inClusterAuth && kubeConfigPath != "" {
return nil, fmt.Errorf("may not use both in-cluster auth and an explicit kubeconfig at the same time")
}
// return the default client if we're using no auth
if !inClusterAuth && kubeConfigPath == "" {
return http.DefaultClient, nil
}
var authConf *rest.Config
if kubeConfigPath != "" {
var err error
loadingRules := &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeConfigPath}
loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{})
authConf, err = loader.ClientConfig()
if err != nil {
return nil, fmt.Errorf("unable to construct auth configuration from %q for connecting to Prometheus: %v", kubeConfigPath, err)
}
} else {
var err error
authConf, err = rest.InClusterConfig()
if err != nil {
return nil, fmt.Errorf("unable to construct in-cluster auth configuration for connecting to Prometheus: %v", err)
}
}
tr, err := rest.TransportFor(authConf)
if err != nil {
return nil, fmt.Errorf("unable to construct client transport for connecting to Prometheus: %v", err)
}
return &http.Client{Transport: tr}, nil
}

View file

@ -1,219 +0,0 @@
/*
Copyright 2017 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 app
import (
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/spf13/cobra"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client"
mprom "github.com/directxman12/k8s-prometheus-adapter/pkg/client/metrics"
adaptercfg "github.com/directxman12/k8s-prometheus-adapter/pkg/config"
cmprov "github.com/directxman12/k8s-prometheus-adapter/pkg/custom-provider"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/cmd/server"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/dynamicmapper"
)
// NewCommandStartPrometheusAdapterServer provides a CLI handler for 'start master' command
func NewCommandStartPrometheusAdapterServer(out, errOut io.Writer, stopCh <-chan struct{}) *cobra.Command {
baseOpts := server.NewCustomMetricsAdapterServerOptions(out, errOut)
o := PrometheusAdapterServerOptions{
CustomMetricsAdapterServerOptions: baseOpts,
MetricsRelistInterval: 10 * time.Minute,
PrometheusURL: "https://localhost",
}
cmd := &cobra.Command{
Short: "Launch the custom metrics API adapter server",
Long: "Launch the custom metrics API adapter server",
RunE: func(c *cobra.Command, args []string) error {
if err := o.Complete(); err != nil {
return err
}
if err := o.Validate(args); err != nil {
return err
}
if err := o.RunCustomMetricsAdapterServer(stopCh); err != nil {
return err
}
return nil
},
}
flags := cmd.Flags()
o.SecureServing.AddFlags(flags)
o.Authentication.AddFlags(flags)
o.Authorization.AddFlags(flags)
o.Features.AddFlags(flags)
flags.StringVar(&o.RemoteKubeConfigFile, "lister-kubeconfig", o.RemoteKubeConfigFile, ""+
"kubeconfig file pointing at the 'core' kubernetes server with enough rights to list "+
"any described objets")
flags.DurationVar(&o.MetricsRelistInterval, "metrics-relist-interval", o.MetricsRelistInterval, ""+
"interval at which to re-list the set of all available metrics from Prometheus")
flags.DurationVar(&o.DiscoveryInterval, "discovery-interval", o.DiscoveryInterval, ""+
"interval at which to refresh API discovery information")
flags.StringVar(&o.PrometheusURL, "prometheus-url", o.PrometheusURL,
"URL for connecting to Prometheus.")
flags.BoolVar(&o.PrometheusAuthInCluster, "prometheus-auth-incluster", o.PrometheusAuthInCluster,
"use auth details from the in-cluster kubeconfig when connecting to prometheus.")
flags.StringVar(&o.PrometheusAuthConf, "prometheus-auth-config", o.PrometheusAuthConf,
"kubeconfig file used to configure auth when connecting to Prometheus.")
flags.StringVar(&o.AdapterConfigFile, "config", o.AdapterConfigFile,
"Configuration file containing details of how to transform between Prometheus metrics "+
"and custom metrics API resources")
cmd.MarkFlagRequired("config")
return cmd
}
// makeHTTPClient constructs an HTTP for connecting with the given auth options.
func makeHTTPClient(inClusterAuth bool, kubeConfigPath string) (*http.Client, error) {
// make sure we're not trying to use two different sources of auth
if inClusterAuth && kubeConfigPath != "" {
return nil, fmt.Errorf("may not use both in-cluster auth and an explicit kubeconfig at the same time")
}
// return the default client if we're using no auth
if !inClusterAuth && kubeConfigPath == "" {
return http.DefaultClient, nil
}
var authConf *rest.Config
if kubeConfigPath != "" {
var err error
loadingRules := &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeConfigPath}
loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{})
authConf, err = loader.ClientConfig()
if err != nil {
return nil, fmt.Errorf("unable to construct auth configuration from %q for connecting to Prometheus: %v", kubeConfigPath, err)
}
} else {
var err error
authConf, err = rest.InClusterConfig()
if err != nil {
return nil, fmt.Errorf("unable to construct in-cluster auth configuration for connecting to Prometheus: %v", err)
}
}
tr, err := rest.TransportFor(authConf)
if err != nil {
return nil, fmt.Errorf("unable to construct client transport for connecting to Prometheus: %v", err)
}
return &http.Client{Transport: tr}, nil
}
func (o PrometheusAdapterServerOptions) RunCustomMetricsAdapterServer(stopCh <-chan struct{}) error {
if o.AdapterConfigFile == "" {
return fmt.Errorf("no discovery configuration file specified")
}
metricsConfig, err := adaptercfg.FromFile(o.AdapterConfigFile)
if err != nil {
return fmt.Errorf("unable to load metrics discovery configuration: %v", err)
}
config, err := o.Config()
if err != nil {
return err
}
config.GenericConfig.EnableMetrics = true
var clientConfig *rest.Config
if len(o.RemoteKubeConfigFile) > 0 {
loadingRules := &clientcmd.ClientConfigLoadingRules{ExplicitPath: o.RemoteKubeConfigFile}
loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{})
clientConfig, err = loader.ClientConfig()
} else {
clientConfig, err = rest.InClusterConfig()
}
if err != nil {
return fmt.Errorf("unable to construct lister client config to initialize provider: %v", err)
}
discoveryClient, err := discovery.NewDiscoveryClientForConfig(clientConfig)
if err != nil {
return fmt.Errorf("unable to construct discovery client for dynamic client: %v", err)
}
dynamicMapper, err := dynamicmapper.NewRESTMapper(discoveryClient, o.DiscoveryInterval)
if err != nil {
return fmt.Errorf("unable to construct dynamic discovery mapper: %v", err)
}
dynamicClient, err := dynamic.NewForConfig(clientConfig)
if err != nil {
return fmt.Errorf("unable to construct lister client to initialize provider: %v", err)
}
// TODO: actually configure this client (strip query vars, etc)
baseURL, err := url.Parse(o.PrometheusURL)
if err != nil {
return fmt.Errorf("invalid Prometheus URL %q: %v", baseURL, err)
}
promHTTPClient, err := makeHTTPClient(o.PrometheusAuthInCluster, o.PrometheusAuthConf)
if err != nil {
return err
}
genericPromClient := prom.NewGenericAPIClient(promHTTPClient, baseURL)
instrumentedGenericPromClient := mprom.InstrumentGenericAPIClient(genericPromClient, baseURL.String())
promClient := prom.NewClientForAPI(instrumentedGenericPromClient)
namers, err := cmprov.NamersFromConfig(metricsConfig, dynamicMapper)
if err != nil {
return fmt.Errorf("unable to construct naming scheme from metrics rules: %v", err)
}
cmProvider, runner := cmprov.NewPrometheusProvider(dynamicMapper, dynamicClient, promClient, namers, o.MetricsRelistInterval)
runner.RunUntil(stopCh)
server, err := config.Complete().New("prometheus-custom-metrics-adapter", cmProvider, nil)
if err != nil {
return err
}
return server.GenericAPIServer.PrepareRun().Run(stopCh)
}
type PrometheusAdapterServerOptions struct {
*server.CustomMetricsAdapterServerOptions
// RemoteKubeConfigFile is the config used to list pods from the master API server
RemoteKubeConfigFile string
// MetricsRelistInterval is the interval at which to relist the set of available metrics
MetricsRelistInterval time.Duration
// DiscoveryInterval is the interval at which discovery information is refreshed
DiscoveryInterval time.Duration
// PrometheusURL is the URL describing how to connect to Prometheus. Query parameters configure connection options.
PrometheusURL string
// PrometheusAuthInCluster enables using the auth details from the in-cluster kubeconfig to connect to Prometheus
PrometheusAuthInCluster bool
// PrometheusAuthConf is the kubeconfig file that contains auth details used to connect to Prometheus
PrometheusAuthConf string
// AdapterConfigFile points to the file containing the metrics discovery configuration.
AdapterConfigFile string
}

View file

@ -89,5 +89,33 @@ func DefaultConfig(rateInterval time.Duration, labelPrefix string) *MetricsDisco
MetricsQuery: fmt.Sprintf("sum(rate(<<.Series>>{<<.LabelMatchers>>}[%s])) by (<<.GroupBy>>)", pmodel.Duration(rateInterval).String()), MetricsQuery: fmt.Sprintf("sum(rate(<<.Series>>{<<.LabelMatchers>>}[%s])) by (<<.GroupBy>>)", pmodel.Duration(rateInterval).String()),
}, },
}, },
ResourceRules: &ResourceRules{
CPU: ResourceRule{
ContainerQuery: fmt.Sprintf("sum(rate(container_cpu_usage_seconds_total{<<.LabelMatchers>>}[%s])) by (<<.GroupBy>>)", pmodel.Duration(rateInterval).String()),
NodeQuery: fmt.Sprintf("sum(rate(container_cpu_usage_seconds_total{<<.LabelMatchers>>, id='/'}[%s])) by (<<.GroupBy>>)", pmodel.Duration(rateInterval).String()),
Resources: ResourceMapping{
Overrides: map[string]GroupResource{
"namespace": {Resource: "namespace"},
"pod_name": {Resource: "pod"},
"instance": {Resource: "node"},
},
},
ContainerLabel: fmt.Sprintf("%scontainer_name", labelPrefix),
},
Memory: ResourceRule{
ContainerQuery: "sum(container_memory_working_set_bytes{<<.LabelMatchers>>}) by (<<.GroupBy>>)",
NodeQuery: "sum(container_memory_working_set_bytes{<<.LabelMatchers>>,id='/'}) by (<<.GroupBy>>)",
Resources: ResourceMapping{
Overrides: map[string]GroupResource{
"namespace": {Resource: "namespace"},
"pod_name": {Resource: "pod"},
"instance": {Resource: "node"},
},
},
ContainerLabel: fmt.Sprintf("%scontainer_name", labelPrefix),
},
Window: pmodel.Duration(rateInterval),
},
} }
} }

View file

@ -17,8 +17,7 @@ data:
name: name:
matches: ^container_(.*)_seconds_total$ matches: ^container_(.*)_seconds_total$
as: "" as: ""
metricsQuery: sum(rate(<<.Series>>{<<.LabelMatchers>>,container_name!="POD"}[5m])) metricsQuery: sum(rate(<<.Series>>{<<.LabelMatchers>>,container_name!="POD"}[1m])) by (<<.GroupBy>>)
by (<<.GroupBy>>)
- seriesQuery: '{__name__=~"^container_.*",container_name!="POD",namespace!="",pod_name!=""}' - seriesQuery: '{__name__=~"^container_.*",container_name!="POD",namespace!="",pod_name!=""}'
seriesFilters: seriesFilters:
- isNot: ^container_.*_seconds_total$ - isNot: ^container_.*_seconds_total$
@ -31,8 +30,7 @@ data:
name: name:
matches: ^container_(.*)_total$ matches: ^container_(.*)_total$
as: "" as: ""
metricsQuery: sum(rate(<<.Series>>{<<.LabelMatchers>>,container_name!="POD"}[5m])) metricsQuery: sum(rate(<<.Series>>{<<.LabelMatchers>>,container_name!="POD"}[1m])) by (<<.GroupBy>>)
by (<<.GroupBy>>)
- seriesQuery: '{__name__=~"^container_.*",container_name!="POD",namespace!="",pod_name!=""}' - seriesQuery: '{__name__=~"^container_.*",container_name!="POD",namespace!="",pod_name!=""}'
seriesFilters: seriesFilters:
- isNot: ^container_.*_total$ - isNot: ^container_.*_total$
@ -63,7 +61,7 @@ data:
name: name:
matches: ^(.*)_total$ matches: ^(.*)_total$
as: "" as: ""
metricsQuery: sum(rate(<<.Series>>{<<.LabelMatchers>>}[5m])) by (<<.GroupBy>>) metricsQuery: sum(rate(<<.Series>>{<<.LabelMatchers>>}[1m])) by (<<.GroupBy>>)
- seriesQuery: '{namespace!="",__name__!~"^container_.*"}' - seriesQuery: '{namespace!="",__name__!~"^container_.*"}'
seriesFilters: [] seriesFilters: []
resources: resources:
@ -71,4 +69,30 @@ data:
name: name:
matches: ^(.*)_seconds_total$ matches: ^(.*)_seconds_total$
as: "" as: ""
metricsQuery: sum(rate(<<.Series>>{<<.LabelMatchers>>}[5m])) by (<<.GroupBy>>) metricsQuery: sum(rate(<<.Series>>{<<.LabelMatchers>>}[1m])) by (<<.GroupBy>>)
resourceRules:
cpu:
containerQuery: sum(rate(container_cpu_usage_seconds_total{<<.LabelMatchers>>}[1m])) by (<<.GroupBy>>)
nodeQuery: sum(rate(container_cpu_usage_seconds_total{<<.LabelMatchers>>, id='/'}[1m])) by (<<.GroupBy>>)
resources:
overrides:
instance:
resource: node
namespace:
resource: namespace
pod_name:
resource: pod
containerLabel: container_name
memory:
containerQuery: sum(container_memory_working_set_bytes{<<.LabelMatchers>>}) by (<<.GroupBy>>)
nodeQuery: sum(container_memory_working_set_bytes{<<.LabelMatchers>>,id='/'}) by (<<.GroupBy>>)
resources:
overrides:
instance:
resource: node
namespace:
resource: namespace
pod_name:
resource: pod
containerLabel: container_name
window: 1m

77
pkg/client/fake/client.go Normal file
View file

@ -0,0 +1,77 @@
/*
Copyright 2018 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 fake
import (
"context"
"fmt"
prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client"
pmodel "github.com/prometheus/common/model"
)
// FakePrometheusClient is a fake instance of prom.Client
type FakePrometheusClient struct {
// AcceptableInterval is the interval in which to return queries
AcceptableInterval pmodel.Interval
// ErrQueries are queries that result in an error (whether from Query or Series)
ErrQueries map[prom.Selector]error
// Series are non-error responses to partial Series calls
SeriesResults map[prom.Selector][]prom.Series
// QueryResults are non-error responses to Query
QueryResults map[prom.Selector]prom.QueryResult
}
func (c *FakePrometheusClient) Series(_ context.Context, interval pmodel.Interval, selectors ...prom.Selector) ([]prom.Series, error) {
if (interval.Start != 0 && interval.Start < c.AcceptableInterval.Start) || (interval.End != 0 && interval.End > c.AcceptableInterval.End) {
return nil, fmt.Errorf("interval [%v, %v] for query is outside range [%v, %v]", interval.Start, interval.End, c.AcceptableInterval.Start, c.AcceptableInterval.End)
}
res := []prom.Series{}
for _, sel := range selectors {
if err, found := c.ErrQueries[sel]; found {
return nil, err
}
if series, found := c.SeriesResults[sel]; found {
res = append(res, series...)
}
}
return res, nil
}
func (c *FakePrometheusClient) Query(_ context.Context, t pmodel.Time, query prom.Selector) (prom.QueryResult, error) {
if t < c.AcceptableInterval.Start || t > c.AcceptableInterval.End {
return prom.QueryResult{}, fmt.Errorf("time %v for query is outside range [%v, %v]", t, c.AcceptableInterval.Start, c.AcceptableInterval.End)
}
if err, found := c.ErrQueries[query]; found {
return prom.QueryResult{}, err
}
if res, found := c.QueryResults[query]; found {
return res, nil
}
return prom.QueryResult{
Type: pmodel.ValVector,
Vector: &pmodel.Vector{},
}, nil
}
func (c *FakePrometheusClient) QueryRange(_ context.Context, r prom.Range, query prom.Selector) (prom.QueryResult, error) {
return prom.QueryResult{}, nil
}

View file

@ -1,11 +1,16 @@
package config package config
import (
pmodel "github.com/prometheus/common/model"
)
type MetricsDiscoveryConfig struct { type MetricsDiscoveryConfig struct {
// Rules specifies how to discover and map Prometheus metrics to // Rules specifies how to discover and map Prometheus metrics to
// custom metrics API resources. The rules are applied independently, // custom metrics API resources. The rules are applied independently,
// and thus must be mutually exclusive. Rules will the same SeriesQuery // and thus must be mutually exclusive. Rules will the same SeriesQuery
// will make only a single API call. // will make only a single API call.
Rules []DiscoveryRule `yaml:"rules"` Rules []DiscoveryRule `yaml:"rules"`
ResourceRules *ResourceRules `yaml:"resourceRules,omitempty"`
} }
// DiscoveryRule describes on set of rules for transforming Prometheus metrics to/from // DiscoveryRule describes on set of rules for transforming Prometheus metrics to/from
@ -73,3 +78,31 @@ type NameMapping struct {
// if only one is present, and will error if multiple are. // if only one is present, and will error if multiple are.
As string `yaml:"as"` As string `yaml:"as"`
} }
// ResourceRules describe the rules for querying resource metrics
// API results. It's assumed that the same metrics can be used
// to aggregate across different resources.
type ResourceRules struct {
CPU ResourceRule `yaml:"cpu"`
Memory ResourceRule `yaml:"memory"`
// Window is the window size reported by the resource metrics API. It should match the value used
// in your containerQuery and nodeQuery if you use a `rate` function.
Window pmodel.Duration `yaml:"window"`
}
// ResourceRule describes how to query metrics for some particular
// system resource metric.
type ResourceRule struct {
// Container is the query used to fetch the metrics for containers.
ContainerQuery string `yaml:"containerQuery"`
// NodeQuery is the query used to fetch the metrics for nodes
// (for instance, simply aggregating by node label is insufficient for
// cadvisor metrics -- you need to select the `/` container).
NodeQuery string `yaml:"nodeQuery"`
// Resources specifies how associated Kubernetes resources should be discovered for
// the given metrics.
Resources ResourceMapping `yaml:"resources"`
// ContainerLabel indicates the name of the Prometheus label containing the container name
// (since "container" is not a resource, this can't go in the `resources` block, but is similar).
ContainerLabel string `yaml:"containerLabel"`
}

View file

@ -1,21 +1,16 @@
package provider package provider
import ( import (
"bytes"
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
"sync"
"text/template"
"github.com/golang/glog"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider"
apimeta "k8s.io/apimachinery/pkg/api/meta" apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client" prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client"
"github.com/directxman12/k8s-prometheus-adapter/pkg/config" "github.com/directxman12/k8s-prometheus-adapter/pkg/config"
pmodel "github.com/prometheus/common/model" "github.com/directxman12/k8s-prometheus-adapter/pkg/naming"
) )
var nsGroupResource = schema.GroupResource{Resource: "namespaces"} var nsGroupResource = schema.GroupResource{Resource: "namespaces"}
@ -34,86 +29,13 @@ type MetricNamer interface {
// constrains beyond the series query. It's assumed that the series given // constrains beyond the series query. It's assumed that the series given
// already matche the series query. // already matche the series query.
FilterSeries(series []prom.Series) []prom.Series FilterSeries(series []prom.Series) []prom.Series
// ResourcesForSeries returns the group-resources associated with the given series,
// as well as whether or not the given series has the "namespace" resource).
ResourcesForSeries(series prom.Series) (res []schema.GroupResource, namespaced bool)
// LabelForResource returns the appropriate label for the given resource.
LabelForResource(resource schema.GroupResource) (pmodel.LabelName, error)
// MetricNameForSeries returns the name (as presented in the API) for a given series. // MetricNameForSeries returns the name (as presented in the API) for a given series.
MetricNameForSeries(series prom.Series) (string, error) MetricNameForSeries(series prom.Series) (string, error)
// QueryForSeries returns the query for a given series (not API metric name), with // QueryForSeries returns the query for a given series (not API metric name), with
// the given namespace name (if relevant), resource, and resource names. // the given namespace name (if relevant), resource, and resource names.
QueryForSeries(series string, resource schema.GroupResource, namespace string, names ...string) (prom.Selector, error) QueryForSeries(series string, resource schema.GroupResource, namespace string, names ...string) (prom.Selector, error)
}
// labelGroupResExtractor extracts schema.GroupResources from series labels. naming.ResourceConverter
type labelGroupResExtractor struct {
regex *regexp.Regexp
resourceInd int
groupInd *int
mapper apimeta.RESTMapper
}
// newLabelGroupResExtractor creates a new labelGroupResExtractor for labels whose form
// matches the given template. It does so by creating a regular expression from the template,
// so anything in the template which limits resource or group name length will cause issues.
func newLabelGroupResExtractor(labelTemplate *template.Template) (*labelGroupResExtractor, error) {
labelRegexBuff := new(bytes.Buffer)
if err := labelTemplate.Execute(labelRegexBuff, schema.GroupResource{"(?P<group>.+?)", "(?P<resource>.+?)"}); err != nil {
return nil, fmt.Errorf("unable to convert label template to matcher: %v", err)
}
if labelRegexBuff.Len() == 0 {
return nil, fmt.Errorf("unable to convert label template to matcher: empty template")
}
labelRegexRaw := "^" + labelRegexBuff.String() + "$"
labelRegex, err := regexp.Compile(labelRegexRaw)
if err != nil {
return nil, fmt.Errorf("unable to convert label template to matcher: %v", err)
}
var groupInd *int
var resInd *int
for i, name := range labelRegex.SubexpNames() {
switch name {
case "group":
ind := i // copy to avoid iteration variable reference
groupInd = &ind
case "resource":
ind := i // copy to avoid iteration variable reference
resInd = &ind
}
}
if resInd == nil {
return nil, fmt.Errorf("must include at least `{{.Resource}}` in the label template")
}
return &labelGroupResExtractor{
regex: labelRegex,
resourceInd: *resInd,
groupInd: groupInd,
}, nil
}
// GroupResourceForLabel extracts a schema.GroupResource from the given label, if possible.
// The second argument indicates whether or not a potential group-resource was found in this label.
func (e *labelGroupResExtractor) GroupResourceForLabel(lbl pmodel.LabelName) (schema.GroupResource, bool) {
matchGroups := e.regex.FindStringSubmatch(string(lbl))
if matchGroups != nil {
group := ""
if e.groupInd != nil {
group = matchGroups[*e.groupInd]
}
return schema.GroupResource{
Group: group,
Resource: matchGroups[e.resourceInd],
}, true
}
return schema.GroupResource{}, false
} }
func (r *metricNamer) Selector() prom.Selector { func (r *metricNamer) Selector() prom.Selector {
@ -161,28 +83,15 @@ func (m *reMatcher) Matches(val string) bool {
type metricNamer struct { type metricNamer struct {
seriesQuery prom.Selector seriesQuery prom.Selector
labelTemplate *template.Template metricsQuery naming.MetricsQuery
labelResExtractor *labelGroupResExtractor
metricsQueryTemplate *template.Template
nameMatches *regexp.Regexp nameMatches *regexp.Regexp
nameAs string nameAs string
seriesMatchers []*reMatcher seriesMatchers []*reMatcher
labelResourceMu sync.RWMutex naming.ResourceConverter
labelToResource map[pmodel.LabelName]schema.GroupResource
resourceToLabel map[schema.GroupResource]pmodel.LabelName
mapper apimeta.RESTMapper
} }
// queryTemplateArgs are the arguments for the metrics query template. // queryTemplateArgs are the arguments for the metrics query template.
type queryTemplateArgs struct {
Series string
LabelMatchers string
LabelValuesByName map[string][]string
GroupBy string
GroupBySlice []string
}
func (n *metricNamer) FilterSeries(initialSeries []prom.Series) []prom.Series { func (n *metricNamer) FilterSeries(initialSeries []prom.Series) []prom.Series {
if len(n.seriesMatchers) == 0 { if len(n.seriesMatchers) == 0 {
return initialSeries return initialSeries
@ -203,163 +112,7 @@ SeriesLoop:
} }
func (n *metricNamer) QueryForSeries(series string, resource schema.GroupResource, namespace string, names ...string) (prom.Selector, error) { func (n *metricNamer) QueryForSeries(series string, resource schema.GroupResource, namespace string, names ...string) (prom.Selector, error) {
var exprs []string return n.metricsQuery.Build(series, resource, namespace, nil, names...)
valuesByName := map[string][]string{}
if namespace != "" {
namespaceLbl, err := n.LabelForResource(nsGroupResource)
if err != nil {
return "", err
}
exprs = append(exprs, prom.LabelEq(string(namespaceLbl), namespace))
valuesByName[string(namespaceLbl)] = []string{namespace}
}
resourceLbl, err := n.LabelForResource(resource)
if err != nil {
return "", err
}
matcher := prom.LabelEq
targetValue := names[0]
if len(names) > 1 {
matcher = prom.LabelMatches
targetValue = strings.Join(names, "|")
}
exprs = append(exprs, matcher(string(resourceLbl), targetValue))
valuesByName[string(resourceLbl)] = names
args := queryTemplateArgs{
Series: series,
LabelMatchers: strings.Join(exprs, ","),
LabelValuesByName: valuesByName,
GroupBy: string(resourceLbl),
GroupBySlice: []string{string(resourceLbl)},
}
queryBuff := new(bytes.Buffer)
if err := n.metricsQueryTemplate.Execute(queryBuff, args); err != nil {
return "", err
}
if queryBuff.Len() == 0 {
return "", fmt.Errorf("empty query produced by metrics query template")
}
return prom.Selector(queryBuff.String()), nil
}
func (n *metricNamer) ResourcesForSeries(series prom.Series) ([]schema.GroupResource, bool) {
// use an updates map to avoid having to drop the read lock to update the cache
// until the end. Since we'll probably have few updates after the first run,
// this should mean that we rarely have to hold the write lock.
var resources []schema.GroupResource
updates := make(map[pmodel.LabelName]schema.GroupResource)
namespaced := false
// use an anon func to get the right defer behavior
func() {
n.labelResourceMu.RLock()
defer n.labelResourceMu.RUnlock()
for lbl := range series.Labels {
var groupRes schema.GroupResource
var ok bool
// check if we have an override
if groupRes, ok = n.labelToResource[lbl]; ok {
resources = append(resources, groupRes)
} else if groupRes, ok = updates[lbl]; ok {
resources = append(resources, groupRes)
} else if n.labelResExtractor != nil {
// if not, check if it matches the form we expect, and if so,
// convert to a group-resource.
if groupRes, ok = n.labelResExtractor.GroupResourceForLabel(lbl); ok {
info, _, err := provider.CustomMetricInfo{GroupResource: groupRes}.Normalized(n.mapper)
if err != nil {
// this is likely to show up for a lot of labels, so make it a verbose info log
glog.V(9).Infof("unable to normalize group-resource %s from label %q, skipping: %v", groupRes.String(), lbl, err)
continue
}
groupRes = info.GroupResource
resources = append(resources, groupRes)
updates[lbl] = groupRes
}
}
if groupRes == nsGroupResource {
namespaced = true
}
}
}()
// update the cache for next time. This should only be called by discovery,
// so we don't really have to worry about the grap between read and write locks
// (plus, we don't care if someone else updates the cache first, since the results
// are necessarily the same, so at most we've done extra work).
if len(updates) > 0 {
n.labelResourceMu.Lock()
defer n.labelResourceMu.Unlock()
for lbl, groupRes := range updates {
n.labelToResource[lbl] = groupRes
}
}
return resources, namespaced
}
func (n *metricNamer) LabelForResource(resource schema.GroupResource) (pmodel.LabelName, error) {
n.labelResourceMu.RLock()
// check if we have a cached copy or override
lbl, ok := n.resourceToLabel[resource]
n.labelResourceMu.RUnlock() // release before we call makeLabelForResource
if ok {
return lbl, nil
}
// NB: we don't actually care about the gap between releasing read lock
// and acquiring the write lock -- if we do duplicate work sometimes, so be
// it, as long as we're correct.
// otherwise, use the template and save the result
lbl, err := n.makeLabelForResource(resource)
if err != nil {
return "", fmt.Errorf("unable to convert resource %s into label: %v", resource.String(), err)
}
return lbl, nil
}
// makeLabelForResource constructs a label name for the given resource, and saves the result.
// It must *not* be called under an existing lock.
func (n *metricNamer) makeLabelForResource(resource schema.GroupResource) (pmodel.LabelName, error) {
if n.labelTemplate == nil {
return "", fmt.Errorf("no generic resource label form specified for this metric")
}
buff := new(bytes.Buffer)
singularRes, err := n.mapper.ResourceSingularizer(resource.Resource)
if err != nil {
return "", fmt.Errorf("unable to singularize resource %s: %v", resource.String(), err)
}
convResource := schema.GroupResource{
Group: groupNameSanitizer.Replace(resource.Group),
Resource: singularRes,
}
if err := n.labelTemplate.Execute(buff, convResource); err != nil {
return "", err
}
if buff.Len() == 0 {
return "", fmt.Errorf("empty label produced by label template")
}
lbl := pmodel.LabelName(buff.String())
n.labelResourceMu.Lock()
defer n.labelResourceMu.Unlock()
n.resourceToLabel[resource] = lbl
n.labelToResource[lbl] = resource
return lbl, nil
} }
func (n *metricNamer) MetricNameForSeries(series prom.Series) (string, error) { func (n *metricNamer) MetricNameForSeries(series prom.Series) (string, error) {
@ -376,24 +129,14 @@ func NamersFromConfig(cfg *config.MetricsDiscoveryConfig, mapper apimeta.RESTMap
namers := make([]MetricNamer, len(cfg.Rules)) namers := make([]MetricNamer, len(cfg.Rules))
for i, rule := range cfg.Rules { for i, rule := range cfg.Rules {
var labelTemplate *template.Template resConv, err := naming.NewResourceConverter(rule.Resources.Template, rule.Resources.Overrides, mapper)
var labelResExtractor *labelGroupResExtractor
var err error
if rule.Resources.Template != "" {
labelTemplate, err = template.New("resource-label").Delims("<<", ">>").Parse(rule.Resources.Template)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to parse label template %q associated with series query %q: %v", rule.Resources.Template, rule.SeriesQuery, err) return nil, err
} }
labelResExtractor, err = newLabelGroupResExtractor(labelTemplate) metricsQuery, err := naming.NewMetricsQuery(rule.MetricsQuery, resConv)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to generate label format from template %q associated with series query %q: %v", rule.Resources.Template, rule.SeriesQuery, err) return nil, fmt.Errorf("unable to construct metrics query associated with series query %q: %v", rule.SeriesQuery, err)
}
}
metricsQueryTemplate, err := template.New("metrics-query").Delims("<<", ">>").Parse(rule.MetricsQuery)
if err != nil {
return nil, fmt.Errorf("unable to parse metrics query template %q associated with series query %q: %v", rule.MetricsQuery, rule.SeriesQuery, err)
} }
seriesMatchers := make([]*reMatcher, len(rule.SeriesFilters)) seriesMatchers := make([]*reMatcher, len(rule.SeriesFilters))
@ -439,33 +182,11 @@ func NamersFromConfig(cfg *config.MetricsDiscoveryConfig, mapper apimeta.RESTMap
namer := &metricNamer{ namer := &metricNamer{
seriesQuery: prom.Selector(rule.SeriesQuery), seriesQuery: prom.Selector(rule.SeriesQuery),
labelTemplate: labelTemplate, metricsQuery: metricsQuery,
labelResExtractor: labelResExtractor,
metricsQueryTemplate: metricsQueryTemplate,
mapper: mapper,
nameMatches: nameMatches, nameMatches: nameMatches,
nameAs: nameAs, nameAs: nameAs,
seriesMatchers: seriesMatchers, seriesMatchers: seriesMatchers,
ResourceConverter: resConv,
labelToResource: make(map[pmodel.LabelName]schema.GroupResource),
resourceToLabel: make(map[schema.GroupResource]pmodel.LabelName),
}
// invert the structure for consistency with the template
for lbl, groupRes := range rule.Resources.Overrides {
infoRaw := provider.CustomMetricInfo{
GroupResource: schema.GroupResource{
Group: groupRes.Group,
Resource: groupRes.Resource,
},
}
info, _, err := infoRaw.Normalized(mapper)
if err != nil {
return nil, fmt.Errorf("unable to normalize group-resource %v: %v", groupRes, err)
}
namer.labelToResource[pmodel.LabelName(lbl)] = info.GroupResource
namer.resourceToLabel[info.GroupResource] = pmodel.LabelName(lbl)
} }
namers[i] = namer namers[i] = namer

View file

@ -23,15 +23,14 @@ import (
"github.com/golang/glog" "github.com/golang/glog"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider" "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider/helpers"
pmodel "github.com/prometheus/common/model" pmodel "github.com/prometheus/common/model"
apierr "k8s.io/apimachinery/pkg/api/errors" apierr "k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta" apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
@ -76,53 +75,39 @@ func NewPrometheusProvider(mapper apimeta.RESTMapper, kubeClient dynamic.Interfa
}, lister }, lister
} }
func (p *prometheusProvider) metricFor(value pmodel.SampleValue, groupResource schema.GroupResource, namespace string, name string, metricName string) (*custom_metrics.MetricValue, error) { func (p *prometheusProvider) metricFor(value pmodel.SampleValue, name types.NamespacedName, info provider.CustomMetricInfo) (*custom_metrics.MetricValue, error) {
kind, err := p.mapper.KindFor(groupResource.WithVersion("")) ref, err := helpers.ReferenceFor(p.mapper, name, info)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &custom_metrics.MetricValue{ return &custom_metrics.MetricValue{
DescribedObject: custom_metrics.ObjectReference{ DescribedObject: ref,
APIVersion: groupResource.Group + "/" + runtime.APIVersionInternal, MetricName: info.Metric,
Kind: kind.Kind, // TODO(directxman12): use the right timestamp
Name: name,
Namespace: namespace,
},
MetricName: metricName,
Timestamp: metav1.Time{time.Now()}, Timestamp: metav1.Time{time.Now()},
Value: *resource.NewMilliQuantity(int64(value*1000.0), resource.DecimalSI), Value: *resource.NewMilliQuantity(int64(value*1000.0), resource.DecimalSI),
}, nil }, nil
} }
func (p *prometheusProvider) metricsFor(valueSet pmodel.Vector, info provider.CustomMetricInfo, list runtime.Object) (*custom_metrics.MetricValueList, error) { func (p *prometheusProvider) metricsFor(valueSet pmodel.Vector, info provider.CustomMetricInfo, namespace string, names []string) (*custom_metrics.MetricValueList, error) {
if !apimeta.IsListType(list) {
return nil, apierr.NewInternalError(fmt.Errorf("result of label selector list operation was not a list"))
}
values, found := p.MatchValuesToNames(info, valueSet) values, found := p.MatchValuesToNames(info, valueSet)
if !found { if !found {
return nil, provider.NewMetricNotFoundError(info.GroupResource, info.Metric) return nil, provider.NewMetricNotFoundError(info.GroupResource, info.Metric)
} }
res := []custom_metrics.MetricValue{} res := []custom_metrics.MetricValue{}
err := apimeta.EachListItem(list, func(item runtime.Object) error { for _, name := range names {
objUnstructured := item.(*unstructured.Unstructured) if _, found := values[name]; !found {
objName := objUnstructured.GetName() continue
if _, found := values[objName]; !found {
return nil
} }
value, err := p.metricFor(values[objName], info.GroupResource, objUnstructured.GetNamespace(), objName, info.Metric)
if err != nil {
return err
}
res = append(res, *value)
return nil value, err := p.metricFor(values[name], types.NamespacedName{Namespace: namespace, Name: name}, info)
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
res = append(res, *value)
}
return &custom_metrics.MetricValueList{ return &custom_metrics.MetricValueList{
Items: res, Items: res,
@ -151,14 +136,16 @@ func (p *prometheusProvider) buildQuery(info provider.CustomMetricInfo, namespac
return *queryResults.Vector, nil return *queryResults.Vector, nil
} }
func (p *prometheusProvider) getSingle(info provider.CustomMetricInfo, namespace, name string) (*custom_metrics.MetricValue, error) { func (p *prometheusProvider) GetMetricByName(name types.NamespacedName, info provider.CustomMetricInfo) (*custom_metrics.MetricValue, error) {
queryResults, err := p.buildQuery(info, namespace, name) // construct a query
queryResults, err := p.buildQuery(info, name.Namespace, name.Name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// associate the metrics
if len(queryResults) < 1 { if len(queryResults) < 1 {
return nil, provider.NewMetricNotFoundForError(info.GroupResource, info.Metric, name) return nil, provider.NewMetricNotFoundForError(info.GroupResource, info.Metric, name.Name)
} }
namedValues, found := p.MatchValuesToNames(info, queryResults) namedValues, found := p.MatchValuesToNames(info, queryResults)
@ -170,97 +157,33 @@ func (p *prometheusProvider) getSingle(info provider.CustomMetricInfo, namespace
glog.V(2).Infof("Got more than one result (%v results) when fetching metric %s for %q, using the first one with a matching name...", len(queryResults), info.String(), name) glog.V(2).Infof("Got more than one result (%v results) when fetching metric %s for %q, using the first one with a matching name...", len(queryResults), info.String(), name)
} }
resultValue, nameFound := namedValues[name] resultValue, nameFound := namedValues[name.Name]
if !nameFound { if !nameFound {
glog.Errorf("None of the results returned by when fetching metric %s for %q matched the resource name", info.String(), name) glog.Errorf("None of the results returned by when fetching metric %s for %q matched the resource name", info.String(), name)
return nil, provider.NewMetricNotFoundForError(info.GroupResource, info.Metric, name) return nil, provider.NewMetricNotFoundForError(info.GroupResource, info.Metric, name.Name)
} }
return p.metricFor(resultValue, info.GroupResource, "", name, info.Metric) // return the resulting metric
return p.metricFor(resultValue, name, info)
} }
func (p *prometheusProvider) getMultiple(info provider.CustomMetricInfo, namespace string, selector labels.Selector) (*custom_metrics.MetricValueList, error) { func (p *prometheusProvider) GetMetricBySelector(namespace string, selector labels.Selector, info provider.CustomMetricInfo) (*custom_metrics.MetricValueList, error) {
fullResources, err := p.mapper.ResourcesFor(info.GroupResource.WithVersion("")) // fetch a list of relevant resource names
if err == nil && len(fullResources) == 0 { resourceNames, err := helpers.ListObjectNames(p.mapper, p.kubeClient, namespace, selector, info)
err = fmt.Errorf("no fully versioned resources known for group-resource %v", info.GroupResource)
}
if err != nil {
glog.Errorf("unable to find preferred version to list matching resource names: %v", err)
// don't leak implementation details to the user
return nil, apierr.NewInternalError(fmt.Errorf("unable to list matching resources"))
}
var client dynamic.ResourceInterface
if namespace != "" {
client = p.kubeClient.Resource(fullResources[0]).Namespace(namespace)
} else {
client = p.kubeClient.Resource(fullResources[0])
}
// actually list the objects matching the label selector
matchingObjectsRaw, err := client.List(metav1.ListOptions{LabelSelector: selector.String()})
if err != nil { if err != nil {
glog.Errorf("unable to list matching resource names: %v", err) glog.Errorf("unable to list matching resource names: %v", err)
// don't leak implementation details to the user // don't leak implementation details to the user
return nil, apierr.NewInternalError(fmt.Errorf("unable to list matching resources")) return nil, apierr.NewInternalError(fmt.Errorf("unable to list matching resources"))
} }
// make sure we have a list
if !apimeta.IsListType(matchingObjectsRaw) {
return nil, apierr.NewInternalError(fmt.Errorf("result of label selector list operation was not a list"))
}
// convert a list of objects into the corresponding list of names
resourceNames := []string{}
err = apimeta.EachListItem(matchingObjectsRaw, func(item runtime.Object) error {
objName := item.(*unstructured.Unstructured).GetName()
resourceNames = append(resourceNames, objName)
return nil
})
// construct the actual query // construct the actual query
queryResults, err := p.buildQuery(info, namespace, resourceNames...) queryResults, err := p.buildQuery(info, namespace, resourceNames...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return p.metricsFor(queryResults, info, matchingObjectsRaw)
}
func (p *prometheusProvider) GetRootScopedMetricByName(groupResource schema.GroupResource, name string, metricName string) (*custom_metrics.MetricValue, error) { // return the resulting metrics
info := provider.CustomMetricInfo{ return p.metricsFor(queryResults, info, namespace, resourceNames)
GroupResource: groupResource,
Metric: metricName,
Namespaced: false,
}
return p.getSingle(info, "", name)
}
func (p *prometheusProvider) GetRootScopedMetricBySelector(groupResource schema.GroupResource, selector labels.Selector, metricName string) (*custom_metrics.MetricValueList, error) {
info := provider.CustomMetricInfo{
GroupResource: groupResource,
Metric: metricName,
Namespaced: false,
}
return p.getMultiple(info, "", selector)
}
func (p *prometheusProvider) GetNamespacedMetricByName(groupResource schema.GroupResource, namespace string, name string, metricName string) (*custom_metrics.MetricValue, error) {
info := provider.CustomMetricInfo{
GroupResource: groupResource,
Metric: metricName,
Namespaced: true,
}
return p.getSingle(info, namespace, name)
}
func (p *prometheusProvider) GetNamespacedMetricBySelector(groupResource schema.GroupResource, namespace string, selector labels.Selector, metricName string) (*custom_metrics.MetricValueList, error) {
info := provider.CustomMetricInfo{
GroupResource: groupResource,
Metric: metricName,
Namespaced: true,
}
return p.getMultiple(info, namespace, selector)
} }
type cachingMetricsLister struct { type cachingMetricsLister struct {

View file

@ -0,0 +1,29 @@
/*
Copyright 2018 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 provider_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestProvider(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Custom Metrics Provider Suite")
}

View file

@ -17,89 +17,35 @@ limitations under the License.
package provider package provider
import ( import (
"context"
"fmt"
"sort"
"testing"
"time" "time"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider" "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider"
"github.com/stretchr/testify/assert" . "github.com/onsi/ginkgo"
"github.com/stretchr/testify/require" . "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
fakedyn "k8s.io/client-go/dynamic/fake" fakedyn "k8s.io/client-go/dynamic/fake"
config "github.com/directxman12/k8s-prometheus-adapter/cmd/config-gen/utils" config "github.com/directxman12/k8s-prometheus-adapter/cmd/config-gen/utils"
prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client" prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client"
fakeprom "github.com/directxman12/k8s-prometheus-adapter/pkg/client/fake"
pmodel "github.com/prometheus/common/model" pmodel "github.com/prometheus/common/model"
) )
const fakeProviderUpdateInterval = 2 * time.Second const fakeProviderUpdateInterval = 2 * time.Second
// fakePromClient is a fake instance of prom.Client func setupPrometheusProvider() (provider.CustomMetricsProvider, *fakeprom.FakePrometheusClient) {
type fakePromClient struct { fakeProm := &fakeprom.FakePrometheusClient{}
// acceptibleInterval is the interval in which to return queries
acceptibleInterval pmodel.Interval
// errQueries are queries that result in an error (whether from Query or Series)
errQueries map[prom.Selector]error
// series are non-error responses to partial Series calls
series map[prom.Selector][]prom.Series
// queryResults are non-error responses to Query
queryResults map[prom.Selector]prom.QueryResult
}
func (c *fakePromClient) Series(_ context.Context, interval pmodel.Interval, selectors ...prom.Selector) ([]prom.Series, error) {
if (interval.Start != 0 && interval.Start < c.acceptibleInterval.Start) || (interval.End != 0 && interval.End > c.acceptibleInterval.End) {
return nil, fmt.Errorf("interval [%v, %v] for query is outside range [%v, %v]", interval.Start, interval.End, c.acceptibleInterval.Start, c.acceptibleInterval.End)
}
res := []prom.Series{}
for _, sel := range selectors {
if err, found := c.errQueries[sel]; found {
return nil, err
}
if series, found := c.series[sel]; found {
res = append(res, series...)
}
}
return res, nil
}
func (c *fakePromClient) Query(_ context.Context, t pmodel.Time, query prom.Selector) (prom.QueryResult, error) {
if t < c.acceptibleInterval.Start || t > c.acceptibleInterval.End {
return prom.QueryResult{}, fmt.Errorf("time %v for query is outside range [%v, %v]", t, c.acceptibleInterval.Start, c.acceptibleInterval.End)
}
if err, found := c.errQueries[query]; found {
return prom.QueryResult{}, err
}
if res, found := c.queryResults[query]; found {
return res, nil
}
return prom.QueryResult{
Type: pmodel.ValVector,
Vector: &pmodel.Vector{},
}, nil
}
func (c *fakePromClient) QueryRange(_ context.Context, r prom.Range, query prom.Selector) (prom.QueryResult, error) {
return prom.QueryResult{}, nil
}
func setupPrometheusProvider(t *testing.T) (provider.CustomMetricsProvider, *fakePromClient) {
fakeProm := &fakePromClient{}
fakeKubeClient := &fakedyn.FakeDynamicClient{} fakeKubeClient := &fakedyn.FakeDynamicClient{}
cfg := config.DefaultConfig(1*time.Minute, "") cfg := config.DefaultConfig(1*time.Minute, "")
namers, err := NamersFromConfig(cfg, restMapper()) namers, err := NamersFromConfig(cfg, restMapper())
require.NoError(t, err) Expect(err).NotTo(HaveOccurred())
prov, _ := NewPrometheusProvider(restMapper(), fakeKubeClient, fakeProm, namers, fakeProviderUpdateInterval) prov, _ := NewPrometheusProvider(restMapper(), fakeKubeClient, fakeProm, namers, fakeProviderUpdateInterval)
containerSel := prom.MatchSeries("", prom.NameMatches("^container_.*"), prom.LabelNeq("container_name", "POD"), prom.LabelNeq("namespace", ""), prom.LabelNeq("pod_name", "")) containerSel := prom.MatchSeries("", prom.NameMatches("^container_.*"), prom.LabelNeq("container_name", "POD"), prom.LabelNeq("namespace", ""), prom.LabelNeq("pod_name", ""))
namespacedSel := prom.MatchSeries("", prom.LabelNeq("namespace", ""), prom.NameNotMatches("^container_.*")) namespacedSel := prom.MatchSeries("", prom.LabelNeq("namespace", ""), prom.NameNotMatches("^container_.*"))
fakeProm.series = map[prom.Selector][]prom.Series{ fakeProm.SeriesResults = map[prom.Selector][]prom.Series{
containerSel: { containerSel: {
{ {
Name: "container_some_usage", Name: "container_some_usage",
@ -129,39 +75,35 @@ func setupPrometheusProvider(t *testing.T) (provider.CustomMetricsProvider, *fak
return prov, fakeProm return prov, fakeProm
} }
func TestListAllMetrics(t *testing.T) { var _ = Describe("Custom Metrics Provider", func() {
// setup It("should be able to list all metrics", func() {
prov, fakeProm := setupPrometheusProvider(t) By("setting up the provider")
prov, fakeProm := setupPrometheusProvider()
// assume we have no updates By("ensuring that no metrics are present before we start listing")
require.Len(t, prov.ListAllMetrics(), 0, "assume: should have no metrics updates at the start") Expect(prov.ListAllMetrics()).To(BeEmpty())
// set the acceptible interval (now until the next update, with a bit of wiggle room) By("setting the acceptible interval to now until the next update, with a bit of wiggle room")
startTime := pmodel.Now().Add(-1*fakeProviderUpdateInterval - fakeProviderUpdateInterval/10) startTime := pmodel.Now().Add(-1*fakeProviderUpdateInterval - fakeProviderUpdateInterval/10)
fakeProm.acceptibleInterval = pmodel.Interval{Start: startTime, End: 0} fakeProm.AcceptableInterval = pmodel.Interval{Start: startTime, End: 0}
// update the metrics (without actually calling RunUntil, so we can avoid timing issues) By("updating the list of available metrics")
// don't call RunUntil to avoid timing issue
lister := prov.(*prometheusProvider).SeriesRegistry.(*cachingMetricsLister) lister := prov.(*prometheusProvider).SeriesRegistry.(*cachingMetricsLister)
require.NoError(t, lister.updateMetrics()) Expect(lister.updateMetrics()).To(Succeed())
// list/sort the metrics By("listing all metrics, and checking that they contain the expected results")
actualMetrics := prov.ListAllMetrics() Expect(prov.ListAllMetrics()).To(ConsistOf(
sort.Sort(metricInfoSorter(actualMetrics)) provider.CustomMetricInfo{schema.GroupResource{Resource: "services"}, true, "ingress_hits"},
provider.CustomMetricInfo{schema.GroupResource{Group: "extensions", Resource: "ingresses"}, true, "ingress_hits"},
expectedMetrics := []provider.CustomMetricInfo{ provider.CustomMetricInfo{schema.GroupResource{Resource: "pods"}, true, "ingress_hits"},
{schema.GroupResource{Resource: "services"}, true, "ingress_hits"}, provider.CustomMetricInfo{schema.GroupResource{Resource: "namespaces"}, false, "ingress_hits"},
{schema.GroupResource{Group: "extensions", Resource: "ingresses"}, true, "ingress_hits"}, provider.CustomMetricInfo{schema.GroupResource{Resource: "services"}, true, "service_proxy_packets"},
{schema.GroupResource{Resource: "pods"}, true, "ingress_hits"}, provider.CustomMetricInfo{schema.GroupResource{Resource: "namespaces"}, false, "service_proxy_packets"},
{schema.GroupResource{Resource: "namespaces"}, false, "ingress_hits"}, provider.CustomMetricInfo{schema.GroupResource{Group: "extensions", Resource: "deployments"}, true, "work_queue_wait"},
{schema.GroupResource{Resource: "services"}, true, "service_proxy_packets"}, provider.CustomMetricInfo{schema.GroupResource{Resource: "namespaces"}, false, "work_queue_wait"},
{schema.GroupResource{Resource: "namespaces"}, false, "service_proxy_packets"}, provider.CustomMetricInfo{schema.GroupResource{Resource: "namespaces"}, false, "some_usage"},
{schema.GroupResource{Group: "extensions", Resource: "deployments"}, true, "work_queue_wait"}, provider.CustomMetricInfo{schema.GroupResource{Resource: "pods"}, true, "some_usage"},
{schema.GroupResource{Resource: "namespaces"}, false, "work_queue_wait"}, ))
{schema.GroupResource{Resource: "namespaces"}, false, "some_usage"}, })
{schema.GroupResource{Resource: "pods"}, true, "some_usage"}, })
}
sort.Sort(metricInfoSorter(expectedMetrics))
// assert that we got what we expected
assert.Equal(t, expectedMetrics, actualMetrics)
}

View file

@ -17,14 +17,13 @@ limitations under the License.
package provider package provider
import ( import (
"sort" "fmt"
"testing"
"time" "time"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider" "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
pmodel "github.com/prometheus/common/model" pmodel "github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
coreapi "k8s.io/api/core/v1" coreapi "k8s.io/api/core/v1"
extapi "k8s.io/api/extensions/v1beta1" extapi "k8s.io/api/extensions/v1beta1"
apimeta "k8s.io/apimachinery/pkg/api/meta" apimeta "k8s.io/apimachinery/pkg/api/meta"
@ -51,10 +50,10 @@ func restMapper() apimeta.RESTMapper {
return mapper return mapper
} }
func setupMetricNamer(t testing.TB) []MetricNamer { func setupMetricNamer() []MetricNamer {
cfg := config.DefaultConfig(1*time.Minute, "kube_") cfg := config.DefaultConfig(1*time.Minute, "kube_")
namers, err := NamersFromConfig(cfg, restMapper()) namers, err := NamersFromConfig(cfg, restMapper())
require.NoError(t, err) Expect(err).NotTo(HaveOccurred())
return namers return namers
} }
@ -117,27 +116,31 @@ var seriesRegistryTestSeries = [][]prom.Series{
}, },
} }
func TestSeriesRegistry(t *testing.T) { type regTestCase struct {
assert := assert.New(t)
require := require.New(t)
namers := setupMetricNamer(t)
registry := &basicSeriesRegistry{
mapper: restMapper(),
}
// set up the registry
require.NoError(registry.SetSeries(seriesRegistryTestSeries, namers))
// make sure each metric got registered and can form queries
testCases := []struct {
title string title string
info provider.CustomMetricInfo info provider.CustomMetricInfo
namespace string namespace string
resourceNames []string resourceNames []string
expectedQuery string expectedQuery string
}{ }
var _ = Describe("Series Registry", func() {
var (
registry *basicSeriesRegistry
)
BeforeEach(func() {
namers := setupMetricNamer()
registry = &basicSeriesRegistry{
mapper: restMapper(),
}
Expect(registry.SetSeries(seriesRegistryTestSeries, namers)).To(Succeed())
})
Context("with the default configuration rules", func() {
// make sure each metric got registered and can form queries
testCases := []regTestCase{
// container metrics // container metrics
{ {
title: "container metrics gauge / multiple resource names", title: "container metrics gauge / multiple resource names",
@ -228,93 +231,38 @@ func TestSeriesRegistry(t *testing.T) {
}, },
} }
for _, testCase := range testCases { for _, tc := range testCases {
outputQuery, found := registry.QueryForMetric(testCase.info, testCase.namespace, testCase.resourceNames...) tc := tc // copy to avoid iteration variable issues
if !assert.True(found, "%s: metric %v should available", testCase.title, testCase.info) { It(fmt.Sprintf("should build a query for %s", tc.title), func() {
continue By(fmt.Sprintf("composing the query for the %s metric on %v in namespace %s", tc.info, tc.resourceNames, tc.namespace))
outputQuery, found := registry.QueryForMetric(tc.info, tc.namespace, tc.resourceNames...)
Expect(found).To(BeTrue(), "metric %s should be available", tc.info)
By("verifying that the query is as expected")
Expect(outputQuery).To(Equal(prom.Selector(tc.expectedQuery)))
})
} }
assert.Equal(prom.Selector(testCase.expectedQuery), outputQuery, "%s: metric %v should have produced the correct query for %v in namespace %s", testCase.title, testCase.info, testCase.resourceNames, testCase.namespace) It("should list all metrics", func() {
} Expect(registry.ListAllMetrics()).To(ConsistOf(
provider.CustomMetricInfo{schema.GroupResource{Resource: "pods"}, true, "some_count"},
allMetrics := registry.ListAllMetrics() provider.CustomMetricInfo{schema.GroupResource{Resource: "namespaces"}, false, "some_count"},
expectedMetrics := []provider.CustomMetricInfo{ provider.CustomMetricInfo{schema.GroupResource{Resource: "pods"}, true, "some_time"},
{schema.GroupResource{Resource: "pods"}, true, "some_count"}, provider.CustomMetricInfo{schema.GroupResource{Resource: "namespaces"}, false, "some_time"},
{schema.GroupResource{Resource: "namespaces"}, false, "some_count"}, provider.CustomMetricInfo{schema.GroupResource{Resource: "pods"}, true, "some_usage"},
{schema.GroupResource{Resource: "pods"}, true, "some_time"}, provider.CustomMetricInfo{schema.GroupResource{Resource: "namespaces"}, false, "some_usage"},
{schema.GroupResource{Resource: "namespaces"}, false, "some_time"}, provider.CustomMetricInfo{schema.GroupResource{Resource: "services"}, true, "ingress_hits"},
{schema.GroupResource{Resource: "pods"}, true, "some_usage"}, provider.CustomMetricInfo{schema.GroupResource{Group: "extensions", Resource: "ingresses"}, true, "ingress_hits"},
{schema.GroupResource{Resource: "namespaces"}, false, "some_usage"}, provider.CustomMetricInfo{schema.GroupResource{Resource: "pods"}, true, "ingress_hits"},
{schema.GroupResource{Resource: "services"}, true, "ingress_hits"}, provider.CustomMetricInfo{schema.GroupResource{Resource: "namespaces"}, false, "ingress_hits"},
{schema.GroupResource{Group: "extensions", Resource: "ingresses"}, true, "ingress_hits"}, provider.CustomMetricInfo{schema.GroupResource{Resource: "services"}, true, "service_proxy_packets"},
{schema.GroupResource{Resource: "pods"}, true, "ingress_hits"}, provider.CustomMetricInfo{schema.GroupResource{Resource: "namespaces"}, false, "service_proxy_packets"},
{schema.GroupResource{Resource: "namespaces"}, false, "ingress_hits"}, provider.CustomMetricInfo{schema.GroupResource{Group: "extensions", Resource: "deployments"}, true, "work_queue_wait"},
{schema.GroupResource{Resource: "services"}, true, "service_proxy_packets"}, provider.CustomMetricInfo{schema.GroupResource{Resource: "namespaces"}, false, "work_queue_wait"},
{schema.GroupResource{Resource: "namespaces"}, false, "service_proxy_packets"}, provider.CustomMetricInfo{schema.GroupResource{Resource: "nodes"}, false, "node_gigawatts"},
{schema.GroupResource{Group: "extensions", Resource: "deployments"}, true, "work_queue_wait"}, provider.CustomMetricInfo{schema.GroupResource{Resource: "persistentvolumes"}, false, "volume_claims"},
{schema.GroupResource{Resource: "namespaces"}, false, "work_queue_wait"}, provider.CustomMetricInfo{schema.GroupResource{Resource: "nodes"}, false, "node_fan"},
{schema.GroupResource{Resource: "nodes"}, false, "node_gigawatts"}, ))
{schema.GroupResource{Resource: "persistentvolumes"}, false, "volume_claims"}, })
{schema.GroupResource{Resource: "nodes"}, false, "node_fan"}, })
} })
// sort both for easy comparison
sort.Sort(metricInfoSorter(allMetrics))
sort.Sort(metricInfoSorter(expectedMetrics))
assert.Equal(expectedMetrics, allMetrics, "should have listed all expected metrics")
}
func BenchmarkSetSeries(b *testing.B) {
namers := setupMetricNamer(b)
registry := &basicSeriesRegistry{
mapper: restMapper(),
}
numDuplicates := 10000
newSeriesSlices := make([][]prom.Series, len(seriesRegistryTestSeries))
for i, seriesSlice := range seriesRegistryTestSeries {
newSlice := make([]prom.Series, len(seriesSlice)*numDuplicates)
for j, series := range seriesSlice {
for k := 0; k < numDuplicates; k++ {
newSlice[j*numDuplicates+k] = series
}
}
newSeriesSlices[i] = newSlice
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
registry.SetSeries(newSeriesSlices, namers)
}
}
// metricInfoSorter is a sort.Interface for sorting provider.CustomMetricInfos
type metricInfoSorter []provider.CustomMetricInfo
func (s metricInfoSorter) Len() int {
return len(s)
}
func (s metricInfoSorter) Less(i, j int) bool {
infoI := s[i]
infoJ := s[j]
if infoI.Metric == infoJ.Metric {
if infoI.GroupResource == infoJ.GroupResource {
return infoI.Namespaced
}
if infoI.GroupResource.Group == infoJ.GroupResource.Group {
return infoI.GroupResource.Resource < infoJ.GroupResource.Resource
}
return infoI.GroupResource.Group < infoJ.GroupResource.Group
}
return infoI.Metric < infoJ.Metric
}
func (s metricInfoSorter) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}

83
pkg/naming/lbl_res.go Normal file
View file

@ -0,0 +1,83 @@
package naming
import (
"bytes"
"fmt"
"regexp"
"text/template"
apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"
pmodel "github.com/prometheus/common/model"
)
// labelGroupResExtractor extracts schema.GroupResources from series labels.
type labelGroupResExtractor struct {
regex *regexp.Regexp
resourceInd int
groupInd *int
mapper apimeta.RESTMapper
}
// newLabelGroupResExtractor creates a new labelGroupResExtractor for labels whose form
// matches the given template. It does so by creating a regular expression from the template,
// so anything in the template which limits resource or group name length will cause issues.
func newLabelGroupResExtractor(labelTemplate *template.Template) (*labelGroupResExtractor, error) {
labelRegexBuff := new(bytes.Buffer)
if err := labelTemplate.Execute(labelRegexBuff, schema.GroupResource{"(?P<group>.+?)", "(?P<resource>.+?)"}); err != nil {
return nil, fmt.Errorf("unable to convert label template to matcher: %v", err)
}
if labelRegexBuff.Len() == 0 {
return nil, fmt.Errorf("unable to convert label template to matcher: empty template")
}
labelRegexRaw := "^" + labelRegexBuff.String() + "$"
labelRegex, err := regexp.Compile(labelRegexRaw)
if err != nil {
return nil, fmt.Errorf("unable to convert label template to matcher: %v", err)
}
var groupInd *int
var resInd *int
for i, name := range labelRegex.SubexpNames() {
switch name {
case "group":
ind := i // copy to avoid iteration variable reference
groupInd = &ind
case "resource":
ind := i // copy to avoid iteration variable reference
resInd = &ind
}
}
if resInd == nil {
return nil, fmt.Errorf("must include at least `{{.Resource}}` in the label template")
}
return &labelGroupResExtractor{
regex: labelRegex,
resourceInd: *resInd,
groupInd: groupInd,
}, nil
}
// GroupResourceForLabel extracts a schema.GroupResource from the given label, if possible.
// The second return value indicates whether or not a potential group-resource was found in this label.
func (e *labelGroupResExtractor) GroupResourceForLabel(lbl pmodel.LabelName) (schema.GroupResource, bool) {
matchGroups := e.regex.FindStringSubmatch(string(lbl))
if matchGroups != nil {
group := ""
if e.groupInd != nil {
group = matchGroups[*e.groupInd]
}
return schema.GroupResource{
Group: group,
Resource: matchGroups[e.resourceInd],
}, true
}
return schema.GroupResource{}, false
}

109
pkg/naming/metrics_query.go Normal file
View file

@ -0,0 +1,109 @@
package naming
import (
"bytes"
"fmt"
"strings"
"text/template"
"k8s.io/apimachinery/pkg/runtime/schema"
prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client"
)
// MetricsQuery represents a compiled metrics query for some set of
// series that can be converted into an series of Prometheus expressions to
// be passed to a client.
type MetricsQuery interface {
// Build constructs Prometheus expressions to represent this query
// over the given group-resource. If namespace is empty, the resource
// is considered to be root-scoped. extraGroupBy may be used for cases
// where we need to scope down more specifically than just the group-resource
// (e.g. container metrics).
Build(series string, groupRes schema.GroupResource, namespace string, extraGroupBy []string, resourceNames ...string) (prom.Selector, error)
}
// NewMetricsQuery constructs a new MetricsQuery by compiling the given Go template.
// The delimiters on the template are `<<` and `>>`, and it may use the following fields:
// - Series: the series in question
// - LabelMatchers: a pre-stringified form of the label matchers for the resources in the query
// - LabelMatchersByName: the raw map-form of the above matchers
// - GroupBy: the group-by clause to use for the resources in the query (stringified)
// - GroupBySlice: the raw slice form of the above group-by clause
func NewMetricsQuery(queryTemplate string, resourceConverter ResourceConverter) (MetricsQuery, error) {
templ, err := template.New("metrics-query").Delims("<<", ">>").Parse(queryTemplate)
if err != nil {
return nil, fmt.Errorf("unable to parse metrics query template %q: %v", queryTemplate, err)
}
return &metricsQuery{
resConverter: resourceConverter,
template: templ,
}, nil
}
// metricsQuery is a MetricsQuery based on a compiled Go text template.
// with the delimiters as `<<` and `>>`, and the arguments found in
// queryTemplateArgs.
type metricsQuery struct {
resConverter ResourceConverter
template *template.Template
}
// queryTemplateArgs contains the arguments for the template used in metricsQuery.
type queryTemplateArgs struct {
Series string
LabelMatchers string
LabelValuesByName map[string][]string
GroupBy string
GroupBySlice []string
}
func (q *metricsQuery) Build(series string, resource schema.GroupResource, namespace string, extraGroupBy []string, names ...string) (prom.Selector, error) {
var exprs []string
valuesByName := map[string][]string{}
if namespace != "" {
namespaceLbl, err := q.resConverter.LabelForResource(nsGroupResource)
if err != nil {
return "", err
}
exprs = append(exprs, prom.LabelEq(string(namespaceLbl), namespace))
valuesByName[string(namespaceLbl)] = []string{namespace}
}
resourceLbl, err := q.resConverter.LabelForResource(resource)
if err != nil {
return "", err
}
matcher := prom.LabelEq
targetValue := names[0]
if len(names) > 1 {
matcher = prom.LabelMatches
targetValue = strings.Join(names, "|")
}
exprs = append(exprs, matcher(string(resourceLbl), targetValue))
valuesByName[string(resourceLbl)] = names
groupBy := make([]string, 0, len(extraGroupBy)+1)
groupBy = append(groupBy, string(resourceLbl))
groupBy = append(groupBy, extraGroupBy...)
args := queryTemplateArgs{
Series: series,
LabelMatchers: strings.Join(exprs, ","),
LabelValuesByName: valuesByName,
GroupBy: strings.Join(groupBy, ","),
GroupBySlice: groupBy,
}
queryBuff := new(bytes.Buffer)
if err := q.template.Execute(queryBuff, args); err != nil {
return "", err
}
if queryBuff.Len() == 0 {
return "", fmt.Errorf("empty query produced by metrics query template")
}
return prom.Selector(queryBuff.String()), nil
}

View file

@ -0,0 +1,200 @@
package naming
import (
"bytes"
"fmt"
"strings"
"sync"
"text/template"
apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"
prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client"
"github.com/directxman12/k8s-prometheus-adapter/pkg/config"
"github.com/golang/glog"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider"
pmodel "github.com/prometheus/common/model"
)
var (
groupNameSanitizer = strings.NewReplacer(".", "_", "-", "_")
nsGroupResource = schema.GroupResource{Resource: "namespaces"}
)
// ResourceConverter knows the relationship between Kubernetes group-resources and Prometheus labels,
// and can convert between the two for any given label or series.
type ResourceConverter interface {
// ResourcesForSeries returns the group-resources associated with the given series,
// as well as whether or not the given series has the "namespace" resource).
ResourcesForSeries(series prom.Series) (res []schema.GroupResource, namespaced bool)
// LabelForResource returns the appropriate label for the given resource.
LabelForResource(resource schema.GroupResource) (pmodel.LabelName, error)
}
type resourceConverter struct {
labelResourceMu sync.RWMutex
labelToResource map[pmodel.LabelName]schema.GroupResource
resourceToLabel map[schema.GroupResource]pmodel.LabelName
labelResExtractor *labelGroupResExtractor
mapper apimeta.RESTMapper
labelTemplate *template.Template
}
// NewResourceConverter creates a ResourceConverter based on a generic template plus any overrides.
// Either overrides or the template may be empty, but not both.
func NewResourceConverter(resourceTemplate string, overrides map[string]config.GroupResource, mapper apimeta.RESTMapper) (ResourceConverter, error) {
converter := &resourceConverter{
labelToResource: make(map[pmodel.LabelName]schema.GroupResource),
resourceToLabel: make(map[schema.GroupResource]pmodel.LabelName),
mapper: mapper,
}
if resourceTemplate != "" {
labelTemplate, err := template.New("resource-label").Delims("<<", ">>").Parse(resourceTemplate)
if err != nil {
return converter, fmt.Errorf("unable to parse label template %q: %v", resourceTemplate, err)
}
converter.labelTemplate = labelTemplate
labelResExtractor, err := newLabelGroupResExtractor(labelTemplate)
if err != nil {
return converter, fmt.Errorf("unable to generate label format from template %q: %v", resourceTemplate, err)
}
converter.labelResExtractor = labelResExtractor
}
// invert the structure for consistency with the template
for lbl, groupRes := range overrides {
infoRaw := provider.CustomMetricInfo{
GroupResource: schema.GroupResource{
Group: groupRes.Group,
Resource: groupRes.Resource,
},
}
info, _, err := infoRaw.Normalized(converter.mapper)
if err != nil {
return nil, fmt.Errorf("unable to normalize group-resource %v: %v", groupRes, err)
}
converter.labelToResource[pmodel.LabelName(lbl)] = info.GroupResource
converter.resourceToLabel[info.GroupResource] = pmodel.LabelName(lbl)
}
return converter, nil
}
func (r *resourceConverter) LabelForResource(resource schema.GroupResource) (pmodel.LabelName, error) {
r.labelResourceMu.RLock()
// check if we have a cached copy or override
lbl, ok := r.resourceToLabel[resource]
r.labelResourceMu.RUnlock() // release before we call makeLabelForResource
if ok {
return lbl, nil
}
// NB: we don't actually care about the gap between releasing read lock
// and acquiring the write lock -- if we do duplicate work sometimes, so be
// it, as long as we're correct.
// otherwise, use the template and save the result
lbl, err := r.makeLabelForResource(resource)
if err != nil {
return "", fmt.Errorf("unable to convert resource %s into label: %v", resource.String(), err)
}
return lbl, nil
}
// makeLabelForResource constructs a label name for the given resource, and saves the result.
// It must *not* be called under an existing lock.
func (r *resourceConverter) makeLabelForResource(resource schema.GroupResource) (pmodel.LabelName, error) {
if r.labelTemplate == nil {
return "", fmt.Errorf("no generic resource label form specified for this metric")
}
buff := new(bytes.Buffer)
singularRes, err := r.mapper.ResourceSingularizer(resource.Resource)
if err != nil {
return "", fmt.Errorf("unable to singularize resource %s: %v", resource.String(), err)
}
convResource := schema.GroupResource{
Group: groupNameSanitizer.Replace(resource.Group),
Resource: singularRes,
}
if err := r.labelTemplate.Execute(buff, convResource); err != nil {
return "", err
}
if buff.Len() == 0 {
return "", fmt.Errorf("empty label produced by label template")
}
lbl := pmodel.LabelName(buff.String())
r.labelResourceMu.Lock()
defer r.labelResourceMu.Unlock()
r.resourceToLabel[resource] = lbl
r.labelToResource[lbl] = resource
return lbl, nil
}
func (r *resourceConverter) ResourcesForSeries(series prom.Series) ([]schema.GroupResource, bool) {
// use an updates map to avoid having to drop the read lock to update the cache
// until the end. Since we'll probably have few updates after the first run,
// this should mean that we rarely have to hold the write lock.
var resources []schema.GroupResource
updates := make(map[pmodel.LabelName]schema.GroupResource)
namespaced := false
// use an anon func to get the right defer behavior
func() {
r.labelResourceMu.RLock()
defer r.labelResourceMu.RUnlock()
for lbl := range series.Labels {
var groupRes schema.GroupResource
var ok bool
// check if we have an override
if groupRes, ok = r.labelToResource[lbl]; ok {
resources = append(resources, groupRes)
} else if groupRes, ok = updates[lbl]; ok {
resources = append(resources, groupRes)
} else if r.labelResExtractor != nil {
// if not, check if it matches the form we expect, and if so,
// convert to a group-resource.
if groupRes, ok = r.labelResExtractor.GroupResourceForLabel(lbl); ok {
info, _, err := provider.CustomMetricInfo{GroupResource: groupRes}.Normalized(r.mapper)
if err != nil {
// this is likely to show up for a lot of labels, so make it a verbose info log
glog.V(9).Infof("unable to normalize group-resource %s from label %q, skipping: %v", groupRes.String(), lbl, err)
continue
}
groupRes = info.GroupResource
resources = append(resources, groupRes)
updates[lbl] = groupRes
}
}
if groupRes == nsGroupResource {
namespaced = true
}
}
}()
// update the cache for next time. This should only be called by discovery,
// so we don't really have to worry about the gap between read and write locks
// (plus, we don't care if someone else updates the cache first, since the results
// are necessarily the same, so at most we've done extra work).
if len(updates) > 0 {
r.labelResourceMu.Lock()
defer r.labelResourceMu.Unlock()
for lbl, groupRes := range updates {
r.labelToResource[lbl] = groupRes
}
}
return resources, namespaced
}

View file

@ -0,0 +1,385 @@
/*
Copyright 2018 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 resourceprovider
import (
"context"
"fmt"
"sync"
"time"
"github.com/golang/glog"
"github.com/kubernetes-incubator/metrics-server/pkg/provider"
corev1 "k8s.io/api/core/v1"
apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/runtime/schema"
apitypes "k8s.io/apimachinery/pkg/types"
metrics "k8s.io/metrics/pkg/apis/metrics"
"github.com/directxman12/k8s-prometheus-adapter/pkg/client"
"github.com/directxman12/k8s-prometheus-adapter/pkg/config"
"github.com/directxman12/k8s-prometheus-adapter/pkg/naming"
pmodel "github.com/prometheus/common/model"
)
var (
nodeResource = schema.GroupResource{Resource: "nodes"}
nsResource = schema.GroupResource{Resource: "ns"}
podResource = schema.GroupResource{Resource: "pods"}
)
// TODO(directxman12): consider support for nanocore values -- adjust scale if less than 1 millicore, or greater than max int64
// newResourceQuery instantiates query information from the give configuration rule for querying
// resource metrics for some resource.
func newResourceQuery(cfg config.ResourceRule, mapper apimeta.RESTMapper) (resourceQuery, error) {
converter, err := naming.NewResourceConverter(cfg.Resources.Template, cfg.Resources.Overrides, mapper)
if err != nil {
return resourceQuery{}, fmt.Errorf("unable to construct label-resource converter: %v", err)
}
contQuery, err := naming.NewMetricsQuery(cfg.ContainerQuery, converter)
if err != nil {
return resourceQuery{}, fmt.Errorf("unable to construct container metrics query: %v", err)
}
nodeQuery, err := naming.NewMetricsQuery(cfg.NodeQuery, converter)
if err != nil {
return resourceQuery{}, fmt.Errorf("unable to construct node metrics query: %v", err)
}
return resourceQuery{
converter: converter,
contQuery: contQuery,
nodeQuery: nodeQuery,
containerLabel: cfg.ContainerLabel,
}, nil
}
// resourceQuery represents query information for querying resource metrics for some resource,
// like CPU or memory.
type resourceQuery struct {
converter naming.ResourceConverter
contQuery naming.MetricsQuery
nodeQuery naming.MetricsQuery
containerLabel string
}
// NewProvider constructs a new MetricsProvider to provide resource metrics from Prometheus using the given rules.
func NewProvider(prom client.Client, mapper apimeta.RESTMapper, cfg *config.ResourceRules) (provider.MetricsProvider, error) {
cpuQuery, err := newResourceQuery(cfg.CPU, mapper)
if err != nil {
return nil, fmt.Errorf("unable to construct querier for CPU metrics: %v", err)
}
memQuery, err := newResourceQuery(cfg.Memory, mapper)
if err != nil {
return nil, fmt.Errorf("unable to construct querier for memory metrics: %v", err)
}
return &resourceProvider{
prom: prom,
cpu: cpuQuery,
mem: memQuery,
window: time.Duration(cfg.Window),
}, nil
}
// resourceProvider is a MetricsProvider that contacts Prometheus to provide
// the resource metrics.
type resourceProvider struct {
prom client.Client
cpu, mem resourceQuery
window time.Duration
}
// nsQueryResults holds the results of one set
// of queries necessary to construct a resource metrics
// API response for a single namespace.
type nsQueryResults struct {
namespace string
cpu, mem queryResults
err error
}
func (p *resourceProvider) GetContainerMetrics(pods ...apitypes.NamespacedName) ([]provider.TimeInfo, [][]metrics.ContainerMetrics, error) {
if len(pods) == 0 {
return nil, nil, fmt.Errorf("no pods to fetch metrics for")
}
// TODO(directxman12): figure out how well this scales if we go to list 1000+ pods
// (and consider adding timeouts)
// group pods by namespace (we could be listing for all pods in the cluster)
podsByNs := make(map[string][]string, len(pods))
for _, pod := range pods {
podsByNs[pod.Namespace] = append(podsByNs[pod.Namespace], pod.Name)
}
// actually fetch the results for each namespace
now := pmodel.Now()
resChan := make(chan nsQueryResults, len(podsByNs))
var wg sync.WaitGroup
wg.Add(len(podsByNs))
for ns, podNames := range podsByNs {
go func(ns string, podNames []string) {
defer wg.Done()
resChan <- p.queryBoth(now, podResource, ns, podNames...)
}(ns, podNames)
}
wg.Wait()
close(resChan)
// index those results in a map for easy lookup
resultsByNs := make(map[string]nsQueryResults, len(podsByNs))
for result := range resChan {
if result.err != nil {
glog.Errorf("unable to fetch metrics for pods in namespace %q, skipping: %v", result.namespace, result.err)
continue
}
resultsByNs[result.namespace] = result
}
// convert the unorganized per-container results into results grouped
// together by namespace, pod, and container
resTimes := make([]provider.TimeInfo, len(pods))
resMetrics := make([][]metrics.ContainerMetrics, len(pods))
for i, pod := range pods {
p.assignForPod(pod, resultsByNs, &resMetrics[i], &resTimes[i])
}
return resTimes, resMetrics, nil
}
// assignForPod takes the resource metrics for all containers in the given pod
// from resultsByNs, and places them in MetricsProvider response format in resMetrics,
// also recording the earliest time in resTime. It will return without operating if
// any data is missing.
func (p *resourceProvider) assignForPod(pod apitypes.NamespacedName, resultsByNs map[string]nsQueryResults, resMetrics *[]metrics.ContainerMetrics, resTime *provider.TimeInfo) {
// check to make sure everything is present
nsRes, nsResPresent := resultsByNs[pod.Namespace]
if !nsResPresent {
glog.Errorf("unable to fetch metrics for pods in namespace %q, skipping pod %s", pod.Namespace, pod.String())
return
}
cpuRes, hasResult := nsRes.cpu[pod.Name]
if !hasResult {
glog.Errorf("unable to fetch CPU metrics for pod %s, skipping", pod.String())
return
}
memRes, hasResult := nsRes.mem[pod.Name]
if !hasResult {
glog.Errorf("unable to fetch memory metrics for pod %s, skipping", pod.String())
return
}
earliestTs := pmodel.Latest
containerMetrics := make(map[string]metrics.ContainerMetrics)
// organize all the CPU results
for _, cpu := range cpuRes {
containerName := string(cpu.Metric[pmodel.LabelName(p.cpu.containerLabel)])
if _, present := containerMetrics[containerName]; !present {
containerMetrics[containerName] = metrics.ContainerMetrics{
Name: containerName,
Usage: corev1.ResourceList{},
}
}
containerMetrics[containerName].Usage[corev1.ResourceCPU] = *resource.NewMilliQuantity(int64(cpu.Value*1000.0), resource.DecimalSI)
if cpu.Timestamp.Before(earliestTs) {
earliestTs = cpu.Timestamp
}
}
// organize the memory results
for _, mem := range memRes {
containerName := string(mem.Metric[pmodel.LabelName(p.mem.containerLabel)])
if _, present := containerMetrics[containerName]; !present {
containerMetrics[containerName] = metrics.ContainerMetrics{
Name: containerName,
Usage: corev1.ResourceList{},
}
}
containerMetrics[containerName].Usage[corev1.ResourceMemory] = *resource.NewMilliQuantity(int64(mem.Value*1000.0), resource.BinarySI)
if mem.Timestamp.Before(earliestTs) {
earliestTs = mem.Timestamp
}
}
// store the time in the final format
*resTime = provider.TimeInfo{
Timestamp: earliestTs.Time(),
Window: p.window,
}
// store the container metrics in the final format
containerMetricsList := make([]metrics.ContainerMetrics, 0, len(containerMetrics))
for _, containerMetric := range containerMetrics {
containerMetricsList = append(containerMetricsList, containerMetric)
}
*resMetrics = containerMetricsList
}
func (p *resourceProvider) GetNodeMetrics(nodes ...string) ([]provider.TimeInfo, []corev1.ResourceList, error) {
if len(nodes) == 0 {
return nil, nil, fmt.Errorf("no nodes to fetch metrics for")
}
now := pmodel.Now()
// run the actual query
qRes := p.queryBoth(now, nodeResource, "", nodes...)
if qRes.err != nil {
return nil, nil, qRes.err
}
resTimes := make([]provider.TimeInfo, len(nodes))
resMetrics := make([]corev1.ResourceList, len(nodes))
// organize the results
for i, nodeName := range nodes {
// skip if any data is missing
rawCPUs, gotResult := qRes.cpu[nodeName]
if !gotResult {
glog.V(1).Infof("missing CPU for node %q, skipping", nodeName)
continue
}
rawMems, gotResult := qRes.mem[nodeName]
if !gotResult {
glog.V(1).Infof("missing memory for node %q, skipping", nodeName)
continue
}
rawMem := rawMems[0]
rawCPU := rawCPUs[0]
// store the results
resMetrics[i] = corev1.ResourceList{
corev1.ResourceCPU: *resource.NewMilliQuantity(int64(rawCPU.Value*1000.0), resource.DecimalSI),
corev1.ResourceMemory: *resource.NewMilliQuantity(int64(rawMem.Value*1000.0), resource.BinarySI),
}
// use the earliest timestamp available (in order to be conservative
// when determining if metrics are tainted by startup)
if rawMem.Timestamp.Before(rawCPU.Timestamp) {
resTimes[i] = provider.TimeInfo{
Timestamp: rawMem.Timestamp.Time(),
Window: p.window,
}
} else {
resTimes[i] = provider.TimeInfo{
Timestamp: rawCPU.Timestamp.Time(),
Window: 1 * time.Minute,
}
}
}
return resTimes, resMetrics, nil
}
// queryBoth queries for both CPU and memory metrics on the given
// Kubernetes API resource (pods or nodes), and errors out if
// either query fails.
func (p *resourceProvider) queryBoth(now pmodel.Time, resource schema.GroupResource, namespace string, names ...string) nsQueryResults {
var cpuRes, memRes queryResults
var cpuErr, memErr error
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
cpuRes, cpuErr = p.runQuery(now, p.cpu, resource, namespace, names...)
}()
go func() {
defer wg.Done()
memRes, memErr = p.runQuery(now, p.mem, resource, namespace, names...)
}()
wg.Wait()
if cpuErr != nil {
return nsQueryResults{
namespace: namespace,
err: fmt.Errorf("unable to fetch node CPU metrics: %v", cpuErr),
}
}
if memErr != nil {
return nsQueryResults{
namespace: namespace,
err: fmt.Errorf("unable to fetch node memory metrics: %v", memErr),
}
}
return nsQueryResults{
namespace: namespace,
cpu: cpuRes,
mem: memRes,
}
}
// queryResults maps an object name to all the results matching that object
type queryResults map[string][]*pmodel.Sample
// runQuery actually queries Prometheus for the metric represented by the given query information, on
// the given Kubernetes API resource (pods or nodes).
func (p *resourceProvider) runQuery(now pmodel.Time, queryInfo resourceQuery, resource schema.GroupResource, namespace string, names ...string) (queryResults, error) {
var query client.Selector
var err error
// build the query, which needs the special "container" group by if this is for pod metrics
if resource == nodeResource {
query, err = queryInfo.nodeQuery.Build("", resource, namespace, nil, names...)
} else {
extraGroupBy := []string{queryInfo.containerLabel}
query, err = queryInfo.contQuery.Build("", resource, namespace, extraGroupBy, names...)
}
if err != nil {
return nil, fmt.Errorf("unable to construct query: %v", err)
}
// run the query
rawRes, err := p.prom.Query(context.Background(), now, query)
if err != nil {
return nil, fmt.Errorf("unable to execute query: %v", err)
}
if rawRes.Type != pmodel.ValVector || rawRes.Vector == nil {
return nil, fmt.Errorf("invalid or empty value of non-vector type (%s) returned", rawRes.Type)
}
// check the appropriate label for the resource in question
resourceLbl, err := queryInfo.converter.LabelForResource(resource)
if err != nil {
return nil, fmt.Errorf("unable to find label for resource %s: %v", resource.String(), err)
}
// associate the results back to each given pod or node
res := make(queryResults, len(*rawRes.Vector))
for _, val := range *rawRes.Vector {
if val == nil {
// skip empty values
continue
}
resKey := string(val.Metric[resourceLbl])
res[resKey] = append(res[resKey], val)
}
return res, nil
}

View file

@ -0,0 +1,29 @@
/*
Copyright 2018 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 resourceprovider
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestProvider(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Resource Metrics Provider Suite")
}

View file

@ -0,0 +1,265 @@
/*
Copyright 2018 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 resourceprovider
import (
"time"
"github.com/kubernetes-incubator/metrics-server/pkg/provider"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/metrics/pkg/apis/metrics"
config "github.com/directxman12/k8s-prometheus-adapter/cmd/config-gen/utils"
prom "github.com/directxman12/k8s-prometheus-adapter/pkg/client"
fakeprom "github.com/directxman12/k8s-prometheus-adapter/pkg/client/fake"
pmodel "github.com/prometheus/common/model"
)
func restMapper() apimeta.RESTMapper {
mapper := apimeta.NewDefaultRESTMapper([]schema.GroupVersion{corev1.SchemeGroupVersion})
mapper.Add(corev1.SchemeGroupVersion.WithKind("Pod"), apimeta.RESTScopeNamespace)
mapper.Add(corev1.SchemeGroupVersion.WithKind("Node"), apimeta.RESTScopeRoot)
mapper.Add(corev1.SchemeGroupVersion.WithKind("Namespace"), apimeta.RESTScopeRoot)
return mapper
}
func buildPodSample(namespace, pod, container string, val float64, ts int64) *pmodel.Sample {
return &pmodel.Sample{
Metric: pmodel.Metric{
"namespace": pmodel.LabelValue(namespace),
"pod_name": pmodel.LabelValue(pod),
"container_name": pmodel.LabelValue(container),
},
Value: pmodel.SampleValue(val),
Timestamp: pmodel.Time(ts),
}
}
func buildNodeSample(node string, val float64, ts int64) *pmodel.Sample {
return &pmodel.Sample{
Metric: pmodel.Metric{
"instance": pmodel.LabelValue(node),
"id": "/",
},
Value: pmodel.SampleValue(val),
Timestamp: pmodel.Time(ts),
}
}
func buildQueryRes(metric string, samples ...*pmodel.Sample) prom.QueryResult {
for _, sample := range samples {
sample.Metric[pmodel.MetricNameLabel] = pmodel.LabelValue(metric)
}
vec := pmodel.Vector(samples)
return prom.QueryResult{
Type: pmodel.ValVector,
Vector: &vec,
}
}
func mustBuild(sel prom.Selector, err error) prom.Selector {
Expect(err).NotTo(HaveOccurred())
return sel
}
func buildResList(cpu, memory float64) corev1.ResourceList {
return corev1.ResourceList{
corev1.ResourceCPU: *resource.NewMilliQuantity(int64(cpu*1000.0), resource.DecimalSI),
corev1.ResourceMemory: *resource.NewMilliQuantity(int64(memory*1000.0), resource.BinarySI),
}
}
var _ = Describe("Resource Metrics Provider", func() {
var (
prov provider.MetricsProvider
fakeProm *fakeprom.FakePrometheusClient
cpuQueries, memQueries resourceQuery
)
BeforeEach(func() {
By("setting up a fake prometheus client and provider")
mapper := restMapper()
cfg := config.DefaultConfig(1*time.Minute, "")
var err error
cpuQueries, err = newResourceQuery(cfg.ResourceRules.CPU, mapper)
Expect(err).NotTo(HaveOccurred())
memQueries, err = newResourceQuery(cfg.ResourceRules.Memory, mapper)
Expect(err).NotTo(HaveOccurred())
fakeProm = &fakeprom.FakePrometheusClient{}
fakeProm.AcceptableInterval = pmodel.Interval{End: pmodel.Latest}
prov, err = NewProvider(fakeProm, restMapper(), cfg.ResourceRules)
Expect(err).NotTo(HaveOccurred())
})
It("should be able to list metrics pods across different namespaces", func() {
pods := []types.NamespacedName{
{Namespace: "some-ns", Name: "pod1"},
{Namespace: "some-ns", Name: "pod3"},
{Namespace: "other-ns", Name: "pod27"},
}
fakeProm.QueryResults = map[prom.Selector]prom.QueryResult{
mustBuild(cpuQueries.contQuery.Build("", podResource, "some-ns", []string{cpuQueries.containerLabel}, "pod1", "pod3")): buildQueryRes("container_cpu_usage_seconds_total",
buildPodSample("some-ns", "pod1", "cont1", 1100.0, 10),
buildPodSample("some-ns", "pod1", "cont2", 1110.0, 20),
buildPodSample("some-ns", "pod3", "cont1", 1300.0, 10),
buildPodSample("some-ns", "pod3", "cont2", 1310.0, 20),
),
mustBuild(cpuQueries.contQuery.Build("", podResource, "other-ns", []string{cpuQueries.containerLabel}, "pod27")): buildQueryRes("container_cpu_usage_seconds_total",
buildPodSample("other-ns", "pod27", "cont1", 2200.0, 270),
),
mustBuild(memQueries.contQuery.Build("", podResource, "some-ns", []string{cpuQueries.containerLabel}, "pod1", "pod3")): buildQueryRes("container_memory_working_set_bytes",
buildPodSample("some-ns", "pod1", "cont1", 3100.0, 11),
buildPodSample("some-ns", "pod1", "cont2", 3110.0, 21),
buildPodSample("some-ns", "pod3", "cont1", 3300.0, 11),
buildPodSample("some-ns", "pod3", "cont2", 3310.0, 21),
),
mustBuild(memQueries.contQuery.Build("", podResource, "other-ns", []string{cpuQueries.containerLabel}, "pod27")): buildQueryRes("container_memory_working_set_bytes",
buildPodSample("other-ns", "pod27", "cont1", 4200.0, 271),
),
}
By("querying for metrics for some pods")
times, metricVals, err := prov.GetContainerMetrics(pods...)
Expect(err).NotTo(HaveOccurred())
By("verifying that the reported times for each are the earliest times for each pod")
Expect(times).To(Equal([]provider.TimeInfo{
{Timestamp: pmodel.Time(10).Time(), Window: 1 * time.Minute},
{Timestamp: pmodel.Time(10).Time(), Window: 1 * time.Minute},
{Timestamp: pmodel.Time(270).Time(), Window: 1 * time.Minute},
}))
By("verifying that the right metrics were fetched")
Expect(metricVals).To(HaveLen(3))
Expect(metricVals[0]).To(ConsistOf(
metrics.ContainerMetrics{Name: "cont1", Usage: buildResList(1100.0, 3100.0)},
metrics.ContainerMetrics{Name: "cont2", Usage: buildResList(1110.0, 3110.0)},
))
Expect(metricVals[1]).To(ConsistOf(
metrics.ContainerMetrics{Name: "cont1", Usage: buildResList(1300.0, 3300.0)},
metrics.ContainerMetrics{Name: "cont2", Usage: buildResList(1310.0, 3310.0)},
))
Expect(metricVals[2]).To(ConsistOf(
metrics.ContainerMetrics{Name: "cont1", Usage: buildResList(2200.0, 4200.0)},
))
})
It("should return nil metrics for missing pods, but still return partial results", func() {
fakeProm.QueryResults = map[prom.Selector]prom.QueryResult{
mustBuild(cpuQueries.contQuery.Build("", podResource, "some-ns", []string{cpuQueries.containerLabel}, "pod1", "pod-nonexistant")): buildQueryRes("container_cpu_usage_seconds_total",
buildPodSample("some-ns", "pod1", "cont1", 1100.0, 10),
buildPodSample("some-ns", "pod1", "cont2", 1110.0, 20),
),
mustBuild(memQueries.contQuery.Build("", podResource, "some-ns", []string{cpuQueries.containerLabel}, "pod1", "pod-nonexistant")): buildQueryRes("container_memory_working_set_bytes",
buildPodSample("some-ns", "pod1", "cont1", 3100.0, 11),
buildPodSample("some-ns", "pod1", "cont2", 3110.0, 21),
),
}
By("querying for metrics for some pods, one of which is missing")
times, metricVals, err := prov.GetContainerMetrics(
types.NamespacedName{Namespace: "some-ns", Name: "pod1"},
types.NamespacedName{Namespace: "some-ns", Name: "pod-nonexistant"},
)
Expect(err).NotTo(HaveOccurred())
By("verifying that the missing pod had nil metrics")
Expect(metricVals).To(HaveLen(2))
Expect(metricVals[1]).To(BeNil())
By("verifying that the rest of time metrics and times are correct")
Expect(metricVals[0]).To(ConsistOf(
metrics.ContainerMetrics{Name: "cont1", Usage: buildResList(1100.0, 3100.0)},
metrics.ContainerMetrics{Name: "cont2", Usage: buildResList(1110.0, 3110.0)},
))
Expect(times).To(HaveLen(2))
Expect(times[0]).To(Equal(provider.TimeInfo{Timestamp: pmodel.Time(10).Time(), Window: 1 * time.Minute}))
})
It("should be able to list metrics for nodes", func() {
fakeProm.QueryResults = map[prom.Selector]prom.QueryResult{
mustBuild(cpuQueries.nodeQuery.Build("", nodeResource, "", nil, "node1", "node2")): buildQueryRes("container_cpu_usage_seconds_total",
buildNodeSample("node1", 1100.0, 10),
buildNodeSample("node2", 1200.0, 14),
),
mustBuild(memQueries.nodeQuery.Build("", nodeResource, "", nil, "node1", "node2")): buildQueryRes("container_memory_working_set_bytes",
buildNodeSample("node1", 2100.0, 11),
buildNodeSample("node2", 2200.0, 12),
),
}
By("querying for metrics for some nodes")
times, metricVals, err := prov.GetNodeMetrics("node1", "node2")
Expect(err).NotTo(HaveOccurred())
By("verifying that the reported times for each are the earliest times for each pod")
Expect(times).To(Equal([]provider.TimeInfo{
{Timestamp: pmodel.Time(10).Time(), Window: 1 * time.Minute},
{Timestamp: pmodel.Time(12).Time(), Window: 1 * time.Minute},
}))
By("verifying that the right metrics were fetched")
Expect(metricVals).To(Equal([]corev1.ResourceList{
buildResList(1100.0, 2100.0),
buildResList(1200.0, 2200.0),
}))
})
It("should return nil metrics for missing nodes, but still return partial results", func() {
fakeProm.QueryResults = map[prom.Selector]prom.QueryResult{
mustBuild(cpuQueries.nodeQuery.Build("", nodeResource, "", nil, "node1", "node2", "node3")): buildQueryRes("container_cpu_usage_seconds_total",
buildNodeSample("node1", 1100.0, 10),
buildNodeSample("node2", 1200.0, 14),
),
mustBuild(memQueries.nodeQuery.Build("", nodeResource, "", nil, "node1", "node2", "node3")): buildQueryRes("container_memory_working_set_bytes",
buildNodeSample("node1", 2100.0, 11),
buildNodeSample("node2", 2200.0, 12),
),
}
By("querying for metrics for some nodes, one of which is missing")
times, metricVals, err := prov.GetNodeMetrics("node1", "node2", "node3")
Expect(err).NotTo(HaveOccurred())
By("verifying that the missing pod had nil metrics")
Expect(metricVals).To(HaveLen(3))
Expect(metricVals[2]).To(BeNil())
By("verifying that the rest of time metrics and times are correct")
Expect(metricVals).To(Equal([]corev1.ResourceList{
buildResList(1100.0, 2100.0),
buildResList(1200.0, 2200.0),
nil,
}))
Expect(times).To(Equal([]provider.TimeInfo{
{Timestamp: pmodel.Time(10).Time(), Window: 1 * time.Minute},
{Timestamp: pmodel.Time(12).Time(), Window: 1 * time.Minute},
{},
}))
})
})

3
vendor/github.com/hpcloud/tail/.gitignore generated vendored Normal file
View file

@ -0,0 +1,3 @@
.test
.go

18
vendor/github.com/hpcloud/tail/.travis.yml generated vendored Normal file
View file

@ -0,0 +1,18 @@
language: go
script:
- go test -race -v ./...
go:
- 1.4
- 1.5
- 1.6
- tip
matrix:
allow_failures:
- go: tip
install:
- go get gopkg.in/fsnotify.v1
- go get gopkg.in/tomb.v1

63
vendor/github.com/hpcloud/tail/CHANGES.md generated vendored Normal file
View file

@ -0,0 +1,63 @@
# API v1 (gopkg.in/hpcloud/tail.v1)
## April, 2016
* Migrated to godep, as depman is not longer supported
* Introduced golang vendoring feature
* Fixed issue [#57](https://github.com/hpcloud/tail/issues/57) related to reopen deleted file
## July, 2015
* Fix inotify watcher leak; remove `Cleanup` (#51)
# API v0 (gopkg.in/hpcloud/tail.v0)
## June, 2015
* Don't return partial lines (PR #40)
* Use stable version of fsnotify (#46)
## July, 2014
* Fix tail for Windows (PR #36)
## May, 2014
* Improved rate limiting using leaky bucket (PR #29)
* Fix odd line splitting (PR #30)
## Apr, 2014
* LimitRate now discards read buffer (PR #28)
* allow reading of longer lines if MaxLineSize is unset (PR #24)
* updated deps.json to latest fsnotify (441bbc86b1)
## Feb, 2014
* added `Config.Logger` to suppress library logging
## Nov, 2013
* add Cleanup to remove leaky inotify watches (PR #20)
## Aug, 2013
* redesigned Location field (PR #12)
* add tail.Tell (PR #14)
## July, 2013
* Rate limiting (PR #10)
## May, 2013
* Detect file deletions/renames in polling file watcher (PR #1)
* Detect file truncation
* Fix potential race condition when reopening the file (issue 5)
* Fix potential blocking of `tail.Stop` (issue 4)
* Fix uncleaned up ChangeEvents goroutines after calling tail.Stop
* Support Follow=false
## Feb, 2013
* Initial open source release

19
vendor/github.com/hpcloud/tail/Dockerfile generated vendored Normal file
View file

@ -0,0 +1,19 @@
FROM golang
RUN mkdir -p $GOPATH/src/github.com/hpcloud/tail/
ADD . $GOPATH/src/github.com/hpcloud/tail/
# expecting to fetch dependencies successfully.
RUN go get -v github.com/hpcloud/tail
# expecting to run the test successfully.
RUN go test -v github.com/hpcloud/tail
# expecting to install successfully
RUN go install -v github.com/hpcloud/tail
RUN go install -v github.com/hpcloud/tail/cmd/gotail
RUN $GOPATH/bin/gotail -h || true
ENV PATH $GOPATH/bin:$PATH
CMD ["gotail"]

21
vendor/github.com/hpcloud/tail/LICENSE.txt generated vendored Normal file
View file

@ -0,0 +1,21 @@
# The MIT License (MIT)
# © Copyright 2015 Hewlett Packard Enterprise Development LP
Copyright (c) 2014 ActiveState
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.

11
vendor/github.com/hpcloud/tail/Makefile generated vendored Normal file
View file

@ -0,0 +1,11 @@
default: test
test: *.go
go test -v -race ./...
fmt:
gofmt -w .
# Run the test in an isolated environment.
fulltest:
docker build -t hpcloud/tail .

28
vendor/github.com/hpcloud/tail/README.md generated vendored Normal file
View file

@ -0,0 +1,28 @@
[![Build Status](https://travis-ci.org/hpcloud/tail.svg)](https://travis-ci.org/hpcloud/tail)
[![Build status](https://ci.appveyor.com/api/projects/status/kohpsf3rvhjhrox6?svg=true)](https://ci.appveyor.com/project/HelionCloudFoundry/tail)
# Go package for tail-ing files
A Go package striving to emulate the features of the BSD `tail` program.
```Go
t, err := tail.TailFile("/var/log/nginx.log", tail.Config{Follow: true})
for line := range t.Lines {
fmt.Println(line.Text)
}
```
See [API documentation](http://godoc.org/github.com/hpcloud/tail).
## Log rotation
Tail comes with full support for truncation/move detection as it is
designed to work with log rotation tools.
## Installing
go get github.com/hpcloud/tail/...
## Windows support
This package [needs assistance](https://github.com/hpcloud/tail/labels/Windows) for full Windows support.

11
vendor/github.com/hpcloud/tail/appveyor.yml generated vendored Normal file
View file

@ -0,0 +1,11 @@
version: 0.{build}
skip_tags: true
cache: C:\Users\appveyor\AppData\Local\NuGet\Cache
build_script:
- SET GOPATH=c:\workspace
- go test -v -race ./...
test: off
clone_folder: c:\workspace\src\github.com\hpcloud\tail
branches:
only:
- master

7
vendor/github.com/hpcloud/tail/ratelimiter/Licence generated vendored Normal file
View file

@ -0,0 +1,7 @@
Copyright (C) 2013 99designs
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.

View file

@ -0,0 +1,97 @@
// Package ratelimiter implements the Leaky Bucket ratelimiting algorithm with memcached and in-memory backends.
package ratelimiter
import (
"time"
)
type LeakyBucket struct {
Size uint16
Fill float64
LeakInterval time.Duration // time.Duration for 1 unit of size to leak
Lastupdate time.Time
Now func() time.Time
}
func NewLeakyBucket(size uint16, leakInterval time.Duration) *LeakyBucket {
bucket := LeakyBucket{
Size: size,
Fill: 0,
LeakInterval: leakInterval,
Now: time.Now,
Lastupdate: time.Now(),
}
return &bucket
}
func (b *LeakyBucket) updateFill() {
now := b.Now()
if b.Fill > 0 {
elapsed := now.Sub(b.Lastupdate)
b.Fill -= float64(elapsed) / float64(b.LeakInterval)
if b.Fill < 0 {
b.Fill = 0
}
}
b.Lastupdate = now
}
func (b *LeakyBucket) Pour(amount uint16) bool {
b.updateFill()
var newfill float64 = b.Fill + float64(amount)
if newfill > float64(b.Size) {
return false
}
b.Fill = newfill
return true
}
// The time at which this bucket will be completely drained
func (b *LeakyBucket) DrainedAt() time.Time {
return b.Lastupdate.Add(time.Duration(b.Fill * float64(b.LeakInterval)))
}
// The duration until this bucket is completely drained
func (b *LeakyBucket) TimeToDrain() time.Duration {
return b.DrainedAt().Sub(b.Now())
}
func (b *LeakyBucket) TimeSinceLastUpdate() time.Duration {
return b.Now().Sub(b.Lastupdate)
}
type LeakyBucketSer struct {
Size uint16
Fill float64
LeakInterval time.Duration // time.Duration for 1 unit of size to leak
Lastupdate time.Time
}
func (b *LeakyBucket) Serialise() *LeakyBucketSer {
bucket := LeakyBucketSer{
Size: b.Size,
Fill: b.Fill,
LeakInterval: b.LeakInterval,
Lastupdate: b.Lastupdate,
}
return &bucket
}
func (b *LeakyBucketSer) DeSerialise() *LeakyBucket {
bucket := LeakyBucket{
Size: b.Size,
Fill: b.Fill,
LeakInterval: b.LeakInterval,
Lastupdate: b.Lastupdate,
Now: time.Now,
}
return &bucket
}

58
vendor/github.com/hpcloud/tail/ratelimiter/memory.go generated vendored Normal file
View file

@ -0,0 +1,58 @@
package ratelimiter
import (
"errors"
"time"
)
const GC_SIZE int = 100
type Memory struct {
store map[string]LeakyBucket
lastGCCollected time.Time
}
func NewMemory() *Memory {
m := new(Memory)
m.store = make(map[string]LeakyBucket)
m.lastGCCollected = time.Now()
return m
}
func (m *Memory) GetBucketFor(key string) (*LeakyBucket, error) {
bucket, ok := m.store[key]
if !ok {
return nil, errors.New("miss")
}
return &bucket, nil
}
func (m *Memory) SetBucketFor(key string, bucket LeakyBucket) error {
if len(m.store) > GC_SIZE {
m.GarbageCollect()
}
m.store[key] = bucket
return nil
}
func (m *Memory) GarbageCollect() {
now := time.Now()
// rate limit GC to once per minute
if now.Add(60*time.Second).Unix() > m.lastGCCollected.Unix() {
for key, bucket := range m.store {
// if the bucket is drained, then GC
if bucket.DrainedAt().Unix() > now.Unix() {
delete(m.store, key)
}
}
m.lastGCCollected = now
}
}

View file

@ -0,0 +1,6 @@
package ratelimiter
type Storage interface {
GetBucketFor(string) (*LeakyBucket, error)
SetBucketFor(string, LeakyBucket) error
}

438
vendor/github.com/hpcloud/tail/tail.go generated vendored Normal file
View file

@ -0,0 +1,438 @@
// Copyright (c) 2015 HPE Software Inc. All rights reserved.
// Copyright (c) 2013 ActiveState Software Inc. All rights reserved.
package tail
import (
"bufio"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"strings"
"sync"
"time"
"github.com/hpcloud/tail/ratelimiter"
"github.com/hpcloud/tail/util"
"github.com/hpcloud/tail/watch"
"gopkg.in/tomb.v1"
)
var (
ErrStop = fmt.Errorf("tail should now stop")
)
type Line struct {
Text string
Time time.Time
Err error // Error from tail
}
// NewLine returns a Line with present time.
func NewLine(text string) *Line {
return &Line{text, time.Now(), nil}
}
// SeekInfo represents arguments to `os.Seek`
type SeekInfo struct {
Offset int64
Whence int // os.SEEK_*
}
type logger interface {
Fatal(v ...interface{})
Fatalf(format string, v ...interface{})
Fatalln(v ...interface{})
Panic(v ...interface{})
Panicf(format string, v ...interface{})
Panicln(v ...interface{})
Print(v ...interface{})
Printf(format string, v ...interface{})
Println(v ...interface{})
}
// Config is used to specify how a file must be tailed.
type Config struct {
// File-specifc
Location *SeekInfo // Seek to this location before tailing
ReOpen bool // Reopen recreated files (tail -F)
MustExist bool // Fail early if the file does not exist
Poll bool // Poll for file changes instead of using inotify
Pipe bool // Is a named pipe (mkfifo)
RateLimiter *ratelimiter.LeakyBucket
// Generic IO
Follow bool // Continue looking for new lines (tail -f)
MaxLineSize int // If non-zero, split longer lines into multiple lines
// Logger, when nil, is set to tail.DefaultLogger
// To disable logging: set field to tail.DiscardingLogger
Logger logger
}
type Tail struct {
Filename string
Lines chan *Line
Config
file *os.File
reader *bufio.Reader
watcher watch.FileWatcher
changes *watch.FileChanges
tomb.Tomb // provides: Done, Kill, Dying
lk sync.Mutex
}
var (
// DefaultLogger is used when Config.Logger == nil
DefaultLogger = log.New(os.Stderr, "", log.LstdFlags)
// DiscardingLogger can be used to disable logging output
DiscardingLogger = log.New(ioutil.Discard, "", 0)
)
// TailFile begins tailing the file. Output stream is made available
// via the `Tail.Lines` channel. To handle errors during tailing,
// invoke the `Wait` or `Err` method after finishing reading from the
// `Lines` channel.
func TailFile(filename string, config Config) (*Tail, error) {
if config.ReOpen && !config.Follow {
util.Fatal("cannot set ReOpen without Follow.")
}
t := &Tail{
Filename: filename,
Lines: make(chan *Line),
Config: config,
}
// when Logger was not specified in config, use default logger
if t.Logger == nil {
t.Logger = log.New(os.Stderr, "", log.LstdFlags)
}
if t.Poll {
t.watcher = watch.NewPollingFileWatcher(filename)
} else {
t.watcher = watch.NewInotifyFileWatcher(filename)
}
if t.MustExist {
var err error
t.file, err = OpenFile(t.Filename)
if err != nil {
return nil, err
}
}
go t.tailFileSync()
return t, nil
}
// Return the file's current position, like stdio's ftell().
// But this value is not very accurate.
// it may readed one line in the chan(tail.Lines),
// so it may lost one line.
func (tail *Tail) Tell() (offset int64, err error) {
if tail.file == nil {
return
}
offset, err = tail.file.Seek(0, os.SEEK_CUR)
if err != nil {
return
}
tail.lk.Lock()
defer tail.lk.Unlock()
if tail.reader == nil {
return
}
offset -= int64(tail.reader.Buffered())
return
}
// Stop stops the tailing activity.
func (tail *Tail) Stop() error {
tail.Kill(nil)
return tail.Wait()
}
// StopAtEOF stops tailing as soon as the end of the file is reached.
func (tail *Tail) StopAtEOF() error {
tail.Kill(errStopAtEOF)
return tail.Wait()
}
var errStopAtEOF = errors.New("tail: stop at eof")
func (tail *Tail) close() {
close(tail.Lines)
tail.closeFile()
}
func (tail *Tail) closeFile() {
if tail.file != nil {
tail.file.Close()
tail.file = nil
}
}
func (tail *Tail) reopen() error {
tail.closeFile()
for {
var err error
tail.file, err = OpenFile(tail.Filename)
if err != nil {
if os.IsNotExist(err) {
tail.Logger.Printf("Waiting for %s to appear...", tail.Filename)
if err := tail.watcher.BlockUntilExists(&tail.Tomb); err != nil {
if err == tomb.ErrDying {
return err
}
return fmt.Errorf("Failed to detect creation of %s: %s", tail.Filename, err)
}
continue
}
return fmt.Errorf("Unable to open file %s: %s", tail.Filename, err)
}
break
}
return nil
}
func (tail *Tail) readLine() (string, error) {
tail.lk.Lock()
line, err := tail.reader.ReadString('\n')
tail.lk.Unlock()
if err != nil {
// Note ReadString "returns the data read before the error" in
// case of an error, including EOF, so we return it as is. The
// caller is expected to process it if err is EOF.
return line, err
}
line = strings.TrimRight(line, "\n")
return line, err
}
func (tail *Tail) tailFileSync() {
defer tail.Done()
defer tail.close()
if !tail.MustExist {
// deferred first open.
err := tail.reopen()
if err != nil {
if err != tomb.ErrDying {
tail.Kill(err)
}
return
}
}
// Seek to requested location on first open of the file.
if tail.Location != nil {
_, err := tail.file.Seek(tail.Location.Offset, tail.Location.Whence)
tail.Logger.Printf("Seeked %s - %+v\n", tail.Filename, tail.Location)
if err != nil {
tail.Killf("Seek error on %s: %s", tail.Filename, err)
return
}
}
tail.openReader()
var offset int64 = 0
var err error
// Read line by line.
for {
// do not seek in named pipes
if !tail.Pipe {
// grab the position in case we need to back up in the event of a half-line
offset, err = tail.Tell()
if err != nil {
tail.Kill(err)
return
}
}
line, err := tail.readLine()
// Process `line` even if err is EOF.
if err == nil {
cooloff := !tail.sendLine(line)
if cooloff {
// Wait a second before seeking till the end of
// file when rate limit is reached.
msg := fmt.Sprintf(
"Too much log activity; waiting a second " +
"before resuming tailing")
tail.Lines <- &Line{msg, time.Now(), fmt.Errorf(msg)}
select {
case <-time.After(time.Second):
case <-tail.Dying():
return
}
if err := tail.seekEnd(); err != nil {
tail.Kill(err)
return
}
}
} else if err == io.EOF {
if !tail.Follow {
if line != "" {
tail.sendLine(line)
}
return
}
if tail.Follow && line != "" {
// this has the potential to never return the last line if
// it's not followed by a newline; seems a fair trade here
err := tail.seekTo(SeekInfo{Offset: offset, Whence: 0})
if err != nil {
tail.Kill(err)
return
}
}
// When EOF is reached, wait for more data to become
// available. Wait strategy is based on the `tail.watcher`
// implementation (inotify or polling).
err := tail.waitForChanges()
if err != nil {
if err != ErrStop {
tail.Kill(err)
}
return
}
} else {
// non-EOF error
tail.Killf("Error reading %s: %s", tail.Filename, err)
return
}
select {
case <-tail.Dying():
if tail.Err() == errStopAtEOF {
continue
}
return
default:
}
}
}
// waitForChanges waits until the file has been appended, deleted,
// moved or truncated. When moved or deleted - the file will be
// reopened if ReOpen is true. Truncated files are always reopened.
func (tail *Tail) waitForChanges() error {
if tail.changes == nil {
pos, err := tail.file.Seek(0, os.SEEK_CUR)
if err != nil {
return err
}
tail.changes, err = tail.watcher.ChangeEvents(&tail.Tomb, pos)
if err != nil {
return err
}
}
select {
case <-tail.changes.Modified:
return nil
case <-tail.changes.Deleted:
tail.changes = nil
if tail.ReOpen {
// XXX: we must not log from a library.
tail.Logger.Printf("Re-opening moved/deleted file %s ...", tail.Filename)
if err := tail.reopen(); err != nil {
return err
}
tail.Logger.Printf("Successfully reopened %s", tail.Filename)
tail.openReader()
return nil
} else {
tail.Logger.Printf("Stopping tail as file no longer exists: %s", tail.Filename)
return ErrStop
}
case <-tail.changes.Truncated:
// Always reopen truncated files (Follow is true)
tail.Logger.Printf("Re-opening truncated file %s ...", tail.Filename)
if err := tail.reopen(); err != nil {
return err
}
tail.Logger.Printf("Successfully reopened truncated %s", tail.Filename)
tail.openReader()
return nil
case <-tail.Dying():
return ErrStop
}
panic("unreachable")
}
func (tail *Tail) openReader() {
if tail.MaxLineSize > 0 {
// add 2 to account for newline characters
tail.reader = bufio.NewReaderSize(tail.file, tail.MaxLineSize+2)
} else {
tail.reader = bufio.NewReader(tail.file)
}
}
func (tail *Tail) seekEnd() error {
return tail.seekTo(SeekInfo{Offset: 0, Whence: os.SEEK_END})
}
func (tail *Tail) seekTo(pos SeekInfo) error {
_, err := tail.file.Seek(pos.Offset, pos.Whence)
if err != nil {
return fmt.Errorf("Seek error on %s: %s", tail.Filename, err)
}
// Reset the read buffer whenever the file is re-seek'ed
tail.reader.Reset(tail.file)
return nil
}
// sendLine sends the line(s) to Lines channel, splitting longer lines
// if necessary. Return false if rate limit is reached.
func (tail *Tail) sendLine(line string) bool {
now := time.Now()
lines := []string{line}
// Split longer lines
if tail.MaxLineSize > 0 && len(line) > tail.MaxLineSize {
lines = util.PartitionString(line, tail.MaxLineSize)
}
for _, line := range lines {
tail.Lines <- &Line{line, now, nil}
}
if tail.Config.RateLimiter != nil {
ok := tail.Config.RateLimiter.Pour(uint16(len(lines)))
if !ok {
tail.Logger.Printf("Leaky bucket full (%v); entering 1s cooloff period.\n",
tail.Filename)
return false
}
}
return true
}
// Cleanup removes inotify watches added by the tail package. This function is
// meant to be invoked from a process's exit handler. Linux kernel may not
// automatically remove inotify watches after the process exits.
func (tail *Tail) Cleanup() {
watch.Cleanup(tail.Filename)
}

11
vendor/github.com/hpcloud/tail/tail_posix.go generated vendored Normal file
View file

@ -0,0 +1,11 @@
// +build linux darwin freebsd netbsd openbsd
package tail
import (
"os"
)
func OpenFile(name string) (file *os.File, err error) {
return os.Open(name)
}

12
vendor/github.com/hpcloud/tail/tail_windows.go generated vendored Normal file
View file

@ -0,0 +1,12 @@
// +build windows
package tail
import (
"github.com/hpcloud/tail/winfile"
"os"
)
func OpenFile(name string) (file *os.File, err error) {
return winfile.OpenFile(name, os.O_RDONLY, 0)
}

48
vendor/github.com/hpcloud/tail/util/util.go generated vendored Normal file
View file

@ -0,0 +1,48 @@
// Copyright (c) 2015 HPE Software Inc. All rights reserved.
// Copyright (c) 2013 ActiveState Software Inc. All rights reserved.
package util
import (
"fmt"
"log"
"os"
"runtime/debug"
)
type Logger struct {
*log.Logger
}
var LOGGER = &Logger{log.New(os.Stderr, "", log.LstdFlags)}
// fatal is like panic except it displays only the current goroutine's stack.
func Fatal(format string, v ...interface{}) {
// https://github.com/hpcloud/log/blob/master/log.go#L45
LOGGER.Output(2, fmt.Sprintf("FATAL -- "+format, v...)+"\n"+string(debug.Stack()))
os.Exit(1)
}
// partitionString partitions the string into chunks of given size,
// with the last chunk of variable size.
func PartitionString(s string, chunkSize int) []string {
if chunkSize <= 0 {
panic("invalid chunkSize")
}
length := len(s)
chunks := 1 + length/chunkSize
start := 0
end := chunkSize
parts := make([]string, 0, chunks)
for {
if end > length {
end = length
}
parts = append(parts, s[start:end])
if end == length {
break
}
start, end = end, end+chunkSize
}
return parts
}

36
vendor/github.com/hpcloud/tail/watch/filechanges.go generated vendored Normal file
View file

@ -0,0 +1,36 @@
package watch
type FileChanges struct {
Modified chan bool // Channel to get notified of modifications
Truncated chan bool // Channel to get notified of truncations
Deleted chan bool // Channel to get notified of deletions/renames
}
func NewFileChanges() *FileChanges {
return &FileChanges{
make(chan bool), make(chan bool), make(chan bool)}
}
func (fc *FileChanges) NotifyModified() {
sendOnlyIfEmpty(fc.Modified)
}
func (fc *FileChanges) NotifyTruncated() {
sendOnlyIfEmpty(fc.Truncated)
}
func (fc *FileChanges) NotifyDeleted() {
sendOnlyIfEmpty(fc.Deleted)
}
// sendOnlyIfEmpty sends on a bool channel only if the channel has no
// backlog to be read by other goroutines. This concurrency pattern
// can be used to notify other goroutines if and only if they are
// looking for it (i.e., subsequent notifications can be compressed
// into one).
func sendOnlyIfEmpty(ch chan bool) {
select {
case ch <- true:
default:
}
}

128
vendor/github.com/hpcloud/tail/watch/inotify.go generated vendored Normal file
View file

@ -0,0 +1,128 @@
// Copyright (c) 2015 HPE Software Inc. All rights reserved.
// Copyright (c) 2013 ActiveState Software Inc. All rights reserved.
package watch
import (
"fmt"
"os"
"path/filepath"
"github.com/hpcloud/tail/util"
"gopkg.in/fsnotify.v1"
"gopkg.in/tomb.v1"
)
// InotifyFileWatcher uses inotify to monitor file changes.
type InotifyFileWatcher struct {
Filename string
Size int64
}
func NewInotifyFileWatcher(filename string) *InotifyFileWatcher {
fw := &InotifyFileWatcher{filepath.Clean(filename), 0}
return fw
}
func (fw *InotifyFileWatcher) BlockUntilExists(t *tomb.Tomb) error {
err := WatchCreate(fw.Filename)
if err != nil {
return err
}
defer RemoveWatchCreate(fw.Filename)
// Do a real check now as the file might have been created before
// calling `WatchFlags` above.
if _, err = os.Stat(fw.Filename); !os.IsNotExist(err) {
// file exists, or stat returned an error.
return err
}
events := Events(fw.Filename)
for {
select {
case evt, ok := <-events:
if !ok {
return fmt.Errorf("inotify watcher has been closed")
}
evtName, err := filepath.Abs(evt.Name)
if err != nil {
return err
}
fwFilename, err := filepath.Abs(fw.Filename)
if err != nil {
return err
}
if evtName == fwFilename {
return nil
}
case <-t.Dying():
return tomb.ErrDying
}
}
panic("unreachable")
}
func (fw *InotifyFileWatcher) ChangeEvents(t *tomb.Tomb, pos int64) (*FileChanges, error) {
err := Watch(fw.Filename)
if err != nil {
return nil, err
}
changes := NewFileChanges()
fw.Size = pos
go func() {
defer RemoveWatch(fw.Filename)
events := Events(fw.Filename)
for {
prevSize := fw.Size
var evt fsnotify.Event
var ok bool
select {
case evt, ok = <-events:
if !ok {
return
}
case <-t.Dying():
return
}
switch {
case evt.Op&fsnotify.Remove == fsnotify.Remove:
fallthrough
case evt.Op&fsnotify.Rename == fsnotify.Rename:
changes.NotifyDeleted()
return
case evt.Op&fsnotify.Write == fsnotify.Write:
fi, err := os.Stat(fw.Filename)
if err != nil {
if os.IsNotExist(err) {
changes.NotifyDeleted()
return
}
// XXX: report this error back to the user
util.Fatal("Failed to stat file %v: %v", fw.Filename, err)
}
fw.Size = fi.Size()
if prevSize > 0 && prevSize > fw.Size {
changes.NotifyTruncated()
} else {
changes.NotifyModified()
}
prevSize = fw.Size
}
}
}()
return changes, nil
}

260
vendor/github.com/hpcloud/tail/watch/inotify_tracker.go generated vendored Normal file
View file

@ -0,0 +1,260 @@
// Copyright (c) 2015 HPE Software Inc. All rights reserved.
// Copyright (c) 2013 ActiveState Software Inc. All rights reserved.
package watch
import (
"log"
"os"
"path/filepath"
"sync"
"syscall"
"github.com/hpcloud/tail/util"
"gopkg.in/fsnotify.v1"
)
type InotifyTracker struct {
mux sync.Mutex
watcher *fsnotify.Watcher
chans map[string]chan fsnotify.Event
done map[string]chan bool
watchNums map[string]int
watch chan *watchInfo
remove chan *watchInfo
error chan error
}
type watchInfo struct {
op fsnotify.Op
fname string
}
func (this *watchInfo) isCreate() bool {
return this.op == fsnotify.Create
}
var (
// globally shared InotifyTracker; ensures only one fsnotify.Watcher is used
shared *InotifyTracker
// these are used to ensure the shared InotifyTracker is run exactly once
once = sync.Once{}
goRun = func() {
shared = &InotifyTracker{
mux: sync.Mutex{},
chans: make(map[string]chan fsnotify.Event),
done: make(map[string]chan bool),
watchNums: make(map[string]int),
watch: make(chan *watchInfo),
remove: make(chan *watchInfo),
error: make(chan error),
}
go shared.run()
}
logger = log.New(os.Stderr, "", log.LstdFlags)
)
// Watch signals the run goroutine to begin watching the input filename
func Watch(fname string) error {
return watch(&watchInfo{
fname: fname,
})
}
// Watch create signals the run goroutine to begin watching the input filename
// if call the WatchCreate function, don't call the Cleanup, call the RemoveWatchCreate
func WatchCreate(fname string) error {
return watch(&watchInfo{
op: fsnotify.Create,
fname: fname,
})
}
func watch(winfo *watchInfo) error {
// start running the shared InotifyTracker if not already running
once.Do(goRun)
winfo.fname = filepath.Clean(winfo.fname)
shared.watch <- winfo
return <-shared.error
}
// RemoveWatch signals the run goroutine to remove the watch for the input filename
func RemoveWatch(fname string) {
remove(&watchInfo{
fname: fname,
})
}
// RemoveWatch create signals the run goroutine to remove the watch for the input filename
func RemoveWatchCreate(fname string) {
remove(&watchInfo{
op: fsnotify.Create,
fname: fname,
})
}
func remove(winfo *watchInfo) {
// start running the shared InotifyTracker if not already running
once.Do(goRun)
winfo.fname = filepath.Clean(winfo.fname)
shared.mux.Lock()
done := shared.done[winfo.fname]
if done != nil {
delete(shared.done, winfo.fname)
close(done)
}
fname := winfo.fname
if winfo.isCreate() {
// Watch for new files to be created in the parent directory.
fname = filepath.Dir(fname)
}
shared.watchNums[fname]--
watchNum := shared.watchNums[fname]
if watchNum == 0 {
delete(shared.watchNums, fname)
}
shared.mux.Unlock()
// If we were the last ones to watch this file, unsubscribe from inotify.
// This needs to happen after releasing the lock because fsnotify waits
// synchronously for the kernel to acknowledge the removal of the watch
// for this file, which causes us to deadlock if we still held the lock.
if watchNum == 0 {
shared.watcher.Remove(fname)
}
shared.remove <- winfo
}
// Events returns a channel to which FileEvents corresponding to the input filename
// will be sent. This channel will be closed when removeWatch is called on this
// filename.
func Events(fname string) <-chan fsnotify.Event {
shared.mux.Lock()
defer shared.mux.Unlock()
return shared.chans[fname]
}
// Cleanup removes the watch for the input filename if necessary.
func Cleanup(fname string) {
RemoveWatch(fname)
}
// watchFlags calls fsnotify.WatchFlags for the input filename and flags, creating
// a new Watcher if the previous Watcher was closed.
func (shared *InotifyTracker) addWatch(winfo *watchInfo) error {
shared.mux.Lock()
defer shared.mux.Unlock()
if shared.chans[winfo.fname] == nil {
shared.chans[winfo.fname] = make(chan fsnotify.Event)
shared.done[winfo.fname] = make(chan bool)
}
fname := winfo.fname
if winfo.isCreate() {
// Watch for new files to be created in the parent directory.
fname = filepath.Dir(fname)
}
// already in inotify watch
if shared.watchNums[fname] > 0 {
shared.watchNums[fname]++
if winfo.isCreate() {
shared.watchNums[winfo.fname]++
}
return nil
}
err := shared.watcher.Add(fname)
if err == nil {
shared.watchNums[fname]++
if winfo.isCreate() {
shared.watchNums[winfo.fname]++
}
}
return err
}
// removeWatch calls fsnotify.RemoveWatch for the input filename and closes the
// corresponding events channel.
func (shared *InotifyTracker) removeWatch(winfo *watchInfo) {
shared.mux.Lock()
defer shared.mux.Unlock()
ch := shared.chans[winfo.fname]
if ch == nil {
return
}
delete(shared.chans, winfo.fname)
close(ch)
if !winfo.isCreate() {
return
}
shared.watchNums[winfo.fname]--
if shared.watchNums[winfo.fname] == 0 {
delete(shared.watchNums, winfo.fname)
}
}
// sendEvent sends the input event to the appropriate Tail.
func (shared *InotifyTracker) sendEvent(event fsnotify.Event) {
name := filepath.Clean(event.Name)
shared.mux.Lock()
ch := shared.chans[name]
done := shared.done[name]
shared.mux.Unlock()
if ch != nil && done != nil {
select {
case ch <- event:
case <-done:
}
}
}
// run starts the goroutine in which the shared struct reads events from its
// Watcher's Event channel and sends the events to the appropriate Tail.
func (shared *InotifyTracker) run() {
watcher, err := fsnotify.NewWatcher()
if err != nil {
util.Fatal("failed to create Watcher")
}
shared.watcher = watcher
for {
select {
case winfo := <-shared.watch:
shared.error <- shared.addWatch(winfo)
case winfo := <-shared.remove:
shared.removeWatch(winfo)
case event, open := <-shared.watcher.Events:
if !open {
return
}
shared.sendEvent(event)
case err, open := <-shared.watcher.Errors:
if !open {
return
} else if err != nil {
sysErr, ok := err.(*os.SyscallError)
if !ok || sysErr.Err != syscall.EINTR {
logger.Printf("Error in Watcher Error channel: %s", err)
}
}
}
}
}

118
vendor/github.com/hpcloud/tail/watch/polling.go generated vendored Normal file
View file

@ -0,0 +1,118 @@
// Copyright (c) 2015 HPE Software Inc. All rights reserved.
// Copyright (c) 2013 ActiveState Software Inc. All rights reserved.
package watch
import (
"os"
"runtime"
"time"
"github.com/hpcloud/tail/util"
"gopkg.in/tomb.v1"
)
// PollingFileWatcher polls the file for changes.
type PollingFileWatcher struct {
Filename string
Size int64
}
func NewPollingFileWatcher(filename string) *PollingFileWatcher {
fw := &PollingFileWatcher{filename, 0}
return fw
}
var POLL_DURATION time.Duration
func (fw *PollingFileWatcher) BlockUntilExists(t *tomb.Tomb) error {
for {
if _, err := os.Stat(fw.Filename); err == nil {
return nil
} else if !os.IsNotExist(err) {
return err
}
select {
case <-time.After(POLL_DURATION):
continue
case <-t.Dying():
return tomb.ErrDying
}
}
panic("unreachable")
}
func (fw *PollingFileWatcher) ChangeEvents(t *tomb.Tomb, pos int64) (*FileChanges, error) {
origFi, err := os.Stat(fw.Filename)
if err != nil {
return nil, err
}
changes := NewFileChanges()
var prevModTime time.Time
// XXX: use tomb.Tomb to cleanly manage these goroutines. replace
// the fatal (below) with tomb's Kill.
fw.Size = pos
go func() {
prevSize := fw.Size
for {
select {
case <-t.Dying():
return
default:
}
time.Sleep(POLL_DURATION)
fi, err := os.Stat(fw.Filename)
if err != nil {
// Windows cannot delete a file if a handle is still open (tail keeps one open)
// so it gives access denied to anything trying to read it until all handles are released.
if os.IsNotExist(err) || (runtime.GOOS == "windows" && os.IsPermission(err)) {
// File does not exist (has been deleted).
changes.NotifyDeleted()
return
}
// XXX: report this error back to the user
util.Fatal("Failed to stat file %v: %v", fw.Filename, err)
}
// File got moved/renamed?
if !os.SameFile(origFi, fi) {
changes.NotifyDeleted()
return
}
// File got truncated?
fw.Size = fi.Size()
if prevSize > 0 && prevSize > fw.Size {
changes.NotifyTruncated()
prevSize = fw.Size
continue
}
// File got bigger?
if prevSize > 0 && prevSize < fw.Size {
changes.NotifyModified()
prevSize = fw.Size
continue
}
prevSize = fw.Size
// File was appended to (changed)?
modTime := fi.ModTime()
if modTime != prevModTime {
prevModTime = modTime
changes.NotifyModified()
}
}
}()
return changes, nil
}
func init() {
POLL_DURATION = 250 * time.Millisecond
}

20
vendor/github.com/hpcloud/tail/watch/watch.go generated vendored Normal file
View file

@ -0,0 +1,20 @@
// Copyright (c) 2015 HPE Software Inc. All rights reserved.
// Copyright (c) 2013 ActiveState Software Inc. All rights reserved.
package watch
import "gopkg.in/tomb.v1"
// FileWatcher monitors file-level events.
type FileWatcher interface {
// BlockUntilExists blocks until the file comes into existence.
BlockUntilExists(*tomb.Tomb) error
// ChangeEvents reports on changes to a file, be it modification,
// deletion, renames or truncations. Returned FileChanges group of
// channels will be closed, thus become unusable, after a deletion
// or truncation event.
// In order to properly report truncations, ChangeEvents requires
// the caller to pass their current offset in the file.
ChangeEvents(*tomb.Tomb, int64) (*FileChanges, error)
}

92
vendor/github.com/hpcloud/tail/winfile/winfile.go generated vendored Normal file
View file

@ -0,0 +1,92 @@
// +build windows
package winfile
import (
"os"
"syscall"
"unsafe"
)
// issue also described here
//https://codereview.appspot.com/8203043/
// https://github.com/jnwhiteh/golang/blob/master/src/pkg/syscall/syscall_windows.go#L218
func Open(path string, mode int, perm uint32) (fd syscall.Handle, err error) {
if len(path) == 0 {
return syscall.InvalidHandle, syscall.ERROR_FILE_NOT_FOUND
}
pathp, err := syscall.UTF16PtrFromString(path)
if err != nil {
return syscall.InvalidHandle, err
}
var access uint32
switch mode & (syscall.O_RDONLY | syscall.O_WRONLY | syscall.O_RDWR) {
case syscall.O_RDONLY:
access = syscall.GENERIC_READ
case syscall.O_WRONLY:
access = syscall.GENERIC_WRITE
case syscall.O_RDWR:
access = syscall.GENERIC_READ | syscall.GENERIC_WRITE
}
if mode&syscall.O_CREAT != 0 {
access |= syscall.GENERIC_WRITE
}
if mode&syscall.O_APPEND != 0 {
access &^= syscall.GENERIC_WRITE
access |= syscall.FILE_APPEND_DATA
}
sharemode := uint32(syscall.FILE_SHARE_READ | syscall.FILE_SHARE_WRITE | syscall.FILE_SHARE_DELETE)
var sa *syscall.SecurityAttributes
if mode&syscall.O_CLOEXEC == 0 {
sa = makeInheritSa()
}
var createmode uint32
switch {
case mode&(syscall.O_CREAT|syscall.O_EXCL) == (syscall.O_CREAT | syscall.O_EXCL):
createmode = syscall.CREATE_NEW
case mode&(syscall.O_CREAT|syscall.O_TRUNC) == (syscall.O_CREAT | syscall.O_TRUNC):
createmode = syscall.CREATE_ALWAYS
case mode&syscall.O_CREAT == syscall.O_CREAT:
createmode = syscall.OPEN_ALWAYS
case mode&syscall.O_TRUNC == syscall.O_TRUNC:
createmode = syscall.TRUNCATE_EXISTING
default:
createmode = syscall.OPEN_EXISTING
}
h, e := syscall.CreateFile(pathp, access, sharemode, sa, createmode, syscall.FILE_ATTRIBUTE_NORMAL, 0)
return h, e
}
// https://github.com/jnwhiteh/golang/blob/master/src/pkg/syscall/syscall_windows.go#L211
func makeInheritSa() *syscall.SecurityAttributes {
var sa syscall.SecurityAttributes
sa.Length = uint32(unsafe.Sizeof(sa))
sa.InheritHandle = 1
return &sa
}
// https://github.com/jnwhiteh/golang/blob/master/src/pkg/os/file_windows.go#L133
func OpenFile(name string, flag int, perm os.FileMode) (file *os.File, err error) {
r, e := Open(name, flag|syscall.O_CLOEXEC, syscallMode(perm))
if e != nil {
return nil, e
}
return os.NewFile(uintptr(r), name), nil
}
// https://github.com/jnwhiteh/golang/blob/master/src/pkg/os/file_posix.go#L61
func syscallMode(i os.FileMode) (o uint32) {
o |= uint32(i.Perm())
if i&os.ModeSetuid != 0 {
o |= syscall.S_ISUID
}
if i&os.ModeSetgid != 0 {
o |= syscall.S_ISGID
}
if i&os.ModeSticky != 0 {
o |= syscall.S_ISVTX
}
// No mapping for Go's ModeTemporary (plan9 only).
return
}

View file

@ -23,6 +23,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/version" "k8s.io/apimachinery/pkg/version"
genericapiserver "k8s.io/apiserver/pkg/server" genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/client-go/informers"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider" "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider"
cminstall "k8s.io/metrics/pkg/apis/custom_metrics/install" cminstall "k8s.io/metrics/pkg/apis/custom_metrics/install"
@ -69,12 +70,12 @@ type completedConfig struct {
} }
// Complete fills in any fields not set that are required to have valid data. It's mutating the receiver. // Complete fills in any fields not set that are required to have valid data. It's mutating the receiver.
func (c *Config) Complete() completedConfig { func (c *Config) Complete(informers informers.SharedInformerFactory) completedConfig {
c.GenericConfig.Version = &version.Info{ c.GenericConfig.Version = &version.Info{
Major: "1", Major: "1",
Minor: "0", Minor: "0",
} }
return completedConfig{c.GenericConfig.Complete(nil)} return completedConfig{c.GenericConfig.Complete(informers)}
} }
// New returns a new instance of CustomMetricsAdapterServer from the given config. // New returns a new instance of CustomMetricsAdapterServer from the given config.

View file

@ -0,0 +1,283 @@
/*
Copyright 2018 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 cmd
import (
"fmt"
"sync"
"time"
"github.com/spf13/pflag"
apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/apiserver"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/cmd/server"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/dynamicmapper"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider"
)
// AdapterBase provides a base set of functionality for any custom metrics adapter.
// Embed it in a struct containing your options, then:
//
// - Use Flags() to add flags, then call Flags().Parse(os.Argv)
// - Use DynamicClient and RESTMapper to fetch handles to common utilities
// - Use WithCustomMetrics(provider) and WithExternalMetrics(provider) to install metrics providers
// - Use Run(stopChannel) to start the server
//
// All methods on this struct are idempotent except for Run -- they'll perform any
// initialization on the first call, then return the existing object on later calls.
// Methods on this struct are not safe to call from multiple goroutines without
// external synchronization.
type AdapterBase struct {
*server.CustomMetricsAdapterServerOptions
// Name is the name of the API server. It defaults to custom-metrics-adapter
Name string
// RemoteKubeConfigFile specifies the kubeconfig to use to construct
// the dynamic client and RESTMapper. It's set from a flag.
RemoteKubeConfigFile string
// DiscoveryInterval specifies the interval at which to recheck discovery
// information for the discovery RESTMapper. It's set from a flag.
DiscoveryInterval time.Duration
// FlagSet is the flagset to add flags to.
// It defaults to the normal CommandLine flags
// if not explicitly set.
FlagSet *pflag.FlagSet
// flagOnce controls initialization of the flags.
flagOnce sync.Once
clientConfig *rest.Config
discoveryClient discovery.DiscoveryInterface
restMapper apimeta.RESTMapper
dynamicClient dynamic.Interface
informers informers.SharedInformerFactory
config *apiserver.Config
server *apiserver.CustomMetricsAdapterServer
cmProvider provider.CustomMetricsProvider
emProvider provider.ExternalMetricsProvider
}
// InstallFlags installs the minimum required set of flags into the flagset.
func (b *AdapterBase) InstallFlags() {
b.initFlagSet()
b.flagOnce.Do(func() {
if b.CustomMetricsAdapterServerOptions == nil {
b.CustomMetricsAdapterServerOptions = server.NewCustomMetricsAdapterServerOptions()
}
b.SecureServing.AddFlags(b.FlagSet)
b.Authentication.AddFlags(b.FlagSet)
b.Authorization.AddFlags(b.FlagSet)
b.Features.AddFlags(b.FlagSet)
b.FlagSet.StringVar(&b.RemoteKubeConfigFile, "lister-kubeconfig", b.RemoteKubeConfigFile,
"kubeconfig file pointing at the 'core' kubernetes server with enough rights to list "+
"any described objects")
b.FlagSet.DurationVar(&b.DiscoveryInterval, "discovery-interval", b.DiscoveryInterval,
"interval at which to refresh API discovery information")
})
}
// initFlagSet populates the flagset to the CommandLine flags if it's not already set.
func (b *AdapterBase) initFlagSet() {
if b.FlagSet == nil {
// default to the normal commandline flags
b.FlagSet = pflag.CommandLine
}
}
// Flags returns the flagset used by this adapter.
// It will initialize the flagset with the minimum required set
// of flags as well.
func (b *AdapterBase) Flags() *pflag.FlagSet {
b.initFlagSet()
b.InstallFlags()
return b.FlagSet
}
// ClientConfig returns the REST client configuration used to construct
// clients for the clients and RESTMapper, and may be used for other
// purposes as well. If you need to mutate it, be sure to copy it with
// rest.CopyConfig first.
func (b *AdapterBase) ClientConfig() (*rest.Config, error) {
if b.clientConfig == nil {
var clientConfig *rest.Config
var err error
if len(b.RemoteKubeConfigFile) > 0 {
loadingRules := &clientcmd.ClientConfigLoadingRules{ExplicitPath: b.RemoteKubeConfigFile}
loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{})
clientConfig, err = loader.ClientConfig()
} else {
clientConfig, err = rest.InClusterConfig()
}
if err != nil {
return nil, fmt.Errorf("unable to construct lister client config to initialize provider: %v", err)
}
b.clientConfig = clientConfig
}
return b.clientConfig, nil
}
// DiscoveryClient returns a DiscoveryInterface suitable to for discovering resources
// available on the cluster.
func (b *AdapterBase) DiscoveryClient() (discovery.DiscoveryInterface, error) {
if b.discoveryClient == nil {
clientConfig, err := b.ClientConfig()
if err != nil {
return nil, err
}
discoveryClient, err := discovery.NewDiscoveryClientForConfig(clientConfig)
if err != nil {
return nil, fmt.Errorf("unable to construct discovery client for dynamic client: %v", err)
}
b.discoveryClient = discoveryClient
}
return b.discoveryClient, nil
}
// RESTMapper returns a RESTMapper dynamically populated with discovery information.
// The discovery information will be periodically repopulated according to DiscoveryInterval.
func (b *AdapterBase) RESTMapper() (apimeta.RESTMapper, error) {
if b.restMapper == nil {
discoveryClient, err := b.DiscoveryClient()
if err != nil {
return nil, err
}
// NB: since we never actually look at the contents of
// the objects we fetch (beyond ObjectMeta), unstructured should be fine
dynamicMapper, err := dynamicmapper.NewRESTMapper(discoveryClient, b.DiscoveryInterval)
if err != nil {
return nil, fmt.Errorf("unable to construct dynamic discovery mapper: %v", err)
}
b.restMapper = dynamicMapper
}
return b.restMapper, nil
}
// DynamicClient returns a dynamic Kubernetes client capable of listing and fetching
// any resources on the cluster.
func (b *AdapterBase) DynamicClient() (dynamic.Interface, error) {
if b.dynamicClient == nil {
clientConfig, err := b.ClientConfig()
if err != nil {
return nil, err
}
dynClient, err := dynamic.NewForConfig(clientConfig)
if err != nil {
return nil, fmt.Errorf("unable to construct lister client to initialize provider: %v", err)
}
b.dynamicClient = dynClient
}
return b.dynamicClient, nil
}
// WithCustomMetrics populates the custom metrics provider for this adapter.
func (b *AdapterBase) WithCustomMetrics(p provider.CustomMetricsProvider) {
b.cmProvider = p
}
// WithExternalMetrics populates the external metrics provider for this adapter.
func (b *AdapterBase) WithExternalMetrics(p provider.ExternalMetricsProvider) {
b.emProvider = p
}
// Config fetches the configuration used to ulitmately create the custom metrics adapter's
// API server. While this method is idempotent, it does "cement" values of some of the other
// fields, so make sure to only call it just before `Server` or `Run`.
// Normal users should not need to call this method -- it's for advanced use cases.
func (b *AdapterBase) Config() (*apiserver.Config, error) {
if b.config == nil {
b.InstallFlags() // just to be sure
config, err := b.CustomMetricsAdapterServerOptions.Config()
if err != nil {
return nil, err
}
b.config = config
}
return b.config, nil
}
// Server fetches API server object used to ulitmately run the custom metrics adapter.
// While this method is idempotent, it does "cement" values of some of the other
// fields, so make sure to only call it just before `Run`.
// Normal users should not need to call this method -- it's for advanced use cases.
func (b *AdapterBase) Server() (*apiserver.CustomMetricsAdapterServer, error) {
if b.server == nil {
config, err := b.Config()
if err != nil {
return nil, err
}
if b.Name == "" {
b.Name = "custom-metrics-adapter"
}
// we add in the informers if they're not nil, but we don't try and
// construct them if the user didn't ask for them
server, err := config.Complete(b.informers).New(b.Name, b.cmProvider, b.emProvider)
if err != nil {
return nil, err
}
b.server = server
}
return b.server, nil
}
// Informers returns a SharedInformerFactory for constructing new informers.
// The informers will be automatically started as part of starting the adapter.
func (b *AdapterBase) Informers() (informers.SharedInformerFactory, error) {
if b.informers == nil {
clientConfig, err := b.ClientConfig()
if err != nil {
return nil, err
}
kubeClient, err := kubernetes.NewForConfig(clientConfig)
if err != nil {
return nil, err
}
b.informers = informers.NewSharedInformerFactory(kubeClient, 0)
}
return b.informers, nil
}
// Run runs this custom metrics adapter until the given stop channel is closed.
func (b *AdapterBase) Run(stopCh <-chan struct{}) error {
server, err := b.Server()
if err != nil {
return err
}
return server.GenericAPIServer.PrepareRun().Run(stopCh)
}

View file

@ -18,7 +18,6 @@ package server
import ( import (
"fmt" "fmt"
"io"
"net" "net"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/apiserver" "github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/apiserver"
@ -32,20 +31,14 @@ type CustomMetricsAdapterServerOptions struct {
Authentication *genericoptions.DelegatingAuthenticationOptions Authentication *genericoptions.DelegatingAuthenticationOptions
Authorization *genericoptions.DelegatingAuthorizationOptions Authorization *genericoptions.DelegatingAuthorizationOptions
Features *genericoptions.FeatureOptions Features *genericoptions.FeatureOptions
StdOut io.Writer
StdErr io.Writer
} }
func NewCustomMetricsAdapterServerOptions(out, errOut io.Writer) *CustomMetricsAdapterServerOptions { func NewCustomMetricsAdapterServerOptions() *CustomMetricsAdapterServerOptions {
o := &CustomMetricsAdapterServerOptions{ o := &CustomMetricsAdapterServerOptions{
SecureServing: genericoptions.WithLoopback(genericoptions.NewSecureServingOptions()), SecureServing: genericoptions.WithLoopback(genericoptions.NewSecureServingOptions()),
Authentication: genericoptions.NewDelegatingAuthenticationOptions(), Authentication: genericoptions.NewDelegatingAuthenticationOptions(),
Authorization: genericoptions.NewDelegatingAuthorizationOptions(), Authorization: genericoptions.NewDelegatingAuthorizationOptions(),
Features: genericoptions.NewFeatureOptions(), Features: genericoptions.NewFeatureOptions(),
StdOut: out,
StdErr: errOut,
} }
return o return o

View file

@ -0,0 +1,106 @@
/*
Copyright 2018 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 helpers
import (
"fmt"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
"k8s.io/metrics/pkg/apis/custom_metrics"
"github.com/kubernetes-incubator/custom-metrics-apiserver/pkg/provider"
)
// ResourceFor attempts to resolve a single qualified resource for the given metric.
// You can use this to resolve a particular piece of CustomMetricInfo to the underlying
// resource that it describes, so that you can list matching objects in the cluster.
func ResourceFor(mapper apimeta.RESTMapper, info provider.CustomMetricInfo) (schema.GroupVersionResource, error) {
fullResources, err := mapper.ResourcesFor(info.GroupResource.WithVersion(""))
if err == nil && len(fullResources) == 0 {
err = fmt.Errorf("no fully versioned resources known for group-resource %v", info.GroupResource)
}
if err != nil {
return schema.GroupVersionResource{}, fmt.Errorf("unable to find preferred version to list matching resource names: %v", err)
}
return fullResources[0], nil
}
// ReferenceFor returns a new ObjectReference for the given group-resource and name.
// The group-resource is converted into a group-version-kind using the given RESTMapper.
// You can use this to easily construct an object reference for use in the DescribedObject
// field of CustomMetricInfo.
func ReferenceFor(mapper apimeta.RESTMapper, name types.NamespacedName, info provider.CustomMetricInfo) (custom_metrics.ObjectReference, error) {
kind, err := mapper.KindFor(info.GroupResource.WithVersion(""))
if err != nil {
return custom_metrics.ObjectReference{}, err
}
// NB: return straight value, not a reference, so that the object can easily
// be copied for use multiple times with a different name.
return custom_metrics.ObjectReference{
APIVersion: kind.Group + "/" + kind.Version,
Kind: kind.Kind,
Name: name.Name,
Namespace: name.Namespace,
}, nil
}
// ListObjectNames uses the given dynamic client to list the names of all objects
// of the given resource matching the given selector. Namespace may be empty
// if the metric is for a root-scoped resource.
func ListObjectNames(mapper apimeta.RESTMapper, client dynamic.Interface, namespace string, selector labels.Selector, info provider.CustomMetricInfo) ([]string, error) {
res, err := ResourceFor(mapper, info)
if err != nil {
return nil, err
}
var resClient dynamic.ResourceInterface
if info.Namespaced {
resClient = client.Resource(res).Namespace(namespace)
} else {
resClient = client.Resource(res)
}
matchingObjectsRaw, err := resClient.List(metav1.ListOptions{LabelSelector: selector.String()})
if err != nil {
return nil, err
}
if !apimeta.IsListType(matchingObjectsRaw) {
return nil, fmt.Errorf("result of label selector list operation was not a list")
}
var names []string
err = apimeta.EachListItem(matchingObjectsRaw, func(item runtime.Object) error {
objName := item.(*unstructured.Unstructured).GetName()
names = append(names, objName)
return nil
})
if err != nil {
return nil, err
}
return names, nil
}

View file

@ -22,6 +22,7 @@ import (
apimeta "k8s.io/apimachinery/pkg/api/meta" apimeta "k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/metrics/pkg/apis/custom_metrics" "k8s.io/metrics/pkg/apis/custom_metrics"
"k8s.io/metrics/pkg/apis/external_metrics" "k8s.io/metrics/pkg/apis/external_metrics"
) )
@ -37,7 +38,6 @@ type CustomMetricInfo struct {
// ExternalMetricInfo describes a metric. // ExternalMetricInfo describes a metric.
type ExternalMetricInfo struct { type ExternalMetricInfo struct {
Metric string Metric string
Labels map[string]string
} }
func (i CustomMetricInfo) String() string { func (i CustomMetricInfo) String() string {
@ -82,19 +82,13 @@ func (i CustomMetricInfo) Normalized(mapper apimeta.RESTMapper) (normalizedInfo
// they may wish to query the main Kubernetes API server, or may // they may wish to query the main Kubernetes API server, or may
// wish to simply make use of stored information in their TSDB. // wish to simply make use of stored information in their TSDB.
type CustomMetricsProvider interface { type CustomMetricsProvider interface {
// GetRootScopedMetricByName fetches a particular metric for a particular root-scoped object. // GetMetricByName fetches a particular metric for a particular object.
GetRootScopedMetricByName(groupResource schema.GroupResource, name string, metricName string) (*custom_metrics.MetricValue, error) // The namespace will be empty if the metric is root-scoped.
GetMetricByName(name types.NamespacedName, info CustomMetricInfo) (*custom_metrics.MetricValue, error)
// GetRootScopedMetricByName fetches a particular metric for a set of root-scoped objects // GetMetricBySelector fetches a particular metric for a set of objects matching
// matching the given label selector. // the given label selector. The namespace will be empty if the metric is root-scoped.
GetRootScopedMetricBySelector(groupResource schema.GroupResource, selector labels.Selector, metricName string) (*custom_metrics.MetricValueList, error) GetMetricBySelector(namespace string, selector labels.Selector, info CustomMetricInfo) (*custom_metrics.MetricValueList, error)
// GetNamespacedMetricByName fetches a particular metric for a particular namespaced object.
GetNamespacedMetricByName(groupResource schema.GroupResource, namespace string, name string, metricName string) (*custom_metrics.MetricValue, error)
// GetNamespacedMetricByName fetches a particular metric for a set of namespaced objects
// matching the given label selector.
GetNamespacedMetricBySelector(groupResource schema.GroupResource, namespace string, selector labels.Selector, metricName string) (*custom_metrics.MetricValueList, error)
// ListAllMetrics provides a list of all available metrics at // ListAllMetrics provides a list of all available metrics at
// the current time. Note that this is not allowed to return // the current time. Note that this is not allowed to return
@ -108,7 +102,7 @@ type CustomMetricsProvider interface {
// implementation how to translate metricSelector to a filter for metric values. // implementation how to translate metricSelector to a filter for metric values.
// Namespace can be used by the implemetation for metric identification, access control or ignored. // Namespace can be used by the implemetation for metric identification, access control or ignored.
type ExternalMetricsProvider interface { type ExternalMetricsProvider interface {
GetExternalMetric(namespace string, metricName string, metricSelector labels.Selector) (*external_metrics.ExternalMetricValueList, error) GetExternalMetric(namespace string, metricSelector labels.Selector, info ExternalMetricInfo) (*external_metrics.ExternalMetricValueList, error)
ListAllExternalMetrics() []ExternalMetricInfo ListAllExternalMetrics() []ExternalMetricInfo
} }

View file

@ -25,6 +25,7 @@ import (
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/endpoints/request"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request" genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
@ -103,14 +104,11 @@ func (r *REST) List(ctx context.Context, options *metainternalversion.ListOption
} }
func (r *REST) handleIndividualOp(namespace string, groupResource schema.GroupResource, name string, metricName string) (*custom_metrics.MetricValueList, error) { func (r *REST) handleIndividualOp(namespace string, groupResource schema.GroupResource, name string, metricName string) (*custom_metrics.MetricValueList, error) {
var err error singleRes, err := r.cmProvider.GetMetricByName(types.NamespacedName{Namespace: namespace, Name: name}, provider.CustomMetricInfo{
var singleRes *custom_metrics.MetricValue GroupResource: groupResource,
if namespace == "" { Metric: metricName,
singleRes, err = r.cmProvider.GetRootScopedMetricByName(groupResource, name, metricName) Namespaced: namespace != "",
} else { })
singleRes, err = r.cmProvider.GetNamespacedMetricByName(groupResource, namespace, name, metricName)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -121,9 +119,9 @@ func (r *REST) handleIndividualOp(namespace string, groupResource schema.GroupRe
} }
func (r *REST) handleWildcardOp(namespace string, groupResource schema.GroupResource, selector labels.Selector, metricName string) (*custom_metrics.MetricValueList, error) { func (r *REST) handleWildcardOp(namespace string, groupResource schema.GroupResource, selector labels.Selector, metricName string) (*custom_metrics.MetricValueList, error) {
if namespace == "" { return r.cmProvider.GetMetricBySelector(namespace, selector, provider.CustomMetricInfo{
return r.cmProvider.GetRootScopedMetricBySelector(groupResource, selector, metricName) GroupResource: groupResource,
} else { Metric: metricName,
return r.cmProvider.GetNamespacedMetricBySelector(groupResource, namespace, selector, metricName) Namespaced: namespace != "",
} })
} }

View file

@ -76,5 +76,5 @@ func (r *REST) List(ctx context.Context, options *metainternalversion.ListOption
} }
metricName := requestInfo.Resource metricName := requestInfo.Resource
return r.emProvider.GetExternalMetric(namespace, metricName, metricSelector) return r.emProvider.GetExternalMetric(namespace, metricSelector, provider.ExternalMetricInfo{Metric: metricName})
} }

View file

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
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.

View file

@ -0,0 +1,72 @@
// Copyright 2018 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 generic
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
coreinf "k8s.io/client-go/informers/core/v1"
"k8s.io/metrics/pkg/apis/metrics"
"k8s.io/metrics/pkg/apis/metrics/install"
"k8s.io/metrics/pkg/apis/metrics/v1beta1"
"github.com/kubernetes-incubator/metrics-server/pkg/provider"
nodemetricsstorage "github.com/kubernetes-incubator/metrics-server/pkg/storage/nodemetrics"
podmetricsstorage "github.com/kubernetes-incubator/metrics-server/pkg/storage/podmetrics"
)
var (
// Scheme contains the types needed by the resource metrics API.
Scheme = runtime.NewScheme()
// Codecs is a codec factory for serving the resource metrics API.
Codecs = serializer.NewCodecFactory(Scheme)
)
func init() {
install.Install(Scheme)
metav1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"})
}
// ProviderConfig holds the providers for node and pod metrics
// for serving the resource metrics API.
type ProviderConfig struct {
Node provider.NodeMetricsProvider
Pod provider.PodMetricsProvider
}
// BuildStorage constructs APIGroupInfo the metrics.k8s.io API group using the given providers.
func BuildStorage(providers *ProviderConfig, informers coreinf.Interface) genericapiserver.APIGroupInfo {
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(metrics.GroupName, Scheme, metav1.ParameterCodec, Codecs)
nodemetricsStorage := nodemetricsstorage.NewStorage(metrics.Resource("nodemetrics"), providers.Node, informers.Nodes().Lister())
podmetricsStorage := podmetricsstorage.NewStorage(metrics.Resource("podmetrics"), providers.Pod, informers.Pods().Lister())
metricsServerResources := map[string]rest.Storage{
"nodes": nodemetricsStorage,
"pods": podmetricsStorage,
}
apiGroupInfo.VersionedResourcesStorageMap[v1beta1.SchemeGroupVersion.Version] = metricsServerResources
return apiGroupInfo
}
// InstallStorage builds the storage for the metrics.k8s.io API, and then installs it into the given API server.
func InstallStorage(providers *ProviderConfig, informers coreinf.Interface, server *genericapiserver.GenericAPIServer) error {
info := BuildStorage(providers, informers)
return server.InstallAPIGroup(&info)
}

View file

@ -0,0 +1,63 @@
// Copyright 2018 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 provider
import (
"time"
corev1 "k8s.io/api/core/v1"
apitypes "k8s.io/apimachinery/pkg/types"
metrics "k8s.io/metrics/pkg/apis/metrics"
)
// MetricsProvider is both a PodMetricsProvider and a NodeMetricsProvider
type MetricsProvider interface {
PodMetricsProvider
NodeMetricsProvider
}
// TimeSpan represents the timing information for a metric, which was
// potentially calculated over some window of time (e.g. for CPU usage rate).
type TimeInfo struct {
// NB: we consider the earliest timestamp amongst multiple containers
// for the purposes of determining if a metric is tained by a time
// period, like pod startup (used by things like the HPA).
// Timestamp is the time at which the metrics were initially collected.
// In the case of a rate metric, it should be the timestamp of the last
// data point used in the calculation. If it represents multiple metric
// points, it should be the earliest such timestamp from all of the points.
Timestamp time.Time
// Window represents the window used to calculate rate metrics associated
// with this timestamp.
Window time.Duration
}
// PodMetricsProvider knows how to fetch metrics for the containers in a pod.
type PodMetricsProvider interface {
// GetContainerMetrics gets the latest metrics for all containers in each listed pod,
// returning both the metrics and the associated collection timestamp.
// If a pod is missing, the container metrics should be nil for that pod.
GetContainerMetrics(pods ...apitypes.NamespacedName) ([]TimeInfo, [][]metrics.ContainerMetrics, error)
}
// NodeMetricsProvider knows how to fetch metrics for a node.
type NodeMetricsProvider interface {
// GetNodeMetrics gets the latest metrics for the given nodes,
// returning both the metrics and the associated collection timestamp.
// If a node is missing, the resourcelist should be nil for that node.
GetNodeMetrics(nodes ...string) ([]TimeInfo, []corev1.ResourceList, error)
}

View file

@ -0,0 +1,149 @@
// Copyright 2018 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 nodemetrics
import (
"context"
"fmt"
"time"
"github.com/golang/glog"
"github.com/kubernetes-incubator/metrics-server/pkg/provider"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/registry/rest"
v1listers "k8s.io/client-go/listers/core/v1"
"k8s.io/metrics/pkg/apis/metrics"
_ "k8s.io/metrics/pkg/apis/metrics/install"
)
type MetricStorage struct {
groupResource schema.GroupResource
prov provider.NodeMetricsProvider
nodeLister v1listers.NodeLister
}
var _ rest.KindProvider = &MetricStorage{}
var _ rest.Storage = &MetricStorage{}
var _ rest.Getter = &MetricStorage{}
var _ rest.Lister = &MetricStorage{}
var _ rest.Scoper = &MetricStorage{}
func NewStorage(groupResource schema.GroupResource, prov provider.NodeMetricsProvider, nodeLister v1listers.NodeLister) *MetricStorage {
return &MetricStorage{
groupResource: groupResource,
prov: prov,
nodeLister: nodeLister,
}
}
// Storage interface
func (m *MetricStorage) New() runtime.Object {
return &metrics.NodeMetrics{}
}
// KindProvider interface
func (m *MetricStorage) Kind() string {
return "NodeMetrics"
}
// Lister interface
func (m *MetricStorage) NewList() runtime.Object {
return &metrics.NodeMetricsList{}
}
// Lister interface
func (m *MetricStorage) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
labelSelector := labels.Everything()
if options != nil && options.LabelSelector != nil {
labelSelector = options.LabelSelector
}
nodes, err := m.nodeLister.ListWithPredicate(func(node *v1.Node) bool {
if labelSelector.Empty() {
return true
}
return labelSelector.Matches(labels.Set(node.Labels))
})
if err != nil {
errMsg := fmt.Errorf("Error while listing nodes for selector %v: %v", labelSelector, err)
glog.Error(errMsg)
return &metrics.NodeMetricsList{}, errMsg
}
names := make([]string, len(nodes))
for i, node := range nodes {
names[i] = node.Name
}
metricsItems, err := m.getNodeMetrics(names...)
if err != nil {
errMsg := fmt.Errorf("Error while fetching node metrics for selector %v: %v", labelSelector, err)
glog.Error(errMsg)
return &metrics.NodeMetricsList{}, errMsg
}
return &metrics.NodeMetricsList{Items: metricsItems}, nil
}
func (m *MetricStorage) Get(ctx context.Context, name string, opts *metav1.GetOptions) (runtime.Object, error) {
nodeMetrics, err := m.getNodeMetrics(name)
if err == nil && len(nodeMetrics) == 0 {
err = fmt.Errorf("no metrics known for node %q", name)
}
if err != nil {
glog.Errorf("unable to fetch node metrics for node %q: %v", name, err)
return nil, errors.NewNotFound(m.groupResource, name)
}
return &nodeMetrics[0], nil
}
func (m *MetricStorage) getNodeMetrics(names ...string) ([]metrics.NodeMetrics, error) {
timestamps, usages, err := m.prov.GetNodeMetrics(names...)
if err != nil {
return nil, err
}
res := make([]metrics.NodeMetrics, 0, len(names))
for i, name := range names {
if usages[i] == nil {
glog.Errorf("unable to fetch node metrics for node %q: no metrics known for node", name)
continue
}
res = append(res, metrics.NodeMetrics{
ObjectMeta: metav1.ObjectMeta{
Name: name,
CreationTimestamp: metav1.NewTime(time.Now()),
},
Timestamp: metav1.NewTime(timestamps[i].Timestamp),
Window: metav1.Duration{Duration: timestamps[i].Window},
Usage: usages[i],
})
}
return res, nil
}
func (m *MetricStorage) NamespaceScoped() bool {
return false
}

View file

@ -0,0 +1,164 @@
// Copyright 2018 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 podmetrics
import (
"context"
"fmt"
"time"
"github.com/golang/glog"
"github.com/kubernetes-incubator/metrics-server/pkg/provider"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
apitypes "k8s.io/apimachinery/pkg/types"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
v1listers "k8s.io/client-go/listers/core/v1"
"k8s.io/metrics/pkg/apis/metrics"
_ "k8s.io/metrics/pkg/apis/metrics/install"
)
type MetricStorage struct {
groupResource schema.GroupResource
prov provider.PodMetricsProvider
podLister v1listers.PodLister
}
var _ rest.KindProvider = &MetricStorage{}
var _ rest.Storage = &MetricStorage{}
var _ rest.Getter = &MetricStorage{}
var _ rest.Lister = &MetricStorage{}
func NewStorage(groupResource schema.GroupResource, prov provider.PodMetricsProvider, podLister v1listers.PodLister) *MetricStorage {
return &MetricStorage{
groupResource: groupResource,
prov: prov,
podLister: podLister,
}
}
// Storage interface
func (m *MetricStorage) New() runtime.Object {
return &metrics.PodMetrics{}
}
// KindProvider interface
func (m *MetricStorage) Kind() string {
return "PodMetrics"
}
// Lister interface
func (m *MetricStorage) NewList() runtime.Object {
return &metrics.PodMetricsList{}
}
// Lister interface
func (m *MetricStorage) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
labelSelector := labels.Everything()
if options != nil && options.LabelSelector != nil {
labelSelector = options.LabelSelector
}
namespace := genericapirequest.NamespaceValue(ctx)
pods, err := m.podLister.Pods(namespace).List(labelSelector)
if err != nil {
errMsg := fmt.Errorf("Error while listing pods for selector %v in namespace %q: %v", labelSelector, namespace, err)
glog.Error(errMsg)
return &metrics.PodMetricsList{}, errMsg
}
metricsItems, err := m.getPodMetrics(pods...)
if err != nil {
errMsg := fmt.Errorf("Error while fetching pod metrics for selector %v in namespace %q: %v", labelSelector, namespace, err)
glog.Error(errMsg)
return &metrics.PodMetricsList{}, errMsg
}
return &metrics.PodMetricsList{Items: metricsItems}, nil
}
// Getter interface
func (m *MetricStorage) Get(ctx context.Context, name string, opts *metav1.GetOptions) (runtime.Object, error) {
namespace := genericapirequest.NamespaceValue(ctx)
pod, err := m.podLister.Pods(namespace).Get(name)
if err != nil {
errMsg := fmt.Errorf("Error while getting pod %v: %v", name, err)
glog.Error(errMsg)
if errors.IsNotFound(err) {
// return not-found errors directly
return &metrics.PodMetrics{}, err
}
return &metrics.PodMetrics{}, errMsg
}
if pod == nil {
return &metrics.PodMetrics{}, errors.NewNotFound(v1.Resource("pods"), fmt.Sprintf("%v/%v", namespace, name))
}
podMetrics, err := m.getPodMetrics(pod)
if err == nil && len(podMetrics) == 0 {
err = fmt.Errorf("no metrics known for pod \"%s/%s\"", pod.Namespace, pod.Name)
}
if err != nil {
glog.Errorf("unable to fetch pod metrics for pod %s/%s: %v", pod.Namespace, pod.Name, err)
return nil, errors.NewNotFound(m.groupResource, fmt.Sprintf("%v/%v", namespace, name))
}
return &podMetrics[0], nil
}
func (m *MetricStorage) getPodMetrics(pods ...*v1.Pod) ([]metrics.PodMetrics, error) {
namespacedNames := make([]apitypes.NamespacedName, len(pods))
for i, pod := range pods {
namespacedNames[i] = apitypes.NamespacedName{
Name: pod.Name,
Namespace: pod.Namespace,
}
}
timestamps, containerMetrics, err := m.prov.GetContainerMetrics(namespacedNames...)
if err != nil {
return nil, err
}
res := make([]metrics.PodMetrics, 0, len(pods))
for i, pod := range pods {
if containerMetrics[i] == nil {
glog.Errorf("unable to fetch pod metrics for pod %s/%s: no metrics known for pod", pod.Namespace, pod.Name)
continue
}
res = append(res, metrics.PodMetrics{
ObjectMeta: metav1.ObjectMeta{
Name: pod.Name,
Namespace: pod.Namespace,
CreationTimestamp: metav1.NewTime(time.Now()),
},
Timestamp: metav1.NewTime(timestamps[i].Timestamp),
Window: metav1.Duration{Duration: timestamps[i].Window},
Containers: containerMetrics[i],
})
}
return res, nil
}
func (m *MetricStorage) NamespaceScoped() bool {
return true
}

7
vendor/github.com/onsi/ginkgo/.gitignore generated vendored Normal file
View file

@ -0,0 +1,7 @@
.DS_Store
TODO
tmp/**/*
*.coverprofile
.vscode
.idea/
*.log

16
vendor/github.com/onsi/ginkgo/.travis.yml generated vendored Normal file
View file

@ -0,0 +1,16 @@
language: go
go:
- 1.6.x
- 1.7.x
- 1.8.x
- 1.9.x
- 1.10.x
install:
- go get -v -t ./...
- go get golang.org/x/tools/cmd/cover
- go get github.com/onsi/gomega
- go install github.com/onsi/ginkgo/ginkgo
- export PATH=$PATH:$HOME/gopath/bin
script: $HOME/gopath/bin/ginkgo -r --randomizeAllSpecs --randomizeSuites --race --trace && go vet

198
vendor/github.com/onsi/ginkgo/CHANGELOG.md generated vendored Normal file
View file

@ -0,0 +1,198 @@
## 1.6.0
### New Features
- add --debug flag to emit node output to files (#499) [39febac]
### Fixes
- fix: for `go vet` to pass [69338ec]
- docs: fix for contributing instructions [7004cb1]
- consolidate and streamline contribution docs (#494) [d848015]
- Make generated Junit file compatable with "Maven Surefire" (#488) [e51bee6]
- all: gofmt [000d317]
- Increase eventually timeout to 30s [c73579c]
- Clarify asynchronous test behaviour [294d8f4]
- Travis badge should only show master [26d2143]
## 1.5.0 5/10/2018
### New Features
- Supports go v1.10 (#443, #446, #451) [e873237, 468e89e, e37dbfe, a37f4c0, c0b857d, bca5260, 4177ca8]
- Add a When() synonym for Context() (#386) [747514b, 7484dad, 7354a07, dd826c8]
- Re-add noisySkippings flag [652e15c]
- Allow coverage to be displayed for focused specs (#367) [11459a8]
- Handle -outputdir flag (#364) [228e3a8]
- Handle -coverprofile flag (#355) [43392d5]
### Fixes
- When using custom reporters register the custom reporters *before* the default reporter. This allows users to see the output of any print statements in their customer reporters. (#365) [8382b23]
- When running a test and calculating the coverage using the `-coverprofile` and `-outputdir` flags, Ginkgo fails with an error if the directory does not exist. This is due to an [issue in go 1.10](https://github.com/golang/go/issues/24588) (#446) [b36a6e0]
- `unfocus` command ignores vendor folder (#459) [e5e551c, c556e43, a3b6351, 9a820dd]
- Ignore packages whose tests are all ignored by go (#456) [7430ca7, 6d8be98]
- Increase the threshold when checking time measuments (#455) [2f714bf, 68f622c]
- Fix race condition in coverage tests (#423) [a5a8ff7, ab9c08b]
- Add an extra new line after reporting spec run completion for test2json [874520d]
- added name name field to junit reported testsuite [ae61c63]
- Do not set the run time of a spec when the dryRun flag is used (#438) [457e2d9, ba8e856]
- Process FWhen and FSpecify when unfocusing (#434) [9008c7b, ee65bd, df87dfe]
- Synchronise the access to the state of specs to avoid race conditions (#430) [7d481bc, ae6829d]
- Added Duration on GinkgoTestDescription (#383) [5f49dad, 528417e, 0747408, 329d7ed]
- Fix Ginkgo stack trace on failure for Specify (#415) [b977ede, 65ca40e, 6c46eb8]
- Update README with Go 1.6+, Golang -> Go (#409) [17f6b97, bc14b66, 20d1598]
- Use fmt.Errorf instead of errors.New(fmt.Sprintf (#401) [a299f56, 44e2eaa]
- Imports in generated code should follow conventions (#398) [0bec0b0, e8536d8]
- Prevent data race error when Recording a benchmark value from multiple go routines (#390) [c0c4881, 7a241e9]
- Replace GOPATH in Environment [4b883f0]
## 1.4.0 7/16/2017
- `ginkgo` now provides a hint if you accidentally forget to run `ginkgo bootstrap` to generate a `*_suite_test.go` file that actually invokes the Ginkgo test runner. [#345](https://github.com/onsi/ginkgo/pull/345)
- thanks to improvements in `go test -c` `ginkgo` no longer needs to fix Go's compilation output to ensure compilation errors are expressed relative to the CWD. [#357]
- `ginkgo watch -watchRegExp=...` allows you to specify a custom regular expression to watch. Only files matching the regular expression are watched for changes (the default is `\.go$`) [#356]
- `ginkgo` now always emits compilation output. Previously, only failed compilation output was printed out. [#277]
- `ginkgo -requireSuite` now fails the test run if there are `*_test.go` files but `go test` fails to detect any tests. Typically this means you forgot to run `ginkgo bootstrap` to generate a suite file. [#344]
- `ginkgo -timeout=DURATION` allows you to adjust the timeout for the entire test suite (default is 24 hours) [#248]
## 1.3.0 3/28/2017
Improvements:
- Significantly improved parallel test distribution. Now instead of pre-sharding test cases across workers (which can result in idle workers and poor test performance) Ginkgo uses a shared queue to keep all workers busy until all tests are complete. This improves test-time performance and consistency.
- `Skip(message)` can be used to skip the current test.
- Added `extensions/table` - a Ginkgo DSL for [Table Driven Tests](http://onsi.github.io/ginkgo/#table-driven-tests)
- Add `GinkgoRandomSeed()` - shorthand for `config.GinkgoConfig.RandomSeed`
- Support for retrying flaky tests with `--flakeAttempts`
- `ginkgo ./...` now recurses as you'd expect
- Added `Specify` a synonym for `It`
- Support colorise on Windows
- Broader support for various go compilation flags in the `ginkgo` CLI
Bug Fixes:
- Ginkgo tests now fail when you `panic(nil)` (#167)
## 1.2.0 5/31/2015
Improvements
- `ginkgo -coverpkg` calls down to `go test -coverpkg` (#160)
- `ginkgo -afterSuiteHook COMMAND` invokes the passed-in `COMMAND` after a test suite completes (#152)
- Relaxed requirement for Go 1.4+. `ginkgo` now works with Go v1.3+ (#166)
## 1.2.0-beta
Ginkgo now requires Go 1.4+
Improvements:
- Call reporters in reverse order when announcing spec completion -- allows custom reporters to emit output before the default reporter does.
- Improved focus behavior. Now, this:
```golang
FDescribe("Some describe", func() {
It("A", func() {})
FIt("B", func() {})
})
```
will run `B` but *not* `A`. This tends to be a common usage pattern when in the thick of writing and debugging tests.
- When `SIGINT` is received, Ginkgo will emit the contents of the `GinkgoWriter` before running the `AfterSuite`. Useful for debugging stuck tests.
- When `--progress` is set, Ginkgo will write test progress (in particular, Ginkgo will say when it is about to run a BeforeEach, AfterEach, It, etc...) to the `GinkgoWriter`. This is useful for debugging stuck tests and tests that generate many logs.
- Improved output when an error occurs in a setup or teardown block.
- When `--dryRun` is set, Ginkgo will walk the spec tree and emit to its reporter *without* actually running anything. Best paired with `-v` to understand which specs will run in which order.
- Add `By` to help document long `It`s. `By` simply writes to the `GinkgoWriter`.
- Add support for precompiled tests:
- `ginkgo build <path-to-package>` will now compile the package, producing a file named `package.test`
- The compiled `package.test` file can be run directly. This runs the tests in series.
- To run precompiled tests in parallel, you can run: `ginkgo -p package.test`
- Support `bootstrap`ping and `generate`ing [Agouti](http://agouti.org) specs.
- `ginkgo generate` and `ginkgo bootstrap` now honor the package name already defined in a given directory
- The `ginkgo` CLI ignores `SIGQUIT`. Prevents its stack dump from interlacing with the underlying test suite's stack dump.
- The `ginkgo` CLI now compiles tests into a temporary directory instead of the package directory. This necessitates upgrading to Go v1.4+.
- `ginkgo -notify` now works on Linux
Bug Fixes:
- If --skipPackages is used and all packages are skipped, Ginkgo should exit 0.
- Fix tempfile leak when running in parallel
- Fix incorrect failure message when a panic occurs during a parallel test run
- Fixed an issue where a pending test within a focused context (or a focused test within a pending context) would skip all other tests.
- Be more consistent about handling SIGTERM as well as SIGINT
- When interupted while concurrently compiling test suites in the background, Ginkgo now cleans up the compiled artifacts.
- Fixed a long standing bug where `ginkgo -p` would hang if a process spawned by one of the Ginkgo parallel nodes does not exit. (Hooray!)
## 1.1.0 (8/2/2014)
No changes, just dropping the beta.
## 1.1.0-beta (7/22/2014)
New Features:
- `ginkgo watch` now monitors packages *and their dependencies* for changes. The depth of the dependency tree can be modified with the `-depth` flag.
- Test suites with a programmatic focus (`FIt`, `FDescribe`, etc...) exit with non-zero status code, even when they pass. This allows CI systems to detect accidental commits of focused test suites.
- `ginkgo -p` runs the testsuite in parallel with an auto-detected number of nodes.
- `ginkgo -tags=TAG_LIST` passes a list of tags down to the `go build` command.
- `ginkgo --failFast` aborts the test suite after the first failure.
- `ginkgo generate file_1 file_2` can take multiple file arguments.
- Ginkgo now summarizes any spec failures that occured at the end of the test run.
- `ginkgo --randomizeSuites` will run tests *suites* in random order using the generated/passed-in seed.
Improvements:
- `ginkgo -skipPackage` now takes a comma-separated list of strings. If the *relative path* to a package matches one of the entries in the comma-separated list, that package is skipped.
- `ginkgo --untilItFails` no longer recompiles between attempts.
- Ginkgo now panics when a runnable node (`It`, `BeforeEach`, `JustBeforeEach`, `AfterEach`, `Measure`) is nested within another runnable node. This is always a mistake. Any test suites that panic because of this change should be fixed.
Bug Fixes:
- `ginkgo boostrap` and `ginkgo generate` no longer fail when dealing with `hyphen-separated-packages`.
- parallel specs are now better distributed across nodes - fixed a crashing bug where (for example) distributing 11 tests across 7 nodes would panic
## 1.0.0 (5/24/2014)
New Features:
- Add `GinkgoParallelNode()` - shorthand for `config.GinkgoConfig.ParallelNode`
Improvements:
- When compilation fails, the compilation output is rewritten to present a correct *relative* path. Allows ⌘-clicking in iTerm open the file in your text editor.
- `--untilItFails` and `ginkgo watch` now generate new random seeds between test runs, unless a particular random seed is specified.
Bug Fixes:
- `-cover` now generates a correctly combined coverprofile when running with in parallel with multiple `-node`s.
- Print out the contents of the `GinkgoWriter` when `BeforeSuite` or `AfterSuite` fail.
- Fix all remaining race conditions in Ginkgo's test suite.
## 1.0.0-beta (4/14/2014)
Breaking changes:
- `thirdparty/gomocktestreporter` is gone. Use `GinkgoT()` instead
- Modified the Reporter interface
- `watch` is now a subcommand, not a flag.
DSL changes:
- `BeforeSuite` and `AfterSuite` for setting up and tearing down test suites.
- `AfterSuite` is triggered on interrupt (`^C`) as well as exit.
- `SynchronizedBeforeSuite` and `SynchronizedAfterSuite` for setting up and tearing down singleton resources across parallel nodes.
CLI changes:
- `watch` is now a subcommand, not a flag
- `--nodot` flag can be passed to `ginkgo generate` and `ginkgo bootstrap` to avoid dot imports. This explicitly imports all exported identifiers in Ginkgo and Gomega. Refreshing this list can be done by running `ginkgo nodot`
- Additional arguments can be passed to specs. Pass them after the `--` separator
- `--skipPackage` flag takes a regexp and ignores any packages with package names passing said regexp.
- `--trace` flag prints out full stack traces when errors occur, not just the line at which the error occurs.
Misc:
- Start using semantic versioning
- Start maintaining changelog
Major refactor:
- Pull out Ginkgo's internal to `internal`
- Rename `example` everywhere to `spec`
- Much more!

33
vendor/github.com/onsi/ginkgo/CONTRIBUTING.md generated vendored Normal file
View file

@ -0,0 +1,33 @@
# Contributing to Ginkgo
Your contributions to Ginkgo are essential for its long-term maintenance and improvement.
- Please **open an issue first** - describe what problem you are trying to solve and give the community a forum for input and feedback ahead of investing time in writing code!
- Ensure adequate test coverage:
- When adding to the Ginkgo library, add unit and/or integration tests (under the `integration` folder).
- When adding to the Ginkgo CLI, note that there are very few unit tests. Please add an integration test.
- Update the documentation. Ginko uses `godoc` comments and documentation on the `gh-pages` branch.
If relevant, please submit a docs PR to that branch alongside your code PR.
Thanks for supporting Ginkgo!
## Setup
Fork the repo, then:
```
go get github.com/onsi/ginkgo
go get github.com/onsi/gomega/...
cd $GOPATH/src/github.com/onsi/ginkgo
git remote add fork git@github.com:<NAME>/ginkgo.git
ginkgo -r -p # ensure tests are green
go vet ./... # ensure linter is happy
```
## Making the PR
- go to a new branch `git checkout -b my-feature`
- make your changes
- run tests and linter again (see above)
- `git push fork`
- open PR 🎉

20
vendor/github.com/onsi/ginkgo/LICENSE generated vendored Normal file
View file

@ -0,0 +1,20 @@
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.

121
vendor/github.com/onsi/ginkgo/README.md generated vendored Normal file
View file

@ -0,0 +1,121 @@
![Ginkgo: A Go BDD Testing Framework](http://onsi.github.io/ginkgo/images/ginkgo.png)
[![Build Status](https://travis-ci.org/onsi/ginkgo.svg?branch=master)](https://travis-ci.org/onsi/ginkgo)
Jump to the [docs](http://onsi.github.io/ginkgo/) to learn more. To start rolling your Ginkgo tests *now* [keep reading](#set-me-up)!
If you have a question, comment, bug report, feature request, etc. please open a GitHub issue.
## Feature List
- Ginkgo uses Go's `testing` package and can live alongside your existing `testing` tests. It's easy to [bootstrap](http://onsi.github.io/ginkgo/#bootstrapping-a-suite) and start writing your [first tests](http://onsi.github.io/ginkgo/#adding-specs-to-a-suite)
- Structure your BDD-style tests expressively:
- Nestable [`Describe`, `Context` and `When` container blocks](http://onsi.github.io/ginkgo/#organizing-specs-with-containers-describe-and-context)
- [`BeforeEach` and `AfterEach` blocks](http://onsi.github.io/ginkgo/#extracting-common-setup-beforeeach) for setup and teardown
- [`It` and `Specify` blocks](http://onsi.github.io/ginkgo/#individual-specs-) that hold your assertions
- [`JustBeforeEach` blocks](http://onsi.github.io/ginkgo/#separating-creation-and-configuration-justbeforeeach) that separate creation from configuration (also known as the subject action pattern).
- [`BeforeSuite` and `AfterSuite` blocks](http://onsi.github.io/ginkgo/#global-setup-and-teardown-beforesuite-and-aftersuite) to prep for and cleanup after a suite.
- A comprehensive test runner that lets you:
- Mark specs as [pending](http://onsi.github.io/ginkgo/#pending-specs)
- [Focus](http://onsi.github.io/ginkgo/#focused-specs) individual specs, and groups of specs, either programmatically or on the command line
- Run your tests in [random order](http://onsi.github.io/ginkgo/#spec-permutation), and then reuse random seeds to replicate the same order.
- Break up your test suite into parallel processes for straightforward [test parallelization](http://onsi.github.io/ginkgo/#parallel-specs)
- `ginkgo`: a command line interface with plenty of handy command line arguments for [running your tests](http://onsi.github.io/ginkgo/#running-tests) and [generating](http://onsi.github.io/ginkgo/#generators) test files. Here are a few choice examples:
- `ginkgo -nodes=N` runs your tests in `N` parallel processes and print out coherent output in realtime
- `ginkgo -cover` runs your tests using Go's code coverage tool
- `ginkgo convert` converts an XUnit-style `testing` package to a Ginkgo-style package
- `ginkgo -focus="REGEXP"` and `ginkgo -skip="REGEXP"` allow you to specify a subset of tests to run via regular expression
- `ginkgo -r` runs all tests suites under the current directory
- `ginkgo -v` prints out identifying information for each tests just before it runs
And much more: run `ginkgo help` for details!
The `ginkgo` CLI is convenient, but purely optional -- Ginkgo works just fine with `go test`
- `ginkgo watch` [watches](https://onsi.github.io/ginkgo/#watching-for-changes) packages *and their dependencies* for changes, then reruns tests. Run tests immediately as you develop!
- Built-in support for testing [asynchronicity](http://onsi.github.io/ginkgo/#asynchronous-tests)
- Built-in support for [benchmarking](http://onsi.github.io/ginkgo/#benchmark-tests) your code. Control the number of benchmark samples as you gather runtimes and other, arbitrary, bits of numerical information about your code.
- [Completions for Sublime Text](https://github.com/onsi/ginkgo-sublime-completions): just use [Package Control](https://sublime.wbond.net/) to install `Ginkgo Completions`.
- [Completions for VSCode](https://github.com/onsi/vscode-ginkgo): just use VSCode's extension installer to install `vscode-ginkgo`.
- Straightforward support for third-party testing libraries such as [Gomock](https://code.google.com/p/gomock/) and [Testify](https://github.com/stretchr/testify). Check out the [docs](http://onsi.github.io/ginkgo/#third-party-integrations) for details.
- A modular architecture that lets you easily:
- Write [custom reporters](http://onsi.github.io/ginkgo/#writing-custom-reporters) (for example, Ginkgo comes with a [JUnit XML reporter](http://onsi.github.io/ginkgo/#generating-junit-xml-output) and a TeamCity reporter).
- [Adapt an existing matcher library (or write your own!)](http://onsi.github.io/ginkgo/#using-other-matcher-libraries) to work with Ginkgo
## [Gomega](http://github.com/onsi/gomega): Ginkgo's Preferred Matcher Library
Ginkgo is best paired with Gomega. Learn more about Gomega [here](http://onsi.github.io/gomega/)
## [Agouti](http://github.com/sclevine/agouti): A Go Acceptance Testing Framework
Agouti allows you run WebDriver integration tests. Learn more about Agouti [here](http://agouti.org)
## Set Me Up!
You'll need the Go command-line tools. Ginkgo is tested with Go 1.6+, but preferably you should get the latest. Follow the [installation instructions](https://golang.org/doc/install) if you don't have it installed.
```bash
go get -u github.com/onsi/ginkgo/ginkgo # installs the ginkgo CLI
go get -u github.com/onsi/gomega/... # fetches the matcher library
cd path/to/package/you/want/to/test
ginkgo bootstrap # set up a new ginkgo suite
ginkgo generate # will create a sample test file. edit this file and add your tests then...
go test # to run your tests
ginkgo # also runs your tests
```
## I'm new to Go: What are my testing options?
Of course, I heartily recommend [Ginkgo](https://github.com/onsi/ginkgo) and [Gomega](https://github.com/onsi/gomega). Both packages are seeing heavy, daily, production use on a number of projects and boast a mature and comprehensive feature-set.
With that said, it's great to know what your options are :)
### What Go gives you out of the box
Testing is a first class citizen in Go, however Go's built-in testing primitives are somewhat limited: The [testing](http://golang.org/pkg/testing) package provides basic XUnit style tests and no assertion library.
### Matcher libraries for Go's XUnit style tests
A number of matcher libraries have been written to augment Go's built-in XUnit style tests. Here are two that have gained traction:
- [testify](https://github.com/stretchr/testify)
- [gocheck](http://labix.org/gocheck)
You can also use Ginkgo's matcher library [Gomega](https://github.com/onsi/gomega) in [XUnit style tests](http://onsi.github.io/gomega/#using-gomega-with-golangs-xunitstyle-tests)
### BDD style testing frameworks
There are a handful of BDD-style testing frameworks written for Go. Here are a few:
- [Ginkgo](https://github.com/onsi/ginkgo) ;)
- [GoConvey](https://github.com/smartystreets/goconvey)
- [Goblin](https://github.com/franela/goblin)
- [Mao](https://github.com/azer/mao)
- [Zen](https://github.com/pranavraja/zen)
Finally, @shageman has [put together](https://github.com/shageman/gotestit) a comprehensive comparison of Go testing libraries.
Go explore!
## License
Ginkgo is MIT-Licensed
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md)

14
vendor/github.com/onsi/ginkgo/RELEASING.md generated vendored Normal file
View file

@ -0,0 +1,14 @@
A Ginkgo release is a tagged git sha and a GitHub release. To cut a release:
1. Ensure CHANGELOG.md is up to date.
- Use `git log --pretty=format:'- %s [%h]' HEAD...vX.X.X` to list all the commits since the last release
- Categorize the changes into
- Breaking Changes (requires a major version)
- New Features (minor version)
- Fixes (fix version)
- Maintenance (which in general should not be mentioned in `CHANGELOG.md` as they have no user impact)
1. Update `VERSION` in `config/config.go`
1. Create a commit with the version number as the commit message (e.g. `v1.3.0`)
1. Tag the commit with the version number as the tag name (e.g. `v1.3.0`)
1. Push the commit and tag to GitHub
1. Create a new [GitHub release](https://help.github.com/articles/creating-releases/) with the version number as the tag (e.g. `v1.3.0`). List the key changes in the release notes.

200
vendor/github.com/onsi/ginkgo/config/config.go generated vendored Normal file
View file

@ -0,0 +1,200 @@
/*
Ginkgo accepts a number of configuration options.
These are documented [here](http://onsi.github.io/ginkgo/#the_ginkgo_cli)
You can also learn more via
ginkgo help
or (I kid you not):
go test -asdf
*/
package config
import (
"flag"
"time"
"fmt"
)
const VERSION = "1.6.0"
type GinkgoConfigType struct {
RandomSeed int64
RandomizeAllSpecs bool
RegexScansFilePath bool
FocusString string
SkipString string
SkipMeasurements bool
FailOnPending bool
FailFast bool
FlakeAttempts int
EmitSpecProgress bool
DryRun bool
DebugParallel bool
ParallelNode int
ParallelTotal int
SyncHost string
StreamHost string
}
var GinkgoConfig = GinkgoConfigType{}
type DefaultReporterConfigType struct {
NoColor bool
SlowSpecThreshold float64
NoisyPendings bool
NoisySkippings bool
Succinct bool
Verbose bool
FullTrace bool
}
var DefaultReporterConfig = DefaultReporterConfigType{}
func processPrefix(prefix string) string {
if prefix != "" {
prefix = prefix + "."
}
return prefix
}
func Flags(flagSet *flag.FlagSet, prefix string, includeParallelFlags bool) {
prefix = processPrefix(prefix)
flagSet.Int64Var(&(GinkgoConfig.RandomSeed), prefix+"seed", time.Now().Unix(), "The seed used to randomize the spec suite.")
flagSet.BoolVar(&(GinkgoConfig.RandomizeAllSpecs), prefix+"randomizeAllSpecs", false, "If set, ginkgo will randomize all specs together. By default, ginkgo only randomizes the top level Describe, Context and When groups.")
flagSet.BoolVar(&(GinkgoConfig.SkipMeasurements), prefix+"skipMeasurements", false, "If set, ginkgo will skip any measurement specs.")
flagSet.BoolVar(&(GinkgoConfig.FailOnPending), prefix+"failOnPending", false, "If set, ginkgo will mark the test suite as failed if any specs are pending.")
flagSet.BoolVar(&(GinkgoConfig.FailFast), prefix+"failFast", false, "If set, ginkgo will stop running a test suite after a failure occurs.")
flagSet.BoolVar(&(GinkgoConfig.DryRun), prefix+"dryRun", false, "If set, ginkgo will walk the test hierarchy without actually running anything. Best paired with -v.")
flagSet.StringVar(&(GinkgoConfig.FocusString), prefix+"focus", "", "If set, ginkgo will only run specs that match this regular expression.")
flagSet.StringVar(&(GinkgoConfig.SkipString), prefix+"skip", "", "If set, ginkgo will only run specs that do not match this regular expression.")
flagSet.BoolVar(&(GinkgoConfig.RegexScansFilePath), prefix+"regexScansFilePath", false, "If set, ginkgo regex matching also will look at the file path (code location).")
flagSet.IntVar(&(GinkgoConfig.FlakeAttempts), prefix+"flakeAttempts", 1, "Make up to this many attempts to run each spec. Please note that if any of the attempts succeed, the suite will not be failed. But any failures will still be recorded.")
flagSet.BoolVar(&(GinkgoConfig.EmitSpecProgress), prefix+"progress", false, "If set, ginkgo will emit progress information as each spec runs to the GinkgoWriter.")
flagSet.BoolVar(&(GinkgoConfig.DebugParallel), prefix+"debug", false, "If set, ginkgo will emit node output to files when running in parallel.")
if includeParallelFlags {
flagSet.IntVar(&(GinkgoConfig.ParallelNode), prefix+"parallel.node", 1, "This worker node's (one-indexed) node number. For running specs in parallel.")
flagSet.IntVar(&(GinkgoConfig.ParallelTotal), prefix+"parallel.total", 1, "The total number of worker nodes. For running specs in parallel.")
flagSet.StringVar(&(GinkgoConfig.SyncHost), prefix+"parallel.synchost", "", "The address for the server that will synchronize the running nodes.")
flagSet.StringVar(&(GinkgoConfig.StreamHost), prefix+"parallel.streamhost", "", "The address for the server that the running nodes should stream data to.")
}
flagSet.BoolVar(&(DefaultReporterConfig.NoColor), prefix+"noColor", false, "If set, suppress color output in default reporter.")
flagSet.Float64Var(&(DefaultReporterConfig.SlowSpecThreshold), prefix+"slowSpecThreshold", 5.0, "(in seconds) Specs that take longer to run than this threshold are flagged as slow by the default reporter.")
flagSet.BoolVar(&(DefaultReporterConfig.NoisyPendings), prefix+"noisyPendings", true, "If set, default reporter will shout about pending tests.")
flagSet.BoolVar(&(DefaultReporterConfig.NoisySkippings), prefix+"noisySkippings", true, "If set, default reporter will shout about skipping tests.")
flagSet.BoolVar(&(DefaultReporterConfig.Verbose), prefix+"v", false, "If set, default reporter print out all specs as they begin.")
flagSet.BoolVar(&(DefaultReporterConfig.Succinct), prefix+"succinct", false, "If set, default reporter prints out a very succinct report")
flagSet.BoolVar(&(DefaultReporterConfig.FullTrace), prefix+"trace", false, "If set, default reporter prints out the full stack trace when a failure occurs")
}
func BuildFlagArgs(prefix string, ginkgo GinkgoConfigType, reporter DefaultReporterConfigType) []string {
prefix = processPrefix(prefix)
result := make([]string, 0)
if ginkgo.RandomSeed > 0 {
result = append(result, fmt.Sprintf("--%sseed=%d", prefix, ginkgo.RandomSeed))
}
if ginkgo.RandomizeAllSpecs {
result = append(result, fmt.Sprintf("--%srandomizeAllSpecs", prefix))
}
if ginkgo.SkipMeasurements {
result = append(result, fmt.Sprintf("--%sskipMeasurements", prefix))
}
if ginkgo.FailOnPending {
result = append(result, fmt.Sprintf("--%sfailOnPending", prefix))
}
if ginkgo.FailFast {
result = append(result, fmt.Sprintf("--%sfailFast", prefix))
}
if ginkgo.DryRun {
result = append(result, fmt.Sprintf("--%sdryRun", prefix))
}
if ginkgo.FocusString != "" {
result = append(result, fmt.Sprintf("--%sfocus=%s", prefix, ginkgo.FocusString))
}
if ginkgo.SkipString != "" {
result = append(result, fmt.Sprintf("--%sskip=%s", prefix, ginkgo.SkipString))
}
if ginkgo.FlakeAttempts > 1 {
result = append(result, fmt.Sprintf("--%sflakeAttempts=%d", prefix, ginkgo.FlakeAttempts))
}
if ginkgo.EmitSpecProgress {
result = append(result, fmt.Sprintf("--%sprogress", prefix))
}
if ginkgo.DebugParallel {
result = append(result, fmt.Sprintf("--%sdebug", prefix))
}
if ginkgo.ParallelNode != 0 {
result = append(result, fmt.Sprintf("--%sparallel.node=%d", prefix, ginkgo.ParallelNode))
}
if ginkgo.ParallelTotal != 0 {
result = append(result, fmt.Sprintf("--%sparallel.total=%d", prefix, ginkgo.ParallelTotal))
}
if ginkgo.StreamHost != "" {
result = append(result, fmt.Sprintf("--%sparallel.streamhost=%s", prefix, ginkgo.StreamHost))
}
if ginkgo.SyncHost != "" {
result = append(result, fmt.Sprintf("--%sparallel.synchost=%s", prefix, ginkgo.SyncHost))
}
if ginkgo.RegexScansFilePath {
result = append(result, fmt.Sprintf("--%sregexScansFilePath", prefix))
}
if reporter.NoColor {
result = append(result, fmt.Sprintf("--%snoColor", prefix))
}
if reporter.SlowSpecThreshold > 0 {
result = append(result, fmt.Sprintf("--%sslowSpecThreshold=%.5f", prefix, reporter.SlowSpecThreshold))
}
if !reporter.NoisyPendings {
result = append(result, fmt.Sprintf("--%snoisyPendings=false", prefix))
}
if !reporter.NoisySkippings {
result = append(result, fmt.Sprintf("--%snoisySkippings=false", prefix))
}
if reporter.Verbose {
result = append(result, fmt.Sprintf("--%sv", prefix))
}
if reporter.Succinct {
result = append(result, fmt.Sprintf("--%ssuccinct", prefix))
}
if reporter.FullTrace {
result = append(result, fmt.Sprintf("--%strace", prefix))
}
return result
}

609
vendor/github.com/onsi/ginkgo/ginkgo_dsl.go generated vendored Normal file
View file

@ -0,0 +1,609 @@
/*
Ginkgo is a BDD-style testing framework for Golang
The godoc documentation describes Ginkgo's API. More comprehensive documentation (with examples!) is available at http://onsi.github.io/ginkgo/
Ginkgo's preferred matcher library is [Gomega](http://github.com/onsi/gomega)
Ginkgo on Github: http://github.com/onsi/ginkgo
Ginkgo is MIT-Licensed
*/
package ginkgo
import (
"flag"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/onsi/ginkgo/config"
"github.com/onsi/ginkgo/internal/codelocation"
"github.com/onsi/ginkgo/internal/failer"
"github.com/onsi/ginkgo/internal/remote"
"github.com/onsi/ginkgo/internal/suite"
"github.com/onsi/ginkgo/internal/testingtproxy"
"github.com/onsi/ginkgo/internal/writer"
"github.com/onsi/ginkgo/reporters"
"github.com/onsi/ginkgo/reporters/stenographer"
colorable "github.com/onsi/ginkgo/reporters/stenographer/support/go-colorable"
"github.com/onsi/ginkgo/types"
)
const GINKGO_VERSION = config.VERSION
const GINKGO_PANIC = `
Your test failed.
Ginkgo panics to prevent subsequent assertions from running.
Normally Ginkgo rescues this panic so you shouldn't see it.
But, if you make an assertion in a goroutine, Ginkgo can't capture the panic.
To circumvent this, you should call
defer GinkgoRecover()
at the top of the goroutine that caused this panic.
`
const defaultTimeout = 1
var globalSuite *suite.Suite
var globalFailer *failer.Failer
func init() {
config.Flags(flag.CommandLine, "ginkgo", true)
GinkgoWriter = writer.New(os.Stdout)
globalFailer = failer.New()
globalSuite = suite.New(globalFailer)
}
//GinkgoWriter implements an io.Writer
//When running in verbose mode any writes to GinkgoWriter will be immediately printed
//to stdout. Otherwise, GinkgoWriter will buffer any writes produced during the current test and flush them to screen
//only if the current test fails.
var GinkgoWriter io.Writer
//The interface by which Ginkgo receives *testing.T
type GinkgoTestingT interface {
Fail()
}
//GinkgoRandomSeed returns the seed used to randomize spec execution order. It is
//useful for seeding your own pseudorandom number generators (PRNGs) to ensure
//consistent executions from run to run, where your tests contain variability (for
//example, when selecting random test data).
func GinkgoRandomSeed() int64 {
return config.GinkgoConfig.RandomSeed
}
//GinkgoParallelNode returns the parallel node number for the current ginkgo process
//The node number is 1-indexed
func GinkgoParallelNode() int {
return config.GinkgoConfig.ParallelNode
}
//Some matcher libraries or legacy codebases require a *testing.T
//GinkgoT implements an interface analogous to *testing.T and can be used if
//the library in question accepts *testing.T through an interface
//
// For example, with testify:
// assert.Equal(GinkgoT(), 123, 123, "they should be equal")
//
// Or with gomock:
// gomock.NewController(GinkgoT())
//
// GinkgoT() takes an optional offset argument that can be used to get the
// correct line number associated with the failure.
func GinkgoT(optionalOffset ...int) GinkgoTInterface {
offset := 3
if len(optionalOffset) > 0 {
offset = optionalOffset[0]
}
return testingtproxy.New(GinkgoWriter, Fail, offset)
}
//The interface returned by GinkgoT(). This covers most of the methods
//in the testing package's T.
type GinkgoTInterface interface {
Fail()
Error(args ...interface{})
Errorf(format string, args ...interface{})
FailNow()
Fatal(args ...interface{})
Fatalf(format string, args ...interface{})
Log(args ...interface{})
Logf(format string, args ...interface{})
Failed() bool
Parallel()
Skip(args ...interface{})
Skipf(format string, args ...interface{})
SkipNow()
Skipped() bool
}
//Custom Ginkgo test reporters must implement the Reporter interface.
//
//The custom reporter is passed in a SuiteSummary when the suite begins and ends,
//and a SpecSummary just before a spec begins and just after a spec ends
type Reporter reporters.Reporter
//Asynchronous specs are given a channel of the Done type. You must close or write to the channel
//to tell Ginkgo that your async test is done.
type Done chan<- interface{}
//GinkgoTestDescription represents the information about the current running test returned by CurrentGinkgoTestDescription
// FullTestText: a concatenation of ComponentTexts and the TestText
// ComponentTexts: a list of all texts for the Describes & Contexts leading up to the current test
// TestText: the text in the actual It or Measure node
// IsMeasurement: true if the current test is a measurement
// FileName: the name of the file containing the current test
// LineNumber: the line number for the current test
// Failed: if the current test has failed, this will be true (useful in an AfterEach)
type GinkgoTestDescription struct {
FullTestText string
ComponentTexts []string
TestText string
IsMeasurement bool
FileName string
LineNumber int
Failed bool
Duration time.Duration
}
//CurrentGinkgoTestDescripton returns information about the current running test.
func CurrentGinkgoTestDescription() GinkgoTestDescription {
summary, ok := globalSuite.CurrentRunningSpecSummary()
if !ok {
return GinkgoTestDescription{}
}
subjectCodeLocation := summary.ComponentCodeLocations[len(summary.ComponentCodeLocations)-1]
return GinkgoTestDescription{
ComponentTexts: summary.ComponentTexts[1:],
FullTestText: strings.Join(summary.ComponentTexts[1:], " "),
TestText: summary.ComponentTexts[len(summary.ComponentTexts)-1],
IsMeasurement: summary.IsMeasurement,
FileName: subjectCodeLocation.FileName,
LineNumber: subjectCodeLocation.LineNumber,
Failed: summary.HasFailureState(),
Duration: summary.RunTime,
}
}
//Measurement tests receive a Benchmarker.
//
//You use the Time() function to time how long the passed in body function takes to run
//You use the RecordValue() function to track arbitrary numerical measurements.
//The RecordValueWithPrecision() function can be used alternatively to provide the unit
//and resolution of the numeric measurement.
//The optional info argument is passed to the test reporter and can be used to
// provide the measurement data to a custom reporter with context.
//
//See http://onsi.github.io/ginkgo/#benchmark_tests for more details
type Benchmarker interface {
Time(name string, body func(), info ...interface{}) (elapsedTime time.Duration)
RecordValue(name string, value float64, info ...interface{})
RecordValueWithPrecision(name string, value float64, units string, precision int, info ...interface{})
}
//RunSpecs is the entry point for the Ginkgo test runner.
//You must call this within a Golang testing TestX(t *testing.T) function.
//
//To bootstrap a test suite you can use the Ginkgo CLI:
//
// ginkgo bootstrap
func RunSpecs(t GinkgoTestingT, description string) bool {
specReporters := []Reporter{buildDefaultReporter()}
return RunSpecsWithCustomReporters(t, description, specReporters)
}
//To run your tests with Ginkgo's default reporter and your custom reporter(s), replace
//RunSpecs() with this method.
func RunSpecsWithDefaultAndCustomReporters(t GinkgoTestingT, description string, specReporters []Reporter) bool {
specReporters = append(specReporters, buildDefaultReporter())
return RunSpecsWithCustomReporters(t, description, specReporters)
}
//To run your tests with your custom reporter(s) (and *not* Ginkgo's default reporter), replace
//RunSpecs() with this method. Note that parallel tests will not work correctly without the default reporter
func RunSpecsWithCustomReporters(t GinkgoTestingT, description string, specReporters []Reporter) bool {
writer := GinkgoWriter.(*writer.Writer)
writer.SetStream(config.DefaultReporterConfig.Verbose)
reporters := make([]reporters.Reporter, len(specReporters))
for i, reporter := range specReporters {
reporters[i] = reporter
}
passed, hasFocusedTests := globalSuite.Run(t, description, reporters, writer, config.GinkgoConfig)
if passed && hasFocusedTests && strings.TrimSpace(os.Getenv("GINKGO_EDITOR_INTEGRATION")) == "" {
fmt.Println("PASS | FOCUSED")
os.Exit(types.GINKGO_FOCUS_EXIT_CODE)
}
return passed
}
func buildDefaultReporter() Reporter {
remoteReportingServer := config.GinkgoConfig.StreamHost
if remoteReportingServer == "" {
stenographer := stenographer.New(!config.DefaultReporterConfig.NoColor, config.GinkgoConfig.FlakeAttempts > 1, colorable.NewColorableStdout())
return reporters.NewDefaultReporter(config.DefaultReporterConfig, stenographer)
} else {
debugFile := ""
if config.GinkgoConfig.DebugParallel {
debugFile = fmt.Sprintf("ginkgo-node-%d.log", config.GinkgoConfig.ParallelNode)
}
return remote.NewForwardingReporter(config.DefaultReporterConfig, remoteReportingServer, &http.Client{}, remote.NewOutputInterceptor(), GinkgoWriter.(*writer.Writer), debugFile)
}
}
//Skip notifies Ginkgo that the current spec was skipped.
func Skip(message string, callerSkip ...int) {
skip := 0
if len(callerSkip) > 0 {
skip = callerSkip[0]
}
globalFailer.Skip(message, codelocation.New(skip+1))
panic(GINKGO_PANIC)
}
//Fail notifies Ginkgo that the current spec has failed. (Gomega will call Fail for you automatically when an assertion fails.)
func Fail(message string, callerSkip ...int) {
skip := 0
if len(callerSkip) > 0 {
skip = callerSkip[0]
}
globalFailer.Fail(message, codelocation.New(skip+1))
panic(GINKGO_PANIC)
}
//GinkgoRecover should be deferred at the top of any spawned goroutine that (may) call `Fail`
//Since Gomega assertions call fail, you should throw a `defer GinkgoRecover()` at the top of any goroutine that
//calls out to Gomega
//
//Here's why: Ginkgo's `Fail` method records the failure and then panics to prevent
//further assertions from running. This panic must be recovered. Ginkgo does this for you
//if the panic originates in a Ginkgo node (an It, BeforeEach, etc...)
//
//Unfortunately, if a panic originates on a goroutine *launched* from one of these nodes there's no
//way for Ginkgo to rescue the panic. To do this, you must remember to `defer GinkgoRecover()` at the top of such a goroutine.
func GinkgoRecover() {
e := recover()
if e != nil {
globalFailer.Panic(codelocation.New(1), e)
}
}
//Describe blocks allow you to organize your specs. A Describe block can contain any number of
//BeforeEach, AfterEach, JustBeforeEach, It, and Measurement blocks.
//
//In addition you can nest Describe, Context and When blocks. Describe, Context and When blocks are functionally
//equivalent. The difference is purely semantic -- you typical Describe the behavior of an object
//or method and, within that Describe, outline a number of Contexts and Whens.
func Describe(text string, body func()) bool {
globalSuite.PushContainerNode(text, body, types.FlagTypeNone, codelocation.New(1))
return true
}
//You can focus the tests within a describe block using FDescribe
func FDescribe(text string, body func()) bool {
globalSuite.PushContainerNode(text, body, types.FlagTypeFocused, codelocation.New(1))
return true
}
//You can mark the tests within a describe block as pending using PDescribe
func PDescribe(text string, body func()) bool {
globalSuite.PushContainerNode(text, body, types.FlagTypePending, codelocation.New(1))
return true
}
//You can mark the tests within a describe block as pending using XDescribe
func XDescribe(text string, body func()) bool {
globalSuite.PushContainerNode(text, body, types.FlagTypePending, codelocation.New(1))
return true
}
//Context blocks allow you to organize your specs. A Context block can contain any number of
//BeforeEach, AfterEach, JustBeforeEach, It, and Measurement blocks.
//
//In addition you can nest Describe, Context and When blocks. Describe, Context and When blocks are functionally
//equivalent. The difference is purely semantic -- you typical Describe the behavior of an object
//or method and, within that Describe, outline a number of Contexts and Whens.
func Context(text string, body func()) bool {
globalSuite.PushContainerNode(text, body, types.FlagTypeNone, codelocation.New(1))
return true
}
//You can focus the tests within a describe block using FContext
func FContext(text string, body func()) bool {
globalSuite.PushContainerNode(text, body, types.FlagTypeFocused, codelocation.New(1))
return true
}
//You can mark the tests within a describe block as pending using PContext
func PContext(text string, body func()) bool {
globalSuite.PushContainerNode(text, body, types.FlagTypePending, codelocation.New(1))
return true
}
//You can mark the tests within a describe block as pending using XContext
func XContext(text string, body func()) bool {
globalSuite.PushContainerNode(text, body, types.FlagTypePending, codelocation.New(1))
return true
}
//When blocks allow you to organize your specs. A When block can contain any number of
//BeforeEach, AfterEach, JustBeforeEach, It, and Measurement blocks.
//
//In addition you can nest Describe, Context and When blocks. Describe, Context and When blocks are functionally
//equivalent. The difference is purely semantic -- you typical Describe the behavior of an object
//or method and, within that Describe, outline a number of Contexts and Whens.
func When(text string, body func()) bool {
globalSuite.PushContainerNode("when "+text, body, types.FlagTypeNone, codelocation.New(1))
return true
}
//You can focus the tests within a describe block using FWhen
func FWhen(text string, body func()) bool {
globalSuite.PushContainerNode("when "+text, body, types.FlagTypeFocused, codelocation.New(1))
return true
}
//You can mark the tests within a describe block as pending using PWhen
func PWhen(text string, body func()) bool {
globalSuite.PushContainerNode("when "+text, body, types.FlagTypePending, codelocation.New(1))
return true
}
//You can mark the tests within a describe block as pending using XWhen
func XWhen(text string, body func()) bool {
globalSuite.PushContainerNode("when "+text, body, types.FlagTypePending, codelocation.New(1))
return true
}
//It blocks contain your test code and assertions. You cannot nest any other Ginkgo blocks
//within an It block.
//
//Ginkgo will normally run It blocks synchronously. To perform asynchronous tests, pass a
//function that accepts a Done channel. When you do this, you can also provide an optional timeout.
func It(text string, body interface{}, timeout ...float64) bool {
globalSuite.PushItNode(text, body, types.FlagTypeNone, codelocation.New(1), parseTimeout(timeout...))
return true
}
//You can focus individual Its using FIt
func FIt(text string, body interface{}, timeout ...float64) bool {
globalSuite.PushItNode(text, body, types.FlagTypeFocused, codelocation.New(1), parseTimeout(timeout...))
return true
}
//You can mark Its as pending using PIt
func PIt(text string, _ ...interface{}) bool {
globalSuite.PushItNode(text, func() {}, types.FlagTypePending, codelocation.New(1), 0)
return true
}
//You can mark Its as pending using XIt
func XIt(text string, _ ...interface{}) bool {
globalSuite.PushItNode(text, func() {}, types.FlagTypePending, codelocation.New(1), 0)
return true
}
//Specify blocks are aliases for It blocks and allow for more natural wording in situations
//which "It" does not fit into a natural sentence flow. All the same protocols apply for Specify blocks
//which apply to It blocks.
func Specify(text string, body interface{}, timeout ...float64) bool {
globalSuite.PushItNode(text, body, types.FlagTypeNone, codelocation.New(1), parseTimeout(timeout...))
return true
}
//You can focus individual Specifys using FSpecify
func FSpecify(text string, body interface{}, timeout ...float64) bool {
globalSuite.PushItNode(text, body, types.FlagTypeFocused, codelocation.New(1), parseTimeout(timeout...))
return true
}
//You can mark Specifys as pending using PSpecify
func PSpecify(text string, is ...interface{}) bool {
globalSuite.PushItNode(text, func() {}, types.FlagTypePending, codelocation.New(1), 0)
return true
}
//You can mark Specifys as pending using XSpecify
func XSpecify(text string, is ...interface{}) bool {
globalSuite.PushItNode(text, func() {}, types.FlagTypePending, codelocation.New(1), 0)
return true
}
//By allows you to better document large Its.
//
//Generally you should try to keep your Its short and to the point. This is not always possible, however,
//especially in the context of integration tests that capture a particular workflow.
//
//By allows you to document such flows. By must be called within a runnable node (It, BeforeEach, Measure, etc...)
//By will simply log the passed in text to the GinkgoWriter. If By is handed a function it will immediately run the function.
func By(text string, callbacks ...func()) {
preamble := "\x1b[1mSTEP\x1b[0m"
if config.DefaultReporterConfig.NoColor {
preamble = "STEP"
}
fmt.Fprintln(GinkgoWriter, preamble+": "+text)
if len(callbacks) == 1 {
callbacks[0]()
}
if len(callbacks) > 1 {
panic("just one callback per By, please")
}
}
//Measure blocks run the passed in body function repeatedly (determined by the samples argument)
//and accumulate metrics provided to the Benchmarker by the body function.
//
//The body function must have the signature:
// func(b Benchmarker)
func Measure(text string, body interface{}, samples int) bool {
globalSuite.PushMeasureNode(text, body, types.FlagTypeNone, codelocation.New(1), samples)
return true
}
//You can focus individual Measures using FMeasure
func FMeasure(text string, body interface{}, samples int) bool {
globalSuite.PushMeasureNode(text, body, types.FlagTypeFocused, codelocation.New(1), samples)
return true
}
//You can mark Maeasurements as pending using PMeasure
func PMeasure(text string, _ ...interface{}) bool {
globalSuite.PushMeasureNode(text, func(b Benchmarker) {}, types.FlagTypePending, codelocation.New(1), 0)
return true
}
//You can mark Maeasurements as pending using XMeasure
func XMeasure(text string, _ ...interface{}) bool {
globalSuite.PushMeasureNode(text, func(b Benchmarker) {}, types.FlagTypePending, codelocation.New(1), 0)
return true
}
//BeforeSuite blocks are run just once before any specs are run. When running in parallel, each
//parallel node process will call BeforeSuite.
//
//BeforeSuite blocks can be made asynchronous by providing a body function that accepts a Done channel
//
//You may only register *one* BeforeSuite handler per test suite. You typically do so in your bootstrap file at the top level.
func BeforeSuite(body interface{}, timeout ...float64) bool {
globalSuite.SetBeforeSuiteNode(body, codelocation.New(1), parseTimeout(timeout...))
return true
}
//AfterSuite blocks are *always* run after all the specs regardless of whether specs have passed or failed.
//Moreover, if Ginkgo receives an interrupt signal (^C) it will attempt to run the AfterSuite before exiting.
//
//When running in parallel, each parallel node process will call AfterSuite.
//
//AfterSuite blocks can be made asynchronous by providing a body function that accepts a Done channel
//
//You may only register *one* AfterSuite handler per test suite. You typically do so in your bootstrap file at the top level.
func AfterSuite(body interface{}, timeout ...float64) bool {
globalSuite.SetAfterSuiteNode(body, codelocation.New(1), parseTimeout(timeout...))
return true
}
//SynchronizedBeforeSuite blocks are primarily meant to solve the problem of setting up singleton external resources shared across
//nodes when running tests in parallel. For example, say you have a shared database that you can only start one instance of that
//must be used in your tests. When running in parallel, only one node should set up the database and all other nodes should wait
//until that node is done before running.
//
//SynchronizedBeforeSuite accomplishes this by taking *two* function arguments. The first is only run on parallel node #1. The second is
//run on all nodes, but *only* after the first function completes succesfully. Ginkgo also makes it possible to send data from the first function (on Node 1)
//to the second function (on all the other nodes).
//
//The functions have the following signatures. The first function (which only runs on node 1) has the signature:
//
// func() []byte
//
//or, to run asynchronously:
//
// func(done Done) []byte
//
//The byte array returned by the first function is then passed to the second function, which has the signature:
//
// func(data []byte)
//
//or, to run asynchronously:
//
// func(data []byte, done Done)
//
//Here's a simple pseudo-code example that starts a shared database on Node 1 and shares the database's address with the other nodes:
//
// var dbClient db.Client
// var dbRunner db.Runner
//
// var _ = SynchronizedBeforeSuite(func() []byte {
// dbRunner = db.NewRunner()
// err := dbRunner.Start()
// Ω(err).ShouldNot(HaveOccurred())
// return []byte(dbRunner.URL)
// }, func(data []byte) {
// dbClient = db.NewClient()
// err := dbClient.Connect(string(data))
// Ω(err).ShouldNot(HaveOccurred())
// })
func SynchronizedBeforeSuite(node1Body interface{}, allNodesBody interface{}, timeout ...float64) bool {
globalSuite.SetSynchronizedBeforeSuiteNode(
node1Body,
allNodesBody,
codelocation.New(1),
parseTimeout(timeout...),
)
return true
}
//SynchronizedAfterSuite blocks complement the SynchronizedBeforeSuite blocks in solving the problem of setting up
//external singleton resources shared across nodes when running tests in parallel.
//
//SynchronizedAfterSuite accomplishes this by taking *two* function arguments. The first runs on all nodes. The second runs only on parallel node #1
//and *only* after all other nodes have finished and exited. This ensures that node 1, and any resources it is running, remain alive until
//all other nodes are finished.
//
//Both functions have the same signature: either func() or func(done Done) to run asynchronously.
//
//Here's a pseudo-code example that complements that given in SynchronizedBeforeSuite. Here, SynchronizedAfterSuite is used to tear down the shared database
//only after all nodes have finished:
//
// var _ = SynchronizedAfterSuite(func() {
// dbClient.Cleanup()
// }, func() {
// dbRunner.Stop()
// })
func SynchronizedAfterSuite(allNodesBody interface{}, node1Body interface{}, timeout ...float64) bool {
globalSuite.SetSynchronizedAfterSuiteNode(
allNodesBody,
node1Body,
codelocation.New(1),
parseTimeout(timeout...),
)
return true
}
//BeforeEach blocks are run before It blocks. When multiple BeforeEach blocks are defined in nested
//Describe and Context blocks the outermost BeforeEach blocks are run first.
//
//Like It blocks, BeforeEach blocks can be made asynchronous by providing a body function that accepts
//a Done channel
func BeforeEach(body interface{}, timeout ...float64) bool {
globalSuite.PushBeforeEachNode(body, codelocation.New(1), parseTimeout(timeout...))
return true
}
//JustBeforeEach blocks are run before It blocks but *after* all BeforeEach blocks. For more details,
//read the [documentation](http://onsi.github.io/ginkgo/#separating_creation_and_configuration_)
//
//Like It blocks, BeforeEach blocks can be made asynchronous by providing a body function that accepts
//a Done channel
func JustBeforeEach(body interface{}, timeout ...float64) bool {
globalSuite.PushJustBeforeEachNode(body, codelocation.New(1), parseTimeout(timeout...))
return true
}
//AfterEach blocks are run after It blocks. When multiple AfterEach blocks are defined in nested
//Describe and Context blocks the innermost AfterEach blocks are run first.
//
//Like It blocks, AfterEach blocks can be made asynchronous by providing a body function that accepts
//a Done channel
func AfterEach(body interface{}, timeout ...float64) bool {
globalSuite.PushAfterEachNode(body, codelocation.New(1), parseTimeout(timeout...))
return true
}
func parseTimeout(timeout ...float64) time.Duration {
if len(timeout) == 0 {
return time.Duration(defaultTimeout * int64(time.Second))
} else {
return time.Duration(timeout[0] * float64(time.Second))
}
}

View file

@ -0,0 +1,32 @@
package codelocation
import (
"regexp"
"runtime"
"runtime/debug"
"strings"
"github.com/onsi/ginkgo/types"
)
func New(skip int) types.CodeLocation {
_, file, line, _ := runtime.Caller(skip + 1)
stackTrace := PruneStack(string(debug.Stack()), skip)
return types.CodeLocation{FileName: file, LineNumber: line, FullStackTrace: stackTrace}
}
func PruneStack(fullStackTrace string, skip int) string {
stack := strings.Split(fullStackTrace, "\n")
if len(stack) > 2*(skip+1) {
stack = stack[2*(skip+1):]
}
prunedStack := []string{}
re := regexp.MustCompile(`\/ginkgo\/|\/pkg\/testing\/|\/pkg\/runtime\/`)
for i := 0; i < len(stack)/2; i++ {
if !re.Match([]byte(stack[i*2])) {
prunedStack = append(prunedStack, stack[i*2])
prunedStack = append(prunedStack, stack[i*2+1])
}
}
return strings.Join(prunedStack, "\n")
}

View file

@ -0,0 +1,151 @@
package containernode
import (
"math/rand"
"sort"
"github.com/onsi/ginkgo/internal/leafnodes"
"github.com/onsi/ginkgo/types"
)
type subjectOrContainerNode struct {
containerNode *ContainerNode
subjectNode leafnodes.SubjectNode
}
func (n subjectOrContainerNode) text() string {
if n.containerNode != nil {
return n.containerNode.Text()
} else {
return n.subjectNode.Text()
}
}
type CollatedNodes struct {
Containers []*ContainerNode
Subject leafnodes.SubjectNode
}
type ContainerNode struct {
text string
flag types.FlagType
codeLocation types.CodeLocation
setupNodes []leafnodes.BasicNode
subjectAndContainerNodes []subjectOrContainerNode
}
func New(text string, flag types.FlagType, codeLocation types.CodeLocation) *ContainerNode {
return &ContainerNode{
text: text,
flag: flag,
codeLocation: codeLocation,
}
}
func (container *ContainerNode) Shuffle(r *rand.Rand) {
sort.Sort(container)
permutation := r.Perm(len(container.subjectAndContainerNodes))
shuffledNodes := make([]subjectOrContainerNode, len(container.subjectAndContainerNodes))
for i, j := range permutation {
shuffledNodes[i] = container.subjectAndContainerNodes[j]
}
container.subjectAndContainerNodes = shuffledNodes
}
func (node *ContainerNode) BackPropagateProgrammaticFocus() bool {
if node.flag == types.FlagTypePending {
return false
}
shouldUnfocus := false
for _, subjectOrContainerNode := range node.subjectAndContainerNodes {
if subjectOrContainerNode.containerNode != nil {
shouldUnfocus = subjectOrContainerNode.containerNode.BackPropagateProgrammaticFocus() || shouldUnfocus
} else {
shouldUnfocus = (subjectOrContainerNode.subjectNode.Flag() == types.FlagTypeFocused) || shouldUnfocus
}
}
if shouldUnfocus {
if node.flag == types.FlagTypeFocused {
node.flag = types.FlagTypeNone
}
return true
}
return node.flag == types.FlagTypeFocused
}
func (node *ContainerNode) Collate() []CollatedNodes {
return node.collate([]*ContainerNode{})
}
func (node *ContainerNode) collate(enclosingContainers []*ContainerNode) []CollatedNodes {
collated := make([]CollatedNodes, 0)
containers := make([]*ContainerNode, len(enclosingContainers))
copy(containers, enclosingContainers)
containers = append(containers, node)
for _, subjectOrContainer := range node.subjectAndContainerNodes {
if subjectOrContainer.containerNode != nil {
collated = append(collated, subjectOrContainer.containerNode.collate(containers)...)
} else {
collated = append(collated, CollatedNodes{
Containers: containers,
Subject: subjectOrContainer.subjectNode,
})
}
}
return collated
}
func (node *ContainerNode) PushContainerNode(container *ContainerNode) {
node.subjectAndContainerNodes = append(node.subjectAndContainerNodes, subjectOrContainerNode{containerNode: container})
}
func (node *ContainerNode) PushSubjectNode(subject leafnodes.SubjectNode) {
node.subjectAndContainerNodes = append(node.subjectAndContainerNodes, subjectOrContainerNode{subjectNode: subject})
}
func (node *ContainerNode) PushSetupNode(setupNode leafnodes.BasicNode) {
node.setupNodes = append(node.setupNodes, setupNode)
}
func (node *ContainerNode) SetupNodesOfType(nodeType types.SpecComponentType) []leafnodes.BasicNode {
nodes := []leafnodes.BasicNode{}
for _, setupNode := range node.setupNodes {
if setupNode.Type() == nodeType {
nodes = append(nodes, setupNode)
}
}
return nodes
}
func (node *ContainerNode) Text() string {
return node.text
}
func (node *ContainerNode) CodeLocation() types.CodeLocation {
return node.codeLocation
}
func (node *ContainerNode) Flag() types.FlagType {
return node.flag
}
//sort.Interface
func (node *ContainerNode) Len() int {
return len(node.subjectAndContainerNodes)
}
func (node *ContainerNode) Less(i, j int) bool {
return node.subjectAndContainerNodes[i].text() < node.subjectAndContainerNodes[j].text()
}
func (node *ContainerNode) Swap(i, j int) {
node.subjectAndContainerNodes[i], node.subjectAndContainerNodes[j] = node.subjectAndContainerNodes[j], node.subjectAndContainerNodes[i]
}

View file

@ -0,0 +1,92 @@
package failer
import (
"fmt"
"sync"
"github.com/onsi/ginkgo/types"
)
type Failer struct {
lock *sync.Mutex
failure types.SpecFailure
state types.SpecState
}
func New() *Failer {
return &Failer{
lock: &sync.Mutex{},
state: types.SpecStatePassed,
}
}
func (f *Failer) Panic(location types.CodeLocation, forwardedPanic interface{}) {
f.lock.Lock()
defer f.lock.Unlock()
if f.state == types.SpecStatePassed {
f.state = types.SpecStatePanicked
f.failure = types.SpecFailure{
Message: "Test Panicked",
Location: location,
ForwardedPanic: fmt.Sprintf("%v", forwardedPanic),
}
}
}
func (f *Failer) Timeout(location types.CodeLocation) {
f.lock.Lock()
defer f.lock.Unlock()
if f.state == types.SpecStatePassed {
f.state = types.SpecStateTimedOut
f.failure = types.SpecFailure{
Message: "Timed out",
Location: location,
}
}
}
func (f *Failer) Fail(message string, location types.CodeLocation) {
f.lock.Lock()
defer f.lock.Unlock()
if f.state == types.SpecStatePassed {
f.state = types.SpecStateFailed
f.failure = types.SpecFailure{
Message: message,
Location: location,
}
}
}
func (f *Failer) Drain(componentType types.SpecComponentType, componentIndex int, componentCodeLocation types.CodeLocation) (types.SpecFailure, types.SpecState) {
f.lock.Lock()
defer f.lock.Unlock()
failure := f.failure
outcome := f.state
if outcome != types.SpecStatePassed {
failure.ComponentType = componentType
failure.ComponentIndex = componentIndex
failure.ComponentCodeLocation = componentCodeLocation
}
f.state = types.SpecStatePassed
f.failure = types.SpecFailure{}
return failure, outcome
}
func (f *Failer) Skip(message string, location types.CodeLocation) {
f.lock.Lock()
defer f.lock.Unlock()
if f.state == types.SpecStatePassed {
f.state = types.SpecStateSkipped
f.failure = types.SpecFailure{
Message: message,
Location: location,
}
}
}

View file

@ -0,0 +1,103 @@
package leafnodes
import (
"math"
"time"
"sync"
"github.com/onsi/ginkgo/types"
)
type benchmarker struct {
mu sync.Mutex
measurements map[string]*types.SpecMeasurement
orderCounter int
}
func newBenchmarker() *benchmarker {
return &benchmarker{
measurements: make(map[string]*types.SpecMeasurement, 0),
}
}
func (b *benchmarker) Time(name string, body func(), info ...interface{}) (elapsedTime time.Duration) {
t := time.Now()
body()
elapsedTime = time.Since(t)
b.mu.Lock()
defer b.mu.Unlock()
measurement := b.getMeasurement(name, "Fastest Time", "Slowest Time", "Average Time", "s", 3, info...)
measurement.Results = append(measurement.Results, elapsedTime.Seconds())
return
}
func (b *benchmarker) RecordValue(name string, value float64, info ...interface{}) {
b.mu.Lock()
measurement := b.getMeasurement(name, "Smallest", " Largest", " Average", "", 3, info...)
defer b.mu.Unlock()
measurement.Results = append(measurement.Results, value)
}
func (b *benchmarker) RecordValueWithPrecision(name string, value float64, units string, precision int, info ...interface{}) {
b.mu.Lock()
measurement := b.getMeasurement(name, "Smallest", " Largest", " Average", units, precision, info...)
defer b.mu.Unlock()
measurement.Results = append(measurement.Results, value)
}
func (b *benchmarker) getMeasurement(name string, smallestLabel string, largestLabel string, averageLabel string, units string, precision int, info ...interface{}) *types.SpecMeasurement {
measurement, ok := b.measurements[name]
if !ok {
var computedInfo interface{}
computedInfo = nil
if len(info) > 0 {
computedInfo = info[0]
}
measurement = &types.SpecMeasurement{
Name: name,
Info: computedInfo,
Order: b.orderCounter,
SmallestLabel: smallestLabel,
LargestLabel: largestLabel,
AverageLabel: averageLabel,
Units: units,
Precision: precision,
Results: make([]float64, 0),
}
b.measurements[name] = measurement
b.orderCounter++
}
return measurement
}
func (b *benchmarker) measurementsReport() map[string]*types.SpecMeasurement {
b.mu.Lock()
defer b.mu.Unlock()
for _, measurement := range b.measurements {
measurement.Smallest = math.MaxFloat64
measurement.Largest = -math.MaxFloat64
sum := float64(0)
sumOfSquares := float64(0)
for _, result := range measurement.Results {
if result > measurement.Largest {
measurement.Largest = result
}
if result < measurement.Smallest {
measurement.Smallest = result
}
sum += result
sumOfSquares += result * result
}
n := float64(len(measurement.Results))
measurement.Average = sum / n
measurement.StdDeviation = math.Sqrt(sumOfSquares/n - (sum/n)*(sum/n))
}
return b.measurements
}

View file

@ -0,0 +1,19 @@
package leafnodes
import (
"github.com/onsi/ginkgo/types"
)
type BasicNode interface {
Type() types.SpecComponentType
Run() (types.SpecState, types.SpecFailure)
CodeLocation() types.CodeLocation
}
type SubjectNode interface {
BasicNode
Text() string
Flag() types.FlagType
Samples() int
}

View file

@ -0,0 +1,47 @@
package leafnodes
import (
"time"
"github.com/onsi/ginkgo/internal/failer"
"github.com/onsi/ginkgo/types"
)
type ItNode struct {
runner *runner
flag types.FlagType
text string
}
func NewItNode(text string, body interface{}, flag types.FlagType, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, componentIndex int) *ItNode {
return &ItNode{
runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeIt, componentIndex),
flag: flag,
text: text,
}
}
func (node *ItNode) Run() (outcome types.SpecState, failure types.SpecFailure) {
return node.runner.run()
}
func (node *ItNode) Type() types.SpecComponentType {
return types.SpecComponentTypeIt
}
func (node *ItNode) Text() string {
return node.text
}
func (node *ItNode) Flag() types.FlagType {
return node.flag
}
func (node *ItNode) CodeLocation() types.CodeLocation {
return node.runner.codeLocation
}
func (node *ItNode) Samples() int {
return 1
}

View file

@ -0,0 +1,62 @@
package leafnodes
import (
"reflect"
"github.com/onsi/ginkgo/internal/failer"
"github.com/onsi/ginkgo/types"
)
type MeasureNode struct {
runner *runner
text string
flag types.FlagType
samples int
benchmarker *benchmarker
}
func NewMeasureNode(text string, body interface{}, flag types.FlagType, codeLocation types.CodeLocation, samples int, failer *failer.Failer, componentIndex int) *MeasureNode {
benchmarker := newBenchmarker()
wrappedBody := func() {
reflect.ValueOf(body).Call([]reflect.Value{reflect.ValueOf(benchmarker)})
}
return &MeasureNode{
runner: newRunner(wrappedBody, codeLocation, 0, failer, types.SpecComponentTypeMeasure, componentIndex),
text: text,
flag: flag,
samples: samples,
benchmarker: benchmarker,
}
}
func (node *MeasureNode) Run() (outcome types.SpecState, failure types.SpecFailure) {
return node.runner.run()
}
func (node *MeasureNode) MeasurementsReport() map[string]*types.SpecMeasurement {
return node.benchmarker.measurementsReport()
}
func (node *MeasureNode) Type() types.SpecComponentType {
return types.SpecComponentTypeMeasure
}
func (node *MeasureNode) Text() string {
return node.text
}
func (node *MeasureNode) Flag() types.FlagType {
return node.flag
}
func (node *MeasureNode) CodeLocation() types.CodeLocation {
return node.runner.codeLocation
}
func (node *MeasureNode) Samples() int {
return node.samples
}

View file

@ -0,0 +1,117 @@
package leafnodes
import (
"fmt"
"reflect"
"time"
"github.com/onsi/ginkgo/internal/codelocation"
"github.com/onsi/ginkgo/internal/failer"
"github.com/onsi/ginkgo/types"
)
type runner struct {
isAsync bool
asyncFunc func(chan<- interface{})
syncFunc func()
codeLocation types.CodeLocation
timeoutThreshold time.Duration
nodeType types.SpecComponentType
componentIndex int
failer *failer.Failer
}
func newRunner(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, nodeType types.SpecComponentType, componentIndex int) *runner {
bodyType := reflect.TypeOf(body)
if bodyType.Kind() != reflect.Func {
panic(fmt.Sprintf("Expected a function but got something else at %v", codeLocation))
}
runner := &runner{
codeLocation: codeLocation,
timeoutThreshold: timeout,
failer: failer,
nodeType: nodeType,
componentIndex: componentIndex,
}
switch bodyType.NumIn() {
case 0:
runner.syncFunc = body.(func())
return runner
case 1:
if !(bodyType.In(0).Kind() == reflect.Chan && bodyType.In(0).Elem().Kind() == reflect.Interface) {
panic(fmt.Sprintf("Must pass a Done channel to function at %v", codeLocation))
}
wrappedBody := func(done chan<- interface{}) {
bodyValue := reflect.ValueOf(body)
bodyValue.Call([]reflect.Value{reflect.ValueOf(done)})
}
runner.isAsync = true
runner.asyncFunc = wrappedBody
return runner
}
panic(fmt.Sprintf("Too many arguments to function at %v", codeLocation))
}
func (r *runner) run() (outcome types.SpecState, failure types.SpecFailure) {
if r.isAsync {
return r.runAsync()
} else {
return r.runSync()
}
}
func (r *runner) runAsync() (outcome types.SpecState, failure types.SpecFailure) {
done := make(chan interface{}, 1)
go func() {
finished := false
defer func() {
if e := recover(); e != nil || !finished {
r.failer.Panic(codelocation.New(2), e)
select {
case <-done:
break
default:
close(done)
}
}
}()
r.asyncFunc(done)
finished = true
}()
// If this goroutine gets no CPU time before the select block,
// the <-done case may complete even if the test took longer than the timeoutThreshold.
// This can cause flaky behaviour, but we haven't seen it in the wild.
select {
case <-done:
case <-time.After(r.timeoutThreshold):
r.failer.Timeout(r.codeLocation)
}
failure, outcome = r.failer.Drain(r.nodeType, r.componentIndex, r.codeLocation)
return
}
func (r *runner) runSync() (outcome types.SpecState, failure types.SpecFailure) {
finished := false
defer func() {
if e := recover(); e != nil || !finished {
r.failer.Panic(codelocation.New(2), e)
}
failure, outcome = r.failer.Drain(r.nodeType, r.componentIndex, r.codeLocation)
}()
r.syncFunc()
finished = true
return
}

View file

@ -0,0 +1,42 @@
package leafnodes
import (
"time"
"github.com/onsi/ginkgo/internal/failer"
"github.com/onsi/ginkgo/types"
)
type SetupNode struct {
runner *runner
}
func (node *SetupNode) Run() (outcome types.SpecState, failure types.SpecFailure) {
return node.runner.run()
}
func (node *SetupNode) Type() types.SpecComponentType {
return node.runner.nodeType
}
func (node *SetupNode) CodeLocation() types.CodeLocation {
return node.runner.codeLocation
}
func NewBeforeEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, componentIndex int) *SetupNode {
return &SetupNode{
runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeBeforeEach, componentIndex),
}
}
func NewAfterEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, componentIndex int) *SetupNode {
return &SetupNode{
runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeAfterEach, componentIndex),
}
}
func NewJustBeforeEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer, componentIndex int) *SetupNode {
return &SetupNode{
runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeJustBeforeEach, componentIndex),
}
}

View file

@ -0,0 +1,55 @@
package leafnodes
import (
"time"
"github.com/onsi/ginkgo/internal/failer"
"github.com/onsi/ginkgo/types"
)
type SuiteNode interface {
Run(parallelNode int, parallelTotal int, syncHost string) bool
Passed() bool
Summary() *types.SetupSummary
}
type simpleSuiteNode struct {
runner *runner
outcome types.SpecState
failure types.SpecFailure
runTime time.Duration
}
func (node *simpleSuiteNode) Run(parallelNode int, parallelTotal int, syncHost string) bool {
t := time.Now()
node.outcome, node.failure = node.runner.run()
node.runTime = time.Since(t)
return node.outcome == types.SpecStatePassed
}
func (node *simpleSuiteNode) Passed() bool {
return node.outcome == types.SpecStatePassed
}
func (node *simpleSuiteNode) Summary() *types.SetupSummary {
return &types.SetupSummary{
ComponentType: node.runner.nodeType,
CodeLocation: node.runner.codeLocation,
State: node.outcome,
RunTime: node.runTime,
Failure: node.failure,
}
}
func NewBeforeSuiteNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer) SuiteNode {
return &simpleSuiteNode{
runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeBeforeSuite, 0),
}
}
func NewAfterSuiteNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer) SuiteNode {
return &simpleSuiteNode{
runner: newRunner(body, codeLocation, timeout, failer, types.SpecComponentTypeAfterSuite, 0),
}
}

View file

@ -0,0 +1,90 @@
package leafnodes
import (
"encoding/json"
"io/ioutil"
"net/http"
"time"
"github.com/onsi/ginkgo/internal/failer"
"github.com/onsi/ginkgo/types"
)
type synchronizedAfterSuiteNode struct {
runnerA *runner
runnerB *runner
outcome types.SpecState
failure types.SpecFailure
runTime time.Duration
}
func NewSynchronizedAfterSuiteNode(bodyA interface{}, bodyB interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer) SuiteNode {
return &synchronizedAfterSuiteNode{
runnerA: newRunner(bodyA, codeLocation, timeout, failer, types.SpecComponentTypeAfterSuite, 0),
runnerB: newRunner(bodyB, codeLocation, timeout, failer, types.SpecComponentTypeAfterSuite, 0),
}
}
func (node *synchronizedAfterSuiteNode) Run(parallelNode int, parallelTotal int, syncHost string) bool {
node.outcome, node.failure = node.runnerA.run()
if parallelNode == 1 {
if parallelTotal > 1 {
node.waitUntilOtherNodesAreDone(syncHost)
}
outcome, failure := node.runnerB.run()
if node.outcome == types.SpecStatePassed {
node.outcome, node.failure = outcome, failure
}
}
return node.outcome == types.SpecStatePassed
}
func (node *synchronizedAfterSuiteNode) Passed() bool {
return node.outcome == types.SpecStatePassed
}
func (node *synchronizedAfterSuiteNode) Summary() *types.SetupSummary {
return &types.SetupSummary{
ComponentType: node.runnerA.nodeType,
CodeLocation: node.runnerA.codeLocation,
State: node.outcome,
RunTime: node.runTime,
Failure: node.failure,
}
}
func (node *synchronizedAfterSuiteNode) waitUntilOtherNodesAreDone(syncHost string) {
for {
if node.canRun(syncHost) {
return
}
time.Sleep(50 * time.Millisecond)
}
}
func (node *synchronizedAfterSuiteNode) canRun(syncHost string) bool {
resp, err := http.Get(syncHost + "/RemoteAfterSuiteData")
if err != nil || resp.StatusCode != http.StatusOK {
return false
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return false
}
resp.Body.Close()
afterSuiteData := types.RemoteAfterSuiteData{}
err = json.Unmarshal(body, &afterSuiteData)
if err != nil {
return false
}
return afterSuiteData.CanRun
}

View file

@ -0,0 +1,181 @@
package leafnodes
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"reflect"
"time"
"github.com/onsi/ginkgo/internal/failer"
"github.com/onsi/ginkgo/types"
)
type synchronizedBeforeSuiteNode struct {
runnerA *runner
runnerB *runner
data []byte
outcome types.SpecState
failure types.SpecFailure
runTime time.Duration
}
func NewSynchronizedBeforeSuiteNode(bodyA interface{}, bodyB interface{}, codeLocation types.CodeLocation, timeout time.Duration, failer *failer.Failer) SuiteNode {
node := &synchronizedBeforeSuiteNode{}
node.runnerA = newRunner(node.wrapA(bodyA), codeLocation, timeout, failer, types.SpecComponentTypeBeforeSuite, 0)
node.runnerB = newRunner(node.wrapB(bodyB), codeLocation, timeout, failer, types.SpecComponentTypeBeforeSuite, 0)
return node
}
func (node *synchronizedBeforeSuiteNode) Run(parallelNode int, parallelTotal int, syncHost string) bool {
t := time.Now()
defer func() {
node.runTime = time.Since(t)
}()
if parallelNode == 1 {
node.outcome, node.failure = node.runA(parallelTotal, syncHost)
} else {
node.outcome, node.failure = node.waitForA(syncHost)
}
if node.outcome != types.SpecStatePassed {
return false
}
node.outcome, node.failure = node.runnerB.run()
return node.outcome == types.SpecStatePassed
}
func (node *synchronizedBeforeSuiteNode) runA(parallelTotal int, syncHost string) (types.SpecState, types.SpecFailure) {
outcome, failure := node.runnerA.run()
if parallelTotal > 1 {
state := types.RemoteBeforeSuiteStatePassed
if outcome != types.SpecStatePassed {
state = types.RemoteBeforeSuiteStateFailed
}
json := (types.RemoteBeforeSuiteData{
Data: node.data,
State: state,
}).ToJSON()
http.Post(syncHost+"/BeforeSuiteState", "application/json", bytes.NewBuffer(json))
}
return outcome, failure
}
func (node *synchronizedBeforeSuiteNode) waitForA(syncHost string) (types.SpecState, types.SpecFailure) {
failure := func(message string) types.SpecFailure {
return types.SpecFailure{
Message: message,
Location: node.runnerA.codeLocation,
ComponentType: node.runnerA.nodeType,
ComponentIndex: node.runnerA.componentIndex,
ComponentCodeLocation: node.runnerA.codeLocation,
}
}
for {
resp, err := http.Get(syncHost + "/BeforeSuiteState")
if err != nil || resp.StatusCode != http.StatusOK {
return types.SpecStateFailed, failure("Failed to fetch BeforeSuite state")
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return types.SpecStateFailed, failure("Failed to read BeforeSuite state")
}
resp.Body.Close()
beforeSuiteData := types.RemoteBeforeSuiteData{}
err = json.Unmarshal(body, &beforeSuiteData)
if err != nil {
return types.SpecStateFailed, failure("Failed to decode BeforeSuite state")
}
switch beforeSuiteData.State {
case types.RemoteBeforeSuiteStatePassed:
node.data = beforeSuiteData.Data
return types.SpecStatePassed, types.SpecFailure{}
case types.RemoteBeforeSuiteStateFailed:
return types.SpecStateFailed, failure("BeforeSuite on Node 1 failed")
case types.RemoteBeforeSuiteStateDisappeared:
return types.SpecStateFailed, failure("Node 1 disappeared before completing BeforeSuite")
}
time.Sleep(50 * time.Millisecond)
}
}
func (node *synchronizedBeforeSuiteNode) Passed() bool {
return node.outcome == types.SpecStatePassed
}
func (node *synchronizedBeforeSuiteNode) Summary() *types.SetupSummary {
return &types.SetupSummary{
ComponentType: node.runnerA.nodeType,
CodeLocation: node.runnerA.codeLocation,
State: node.outcome,
RunTime: node.runTime,
Failure: node.failure,
}
}
func (node *synchronizedBeforeSuiteNode) wrapA(bodyA interface{}) interface{} {
typeA := reflect.TypeOf(bodyA)
if typeA.Kind() != reflect.Func {
panic("SynchronizedBeforeSuite expects a function as its first argument")
}
takesNothing := typeA.NumIn() == 0
takesADoneChannel := typeA.NumIn() == 1 && typeA.In(0).Kind() == reflect.Chan && typeA.In(0).Elem().Kind() == reflect.Interface
returnsBytes := typeA.NumOut() == 1 && typeA.Out(0).Kind() == reflect.Slice && typeA.Out(0).Elem().Kind() == reflect.Uint8
if !((takesNothing || takesADoneChannel) && returnsBytes) {
panic("SynchronizedBeforeSuite's first argument should be a function that returns []byte and either takes no arguments or takes a Done channel.")
}
if takesADoneChannel {
return func(done chan<- interface{}) {
out := reflect.ValueOf(bodyA).Call([]reflect.Value{reflect.ValueOf(done)})
node.data = out[0].Interface().([]byte)
}
}
return func() {
out := reflect.ValueOf(bodyA).Call([]reflect.Value{})
node.data = out[0].Interface().([]byte)
}
}
func (node *synchronizedBeforeSuiteNode) wrapB(bodyB interface{}) interface{} {
typeB := reflect.TypeOf(bodyB)
if typeB.Kind() != reflect.Func {
panic("SynchronizedBeforeSuite expects a function as its second argument")
}
returnsNothing := typeB.NumOut() == 0
takesBytesOnly := typeB.NumIn() == 1 && typeB.In(0).Kind() == reflect.Slice && typeB.In(0).Elem().Kind() == reflect.Uint8
takesBytesAndDone := typeB.NumIn() == 2 &&
typeB.In(0).Kind() == reflect.Slice && typeB.In(0).Elem().Kind() == reflect.Uint8 &&
typeB.In(1).Kind() == reflect.Chan && typeB.In(1).Elem().Kind() == reflect.Interface
if !((takesBytesOnly || takesBytesAndDone) && returnsNothing) {
panic("SynchronizedBeforeSuite's second argument should be a function that returns nothing and either takes []byte or ([]byte, Done)")
}
if takesBytesAndDone {
return func(done chan<- interface{}) {
reflect.ValueOf(bodyB).Call([]reflect.Value{reflect.ValueOf(node.data), reflect.ValueOf(done)})
}
}
return func() {
reflect.ValueOf(bodyB).Call([]reflect.Value{reflect.ValueOf(node.data)})
}
}

View file

@ -0,0 +1,249 @@
/*
Aggregator is a reporter used by the Ginkgo CLI to aggregate and present parallel test output
coherently as tests complete. You shouldn't need to use this in your code. To run tests in parallel:
ginkgo -nodes=N
where N is the number of nodes you desire.
*/
package remote
import (
"time"
"github.com/onsi/ginkgo/config"
"github.com/onsi/ginkgo/reporters/stenographer"
"github.com/onsi/ginkgo/types"
)
type configAndSuite struct {
config config.GinkgoConfigType
summary *types.SuiteSummary
}
type Aggregator struct {
nodeCount int
config config.DefaultReporterConfigType
stenographer stenographer.Stenographer
result chan bool
suiteBeginnings chan configAndSuite
aggregatedSuiteBeginnings []configAndSuite
beforeSuites chan *types.SetupSummary
aggregatedBeforeSuites []*types.SetupSummary
afterSuites chan *types.SetupSummary
aggregatedAfterSuites []*types.SetupSummary
specCompletions chan *types.SpecSummary
completedSpecs []*types.SpecSummary
suiteEndings chan *types.SuiteSummary
aggregatedSuiteEndings []*types.SuiteSummary
specs []*types.SpecSummary
startTime time.Time
}
func NewAggregator(nodeCount int, result chan bool, config config.DefaultReporterConfigType, stenographer stenographer.Stenographer) *Aggregator {
aggregator := &Aggregator{
nodeCount: nodeCount,
result: result,
config: config,
stenographer: stenographer,
suiteBeginnings: make(chan configAndSuite, 0),
beforeSuites: make(chan *types.SetupSummary, 0),
afterSuites: make(chan *types.SetupSummary, 0),
specCompletions: make(chan *types.SpecSummary, 0),
suiteEndings: make(chan *types.SuiteSummary, 0),
}
go aggregator.mux()
return aggregator
}
func (aggregator *Aggregator) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) {
aggregator.suiteBeginnings <- configAndSuite{config, summary}
}
func (aggregator *Aggregator) BeforeSuiteDidRun(setupSummary *types.SetupSummary) {
aggregator.beforeSuites <- setupSummary
}
func (aggregator *Aggregator) AfterSuiteDidRun(setupSummary *types.SetupSummary) {
aggregator.afterSuites <- setupSummary
}
func (aggregator *Aggregator) SpecWillRun(specSummary *types.SpecSummary) {
//noop
}
func (aggregator *Aggregator) SpecDidComplete(specSummary *types.SpecSummary) {
aggregator.specCompletions <- specSummary
}
func (aggregator *Aggregator) SpecSuiteDidEnd(summary *types.SuiteSummary) {
aggregator.suiteEndings <- summary
}
func (aggregator *Aggregator) mux() {
loop:
for {
select {
case configAndSuite := <-aggregator.suiteBeginnings:
aggregator.registerSuiteBeginning(configAndSuite)
case setupSummary := <-aggregator.beforeSuites:
aggregator.registerBeforeSuite(setupSummary)
case setupSummary := <-aggregator.afterSuites:
aggregator.registerAfterSuite(setupSummary)
case specSummary := <-aggregator.specCompletions:
aggregator.registerSpecCompletion(specSummary)
case suite := <-aggregator.suiteEndings:
finished, passed := aggregator.registerSuiteEnding(suite)
if finished {
aggregator.result <- passed
break loop
}
}
}
}
func (aggregator *Aggregator) registerSuiteBeginning(configAndSuite configAndSuite) {
aggregator.aggregatedSuiteBeginnings = append(aggregator.aggregatedSuiteBeginnings, configAndSuite)
if len(aggregator.aggregatedSuiteBeginnings) == 1 {
aggregator.startTime = time.Now()
}
if len(aggregator.aggregatedSuiteBeginnings) != aggregator.nodeCount {
return
}
aggregator.stenographer.AnnounceSuite(configAndSuite.summary.SuiteDescription, configAndSuite.config.RandomSeed, configAndSuite.config.RandomizeAllSpecs, aggregator.config.Succinct)
totalNumberOfSpecs := 0
if len(aggregator.aggregatedSuiteBeginnings) > 0 {
totalNumberOfSpecs = configAndSuite.summary.NumberOfSpecsBeforeParallelization
}
aggregator.stenographer.AnnounceTotalNumberOfSpecs(totalNumberOfSpecs, aggregator.config.Succinct)
aggregator.stenographer.AnnounceAggregatedParallelRun(aggregator.nodeCount, aggregator.config.Succinct)
aggregator.flushCompletedSpecs()
}
func (aggregator *Aggregator) registerBeforeSuite(setupSummary *types.SetupSummary) {
aggregator.aggregatedBeforeSuites = append(aggregator.aggregatedBeforeSuites, setupSummary)
aggregator.flushCompletedSpecs()
}
func (aggregator *Aggregator) registerAfterSuite(setupSummary *types.SetupSummary) {
aggregator.aggregatedAfterSuites = append(aggregator.aggregatedAfterSuites, setupSummary)
aggregator.flushCompletedSpecs()
}
func (aggregator *Aggregator) registerSpecCompletion(specSummary *types.SpecSummary) {
aggregator.completedSpecs = append(aggregator.completedSpecs, specSummary)
aggregator.specs = append(aggregator.specs, specSummary)
aggregator.flushCompletedSpecs()
}
func (aggregator *Aggregator) flushCompletedSpecs() {
if len(aggregator.aggregatedSuiteBeginnings) != aggregator.nodeCount {
return
}
for _, setupSummary := range aggregator.aggregatedBeforeSuites {
aggregator.announceBeforeSuite(setupSummary)
}
for _, specSummary := range aggregator.completedSpecs {
aggregator.announceSpec(specSummary)
}
for _, setupSummary := range aggregator.aggregatedAfterSuites {
aggregator.announceAfterSuite(setupSummary)
}
aggregator.aggregatedBeforeSuites = []*types.SetupSummary{}
aggregator.completedSpecs = []*types.SpecSummary{}
aggregator.aggregatedAfterSuites = []*types.SetupSummary{}
}
func (aggregator *Aggregator) announceBeforeSuite(setupSummary *types.SetupSummary) {
aggregator.stenographer.AnnounceCapturedOutput(setupSummary.CapturedOutput)
if setupSummary.State != types.SpecStatePassed {
aggregator.stenographer.AnnounceBeforeSuiteFailure(setupSummary, aggregator.config.Succinct, aggregator.config.FullTrace)
}
}
func (aggregator *Aggregator) announceAfterSuite(setupSummary *types.SetupSummary) {
aggregator.stenographer.AnnounceCapturedOutput(setupSummary.CapturedOutput)
if setupSummary.State != types.SpecStatePassed {
aggregator.stenographer.AnnounceAfterSuiteFailure(setupSummary, aggregator.config.Succinct, aggregator.config.FullTrace)
}
}
func (aggregator *Aggregator) announceSpec(specSummary *types.SpecSummary) {
if aggregator.config.Verbose && specSummary.State != types.SpecStatePending && specSummary.State != types.SpecStateSkipped {
aggregator.stenographer.AnnounceSpecWillRun(specSummary)
}
aggregator.stenographer.AnnounceCapturedOutput(specSummary.CapturedOutput)
switch specSummary.State {
case types.SpecStatePassed:
if specSummary.IsMeasurement {
aggregator.stenographer.AnnounceSuccesfulMeasurement(specSummary, aggregator.config.Succinct)
} else if specSummary.RunTime.Seconds() >= aggregator.config.SlowSpecThreshold {
aggregator.stenographer.AnnounceSuccesfulSlowSpec(specSummary, aggregator.config.Succinct)
} else {
aggregator.stenographer.AnnounceSuccesfulSpec(specSummary)
}
case types.SpecStatePending:
aggregator.stenographer.AnnouncePendingSpec(specSummary, aggregator.config.NoisyPendings && !aggregator.config.Succinct)
case types.SpecStateSkipped:
aggregator.stenographer.AnnounceSkippedSpec(specSummary, aggregator.config.Succinct || !aggregator.config.NoisySkippings, aggregator.config.FullTrace)
case types.SpecStateTimedOut:
aggregator.stenographer.AnnounceSpecTimedOut(specSummary, aggregator.config.Succinct, aggregator.config.FullTrace)
case types.SpecStatePanicked:
aggregator.stenographer.AnnounceSpecPanicked(specSummary, aggregator.config.Succinct, aggregator.config.FullTrace)
case types.SpecStateFailed:
aggregator.stenographer.AnnounceSpecFailed(specSummary, aggregator.config.Succinct, aggregator.config.FullTrace)
}
}
func (aggregator *Aggregator) registerSuiteEnding(suite *types.SuiteSummary) (finished bool, passed bool) {
aggregator.aggregatedSuiteEndings = append(aggregator.aggregatedSuiteEndings, suite)
if len(aggregator.aggregatedSuiteEndings) < aggregator.nodeCount {
return false, false
}
aggregatedSuiteSummary := &types.SuiteSummary{}
aggregatedSuiteSummary.SuiteSucceeded = true
for _, suiteSummary := range aggregator.aggregatedSuiteEndings {
if suiteSummary.SuiteSucceeded == false {
aggregatedSuiteSummary.SuiteSucceeded = false
}
aggregatedSuiteSummary.NumberOfSpecsThatWillBeRun += suiteSummary.NumberOfSpecsThatWillBeRun
aggregatedSuiteSummary.NumberOfTotalSpecs += suiteSummary.NumberOfTotalSpecs
aggregatedSuiteSummary.NumberOfPassedSpecs += suiteSummary.NumberOfPassedSpecs
aggregatedSuiteSummary.NumberOfFailedSpecs += suiteSummary.NumberOfFailedSpecs
aggregatedSuiteSummary.NumberOfPendingSpecs += suiteSummary.NumberOfPendingSpecs
aggregatedSuiteSummary.NumberOfSkippedSpecs += suiteSummary.NumberOfSkippedSpecs
aggregatedSuiteSummary.NumberOfFlakedSpecs += suiteSummary.NumberOfFlakedSpecs
}
aggregatedSuiteSummary.RunTime = time.Since(aggregator.startTime)
aggregator.stenographer.SummarizeFailures(aggregator.specs)
aggregator.stenographer.AnnounceSpecRunCompletion(aggregatedSuiteSummary, aggregator.config.Succinct)
return true, aggregatedSuiteSummary.SuiteSucceeded
}

View file

@ -0,0 +1,147 @@
package remote
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"github.com/onsi/ginkgo/internal/writer"
"github.com/onsi/ginkgo/reporters"
"github.com/onsi/ginkgo/reporters/stenographer"
"github.com/onsi/ginkgo/config"
"github.com/onsi/ginkgo/types"
)
//An interface to net/http's client to allow the injection of fakes under test
type Poster interface {
Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error)
}
/*
The ForwardingReporter is a Ginkgo reporter that forwards information to
a Ginkgo remote server.
When streaming parallel test output, this repoter is automatically installed by Ginkgo.
This is accomplished by passing in the GINKGO_REMOTE_REPORTING_SERVER environment variable to `go test`, the Ginkgo test runner
detects this environment variable (which should contain the host of the server) and automatically installs a ForwardingReporter
in place of Ginkgo's DefaultReporter.
*/
type ForwardingReporter struct {
serverHost string
poster Poster
outputInterceptor OutputInterceptor
debugMode bool
debugFile *os.File
nestedReporter *reporters.DefaultReporter
}
func NewForwardingReporter(config config.DefaultReporterConfigType, serverHost string, poster Poster, outputInterceptor OutputInterceptor, ginkgoWriter *writer.Writer, debugFile string) *ForwardingReporter {
reporter := &ForwardingReporter{
serverHost: serverHost,
poster: poster,
outputInterceptor: outputInterceptor,
}
if debugFile != "" {
var err error
reporter.debugMode = true
reporter.debugFile, err = os.Create(debugFile)
if err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
if !config.Verbose {
//if verbose is true then the GinkgoWriter emits to stdout. Don't _also_ redirect GinkgoWriter output as that will result in duplication.
ginkgoWriter.AndRedirectTo(reporter.debugFile)
}
outputInterceptor.StreamTo(reporter.debugFile) //This is not working
stenographer := stenographer.New(false, true, reporter.debugFile)
config.Succinct = false
config.Verbose = true
config.FullTrace = true
reporter.nestedReporter = reporters.NewDefaultReporter(config, stenographer)
}
return reporter
}
func (reporter *ForwardingReporter) post(path string, data interface{}) {
encoded, _ := json.Marshal(data)
buffer := bytes.NewBuffer(encoded)
reporter.poster.Post(reporter.serverHost+path, "application/json", buffer)
}
func (reporter *ForwardingReporter) SpecSuiteWillBegin(conf config.GinkgoConfigType, summary *types.SuiteSummary) {
data := struct {
Config config.GinkgoConfigType `json:"config"`
Summary *types.SuiteSummary `json:"suite-summary"`
}{
conf,
summary,
}
reporter.outputInterceptor.StartInterceptingOutput()
if reporter.debugMode {
reporter.nestedReporter.SpecSuiteWillBegin(conf, summary)
reporter.debugFile.Sync()
}
reporter.post("/SpecSuiteWillBegin", data)
}
func (reporter *ForwardingReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) {
output, _ := reporter.outputInterceptor.StopInterceptingAndReturnOutput()
reporter.outputInterceptor.StartInterceptingOutput()
setupSummary.CapturedOutput = output
if reporter.debugMode {
reporter.nestedReporter.BeforeSuiteDidRun(setupSummary)
reporter.debugFile.Sync()
}
reporter.post("/BeforeSuiteDidRun", setupSummary)
}
func (reporter *ForwardingReporter) SpecWillRun(specSummary *types.SpecSummary) {
if reporter.debugMode {
reporter.nestedReporter.SpecWillRun(specSummary)
reporter.debugFile.Sync()
}
reporter.post("/SpecWillRun", specSummary)
}
func (reporter *ForwardingReporter) SpecDidComplete(specSummary *types.SpecSummary) {
output, _ := reporter.outputInterceptor.StopInterceptingAndReturnOutput()
reporter.outputInterceptor.StartInterceptingOutput()
specSummary.CapturedOutput = output
if reporter.debugMode {
reporter.nestedReporter.SpecDidComplete(specSummary)
reporter.debugFile.Sync()
}
reporter.post("/SpecDidComplete", specSummary)
}
func (reporter *ForwardingReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) {
output, _ := reporter.outputInterceptor.StopInterceptingAndReturnOutput()
reporter.outputInterceptor.StartInterceptingOutput()
setupSummary.CapturedOutput = output
if reporter.debugMode {
reporter.nestedReporter.AfterSuiteDidRun(setupSummary)
reporter.debugFile.Sync()
}
reporter.post("/AfterSuiteDidRun", setupSummary)
}
func (reporter *ForwardingReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) {
reporter.outputInterceptor.StopInterceptingAndReturnOutput()
if reporter.debugMode {
reporter.nestedReporter.SpecSuiteDidEnd(summary)
reporter.debugFile.Sync()
}
reporter.post("/SpecSuiteDidEnd", summary)
}

View file

@ -0,0 +1,13 @@
package remote
import "os"
/*
The OutputInterceptor is used by the ForwardingReporter to
intercept and capture all stdin and stderr output during a test run.
*/
type OutputInterceptor interface {
StartInterceptingOutput() error
StopInterceptingAndReturnOutput() (string, error)
StreamTo(*os.File)
}

View file

@ -0,0 +1,83 @@
// +build freebsd openbsd netbsd dragonfly darwin linux solaris
package remote
import (
"errors"
"io/ioutil"
"os"
"github.com/hpcloud/tail"
)
func NewOutputInterceptor() OutputInterceptor {
return &outputInterceptor{}
}
type outputInterceptor struct {
redirectFile *os.File
streamTarget *os.File
intercepting bool
tailer *tail.Tail
doneTailing chan bool
}
func (interceptor *outputInterceptor) StartInterceptingOutput() error {
if interceptor.intercepting {
return errors.New("Already intercepting output!")
}
interceptor.intercepting = true
var err error
interceptor.redirectFile, err = ioutil.TempFile("", "ginkgo-output")
if err != nil {
return err
}
// Call a function in ./syscall_dup_*.go
// If building for everything other than linux_arm64,
// use a "normal" syscall.Dup2(oldfd, newfd) call. If building for linux_arm64 (which doesn't have syscall.Dup2)
// call syscall.Dup3(oldfd, newfd, 0). They are nearly identical, see: http://linux.die.net/man/2/dup3
syscallDup(int(interceptor.redirectFile.Fd()), 1)
syscallDup(int(interceptor.redirectFile.Fd()), 2)
if interceptor.streamTarget != nil {
interceptor.tailer, _ = tail.TailFile(interceptor.redirectFile.Name(), tail.Config{Follow: true})
interceptor.doneTailing = make(chan bool)
go func() {
for line := range interceptor.tailer.Lines {
interceptor.streamTarget.Write([]byte(line.Text + "\n"))
}
close(interceptor.doneTailing)
}()
}
return nil
}
func (interceptor *outputInterceptor) StopInterceptingAndReturnOutput() (string, error) {
if !interceptor.intercepting {
return "", errors.New("Not intercepting output!")
}
interceptor.redirectFile.Close()
output, err := ioutil.ReadFile(interceptor.redirectFile.Name())
os.Remove(interceptor.redirectFile.Name())
interceptor.intercepting = false
if interceptor.streamTarget != nil {
interceptor.tailer.Stop()
interceptor.tailer.Cleanup()
<-interceptor.doneTailing
interceptor.streamTarget.Sync()
}
return string(output), err
}
func (interceptor *outputInterceptor) StreamTo(out *os.File) {
interceptor.streamTarget = out
}

View file

@ -0,0 +1,36 @@
// +build windows
package remote
import (
"errors"
"os"
)
func NewOutputInterceptor() OutputInterceptor {
return &outputInterceptor{}
}
type outputInterceptor struct {
intercepting bool
}
func (interceptor *outputInterceptor) StartInterceptingOutput() error {
if interceptor.intercepting {
return errors.New("Already intercepting output!")
}
interceptor.intercepting = true
// not working on windows...
return nil
}
func (interceptor *outputInterceptor) StopInterceptingAndReturnOutput() (string, error) {
// not working on windows...
interceptor.intercepting = false
return "", nil
}
func (interceptor *outputInterceptor) StreamTo(*os.File) {}

224
vendor/github.com/onsi/ginkgo/internal/remote/server.go generated vendored Normal file
View file

@ -0,0 +1,224 @@
/*
The remote package provides the pieces to allow Ginkgo test suites to report to remote listeners.
This is used, primarily, to enable streaming parallel test output but has, in principal, broader applications (e.g. streaming test output to a browser).
*/
package remote
import (
"encoding/json"
"io/ioutil"
"net"
"net/http"
"sync"
"github.com/onsi/ginkgo/internal/spec_iterator"
"github.com/onsi/ginkgo/config"
"github.com/onsi/ginkgo/reporters"
"github.com/onsi/ginkgo/types"
)
/*
Server spins up on an automatically selected port and listens for communication from the forwarding reporter.
It then forwards that communication to attached reporters.
*/
type Server struct {
listener net.Listener
reporters []reporters.Reporter
alives []func() bool
lock *sync.Mutex
beforeSuiteData types.RemoteBeforeSuiteData
parallelTotal int
counter int
}
//Create a new server, automatically selecting a port
func NewServer(parallelTotal int) (*Server, error) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, err
}
return &Server{
listener: listener,
lock: &sync.Mutex{},
alives: make([]func() bool, parallelTotal),
beforeSuiteData: types.RemoteBeforeSuiteData{Data: nil, State: types.RemoteBeforeSuiteStatePending},
parallelTotal: parallelTotal,
}, nil
}
//Start the server. You don't need to `go s.Start()`, just `s.Start()`
func (server *Server) Start() {
httpServer := &http.Server{}
mux := http.NewServeMux()
httpServer.Handler = mux
//streaming endpoints
mux.HandleFunc("/SpecSuiteWillBegin", server.specSuiteWillBegin)
mux.HandleFunc("/BeforeSuiteDidRun", server.beforeSuiteDidRun)
mux.HandleFunc("/AfterSuiteDidRun", server.afterSuiteDidRun)
mux.HandleFunc("/SpecWillRun", server.specWillRun)
mux.HandleFunc("/SpecDidComplete", server.specDidComplete)
mux.HandleFunc("/SpecSuiteDidEnd", server.specSuiteDidEnd)
//synchronization endpoints
mux.HandleFunc("/BeforeSuiteState", server.handleBeforeSuiteState)
mux.HandleFunc("/RemoteAfterSuiteData", server.handleRemoteAfterSuiteData)
mux.HandleFunc("/counter", server.handleCounter)
mux.HandleFunc("/has-counter", server.handleHasCounter) //for backward compatibility
go httpServer.Serve(server.listener)
}
//Stop the server
func (server *Server) Close() {
server.listener.Close()
}
//The address the server can be reached it. Pass this into the `ForwardingReporter`.
func (server *Server) Address() string {
return "http://" + server.listener.Addr().String()
}
//
// Streaming Endpoints
//
//The server will forward all received messages to Ginkgo reporters registered with `RegisterReporters`
func (server *Server) readAll(request *http.Request) []byte {
defer request.Body.Close()
body, _ := ioutil.ReadAll(request.Body)
return body
}
func (server *Server) RegisterReporters(reporters ...reporters.Reporter) {
server.reporters = reporters
}
func (server *Server) specSuiteWillBegin(writer http.ResponseWriter, request *http.Request) {
body := server.readAll(request)
var data struct {
Config config.GinkgoConfigType `json:"config"`
Summary *types.SuiteSummary `json:"suite-summary"`
}
json.Unmarshal(body, &data)
for _, reporter := range server.reporters {
reporter.SpecSuiteWillBegin(data.Config, data.Summary)
}
}
func (server *Server) beforeSuiteDidRun(writer http.ResponseWriter, request *http.Request) {
body := server.readAll(request)
var setupSummary *types.SetupSummary
json.Unmarshal(body, &setupSummary)
for _, reporter := range server.reporters {
reporter.BeforeSuiteDidRun(setupSummary)
}
}
func (server *Server) afterSuiteDidRun(writer http.ResponseWriter, request *http.Request) {
body := server.readAll(request)
var setupSummary *types.SetupSummary
json.Unmarshal(body, &setupSummary)
for _, reporter := range server.reporters {
reporter.AfterSuiteDidRun(setupSummary)
}
}
func (server *Server) specWillRun(writer http.ResponseWriter, request *http.Request) {
body := server.readAll(request)
var specSummary *types.SpecSummary
json.Unmarshal(body, &specSummary)
for _, reporter := range server.reporters {
reporter.SpecWillRun(specSummary)
}
}
func (server *Server) specDidComplete(writer http.ResponseWriter, request *http.Request) {
body := server.readAll(request)
var specSummary *types.SpecSummary
json.Unmarshal(body, &specSummary)
for _, reporter := range server.reporters {
reporter.SpecDidComplete(specSummary)
}
}
func (server *Server) specSuiteDidEnd(writer http.ResponseWriter, request *http.Request) {
body := server.readAll(request)
var suiteSummary *types.SuiteSummary
json.Unmarshal(body, &suiteSummary)
for _, reporter := range server.reporters {
reporter.SpecSuiteDidEnd(suiteSummary)
}
}
//
// Synchronization Endpoints
//
func (server *Server) RegisterAlive(node int, alive func() bool) {
server.lock.Lock()
defer server.lock.Unlock()
server.alives[node-1] = alive
}
func (server *Server) nodeIsAlive(node int) bool {
server.lock.Lock()
defer server.lock.Unlock()
alive := server.alives[node-1]
if alive == nil {
return true
}
return alive()
}
func (server *Server) handleBeforeSuiteState(writer http.ResponseWriter, request *http.Request) {
if request.Method == "POST" {
dec := json.NewDecoder(request.Body)
dec.Decode(&(server.beforeSuiteData))
} else {
beforeSuiteData := server.beforeSuiteData
if beforeSuiteData.State == types.RemoteBeforeSuiteStatePending && !server.nodeIsAlive(1) {
beforeSuiteData.State = types.RemoteBeforeSuiteStateDisappeared
}
enc := json.NewEncoder(writer)
enc.Encode(beforeSuiteData)
}
}
func (server *Server) handleRemoteAfterSuiteData(writer http.ResponseWriter, request *http.Request) {
afterSuiteData := types.RemoteAfterSuiteData{
CanRun: true,
}
for i := 2; i <= server.parallelTotal; i++ {
afterSuiteData.CanRun = afterSuiteData.CanRun && !server.nodeIsAlive(i)
}
enc := json.NewEncoder(writer)
enc.Encode(afterSuiteData)
}
func (server *Server) handleCounter(writer http.ResponseWriter, request *http.Request) {
c := spec_iterator.Counter{}
server.lock.Lock()
c.Index = server.counter
server.counter = server.counter + 1
server.lock.Unlock()
json.NewEncoder(writer).Encode(c)
}
func (server *Server) handleHasCounter(writer http.ResponseWriter, request *http.Request) {
writer.Write([]byte(""))
}

View file

@ -0,0 +1,11 @@
// +build linux,arm64
package remote
import "syscall"
// linux_arm64 doesn't have syscall.Dup2 which ginkgo uses, so
// use the nearly identical syscall.Dup3 instead
func syscallDup(oldfd int, newfd int) (err error) {
return syscall.Dup3(oldfd, newfd, 0)
}

View file

@ -0,0 +1,9 @@
// +build solaris
package remote
import "golang.org/x/sys/unix"
func syscallDup(oldfd int, newfd int) (err error) {
return unix.Dup2(oldfd, newfd)
}

View file

@ -0,0 +1,11 @@
// +build !linux !arm64
// +build !windows
// +build !solaris
package remote
import "syscall"
func syscallDup(oldfd int, newfd int) (err error) {
return syscall.Dup2(oldfd, newfd)
}

235
vendor/github.com/onsi/ginkgo/internal/spec/spec.go generated vendored Normal file
View file

@ -0,0 +1,235 @@
package spec
import (
"fmt"
"io"
"time"
"sync"
"github.com/onsi/ginkgo/internal/containernode"
"github.com/onsi/ginkgo/internal/leafnodes"
"github.com/onsi/ginkgo/types"
)
type Spec struct {
subject leafnodes.SubjectNode
focused bool
announceProgress bool
containers []*containernode.ContainerNode
state types.SpecState
runTime time.Duration
startTime time.Time
failure types.SpecFailure
previousFailures bool
stateMutex *sync.Mutex
}
func New(subject leafnodes.SubjectNode, containers []*containernode.ContainerNode, announceProgress bool) *Spec {
spec := &Spec{
subject: subject,
containers: containers,
focused: subject.Flag() == types.FlagTypeFocused,
announceProgress: announceProgress,
stateMutex: &sync.Mutex{},
}
spec.processFlag(subject.Flag())
for i := len(containers) - 1; i >= 0; i-- {
spec.processFlag(containers[i].Flag())
}
return spec
}
func (spec *Spec) processFlag(flag types.FlagType) {
if flag == types.FlagTypeFocused {
spec.focused = true
} else if flag == types.FlagTypePending {
spec.setState(types.SpecStatePending)
}
}
func (spec *Spec) Skip() {
spec.setState(types.SpecStateSkipped)
}
func (spec *Spec) Failed() bool {
return spec.getState() == types.SpecStateFailed || spec.getState() == types.SpecStatePanicked || spec.getState() == types.SpecStateTimedOut
}
func (spec *Spec) Passed() bool {
return spec.getState() == types.SpecStatePassed
}
func (spec *Spec) Flaked() bool {
return spec.getState() == types.SpecStatePassed && spec.previousFailures
}
func (spec *Spec) Pending() bool {
return spec.getState() == types.SpecStatePending
}
func (spec *Spec) Skipped() bool {
return spec.getState() == types.SpecStateSkipped
}
func (spec *Spec) Focused() bool {
return spec.focused
}
func (spec *Spec) IsMeasurement() bool {
return spec.subject.Type() == types.SpecComponentTypeMeasure
}
func (spec *Spec) Summary(suiteID string) *types.SpecSummary {
componentTexts := make([]string, len(spec.containers)+1)
componentCodeLocations := make([]types.CodeLocation, len(spec.containers)+1)
for i, container := range spec.containers {
componentTexts[i] = container.Text()
componentCodeLocations[i] = container.CodeLocation()
}
componentTexts[len(spec.containers)] = spec.subject.Text()
componentCodeLocations[len(spec.containers)] = spec.subject.CodeLocation()
runTime := spec.runTime
if runTime == 0 && !spec.startTime.IsZero() {
runTime = time.Since(spec.startTime)
}
return &types.SpecSummary{
IsMeasurement: spec.IsMeasurement(),
NumberOfSamples: spec.subject.Samples(),
ComponentTexts: componentTexts,
ComponentCodeLocations: componentCodeLocations,
State: spec.getState(),
RunTime: runTime,
Failure: spec.failure,
Measurements: spec.measurementsReport(),
SuiteID: suiteID,
}
}
func (spec *Spec) ConcatenatedString() string {
s := ""
for _, container := range spec.containers {
s += container.Text() + " "
}
return s + spec.subject.Text()
}
func (spec *Spec) Run(writer io.Writer) {
if spec.getState() == types.SpecStateFailed {
spec.previousFailures = true
}
spec.startTime = time.Now()
defer func() {
spec.runTime = time.Since(spec.startTime)
}()
for sample := 0; sample < spec.subject.Samples(); sample++ {
spec.runSample(sample, writer)
if spec.getState() != types.SpecStatePassed {
return
}
}
}
func (spec *Spec) getState() types.SpecState {
spec.stateMutex.Lock()
defer spec.stateMutex.Unlock()
return spec.state
}
func (spec *Spec) setState(state types.SpecState) {
spec.stateMutex.Lock()
defer spec.stateMutex.Unlock()
spec.state = state
}
func (spec *Spec) runSample(sample int, writer io.Writer) {
spec.setState(types.SpecStatePassed)
spec.failure = types.SpecFailure{}
innerMostContainerIndexToUnwind := -1
defer func() {
for i := innerMostContainerIndexToUnwind; i >= 0; i-- {
container := spec.containers[i]
for _, afterEach := range container.SetupNodesOfType(types.SpecComponentTypeAfterEach) {
spec.announceSetupNode(writer, "AfterEach", container, afterEach)
afterEachState, afterEachFailure := afterEach.Run()
if afterEachState != types.SpecStatePassed && spec.getState() == types.SpecStatePassed {
spec.setState(afterEachState)
spec.failure = afterEachFailure
}
}
}
}()
for i, container := range spec.containers {
innerMostContainerIndexToUnwind = i
for _, beforeEach := range container.SetupNodesOfType(types.SpecComponentTypeBeforeEach) {
spec.announceSetupNode(writer, "BeforeEach", container, beforeEach)
s, f := beforeEach.Run()
spec.failure = f
spec.setState(s)
if spec.getState() != types.SpecStatePassed {
return
}
}
}
for _, container := range spec.containers {
for _, justBeforeEach := range container.SetupNodesOfType(types.SpecComponentTypeJustBeforeEach) {
spec.announceSetupNode(writer, "JustBeforeEach", container, justBeforeEach)
s, f := justBeforeEach.Run()
spec.failure = f
spec.setState(s)
if spec.getState() != types.SpecStatePassed {
return
}
}
}
spec.announceSubject(writer, spec.subject)
s, f := spec.subject.Run()
spec.failure = f
spec.setState(s)
}
func (spec *Spec) announceSetupNode(writer io.Writer, nodeType string, container *containernode.ContainerNode, setupNode leafnodes.BasicNode) {
if spec.announceProgress {
s := fmt.Sprintf("[%s] %s\n %s\n", nodeType, container.Text(), setupNode.CodeLocation().String())
writer.Write([]byte(s))
}
}
func (spec *Spec) announceSubject(writer io.Writer, subject leafnodes.SubjectNode) {
if spec.announceProgress {
nodeType := ""
switch subject.Type() {
case types.SpecComponentTypeIt:
nodeType = "It"
case types.SpecComponentTypeMeasure:
nodeType = "Measure"
}
s := fmt.Sprintf("[%s] %s\n %s\n", nodeType, subject.Text(), subject.CodeLocation().String())
writer.Write([]byte(s))
}
}
func (spec *Spec) measurementsReport() map[string]*types.SpecMeasurement {
if !spec.IsMeasurement() || spec.Failed() {
return map[string]*types.SpecMeasurement{}
}
return spec.subject.(*leafnodes.MeasureNode).MeasurementsReport()
}

123
vendor/github.com/onsi/ginkgo/internal/spec/specs.go generated vendored Normal file
View file

@ -0,0 +1,123 @@
package spec
import (
"math/rand"
"regexp"
"sort"
)
type Specs struct {
specs []*Spec
hasProgrammaticFocus bool
RegexScansFilePath bool
}
func NewSpecs(specs []*Spec) *Specs {
return &Specs{
specs: specs,
}
}
func (e *Specs) Specs() []*Spec {
return e.specs
}
func (e *Specs) HasProgrammaticFocus() bool {
return e.hasProgrammaticFocus
}
func (e *Specs) Shuffle(r *rand.Rand) {
sort.Sort(e)
permutation := r.Perm(len(e.specs))
shuffledSpecs := make([]*Spec, len(e.specs))
for i, j := range permutation {
shuffledSpecs[i] = e.specs[j]
}
e.specs = shuffledSpecs
}
func (e *Specs) ApplyFocus(description string, focusString string, skipString string) {
if focusString == "" && skipString == "" {
e.applyProgrammaticFocus()
} else {
e.applyRegExpFocusAndSkip(description, focusString, skipString)
}
}
func (e *Specs) applyProgrammaticFocus() {
e.hasProgrammaticFocus = false
for _, spec := range e.specs {
if spec.Focused() && !spec.Pending() {
e.hasProgrammaticFocus = true
break
}
}
if e.hasProgrammaticFocus {
for _, spec := range e.specs {
if !spec.Focused() {
spec.Skip()
}
}
}
}
// toMatch returns a byte[] to be used by regex matchers. When adding new behaviours to the matching function,
// this is the place which we append to.
func (e *Specs) toMatch(description string, spec *Spec) []byte {
if e.RegexScansFilePath {
return []byte(
description + " " +
spec.ConcatenatedString() + " " +
spec.subject.CodeLocation().FileName)
} else {
return []byte(
description + " " +
spec.ConcatenatedString())
}
}
func (e *Specs) applyRegExpFocusAndSkip(description string, focusString string, skipString string) {
for _, spec := range e.specs {
matchesFocus := true
matchesSkip := false
toMatch := e.toMatch(description, spec)
if focusString != "" {
focusFilter := regexp.MustCompile(focusString)
matchesFocus = focusFilter.Match([]byte(toMatch))
}
if skipString != "" {
skipFilter := regexp.MustCompile(skipString)
matchesSkip = skipFilter.Match([]byte(toMatch))
}
if !matchesFocus || matchesSkip {
spec.Skip()
}
}
}
func (e *Specs) SkipMeasurements() {
for _, spec := range e.specs {
if spec.IsMeasurement() {
spec.Skip()
}
}
}
//sort.Interface
func (e *Specs) Len() int {
return len(e.specs)
}
func (e *Specs) Less(i, j int) bool {
return e.specs[i].ConcatenatedString() < e.specs[j].ConcatenatedString()
}
func (e *Specs) Swap(i, j int) {
e.specs[i], e.specs[j] = e.specs[j], e.specs[i]
}

View file

@ -0,0 +1,55 @@
package spec_iterator
func ParallelizedIndexRange(length int, parallelTotal int, parallelNode int) (startIndex int, count int) {
if length == 0 {
return 0, 0
}
// We have more nodes than tests. Trivial case.
if parallelTotal >= length {
if parallelNode > length {
return 0, 0
} else {
return parallelNode - 1, 1
}
}
// This is the minimum amount of tests that a node will be required to run
minTestsPerNode := length / parallelTotal
// This is the maximum amount of tests that a node will be required to run
// The algorithm guarantees that this would be equal to at least the minimum amount
// and at most one more
maxTestsPerNode := minTestsPerNode
if length%parallelTotal != 0 {
maxTestsPerNode++
}
// Number of nodes that will have to run the maximum amount of tests per node
numMaxLoadNodes := length % parallelTotal
// Number of nodes that precede the current node and will have to run the maximum amount of tests per node
var numPrecedingMaxLoadNodes int
if parallelNode > numMaxLoadNodes {
numPrecedingMaxLoadNodes = numMaxLoadNodes
} else {
numPrecedingMaxLoadNodes = parallelNode - 1
}
// Number of nodes that precede the current node and will have to run the minimum amount of tests per node
var numPrecedingMinLoadNodes int
if parallelNode <= numMaxLoadNodes {
numPrecedingMinLoadNodes = 0
} else {
numPrecedingMinLoadNodes = parallelNode - numMaxLoadNodes - 1
}
// Evaluate the test start index and number of tests to run
startIndex = numPrecedingMaxLoadNodes*maxTestsPerNode + numPrecedingMinLoadNodes*minTestsPerNode
if parallelNode > numMaxLoadNodes {
count = minTestsPerNode
} else {
count = maxTestsPerNode
}
return
}

View file

@ -0,0 +1,59 @@
package spec_iterator
import (
"encoding/json"
"fmt"
"net/http"
"github.com/onsi/ginkgo/internal/spec"
)
type ParallelIterator struct {
specs []*spec.Spec
host string
client *http.Client
}
func NewParallelIterator(specs []*spec.Spec, host string) *ParallelIterator {
return &ParallelIterator{
specs: specs,
host: host,
client: &http.Client{},
}
}
func (s *ParallelIterator) Next() (*spec.Spec, error) {
resp, err := s.client.Get(s.host + "/counter")
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode)
}
var counter Counter
err = json.NewDecoder(resp.Body).Decode(&counter)
if err != nil {
return nil, err
}
if counter.Index >= len(s.specs) {
return nil, ErrClosed
}
return s.specs[counter.Index], nil
}
func (s *ParallelIterator) NumberOfSpecsPriorToIteration() int {
return len(s.specs)
}
func (s *ParallelIterator) NumberOfSpecsToProcessIfKnown() (int, bool) {
return -1, false
}
func (s *ParallelIterator) NumberOfSpecsThatWillBeRunIfKnown() (int, bool) {
return -1, false
}

View file

@ -0,0 +1,45 @@
package spec_iterator
import (
"github.com/onsi/ginkgo/internal/spec"
)
type SerialIterator struct {
specs []*spec.Spec
index int
}
func NewSerialIterator(specs []*spec.Spec) *SerialIterator {
return &SerialIterator{
specs: specs,
index: 0,
}
}
func (s *SerialIterator) Next() (*spec.Spec, error) {
if s.index >= len(s.specs) {
return nil, ErrClosed
}
spec := s.specs[s.index]
s.index += 1
return spec, nil
}
func (s *SerialIterator) NumberOfSpecsPriorToIteration() int {
return len(s.specs)
}
func (s *SerialIterator) NumberOfSpecsToProcessIfKnown() (int, bool) {
return len(s.specs), true
}
func (s *SerialIterator) NumberOfSpecsThatWillBeRunIfKnown() (int, bool) {
count := 0
for _, s := range s.specs {
if !s.Skipped() && !s.Pending() {
count += 1
}
}
return count, true
}

View file

@ -0,0 +1,47 @@
package spec_iterator
import "github.com/onsi/ginkgo/internal/spec"
type ShardedParallelIterator struct {
specs []*spec.Spec
index int
maxIndex int
}
func NewShardedParallelIterator(specs []*spec.Spec, total int, node int) *ShardedParallelIterator {
startIndex, count := ParallelizedIndexRange(len(specs), total, node)
return &ShardedParallelIterator{
specs: specs,
index: startIndex,
maxIndex: startIndex + count,
}
}
func (s *ShardedParallelIterator) Next() (*spec.Spec, error) {
if s.index >= s.maxIndex {
return nil, ErrClosed
}
spec := s.specs[s.index]
s.index += 1
return spec, nil
}
func (s *ShardedParallelIterator) NumberOfSpecsPriorToIteration() int {
return len(s.specs)
}
func (s *ShardedParallelIterator) NumberOfSpecsToProcessIfKnown() (int, bool) {
return s.maxIndex - s.index, true
}
func (s *ShardedParallelIterator) NumberOfSpecsThatWillBeRunIfKnown() (int, bool) {
count := 0
for i := s.index; i < s.maxIndex; i += 1 {
if !s.specs[i].Skipped() && !s.specs[i].Pending() {
count += 1
}
}
return count, true
}

View file

@ -0,0 +1,20 @@
package spec_iterator
import (
"errors"
"github.com/onsi/ginkgo/internal/spec"
)
var ErrClosed = errors.New("no more specs to run")
type SpecIterator interface {
Next() (*spec.Spec, error)
NumberOfSpecsPriorToIteration() int
NumberOfSpecsToProcessIfKnown() (int, bool)
NumberOfSpecsThatWillBeRunIfKnown() (int, bool)
}
type Counter struct {
Index int `json:"index"`
}

View file

@ -0,0 +1,15 @@
package specrunner
import (
"crypto/rand"
"fmt"
)
func randomID() string {
b := make([]byte, 8)
_, err := rand.Read(b)
if err != nil {
return ""
}
return fmt.Sprintf("%x-%x-%x-%x", b[0:2], b[2:4], b[4:6], b[6:8])
}

View file

@ -0,0 +1,411 @@
package specrunner
import (
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"github.com/onsi/ginkgo/internal/spec_iterator"
"github.com/onsi/ginkgo/config"
"github.com/onsi/ginkgo/internal/leafnodes"
"github.com/onsi/ginkgo/internal/spec"
Writer "github.com/onsi/ginkgo/internal/writer"
"github.com/onsi/ginkgo/reporters"
"github.com/onsi/ginkgo/types"
"time"
)
type SpecRunner struct {
description string
beforeSuiteNode leafnodes.SuiteNode
iterator spec_iterator.SpecIterator
afterSuiteNode leafnodes.SuiteNode
reporters []reporters.Reporter
startTime time.Time
suiteID string
runningSpec *spec.Spec
writer Writer.WriterInterface
config config.GinkgoConfigType
interrupted bool
processedSpecs []*spec.Spec
lock *sync.Mutex
}
func New(description string, beforeSuiteNode leafnodes.SuiteNode, iterator spec_iterator.SpecIterator, afterSuiteNode leafnodes.SuiteNode, reporters []reporters.Reporter, writer Writer.WriterInterface, config config.GinkgoConfigType) *SpecRunner {
return &SpecRunner{
description: description,
beforeSuiteNode: beforeSuiteNode,
iterator: iterator,
afterSuiteNode: afterSuiteNode,
reporters: reporters,
writer: writer,
config: config,
suiteID: randomID(),
lock: &sync.Mutex{},
}
}
func (runner *SpecRunner) Run() bool {
if runner.config.DryRun {
runner.performDryRun()
return true
}
runner.reportSuiteWillBegin()
signalRegistered := make(chan struct{})
go runner.registerForInterrupts(signalRegistered)
<-signalRegistered
suitePassed := runner.runBeforeSuite()
if suitePassed {
suitePassed = runner.runSpecs()
}
runner.blockForeverIfInterrupted()
suitePassed = runner.runAfterSuite() && suitePassed
runner.reportSuiteDidEnd(suitePassed)
return suitePassed
}
func (runner *SpecRunner) performDryRun() {
runner.reportSuiteWillBegin()
if runner.beforeSuiteNode != nil {
summary := runner.beforeSuiteNode.Summary()
summary.State = types.SpecStatePassed
runner.reportBeforeSuite(summary)
}
for {
spec, err := runner.iterator.Next()
if err == spec_iterator.ErrClosed {
break
}
if err != nil {
fmt.Println("failed to iterate over tests:\n" + err.Error())
break
}
runner.processedSpecs = append(runner.processedSpecs, spec)
summary := spec.Summary(runner.suiteID)
runner.reportSpecWillRun(summary)
if summary.State == types.SpecStateInvalid {
summary.State = types.SpecStatePassed
}
runner.reportSpecDidComplete(summary, false)
}
if runner.afterSuiteNode != nil {
summary := runner.afterSuiteNode.Summary()
summary.State = types.SpecStatePassed
runner.reportAfterSuite(summary)
}
runner.reportSuiteDidEnd(true)
}
func (runner *SpecRunner) runBeforeSuite() bool {
if runner.beforeSuiteNode == nil || runner.wasInterrupted() {
return true
}
runner.writer.Truncate()
conf := runner.config
passed := runner.beforeSuiteNode.Run(conf.ParallelNode, conf.ParallelTotal, conf.SyncHost)
if !passed {
runner.writer.DumpOut()
}
runner.reportBeforeSuite(runner.beforeSuiteNode.Summary())
return passed
}
func (runner *SpecRunner) runAfterSuite() bool {
if runner.afterSuiteNode == nil {
return true
}
runner.writer.Truncate()
conf := runner.config
passed := runner.afterSuiteNode.Run(conf.ParallelNode, conf.ParallelTotal, conf.SyncHost)
if !passed {
runner.writer.DumpOut()
}
runner.reportAfterSuite(runner.afterSuiteNode.Summary())
return passed
}
func (runner *SpecRunner) runSpecs() bool {
suiteFailed := false
skipRemainingSpecs := false
for {
spec, err := runner.iterator.Next()
if err == spec_iterator.ErrClosed {
break
}
if err != nil {
fmt.Println("failed to iterate over tests:\n" + err.Error())
suiteFailed = true
break
}
runner.processedSpecs = append(runner.processedSpecs, spec)
if runner.wasInterrupted() {
break
}
if skipRemainingSpecs {
spec.Skip()
}
if !spec.Skipped() && !spec.Pending() {
if passed := runner.runSpec(spec); !passed {
suiteFailed = true
}
} else if spec.Pending() && runner.config.FailOnPending {
runner.reportSpecWillRun(spec.Summary(runner.suiteID))
suiteFailed = true
runner.reportSpecDidComplete(spec.Summary(runner.suiteID), spec.Failed())
} else {
runner.reportSpecWillRun(spec.Summary(runner.suiteID))
runner.reportSpecDidComplete(spec.Summary(runner.suiteID), spec.Failed())
}
if spec.Failed() && runner.config.FailFast {
skipRemainingSpecs = true
}
}
return !suiteFailed
}
func (runner *SpecRunner) runSpec(spec *spec.Spec) (passed bool) {
maxAttempts := 1
if runner.config.FlakeAttempts > 0 {
// uninitialized configs count as 1
maxAttempts = runner.config.FlakeAttempts
}
for i := 0; i < maxAttempts; i++ {
runner.reportSpecWillRun(spec.Summary(runner.suiteID))
runner.runningSpec = spec
spec.Run(runner.writer)
runner.runningSpec = nil
runner.reportSpecDidComplete(spec.Summary(runner.suiteID), spec.Failed())
if !spec.Failed() {
return true
}
}
return false
}
func (runner *SpecRunner) CurrentSpecSummary() (*types.SpecSummary, bool) {
if runner.runningSpec == nil {
return nil, false
}
return runner.runningSpec.Summary(runner.suiteID), true
}
func (runner *SpecRunner) registerForInterrupts(signalRegistered chan struct{}) {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
close(signalRegistered)
<-c
signal.Stop(c)
runner.markInterrupted()
go runner.registerForHardInterrupts()
runner.writer.DumpOutWithHeader(`
Received interrupt. Emitting contents of GinkgoWriter...
---------------------------------------------------------
`)
if runner.afterSuiteNode != nil {
fmt.Fprint(os.Stderr, `
---------------------------------------------------------
Received interrupt. Running AfterSuite...
^C again to terminate immediately
`)
runner.runAfterSuite()
}
runner.reportSuiteDidEnd(false)
os.Exit(1)
}
func (runner *SpecRunner) registerForHardInterrupts() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
<-c
fmt.Fprintln(os.Stderr, "\nReceived second interrupt. Shutting down.")
os.Exit(1)
}
func (runner *SpecRunner) blockForeverIfInterrupted() {
runner.lock.Lock()
interrupted := runner.interrupted
runner.lock.Unlock()
if interrupted {
select {}
}
}
func (runner *SpecRunner) markInterrupted() {
runner.lock.Lock()
defer runner.lock.Unlock()
runner.interrupted = true
}
func (runner *SpecRunner) wasInterrupted() bool {
runner.lock.Lock()
defer runner.lock.Unlock()
return runner.interrupted
}
func (runner *SpecRunner) reportSuiteWillBegin() {
runner.startTime = time.Now()
summary := runner.suiteWillBeginSummary()
for _, reporter := range runner.reporters {
reporter.SpecSuiteWillBegin(runner.config, summary)
}
}
func (runner *SpecRunner) reportBeforeSuite(summary *types.SetupSummary) {
for _, reporter := range runner.reporters {
reporter.BeforeSuiteDidRun(summary)
}
}
func (runner *SpecRunner) reportAfterSuite(summary *types.SetupSummary) {
for _, reporter := range runner.reporters {
reporter.AfterSuiteDidRun(summary)
}
}
func (runner *SpecRunner) reportSpecWillRun(summary *types.SpecSummary) {
runner.writer.Truncate()
for _, reporter := range runner.reporters {
reporter.SpecWillRun(summary)
}
}
func (runner *SpecRunner) reportSpecDidComplete(summary *types.SpecSummary, failed bool) {
if failed && len(summary.CapturedOutput) == 0 {
summary.CapturedOutput = string(runner.writer.Bytes())
}
for i := len(runner.reporters) - 1; i >= 1; i-- {
runner.reporters[i].SpecDidComplete(summary)
}
if failed {
runner.writer.DumpOut()
}
runner.reporters[0].SpecDidComplete(summary)
}
func (runner *SpecRunner) reportSuiteDidEnd(success bool) {
summary := runner.suiteDidEndSummary(success)
summary.RunTime = time.Since(runner.startTime)
for _, reporter := range runner.reporters {
reporter.SpecSuiteDidEnd(summary)
}
}
func (runner *SpecRunner) countSpecsThatRanSatisfying(filter func(ex *spec.Spec) bool) (count int) {
count = 0
for _, spec := range runner.processedSpecs {
if filter(spec) {
count++
}
}
return count
}
func (runner *SpecRunner) suiteDidEndSummary(success bool) *types.SuiteSummary {
numberOfSpecsThatWillBeRun := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool {
return !ex.Skipped() && !ex.Pending()
})
numberOfPendingSpecs := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool {
return ex.Pending()
})
numberOfSkippedSpecs := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool {
return ex.Skipped()
})
numberOfPassedSpecs := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool {
return ex.Passed()
})
numberOfFlakedSpecs := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool {
return ex.Flaked()
})
numberOfFailedSpecs := runner.countSpecsThatRanSatisfying(func(ex *spec.Spec) bool {
return ex.Failed()
})
if runner.beforeSuiteNode != nil && !runner.beforeSuiteNode.Passed() && !runner.config.DryRun {
var known bool
numberOfSpecsThatWillBeRun, known = runner.iterator.NumberOfSpecsThatWillBeRunIfKnown()
if !known {
numberOfSpecsThatWillBeRun = runner.iterator.NumberOfSpecsPriorToIteration()
}
numberOfFailedSpecs = numberOfSpecsThatWillBeRun
}
return &types.SuiteSummary{
SuiteDescription: runner.description,
SuiteSucceeded: success,
SuiteID: runner.suiteID,
NumberOfSpecsBeforeParallelization: runner.iterator.NumberOfSpecsPriorToIteration(),
NumberOfTotalSpecs: len(runner.processedSpecs),
NumberOfSpecsThatWillBeRun: numberOfSpecsThatWillBeRun,
NumberOfPendingSpecs: numberOfPendingSpecs,
NumberOfSkippedSpecs: numberOfSkippedSpecs,
NumberOfPassedSpecs: numberOfPassedSpecs,
NumberOfFailedSpecs: numberOfFailedSpecs,
NumberOfFlakedSpecs: numberOfFlakedSpecs,
}
}
func (runner *SpecRunner) suiteWillBeginSummary() *types.SuiteSummary {
numTotal, known := runner.iterator.NumberOfSpecsToProcessIfKnown()
if !known {
numTotal = -1
}
numToRun, known := runner.iterator.NumberOfSpecsThatWillBeRunIfKnown()
if !known {
numToRun = -1
}
return &types.SuiteSummary{
SuiteDescription: runner.description,
SuiteID: runner.suiteID,
NumberOfSpecsBeforeParallelization: runner.iterator.NumberOfSpecsPriorToIteration(),
NumberOfTotalSpecs: numTotal,
NumberOfSpecsThatWillBeRun: numToRun,
NumberOfPendingSpecs: -1,
NumberOfSkippedSpecs: -1,
NumberOfPassedSpecs: -1,
NumberOfFailedSpecs: -1,
NumberOfFlakedSpecs: -1,
}
}

183
vendor/github.com/onsi/ginkgo/internal/suite/suite.go generated vendored Normal file
View file

@ -0,0 +1,183 @@
package suite
import (
"math/rand"
"net/http"
"time"
"github.com/onsi/ginkgo/internal/spec_iterator"
"github.com/onsi/ginkgo/config"
"github.com/onsi/ginkgo/internal/containernode"
"github.com/onsi/ginkgo/internal/failer"
"github.com/onsi/ginkgo/internal/leafnodes"
"github.com/onsi/ginkgo/internal/spec"
"github.com/onsi/ginkgo/internal/specrunner"
"github.com/onsi/ginkgo/internal/writer"
"github.com/onsi/ginkgo/reporters"
"github.com/onsi/ginkgo/types"
)
type ginkgoTestingT interface {
Fail()
}
type Suite struct {
topLevelContainer *containernode.ContainerNode
currentContainer *containernode.ContainerNode
containerIndex int
beforeSuiteNode leafnodes.SuiteNode
afterSuiteNode leafnodes.SuiteNode
runner *specrunner.SpecRunner
failer *failer.Failer
running bool
}
func New(failer *failer.Failer) *Suite {
topLevelContainer := containernode.New("[Top Level]", types.FlagTypeNone, types.CodeLocation{})
return &Suite{
topLevelContainer: topLevelContainer,
currentContainer: topLevelContainer,
failer: failer,
containerIndex: 1,
}
}
func (suite *Suite) Run(t ginkgoTestingT, description string, reporters []reporters.Reporter, writer writer.WriterInterface, config config.GinkgoConfigType) (bool, bool) {
if config.ParallelTotal < 1 {
panic("ginkgo.parallel.total must be >= 1")
}
if config.ParallelNode > config.ParallelTotal || config.ParallelNode < 1 {
panic("ginkgo.parallel.node is one-indexed and must be <= ginkgo.parallel.total")
}
r := rand.New(rand.NewSource(config.RandomSeed))
suite.topLevelContainer.Shuffle(r)
iterator, hasProgrammaticFocus := suite.generateSpecsIterator(description, config)
suite.runner = specrunner.New(description, suite.beforeSuiteNode, iterator, suite.afterSuiteNode, reporters, writer, config)
suite.running = true
success := suite.runner.Run()
if !success {
t.Fail()
}
return success, hasProgrammaticFocus
}
func (suite *Suite) generateSpecsIterator(description string, config config.GinkgoConfigType) (spec_iterator.SpecIterator, bool) {
specsSlice := []*spec.Spec{}
suite.topLevelContainer.BackPropagateProgrammaticFocus()
for _, collatedNodes := range suite.topLevelContainer.Collate() {
specsSlice = append(specsSlice, spec.New(collatedNodes.Subject, collatedNodes.Containers, config.EmitSpecProgress))
}
specs := spec.NewSpecs(specsSlice)
specs.RegexScansFilePath = config.RegexScansFilePath
if config.RandomizeAllSpecs {
specs.Shuffle(rand.New(rand.NewSource(config.RandomSeed)))
}
specs.ApplyFocus(description, config.FocusString, config.SkipString)
if config.SkipMeasurements {
specs.SkipMeasurements()
}
var iterator spec_iterator.SpecIterator
if config.ParallelTotal > 1 {
iterator = spec_iterator.NewParallelIterator(specs.Specs(), config.SyncHost)
resp, err := http.Get(config.SyncHost + "/has-counter")
if err != nil || resp.StatusCode != http.StatusOK {
iterator = spec_iterator.NewShardedParallelIterator(specs.Specs(), config.ParallelTotal, config.ParallelNode)
}
} else {
iterator = spec_iterator.NewSerialIterator(specs.Specs())
}
return iterator, specs.HasProgrammaticFocus()
}
func (suite *Suite) CurrentRunningSpecSummary() (*types.SpecSummary, bool) {
return suite.runner.CurrentSpecSummary()
}
func (suite *Suite) SetBeforeSuiteNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration) {
if suite.beforeSuiteNode != nil {
panic("You may only call BeforeSuite once!")
}
suite.beforeSuiteNode = leafnodes.NewBeforeSuiteNode(body, codeLocation, timeout, suite.failer)
}
func (suite *Suite) SetAfterSuiteNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration) {
if suite.afterSuiteNode != nil {
panic("You may only call AfterSuite once!")
}
suite.afterSuiteNode = leafnodes.NewAfterSuiteNode(body, codeLocation, timeout, suite.failer)
}
func (suite *Suite) SetSynchronizedBeforeSuiteNode(bodyA interface{}, bodyB interface{}, codeLocation types.CodeLocation, timeout time.Duration) {
if suite.beforeSuiteNode != nil {
panic("You may only call BeforeSuite once!")
}
suite.beforeSuiteNode = leafnodes.NewSynchronizedBeforeSuiteNode(bodyA, bodyB, codeLocation, timeout, suite.failer)
}
func (suite *Suite) SetSynchronizedAfterSuiteNode(bodyA interface{}, bodyB interface{}, codeLocation types.CodeLocation, timeout time.Duration) {
if suite.afterSuiteNode != nil {
panic("You may only call AfterSuite once!")
}
suite.afterSuiteNode = leafnodes.NewSynchronizedAfterSuiteNode(bodyA, bodyB, codeLocation, timeout, suite.failer)
}
func (suite *Suite) PushContainerNode(text string, body func(), flag types.FlagType, codeLocation types.CodeLocation) {
container := containernode.New(text, flag, codeLocation)
suite.currentContainer.PushContainerNode(container)
previousContainer := suite.currentContainer
suite.currentContainer = container
suite.containerIndex++
body()
suite.containerIndex--
suite.currentContainer = previousContainer
}
func (suite *Suite) PushItNode(text string, body interface{}, flag types.FlagType, codeLocation types.CodeLocation, timeout time.Duration) {
if suite.running {
suite.failer.Fail("You may only call It from within a Describe, Context or When", codeLocation)
}
suite.currentContainer.PushSubjectNode(leafnodes.NewItNode(text, body, flag, codeLocation, timeout, suite.failer, suite.containerIndex))
}
func (suite *Suite) PushMeasureNode(text string, body interface{}, flag types.FlagType, codeLocation types.CodeLocation, samples int) {
if suite.running {
suite.failer.Fail("You may only call Measure from within a Describe, Context or When", codeLocation)
}
suite.currentContainer.PushSubjectNode(leafnodes.NewMeasureNode(text, body, flag, codeLocation, samples, suite.failer, suite.containerIndex))
}
func (suite *Suite) PushBeforeEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration) {
if suite.running {
suite.failer.Fail("You may only call BeforeEach from within a Describe, Context or When", codeLocation)
}
suite.currentContainer.PushSetupNode(leafnodes.NewBeforeEachNode(body, codeLocation, timeout, suite.failer, suite.containerIndex))
}
func (suite *Suite) PushJustBeforeEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration) {
if suite.running {
suite.failer.Fail("You may only call JustBeforeEach from within a Describe, Context or When", codeLocation)
}
suite.currentContainer.PushSetupNode(leafnodes.NewJustBeforeEachNode(body, codeLocation, timeout, suite.failer, suite.containerIndex))
}
func (suite *Suite) PushAfterEachNode(body interface{}, codeLocation types.CodeLocation, timeout time.Duration) {
if suite.running {
suite.failer.Fail("You may only call AfterEach from within a Describe, Context or When", codeLocation)
}
suite.currentContainer.PushSetupNode(leafnodes.NewAfterEachNode(body, codeLocation, timeout, suite.failer, suite.containerIndex))
}

View file

@ -0,0 +1,76 @@
package testingtproxy
import (
"fmt"
"io"
)
type failFunc func(message string, callerSkip ...int)
func New(writer io.Writer, fail failFunc, offset int) *ginkgoTestingTProxy {
return &ginkgoTestingTProxy{
fail: fail,
offset: offset,
writer: writer,
}
}
type ginkgoTestingTProxy struct {
fail failFunc
offset int
writer io.Writer
}
func (t *ginkgoTestingTProxy) Error(args ...interface{}) {
t.fail(fmt.Sprintln(args...), t.offset)
}
func (t *ginkgoTestingTProxy) Errorf(format string, args ...interface{}) {
t.fail(fmt.Sprintf(format, args...), t.offset)
}
func (t *ginkgoTestingTProxy) Fail() {
t.fail("failed", t.offset)
}
func (t *ginkgoTestingTProxy) FailNow() {
t.fail("failed", t.offset)
}
func (t *ginkgoTestingTProxy) Fatal(args ...interface{}) {
t.fail(fmt.Sprintln(args...), t.offset)
}
func (t *ginkgoTestingTProxy) Fatalf(format string, args ...interface{}) {
t.fail(fmt.Sprintf(format, args...), t.offset)
}
func (t *ginkgoTestingTProxy) Log(args ...interface{}) {
fmt.Fprintln(t.writer, args...)
}
func (t *ginkgoTestingTProxy) Logf(format string, args ...interface{}) {
t.Log(fmt.Sprintf(format, args...))
}
func (t *ginkgoTestingTProxy) Failed() bool {
return false
}
func (t *ginkgoTestingTProxy) Parallel() {
}
func (t *ginkgoTestingTProxy) Skip(args ...interface{}) {
fmt.Println(args...)
}
func (t *ginkgoTestingTProxy) Skipf(format string, args ...interface{}) {
t.Skip(fmt.Sprintf(format, args...))
}
func (t *ginkgoTestingTProxy) SkipNow() {
}
func (t *ginkgoTestingTProxy) Skipped() bool {
return false
}

View file

@ -0,0 +1,36 @@
package writer
type FakeGinkgoWriter struct {
EventStream []string
}
func NewFake() *FakeGinkgoWriter {
return &FakeGinkgoWriter{
EventStream: []string{},
}
}
func (writer *FakeGinkgoWriter) AddEvent(event string) {
writer.EventStream = append(writer.EventStream, event)
}
func (writer *FakeGinkgoWriter) Truncate() {
writer.EventStream = append(writer.EventStream, "TRUNCATE")
}
func (writer *FakeGinkgoWriter) DumpOut() {
writer.EventStream = append(writer.EventStream, "DUMP")
}
func (writer *FakeGinkgoWriter) DumpOutWithHeader(header string) {
writer.EventStream = append(writer.EventStream, "DUMP_WITH_HEADER: "+header)
}
func (writer *FakeGinkgoWriter) Bytes() []byte {
writer.EventStream = append(writer.EventStream, "BYTES")
return nil
}
func (writer *FakeGinkgoWriter) Write(data []byte) (n int, err error) {
return 0, nil
}

View file

@ -0,0 +1,89 @@
package writer
import (
"bytes"
"io"
"sync"
)
type WriterInterface interface {
io.Writer
Truncate()
DumpOut()
DumpOutWithHeader(header string)
Bytes() []byte
}
type Writer struct {
buffer *bytes.Buffer
outWriter io.Writer
lock *sync.Mutex
stream bool
redirector io.Writer
}
func New(outWriter io.Writer) *Writer {
return &Writer{
buffer: &bytes.Buffer{},
lock: &sync.Mutex{},
outWriter: outWriter,
stream: true,
}
}
func (w *Writer) AndRedirectTo(writer io.Writer) {
w.redirector = writer
}
func (w *Writer) SetStream(stream bool) {
w.lock.Lock()
defer w.lock.Unlock()
w.stream = stream
}
func (w *Writer) Write(b []byte) (n int, err error) {
w.lock.Lock()
defer w.lock.Unlock()
n, err = w.buffer.Write(b)
if w.redirector != nil {
w.redirector.Write(b)
}
if w.stream {
return w.outWriter.Write(b)
}
return n, err
}
func (w *Writer) Truncate() {
w.lock.Lock()
defer w.lock.Unlock()
w.buffer.Reset()
}
func (w *Writer) DumpOut() {
w.lock.Lock()
defer w.lock.Unlock()
if !w.stream {
w.buffer.WriteTo(w.outWriter)
}
}
func (w *Writer) Bytes() []byte {
w.lock.Lock()
defer w.lock.Unlock()
b := w.buffer.Bytes()
copied := make([]byte, len(b))
copy(copied, b)
return copied
}
func (w *Writer) DumpOutWithHeader(header string) {
w.lock.Lock()
defer w.lock.Unlock()
if !w.stream && w.buffer.Len() > 0 {
w.outWriter.Write([]byte(header))
w.buffer.WriteTo(w.outWriter)
}
}

View file

@ -0,0 +1,84 @@
/*
Ginkgo's Default Reporter
A number of command line flags are available to tweak Ginkgo's default output.
These are documented [here](http://onsi.github.io/ginkgo/#running_tests)
*/
package reporters
import (
"github.com/onsi/ginkgo/config"
"github.com/onsi/ginkgo/reporters/stenographer"
"github.com/onsi/ginkgo/types"
)
type DefaultReporter struct {
config config.DefaultReporterConfigType
stenographer stenographer.Stenographer
specSummaries []*types.SpecSummary
}
func NewDefaultReporter(config config.DefaultReporterConfigType, stenographer stenographer.Stenographer) *DefaultReporter {
return &DefaultReporter{
config: config,
stenographer: stenographer,
}
}
func (reporter *DefaultReporter) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) {
reporter.stenographer.AnnounceSuite(summary.SuiteDescription, config.RandomSeed, config.RandomizeAllSpecs, reporter.config.Succinct)
if config.ParallelTotal > 1 {
reporter.stenographer.AnnounceParallelRun(config.ParallelNode, config.ParallelTotal, reporter.config.Succinct)
} else {
reporter.stenographer.AnnounceNumberOfSpecs(summary.NumberOfSpecsThatWillBeRun, summary.NumberOfTotalSpecs, reporter.config.Succinct)
}
}
func (reporter *DefaultReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) {
if setupSummary.State != types.SpecStatePassed {
reporter.stenographer.AnnounceBeforeSuiteFailure(setupSummary, reporter.config.Succinct, reporter.config.FullTrace)
}
}
func (reporter *DefaultReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) {
if setupSummary.State != types.SpecStatePassed {
reporter.stenographer.AnnounceAfterSuiteFailure(setupSummary, reporter.config.Succinct, reporter.config.FullTrace)
}
}
func (reporter *DefaultReporter) SpecWillRun(specSummary *types.SpecSummary) {
if reporter.config.Verbose && !reporter.config.Succinct && specSummary.State != types.SpecStatePending && specSummary.State != types.SpecStateSkipped {
reporter.stenographer.AnnounceSpecWillRun(specSummary)
}
}
func (reporter *DefaultReporter) SpecDidComplete(specSummary *types.SpecSummary) {
switch specSummary.State {
case types.SpecStatePassed:
if specSummary.IsMeasurement {
reporter.stenographer.AnnounceSuccesfulMeasurement(specSummary, reporter.config.Succinct)
} else if specSummary.RunTime.Seconds() >= reporter.config.SlowSpecThreshold {
reporter.stenographer.AnnounceSuccesfulSlowSpec(specSummary, reporter.config.Succinct)
} else {
reporter.stenographer.AnnounceSuccesfulSpec(specSummary)
}
case types.SpecStatePending:
reporter.stenographer.AnnouncePendingSpec(specSummary, reporter.config.NoisyPendings && !reporter.config.Succinct)
case types.SpecStateSkipped:
reporter.stenographer.AnnounceSkippedSpec(specSummary, reporter.config.Succinct || !reporter.config.NoisySkippings, reporter.config.FullTrace)
case types.SpecStateTimedOut:
reporter.stenographer.AnnounceSpecTimedOut(specSummary, reporter.config.Succinct, reporter.config.FullTrace)
case types.SpecStatePanicked:
reporter.stenographer.AnnounceSpecPanicked(specSummary, reporter.config.Succinct, reporter.config.FullTrace)
case types.SpecStateFailed:
reporter.stenographer.AnnounceSpecFailed(specSummary, reporter.config.Succinct, reporter.config.FullTrace)
}
reporter.specSummaries = append(reporter.specSummaries, specSummary)
}
func (reporter *DefaultReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) {
reporter.stenographer.SummarizeFailures(reporter.specSummaries)
reporter.stenographer.AnnounceSpecRunCompletion(summary, reporter.config.Succinct)
}

View file

@ -0,0 +1,59 @@
package reporters
import (
"github.com/onsi/ginkgo/config"
"github.com/onsi/ginkgo/types"
)
//FakeReporter is useful for testing purposes
type FakeReporter struct {
Config config.GinkgoConfigType
BeginSummary *types.SuiteSummary
BeforeSuiteSummary *types.SetupSummary
SpecWillRunSummaries []*types.SpecSummary
SpecSummaries []*types.SpecSummary
AfterSuiteSummary *types.SetupSummary
EndSummary *types.SuiteSummary
SpecWillRunStub func(specSummary *types.SpecSummary)
SpecDidCompleteStub func(specSummary *types.SpecSummary)
}
func NewFakeReporter() *FakeReporter {
return &FakeReporter{
SpecWillRunSummaries: make([]*types.SpecSummary, 0),
SpecSummaries: make([]*types.SpecSummary, 0),
}
}
func (fakeR *FakeReporter) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) {
fakeR.Config = config
fakeR.BeginSummary = summary
}
func (fakeR *FakeReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) {
fakeR.BeforeSuiteSummary = setupSummary
}
func (fakeR *FakeReporter) SpecWillRun(specSummary *types.SpecSummary) {
if fakeR.SpecWillRunStub != nil {
fakeR.SpecWillRunStub(specSummary)
}
fakeR.SpecWillRunSummaries = append(fakeR.SpecWillRunSummaries, specSummary)
}
func (fakeR *FakeReporter) SpecDidComplete(specSummary *types.SpecSummary) {
if fakeR.SpecDidCompleteStub != nil {
fakeR.SpecDidCompleteStub(specSummary)
}
fakeR.SpecSummaries = append(fakeR.SpecSummaries, specSummary)
}
func (fakeR *FakeReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) {
fakeR.AfterSuiteSummary = setupSummary
}
func (fakeR *FakeReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) {
fakeR.EndSummary = summary
}

View file

@ -0,0 +1,152 @@
/*
JUnit XML Reporter for Ginkgo
For usage instructions: http://onsi.github.io/ginkgo/#generating_junit_xml_output
*/
package reporters
import (
"encoding/xml"
"fmt"
"math"
"os"
"strings"
"github.com/onsi/ginkgo/config"
"github.com/onsi/ginkgo/types"
)
type JUnitTestSuite struct {
XMLName xml.Name `xml:"testsuite"`
TestCases []JUnitTestCase `xml:"testcase"`
Name string `xml:"name,attr"`
Tests int `xml:"tests,attr"`
Failures int `xml:"failures,attr"`
Errors int `xml:"errors,attr"`
Time float64 `xml:"time,attr"`
}
type JUnitTestCase struct {
Name string `xml:"name,attr"`
ClassName string `xml:"classname,attr"`
FailureMessage *JUnitFailureMessage `xml:"failure,omitempty"`
Skipped *JUnitSkipped `xml:"skipped,omitempty"`
Time float64 `xml:"time,attr"`
SystemOut string `xml:"system-out,omitempty"`
}
type JUnitFailureMessage struct {
Type string `xml:"type,attr"`
Message string `xml:",chardata"`
}
type JUnitSkipped struct {
XMLName xml.Name `xml:"skipped"`
}
type JUnitReporter struct {
suite JUnitTestSuite
filename string
testSuiteName string
}
//NewJUnitReporter creates a new JUnit XML reporter. The XML will be stored in the passed in filename.
func NewJUnitReporter(filename string) *JUnitReporter {
return &JUnitReporter{
filename: filename,
}
}
func (reporter *JUnitReporter) SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary) {
reporter.suite = JUnitTestSuite{
Name: summary.SuiteDescription,
TestCases: []JUnitTestCase{},
}
reporter.testSuiteName = summary.SuiteDescription
}
func (reporter *JUnitReporter) SpecWillRun(specSummary *types.SpecSummary) {
}
func (reporter *JUnitReporter) BeforeSuiteDidRun(setupSummary *types.SetupSummary) {
reporter.handleSetupSummary("BeforeSuite", setupSummary)
}
func (reporter *JUnitReporter) AfterSuiteDidRun(setupSummary *types.SetupSummary) {
reporter.handleSetupSummary("AfterSuite", setupSummary)
}
func failureMessage(failure types.SpecFailure) string {
return fmt.Sprintf("%s\n%s\n%s", failure.ComponentCodeLocation.String(), failure.Message, failure.Location.String())
}
func (reporter *JUnitReporter) handleSetupSummary(name string, setupSummary *types.SetupSummary) {
if setupSummary.State != types.SpecStatePassed {
testCase := JUnitTestCase{
Name: name,
ClassName: reporter.testSuiteName,
}
testCase.FailureMessage = &JUnitFailureMessage{
Type: reporter.failureTypeForState(setupSummary.State),
Message: failureMessage(setupSummary.Failure),
}
testCase.SystemOut = setupSummary.CapturedOutput
testCase.Time = setupSummary.RunTime.Seconds()
reporter.suite.TestCases = append(reporter.suite.TestCases, testCase)
}
}
func (reporter *JUnitReporter) SpecDidComplete(specSummary *types.SpecSummary) {
testCase := JUnitTestCase{
Name: strings.Join(specSummary.ComponentTexts[1:], " "),
ClassName: reporter.testSuiteName,
}
if specSummary.State == types.SpecStateFailed || specSummary.State == types.SpecStateTimedOut || specSummary.State == types.SpecStatePanicked {
testCase.FailureMessage = &JUnitFailureMessage{
Type: reporter.failureTypeForState(specSummary.State),
Message: failureMessage(specSummary.Failure),
}
testCase.SystemOut = specSummary.CapturedOutput
}
if specSummary.State == types.SpecStateSkipped || specSummary.State == types.SpecStatePending {
testCase.Skipped = &JUnitSkipped{}
}
testCase.Time = specSummary.RunTime.Seconds()
reporter.suite.TestCases = append(reporter.suite.TestCases, testCase)
}
func (reporter *JUnitReporter) SpecSuiteDidEnd(summary *types.SuiteSummary) {
reporter.suite.Tests = summary.NumberOfSpecsThatWillBeRun
reporter.suite.Time = math.Trunc(summary.RunTime.Seconds() * 1000 / 1000)
reporter.suite.Failures = summary.NumberOfFailedSpecs
reporter.suite.Errors = 0
file, err := os.Create(reporter.filename)
if err != nil {
fmt.Printf("Failed to create JUnit report file: %s\n\t%s", reporter.filename, err.Error())
}
defer file.Close()
file.WriteString(xml.Header)
encoder := xml.NewEncoder(file)
encoder.Indent(" ", " ")
err = encoder.Encode(reporter.suite)
if err != nil {
fmt.Printf("Failed to generate JUnit report\n\t%s", err.Error())
}
}
func (reporter *JUnitReporter) failureTypeForState(state types.SpecState) string {
switch state {
case types.SpecStateFailed:
return "Failure"
case types.SpecStateTimedOut:
return "Timeout"
case types.SpecStatePanicked:
return "Panic"
default:
return ""
}
}

15
vendor/github.com/onsi/ginkgo/reporters/reporter.go generated vendored Normal file
View file

@ -0,0 +1,15 @@
package reporters
import (
"github.com/onsi/ginkgo/config"
"github.com/onsi/ginkgo/types"
)
type Reporter interface {
SpecSuiteWillBegin(config config.GinkgoConfigType, summary *types.SuiteSummary)
BeforeSuiteDidRun(setupSummary *types.SetupSummary)
SpecWillRun(specSummary *types.SpecSummary)
SpecDidComplete(specSummary *types.SpecSummary)
AfterSuiteDidRun(setupSummary *types.SetupSummary)
SpecSuiteDidEnd(summary *types.SuiteSummary)
}

Some files were not shown because too many files have changed in this diff Show more