Skip to content

Commit 50998de

Browse files
authored
Merge pull request kubernetes#128457 from neolit123/1.31-improve-dry-run-logic
kubeadm: support dryrunning upgrade without a real cluster
2 parents 8233d1e + 07918a5 commit 50998de

File tree

11 files changed

+373
-71
lines changed

11 files changed

+373
-71
lines changed

cmd/kubeadm/app/cmd/phases/upgrade/apply/preflight.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ func runPreflight(c workflow.RunData) error {
8080

8181
// Run healthchecks against the cluster.
8282
klog.V(1).Infoln("[upgrade/preflight] Verifying the cluster health")
83-
if err := upgrade.CheckClusterHealth(client, &initCfg.ClusterConfiguration, ignorePreflightErrors, printer); err != nil {
83+
if err := upgrade.CheckClusterHealth(client, &initCfg.ClusterConfiguration, ignorePreflightErrors, data.DryRun(), printer); err != nil {
8484
return err
8585
}
8686

cmd/kubeadm/app/cmd/upgrade/apply.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,13 +223,13 @@ func newApplyData(cmd *cobra.Command, args []string, applyFlags *applyFlags) (*a
223223
return nil, cmdutil.TypeMismatchErr("printConfig", "bool")
224224
}
225225

226-
client, err := getClient(applyFlags.kubeConfigPath, *dryRun)
226+
printer := &output.TextPrinter{}
227+
228+
client, err := getClient(applyFlags.kubeConfigPath, *dryRun, printer)
227229
if err != nil {
228230
return nil, errors.Wrapf(err, "couldn't create a Kubernetes client from file %q", applyFlags.kubeConfigPath)
229231
}
230232

231-
printer := &output.TextPrinter{}
232-
233233
// Fetches the cluster configuration.
234234
klog.V(1).Infoln("[upgrade] retrieving configuration from cluster")
235235
initCfg, err := configutil.FetchInitConfigurationFromCluster(client, nil, "upgrade", false, false)

cmd/kubeadm/app/cmd/upgrade/common.go

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"bytes"
2222
"io"
2323
"os"
24+
"path/filepath"
2425

2526
"github.com/pkg/errors"
2627
"github.com/spf13/pflag"
@@ -71,7 +72,7 @@ func enforceRequirements(flagSet *pflag.FlagSet, flags *applyPlanFlags, args []s
7172
}
7273
}
7374

74-
client, err := getClient(flags.kubeConfigPath, *isDryRun)
75+
client, err := getClient(flags.kubeConfigPath, *isDryRun, printer)
7576
if err != nil {
7677
return nil, nil, nil, nil, errors.Wrapf(err, "couldn't create a Kubernetes client from file %q", flags.kubeConfigPath)
7778
}
@@ -137,7 +138,7 @@ func enforceRequirements(flagSet *pflag.FlagSet, flags *applyPlanFlags, args []s
137138
}
138139

139140
// Run healthchecks against the cluster
140-
if err := upgrade.CheckClusterHealth(client, &initCfg.ClusterConfiguration, ignorePreflightErrorsSet, printer); err != nil {
141+
if err := upgrade.CheckClusterHealth(client, &initCfg.ClusterConfiguration, ignorePreflightErrorsSet, dryRun, printer); err != nil {
141142
return nil, nil, nil, nil, errors.Wrap(err, "[upgrade/health] FATAL")
142143
}
143144

@@ -189,32 +190,57 @@ func runPreflightChecks(client clientset.Interface, ignorePreflightErrors sets.S
189190
}
190191

191192
// getClient gets a real or fake client depending on whether the user is dry-running or not
192-
func getClient(file string, dryRun bool) (clientset.Interface, error) {
193+
func getClient(file string, dryRun bool, printer output.Printer) (clientset.Interface, error) {
193194
if dryRun {
195+
// Default the server version to the kubeadm version.
196+
serverVersion := constants.CurrentKubernetesVersion.Info()
197+
194198
dryRun := apiclient.NewDryRun()
195-
if err := dryRun.WithKubeConfigFile(file); err != nil {
196-
return nil, err
197-
}
198199
dryRun.WithDefaultMarshalFunction().
199200
WithWriter(os.Stdout).
200201
PrependReactor(dryRun.HealthCheckJobReactor()).
201202
PrependReactor(dryRun.PatchNodeReactor())
202203

203-
// In order for fakeclient.Discovery().ServerVersion() to return the backing API Server's
204-
// real version; we have to do some clever API machinery tricks. First, we get the real
205-
// API Server's version.
206-
realServerVersion, err := dryRun.Client().Discovery().ServerVersion()
207-
if err != nil {
208-
return nil, errors.Wrap(err, "failed to get server version")
204+
// If the kubeconfig exists, construct a real client from it and get the real serverVersion.
205+
if _, err := os.Stat(file); err == nil {
206+
_, _ = printer.Printf("[dryrun] Creating a real client from %q\n", file)
207+
if err := dryRun.WithKubeConfigFile(file); err != nil {
208+
return nil, err
209+
}
210+
serverVersion, err = dryRun.Client().Discovery().ServerVersion()
211+
if err != nil {
212+
return nil, errors.Wrap(err, "failed to get server version")
213+
}
214+
} else if os.IsNotExist(err) {
215+
// If the file (supposedly admin.conf) does not exist, add more reactors.
216+
// Knowing the node name is required by the ListPodsReactor. For that we try to use
217+
// the kubelet.conf client, if it exists. If not, it falls back to hostname.
218+
_, _ = printer.Printf("[dryrun] Dryrunning without a real client\n")
219+
kubeconfigPath := filepath.Join(constants.KubernetesDir, constants.KubeletKubeConfigFileName)
220+
nodeName, err := configutil.GetNodeName(kubeconfigPath)
221+
if err != nil {
222+
return nil, err
223+
}
224+
dryRun.PrependReactor(dryRun.GetKubeadmConfigReactor()).
225+
PrependReactor(dryRun.GetKubeletConfigReactor()).
226+
PrependReactor(dryRun.GetKubeProxyConfigReactor()).
227+
PrependReactor(dryRun.GetNodeReactor()).
228+
PrependReactor(dryRun.ListPodsReactor(nodeName)).
229+
PrependReactor(dryRun.GetCoreDNSConfigReactor()).
230+
PrependReactor(dryRun.ListDeploymentsReactor())
231+
} else {
232+
// Throw an error if the file exists but there was a different stat error.
233+
return nil, errors.Wrapf(err, "could not create a client from %q", file)
209234
}
235+
210236
// Obtain the FakeDiscovery object for this fake client.
211237
fakeClient := dryRun.FakeClient()
212238
fakeClientDiscovery, ok := fakeClient.Discovery().(*fakediscovery.FakeDiscovery)
213239
if !ok {
214240
return nil, errors.New("could not set fake discovery's server version")
215241
}
216-
// Lastly, set the right server version to be used.
217-
fakeClientDiscovery.FakedServerVersion = realServerVersion
242+
// Set the right server version for it.
243+
fakeClientDiscovery.FakedServerVersion = serverVersion
218244

219245
return fakeClient, nil
220246
}

cmd/kubeadm/app/cmd/upgrade/node.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package upgrade
1818

1919
import (
20+
"fmt"
2021
"io"
2122
"os"
2223

@@ -26,6 +27,7 @@ import (
2627

2728
"k8s.io/apimachinery/pkg/util/sets"
2829
clientset "k8s.io/client-go/kubernetes"
30+
"k8s.io/klog/v2"
2931

3032
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
3133
"k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta4"
@@ -37,6 +39,7 @@ import (
3739
cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util"
3840
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
3941
configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config"
42+
"k8s.io/kubernetes/cmd/kubeadm/app/util/output"
4043
)
4144

4245
// nodeOptions defines all the options exposed via flags by kubeadm upgrade node.
@@ -84,7 +87,15 @@ func newCmdNode(out io.Writer) *cobra.Command {
8487
return err
8588
}
8689

87-
return nodeRunner.Run(args)
90+
if err := nodeRunner.Run(args); err != nil {
91+
return err
92+
}
93+
if nodeOptions.dryRun {
94+
fmt.Println("[upgrade/successful] Finished dryrunning successfully!")
95+
return nil
96+
}
97+
98+
return nil
8899
},
89100
Args: cobra.NoArgs,
90101
}
@@ -150,6 +161,7 @@ func newNodeData(cmd *cobra.Command, nodeOptions *nodeOptions, out io.Writer) (*
150161
isControlPlaneNode := true
151162
filepath := constants.GetStaticPodFilepath(constants.KubeAPIServer, constants.GetStaticPodDirectory())
152163
if _, err := os.Stat(filepath); os.IsNotExist(err) {
164+
klog.V(1).Infof("assuming this is not a control plane node because %q is missing", filepath)
153165
isControlPlaneNode = false
154166
}
155167
if len(nodeOptions.kubeConfigPath) == 0 {
@@ -171,7 +183,9 @@ func newNodeData(cmd *cobra.Command, nodeOptions *nodeOptions, out io.Writer) (*
171183
if !ok {
172184
return nil, cmdutil.TypeMismatchErr("dryRun", "bool")
173185
}
174-
client, err := getClient(nodeOptions.kubeConfigPath, *dryRun)
186+
187+
printer := &output.TextPrinter{}
188+
client, err := getClient(nodeOptions.kubeConfigPath, *dryRun, printer)
175189
if err != nil {
176190
return nil, errors.Wrapf(err, "couldn't create a Kubernetes client from file %q", nodeOptions.kubeConfigPath)
177191
}

cmd/kubeadm/app/phases/upgrade/health.go

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,18 @@ const createJobHealthCheckPrefix = "upgrade-health-check"
4646

4747
// healthCheck is a helper struct for easily performing healthchecks against the cluster and printing the output
4848
type healthCheck struct {
49-
name string
50-
client clientset.Interface
51-
cfg *kubeadmapi.ClusterConfiguration
49+
name string
50+
client clientset.Interface
51+
cfg *kubeadmapi.ClusterConfiguration
52+
dryRun bool
53+
printer output.Printer
5254
// f is invoked with a k8s client and a kubeadm ClusterConfiguration passed to it. Should return an optional error
53-
f func(clientset.Interface, *kubeadmapi.ClusterConfiguration) error
55+
f func(clientset.Interface, *kubeadmapi.ClusterConfiguration, bool, output.Printer) error
5456
}
5557

5658
// Check is part of the preflight.Checker interface
5759
func (c *healthCheck) Check() (warnings, errors []error) {
58-
if err := c.f(c.client, c.cfg); err != nil {
60+
if err := c.f(c.client, c.cfg, c.dryRun, c.printer); err != nil {
5961
return nil, []error{err}
6062
}
6163
return nil, nil
@@ -70,32 +72,38 @@ func (c *healthCheck) Name() string {
7072
// - the cluster can accept a workload
7173
// - all control-plane Nodes are Ready
7274
// - (if static pod-hosted) that all required Static Pod manifests exist on disk
73-
func CheckClusterHealth(client clientset.Interface, cfg *kubeadmapi.ClusterConfiguration, ignoreChecksErrors sets.Set[string], printer output.Printer) error {
75+
func CheckClusterHealth(client clientset.Interface, cfg *kubeadmapi.ClusterConfiguration, ignoreChecksErrors sets.Set[string], dryRun bool, printer output.Printer) error {
7476
_, _ = printer.Println("[upgrade] Running cluster health checks")
7577

7678
healthChecks := []preflight.Checker{
7779
&healthCheck{
78-
name: "CreateJob",
79-
client: client,
80-
cfg: cfg,
81-
f: createJob,
80+
name: "CreateJob",
81+
client: client,
82+
cfg: cfg,
83+
f: createJob,
84+
dryRun: dryRun,
85+
printer: printer,
8286
},
8387
&healthCheck{
84-
name: "ControlPlaneNodesReady",
85-
client: client,
86-
f: controlPlaneNodesReady,
88+
name: "ControlPlaneNodesReady",
89+
client: client,
90+
f: controlPlaneNodesReady,
91+
dryRun: dryRun,
92+
printer: printer,
8793
},
8894
&healthCheck{
89-
name: "StaticPodManifest",
90-
f: staticPodManifestHealth,
95+
name: "StaticPodManifest",
96+
f: staticPodManifestHealth,
97+
dryRun: dryRun,
98+
printer: printer,
9199
},
92100
}
93101

94102
return preflight.RunChecks(healthChecks, os.Stderr, ignoreChecksErrors)
95103
}
96104

97105
// createJob is a check that verifies that a Job can be created in the cluster
98-
func createJob(client clientset.Interface, cfg *kubeadmapi.ClusterConfiguration) error {
106+
func createJob(client clientset.Interface, cfg *kubeadmapi.ClusterConfiguration, _ bool, _ output.Printer) error {
99107
const (
100108
fieldSelector = "spec.unschedulable=false"
101109
ns = metav1.NamespaceSystem
@@ -213,7 +221,7 @@ func createJob(client clientset.Interface, cfg *kubeadmapi.ClusterConfiguration)
213221
}
214222

215223
// controlPlaneNodesReady checks whether all control-plane Nodes in the cluster are in the Running state
216-
func controlPlaneNodesReady(client clientset.Interface, _ *kubeadmapi.ClusterConfiguration) error {
224+
func controlPlaneNodesReady(client clientset.Interface, _ *kubeadmapi.ClusterConfiguration, _ bool, _ output.Printer) error {
217225
selectorControlPlane := labels.SelectorFromSet(map[string]string{
218226
constants.LabelNodeRoleControlPlane: "",
219227
})
@@ -232,18 +240,22 @@ func controlPlaneNodesReady(client clientset.Interface, _ *kubeadmapi.ClusterCon
232240
}
233241

234242
// staticPodManifestHealth makes sure the required static pods are presents
235-
func staticPodManifestHealth(_ clientset.Interface, _ *kubeadmapi.ClusterConfiguration) error {
243+
func staticPodManifestHealth(_ clientset.Interface, _ *kubeadmapi.ClusterConfiguration, dryRun bool, printer output.Printer) error {
236244
var nonExistentManifests []string
237245
for _, component := range constants.ControlPlaneComponents {
238246
manifestFile := constants.GetStaticPodFilepath(component, constants.GetStaticPodDirectory())
247+
if dryRun {
248+
_, _ = printer.Printf("[dryrun] would check if %s exists\n", manifestFile)
249+
continue
250+
}
239251
if _, err := os.Stat(manifestFile); os.IsNotExist(err) {
240252
nonExistentManifests = append(nonExistentManifests, manifestFile)
241253
}
242254
}
243255
if len(nonExistentManifests) == 0 {
244256
return nil
245257
}
246-
return errors.Errorf("The control plane seems to be Static Pod-hosted, but some of the manifests don't seem to exist on disk. This probably means you're running 'kubeadm upgrade' on a remote machine, which is not supported for a Static Pod-hosted cluster. Manifest files not found: %v", nonExistentManifests)
258+
return errors.Errorf("manifest files not found: %v", nonExistentManifests)
247259
}
248260

249261
// getNotReadyNodes returns a string slice of nodes in the cluster that are NotReady

cmd/kubeadm/app/phases/upgrade/staticpods_test.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,6 @@ func (w *fakeWaiter) WaitForPodsWithLabel(kvLabel string) error {
113113
return w.errsToReturn[waitForPodsWithLabel]
114114
}
115115

116-
// WaitForPodToDisappear just returns a dummy nil, to indicate that the program should just proceed
117-
func (w *fakeWaiter) WaitForPodToDisappear(podName string) error {
118-
return nil
119-
}
120-
121116
// SetTimeout is a no-op; we don't use it in this implementation
122117
func (w *fakeWaiter) SetTimeout(_ time.Duration) {}
123118

0 commit comments

Comments
 (0)