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
15 changes: 15 additions & 0 deletions cmd/machine-config-controller/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"

features "github.com/openshift/api/features"
mcfginformersv1alpha1 "github.com/openshift/client-go/machineconfiguration/informers/externalversions/machineconfiguration/v1alpha1"
"github.com/openshift/machine-config-operator/cmd/common"
"github.com/openshift/machine-config-operator/internal/clients"
bootimagecontroller "github.com/openshift/machine-config-operator/pkg/controller/bootimage"
Expand All @@ -23,6 +24,7 @@ import (
"github.com/openshift/machine-config-operator/pkg/version"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
coreinformersv1 "k8s.io/client-go/informers/core/v1"
"k8s.io/client-go/tools/leaderelection"
"k8s.io/klog/v2"
)
Expand Down Expand Up @@ -201,6 +203,16 @@ func runStartCmd(_ *cobra.Command, _ []string) {
}

func createControllers(ctx *ctrlcommon.ControllerContext) []ctrlcommon.Controller {
// Only watch IRI informers when the feature gate is enabled. The
// InternalReleaseImages CRD is not installed on clusters where the gate is
// off, so the informer list call would fail and WaitForCacheSync in the
// template controller would block forever.
var iriSecretsInformer coreinformersv1.SecretInformer
var iriInformer mcfginformersv1alpha1.InternalReleaseImageInformer
if ctx.FeatureGatesHandler.Enabled(features.FeatureGateNoRegistryClusterInstall) {
iriSecretsInformer = ctx.KubeInformerFactory.Core().V1().Secrets()
iriInformer = ctx.InformerFactory.Machineconfiguration().V1alpha1().InternalReleaseImages()
}

var controllers []ctrlcommon.Controller
controllers = append(controllers,
Expand All @@ -209,9 +221,12 @@ func createControllers(ctx *ctrlcommon.ControllerContext) []ctrlcommon.Controlle
rootOpts.templates,
ctx.InformerFactory.Machineconfiguration().V1().ControllerConfigs(),
ctx.OpenShiftConfigKubeNamespacedInformerFactory.Core().V1().Secrets(),
iriSecretsInformer,
iriInformer,
ctx.ConfigInformerFactory.Config().V1().APIServers(),
ctx.ClientBuilder.KubeClientOrDie("template-controller"),
ctx.ClientBuilder.MachineConfigClientOrDie("template-controller"),
ctx.FeatureGatesHandler,
),
// Add all "sub-renderers here"
kubeletconfig.New(
Expand Down
20 changes: 20 additions & 0 deletions install/0000_80_machine-config_00_service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,23 @@ spec:
port: 9637
targetPort: 9637
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: internal-release-image-registry
namespace: openshift-machine-config-operator
labels:
k8s-app: internal-release-image-registry

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yuqi-zhang given that this Service is required only for documentation/tracking purposes, and not really used, what about using a more speaking label, ie something like "not-active-service"?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no strong feelings about this, I'm fine with an explicit name if you think that helps

# This Service has no backing pods. It exists solely to document port 22625
# in the OCP communication flows matrix (see enhancements/network/communication-flows-matrix-ingress.md).
not-active-service: "true"
annotations:
include.release.openshift.io/self-managed-high-availability: "true"
spec:
type: ClusterIP
ports:
- name: https
port: 22625
targetPort: 22625
protocol: TCP

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rwsu we never discussed to add k8s service for the IRI service (while the proposed design was just a plain systemd unit), so not sure why this was added here and I think it must be removed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, this came from me - we told at some point that every open port should be backed by an EndpointSlice, so it can be documented and visible in the communication flows matrix. This gets into our OCP docs eventually so users don't block any of these accidentally if they have e.g. a firewall configuration. See: https://github.com/openshift/enhancements/blob/master/enhancements/network/communication-flows-matrix-ingress.md

Also see previous MCO bug chain https://redhat.atlassian.net/browse/OCPBUGS-65544 and https://redhat.atlassian.net/browse/MCO-1886 .

I'm not entirely sold on the details, but based on the original context I think it's probably best to have this dummy service here like we do for the MCS ports, which really is doing the same thing.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I though that it could have been enough to document this requirement (in the usual pre-requisites section) as discussed in the EP. As long as it does not conflict with the systemd unit I think it's fine (it could be useful to put a comment about that if possible, as it could be easily missed)

17 changes: 16 additions & 1 deletion pkg/controller/bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ func (b *Bootstrap) Run(destDir string) error {
iri *mcfgv1alpha1.InternalReleaseImage
iriTLSCert *corev1.Secret
osImageStream *mcfgv1alpha1.OSImageStream
iriCredentialsSecret *corev1.Secret
)
for _, info := range infos {
if info.IsDir() {
Expand Down Expand Up @@ -202,6 +203,9 @@ func (b *Bootstrap) Run(destDir string) error {
if obj.GetName() == ctrlcommon.InternalReleaseImageTLSSecretName {
iriTLSCert = obj
}
if obj.GetName() == ctrlcommon.InternalReleaseImageAuthSecretName {
iriCredentialsSecret = obj
}
case *mcfgv1alpha1.OSImageStream:
// If given, it's treated as user input with config such as the default stream
osImageStream = obj
Expand Down Expand Up @@ -259,6 +263,17 @@ func (b *Bootstrap) Run(destDir string) error {
}

pullSecretBytes := pullSecret.Data[corev1.DockerConfigJsonKey]

// Merge IRI registry credentials into the pull secret for first-boot authentication.
// The template controller has not yet run at this point, so machine-config-daemon-pull.service
// would otherwise fail to authenticate against the IRI registry.
// Merge is a no-op if the feature gate is off or the IRI resource is absent.
merger := ctrlcommon.NewIRISecretMergerFromObjects(iriCredentialsSecret, cconfig, fgHandler, iri)
pullSecretBytes, err = merger.Merge(pullSecretBytes)
if err != nil {
return fmt.Errorf("could not merge IRI credentials into pull secret for bootstrap: %w", err)
}

iconfigs, err := template.RunBootstrap(b.templatesDir, cconfig, pullSecretBytes, apiServer)
if err != nil {
return err
Expand Down Expand Up @@ -321,7 +336,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, iriCredentialsSecret, cconfig)
if err != nil {
return err
}
Expand Down
9 changes: 9 additions & 0 deletions pkg/controller/common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@ 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"

// IRIRegistryPort is the port on which the IRI registry listens on master nodes.
IRIRegistryPort = 22625

// IRIRegistryUsername is the fixed username used for IRI registry htpasswd authentication.
IRIRegistryUsername = "openshift"

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

Expand Down
192 changes: 192 additions & 0 deletions pkg/controller/common/iri_secret_merger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package common

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

features "github.com/openshift/api/features"
mcfgv1 "github.com/openshift/api/machineconfiguration/v1"
mcfgv1alpha1 "github.com/openshift/api/machineconfiguration/v1alpha1"
mcfglistersv1 "github.com/openshift/client-go/machineconfiguration/listers/machineconfiguration/v1"
mcfglistersv1alpha1 "github.com/openshift/client-go/machineconfiguration/listers/machineconfiguration/v1alpha1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/klog/v2"
corelistersv1 "k8s.io/client-go/listers/core/v1"
)

// errIRIDisabled is returned by resolve when the NoRegistryClusterInstall
// feature gate is off or the InternalReleaseImage resource is absent.
// Merge treats it as a skip signal rather than an error.
var errIRIDisabled = errors.New("IRI not enabled or not present")

// IRISecretMerger merges IRI registry credentials into a pull secret.
// Construct via NewIRISecretMerger (controller use) or NewIRISecretMergerFromObjects
// (bootstrap use); then call Merge for each pull secret that needs updating.
type IRISecretMerger struct {
// resolve returns the password and baseDomain needed for merging, or
// errIRIDisabled when IRI is not in use on this cluster.
resolve func() (password, baseDomain string, err error)
}

// NewIRISecretMerger creates an IRISecretMerger that resolves the feature gate,
// IRI resource, credentials secret, and ControllerConfig from the informer cache
// at merge time. Use this in controllers where informers are available.
// fgHandler must not be nil.
func NewIRISecretMerger(
secretLister corelistersv1.SecretLister,
ccLister mcfglistersv1.ControllerConfigLister,
iriLister mcfglistersv1alpha1.InternalReleaseImageLister,
fgHandler FeatureGatesHandler,
) *IRISecretMerger {
return &IRISecretMerger{
resolve: func() (string, string, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this pattern IMHO works pretty bad from a quality point of view, it makes the code more complicated and harder to read (to not cite the duplication). Not a blocking point now though, but I'd like to get it improved in future

if !fgHandler.Enabled(features.FeatureGateNoRegistryClusterInstall) {
return "", "", errIRIDisabled
}
_, err := iriLister.Get(InternalReleaseImageInstanceName)
if apierrors.IsNotFound(err) {
return "", "", errIRIDisabled
}
if err != nil {
return "", "", fmt.Errorf("could not get InternalReleaseImage: %w", err)
}
secret, err := secretLister.Secrets(MCONamespace).Get(InternalReleaseImageAuthSecretName)
if err != nil {
return "", "", fmt.Errorf("could not get IRI auth secret: %w", err)
}
cconfig, err := ccLister.Get(ControllerConfigName)
if err != nil {
return "", "", fmt.Errorf("could not get ControllerConfig: %w", err)
}
return extractIRICredentials(secret, cconfig)
},
}
}

// NewIRISecretMergerFromObjects creates an IRISecretMerger from pre-fetched objects.
// Use this during bootstrap where informer caches are not yet available.
// The feature gate and iri checks are deferred to Merge time so the constructor
// never returns an error; if either check fails, Merge skips and logs.
func NewIRISecretMergerFromObjects(
secret *corev1.Secret,
cconfig *mcfgv1.ControllerConfig,
fgHandler FeatureGatesHandler,
iri *mcfgv1alpha1.InternalReleaseImage,
) *IRISecretMerger {
return &IRISecretMerger{
resolve: func() (string, string, error) {
if fgHandler == nil || !fgHandler.Enabled(features.FeatureGateNoRegistryClusterInstall) {
return "", "", errIRIDisabled
}
if iri == nil {
return "", "", errIRIDisabled
}
return extractIRICredentials(secret, cconfig)
},
}
}

// Merge merges IRI registry credentials into pullSecretRaw, adding auth entries
// for api-int.<baseDomain>:<IRIRegistryPort> (all nodes) and
// localhost:<IRIRegistryPort> (masters, where the registry runs locally).
// If the feature gate is disabled or the InternalReleaseImage resource is absent,
// Merge logs and returns pullSecretRaw unchanged.
func (m *IRISecretMerger) Merge(pullSecretRaw []byte) ([]byte, error) {
password, baseDomain, err := m.resolve()
if errors.Is(err, errIRIDisabled) {
klog.V(4).Info("Skipping IRI registry credential merge: IRI not enabled or not present")
return pullSecretRaw, nil
}
if err != nil {
return nil, err
}
merged, changed, err := mergeIRIRegistryCredentialsIntoPullSecret(pullSecretRaw, password, baseDomain)
if err != nil {
return nil, err
}
if changed {
klog.V(4).Info("Merged IRI registry credentials into pull secret")
}
return merged, nil
}

// extractIRICredentials validates and extracts the password and baseDomain from
// the IRI credentials secret and ControllerConfig.
func extractIRICredentials(secret *corev1.Secret, cconfig *mcfgv1.ControllerConfig) (password, baseDomain string, err error) {
if secret == nil {
return "", "", fmt.Errorf("IRI registry credentials secret must not be nil")
}
if cconfig == nil {
return "", "", fmt.Errorf("ControllerConfig must not be nil")
}
if cconfig.Spec.DNS == nil {
return "", "", fmt.Errorf("ControllerConfig DNS spec must not be nil")
}
pw, ok := secret.Data["password"]
if !ok || len(pw) == 0 {
return "", "", fmt.Errorf("IRI registry credentials secret missing or empty \"password\" field")
}
bd := cconfig.Spec.DNS.Spec.BaseDomain
if strings.TrimSpace(bd) == "" {
return "", "", fmt.Errorf("ControllerConfig baseDomain must not be empty")
}
return string(pw), bd, nil
}

// mergeIRIRegistryCredentialsIntoPullSecret merges IRI registry authentication
// credentials into a dockerconfigjson pull secret. It adds auth entries for
// api-int.<baseDomain>:<IRIRegistryPort> (all nodes) and
// localhost:<IRIRegistryPort> (masters, where the registry runs locally).
// Returns the merged bytes, a boolean indicating whether the pull secret was
// changed, and any error.
func mergeIRIRegistryCredentialsIntoPullSecret(pullSecretRaw []byte, password, baseDomain string) ([]byte, bool, error) {
// The IRI registry is reachable via api-int on all nodes, and also via
// localhost on master nodes where it runs locally. registries.conf mirror
// rules on masters use localhost:22625, so credentials must be present for
// both hostnames to avoid authentication failures.
iriRegistryAPIIntHost := fmt.Sprintf("api-int.%s:%d", baseDomain, IRIRegistryPort)
iriRegistryLocalHost := fmt.Sprintf("localhost:%d", IRIRegistryPort)

var dockerConfig map[string]interface{}
if err := json.Unmarshal(pullSecretRaw, &dockerConfig); err != nil {
return nil, false, fmt.Errorf("could not parse pull secret: %w", err)
}

auths, ok := dockerConfig["auths"].(map[string]interface{})
if !ok {
return nil, false, fmt.Errorf("pull secret missing 'auths' field")
}

authValue := base64.StdEncoding.EncodeToString([]byte(IRIRegistryUsername + ":" + password))

// Check if both IRI entries already exist and are current — no update needed.
if pullSecretHasAuth(auths, iriRegistryAPIIntHost, authValue) && pullSecretHasAuth(auths, iriRegistryLocalHost, authValue) {
return pullSecretRaw, false, nil
}

klog.V(4).Infof("Merging IRI auth credentials into pull secret for %s and %s", iriRegistryAPIIntHost, iriRegistryLocalHost)
auths[iriRegistryAPIIntHost] = map[string]interface{}{
Comment thread
rwsu marked this conversation as resolved.
"auth": authValue,
}
auths[iriRegistryLocalHost] = map[string]interface{}{
"auth": authValue,
}

mergedBytes, err := json.Marshal(dockerConfig)
if err != nil {
return nil, false, fmt.Errorf("could not marshal merged pull secret: %w", err)
}

return mergedBytes, true, nil
}

// pullSecretHasAuth returns true if auths[host] exists and its "auth" field
// matches expected.
func pullSecretHasAuth(auths map[string]interface{}, host, expected string) bool {
e, ok := auths[host].(map[string]interface{})
return ok && e["auth"] == expected
}
Loading