Skip to content

Commit

Permalink
refactor: covering dynamic credentials w/ unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
nettoclaudio committed Feb 16, 2024
1 parent 736657a commit 17a03b0
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 6 deletions.
2 changes: 1 addition & 1 deletion pkg/apis/cluster/v1alpha1/clustergateway_types_secret.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ func buildCredentialFromExecConfig(secret *v1.Secret, ep *ClusterEndpoint) (*Clu

var ec clientcmdapi.ExecConfig
if err := json.Unmarshal(execConfigRaw, &ec); err != nil {
return nil, fmt.Errorf("failed to decode exec config from secret data: %v", err)
return nil, fmt.Errorf("failed to decode exec config JSON from secret data: %v", err)
}

cluster := &clientauthentication.Cluster{
Expand Down
177 changes: 177 additions & 0 deletions pkg/apis/cluster/v1alpha1/clustergateway_types_secret_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -617,3 +617,180 @@ func TestListHybridClusterGateway(t *testing.T) {
}
assert.Equal(t, expectedNames, actualNames)
}

func TestBuildCredentialFromExecConfig(t *testing.T) {
cases := []struct {
name string
secret func(s *corev1.Secret) *corev1.Secret
cluster func(ce *ClusterEndpoint) *ClusterEndpoint
expectedError string
expected *ClusterAccessCredential
}{
{
name: "missing exec config",
expectedError: "missing secret data key: exec",
},

{
name: "invalid exec config format",
secret: func(s *corev1.Secret) *corev1.Secret {
s.Data["exec"] = []byte("some invalid exec config")
return s
},
expectedError: "failed to decode exec config JSON from secret data: invalid character 's' looking for beginning of value",
},

{
name: "missing command property within exec config",
secret: func(s *corev1.Secret) *corev1.Secret {
s.Data["exec"] = []byte(`{}`)
return s
},
expectedError: "missing command key in ExecConfig object",
},

{
name: "failed to run external command: command not found",
secret: func(s *corev1.Secret) *corev1.Secret {
s.Data["exec"] = []byte(`{"command": "/path/to/command/not/found"}`)
return s
},
expectedError: "exec: executable /path/to/command/not/found not found",
},

{
name: "failed to run external command: finished with non-zero exit code",
secret: func(s *corev1.Secret) *corev1.Secret {
s.Data["exec"] = []byte(`{"command": "false"}`)
return s
},
expectedError: "exec: executable /usr/bin/false failed with exit code 1",
},

{
name: "missing API version in exec config",
secret: func(s *corev1.Secret) *corev1.Secret {
s.Data["exec"] = []byte(`{"command": "true"}`)
return s
},
expectedError: "exec plugin: invalid apiVersion \"\"",
},

{
name: "invalid API version in exec config",
secret: func(s *corev1.Secret) *corev1.Secret {
s.Data["exec"] = []byte(`{"apiVersion": "example.org/v1", "command": "true"}`)
return s
},
expectedError: "exec plugin: invalid apiVersion \"example.org/v1\"",
},

{
name: "invalid external command output",
secret: func(s *corev1.Secret) *corev1.Secret {
s.Data["exec"] = []byte(`{"apiVersion": "client.authentication.k8s.io/v1", "command": "echo", "args": ["invalid output"]}`)
return s
},
expectedError: "decoding stdout: couldn't get version/kind; json parse error: json: cannot unmarshal string into Go value of type struct { APIVersion string \"json:\\\"apiVersion,omitempty\\\"\"; Kind string \"json:\\\"kind,omitempty\\\"\" }",
},

{
name: "API version mismatch between exec config and command output",
secret: func(s *corev1.Secret) *corev1.Secret {
s.Data["exec"] = []byte(`{"apiVersion": "client.authentication.k8s.io/v1", "command": "echo", "args": ["{\"apiVersion\": \"client.authentication.k8s.io/v1beta1\"}"]}`)
return s
},
expectedError: "exec plugin is configured to use API version client.authentication.k8s.io/v1, plugin returned version client.authentication.k8s.io/v1beta1",
},

{
name: "missing status property on external command output",
secret: func(s *corev1.Secret) *corev1.Secret {
s.Data["exec"] = []byte(`{"apiVersion": "client.authentication.k8s.io/v1", "command": "echo", "args": ["{\"apiVersion\": \"client.authentication.k8s.io/v1\"}"]}`)
return s
},
expectedError: "exec plugin didn't return a status field",
},

{
name: "missing any auth credential on status",
secret: func(s *corev1.Secret) *corev1.Secret {
s.Data["exec"] = []byte(`{"apiVersion": "client.authentication.k8s.io/v1", "command": "echo", "args": ["{\"apiVersion\": \"client.authentication.k8s.io/v1\", \"status\": {}}"]}`)
return s
},
expectedError: "exec plugin didn't return a token or cert/key pair",
},

{
name: "has cert but no private key",
secret: func(s *corev1.Secret) *corev1.Secret {
s.Data["exec"] = []byte(`{"apiVersion": "client.authentication.k8s.io/v1", "command": "echo", "args": ["{\"apiVersion\": \"client.authentication.k8s.io/v1\", \"status\": {\"clientCertificateData\": \"certData\"}}"]}`)
return s
},
expectedError: "exec plugin returned only certificate or key, not both",
},

{
name: "returns successfully a service account token",
secret: func(s *corev1.Secret) *corev1.Secret {
s.Data["exec"] = []byte(`{"apiVersion": "client.authentication.k8s.io/v1", "command": "echo", "args": ["{\"apiVersion\": \"client.authentication.k8s.io/v1\", \"status\": {\"token\": \"token\"}}"]}`)
return s
},
expected: &ClusterAccessCredential{
Type: CredentialTypeServiceAccountToken,
ServiceAccountToken: testToken,
},
},

{
name: "returns successfully a X509 client certificate",
secret: func(s *corev1.Secret) *corev1.Secret {
s.Data["exec"] = []byte(`{"apiVersion": "client.authentication.k8s.io/v1", "command": "echo", "args": ["{\"apiVersion\": \"client.authentication.k8s.io/v1\", \"status\": {\"clientCertificateData\": \"certData\", \"clientKeyData\": \"keyData\"}}"]}`)
return s
},
expected: &ClusterAccessCredential{
Type: CredentialTypeX509Certificate,
X509: &X509{
Certificate: []byte(testCertData),
PrivateKey: []byte(testKeyData),
},
},
},
}

for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: testName,
Namespace: testNamespace,
},
Data: map[string][]byte{},
}
if tt.secret != nil {
secret = tt.secret(secret)
}

cluster := &ClusterEndpoint{
Type: ClusterEndpointTypeConst,
Const: &ClusterEndpointConst{
Address: testEndpoint,
CABundle: []byte(testCAData),
},
}
if tt.cluster != nil {
cluster = tt.cluster(cluster)
}

got, err := buildCredentialFromExecConfig(secret, cluster)
if tt.expectedError != "" {
assert.Error(t, err)
assert.EqualError(t, err, tt.expectedError)
return
}

assert.NoError(t, err)
assert.Equal(t, tt.expected, got)
})
}
}
20 changes: 15 additions & 5 deletions pkg/util/exec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package exec

import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
Expand Down Expand Up @@ -34,7 +35,16 @@ func init() {
}

func GetToken(ec *clientcmdapi.ExecConfig, cluster *clientauthentication.Cluster) (*clientauthentication.ExecCredential, error) {
cmd := exec.Command(ec.Command, ec.Args...)
if ec.Command == "" {
return nil, errors.New("missing command key in ExecConfig object")
}

command, err := exec.LookPath(ec.Command)
if err != nil {
return nil, unwrapExecCommandError(ec.Command, err)
}

cmd := exec.Command(command, ec.Args...)
cmd.Env = os.Environ()

for _, env := range ec.Env {
Expand All @@ -46,7 +56,7 @@ func GetToken(ec *clientcmdapi.ExecConfig, cluster *clientauthentication.Cluster
cmd.Stdout = &stdout

if err := cmd.Run(); err != nil {
return nil, wrapCmdRunErrorLocked(cmd, err)
return nil, unwrapExecCommandError(command, err)
}

ecgv, err := schema.ParseGroupVersion(ec.APIVersion)
Expand Down Expand Up @@ -94,14 +104,14 @@ func GetToken(ec *clientcmdapi.ExecConfig, cluster *clientauthentication.Cluster
return cred, nil
}

func wrapCmdRunErrorLocked(cmd *exec.Cmd, err error) error {
func unwrapExecCommandError(path string, err error) error {
switch err.(type) {
case *exec.Error: // Binary does not exist (see exec.Error).
return fmt.Errorf("exec: executable %s not found", cmd.Path)
return fmt.Errorf("exec: executable %s not found", path)

case *exec.ExitError: // Binary execution failed (see exec.Cmd.Run()).
e := err.(*exec.ExitError)
return fmt.Errorf("exec: executable %s failed with exit code %d", cmd.Path, e.ProcessState.ExitCode())
return fmt.Errorf("exec: executable %s failed with exit code %d", path, e.ProcessState.ExitCode())

default:
return fmt.Errorf("exec: %v", err)
Expand Down

0 comments on commit 17a03b0

Please sign in to comment.