Skip to content
Open
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
24 changes: 23 additions & 1 deletion pkg/controller/bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ func (b *Bootstrap) Run(destDir string) error {
imageStream *imagev1.ImageStream
iri *mcfgv1alpha1.InternalReleaseImage
iriTLSCert *corev1.Secret
iriAuthSecret *corev1.Secret
)
for _, info := range infos {
if info.IsDir() {
Expand Down Expand Up @@ -199,6 +200,9 @@ func (b *Bootstrap) Run(destDir string) error {
if obj.GetName() == ctrlcommon.InternalReleaseImageTLSSecretName {
iriTLSCert = obj
}
if obj.GetName() == ctrlcommon.InternalReleaseImageAuthSecretName {
iriAuthSecret = obj
}
default:
klog.Infof("skipping %q [%d] manifest because of unhandled %T", file.Name(), idx+1, obji)
}
Expand Down Expand Up @@ -243,6 +247,24 @@ func (b *Bootstrap) Run(destDir string) error {
}

pullSecretBytes := pullSecret.Data[corev1.DockerConfigJsonKey]

// If IRI auth is enabled, merge the IRI registry credentials into the pull
// secret before rendering template MCs. This ensures the bootstrap-rendered
// MCs use the same pull secret content as the in-cluster IRI controller
// will produce, avoiding a rendered MachineConfig hash mismatch.
if fgHandler != nil && fgHandler.Enabled(features.FeatureGateNoRegistryClusterInstall) {
if iriAuthSecret != nil && cconfig.Spec.DNS != nil {
password := string(iriAuthSecret.Data["password"])
merged, mergeErr := internalreleaseimage.MergeIRIAuthIntoPullSecret(pullSecretBytes, password, cconfig.Spec.DNS.Spec.BaseDomain)
if mergeErr != nil {
klog.Warningf("Failed to merge IRI auth into pull secret during bootstrap: %v", mergeErr)
} else {
pullSecretBytes = merged
klog.Infof("Merged IRI registry auth into pull secret for bootstrap rendering")
}
}
}

iconfigs, err := template.RunBootstrap(b.templatesDir, cconfig, pullSecretBytes, apiServer)
if err != nil {
return err
Expand Down Expand Up @@ -305,7 +327,7 @@ func (b *Bootstrap) Run(destDir string) error {

if fgHandler != nil && fgHandler.Enabled(features.FeatureGateNoRegistryClusterInstall) {
if iri != nil {
iriConfigs, err := internalreleaseimage.RunInternalReleaseImageBootstrap(iri, iriTLSCert, cconfig)
iriConfigs, err := internalreleaseimage.RunInternalReleaseImageBootstrap(iri, iriTLSCert, iriAuthSecret, cconfig)
if err != nil {
return err
}
Expand Down
3 changes: 3 additions & 0 deletions pkg/controller/common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ const (
// InternalReleaseImageTLSSecretName is the name of the secret manifest containing the InternalReleaseImage TLS certificate.
InternalReleaseImageTLSSecretName = "internal-release-image-tls"

// InternalReleaseImageAuthSecretName is the name of the secret containing IRI registry htpasswd auth credentials.
InternalReleaseImageAuthSecretName = "internal-release-image-registry-auth"

// APIServerInstanceName is a singleton name for APIServer configuration
APIServerBootstrapFileLocation = "/etc/mcs/bootstrap/api-server/api-server.yaml"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import (
)

// RunInternalReleaseImageBootstrap generates the MachineConfig objects for InternalReleaseImage that would have been generated by syncInternalReleaseImage.
func RunInternalReleaseImageBootstrap(iri *mcfgv1alpha1.InternalReleaseImage, iriSecret *corev1.Secret, cconfig *mcfgv1.ControllerConfig) ([]*mcfgv1.MachineConfig, error) {
func RunInternalReleaseImageBootstrap(iri *mcfgv1alpha1.InternalReleaseImage, iriSecret *corev1.Secret, iriAuthSecret *corev1.Secret, cconfig *mcfgv1.ControllerConfig) ([]*mcfgv1.MachineConfig, error) {
configs := []*mcfgv1.MachineConfig{}

for _, role := range SupportedRoles {
r := NewRendererByRole(role, iri, iriSecret, cconfig)
r := NewRendererByRole(role, iri, iriSecret, iriAuthSecret, cconfig)
mc, err := r.CreateEmptyMachineConfig()
if err != nil {
return nil, err
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,15 @@ import (
)

func TestRunInternalReleaseImageBootstrap(t *testing.T) {
configs, err := RunInternalReleaseImageBootstrap(&mcfgv1alpha1.InternalReleaseImage{}, iriCertSecret().obj, cconfig().obj)
configs, err := RunInternalReleaseImageBootstrap(&mcfgv1alpha1.InternalReleaseImage{}, iriCertSecret().obj, nil, cconfig().obj)
assert.NoError(t, err)
verifyAllInternalReleaseImageMachineConfigs(t, configs)
}

func TestRunInternalReleaseImageBootstrapWithAuth(t *testing.T) {
configs, err := RunInternalReleaseImageBootstrap(&mcfgv1alpha1.InternalReleaseImage{}, iriCertSecret().obj, iriAuthSecret().obj, cconfig().obj)
assert.NoError(t, err)
assert.Len(t, configs, 2)
verifyInternalReleaseMasterMachineConfigWithAuth(t, configs[0])
verifyInternalReleaseWorkerMachineConfig(t, configs[1])
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ var (
// Controller defines the InternalReleaseImage controller.
type Controller struct {
client mcfgclientset.Interface
kubeClient clientset.Interface
eventRecorder record.EventRecorder

syncHandler func(mcp string) error
Expand Down Expand Up @@ -93,6 +94,7 @@ func New(

ctrl := &Controller{
client: mcfgClient,
kubeClient: kubeClient,
eventRecorder: ctrlcommon.NamespacedEventRecorder(eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "machineconfigcontroller-internalreleaseimagecontroller"})),
queue: workqueue.NewTypedRateLimitingQueueWithConfig(
workqueue.DefaultTypedControllerRateLimiter[string](),
Expand Down Expand Up @@ -278,7 +280,8 @@ func (ctrl *Controller) updateSecret(obj, _ interface{}) {
secret := obj.(*corev1.Secret)

// Skip any event not related to the InternalReleaseImage secrets
if secret.Name != ctrlcommon.InternalReleaseImageTLSSecretName {
if secret.Name != ctrlcommon.InternalReleaseImageTLSSecretName &&
secret.Name != ctrlcommon.InternalReleaseImageAuthSecretName {
return
}

Expand Down Expand Up @@ -341,8 +344,11 @@ func (ctrl *Controller) syncInternalReleaseImage(key string) error {
return fmt.Errorf("could not get Secret %s: %w", ctrlcommon.InternalReleaseImageTLSSecretName, err)
}

// Auth secret may not exist during upgrades from non-auth clusters
iriAuthSecret, _ := ctrl.secretLister.Secrets(ctrlcommon.MCONamespace).Get(ctrlcommon.InternalReleaseImageAuthSecretName)

for _, role := range SupportedRoles {
r := NewRendererByRole(role, iri, iriSecret, cconfig)
r := NewRendererByRole(role, iri, iriSecret, iriAuthSecret, cconfig)

mc, err := ctrl.mcLister.Get(r.GetMachineConfigName())
isNotFound := errors.IsNotFound(err)
Expand All @@ -369,6 +375,13 @@ func (ctrl *Controller) syncInternalReleaseImage(key string) error {
}
}

// Merge IRI auth credentials into the global pull secret
if iriAuthSecret != nil {
if err := ctrl.mergeIRIAuthIntoPullSecret(cconfig, iriAuthSecret); err != nil {
klog.Warningf("Failed to merge IRI auth into pull secret: %v", err)
}
}

// Initialize status if empty
if err := ctrl.initializeInternalReleaseImageStatus(iri); err != nil {
return err
Expand Down Expand Up @@ -449,6 +462,43 @@ func (ctrl *Controller) addFinalizerToInternalReleaseImage(iri *mcfgv1alpha1.Int
return err
}

func (ctrl *Controller) mergeIRIAuthIntoPullSecret(cconfig *mcfgv1.ControllerConfig, authSecret *corev1.Secret) error {
password := string(authSecret.Data["password"])
if password == "" {
return nil
}

if cconfig.Spec.DNS == nil {
return fmt.Errorf("ControllerConfig DNS is not set")
}
baseDomain := cconfig.Spec.DNS.Spec.BaseDomain

// Fetch current pull secret from openshift-config
pullSecret, err := ctrl.kubeClient.CoreV1().Secrets(ctrlcommon.OpenshiftConfigNamespace).Get(
context.TODO(), ctrlcommon.GlobalPullSecretName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("could not get pull-secret: %w", err)
}

mergedBytes, err := MergeIRIAuthIntoPullSecret(pullSecret.Data[corev1.DockerConfigJsonKey], password, baseDomain)
if err != nil {
return err
}

// No change needed
if string(mergedBytes) == string(pullSecret.Data[corev1.DockerConfigJsonKey]) {
return nil
}

pullSecret.Data[corev1.DockerConfigJsonKey] = mergedBytes
_, err = ctrl.kubeClient.CoreV1().Secrets(ctrlcommon.OpenshiftConfigNamespace).Update(
context.TODO(), pullSecret, metav1.UpdateOptions{})
if err == nil {
klog.Infof("Updated pull secret with IRI registry auth credentials from secret %s/%s (uid=%s, resourceVersion=%s)", authSecret.Namespace, authSecret.Name, authSecret.UID, authSecret.ResourceVersion)
}
return err
}

func (ctrl *Controller) cascadeDelete(iri *mcfgv1alpha1.InternalReleaseImage) error {
mcName := iri.GetFinalizers()[0]
err := ctrl.client.MachineconfigurationV1().MachineConfigs().Delete(context.TODO(), mcName, metav1.DeleteOptions{})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package internalreleaseimage

import (
"context"
"encoding/json"
"testing"
"time"

Expand All @@ -28,9 +29,10 @@ import (

func TestInternalReleaseImageCreate(t *testing.T) {
cases := []struct {
name string
initialObjects func() []runtime.Object
verify func(t *testing.T, actualIRI *mcfgv1alpha1.InternalReleaseImage, actualMasterMC *mcfgv1.MachineConfig, actualWorkerMC *mcfgv1.MachineConfig)
name string
initialObjects func() []runtime.Object
verify func(t *testing.T, actualIRI *mcfgv1alpha1.InternalReleaseImage, actualMasterMC *mcfgv1.MachineConfig, actualWorkerMC *mcfgv1.MachineConfig)
verifyPullSecret func(t *testing.T, f *fixture)
}{
{
name: "feature inactive",
Expand Down Expand Up @@ -72,6 +74,34 @@ func TestInternalReleaseImageCreate(t *testing.T) {
verifyInternalReleaseWorkerMachineConfig(t, actualWorkerMC)
},
},
{
name: "generate iri machine-config with auth",
initialObjects: objs(iri(), clusterVersion(), cconfig(), iriCertSecret(), iriAuthSecret()),
verify: func(t *testing.T, actualIRI *mcfgv1alpha1.InternalReleaseImage, actualMasterMC *mcfgv1.MachineConfig, actualWorkerMC *mcfgv1.MachineConfig) {
verifyInternalReleaseMasterMachineConfigWithAuth(t, actualMasterMC)
verifyInternalReleaseWorkerMachineConfig(t, actualWorkerMC)
},
},
{
name: "merge iri auth into pull secret",
initialObjects: objs(iri(), clusterVersion(), cconfig().withDNS("example.com"), iriCertSecret(), iriAuthSecret(), pullSecret()),
verify: func(t *testing.T, actualIRI *mcfgv1alpha1.InternalReleaseImage, actualMasterMC *mcfgv1.MachineConfig, actualWorkerMC *mcfgv1.MachineConfig) {
verifyInternalReleaseMasterMachineConfigWithAuth(t, actualMasterMC)
verifyInternalReleaseWorkerMachineConfig(t, actualWorkerMC)
},
verifyPullSecret: func(t *testing.T, f *fixture) {
ps, err := f.k8sClient.CoreV1().Secrets(ctrlcommon.OpenshiftConfigNamespace).Get(
context.TODO(), ctrlcommon.GlobalPullSecretName, metav1.GetOptions{})
assert.NoError(t, err)
var dockerConfig map[string]interface{}
err = json.Unmarshal(ps.Data[corev1.DockerConfigJsonKey], &dockerConfig)
assert.NoError(t, err)
auths := dockerConfig["auths"].(map[string]interface{})
iriEntry, ok := auths["api-int.example.com:22625"]
assert.True(t, ok, "IRI auth entry should be present in pull secret")
assert.NotNil(t, iriEntry)
},
},
{
name: "avoid machine-config drifting",
initialObjects: objs(
Expand Down Expand Up @@ -155,6 +185,9 @@ func TestInternalReleaseImageCreate(t *testing.T) {
}
tc.verify(t, actualIRI, actualMasterMC, actualWorkerMC)
}
if tc.verifyPullSecret != nil {
tc.verifyPullSecret(t, f)
}

})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,32 @@ func verifyInternalReleaseMasterMachineConfig(t *testing.T, mc *mcfgv1.MachineCo
assert.Len(t, ignCfg.Systemd.Units, 1)
assert.Contains(t, *ignCfg.Systemd.Units[0].Contents, "docker-registry-image-pullspec")

assert.Len(t, ignCfg.Storage.Files, 4, "Found an unexpected file")
assert.Len(t, ignCfg.Storage.Files, 5, "Found an unexpected file")
verifyIgnitionFile(t, &ignCfg, "/etc/pki/ca-trust/source/anchors/iri-root-ca.crt", "iri-root-ca-data")
verifyIgnitionFile(t, &ignCfg, "/etc/iri-registry/certs/tls.key", "iri-tls-key")
verifyIgnitionFile(t, &ignCfg, "/etc/iri-registry/certs/tls.crt", "iri-tls-crt")
verifyIgnitionFile(t, &ignCfg, "/etc/iri-registry/auth/htpasswd", "")
verifyIgnitionFileContains(t, &ignCfg, "/usr/local/bin/load-registry-image.sh", "docker-registry-image-pullspec")
}

func verifyInternalReleaseMasterMachineConfigWithAuth(t *testing.T, mc *mcfgv1.MachineConfig) {
assert.Equal(t, masterName(), mc.Name)
assert.Equal(t, ctrlcommon.MachineConfigPoolMaster, mc.Labels[mcfgv1.MachineConfigRoleLabelKey])
assert.Equal(t, controllerKind.Kind, mc.OwnerReferences[0].Kind)

ignCfg, err := ctrlcommon.ParseAndConvertConfig(mc.Spec.Config.Raw)
assert.NoError(t, err, mc.Name)

assert.Len(t, ignCfg.Systemd.Units, 1)
assert.Contains(t, *ignCfg.Systemd.Units[0].Contents, "docker-registry-image-pullspec")
assert.Contains(t, *ignCfg.Systemd.Units[0].Contents, "REGISTRY_AUTH_HTPASSWD_REALM")
assert.Contains(t, *ignCfg.Systemd.Units[0].Contents, "REGISTRY_AUTH_HTPASSWD_PATH")

assert.Len(t, ignCfg.Storage.Files, 5, "Found an unexpected file")
verifyIgnitionFile(t, &ignCfg, "/etc/pki/ca-trust/source/anchors/iri-root-ca.crt", "iri-root-ca-data")
verifyIgnitionFile(t, &ignCfg, "/etc/iri-registry/certs/tls.key", "iri-tls-key")
verifyIgnitionFile(t, &ignCfg, "/etc/iri-registry/certs/tls.crt", "iri-tls-crt")
verifyIgnitionFile(t, &ignCfg, "/etc/iri-registry/auth/htpasswd", "openshift:$2y$05$testhash")
verifyIgnitionFileContains(t, &ignCfg, "/usr/local/bin/load-registry-image.sh", "docker-registry-image-pullspec")
}

Expand Down Expand Up @@ -140,6 +162,15 @@ func cconfig() *controllerConfigBuilder {
}
}

func (ccb *controllerConfigBuilder) withDNS(baseDomain string) *controllerConfigBuilder {
ccb.obj.Spec.DNS = &configv1.DNS{
Spec: configv1.DNSSpec{
BaseDomain: baseDomain,
},
}
return ccb
}

func (ccb *controllerConfigBuilder) dockerRegistryImage(image string) *controllerConfigBuilder {
ccb.obj.Spec.Images[templatectrl.DockerRegistryKey] = image
return ccb
Expand Down Expand Up @@ -224,6 +255,35 @@ func (sb *secretBuilder) build() runtime.Object {
return sb.obj
}

func pullSecret() *secretBuilder {
return &secretBuilder{
obj: &corev1.Secret{
ObjectMeta: v1.ObjectMeta{
Namespace: ctrlcommon.OpenshiftConfigNamespace,
Name: ctrlcommon.GlobalPullSecretName,
},
Data: map[string][]byte{
corev1.DockerConfigJsonKey: []byte(`{"auths":{"quay.io":{"auth":"dGVzdDp0ZXN0"}}}`),
},
},
}
}

func iriAuthSecret() *secretBuilder {
return &secretBuilder{
obj: &corev1.Secret{
ObjectMeta: v1.ObjectMeta{
Namespace: ctrlcommon.MCONamespace,
Name: ctrlcommon.InternalReleaseImageAuthSecretName,
},
Data: map[string][]byte{
"htpasswd": []byte("openshift:$2y$05$testhash"),
"password": []byte("testpassword"),
},
},
}
}

// clusterVersionBuilder simplifies the creation of a Secret resource in the test.
type clusterVersionBuilder struct {
obj *configv1.ClusterVersion
Expand Down
Loading