Skip to content

Commit

Permalink
flag to set ServiceAccount token expiry (#30)
Browse files Browse the repository at this point in the history
Co-authored-by: Xavier Coulon <xcoulon@redhat.com>
Co-authored-by: Francesco Ilario <filario@redhat.com>
  • Loading branch information
3 people authored Apr 17, 2024
1 parent a0b137d commit 11f8a31
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 28 deletions.
24 changes: 14 additions & 10 deletions pkg/cmd/generate/cli_configs.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type generateFlags struct {
kubeSawAdminsFile, outDir string
dev bool
kubeconfigs []string
tokenExpirationDays uint
}

func NewCliConfigsCmd() *cobra.Command {
Expand All @@ -51,6 +52,7 @@ func NewCliConfigsCmd() *cobra.Command {

configDirPath := fmt.Sprintf("%s/src/github.com/kubesaw/ksctl/out/config", os.Getenv("GOPATH"))
command.Flags().StringVarP(&f.outDir, "out-dir", "o", configDirPath, "Directory where generated ksctl.yaml files should be stored")
command.Flags().UintVarP(&f.tokenExpirationDays, "token-expiration-days", "e", 365, "Expiration time of the ServiceAccount tokens in days")

defaultKubeconfigPath := ""
if home := homedir.HomeDir(); home != "" {
Expand Down Expand Up @@ -88,10 +90,11 @@ func generate(term ioutils.Terminal, flags generateFlags, newExternalClient NewR
}

ctx := &generateContext{
Terminal: term,
newRESTClient: newExternalClient,
kubeSawAdmins: kubeSawAdmins,
kubeconfigPaths: flags.kubeconfigs,
Terminal: term,
newRESTClient: newExternalClient,
kubeSawAdmins: kubeSawAdmins,
kubeconfigPaths: flags.kubeconfigs,
tokenExpirationDays: flags.tokenExpirationDays,
}

// ksctlConfigsPerName contains all ksctlConfig objects that will be marshalled to ksctl.yaml files
Expand Down Expand Up @@ -159,9 +162,10 @@ func writeKsctlConfigs(term ioutils.Terminal, configDirPath string, ksctlConfigs

type generateContext struct {
ioutils.Terminal
newRESTClient NewRESTClientFromConfigFunc
kubeSawAdmins *assets.KubeSawAdmins
kubeconfigPaths []string
newRESTClient NewRESTClientFromConfigFunc
kubeSawAdmins *assets.KubeSawAdmins
kubeconfigPaths []string
tokenExpirationDays uint
}

// contains tokens mapped by SA name
Expand Down Expand Up @@ -196,7 +200,7 @@ func generateForCluster(ctx *generateContext, clusterType configuration.ClusterT
ctx.Printlnf("Getting token for SA '%s' in namespace '%s'", sa.Name, saNamespace)
token, err := getServiceAccountToken(externalClient, types.NamespacedName{
Namespace: saNamespace,
Name: sa.Name})
Name: sa.Name}, ctx.tokenExpirationDays)
if token == "" || err != nil {
return err
}
Expand Down Expand Up @@ -243,10 +247,10 @@ func buildClientFromKubeconfigFiles(ctx *generateContext, API string, kubeconfig
// NOTE: due to a changes in OpenShift 4.11, tokens are not listed as `secrets` in ServiceAccounts.
// The recommended solution is to use the TokenRequest API when server version >= 4.11
// (see https://docs.openshift.com/container-platform/4.11/release_notes/ocp-4-11-release-notes.html#ocp-4-11-notable-technical-changes)
func getServiceAccountToken(cl *rest.RESTClient, namespacedName types.NamespacedName) (string, error) {
func getServiceAccountToken(cl *rest.RESTClient, namespacedName types.NamespacedName, tokenExpirationDays uint) (string, error) {
tokenRequest := &authv1.TokenRequest{
Spec: authv1.TokenRequestSpec{
ExpirationSeconds: pointer.Int64(int64(365 * 24 * 60 * 60)), // token will be valid for 1 year
ExpirationSeconds: pointer.Int64(int64(tokenExpirationDays * 24 * 60 * 60)),
},
}
result := &authv1.TokenRequest{}
Expand Down
62 changes: 44 additions & 18 deletions pkg/cmd/generate/cli_configs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,24 @@ package generate
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path"
"testing"

"github.com/h2non/gock"
"github.com/kubesaw/ksctl/pkg/client"
"github.com/kubesaw/ksctl/pkg/configuration"
. "github.com/kubesaw/ksctl/pkg/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
authv1 "k8s.io/api/authentication/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/rest"

"github.com/h2non/gock"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)

func TestGenerateCliConfigs(t *testing.T) {
Expand All @@ -46,15 +47,15 @@ func TestGenerateCliConfigs(t *testing.T) {
setupGockForListServiceAccounts(t, Member1ServerAPI, configuration.Member)
setupGockForListServiceAccounts(t, Member2ServerAPI, configuration.Member)

setupGockForServiceAccounts(t, HostServerAPI,
setupGockForServiceAccounts(t, HostServerAPI, 50,
newServiceAccount("sandbox-sre-host", "john"),
newServiceAccount("sandbox-sre-host", "bob"),
)
setupGockForServiceAccounts(t, Member1ServerAPI,
setupGockForServiceAccounts(t, Member1ServerAPI, 50,
newServiceAccount("sandbox-sre-member", "john"),
newServiceAccount("sandbox-sre-member", "bob"),
)
setupGockForServiceAccounts(t, Member2ServerAPI,
setupGockForServiceAccounts(t, Member2ServerAPI, 50,
newServiceAccount("sandbox-sre-member", "john"),
newServiceAccount("sandbox-sre-member", "bob"),
)
Expand All @@ -73,7 +74,7 @@ func TestGenerateCliConfigs(t *testing.T) {
// given
tempDir, err := os.MkdirTemp("", "sandbox-sre-out-")
require.NoError(t, err)
flags := generateFlags{kubeconfigs: kubeconfigFiles, kubeSawAdminsFile: configFile, outDir: tempDir}
flags := generateFlags{kubeconfigs: kubeconfigFiles, kubeSawAdminsFile: configFile, outDir: tempDir, tokenExpirationDays: 50}

// when
err = generate(term, flags, newExternalClient)
Expand Down Expand Up @@ -101,7 +102,7 @@ func TestGenerateCliConfigs(t *testing.T) {
configFile := createKubeSawAdminsFile(t, "kubesaw.host.openshiftapps.com", kubeSawAdminsContent)
tempDir, err := os.MkdirTemp("", "sandbox-sre-out-")
require.NoError(t, err)
flags := generateFlags{kubeconfigs: kubeconfigFiles, kubeSawAdminsFile: configFile, outDir: tempDir}
flags := generateFlags{kubeconfigs: kubeconfigFiles, kubeSawAdminsFile: configFile, outDir: tempDir, tokenExpirationDays: 50}

// when
err = generate(term, flags, newExternalClient)
Expand All @@ -115,14 +116,14 @@ func TestGenerateCliConfigs(t *testing.T) {
t.Run("in dev mode", func(t *testing.T) {
// given
setupGockForListServiceAccounts(t, HostServerAPI, configuration.Member)
setupGockForServiceAccounts(t, HostServerAPI,
setupGockForServiceAccounts(t, HostServerAPI, 50,
newServiceAccount("sandbox-sre-member", "john"),
newServiceAccount("sandbox-sre-member", "bob"),
)
tempDir, err := os.MkdirTemp("", "sandbox-sre-out-")
require.NoError(t, err)
kubeconfigFiles := createKubeconfigFiles(t, ksctlKubeconfigContent)
flags := generateFlags{kubeconfigs: kubeconfigFiles, kubeSawAdminsFile: configFile, outDir: tempDir, dev: true}
flags := generateFlags{kubeconfigs: kubeconfigFiles, kubeSawAdminsFile: configFile, outDir: tempDir, dev: true, tokenExpirationDays: 50}

// when
err = generate(term, flags, newExternalClient)
Expand Down Expand Up @@ -157,10 +158,11 @@ func TestGenerateCliConfigs(t *testing.T) {
path := fmt.Sprintf("api/v1/namespaces/%s/serviceaccounts/", sandboxSRENamespace(configuration.Host))
gock.New("https://dummy.openshift.com").Get(path).Persist().Reply(403)
ctx := &generateContext{
Terminal: term,
newRESTClient: newExternalClient,
kubeSawAdmins: kubeSawAdmins,
kubeconfigPaths: kubeconfigFiles,
Terminal: term,
newRESTClient: newExternalClient,
kubeSawAdmins: kubeSawAdmins,
kubeconfigPaths: kubeconfigFiles,
tokenExpirationDays: 365,
}

// when
Expand Down Expand Up @@ -228,7 +230,7 @@ func TestGetServiceAccountToken(t *testing.T) {
// given
require.NoError(t, client.AddToScheme())

setupGockForServiceAccounts(t, "https://api.example.com", newServiceAccount("openshift-customer-monitoring", "loki"))
setupGockForServiceAccounts(t, "https://api.example.com", 365, newServiceAccount("openshift-customer-monitoring", "loki"))
t.Cleanup(gock.OffAll)
cl, err := client.NewRESTClient("secret_token", "https://api.example.com")
cl.Client.Transport = gock.DefaultTransport // make sure that the underlying client's request are intercepted by Gock
Expand All @@ -238,7 +240,7 @@ func TestGetServiceAccountToken(t *testing.T) {
actualToken, err := getServiceAccountToken(cl, types.NamespacedName{
Namespace: "openshift-customer-monitoring",
Name: "loki",
})
}, 365)

// then
require.NoError(t, err)
Expand Down Expand Up @@ -343,7 +345,7 @@ func setupGockForListServiceAccounts(t *testing.T, apiEndpoint string, clusterTy
BodyString(string(resultServiceAccountsStr))
}

func setupGockForServiceAccounts(t *testing.T, apiEndpoint string, sas ...*corev1.ServiceAccount) {
func setupGockForServiceAccounts(t *testing.T, apiEndpoint string, tokenExpirationDays int, sas ...*corev1.ServiceAccount) {
for _, sa := range sas {
expectedToken := "token-secret-for-" + sa.Name
resultTokenRequest := &authv1.TokenRequest{
Expand All @@ -357,6 +359,30 @@ func setupGockForServiceAccounts(t *testing.T, apiEndpoint string, sas ...*corev
t.Logf("mocking access to POST %s/%s", apiEndpoint, path)
gock.New(apiEndpoint).
Post(path).
AddMatcher(func(request *http.Request, _ *gock.Request) (bool, error) {
requestBody, err := io.ReadAll(request.Body)
if err != nil {
return false, err
}
if err := request.Body.Close(); err != nil {
return false, err
}
tokenRequest := &authv1.TokenRequest{}
if err := json.Unmarshal(requestBody, tokenRequest); err != nil {
return false, err
}
fmt.Println(tokenRequest)
expectedExpiry := int64(tokenExpirationDays * 24 * 60 * 60)
if tokenRequest.Spec.ExpirationSeconds == nil {
assert.NotEmpty(t, tokenRequest.Spec.ExpirationSeconds)
return false, nil
}
if *tokenRequest.Spec.ExpirationSeconds != expectedExpiry {
assert.Equal(t, expectedExpiry, *tokenRequest.Spec.ExpirationSeconds)
return false, nil
}
return true, nil
}).
Persist().
Reply(200).
BodyString(string(resultTokenRequestStr))
Expand Down

0 comments on commit 11f8a31

Please sign in to comment.