k8s.io/client-go@v0.22.2/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/pkg/apis/clientauthentication"
    27  	"k8s.io/client-go/tools/clientcmd/api"
    28  	"k8s.io/client-go/tools/metrics"
    29  )
    30  
    31  type mockExpiryGauge struct {
    32  	v *time.Time
    33  }
    34  
    35  func (m *mockExpiryGauge) Set(t *time.Time) {
    36  	m.v = t
    37  }
    38  
    39  func ptr(t time.Time) *time.Time {
    40  	return &t
    41  }
    42  
    43  func TestCertificateExpirationTracker(t *testing.T) {
    44  	now := time.Now()
    45  	mockMetric := &mockExpiryGauge{}
    46  
    47  	tracker := &certificateExpirationTracker{
    48  		m:         map[*Authenticator]time.Time{},
    49  		metricSet: mockMetric.Set,
    50  	}
    51  
    52  	firstAuthenticator := &Authenticator{}
    53  	secondAuthenticator := &Authenticator{}
    54  	for _, tc := range []struct {
    55  		desc string
    56  		auth *Authenticator
    57  		time time.Time
    58  		want *time.Time
    59  	}{
    60  		{
    61  			desc: "ttl for one authenticator",
    62  			auth: firstAuthenticator,
    63  			time: now.Add(time.Minute * 10),
    64  			want: ptr(now.Add(time.Minute * 10)),
    65  		},
    66  		{
    67  			desc: "second authenticator shorter ttl",
    68  			auth: secondAuthenticator,
    69  			time: now.Add(time.Minute * 5),
    70  			want: ptr(now.Add(time.Minute * 5)),
    71  		},
    72  		{
    73  			desc: "update shorter to be longer",
    74  			auth: secondAuthenticator,
    75  			time: now.Add(time.Minute * 15),
    76  			want: ptr(now.Add(time.Minute * 10)),
    77  		},
    78  		{
    79  			desc: "update shorter to be zero time",
    80  			auth: firstAuthenticator,
    81  			time: time.Time{},
    82  			want: ptr(now.Add(time.Minute * 15)),
    83  		},
    84  		{
    85  			desc: "update last to be zero time records nil",
    86  			auth: secondAuthenticator,
    87  			time: time.Time{},
    88  			want: nil,
    89  		},
    90  	} {
    91  		// Must run in series as the tests build off each other.
    92  		t.Run(tc.desc, func(t *testing.T) {
    93  			tracker.set(tc.auth, tc.time)
    94  			if mockMetric.v != nil && tc.want != nil {
    95  				if !mockMetric.v.Equal(*tc.want) {
    96  					t.Errorf("got: %s; want: %s", mockMetric.v, tc.want)
    97  				}
    98  			} else if mockMetric.v != tc.want {
    99  				t.Errorf("got: %s; want: %s", mockMetric.v, tc.want)
   100  			}
   101  		})
   102  	}
   103  }
   104  
   105  type mockCallsMetric struct {
   106  	exitCode  int
   107  	errorType string
   108  }
   109  
   110  type mockCallsMetricCounter struct {
   111  	calls []mockCallsMetric
   112  }
   113  
   114  func (f *mockCallsMetricCounter) Increment(exitCode int, errorType string) {
   115  	f.calls = append(f.calls, mockCallsMetric{exitCode: exitCode, errorType: errorType})
   116  }
   117  
   118  func TestCallsMetric(t *testing.T) {
   119  	const (
   120  		goodOutput = `{
   121  			"kind": "ExecCredential",
   122  			"apiVersion": "client.authentication.k8s.io/v1beta1",
   123  			"status": {
   124  				"token": "foo-bar"
   125  			}
   126  		}`
   127  	)
   128  
   129  	callsMetricCounter := &mockCallsMetricCounter{}
   130  	originalExecPluginCalls := metrics.ExecPluginCalls
   131  	t.Cleanup(func() { metrics.ExecPluginCalls = originalExecPluginCalls })
   132  	metrics.ExecPluginCalls = callsMetricCounter
   133  
   134  	exitCodes := []int{0, 1, 2, 0}
   135  	var wantCallsMetrics []mockCallsMetric
   136  	for _, exitCode := range exitCodes {
   137  		c := api.ExecConfig{
   138  			Command:    "./testdata/test-plugin.sh",
   139  			APIVersion: "client.authentication.k8s.io/v1beta1",
   140  			Env: []api.ExecEnvVar{
   141  				{Name: "TEST_EXIT_CODE", Value: fmt.Sprintf("%d", exitCode)},
   142  				{Name: "TEST_OUTPUT", Value: goodOutput},
   143  			},
   144  			InteractiveMode: api.IfAvailableExecInteractiveMode,
   145  		}
   146  
   147  		a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &c, nil)
   148  		if err != nil {
   149  			t.Fatal(err)
   150  		}
   151  		a.stderr = io.Discard
   152  
   153  		// Run refresh creds twice so that our test validates that the metrics are set correctly twice
   154  		// in a row with the same authenticator.
   155  		refreshCreds := func() {
   156  			if err := a.refreshCredsLocked(&clientauthentication.Response{}); (err == nil) != (exitCode == 0) {
   157  				if err != nil {
   158  					t.Fatalf("wanted no error, but got %q", err.Error())
   159  				} else {
   160  					t.Fatal("wanted error, but got nil")
   161  				}
   162  			}
   163  			mockCallsMetric := mockCallsMetric{exitCode: exitCode, errorType: "no_error"}
   164  			if exitCode != 0 {
   165  				mockCallsMetric.errorType = "plugin_execution_error"
   166  			}
   167  			wantCallsMetrics = append(wantCallsMetrics, mockCallsMetric)
   168  		}
   169  		refreshCreds()
   170  		refreshCreds()
   171  	}
   172  
   173  	// Run some iterations of the authenticator where the exec plugin fails to run to test special
   174  	// metric values.
   175  	refreshCreds := func(command string) {
   176  		c := api.ExecConfig{
   177  			Command:         command,
   178  			APIVersion:      "client.authentication.k8s.io/v1beta1",
   179  			InteractiveMode: api.IfAvailableExecInteractiveMode,
   180  		}
   181  		a, err := newAuthenticator(newCache(), func(_ int) bool { return false }, &c, nil)
   182  		if err != nil {
   183  			t.Fatal(err)
   184  		}
   185  		a.stderr = io.Discard
   186  		if err := a.refreshCredsLocked(&clientauthentication.Response{}); err == nil {
   187  			t.Fatal("expected the authenticator to fail because the plugin does not exist")
   188  		}
   189  		wantCallsMetrics = append(wantCallsMetrics, mockCallsMetric{exitCode: 1, errorType: "plugin_not_found_error"})
   190  	}
   191  	refreshCreds("does not exist without path slashes")
   192  	refreshCreds("./does/not/exist/with/relative/path")
   193  	refreshCreds("/does/not/exist/with/absolute/path")
   194  
   195  	callsMetricComparer := cmp.Comparer(func(a, b mockCallsMetric) bool {
   196  		return a.exitCode == b.exitCode && a.errorType == b.errorType
   197  	})
   198  	actuallCallsMetrics := callsMetricCounter.calls
   199  	if diff := cmp.Diff(wantCallsMetrics, actuallCallsMetrics, callsMetricComparer); diff != "" {
   200  		t.Fatalf("got unexpected metrics calls; -want, +got:\n%s", diff)
   201  	}
   202  }