Merge pull request #98 from suzerain-io/get_kubeconfig_cli

Organize Pinniped CLI into subcommands; Add get-kubeconfig subcommand
This commit is contained in:
Ryan Richard
2020-09-15 13:34:14 -07:00
committed by GitHub
25 changed files with 1812 additions and 172 deletions
+91
View File
@@ -0,0 +1,91 @@
/*
Copyright 2020 VMware, Inc.
SPDX-License-Identifier: Apache-2.0
*/
package integration
import (
"context"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/suzerain-io/pinniped/test/library"
)
func TestCLI(t *testing.T) {
library.SkipUnlessIntegration(t)
library.SkipUnlessClusterHasCapability(t, library.ClusterSigningKeyIsAvailable)
token := library.GetEnv(t, "PINNIPED_TEST_USER_TOKEN")
namespaceName := library.GetEnv(t, "PINNIPED_NAMESPACE")
testUsername := library.GetEnv(t, "PINNIPED_TEST_USER_USERNAME")
expectedTestUserGroups := strings.Split(
strings.ReplaceAll(library.GetEnv(t, "PINNIPED_TEST_USER_GROUPS"), " ", ""), ",",
)
// Build pinniped CLI.
pinnipedExe, cleanupFunc := buildPinnipedCLI(t)
defer cleanupFunc()
// Run pinniped CLI to get kubeconfig.
kubeConfig := runPinnipedCLI(t, pinnipedExe, token, namespaceName)
// Create Kubernetes client with kubeconfig from pinniped CLI.
kubeClient := library.NewClientsetForKubeConfig(t, kubeConfig)
// Validate that we can auth to the API via our user.
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*3)
defer cancelFunc()
adminClient := library.NewClientset(t)
t.Run("access as user", accessAsUserTest(ctx, adminClient, testUsername, kubeClient))
for _, group := range expectedTestUserGroups {
group := group
t.Run(
"access as group "+group,
accessAsGroupTest(ctx, adminClient, group, kubeClient),
)
}
}
func buildPinnipedCLI(t *testing.T) (string, func()) {
t.Helper()
pinnipedExeDir, err := ioutil.TempDir("", "pinniped-cli-test-*")
require.NoError(t, err)
pinnipedExe := filepath.Join(pinnipedExeDir, "pinniped")
output, err := exec.Command(
"go",
"build",
"-o",
pinnipedExe,
"github.com/suzerain-io/pinniped/cmd/pinniped",
).CombinedOutput()
require.NoError(t, err, string(output))
return pinnipedExe, func() {
require.NoError(t, os.RemoveAll(pinnipedExeDir))
}
}
func runPinnipedCLI(t *testing.T, pinnipedExe, token, namespaceName string) string {
t.Helper()
output, err := exec.Command(
pinnipedExe,
"get-kubeconfig",
"--token", token,
"--pinniped-namespace", namespaceName,
).CombinedOutput()
require.NoError(t, err, string(output))
return string(output)
}
+22 -20
View File
@@ -14,6 +14,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/suzerain-io/pinniped/internal/client"
"github.com/suzerain-io/pinniped/internal/here"
"github.com/suzerain-io/pinniped/test/library"
)
@@ -28,27 +29,28 @@ Test certificate and private key that should get an authentication error. Genera
[1]: https://github.com/cloudflare/cfssl
*/
var (
testCert = strings.TrimSpace(`
-----BEGIN CERTIFICATE-----
MIICBDCCAaugAwIBAgIUeidKWlZQuoKfBGydObI1hMwzt9cwCgYIKoZIzj0EAwIw
SDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNp
c2NvMRQwEgYDVQQDEwtleGFtcGxlLm5ldDAeFw0yMDA3MjgxOTI3MDBaFw0yMTA3
MjgxOTI3MDBaMEgxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMN
U2FuIEZyYW5jaXNjbzEUMBIGA1UEAxMLZXhhbXBsZS5uZXQwWTATBgcqhkjOPQIB
BggqhkjOPQMBBwNCAARk7XBC+OjYmrXOhm7RaJiHW4Q5VsE+iMV90Bzq7ansqAhb
04RI63Y7YPwu1aExutjLvnkWCrgf2ze8KB+8djUBo3MwcTAOBgNVHQ8BAf8EBAMC
BaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAw
HQYDVR0OBBYEFG0oZxV+LHUKfE4gQ67xfHJuGQ/4MBMGA1UdEQQMMAqCCHRlc3R1
c2VyMAoGCCqGSM49BAMCA0cAMEQCIEwPZhPpYhYHndfTEsWOxnxzJkmhAcYIMCeJ
d9kyq/fPAiBNCJw1MCLT8LjNlyUZCfwI2zuI3e0w6vuau89oj2zvVA==
-----END CERTIFICATE-----
testCert = here.Doc(`
-----BEGIN CERTIFICATE-----
MIICBDCCAaugAwIBAgIUeidKWlZQuoKfBGydObI1hMwzt9cwCgYIKoZIzj0EAwIw
SDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNp
c2NvMRQwEgYDVQQDEwtleGFtcGxlLm5ldDAeFw0yMDA3MjgxOTI3MDBaFw0yMTA3
MjgxOTI3MDBaMEgxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMN
U2FuIEZyYW5jaXNjbzEUMBIGA1UEAxMLZXhhbXBsZS5uZXQwWTATBgcqhkjOPQIB
BggqhkjOPQMBBwNCAARk7XBC+OjYmrXOhm7RaJiHW4Q5VsE+iMV90Bzq7ansqAhb
04RI63Y7YPwu1aExutjLvnkWCrgf2ze8KB+8djUBo3MwcTAOBgNVHQ8BAf8EBAMC
BaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAw
HQYDVR0OBBYEFG0oZxV+LHUKfE4gQ67xfHJuGQ/4MBMGA1UdEQQMMAqCCHRlc3R1
c2VyMAoGCCqGSM49BAMCA0cAMEQCIEwPZhPpYhYHndfTEsWOxnxzJkmhAcYIMCeJ
d9kyq/fPAiBNCJw1MCLT8LjNlyUZCfwI2zuI3e0w6vuau89oj2zvVA==
-----END CERTIFICATE-----
`)
testKey = maskKey(strings.TrimSpace(`
-----BEGIN EC TESTING KEY-----
MHcCAQEEIAqkBGGKTH5GzLx8XZLAHEFW2E8jT+jpy0p6w6MMR7DkoAoGCCqGSM49
AwEHoUQDQgAEZO1wQvjo2Jq1zoZu0WiYh1uEOVbBPojFfdAc6u2p7KgIW9OESOt2
O2D8LtWhMbrYy755Fgq4H9s3vCgfvHY1AQ==
-----END EC TESTING KEY-----
testKey = maskKey(here.Doc(`
-----BEGIN EC TESTING KEY-----
MHcCAQEEIAqkBGGKTH5GzLx8XZLAHEFW2E8jT+jpy0p6w6MMR7DkoAoGCCqGSM49
AwEHoUQDQgAEZO1wQvjo2Jq1zoZu0WiYh1uEOVbBPojFfdAc6u2p7KgIW9OESOt2
O2D8LtWhMbrYy755Fgq4H9s3vCgfvHY1AQ==
-----END EC TESTING KEY-----
`))
)
+102
View File
@@ -0,0 +1,102 @@
/*
Copyright 2020 VMware, Inc.
SPDX-License-Identifier: Apache-2.0
*/
package integration
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
// accessAsUserTest runs a generic test in which a clientUnderTest operating with username
// testUsername tries to auth to the kube API (i.e., list namespaces).
//
// Use this function if you want to simply validate that a user can auth to the kube API after
// performing a Pinniped credential exchange.
func accessAsUserTest(
ctx context.Context,
adminClient kubernetes.Interface,
testUsername string,
clientUnderTest kubernetes.Interface,
) func(t *testing.T) {
return func(t *testing.T) {
addTestClusterRoleBinding(ctx, t, adminClient, &rbacv1.ClusterRoleBinding{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: "integration-test-user-readonly-role-binding",
},
Subjects: []rbacv1.Subject{{
Kind: rbacv1.UserKind,
APIGroup: rbacv1.GroupName,
Name: testUsername,
}},
RoleRef: rbacv1.RoleRef{
Kind: "ClusterRole",
APIGroup: rbacv1.GroupName,
Name: "view",
},
})
// Use the client which is authenticated as the test user to list namespaces
var listNamespaceResponse *v1.NamespaceList
var err error
var canListNamespaces = func() bool {
listNamespaceResponse, err = clientUnderTest.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
return err == nil
}
assert.Eventually(t, canListNamespaces, 3*time.Second, 250*time.Millisecond)
require.NoError(t, err) // prints out the error and stops the test in case of failure
require.NotEmpty(t, listNamespaceResponse.Items)
}
}
// accessAsGroupTest runs a generic test in which a clientUnderTest with membership in group
// testGroup tries to auth to the kube API (i.e., list namespaces).
//
// Use this function if you want to simply validate that a user can auth to the kube API (via
// a group membership) after performing a Pinniped credential exchange.
func accessAsGroupTest(
ctx context.Context,
adminClient kubernetes.Interface,
testGroup string,
clientUnderTest kubernetes.Interface,
) func(t *testing.T) {
return func(t *testing.T) {
addTestClusterRoleBinding(ctx, t, adminClient, &rbacv1.ClusterRoleBinding{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: "integration-test-group-readonly-role-binding",
},
Subjects: []rbacv1.Subject{{
Kind: rbacv1.GroupKind,
APIGroup: rbacv1.GroupName,
Name: testGroup,
}},
RoleRef: rbacv1.RoleRef{
Kind: "ClusterRole",
APIGroup: rbacv1.GroupName,
Name: "view",
},
})
// Use the client which is authenticated as the test user to list namespaces
var listNamespaceResponse *v1.NamespaceList
var err error
var canListNamespaces = func() bool {
listNamespaceResponse, err = clientUnderTest.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
return err == nil
}
assert.Eventually(t, canListNamespaces, 3*time.Second, 250*time.Millisecond)
require.NoError(t, err) // prints out the error and stops the test in case of failure
require.NotEmpty(t, listNamespaceResponse.Items)
}
}
+8 -59
View File
@@ -14,9 +14,7 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -61,65 +59,16 @@ func TestSuccessfulCredentialRequest(t *testing.T) {
response.Status.Credential.ClientKeyData,
)
t.Run("access as user", func(t *testing.T) {
addTestClusterRoleBinding(ctx, t, adminClient, &rbacv1.ClusterRoleBinding{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: "integration-test-user-readonly-role-binding",
},
Subjects: []rbacv1.Subject{{
Kind: rbacv1.UserKind,
APIGroup: rbacv1.GroupName,
Name: testUsername,
}},
RoleRef: rbacv1.RoleRef{
Kind: "ClusterRole",
APIGroup: rbacv1.GroupName,
Name: "view",
},
})
// Use the client which is authenticated as the test user to list namespaces
var listNamespaceResponse *v1.NamespaceList
var canListNamespaces = func() bool {
listNamespaceResponse, err = clientWithCertFromCredentialRequest.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
return err == nil
}
assert.Eventually(t, canListNamespaces, 3*time.Second, 250*time.Millisecond)
require.NoError(t, err) // prints out the error and stops the test in case of failure
require.NotEmpty(t, listNamespaceResponse.Items)
})
t.Run(
"access as user",
accessAsUserTest(ctx, adminClient, testUsername, clientWithCertFromCredentialRequest),
)
for _, group := range expectedTestUserGroups {
group := group
t.Run("access as group "+group, func(t *testing.T) {
addTestClusterRoleBinding(ctx, t, adminClient, &rbacv1.ClusterRoleBinding{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: "integration-test-group-readonly-role-binding",
},
Subjects: []rbacv1.Subject{{
Kind: rbacv1.GroupKind,
APIGroup: rbacv1.GroupName,
Name: group,
}},
RoleRef: rbacv1.RoleRef{
Kind: "ClusterRole",
APIGroup: rbacv1.GroupName,
Name: "view",
},
})
// Use the client which is authenticated as the test user to list namespaces
var listNamespaceResponse *v1.NamespaceList
var canListNamespaces = func() bool {
listNamespaceResponse, err = clientWithCertFromCredentialRequest.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
return err == nil
}
assert.Eventually(t, canListNamespaces, 3*time.Second, 250*time.Millisecond)
require.NoError(t, err) // prints out the error and stops the test in case of failure
require.NotEmpty(t, listNamespaceResponse.Items)
})
t.Run(
"access as group "+group,
accessAsGroupTest(ctx, adminClient, group, clientWithCertFromCredentialRequest),
)
}
}
+16
View File
@@ -36,6 +36,22 @@ func NewClientset(t *testing.T) kubernetes.Interface {
return newClientsetWithConfig(t, NewClientConfig(t))
}
func NewClientsetForKubeConfig(t *testing.T, kubeConfig string) kubernetes.Interface {
t.Helper()
kubeConfigFile, err := ioutil.TempFile("", "pinniped-cli-test-*")
require.NoError(t, err)
defer os.Remove(kubeConfigFile.Name())
_, err = kubeConfigFile.Write([]byte(kubeConfig))
require.NoError(t, err)
restConfig, err := clientcmd.BuildConfigFromFlags("", kubeConfigFile.Name())
require.NoError(t, err)
return newClientsetWithConfig(t, restConfig)
}
func NewClientsetWithCertAndKey(t *testing.T, clientCertificateData, clientKeyData string) kubernetes.Interface {
t.Helper()