mirror of https://github.com/prometheus/prometheus
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
356 lines
10 KiB
356 lines
10 KiB
// Copyright 2018 The Prometheus 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 kubernetes |
|
|
|
import ( |
|
"context" |
|
"encoding/json" |
|
"errors" |
|
"testing" |
|
"time" |
|
|
|
"github.com/go-kit/log" |
|
prom_testutil "github.com/prometheus/client_golang/prometheus/testutil" |
|
"github.com/stretchr/testify/require" |
|
apierrors "k8s.io/apimachinery/pkg/api/errors" |
|
"k8s.io/apimachinery/pkg/runtime" |
|
"k8s.io/apimachinery/pkg/version" |
|
"k8s.io/apimachinery/pkg/watch" |
|
fakediscovery "k8s.io/client-go/discovery/fake" |
|
"k8s.io/client-go/kubernetes" |
|
"k8s.io/client-go/kubernetes/fake" |
|
kubetesting "k8s.io/client-go/testing" |
|
"k8s.io/client-go/tools/cache" |
|
|
|
"github.com/prometheus/client_golang/prometheus" |
|
|
|
"github.com/prometheus/prometheus/discovery" |
|
"github.com/prometheus/prometheus/discovery/targetgroup" |
|
"github.com/prometheus/prometheus/util/testutil" |
|
) |
|
|
|
func TestMain(m *testing.M) { |
|
testutil.TolerantVerifyLeak(m) |
|
} |
|
|
|
// makeDiscovery creates a kubernetes.Discovery instance for testing. |
|
func makeDiscovery(role Role, nsDiscovery NamespaceDiscovery, objects ...runtime.Object) (*Discovery, kubernetes.Interface) { |
|
return makeDiscoveryWithVersion(role, nsDiscovery, "v1.22.0", objects...) |
|
} |
|
|
|
// makeDiscoveryWithVersion creates a kubernetes.Discovery instance with the specified kubernetes version for testing. |
|
func makeDiscoveryWithVersion(role Role, nsDiscovery NamespaceDiscovery, k8sVer string, objects ...runtime.Object) (*Discovery, kubernetes.Interface) { |
|
clientset := fake.NewSimpleClientset(objects...) |
|
fakeDiscovery, _ := clientset.Discovery().(*fakediscovery.FakeDiscovery) |
|
fakeDiscovery.FakedServerVersion = &version.Info{GitVersion: k8sVer} |
|
|
|
reg := prometheus.NewRegistry() |
|
refreshMetrics := discovery.NewRefreshMetrics(reg) |
|
metrics := newDiscovererMetrics(reg, refreshMetrics) |
|
err := metrics.Register() |
|
if err != nil { |
|
panic(err) |
|
} |
|
// TODO(ptodev): Unregister the metrics at the end of the test. |
|
|
|
kubeMetrics, ok := metrics.(*kubernetesMetrics) |
|
if !ok { |
|
panic("invalid discovery metrics type") |
|
} |
|
|
|
d := &Discovery{ |
|
client: clientset, |
|
logger: log.NewNopLogger(), |
|
role: role, |
|
namespaceDiscovery: &nsDiscovery, |
|
ownNamespace: "own-ns", |
|
metrics: kubeMetrics, |
|
} |
|
|
|
return d, clientset |
|
} |
|
|
|
// makeDiscoveryWithMetadata creates a kubernetes.Discovery instance with the specified metadata config. |
|
func makeDiscoveryWithMetadata(role Role, nsDiscovery NamespaceDiscovery, attachMetadata AttachMetadataConfig, objects ...runtime.Object) (*Discovery, kubernetes.Interface) { |
|
d, k8s := makeDiscovery(role, nsDiscovery, objects...) |
|
d.attachMetadata = attachMetadata |
|
return d, k8s |
|
} |
|
|
|
type k8sDiscoveryTest struct { |
|
// discovery is instance of discovery.Discoverer |
|
discovery discovery.Discoverer |
|
// beforeRun runs before discoverer run |
|
beforeRun func() |
|
// afterStart runs after discoverer has synced |
|
afterStart func() |
|
// expectedMaxItems is expected max items we may get from channel |
|
expectedMaxItems int |
|
// expectedRes is expected final result |
|
expectedRes map[string]*targetgroup.Group |
|
} |
|
|
|
func (d k8sDiscoveryTest) Run(t *testing.T) { |
|
t.Helper() |
|
ch := make(chan []*targetgroup.Group) |
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute) |
|
defer cancel() |
|
|
|
if d.beforeRun != nil { |
|
d.beforeRun() |
|
} |
|
|
|
// Run discoverer and start a goroutine to read results. |
|
go d.discovery.Run(ctx, ch) |
|
|
|
// Ensure that discovery has a discoverer set. This prevents a race |
|
// condition where the above go routine may or may not have set a |
|
// discoverer yet. |
|
lastDiscoverersCount := 0 |
|
dis := d.discovery.(*Discovery) |
|
for { |
|
dis.RLock() |
|
l := len(dis.discoverers) |
|
dis.RUnlock() |
|
if l > 0 && l == lastDiscoverersCount { |
|
break |
|
} |
|
time.Sleep(100 * time.Millisecond) |
|
|
|
lastDiscoverersCount = l |
|
} |
|
|
|
resChan := make(chan map[string]*targetgroup.Group) |
|
go readResultWithTimeout(t, ctx, ch, d.expectedMaxItems, time.Second, resChan) |
|
|
|
dd, ok := d.discovery.(hasSynced) |
|
require.True(t, ok, "discoverer does not implement hasSynced interface") |
|
require.True(t, cache.WaitForCacheSync(ctx.Done(), dd.hasSynced), "discoverer failed to sync: %v", dd) |
|
|
|
if d.afterStart != nil { |
|
d.afterStart() |
|
} |
|
|
|
if d.expectedRes != nil { |
|
res := <-resChan |
|
requireTargetGroups(t, d.expectedRes, res) |
|
} else { |
|
// Stop readResultWithTimeout and wait for it. |
|
cancel() |
|
<-resChan |
|
} |
|
} |
|
|
|
// readResultWithTimeout reads all targetgroups from channel with timeout. |
|
// It merges targetgroups by source and sends the result to result channel. |
|
func readResultWithTimeout(t *testing.T, ctx context.Context, ch <-chan []*targetgroup.Group, max int, stopAfter time.Duration, resChan chan<- map[string]*targetgroup.Group) { |
|
res := make(map[string]*targetgroup.Group) |
|
timeout := time.After(stopAfter) |
|
Loop: |
|
for { |
|
select { |
|
case tgs := <-ch: |
|
for _, tg := range tgs { |
|
if tg == nil { |
|
continue |
|
} |
|
res[tg.Source] = tg |
|
} |
|
if len(res) == max { |
|
// Reached max target groups we may get, break fast. |
|
break Loop |
|
} |
|
case <-timeout: |
|
// Because we use queue, an object that is created then |
|
// deleted or updated may be processed only once. |
|
// So possibly we may skip events, timed out here. |
|
t.Logf("timed out, got %d (max: %d) items, some events are skipped", len(res), max) |
|
break Loop |
|
case <-ctx.Done(): |
|
t.Logf("stopped, got %d (max: %d) items", len(res), max) |
|
break Loop |
|
} |
|
} |
|
|
|
resChan <- res |
|
} |
|
|
|
func requireTargetGroups(t *testing.T, expected, res map[string]*targetgroup.Group) { |
|
t.Helper() |
|
b1, err := marshalTargetGroups(expected) |
|
if err != nil { |
|
panic(err) |
|
} |
|
b2, err := marshalTargetGroups(res) |
|
if err != nil { |
|
panic(err) |
|
} |
|
|
|
require.Equal(t, string(b1), string(b2)) |
|
} |
|
|
|
// marshalTargetGroups serializes a set of target groups to JSON, ignoring the |
|
// custom MarshalJSON function defined on the targetgroup.Group struct. |
|
// marshalTargetGroups can be used for making exact comparisons between target groups |
|
// as it will serialize all target labels. |
|
func marshalTargetGroups(tgs map[string]*targetgroup.Group) ([]byte, error) { |
|
type targetGroupAlias targetgroup.Group |
|
|
|
aliases := make(map[string]*targetGroupAlias, len(tgs)) |
|
for k, v := range tgs { |
|
tg := targetGroupAlias(*v) |
|
aliases[k] = &tg |
|
} |
|
|
|
return json.Marshal(aliases) |
|
} |
|
|
|
type hasSynced interface { |
|
// hasSynced returns true if all informers synced. |
|
// This is only used in testing to determine when discoverer synced to |
|
// kubernetes apiserver. |
|
hasSynced() bool |
|
} |
|
|
|
var ( |
|
_ hasSynced = &Discovery{} |
|
_ hasSynced = &Node{} |
|
_ hasSynced = &Endpoints{} |
|
_ hasSynced = &EndpointSlice{} |
|
_ hasSynced = &Ingress{} |
|
_ hasSynced = &Pod{} |
|
_ hasSynced = &Service{} |
|
) |
|
|
|
func (d *Discovery) hasSynced() bool { |
|
d.RLock() |
|
defer d.RUnlock() |
|
for _, discoverer := range d.discoverers { |
|
if hasSynceddiscoverer, ok := discoverer.(hasSynced); ok { |
|
if !hasSynceddiscoverer.hasSynced() { |
|
return false |
|
} |
|
} |
|
} |
|
return true |
|
} |
|
|
|
func (n *Node) hasSynced() bool { |
|
return n.informer.HasSynced() |
|
} |
|
|
|
func (e *Endpoints) hasSynced() bool { |
|
return e.endpointsInf.HasSynced() && e.serviceInf.HasSynced() && e.podInf.HasSynced() |
|
} |
|
|
|
func (e *EndpointSlice) hasSynced() bool { |
|
return e.endpointSliceInf.HasSynced() && e.serviceInf.HasSynced() && e.podInf.HasSynced() |
|
} |
|
|
|
func (i *Ingress) hasSynced() bool { |
|
return i.informer.HasSynced() |
|
} |
|
|
|
func (p *Pod) hasSynced() bool { |
|
return p.podInf.HasSynced() |
|
} |
|
|
|
func (s *Service) hasSynced() bool { |
|
return s.informer.HasSynced() |
|
} |
|
|
|
func TestRetryOnError(t *testing.T) { |
|
for _, successAt := range []int{1, 2, 3} { |
|
var called int |
|
f := func() error { |
|
called++ |
|
if called >= successAt { |
|
return nil |
|
} |
|
return errors.New("dummy") |
|
} |
|
retryOnError(context.TODO(), 0, f) |
|
require.Equal(t, successAt, called) |
|
} |
|
} |
|
|
|
func TestCheckNetworkingV1Supported(t *testing.T) { |
|
tests := []struct { |
|
version string |
|
wantSupported bool |
|
wantErr bool |
|
}{ |
|
{version: "v1.18.0", wantSupported: false, wantErr: false}, |
|
{version: "v1.18.1", wantSupported: false, wantErr: false}, |
|
// networking v1 is supported since Kubernetes v1.19 |
|
{version: "v1.19.0", wantSupported: true, wantErr: false}, |
|
{version: "v1.20.0-beta.2", wantSupported: true, wantErr: false}, |
|
// error patterns |
|
{version: "", wantSupported: false, wantErr: true}, |
|
{version: "<>", wantSupported: false, wantErr: true}, |
|
} |
|
|
|
for _, tc := range tests { |
|
tc := tc |
|
t.Run(tc.version, func(t *testing.T) { |
|
clientset := fake.NewSimpleClientset() |
|
fakeDiscovery, _ := clientset.Discovery().(*fakediscovery.FakeDiscovery) |
|
fakeDiscovery.FakedServerVersion = &version.Info{GitVersion: tc.version} |
|
supported, err := checkNetworkingV1Supported(clientset) |
|
|
|
if tc.wantErr { |
|
require.Error(t, err) |
|
} else { |
|
require.NoError(t, err) |
|
} |
|
require.Equal(t, tc.wantSupported, supported) |
|
}) |
|
} |
|
} |
|
|
|
func TestFailuresCountMetric(t *testing.T) { |
|
tests := []struct { |
|
role Role |
|
minFailedWatches int |
|
}{ |
|
{RoleNode, 1}, |
|
{RolePod, 1}, |
|
{RoleService, 1}, |
|
{RoleEndpoint, 3}, |
|
{RoleEndpointSlice, 3}, |
|
{RoleIngress, 1}, |
|
} |
|
|
|
for _, tc := range tests { |
|
tc := tc |
|
t.Run(string(tc.role), func(t *testing.T) { |
|
t.Parallel() |
|
|
|
n, c := makeDiscovery(tc.role, NamespaceDiscovery{}) |
|
// The counter is initialized and no failures at the beginning. |
|
require.Equal(t, float64(0), prom_testutil.ToFloat64(n.metrics.failuresCount)) |
|
|
|
// Simulate an error on watch requests. |
|
c.Discovery().(*fakediscovery.FakeDiscovery).PrependWatchReactor("*", func(action kubetesting.Action) (bool, watch.Interface, error) { |
|
return true, nil, apierrors.NewUnauthorized("unauthorized") |
|
}) |
|
|
|
// Start the discovery. |
|
k8sDiscoveryTest{discovery: n}.Run(t) |
|
|
|
// At least the errors of the initial watches should be caught (watches are retried on errors). |
|
require.GreaterOrEqual(t, prom_testutil.ToFloat64(n.metrics.failuresCount), float64(tc.minFailedWatches)) |
|
}) |
|
} |
|
}
|
|
|