Skip to content

Commit

Permalink
Add unit tests for add-nodes workflow related to auth tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
pawanpinjarkar committed Nov 19, 2024
1 parent 7eac6a1 commit 29e884d
Show file tree
Hide file tree
Showing 3 changed files with 316 additions and 63 deletions.
2 changes: 1 addition & 1 deletion pkg/agent/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func NewCluster(ctx context.Context, assetDir, rendezvousIP, kubeconfigPath, ssh
return nil, err
}
case workflow.AgentWorkflowTypeAddNodes:
watcherAuthToken, err = gencrypto.GetAuthTokenFromCluster(ctx, kubeconfigPath)
watcherAuthToken, err = gencrypto.GetWatcherAuthTokenFromCluster(ctx, kubeconfigPath)
if err != nil {
return nil, err
}
Expand Down
113 changes: 78 additions & 35 deletions pkg/asset/agent/gencrypto/authconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,14 @@ import (
"github.com/openshift/installer/pkg/asset/agent/workflow"
)

var (
const (
authTokenSecretNamespace = "openshift-config" //nolint:gosec // no sensitive info
authTokenSecretName = "agent-auth-token" //nolint:gosec // no sensitive info
authTokenSecretDataKey = "watcherAuthToken"
agentAuthKey = "agentAuthToken"
userAuthKey = "userAuthToken"
watcherAuthKey = "watcherAuthToken"
authTokenPublicDataKey = "authTokenPublicKey"
)

// AuthType holds the authenticator type for agent based installer.
const (
// AuthType holds the authenticator type for agent based installer.
AuthType = "agent-installer-local"
agentPersona = "agentAuth"
userPersona = "userAuth"
Expand All @@ -44,6 +43,7 @@ const (
// AuthConfig is an asset that generates ECDSA public/private keys, JWT token.
type AuthConfig struct {
PublicKey, AgentAuthToken, UserAuthToken, WatcherAuthToken, AuthTokenExpiry, AuthType string
Client kubernetes.Interface
}

var _ asset.Asset = (*AuthConfig)(nil)
Expand Down Expand Up @@ -210,27 +210,39 @@ func initClient(kubeconfig string) (*kubernetes.Clientset, error) {
}

func (a *AuthConfig) createOrUpdateAuthTokenSecret(kubeconfigPath string) error {
k8sclientset, err := initClient(kubeconfigPath)
if err != nil {
return err
}
// check if secret exists
retrievedSecret, err := k8sclientset.CoreV1().Secrets(authTokenSecretNamespace).Get(context.Background(), authTokenSecretName, metav1.GetOptions{})
// if the secret does not exist
if err != nil {
if errors.IsNotFound(err) {
return a.createSecret(k8sclientset)
var retrievedSecret *corev1.Secret
var k8sclientset *kubernetes.Clientset
var err error
if a.Client != nil {
// useful when fakeclient is set in testing
retrievedSecret, err = a.Client.CoreV1().Secrets(authTokenSecretNamespace).Get(context.Background(), authTokenSecretName, metav1.GetOptions{})
if err != nil {
return err
}
} else {
k8sclientset, err := initClient(kubeconfigPath)
if err != nil {
return err
}
// check if secret exists
retrievedSecret, err = k8sclientset.CoreV1().Secrets(authTokenSecretNamespace).Get(context.Background(), authTokenSecretName, metav1.GetOptions{})
// if the secret does not exist
if err != nil {
if errors.IsNotFound(err) {
return a.createSecret(k8sclientset)
}
// Other errors while trying to get the secret
return fmt.Errorf("unable to retrieve secret %s/%s: %w", authTokenSecretNamespace, authTokenSecretName, err)
}
// Other errors while trying to get the secret
return fmt.Errorf("unable to retrieve secret %s/%s: %w", authTokenSecretNamespace, authTokenSecretName, err)
}

// if the secret exists in the cluster, get the token
retrievedToken, err := extractAuthTokenFromSecret(retrievedSecret)
retrievedAgentAuthToken, retrievedUserAuthToken, retrievedWatcherAuthToken, err := extractAuthTokensFromSecret(retrievedSecret)
if err != nil {
return err
}
expiryTime, err := ParseExpirationFromToken(retrievedToken)
// All auth tokens expire at the same time so we could only check any 1 token to get the expiry time
expiryTime, err := ParseExpirationFromToken(retrievedAgentAuthToken)
if err != nil {
return err
}
Expand All @@ -245,7 +257,9 @@ func (a *AuthConfig) createOrUpdateAuthTokenSecret(kubeconfigPath string) error
}
} else {
// Update the token in asset store with the retrieved token from the cluster
a.WatcherAuthToken = retrievedToken
a.AgentAuthToken = retrievedAgentAuthToken
a.UserAuthToken = retrievedUserAuthToken
a.WatcherAuthToken = retrievedWatcherAuthToken
// get the token expiry time of the retrieved token from the cluster
a.AuthTokenExpiry = expiryTime.UTC().Format(time.RFC3339)

Expand Down Expand Up @@ -273,7 +287,9 @@ func (a *AuthConfig) createSecret(k8sclientset kubernetes.Interface) error {
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{
authTokenSecretDataKey: []byte(a.WatcherAuthToken),
agentAuthKey: []byte(a.AgentAuthToken),
userAuthKey: []byte(a.UserAuthToken),
watcherAuthKey: []byte(a.WatcherAuthToken),
authTokenPublicDataKey: []byte(a.PublicKey),
},
}
Expand All @@ -288,23 +304,32 @@ func (a *AuthConfig) createSecret(k8sclientset kubernetes.Interface) error {
}

func (a *AuthConfig) refreshAuthTokenSecret(k8sclientset kubernetes.Interface, retrievedSecret *corev1.Secret) error {
retrievedSecret.Data[authTokenSecretDataKey] = []byte(a.WatcherAuthToken)
retrievedSecret.Data[agentAuthKey] = []byte(a.AgentAuthToken)
retrievedSecret.Data[userAuthKey] = []byte(a.UserAuthToken)
retrievedSecret.Data[watcherAuthKey] = []byte(a.WatcherAuthToken)
retrievedSecret.Data[authTokenPublicDataKey] = []byte(a.PublicKey)
// only for informational purposes
retrievedSecret.Annotations["updatedAt"] = time.Now().UTC().Format(time.RFC3339)
retrievedSecret.Annotations["expiresAt"] = a.AuthTokenExpiry

_, err := k8sclientset.CoreV1().Secrets(authTokenSecretNamespace).Update(context.TODO(), retrievedSecret, metav1.UpdateOptions{})
if err != nil {
return err
if a.Client != nil {
_, err := a.Client.CoreV1().Secrets(authTokenSecretNamespace).Update(context.TODO(), retrievedSecret, metav1.UpdateOptions{})
if err != nil {
return err
}
} else {
_, err := k8sclientset.CoreV1().Secrets(authTokenSecretNamespace).Update(context.TODO(), retrievedSecret, metav1.UpdateOptions{})
if err != nil {
return err
}
}
logrus.Infof("Auth token regenerated (valid up to %s)", a.AuthTokenExpiry)
logrus.Infof("Updated secret %s/%s", authTokenSecretNamespace, authTokenSecretName)
return nil
}

// GetAuthTokenFromCluster returns a token string stored as the secret from the cluster.
func GetAuthTokenFromCluster(ctx context.Context, kubeconfigPath string) (string, error) {
// GetWatcherAuthTokenFromCluster returns a watcherAuth token string stored as the secret from the cluster.
func GetWatcherAuthTokenFromCluster(ctx context.Context, kubeconfigPath string) (string, error) {
client, err := initClient(kubeconfigPath)
if err != nil {
return "", err
Expand All @@ -314,19 +339,37 @@ func GetAuthTokenFromCluster(ctx context.Context, kubeconfigPath string) (string
if err != nil {
return "", err
}
authToken, err := extractAuthTokenFromSecret(retrievedSecret)
_, _, watcherAuthToken, err := extractAuthTokensFromSecret(retrievedSecret)
if err != nil {
return "", err
}
return authToken, err
return watcherAuthToken, err
}

func extractAuthTokenFromSecret(secret *corev1.Secret) (string, error) {
existingWatcherAuthToken, exists := secret.Data[authTokenSecretDataKey]
if !exists || len(existingWatcherAuthToken) == 0 {
return "", fmt.Errorf("auth token secret %s/%s does not contain the key %s or is empty", authTokenSecretNamespace, authTokenSecretName, authTokenSecretDataKey)
func extractAuthTokensFromSecret(secret *corev1.Secret) (string, string, string, error) {
// Check for agentAuthKey, which must exist in both old (4.17) and new versions (4.18+)
existingAgentAuthToken, agentAuthTokenExists := secret.Data[agentAuthKey]
if !agentAuthTokenExists || len(existingAgentAuthToken) == 0 {
return "", "", "", fmt.Errorf("auth token secret %s/%s does not contain the key %s or is empty", authTokenSecretNamespace, authTokenSecretName, agentAuthKey)
}

existingUserAuthToken, userAuthTokenExists := secret.Data[userAuthKey]
existingWatcherAuthToken, watcherAuthTokenExists := secret.Data[watcherAuthKey]

// Handle old version compatibility for OCP 4.17
if !userAuthTokenExists && !watcherAuthTokenExists {
// For old version OCP 4.17, where only agentAuthToken is present
return string(existingAgentAuthToken), "", "", nil
}

// Handle cases where new keys are missing in OCP 4.18+
if (!userAuthTokenExists || len(existingUserAuthToken) == 0) || (!watcherAuthTokenExists || len(existingWatcherAuthToken) == 0) {
return "", "", "", fmt.Errorf("auth token secret %s/%s is missing one or more required keys (%s, %s, or %s) or they are empty",
authTokenSecretNamespace, authTokenSecretName, agentAuthKey, userAuthKey, watcherAuthKey)
}
return string(existingWatcherAuthToken), nil

// Return all keys if present
return string(existingAgentAuthToken), string(existingUserAuthToken), string(existingWatcherAuthToken), nil
}

func extractPublicKeyFromSecret(secret *corev1.Secret) (string, error) {
Expand Down
Loading

0 comments on commit 29e884d

Please sign in to comment.