k8s.io/client-go@v0.31.1/plugin/pkg/client/auth/exec/metrics_test.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package exec
    18  
    19  import (
    20  	"fmt"
    21  	"io"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/google/go-cmp/cmp"
    26  	"k8s.io/client-go/tools/clientcmd/api"
    27  	"k8s.io/client-go/tools/metrics"
    28  )
    29  
    30  type mockExpiryGauge struct {
    31  	v *time.Time
    32  }
    33  
    34  func (m *mockExpiryGauge) Set(t *time.Time) {
    35  	m.v = t
    36  }
    37  
    38  func ptr(t time.Time) *time.Time {
    39  	return &t
    40  }
    41  
    42  func TestCertificateExpirationTracker(t *testing.T) {
    43  	now := time.Now()
    44  	mockMetric := &mockExpiryGauge{}
    45  
    46  	tracker := &certificateExpirationTracker{
    47  		m:         map[*Authenticator]time.Time{},
    48  		metricSet: mockMetric.Set,
    49  	}
    50  
    51  	firstAuthenticator := &Authenticator{}
    52  	secondAuthenticator := &Authenticator{}
    53  	for _, tc := range []struct {
    54  		desc string
    55  		auth *Authenticator
    56  		time time.Time
    57  		want *time.Time
    58  	}{
    59  		{
    60  			desc: "ttl for one authenticator",
    61  			auth: firstAuthenticator,
    62  			time: now.Add(time.Minute * 10),
    63  			want: ptr(now.Add(time.Minute * 10)),
    64  		},
    65  		{
    66  			desc: "second authenticator shorter ttl",
    67  			auth: secondAuthenticator,
    68  			time: now.Add(time.Minute * 5),
    69  			want: ptr(now.Add(time.Minute * 5)),
    70  		},
    71  		{
    72  			desc: "update shorter to be longer",
    73  			auth: secondAuthenticator,
    74  			time: now.Add(time.Minute * 15),
    75  			want: ptr(now.Add(time.Minute * 10)),
    76  		},
    77  		{
    78  			desc: "update shorter to be zero time",
    79  			auth: firstAuthenticator,
    80  			time: time.Time{},
    81  			want: ptr(now.Add(time.Minute * 15)),
    82  		},
    83  		{
    84  			desc: "update last to be zero time records nil",
    85  			auth: secondAuthenticator,
    86  			time: time.Time{},
    87  			want: nil,
    88  		},
    89  	} {
    90  		// Must run in series as the tests build off each other.
    91  		t.Run(tc.desc, func(t *testing.T) {
    92  			tracker.set(tc.auth, tc.time)
    93  			if mockMetric.v != nil && tc.want != nil {
    94  				if !mockMetric.v.Equal(*tc.want) {
    95  					t.Errorf("got: %s; want: %s", mockMetric.v, tc.want)
    96  				}
    97  			} else if mockMetric.v != tc.want {
    98  				t.Errorf("got: %s; want: %s", mockMetric.v, tc.want)
    99  			}
   100  		})
   101  	}
   102  }
   103  
   104  type mockCallsMetric struct {
   105  	exitCode  int
   106  	errorType string
   107  }
   108  
   109  type mockCallsMetricCounter struct {
   110  	calls []mockCallsMetric
   111  }
   112  
   113  func (f *mockCallsMetricCounter) Increment(exitCode int, errorType string) {
   114  	f.calls = append(f.calls, mockCallsMetric{exitCode: exitCode, errorType: errorType})
   115  }
   116  
   117  func TestCallsMetric(t *testing.T) {
   118  	const (
   119  		goodOutput = `{
   120  			"kind": "ExecCredential",
   121  			"apiVersion": "client.authentication.k8s.io/v1beta1",
   122  			"status": {
   123  				"token": "foo-bar"
   124  			}
   125  		}`
   126  	)
   127  
   128  	callsMetricCounter := &mockCallsMetricCounter{}
   129  	originalExecPluginCalls := metrics.ExecPluginCalls
   130  	t.Cleanup(func() { metrics.ExecPluginCalls = originalExecPluginCalls })
   131  	metrics.ExecPluginCalls = callsMetricCounter
   132  
   133  	exitCodes := []int{0, 1, 2, 0}
   134  	var wantCallsMetrics []mockCallsMetric
   135  	for _, exitCode := range exitCodes {
   136  		c := api.ExecConfig{
   137  			Command:    "./testdata/test-plugin.sh",
   138  			APIVersion: "client.authentication.k8s.io/v1beta1",
   139  			Env: []api.ExecEnvVar{
   140  				{Name: "TEST_EXIT_CODE", Value: fmt.Sprintf("%d", exitCode)},
   141  				{Name: "TEST_OUTPUT", Value: goodOutput},
   142  			},
   143  			InteractiveMode: api.IfAvailableExecInteractiveMode,
   144  		}
   145  
   146  		a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &c, nil)
   147  		if err != nil {
   148  			t.Fatal(err)
   149  		}
   150  		a.stderr = io.Discard
   151  
   152  		// Run refresh creds twice so that our test validates that the metrics are set correctly twice
   153  		// in a row with the same authenticator.
   154  		refreshCreds := func() {
   155  			if err := a.refreshCredsLocked(); (err == nil) != (exitCode == 0) {
   156  				if err != nil {
   157  					t.Fatalf("wanted no error, but got %q", err.Error())
   158  				} else {
   159  					t.Fatal("wanted error, but got nil")
   160  				}
   161  			}
   162  			mockCallsMetric := mockCallsMetric{exitCode: exitCode, errorType: "no_error"}
   163  			if exitCode != 0 {
   164  				mockCallsMetric.errorType = "plugin_execution_error"
   165  			}
   166  			wantCallsMetrics = append(wantCallsMetrics, mockCallsMetric)
   167  		}
   168  		refreshCreds()
   169  		refreshCreds()
   170  	}
   171  
   172  	// Run some iterations of the authenticator where the exec plugin fails to run to test special
   173  	// metric values.
   174  	refreshCreds := func(command string) {
   175  		c := api.ExecConfig{
   176  			Command:         command,
   177  			APIVersion:      "client.authentication.k8s.io/v1beta1",
   178  			InteractiveMode: api.IfAvailableExecInteractiveMode,
   179  		}
   180  		a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &c, nil)
   181  		if err != nil {
   182  			t.Fatal(err)
   183  		}
   184  		a.stderr = io.Discard
   185  		if err := a.refreshCredsLocked(); err == nil {
   186  			t.Fatal("expected the authenticator to fail because the plugin does not exist")
   187  		}
   188  		wantCallsMetrics = append(wantCallsMetrics, mockCallsMetric{exitCode: 1, errorType: "plugin_not_found_error"})
   189  	}
   190  	refreshCreds("does not exist without path slashes")
   191  	refreshCreds("./does/not/exist/with/relative/path")
   192  	refreshCreds("/does/not/exist/with/absolute/path")
   193  
   194  	callsMetricComparer := cmp.Comparer(func(a, b mockCallsMetric) bool {
   195  		return a.exitCode == b.exitCode && a.errorType == b.errorType
   196  	})
   197  	actuallCallsMetrics := callsMetricCounter.calls
   198  	if diff := cmp.Diff(wantCallsMetrics, actuallCallsMetrics, callsMetricComparer); diff != "" {
   199  		t.Fatalf("got unexpected metrics calls; -want, +got:\n%s", diff)
   200  	}
   201  }