Skip to content

Commit 1cdb2af

Browse files
minimizing reboot for scaling image mode enabled node
1 parent c6faace commit 1cdb2af

File tree

7 files changed

+248
-62
lines changed

7 files changed

+248
-62
lines changed

manifests/machineconfigserver/clusterrole.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,19 @@ rules:
99
- apiGroups: ["machineconfiguration.openshift.io"]
1010
resources: ["controllerconfigs"]
1111
verbs: ["get", "watch", "list"]
12+
- apiGroups: ["machineconfiguration.openshift.io"]
13+
resources: ["machineosconfigs", "machineosbuilds"]
14+
verbs: ["get", "list", "watch"]
1215
- apiGroups: ["security.openshift.io"]
1316
resourceNames: ["hostnetwork"]
1417
resources: ["securitycontextconstraints"]
1518
verbs: ["use"]
1619
- apiGroups: [""]
1720
resources: ["configmaps"]
1821
verbs: ["get", "list", "watch"]
22+
- apiGroups: [""]
23+
resources: ["services"]
24+
verbs: ["get", "list"]
25+
- apiGroups: ["route.openshift.io"]
26+
resources: ["routes"]
27+
verbs: ["get", "list"]

pkg/controller/build/reconciler.go

Lines changed: 1 addition & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -999,7 +999,7 @@ func (b *buildReconciler) deleteMOSBImage(ctx context.Context, mosb *mcfgv1.Mach
999999
}
10001000

10011001
image := string(mosb.Spec.RenderedImagePushSpec)
1002-
isOpenShiftRegistry, err := b.isOpenShiftRegistry(image)
1002+
isOpenShiftRegistry, err := ctrlcommon.IsOpenShiftRegistry(context.TODO(), image, b.kubeclient, b.routeclient)
10031003
if err != nil {
10041004
return err
10051005
}
@@ -1039,53 +1039,6 @@ func (b *buildReconciler) deleteMOSBImage(ctx context.Context, mosb *mcfgv1.Mach
10391039
return nil
10401040
}
10411041

1042-
// getInternalRegistryHostnames discovers OpenShift internal registry hostnames
1043-
func (b *buildReconciler) getInternalRegistryHostnames(ctx context.Context) ([]string, error) {
1044-
var hostnames []string
1045-
1046-
// Get the list of services in the openshift-image-registry namespace (cluster-local)
1047-
services, err := b.kubeclient.CoreV1().Services("openshift-image-registry").List(ctx, metav1.ListOptions{})
1048-
if err != nil {
1049-
return nil, err
1050-
}
1051-
for _, svc := range services.Items {
1052-
clusterHostname := fmt.Sprintf("%s.%s.svc", svc.Name, svc.Namespace)
1053-
if len(svc.Spec.Ports) > 0 {
1054-
port := svc.Spec.Ports[0].Port
1055-
hostnames = append(hostnames, fmt.Sprintf("%s:%d", clusterHostname, port))
1056-
} else {
1057-
hostnames = append(hostnames, clusterHostname)
1058-
}
1059-
}
1060-
1061-
// Get the list of routes in the openshift-image-registry namespace (external access)
1062-
routes, err := b.routeclient.RouteV1().Routes("openshift-image-registry").List(ctx, metav1.ListOptions{})
1063-
if err != nil {
1064-
return nil, err
1065-
}
1066-
for _, route := range routes.Items {
1067-
if route.Spec.Host != "" {
1068-
hostnames = append(hostnames, route.Spec.Host)
1069-
}
1070-
}
1071-
1072-
return hostnames, nil
1073-
}
1074-
1075-
// isOpenShiftRegistry checks if the imageRef points to one of the known internal hostnames
1076-
func (b *buildReconciler) isOpenShiftRegistry(imageRef string) (bool, error) {
1077-
registryHosts, err := b.getInternalRegistryHostnames(context.TODO())
1078-
if err != nil {
1079-
return false, err
1080-
}
1081-
for _, host := range registryHosts {
1082-
if strings.HasPrefix(imageRef, host) {
1083-
return true, nil
1084-
}
1085-
}
1086-
return false, nil
1087-
}
1088-
10891042
// Finds and deletes any other running builds for a given MachineOSConfig.
10901043
func (b *buildReconciler) deleteOtherBuildsForMachineOSConfig(ctx context.Context, newMosb *mcfgv1.MachineOSBuild, mosc *mcfgv1.MachineOSConfig) error {
10911044
mosbList, err := b.getMachineOSBuildsForMachineOSConfig(mosc)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package common
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
routeclientset "github.com/openshift/client-go/route/clientset/versioned"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
clientset "k8s.io/client-go/kubernetes"
11+
)
12+
13+
// GetInternalRegistryHostnames discovers OpenShift internal registry hostnames
14+
// by querying Services and Routes in the openshift-image-registry namespace.
15+
func GetInternalRegistryHostnames(ctx context.Context, kubeclient clientset.Interface, routeclient routeclientset.Interface) ([]string, error) {
16+
var hostnames []string
17+
18+
// Get the list of services in the openshift-image-registry namespace (cluster-local)
19+
services, err := kubeclient.CoreV1().Services("openshift-image-registry").List(ctx, metav1.ListOptions{})
20+
if err != nil {
21+
return nil, err
22+
}
23+
for _, svc := range services.Items {
24+
clusterHostname := fmt.Sprintf("%s.%s.svc", svc.Name, svc.Namespace)
25+
if len(svc.Spec.Ports) > 0 {
26+
port := svc.Spec.Ports[0].Port
27+
hostnames = append(hostnames, fmt.Sprintf("%s:%d", clusterHostname, port))
28+
} else {
29+
hostnames = append(hostnames, clusterHostname)
30+
}
31+
}
32+
33+
// Get the list of routes in the openshift-image-registry namespace (external access)
34+
routes, err := routeclient.RouteV1().Routes("openshift-image-registry").List(ctx, metav1.ListOptions{})
35+
if err != nil {
36+
return nil, err
37+
}
38+
for _, route := range routes.Items {
39+
if route.Spec.Host != "" {
40+
hostnames = append(hostnames, route.Spec.Host)
41+
}
42+
}
43+
44+
return hostnames, nil
45+
}
46+
47+
// IsOpenShiftRegistry checks if the imageRef points to one of the known internal registry hostnames
48+
func IsOpenShiftRegistry(ctx context.Context, imageRef string, kubeclient clientset.Interface, routeclient routeclientset.Interface) (bool, error) {
49+
registryHosts, err := GetInternalRegistryHostnames(ctx, kubeclient, routeclient)
50+
if err != nil {
51+
return false, err
52+
}
53+
54+
for _, host := range registryHosts {
55+
if strings.HasPrefix(imageRef, host) {
56+
return true, nil
57+
}
58+
}
59+
60+
return false, nil
61+
}

pkg/daemon/daemon.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2174,7 +2174,12 @@ func (dn *Daemon) checkStateOnFirstRun() error {
21742174

21752175
// Bootstrapping state is when we have the node annotations file
21762176
if state.bootstrapping {
2177-
targetOSImageURL := state.currentConfig.Spec.OSImageURL
2177+
// During bootstrap, prefer the image from node annotations (if set) over the MC from cluster lister
2178+
targetOSImageURL := state.currentImage
2179+
if targetOSImageURL == "" {
2180+
// Fall back to MC's OSImageURL if no image annotation was provided
2181+
targetOSImageURL = state.currentConfig.Spec.OSImageURL
2182+
}
21782183
osMatch := dn.checkOS(targetOSImageURL)
21792184
if !osMatch {
21802185
logSystem("Bootstrap pivot required to: %s", targetOSImageURL)

pkg/server/cluster_server.go

Lines changed: 160 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package server
22

33
import (
4+
"context"
45
"encoding/json"
56
"errors"
67
"fmt"
@@ -9,14 +10,19 @@ import (
910
"path/filepath"
1011
"time"
1112

13+
ign3types "github.com/coreos/ignition/v2/config/v3_5/types"
1214
yaml "github.com/ghodss/yaml"
15+
mcfgv1 "github.com/openshift/api/machineconfiguration/v1"
1316
mcfginformers "github.com/openshift/client-go/machineconfiguration/informers/externalversions"
1417

18+
routeclientset "github.com/openshift/client-go/route/clientset/versioned"
1519
"github.com/openshift/machine-config-operator/internal/clients"
1620
ctrlcommon "github.com/openshift/machine-config-operator/pkg/controller/common"
1721
corev1 "k8s.io/api/core/v1"
22+
"k8s.io/apimachinery/pkg/labels"
1823
"k8s.io/apimachinery/pkg/runtime"
1924
"k8s.io/client-go/informers"
25+
clientset "k8s.io/client-go/kubernetes"
2026
corelisterv1 "k8s.io/client-go/listers/core/v1"
2127
"k8s.io/client-go/tools/cache"
2228
clientcmdv1 "k8s.io/client-go/tools/clientcmd/api/v1"
@@ -42,6 +48,11 @@ type clusterServer struct {
4248
machineConfigLister v1.MachineConfigLister
4349
controllerConfigLister v1.ControllerConfigLister
4450
configMapLister corelisterv1.ConfigMapLister
51+
machineOSConfigLister v1.MachineOSConfigLister
52+
machineOSBuildLister v1.MachineOSBuildLister
53+
54+
kubeclient clientset.Interface
55+
routeclient routeclientset.Interface
4556

4657
kubeconfigFunc kubeconfigFunc
4758
apiserverURL string
@@ -73,26 +84,31 @@ func NewClusterServer(kubeConfig, apiserverURL string) (Server, error) {
7384

7485
machineConfigClient := clientsBuilder.MachineConfigClientOrDie("machine-config-shared-informer")
7586
kubeClient := clientsBuilder.KubeClientOrDie("kube-client-shared-informer")
87+
routeClient := clientsBuilder.RouteClientOrDie("route-client")
7688
sharedInformerFactory := mcfginformers.NewSharedInformerFactory(machineConfigClient, resyncPeriod()())
7789
kubeNamespacedSharedInformer := informers.NewFilteredSharedInformerFactory(kubeClient, resyncPeriod()(), "openshift-machine-config-operator", nil)
7890

79-
mcpInformer, mcInformer, ccInformer, cmInformer :=
91+
mcpInformer, mcInformer, ccInformer, cmInformer, moscInformer, mosbInformer :=
8092
sharedInformerFactory.Machineconfiguration().V1().MachineConfigPools(),
8193
sharedInformerFactory.Machineconfiguration().V1().MachineConfigs(),
8294
sharedInformerFactory.Machineconfiguration().V1().ControllerConfigs(),
83-
kubeNamespacedSharedInformer.Core().V1().ConfigMaps()
84-
mcpLister, mcLister, ccLister, cmLister := mcpInformer.Lister(), mcInformer.Lister(), ccInformer.Lister(), cmInformer.Lister()
85-
mcpListerHasSynced, mcListerHasSynced, ccListerHasSynced, cmListerHasSynced :=
95+
kubeNamespacedSharedInformer.Core().V1().ConfigMaps(),
96+
sharedInformerFactory.Machineconfiguration().V1().MachineOSConfigs(),
97+
sharedInformerFactory.Machineconfiguration().V1().MachineOSBuilds()
98+
mcpLister, mcLister, ccLister, cmLister, moscLister, mosbLister := mcpInformer.Lister(), mcInformer.Lister(), ccInformer.Lister(), cmInformer.Lister(), moscInformer.Lister(), mosbInformer.Lister()
99+
mcpListerHasSynced, mcListerHasSynced, ccListerHasSynced, cmListerHasSynced, moscListerHasSynced, mosbListerHasSynced :=
86100
mcpInformer.Informer().HasSynced,
87101
mcInformer.Informer().HasSynced,
88102
ccInformer.Informer().HasSynced,
89-
cmInformer.Informer().HasSynced
103+
cmInformer.Informer().HasSynced,
104+
moscInformer.Informer().HasSynced,
105+
mosbInformer.Informer().HasSynced
90106

91107
var informerStopCh chan struct{}
92108
go sharedInformerFactory.Start(informerStopCh)
93109
go kubeNamespacedSharedInformer.Start(informerStopCh)
94110

95-
if !cache.WaitForCacheSync(informerStopCh, mcpListerHasSynced, mcListerHasSynced, ccListerHasSynced, cmListerHasSynced) {
111+
if !cache.WaitForCacheSync(informerStopCh, mcpListerHasSynced, mcListerHasSynced, ccListerHasSynced, cmListerHasSynced, moscListerHasSynced, mosbListerHasSynced) {
96112
return nil, errors.New("failed to wait for cache sync")
97113
}
98114

@@ -101,6 +117,10 @@ func NewClusterServer(kubeConfig, apiserverURL string) (Server, error) {
101117
machineConfigLister: mcLister,
102118
controllerConfigLister: ccLister,
103119
configMapLister: cmLister,
120+
machineOSConfigLister: moscLister,
121+
machineOSBuildLister: mosbLister,
122+
kubeclient: kubeClient,
123+
routeclient: routeClient,
104124
kubeconfigFunc: func() ([]byte, []byte, error) { return kubeconfigFromSecret(bootstrapTokenDir, apiserverURL, nil) },
105125
apiserverURL: apiserverURL,
106126
}, nil
@@ -163,7 +183,27 @@ func (cs *clusterServer) GetConfig(cr poolRequest) (*runtime.RawExtension, error
163183

164184
addDataAndMaybeAppendToIgnition(caBundleFilePath, cc.Spec.KubeAPIServerServingCAData, &ignConf)
165185
addDataAndMaybeAppendToIgnition(cloudProviderCAPath, cc.Spec.CloudProviderCAData, &ignConf)
166-
appenders := getAppenders(currConf, cr.version, cs.kubeconfigFunc, []string{}, "")
186+
187+
desiredImage := cs.resolveDesiredImageForPool(mp)
188+
klog.Infof("desiredImage is found to be %s", desiredImage)
189+
190+
appenders := []appenderFunc{
191+
func(cfg *ign3types.Config, _ *mcfgv1.MachineConfig) error {
192+
return appendNodeAnnotations(cfg, currConf, desiredImage)
193+
},
194+
func(cfg *ign3types.Config, _ *mcfgv1.MachineConfig) error {
195+
return appendKubeConfig(cfg, cs.kubeconfigFunc)
196+
},
197+
appendInitialMachineConfig,
198+
func(cfg *ign3types.Config, _ *mcfgv1.MachineConfig) error { return appendCerts(cfg, []string{}, "") },
199+
// Inject desired image into the MC
200+
appendDesiredOSImage(desiredImage),
201+
// Must be last
202+
func(cfg *ign3types.Config, mc *mcfgv1.MachineConfig) error {
203+
return appendEncapsulated(cfg, mc, cr.version)
204+
},
205+
}
206+
167207
for _, a := range appenders {
168208
if err := a(&ignConf, mc); err != nil {
169209
return nil, err
@@ -224,3 +264,116 @@ func kubeconfigFromSecret(secretDir, apiserverURL string, caData []byte) ([]byte
224264
}
225265
return kcData, caData, nil
226266
}
267+
268+
// Finds the MOSC that targets this MCO and verfies that the MOSC has an image in status
269+
// locates the matching MOSB for the MCP's current or next rendered MC and confirms build succeeded
270+
// and returns the image pullspec when its ready
271+
func (cs *clusterServer) resolveDesiredImageForPool(pool *mcfgv1.MachineConfigPool) string {
272+
// If listers are not initialized (e.g., in tests or clusters without layering), return empty
273+
if cs.machineOSConfigLister == nil || cs.machineOSBuildLister == nil {
274+
return ""
275+
}
276+
277+
// Find MachineOSConfig for this pool
278+
moscList, err := cs.machineOSConfigLister.List(labels.Everything())
279+
if err != nil {
280+
// MOSC resources not available
281+
klog.Infof("Could not list MachineOSConfigs for pool %s: %v", pool.Name, err)
282+
return ""
283+
}
284+
285+
// TODO(dkhater): Simplify to directly get MOSC using pool name with admin_ack enforcing MOSC name == pool name.
286+
// Versions 4.18.21-4.18.23 lack OCPBUGS-60904 validation (MOSCs could have non-matching names).
287+
// Can use admin_ack (e.g., at 4.23) to block upgrades until MOSCs are corrected, then replace List+filter with: mosc, err := cs.machineOSConfigLister.Get(pool.Name)
288+
// See: https://issues.redhat.com/browse/OCPBUGS-60904 for validation bug.
289+
// See: https://issues.redhat.com/browse/MCO-1935 for cleanup story.
290+
291+
var mosc *mcfgv1.MachineOSConfig
292+
for _, config := range moscList {
293+
if config.Spec.MachineConfigPool.Name == pool.Name {
294+
mosc = config
295+
break
296+
}
297+
}
298+
299+
// No MOSC for this pool - not layered
300+
if mosc == nil {
301+
return ""
302+
}
303+
304+
// Check if image is ready in MOSC status
305+
moscState := ctrlcommon.NewMachineOSConfigState(mosc)
306+
if !moscState.HasOSImage() {
307+
klog.Infof("Pool %s has MachineOSConfig but image not ready yet", pool.Name)
308+
return ""
309+
}
310+
311+
// Also verify the corresponding MOSB is successful to ensure we don't serve an image that hasn't been built yet
312+
mosbList, err := cs.machineOSBuildLister.List(labels.Everything())
313+
if err != nil {
314+
klog.Infof("Could not list MachineOSBuilds for pool %s: %v", pool.Name, err)
315+
return ""
316+
}
317+
318+
var currentConf string
319+
if pool.Status.UpdatedMachineCount > 0 {
320+
currentConf = pool.Spec.Configuration.Name
321+
} else {
322+
currentConf = pool.Status.Configuration.Name
323+
}
324+
325+
var mosb *mcfgv1.MachineOSBuild
326+
for _, build := range mosbList {
327+
if build.Spec.MachineOSConfig.Name == mosc.Name &&
328+
build.Spec.MachineConfig.Name == currentConf {
329+
mosb = build
330+
break
331+
}
332+
}
333+
334+
if mosb == nil {
335+
klog.Infof("Pool %s has MachineOSConfig but no matching MachineOSBuild for MC %s", pool.Name, currentConf)
336+
return ""
337+
}
338+
339+
// Check build is successful
340+
mosbState := ctrlcommon.NewMachineOSBuildState(mosb)
341+
if !mosbState.IsBuildSuccess() {
342+
klog.Infof("Pool %s has MachineOSBuild but build not successful yet", pool.Name)
343+
return ""
344+
}
345+
346+
imageSpec := moscState.GetOSImage()
347+
348+
// Don't serve internal registry images during bootstrap because cluster DNS is not available yet
349+
// New nodes will bootstrap with base image, then update to layered image after joining cluster (2 reboots)
350+
// External registries (Quay, etc.) will still get the 1-reboot optimization
351+
isInternal, err := ctrlcommon.IsOpenShiftRegistry(context.TODO(), imageSpec, cs.kubeclient, cs.routeclient)
352+
if err != nil {
353+
klog.Errorf("Failed to check if image is internal registry for pool %s: %v", pool.Name, err)
354+
return ""
355+
}
356+
357+
if isInternal {
358+
klog.V(4).Infof("New nodes will bootstrap with base image, then update to layered image after joining cluster")
359+
return ""
360+
}
361+
362+
klog.Infof("Resolved layered image for pool %s: %s", pool.Name, imageSpec)
363+
return imageSpec
364+
}
365+
366+
// appendDesiredOSImage mutates the MC to include the desired OS image.
367+
// This runs appendEncapsulated so mcd-firstboot sees the image on first boot.
368+
func appendDesiredOSImage(desired string) appenderFunc {
369+
return func(_ *ign3types.Config, mc *mcfgv1.MachineConfig) error {
370+
if desired == "" {
371+
return nil
372+
}
373+
if mc.Spec.OSImageURL != desired {
374+
klog.Infof("overriding MC OSImageURL: %q -> %q", mc.Spec.OSImageURL, desired)
375+
mc.Spec.OSImageURL = desired
376+
}
377+
return nil
378+
}
379+
}

0 commit comments

Comments
 (0)