github.com/oam-dev/cluster-gateway@v1.9.0/pkg/util/exec/exec_test.go (about)

     1  //go:build unix
     2  
     3  package exec
     4  
     5  import (
     6  	"fmt"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/stretchr/testify/assert"
    11  
    12  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    13  	"k8s.io/client-go/pkg/apis/clientauthentication"
    14  	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
    15  )
    16  
    17  var (
    18  	testClusterName = "my-cluster"
    19  )
    20  
    21  func TestIssueClusterCredential(t *testing.T) {
    22  	t0 := time.Now()
    23  
    24  	cases := map[string]struct {
    25  		clusterName   string
    26  		execConfig    *clientcmdapi.ExecConfig
    27  		expected      *clientauthentication.ExecCredential
    28  		expectedError string
    29  		setup         func(t *testing.T)
    30  	}{
    31  		"missing cluster name": {
    32  			expectedError: "cluster name not provided",
    33  		},
    34  
    35  		"missing exec config": {
    36  			clusterName:   testClusterName,
    37  			expectedError: "exec config not provided",
    38  		},
    39  
    40  		"missing command property within exec config": {
    41  			clusterName:   testClusterName,
    42  			execConfig:    &clientcmdapi.ExecConfig{},
    43  			expectedError: "missing \"command\" property on exec config object",
    44  		},
    45  
    46  		"failed to run external command: command not found": {
    47  			clusterName: testClusterName,
    48  			execConfig: &clientcmdapi.ExecConfig{
    49  				Command: "/path/to/command/not/found",
    50  			},
    51  			expectedError: "exec: executable /path/to/command/not/found not found",
    52  		},
    53  
    54  		"failed to run external command: finished with non-zero exit code": {
    55  			clusterName: testClusterName,
    56  			execConfig: &clientcmdapi.ExecConfig{
    57  				APIVersion: "client.authentication.k8s.io/v1",
    58  				Command:    "false",
    59  			},
    60  			expectedError: "exec: executable /usr/bin/false failed with exit code 1",
    61  		},
    62  
    63  		"missing API version in exec config": {
    64  			clusterName: testClusterName,
    65  			execConfig: &clientcmdapi.ExecConfig{
    66  				Command: "true",
    67  			},
    68  			expectedError: `exec plugin: invalid apiVersion ""`,
    69  		},
    70  
    71  		"invalid API version in exec config": {
    72  			clusterName: testClusterName,
    73  			execConfig: &clientcmdapi.ExecConfig{
    74  				APIVersion: "example.org/v1",
    75  				Command:    "true",
    76  			},
    77  			expectedError: `exec plugin: invalid apiVersion "example.org/v1"`,
    78  		},
    79  
    80  		"invalid exec credential JSON": {
    81  			clusterName: testClusterName,
    82  			execConfig: &clientcmdapi.ExecConfig{
    83  				APIVersion: "client.authentication.k8s.io/v1",
    84  				Command:    "echo",
    85  				Args:       []string{"-n", `[]`},
    86  			},
    87  			expectedError: "decoding stdout: couldn't get version/kind; json parse error: json: cannot unmarshal array into Go value of type struct { APIVersion string \"json:\\\"apiVersion,omitempty\\\"\"; Kind string \"json:\\\"kind,omitempty\\\"\" }",
    88  		},
    89  
    90  		"cannot parse de API version": {
    91  			clusterName: testClusterName,
    92  			execConfig: &clientcmdapi.ExecConfig{
    93  				APIVersion: "a/b/c/d/e",
    94  				Command:    "true",
    95  			},
    96  			expectedError: "failed to parse exec config API version: unexpected GroupVersion string: a/b/c/d/e",
    97  		},
    98  
    99  		"API version mismatch": {
   100  			clusterName: testClusterName,
   101  			execConfig: &clientcmdapi.ExecConfig{
   102  				APIVersion: "client.authentication.k8s.io/v1",
   103  				Command:    "echo",
   104  				Args: []string{"-n", `{
   105    "apiVersion": "client.authentication.k8s.io/v1beta1",
   106    "kind": "ExecCredential",
   107    "status": {
   108      "token": "testToken"
   109    }
   110  }`},
   111  			},
   112  			expectedError: "exec plugin is configured to use API version client.authentication.k8s.io/v1, plugin returned version client.authentication.k8s.io/v1beta1",
   113  		},
   114  
   115  		"missing status property on external command output": {
   116  			clusterName: testClusterName,
   117  			execConfig: &clientcmdapi.ExecConfig{
   118  				APIVersion: "client.authentication.k8s.io/v1",
   119  				Command:    "echo",
   120  				Args:       []string{"-n", `{"apiVersion": "client.authentication.k8s.io/v1", "kind": "ExecCredential"}`},
   121  			},
   122  			expectedError: "exec plugin didn't return a status field",
   123  		},
   124  
   125  		"missing any auth credential on status": {
   126  			clusterName: testClusterName,
   127  			execConfig: &clientcmdapi.ExecConfig{
   128  				APIVersion: "client.authentication.k8s.io/v1",
   129  				Command:    "echo",
   130  				Args:       []string{"-n", `{"apiVersion": "client.authentication.k8s.io/v1", "kind": "ExecCredential", "status": {}}`},
   131  			},
   132  			expectedError: "exec plugin didn't return a token or cert/key pair",
   133  		},
   134  
   135  		"has cert but no private key": {
   136  			clusterName: testClusterName,
   137  			execConfig: &clientcmdapi.ExecConfig{
   138  				APIVersion: "client.authentication.k8s.io/v1",
   139  				Command:    "echo",
   140  				Args:       []string{"-n", `{"apiVersion": "client.authentication.k8s.io/v1", "kind": "ExecCredential", "status": {"clientCertificateData": "certData"}}`},
   141  			},
   142  			expectedError: "exec plugin returned only certificate or key, not both",
   143  		},
   144  
   145  		"invalid exec credential item on cache": {
   146  			setup: func(t *testing.T) {
   147  				credentials.Store(testClusterName, "invalid exec credential")
   148  			},
   149  			clusterName: testClusterName,
   150  			execConfig: &clientcmdapi.ExecConfig{
   151  				APIVersion: "client.authentication.k8s.io/v1",
   152  				Command:    "should_be_ignored",
   153  			},
   154  			expectedError: "failed to convert item in cache to ExecCredential",
   155  		},
   156  
   157  		"MISS credential from cache, should issue a new credential": {
   158  			clusterName: testClusterName,
   159  			execConfig: &clientcmdapi.ExecConfig{
   160  				APIVersion: "client.authentication.k8s.io/v1",
   161  				Env: []clientcmdapi.ExecEnvVar{
   162  					{Name: "TOKEN", Value: "testToken"},
   163  				},
   164  				Command: "echo",
   165  				Args: []string{"-n", `{
   166    "apiVersion": "client.authentication.k8s.io/v1",
   167    "kind": "ExecCredential",
   168    "status": {
   169      "token": "testToken"
   170    }
   171  }`},
   172  			},
   173  			expected: &clientauthentication.ExecCredential{
   174  				TypeMeta: metav1.TypeMeta{
   175  					APIVersion: "client.authentication.k8s.io/v1",
   176  					Kind:       "ExecCredential",
   177  				},
   178  				Status: &clientauthentication.ExecCredentialStatus{
   179  					Token: "testToken",
   180  				},
   181  			},
   182  		},
   183  
   184  		"HIT credential from cache": {
   185  			setup: func(t *testing.T) {
   186  				credentials.Store(testClusterName, &clientauthentication.ExecCredential{
   187  					TypeMeta: metav1.TypeMeta{
   188  						APIVersion: "client.authentication.k8s.io/v1",
   189  						Kind:       "ExecCredential",
   190  					},
   191  					Status: &clientauthentication.ExecCredentialStatus{
   192  						ExpirationTimestamp: &metav1.Time{Time: t0.Add(time.Hour).Local().Truncate(time.Second)},
   193  						Token:               "testToken",
   194  					},
   195  				})
   196  			},
   197  			clusterName: testClusterName,
   198  			execConfig: &clientcmdapi.ExecConfig{
   199  				APIVersion: "client.authentication.k8s.io/v1",
   200  				Command:    "should_be_ignored",
   201  			},
   202  			expected: &clientauthentication.ExecCredential{
   203  				TypeMeta: metav1.TypeMeta{
   204  					APIVersion: "client.authentication.k8s.io/v1",
   205  					Kind:       "ExecCredential",
   206  				},
   207  				Status: &clientauthentication.ExecCredentialStatus{
   208  					ExpirationTimestamp: &metav1.Time{Time: t0.Add(time.Hour).Local().Truncate(time.Second)},
   209  					Token:               "testToken",
   210  				},
   211  			},
   212  		},
   213  
   214  		"expired credential on cache, should issue a new credential": {
   215  			setup: func(t *testing.T) {
   216  				credentials.Store(testClusterName, &clientauthentication.ExecCredential{
   217  					TypeMeta: metav1.TypeMeta{
   218  						APIVersion: "client.authentication.k8s.io/v1",
   219  						Kind:       "ExecCredential",
   220  					},
   221  					Status: &clientauthentication.ExecCredentialStatus{
   222  						ExpirationTimestamp: &metav1.Time{Time: t0},
   223  						Token:               "oldToken",
   224  					},
   225  				})
   226  			},
   227  			clusterName: testClusterName,
   228  			execConfig: &clientcmdapi.ExecConfig{
   229  				APIVersion: "client.authentication.k8s.io/v1",
   230  				Command:    "echo",
   231  				Args: []string{
   232  					"-n",
   233  					fmt.Sprintf(`{
   234    "apiVersion": "client.authentication.k8s.io/v1",
   235    "kind": "ExecCredential",
   236    "status": {
   237      "expirationTimestamp": %q,
   238      "token": "newToken"
   239    }
   240  }`, t0.Add(24*time.Hour).Format(time.RFC3339)),
   241  				},
   242  			},
   243  			expected: &clientauthentication.ExecCredential{
   244  				TypeMeta: metav1.TypeMeta{
   245  					APIVersion: "client.authentication.k8s.io/v1",
   246  					Kind:       "ExecCredential",
   247  				},
   248  				Status: &clientauthentication.ExecCredentialStatus{
   249  					ExpirationTimestamp: &metav1.Time{Time: t0.Add(24 * time.Hour).Local().Truncate(time.Second)},
   250  					Token:               "newToken",
   251  				},
   252  			},
   253  		},
   254  	}
   255  
   256  	for name, tt := range cases {
   257  		t.Run(name, func(t *testing.T) {
   258  			cleanAllCache(t)
   259  
   260  			if tt.setup != nil {
   261  				tt.setup(t)
   262  			}
   263  
   264  			cred, err := IssueClusterCredential(tt.clusterName, tt.execConfig)
   265  			if tt.expectedError != "" {
   266  				assert.Error(t, err)
   267  				assert.EqualError(t, err, tt.expectedError)
   268  				return
   269  			}
   270  
   271  			assert.NoError(t, err)
   272  			assert.Equal(t, tt.expected, cred)
   273  		})
   274  	}
   275  }
   276  
   277  func cleanAllCache(t *testing.T) {
   278  	t.Helper()
   279  
   280  	credentials.Range(func(key, value any) bool {
   281  		credentials.Delete(key)
   282  		return true
   283  	})
   284  }