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 }