@@ -27,11 +27,14 @@ import (
27
27
28
28
"github.com/spf13/cobra"
29
29
"github.com/spf13/viper"
30
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30
31
"k8s.io/apimachinery/pkg/util/sets"
32
+ "k8s.io/client-go/kubernetes"
31
33
"k8s.io/client-go/tools/clientcmd"
32
34
"k8s.io/klog/v2"
33
35
"k8s.io/kops/cmd/kops/util"
34
36
"k8s.io/kops/pkg/apis/kops"
37
+ apisutil "k8s.io/kops/pkg/apis/kops/util"
35
38
"k8s.io/kops/pkg/assets"
36
39
"k8s.io/kops/pkg/commands/commandutils"
37
40
"k8s.io/kops/pkg/kubeconfig"
@@ -67,6 +70,10 @@ type UpdateClusterOptions struct {
67
70
SSHPublicKey string
68
71
RunTasksOptions fi.RunTasksOptions
69
72
AllowKopsDowngrade bool
73
+ // Bypasses kubelet vs control plane version skew checks,
74
+ // which by default prevent non-control plane instancegroups
75
+ // from being updated to a version greater than the control plane
76
+ IgnoreKubeletVersionSkew bool
70
77
// GetAssets is whether this is invoked from the CmdGetAssets.
71
78
GetAssets bool
72
79
@@ -103,6 +110,8 @@ func (o *UpdateClusterOptions) InitDefaults() {
103
110
o .Target = "direct"
104
111
o .SSHPublicKey = ""
105
112
o .OutDir = ""
113
+ // By default we enforce the version skew between control plane and worker nodes
114
+ o .IgnoreKubeletVersionSkew = false
106
115
107
116
// By default we export a kubecfg, but it doesn't have a static/eternal credential in it any more.
108
117
o .CreateKubecfg = true
@@ -163,6 +172,7 @@ func NewCmdUpdateCluster(f *util.Factory, out io.Writer) *cobra.Command {
163
172
cmd .RegisterFlagCompletionFunc ("lifecycle-overrides" , completeLifecycleOverrides )
164
173
165
174
cmd .Flags ().BoolVar (& options .Prune , "prune" , options .Prune , "Delete old revisions of cloud resources that were needed during an upgrade" )
175
+ cmd .Flags ().BoolVar (& options .IgnoreKubeletVersionSkew , "ignore-kubelet-version-skew" , options .IgnoreKubeletVersionSkew , "Setting this to true will force updating the kubernetes version on all instance groups, regardles of which control plane version is running" )
166
176
167
177
return cmd
168
178
}
@@ -318,20 +328,30 @@ func RunUpdateCluster(ctx context.Context, f *util.Factory, out io.Writer, c *Up
318
328
return nil , err
319
329
}
320
330
331
+ minControlPlaneRunningVersion := cluster .Spec .KubernetesVersion
332
+ if ! c .IgnoreKubeletVersionSkew {
333
+ minControlPlaneRunningVersion , err = checkControlPlaneRunningVersion (ctx , cluster .ObjectMeta .Name , minControlPlaneRunningVersion )
334
+ if err != nil {
335
+ klog .Warningf ("error checking control plane running version, assuming no k8s upgrade in progress: %v" , err )
336
+ } else {
337
+ klog .V (2 ).Infof ("successfully checked control plane running version: %v" , minControlPlaneRunningVersion )
338
+ }
339
+ }
321
340
applyCmd := & cloudup.ApplyClusterCmd {
322
- Cloud : cloud ,
323
- Clientset : clientset ,
324
- Cluster : cluster ,
325
- DryRun : isDryrun ,
326
- AllowKopsDowngrade : c .AllowKopsDowngrade ,
327
- RunTasksOptions : & c .RunTasksOptions ,
328
- OutDir : c .OutDir ,
329
- InstanceGroupFilter : predicates .AllOf (instanceGroupFilters ... ),
330
- Phase : phase ,
331
- TargetName : targetName ,
332
- LifecycleOverrides : lifecycleOverrideMap ,
333
- GetAssets : c .GetAssets ,
334
- DeletionProcessing : deletionProcessing ,
341
+ Cloud : cloud ,
342
+ Clientset : clientset ,
343
+ Cluster : cluster ,
344
+ DryRun : isDryrun ,
345
+ AllowKopsDowngrade : c .AllowKopsDowngrade ,
346
+ RunTasksOptions : & c .RunTasksOptions ,
347
+ OutDir : c .OutDir ,
348
+ InstanceGroupFilter : predicates .AllOf (instanceGroupFilters ... ),
349
+ Phase : phase ,
350
+ TargetName : targetName ,
351
+ LifecycleOverrides : lifecycleOverrideMap ,
352
+ GetAssets : c .GetAssets ,
353
+ DeletionProcessing : deletionProcessing ,
354
+ ControlPlaneRunningVersion : minControlPlaneRunningVersion ,
335
355
}
336
356
337
357
applyResults , err := applyCmd .Run (ctx )
@@ -575,3 +595,38 @@ func matchInstanceGroupRoles(roles []string) predicates.Predicate[*kops.Instance
575
595
return false
576
596
}
577
597
}
598
+
599
+ // checkControlPlaneRunningVersion returns the minimum control plane running version
600
+ func checkControlPlaneRunningVersion (ctx context.Context , clusterName string , version string ) (string , error ) {
601
+ configLoadingRules := clientcmd .NewDefaultClientConfigLoadingRules ()
602
+ config , err := clientcmd .NewNonInteractiveDeferredLoadingClientConfig (
603
+ configLoadingRules ,
604
+ & clientcmd.ConfigOverrides {CurrentContext : clusterName }).ClientConfig ()
605
+ if err != nil {
606
+ return version , fmt .Errorf ("cannot load kubecfg settings for %q: %v" , clusterName , err )
607
+ }
608
+
609
+ k8sClient , err := kubernetes .NewForConfig (config )
610
+ if err != nil {
611
+ return version , fmt .Errorf ("cannot build kubernetes api client for %q: %v" , clusterName , err )
612
+ }
613
+
614
+ parsedVersion , err := apisutil .ParseKubernetesVersion (version )
615
+ if err != nil {
616
+ return version , fmt .Errorf ("cannot parse kubernetes version %q: %v" , clusterName , err )
617
+ }
618
+ nodeList , err := k8sClient .CoreV1 ().Nodes ().List (ctx , metav1.ListOptions {
619
+ LabelSelector : "node-role.kubernetes.io/control-plane" ,
620
+ })
621
+ if err != nil {
622
+ return version , fmt .Errorf ("cannot list nodes in cluster %q: %v" , clusterName , err )
623
+ }
624
+ for _ , node := range nodeList .Items {
625
+ if apisutil .IsKubernetesGTE (node .Status .NodeInfo .KubeletVersion , * parsedVersion ) {
626
+ version = node .Status .NodeInfo .KubeletVersion
627
+ parsedVersion , _ = apisutil .ParseKubernetesVersion (version )
628
+ }
629
+
630
+ }
631
+ return strings .TrimPrefix (version , "v" ), nil
632
+ }
0 commit comments