kubeadm: make "upgrade node" include URL scheme in socket paths

The CRI socket that kubeadm writes as an annotation
on a particular Node object can include an endpoint that
does not have an URL scheme. This is undesired as long term
the kubelet can stop allowing endpoints without URL scheme.

For control plane nodes "kubeadm upgrade apply" takes
the locally defaulted / populated NodeRegistration and refreshes
the CRI socket in PerformPostUpgradeTasks. But for secondary
nodes "kubeadm upgrade node" does not.

Adapt "upgrade node" to fetch the NodeRegistration for this node
and fix the CRI socket missing URL scheme if needed in the Node
annotation.
This commit is contained in:
Lubomir I. Ivanov 2021-12-29 01:59:07 +02:00
parent 7594f0ef90
commit 207556e057
5 changed files with 51 additions and 8 deletions

View File

@ -34,4 +34,5 @@ type Data interface {
Client() clientset.Interface Client() clientset.Interface
IgnorePreflightErrors() sets.String IgnorePreflightErrors() sets.String
PatchesDir() string PatchesDir() string
KubeConfigPath() string
} }

View File

@ -20,15 +20,22 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/pkg/errors" "github.com/pkg/errors"
"k8s.io/klog/v2"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmapiv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
"k8s.io/kubernetes/cmd/kubeadm/app/cmd/options" "k8s.io/kubernetes/cmd/kubeadm/app/cmd/options"
"k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases/workflow" "k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases/workflow"
cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util" cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util"
"k8s.io/kubernetes/cmd/kubeadm/app/constants" "k8s.io/kubernetes/cmd/kubeadm/app/constants"
kubeletphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubelet" kubeletphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubelet"
patchnodephase "k8s.io/kubernetes/cmd/kubeadm/app/phases/patchnode"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/upgrade" "k8s.io/kubernetes/cmd/kubeadm/app/phases/upgrade"
configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config"
dryrunutil "k8s.io/kubernetes/cmd/kubeadm/app/util/dryrun" dryrunutil "k8s.io/kubernetes/cmd/kubeadm/app/util/dryrun"
) )
@ -87,6 +94,34 @@ func runKubeletConfigPhase() func(c workflow.RunData) error {
return nil return nil
} }
// Handle a missing URL scheme in the Node CRI socket.
// Older versions of kubeadm tolerate CRI sockets without URL schemes (/var/run/foo without unix://).
// During "upgrade node" for worker nodes the cfg.NodeRegistration would be left empty.
// This requires to call GetNodeRegistration on demand and fetch the node name and CRI socket.
// If the NodeRegistration (nro) contains a socket without a URL scheme, update it.
//
// TODO: this workaround can be removed in 1.25 once all user node sockets have a URL scheme:
// https://github.com/kubernetes/kubeadm/issues/2426
var nro *kubeadmapi.NodeRegistrationOptions
var missingURLScheme bool
if !dryRun {
if err := configutil.GetNodeRegistration(data.KubeConfigPath(), data.Client(), nro); err != nil {
return errors.Wrap(err, "could not retrieve the node registration options for this node")
}
missingURLScheme = strings.HasPrefix(nro.CRISocket, kubeadmapiv1.DefaultContainerRuntimeURLScheme)
}
if missingURLScheme {
if !dryRun {
newSocket := kubeadmapiv1.DefaultContainerRuntimeURLScheme + "://" + nro.CRISocket
klog.V(2).Infof("ensuring that Node %q has a CRI socket annotation with URL scheme %q", nro.Name, newSocket)
if err := patchnodephase.AnnotateCRISocket(data.Client(), nro.Name, newSocket); err != nil {
return errors.Wrapf(err, "error updating the CRI socket for Node %q", nro.Name)
}
} else {
fmt.Println("[dryrun] would update the node CRI socket path to include an URL scheme")
}
}
fmt.Println("[upgrade] The configuration for this node was successfully updated!") fmt.Println("[upgrade] The configuration for this node was successfully updated!")
fmt.Println("[upgrade] Now you should go ahead and upgrade the kubelet package using your package manager.") fmt.Println("[upgrade] Now you should go ahead and upgrade the kubelet package using your package manager.")
return nil return nil

View File

@ -61,6 +61,7 @@ type nodeData struct {
client clientset.Interface client clientset.Interface
patchesDir string patchesDir string
ignorePreflightErrors sets.String ignorePreflightErrors sets.String
kubeConfigPath string
} }
// newCmdNode returns the cobra command for `kubeadm upgrade node` // newCmdNode returns the cobra command for `kubeadm upgrade node`
@ -159,6 +160,7 @@ func newNodeData(cmd *cobra.Command, args []string, options *nodeOptions) (*node
isControlPlaneNode: isControlPlaneNode, isControlPlaneNode: isControlPlaneNode,
patchesDir: options.patchesDir, patchesDir: options.patchesDir,
ignorePreflightErrors: ignorePreflightErrorsSet, ignorePreflightErrors: ignorePreflightErrorsSet,
kubeConfigPath: options.kubeConfigPath,
}, nil }, nil
} }
@ -201,3 +203,8 @@ func (d *nodeData) PatchesDir() string {
func (d *nodeData) IgnorePreflightErrors() sets.String { func (d *nodeData) IgnorePreflightErrors() sets.String {
return d.ignorePreflightErrors return d.ignorePreflightErrors
} }
// KubeconfigPath returns the path to the user kubeconfig file.
func (d *nodeData) KubeConfigPath() string {
return d.kubeConfigPath
}

View File

@ -97,7 +97,8 @@ func getInitConfigurationFromCluster(kubeconfigDir string, client clientset.Inte
// get nodes specific information as well // get nodes specific information as well
if !newControlPlane { if !newControlPlane {
// gets the nodeRegistration for the current from the node object // gets the nodeRegistration for the current from the node object
if err := getNodeRegistration(kubeconfigDir, client, &initcfg.NodeRegistration); err != nil { kubeconfigFile := filepath.Join(kubeconfigDir, constants.KubeletKubeConfigFileName)
if err := GetNodeRegistration(kubeconfigFile, client, &initcfg.NodeRegistration); err != nil {
return nil, errors.Wrap(err, "failed to get node registration") return nil, errors.Wrap(err, "failed to get node registration")
} }
// gets the APIEndpoint for the current node // gets the APIEndpoint for the current node
@ -117,10 +118,10 @@ func getInitConfigurationFromCluster(kubeconfigDir string, client clientset.Inte
return initcfg, nil return initcfg, nil
} }
// getNodeRegistration returns the nodeRegistration for the current node // GetNodeRegistration returns the nodeRegistration for the current node
func getNodeRegistration(kubeconfigDir string, client clientset.Interface, nodeRegistration *kubeadmapi.NodeRegistrationOptions) error { func GetNodeRegistration(kubeconfigFile string, client clientset.Interface, nodeRegistration *kubeadmapi.NodeRegistrationOptions) error {
// gets the name of the current node // gets the name of the current node
nodeName, err := getNodeNameFromKubeletConfig(kubeconfigDir) nodeName, err := getNodeNameFromKubeletConfig(kubeconfigFile)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to get node name from kubelet config") return errors.Wrap(err, "failed to get node name from kubelet config")
} }
@ -149,9 +150,8 @@ func getNodeRegistration(kubeconfigDir string, client clientset.Interface, nodeR
// getNodeNameFromKubeletConfig gets the node name from a kubelet config file // getNodeNameFromKubeletConfig gets the node name from a kubelet config file
// TODO: in future we want to switch to a more canonical way for doing this e.g. by having this // TODO: in future we want to switch to a more canonical way for doing this e.g. by having this
// information in the local kubelet config.yaml // information in the local kubelet config.yaml
func getNodeNameFromKubeletConfig(kubeconfigDir string) (string, error) { func getNodeNameFromKubeletConfig(fileName string) (string, error) {
// loads the kubelet.conf file // loads the kubelet.conf file
fileName := filepath.Join(kubeconfigDir, constants.KubeletKubeConfigFileName)
config, err := clientcmd.LoadFromFile(fileName) config, err := clientcmd.LoadFromFile(fileName)
if err != nil { if err != nil {
return "", err return "", err

View File

@ -261,7 +261,7 @@ func TestGetNodeNameFromKubeletConfig(t *testing.T) {
return return
} }
name, err := getNodeNameFromKubeletConfig(tmpdir) name, err := getNodeNameFromKubeletConfig(kubeconfigPath)
if rt.expectedError != (err != nil) { if rt.expectedError != (err != nil) {
t.Errorf("unexpected return err from getNodeRegistration: %v", err) t.Errorf("unexpected return err from getNodeRegistration: %v", err)
return return
@ -338,7 +338,7 @@ func TestGetNodeRegistration(t *testing.T) {
} }
cfg := &kubeadmapi.InitConfiguration{} cfg := &kubeadmapi.InitConfiguration{}
err = getNodeRegistration(tmpdir, client, &cfg.NodeRegistration) err = GetNodeRegistration(cfgPath, client, &cfg.NodeRegistration)
if rt.expectedError != (err != nil) { if rt.expectedError != (err != nil) {
t.Errorf("unexpected return err from getNodeRegistration: %v", err) t.Errorf("unexpected return err from getNodeRegistration: %v", err)
return return