Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 9 additions & 31 deletions test/e2e/cluster_pullsecret.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ package e2e

import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -45,7 +44,7 @@ var _ = Describe("Customer", func() {

// Tests the HyperShift HCCO global pull secret reconciliation flow:
// additional-pull-secret in kube-system -> HCCO merges into global-pull-secret -> DaemonSet syncs to nodes
// Upstream documentation: https://hypershift.pages.dev/how-to/aws/global-pull-secret/
// See https://hypershift.pages.dev/how-to/aws/global-pull-secret/
It("should be able to create an HCP cluster and manage pull secrets",
labels.RequireNothing,
labels.Critical,
Expand Down Expand Up @@ -146,12 +145,9 @@ var _ = Describe("Customer", func() {
Expect(err).NotTo(HaveOccurred(), "failed to create kubernetes client")

By("creating test pull secret")
username := "test-user"
auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + testPullSecretPassword))

testPullSecret, err := framework.CreateTestDockerConfigSecret(
testPullSecret, testRegistryAuth, err := framework.CreateTestDockerConfigSecret(
testPullSecretHost,
username,
"test-user",
testPullSecretPassword,
testPullSecretEmail,
pullSecretName,
Expand Down Expand Up @@ -180,8 +176,8 @@ var _ = Describe("Customer", func() {
"global-pull-secret",
pullSecretNamespace,
testPullSecretHost,
auth,
testPullSecretEmail,
testRegistryAuth.Auth,
testRegistryAuth.Email,
).Verify(ctx, adminRESTConfig)
Expect(err).NotTo(HaveOccurred(), "failed to verify pull secret auth data for host.example.com in global-pull-secret")

Expand All @@ -198,31 +194,13 @@ var _ = Describe("Customer", func() {
redhatRegistryAuth, ok := pullSecretConfig.Auths[redhatRegistryHost]
Expect(ok).To(BeTrue(), "registry.redhat.io credentials not found in pull-secret file")

redhatRegistryAuthString := redhatRegistryAuth.Auth
redhatRegistryEmail := redhatRegistryAuth.Email

By("updating additional-pull-secret to add registry.redhat.io credentials")
// Get the current additional-pull-secret
currentSecret, err := kubeClient.CoreV1().Secrets(pullSecretNamespace).Get(ctx, pullSecretName, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred(), "failed to get existing additional-pull-secret")

// Parse the current dockerconfigjson
var currentConfig framework.DockerConfigJSON
err = json.Unmarshal(currentSecret.Data[corev1.DockerConfigJsonKey], &currentConfig)
Expect(err).NotTo(HaveOccurred(), "failed to parse current pull secret")

// Add registry.redhat.io credentials to the existing auths
currentConfig.Auths[redhatRegistryHost] = framework.RegistryAuth{
Auth: redhatRegistryAuthString,
Email: redhatRegistryEmail,
}

// Marshal back to JSON
updatedDockerConfigJSON, err := json.Marshal(currentConfig)
Expect(err).NotTo(HaveOccurred(), "failed to marshal updated docker config JSON with registry.redhat.io credentials")
err = framework.AddRegistryAuthToSecret(currentSecret, redhatRegistryHost, redhatRegistryAuth)
Expect(err).NotTo(HaveOccurred(), "failed to add registry.redhat.io credentials to additional-pull-secret")

// Update the secret
currentSecret.Data[corev1.DockerConfigJsonKey] = updatedDockerConfigJSON
_, err = kubeClient.CoreV1().Secrets(pullSecretNamespace).Update(ctx, currentSecret, metav1.UpdateOptions{})
Expect(err).NotTo(HaveOccurred(), "failed to update additional-pull-secret with registry.redhat.io credentials")

Expand All @@ -244,8 +222,8 @@ var _ = Describe("Customer", func() {
"global-pull-secret",
pullSecretNamespace,
redhatRegistryHost,
redhatRegistryAuthString,
redhatRegistryEmail,
redhatRegistryAuth.Auth,
redhatRegistryAuth.Email,
).Verify(ctx, adminRESTConfig)
Expect(err).NotTo(HaveOccurred(), "failed to verify registry.redhat.io auth data in global-pull-secret")

Expand Down
31 changes: 0 additions & 31 deletions test/util/framework/hcp_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
Expand Down Expand Up @@ -162,36 +161,6 @@ func CreateClusterRoleBinding(ctx context.Context, subject string, adminRESTConf
return nil
}

// CreateTestDockerConfigSecret creates a Docker config secret for testing pull secret functionality
func CreateTestDockerConfigSecret(host, username, password, email, secretName, namespace string) (*corev1.Secret, error) {
auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))

dockerConfig := DockerConfigJSON{
Auths: map[string]RegistryAuth{
host: {
Email: email,
Auth: auth,
},
},
}

dockerConfigJSON, err := json.Marshal(dockerConfig)
if err != nil {
return nil, fmt.Errorf("failed to marshal docker config: %w", err)
}

return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: namespace,
},
Type: corev1.SecretTypeDockerConfigJson,
Data: map[string][]byte{
corev1.DockerConfigJsonKey: dockerConfigJSON,
},
}, nil
}

// Helper to generate SSH key pair
func GenerateSSHKeyPair() (publicKey string, privateKey string, err error) {
// Generate RSA key pair
Expand Down
107 changes: 107 additions & 0 deletions test/util/framework/pullsecret_helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright 2025 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package framework

import (
"encoding/base64"
"encoding/json"
"fmt"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// RegistryAuth represents authentication credentials for a single container
// image registry. It models one entry inside the "auths" map of a
// kubernetes.io/dockerconfigjson Secret. The Auth field is a base64 encoding
// of "username:password"; Username and Email are optional metadata.
//
// See https://kubernetes.io/docs/concepts/configuration/secret/#docker-config-secrets
type RegistryAuth struct {
Username string `json:"username,omitempty"`
Email string `json:"email,omitempty"`
Auth string `json:"auth"`
}

// DockerConfigJSON is the root structure stored under the .dockerconfigjson
// key of a kubernetes.io/dockerconfigjson Secret. Auths maps registry
// hostnames (e.g. "quay.io", "registry.redhat.io") to their credentials.
//
// See https://kubernetes.io/docs/concepts/configuration/secret/#docker-config-secrets
type DockerConfigJSON struct {
Auths map[string]RegistryAuth `json:"auths"`
}

// CreateTestDockerConfigSecret builds a corev1.Secret of type
// kubernetes.io/dockerconfigjson containing credentials for a single registry.
// It returns both the Secret and the RegistryAuth it constructed, so callers
// can pass the auth data directly to verifiers without recomputing it.
// The returned Secret is suitable for use as the HCCO "additional-pull-secret"
// in kube-system, which HCCO merges into the cluster's global pull secret.
//
// See https://hypershift.pages.dev/how-to/aws/global-pull-secret/
func CreateTestDockerConfigSecret(host, username, password, email, secretName, namespace string) (*corev1.Secret, RegistryAuth, error) {
registryAuth := RegistryAuth{
Email: email,
Auth: base64.StdEncoding.EncodeToString([]byte(username + ":" + password)),
}

dockerConfig := DockerConfigJSON{
Auths: map[string]RegistryAuth{
host: registryAuth,
},
}

dockerConfigJSON, err := json.Marshal(dockerConfig)
if err != nil {
return nil, RegistryAuth{}, fmt.Errorf("failed to marshal docker config: %w", err)
}

return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: namespace,
},
Type: corev1.SecretTypeDockerConfigJson,
Data: map[string][]byte{
corev1.DockerConfigJsonKey: dockerConfigJSON,
},
}, registryAuth, nil
}

// AddRegistryAuthToSecret adds or replaces a registry entry in an existing
// dockerconfigjson Secret. It unmarshals the Secret's current .dockerconfigjson
// data, inserts (or overwrites) the entry for host, and marshals the result
// back into the Secret's Data field. The caller is responsible for applying the
// updated Secret to the cluster (e.g. via a Kubernetes Update call).
func AddRegistryAuthToSecret(secret *corev1.Secret, host string, registryAuth RegistryAuth) error {
var config DockerConfigJSON
if err := json.Unmarshal(secret.Data[corev1.DockerConfigJsonKey], &config); err != nil {
return fmt.Errorf("failed to unmarshal pull secret: %w", err)
}

if config.Auths == nil {
config.Auths = make(map[string]RegistryAuth)
}
config.Auths[host] = registryAuth
Comment thread
mvacula02 marked this conversation as resolved.

updated, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("failed to marshal updated docker config: %w", err)
}

secret.Data[corev1.DockerConfigJsonKey] = updated
return nil
}
134 changes: 134 additions & 0 deletions test/util/framework/pullsecret_helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright 2026 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package framework

import (
"encoding/base64"
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

corev1 "k8s.io/api/core/v1"
)

func TestCreateTestDockerConfigSecret(t *testing.T) {
t.Parallel()

secret, registryAuth, err := CreateTestDockerConfigSecret(
"registry.example.com",
"user",
"pass",
"user@example.com",
"my-secret",
"my-namespace",
)
require.NoError(t, err)

assert.Equal(t, "my-secret", secret.Name)
assert.Equal(t, "my-namespace", secret.Namespace)
assert.Equal(t, corev1.SecretTypeDockerConfigJson, secret.Type)

expectedAuth := base64.StdEncoding.EncodeToString([]byte("user:pass"))
assert.Equal(t, expectedAuth, registryAuth.Auth)
assert.Equal(t, "user@example.com", registryAuth.Email)

var config DockerConfigJSON
require.NoError(t, json.Unmarshal(secret.Data[corev1.DockerConfigJsonKey], &config))

hostAuth, exists := config.Auths["registry.example.com"]
assert.True(t, exists, "expected registry.example.com in auths")
assert.Equal(t, registryAuth, hostAuth, "returned RegistryAuth must match what is in the Secret")
}

func TestAddRegistryAuthToSecret(t *testing.T) {
t.Parallel()

secret, originalAuth, err := CreateTestDockerConfigSecret(
"original.example.com",
"user1",
"pass1",
"user1@example.com",
"test-secret",
"default",
)
require.NoError(t, err)

newAuth := RegistryAuth{
Auth: base64.StdEncoding.EncodeToString([]byte("user2:pass2")),
Email: "user2@example.com",
}
err = AddRegistryAuthToSecret(secret, "new.example.com", newAuth)
require.NoError(t, err)

var config DockerConfigJSON
require.NoError(t, json.Unmarshal(secret.Data[corev1.DockerConfigJsonKey], &config))

assert.Contains(t, config.Auths, "original.example.com", "original entry must be preserved")
assert.Equal(t, originalAuth, config.Auths["original.example.com"])
assert.Contains(t, config.Auths, "new.example.com", "new entry must be present")
assert.Equal(t, newAuth, config.Auths["new.example.com"])
}

func TestAddRegistryAuthToSecret_OverwritesExisting(t *testing.T) {
t.Parallel()

secret, _, err := CreateTestDockerConfigSecret(
"registry.example.com",
"user",
"pass",
"old@example.com",
"test-secret",
"default",
)
require.NoError(t, err)

updatedAuth := RegistryAuth{
Auth: base64.StdEncoding.EncodeToString([]byte("newuser:newpass")),
Email: "new@example.com",
}
err = AddRegistryAuthToSecret(secret, "registry.example.com", updatedAuth)
require.NoError(t, err)

var config DockerConfigJSON
require.NoError(t, json.Unmarshal(secret.Data[corev1.DockerConfigJsonKey], &config))

assert.Len(t, config.Auths, 1)
assert.Equal(t, updatedAuth, config.Auths["registry.example.com"])
}

func TestAddRegistryAuthToSecret_NilAuth(t *testing.T) {
t.Parallel()

secret := &corev1.Secret{
Data: map[string][]byte{
corev1.DockerConfigJsonKey: []byte(`{}`),
},
}

newAuth := RegistryAuth{
Auth: base64.StdEncoding.EncodeToString([]byte("user:pass")),
Email: "user@example.com",
}
err := AddRegistryAuthToSecret(secret, "registry.example.com", newAuth)
require.NoError(t, err)

var config DockerConfigJSON
require.NoError(t, json.Unmarshal(secret.Data[corev1.DockerConfigJsonKey], &config))

assert.Len(t, config.Auths, 1)
assert.Equal(t, newAuth, config.Auths["registry.example.com"])
}
Loading