Refactor the kubeadm join command

pull/564/head
fabriziopandini 2019-01-09 12:46:53 +01:00
parent bf56c7be42
commit fd842480d1
5 changed files with 404 additions and 266 deletions

View File

@ -88,8 +88,6 @@ go_test(
],
embed = [":go_default_library"],
deps = [
"//cmd/kubeadm/app/apis/kubeadm:go_default_library",
"//cmd/kubeadm/app/apis/kubeadm/scheme:go_default_library",
"//cmd/kubeadm/app/apis/kubeadm/v1beta1:go_default_library",
"//cmd/kubeadm/app/apis/kubeadm/validation:go_default_library",
"//cmd/kubeadm/app/cmd/options:go_default_library",

View File

@ -83,7 +83,7 @@ func NewKubeadmCommand(in io.Reader, out, err io.Writer) *cobra.Command {
cmds.AddCommand(NewCmdCompletion(out, ""))
cmds.AddCommand(NewCmdConfig(out))
cmds.AddCommand(NewCmdInit(out, nil))
cmds.AddCommand(NewCmdJoin(out))
cmds.AddCommand(NewCmdJoin(out, nil))
cmds.AddCommand(NewCmdReset(in, out))
cmds.AddCommand(NewCmdVersion(out))
cmds.AddCommand(NewCmdToken(out, err))

View File

@ -37,6 +37,7 @@ import (
kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme"
kubeadmapiv1beta1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta1"
"k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/validation"
"k8s.io/kubernetes/cmd/kubeadm/app/cmd/options"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
"k8s.io/kubernetes/cmd/kubeadm/app/discovery"
certsphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs"
@ -152,206 +153,258 @@ var (
`)
)
// joinOptions defines all the options exposed via flags by kubeadm join.
// Please note that this structure includes the public kubeadm config API, but only a subset of the options
// supported by this api will be exposed as a flag.
type joinOptions struct {
cfgPath string
token string
controlPlane bool
ignorePreflightErrors []string
externalcfg *kubeadmapiv1beta1.JoinConfiguration
}
// joinData defines all the runtime information used when running the kubeadm join worklow;
// this data is shared across all the phases that are included in the workflow.
type joinData struct {
cfg *kubeadmapi.JoinConfiguration
ignorePreflightErrors sets.String
outputWriter io.Writer
}
// NewCmdJoin returns "kubeadm join" command.
func NewCmdJoin(out io.Writer) *cobra.Command {
cfg := &kubeadmapiv1beta1.JoinConfiguration{}
kubeadmscheme.Scheme.Default(cfg)
fd := &kubeadmapiv1beta1.FileDiscovery{}
btd := &kubeadmapiv1beta1.BootstrapTokenDiscovery{}
var token string
var cfgPath string
var ignorePreflightErrors []string
var controlPlane bool
var advertiseAddress string
var bindPort int32 = kubeadmapiv1beta1.DefaultAPIBindPort
// NB. joinOptions is exposed as parameter for allowing unit testing of
// the newJoinData method, that implements all the command options validation logic
func NewCmdJoin(out io.Writer, joinOptions *joinOptions) *cobra.Command {
if joinOptions == nil {
joinOptions = newJoinOptions()
}
cmd := &cobra.Command{
Use: "join",
Short: "Run this on any machine you wish to join an existing cluster",
Long: joinLongDescription,
Run: func(cmd *cobra.Command, args []string) {
if len(fd.KubeConfigPath) != 0 {
cfg.Discovery.File = fd
} else {
cfg.Discovery.BootstrapToken = btd
if len(cfg.Discovery.BootstrapToken.Token) == 0 {
cfg.Discovery.BootstrapToken.Token = token
}
if len(args) > 0 {
if len(cfgPath) == 0 && len(args) > 1 {
klog.Warningf("[join] WARNING: More than one API server endpoint supplied on command line %v. Using the first one.", args)
}
cfg.Discovery.BootstrapToken.APIServerEndpoint = args[0]
}
}
if len(cfg.Discovery.TLSBootstrapToken) == 0 {
cfg.Discovery.TLSBootstrapToken = token
}
if controlPlane {
cfg.ControlPlane = &kubeadmapiv1beta1.JoinControlPlane{
LocalAPIEndpoint: kubeadmapiv1beta1.APIEndpoint{
AdvertiseAddress: advertiseAddress,
BindPort: bindPort,
},
}
}
j, err := NewValidJoin(cmd.PersistentFlags(), cfg, cfgPath, ignorePreflightErrors)
joinData, err := newJoinData(cmd, args, joinOptions, out)
kubeadmutil.CheckErr(err)
err = joinData.Run()
kubeadmutil.CheckErr(err)
kubeadmutil.CheckErr(j.Run(out))
},
}
AddJoinConfigFlags(cmd.PersistentFlags(), cfg, &token)
AddJoinBootstrapTokenDiscoveryFlags(cmd.PersistentFlags(), btd)
AddJoinFileDiscoveryFlags(cmd.PersistentFlags(), fd)
AddJoinOtherFlags(cmd.PersistentFlags(), &cfgPath, &ignorePreflightErrors, &controlPlane, &advertiseAddress, &bindPort)
AddJoinConfigFlags(cmd.Flags(), joinOptions.externalcfg)
AddJoinOtherFlags(cmd.Flags(), &joinOptions.cfgPath, &joinOptions.ignorePreflightErrors, &joinOptions.controlPlane, &joinOptions.token)
return cmd
}
// NewValidJoin validates the command line that are passed to the cobra command
func NewValidJoin(flagSet *flag.FlagSet, cfg *kubeadmapiv1beta1.JoinConfiguration, cfgPath string, ignorePreflightErrors []string) (*Join, error) {
var err error
if err = validation.ValidateMixedArguments(flagSet); err != nil {
return nil, err
}
ignorePreflightErrorsSet, err := validation.ValidateIgnorePreflightErrors(ignorePreflightErrors)
if err != nil {
return nil, err
}
return NewJoin(cfgPath, cfg, ignorePreflightErrorsSet)
}
// AddJoinConfigFlags adds join flags bound to the config to the specified flagset
func AddJoinConfigFlags(flagSet *flag.FlagSet, cfg *kubeadmapiv1beta1.JoinConfiguration, token *string) {
func AddJoinConfigFlags(flagSet *flag.FlagSet, cfg *kubeadmapiv1beta1.JoinConfiguration) {
flagSet.StringVar(
&cfg.NodeRegistration.Name, "node-name", cfg.NodeRegistration.Name,
"Specify the node name.")
&cfg.NodeRegistration.Name, options.NodeName, cfg.NodeRegistration.Name,
`Specify the node name.`,
)
flagSet.StringVar(
token, "token", "",
"Use this token for both discovery-token and tls-bootstrap-token when those values are not provided.")
flagSet.StringVar(
&cfg.NodeRegistration.CRISocket, "cri-socket", cfg.NodeRegistration.CRISocket,
&cfg.NodeRegistration.CRISocket, options.NodeCRISocket, cfg.NodeRegistration.CRISocket,
`Specify the CRI socket to connect to.`,
)
flagSet.StringVar(
&cfg.Discovery.TLSBootstrapToken, options.TLSBootstrapToken, cfg.Discovery.TLSBootstrapToken,
`Specify the token used to temporarily authenticate with the Kubernetes Master while joining the node.`,
)
AddControlPlaneFlags(flagSet, cfg.ControlPlane)
AddJoinBootstrapTokenDiscoveryFlags(flagSet, cfg.Discovery.BootstrapToken)
AddJoinFileDiscoveryFlags(flagSet, cfg.Discovery.File)
}
// AddJoinBootstrapTokenDiscoveryFlags adds bootstrap token specific discovery flags to the specified flagset
func AddJoinBootstrapTokenDiscoveryFlags(flagSet *flag.FlagSet, btd *kubeadmapiv1beta1.BootstrapTokenDiscovery) {
flagSet.StringVar(
&btd.Token, "discovery-token", "",
"A token used to validate cluster information fetched from the API server.")
&btd.Token, options.TokenDiscovery, "",
"For token-based discovery, the token used to validate cluster information fetched from the API server.",
)
flagSet.StringSliceVar(
&btd.CACertHashes, "discovery-token-ca-cert-hash", []string{},
"For token-based discovery, validate that the root CA public key matches this hash (format: \"<type>:<value>\").")
&btd.CACertHashes, options.TokenDiscoveryCAHash, []string{},
"For token-based discovery, validate that the root CA public key matches this hash (format: \"<type>:<value>\").",
)
flagSet.BoolVar(
&btd.UnsafeSkipCAVerification, "discovery-token-unsafe-skip-ca-verification", false,
"For token-based discovery, allow joining without --discovery-token-ca-cert-hash pinning.")
&btd.UnsafeSkipCAVerification, options.TokenDiscoverySkipCAHash, false,
"For token-based discovery, allow joining without --discovery-token-ca-cert-hash pinning.",
)
}
// AddJoinFileDiscoveryFlags adds file discovery flags to the specified flagset
func AddJoinFileDiscoveryFlags(flagSet *flag.FlagSet, fd *kubeadmapiv1beta1.FileDiscovery) {
flagSet.StringVar(
&fd.KubeConfigPath, "discovery-file", "",
"A file or URL from which to load cluster information.")
&fd.KubeConfigPath, options.FileDiscovery, "",
"For file-based discovery, a file or URL from which to load cluster information.",
)
}
// AddControlPlaneFlags adds file control plane flags to the specified flagset
func AddControlPlaneFlags(flagSet *flag.FlagSet, cp *kubeadmapiv1beta1.JoinControlPlane) {
flagSet.StringVar(
&cp.LocalAPIEndpoint.AdvertiseAddress, options.APIServerAdvertiseAddress, cp.LocalAPIEndpoint.AdvertiseAddress,
"If the node should host a new control plane instance, the IP address the API Server will advertise it's listening on. If not set the default network interface will be used.",
)
flagSet.Int32Var(
&cp.LocalAPIEndpoint.BindPort, options.APIServerBindPort, cp.LocalAPIEndpoint.BindPort,
"If the node should host a new control plane instance, the port for the API Server to bind to.",
)
}
// AddJoinOtherFlags adds join flags that are not bound to a configuration file to the given flagset
func AddJoinOtherFlags(flagSet *flag.FlagSet, cfgPath *string, ignorePreflightErrors *[]string, controlPlane *bool, advertiseAddress *string, bindPort *int32) {
func AddJoinOtherFlags(flagSet *flag.FlagSet, cfgPath *string, ignorePreflightErrors *[]string, controlPlane *bool, token *string) {
flagSet.StringVar(
cfgPath, "config", *cfgPath,
"Path to kubeadm config file.")
cfgPath, options.CfgPath, *cfgPath,
"Path to kubeadm config file.",
)
flagSet.StringSliceVar(
ignorePreflightErrors, "ignore-preflight-errors", *ignorePreflightErrors,
"A list of checks whose errors will be shown as warnings. Example: 'IsPrivilegedUser,Swap'. Value 'all' ignores errors from all checks.")
flagSet.BoolVar(
controlPlane, "experimental-control-plane", *controlPlane,
"Create a new control plane instance on this node")
ignorePreflightErrors, options.IgnorePreflightErrors, *ignorePreflightErrors,
"A list of checks whose errors will be shown as warnings. Example: 'IsPrivilegedUser,Swap'. Value 'all' ignores errors from all checks.",
)
flagSet.StringVar(
advertiseAddress, "apiserver-advertise-address", *advertiseAddress,
"If the node should host a new control plane instance, the IP address the API Server will advertise it's listening on.")
flagSet.Int32Var(
bindPort, "apiserver-bind-port", *bindPort,
"If the node should host a new control plane instance, the port for the API Server to bind to.")
token, options.TokenStr, "",
"Use this token for both discovery-token and tls-bootstrap-token when those values are not provided.",
)
flagSet.BoolVar(
controlPlane, options.ControlPlane, *controlPlane,
"Create a new control plane instance on this node",
)
}
// Join defines struct used by kubeadm join command
type Join struct {
cfg *kubeadmapi.JoinConfiguration
initCfg *kubeadmapi.InitConfiguration
tlsBootstrapCfg *clientcmdapi.Config
ignorePreflightErrors sets.String
// newJoinOptions returns a struct ready for being used for creating cmd join flags.
func newJoinOptions() *joinOptions {
// initialize the public kubeadm config API by appling defaults
externalcfg := &kubeadmapiv1beta1.JoinConfiguration{}
// Add optional config objects to host flags.
// un-set objects will be cleaned up afterwards (into newJoinData func)
externalcfg.Discovery.File = &kubeadmapiv1beta1.FileDiscovery{}
externalcfg.Discovery.BootstrapToken = &kubeadmapiv1beta1.BootstrapTokenDiscovery{}
externalcfg.ControlPlane = &kubeadmapiv1beta1.JoinControlPlane{}
// Apply defaults
kubeadmscheme.Scheme.Default(externalcfg)
return &joinOptions{
externalcfg: externalcfg,
}
}
// NewJoin instantiates Join struct with given arguments
func NewJoin(cfgPath string, defaultcfg *kubeadmapiv1beta1.JoinConfiguration, ignorePreflightErrors sets.String) (*Join, error) {
// newJoinData returns a new joinData struct to be used for the execution of the kubeadm join workflow.
// This func takes care of validating joinOptions passed to the command, and then it converts
// options into the internal JoinConfiguration type that is used as input all the phases in the kubeadm join workflow
func newJoinData(cmd *cobra.Command, args []string, options *joinOptions, out io.Writer) (joinData, error) {
// Re-apply defaults to the public kubeadm API (this will set only values not exposed/not set as a flags)
kubeadmscheme.Scheme.Default(options.externalcfg)
if defaultcfg.NodeRegistration.Name == "" {
klog.V(1).Infoln("[join] found NodeName empty; using OS hostname as NodeName")
}
// Validate standalone flags values and/or combination of flags and then assigns
// validated values to the public kubeadm config API when applicable
if defaultcfg.ControlPlane != nil && defaultcfg.ControlPlane.LocalAPIEndpoint.AdvertiseAddress == "" {
klog.V(1).Infoln("[join] found advertiseAddress empty; using default interface's IP address as advertiseAddress")
}
internalCfg, err := configutil.JoinConfigFileAndDefaultsToInternalConfig(cfgPath, defaultcfg)
if err != nil {
return nil, err
}
// override node name and CRI socket from the command line options
if defaultcfg.NodeRegistration.Name != "" {
internalCfg.NodeRegistration.Name = defaultcfg.NodeRegistration.Name
}
if defaultcfg.NodeRegistration.CRISocket != kubeadmapiv1beta1.DefaultCRISocket {
internalCfg.NodeRegistration.CRISocket = defaultcfg.NodeRegistration.CRISocket
}
if internalCfg.ControlPlane != nil {
if err := configutil.VerifyAPIServerBindAddress(internalCfg.ControlPlane.LocalAPIEndpoint.AdvertiseAddress); err != nil {
return nil, err
// if a token is provided, use this value for both discovery-token and tls-bootstrap-token when those values are not provided
if len(options.token) > 0 {
if len(options.externalcfg.Discovery.TLSBootstrapToken) == 0 {
options.externalcfg.Discovery.TLSBootstrapToken = options.token
}
if len(options.externalcfg.Discovery.BootstrapToken.Token) == 0 {
options.externalcfg.Discovery.BootstrapToken.Token = options.token
}
}
// if a file or URL from which to load cluster information was not provided, unset the Discovery.File object
if len(options.externalcfg.Discovery.File.KubeConfigPath) == 0 {
options.externalcfg.Discovery.File = nil
}
// if an APIServerEndpoint from which to retrive cluster information was not provided, unset the Discovery.BootstrapToken object
if len(args) == 0 {
options.externalcfg.Discovery.BootstrapToken = nil
} else {
if len(options.cfgPath) == 0 && len(args) > 1 {
klog.Warningf("[join] WARNING: More than one API server endpoint supplied on command line %v. Using the first one.", args)
}
options.externalcfg.Discovery.BootstrapToken.APIServerEndpoint = args[0]
}
// if not joining a control plane, unset the ControlPlane object
if !options.controlPlane {
options.externalcfg.ControlPlane = nil
}
ignorePreflightErrorsSet, err := validation.ValidateIgnorePreflightErrors(options.ignorePreflightErrors)
if err != nil {
return joinData{}, err
}
if err = validation.ValidateMixedArguments(cmd.Flags()); err != nil {
return joinData{}, err
}
// Either use the config file if specified, or convert public kubeadm API to the internal JoinConfiguration
// and validates JoinConfiguration
if options.externalcfg.NodeRegistration.Name == "" {
klog.V(1).Infoln("[join] found NodeName empty; using OS hostname as NodeName")
}
if options.externalcfg.ControlPlane != nil && options.externalcfg.ControlPlane.LocalAPIEndpoint.AdvertiseAddress == "" {
klog.V(1).Infoln("[join] found advertiseAddress empty; using default interface's IP address as advertiseAddress")
}
cfg, err := configutil.JoinConfigFileAndDefaultsToInternalConfig(options.cfgPath, options.externalcfg)
if err != nil {
return joinData{}, err
}
// override node name and CRI socket from the command line options
if options.externalcfg.NodeRegistration.Name != "" {
cfg.NodeRegistration.Name = options.externalcfg.NodeRegistration.Name
}
if options.externalcfg.NodeRegistration.CRISocket != kubeadmapiv1beta1.DefaultCRISocket {
cfg.NodeRegistration.CRISocket = options.externalcfg.NodeRegistration.CRISocket
}
if cfg.ControlPlane != nil {
if err := configutil.VerifyAPIServerBindAddress(cfg.ControlPlane.LocalAPIEndpoint.AdvertiseAddress); err != nil {
return joinData{}, err
}
}
return joinData{
cfg: cfg,
ignorePreflightErrors: ignorePreflightErrorsSet,
outputWriter: out,
}, nil
}
// Run executes worker node provisioning and tries to join an existing cluster.
func (j *joinData) Run() error {
fmt.Println("[preflight] Running pre-flight checks")
// Start with general checks
klog.V(1).Infoln("[preflight] Running general checks")
if err := preflight.RunJoinNodeChecks(utilsexec.New(), internalCfg, ignorePreflightErrors); err != nil {
return nil, err
if err := preflight.RunJoinNodeChecks(utilsexec.New(), j.cfg, j.ignorePreflightErrors); err != nil {
return err
}
// Fetch the init configuration based on the join configuration
klog.V(1).Infoln("[preflight] Fetching init configuration")
initCfg, tlsBootstrapCfg, err := fetchInitConfigurationFromJoinConfiguration(internalCfg)
initCfg, tlsBootstrapCfg, err := fetchInitConfigurationFromJoinConfiguration(j.cfg)
if err != nil {
return nil, err
return err
}
// Continue with more specific checks based on the init configuration
klog.V(1).Infoln("[preflight] Running configuration dependant checks")
if err := preflight.RunOptionalJoinNodeChecks(utilsexec.New(), initCfg, ignorePreflightErrors); err != nil {
return nil, err
if err := preflight.RunOptionalJoinNodeChecks(utilsexec.New(), initCfg, j.ignorePreflightErrors); err != nil {
return err
}
return &Join{cfg: internalCfg, initCfg: initCfg, tlsBootstrapCfg: tlsBootstrapCfg, ignorePreflightErrors: ignorePreflightErrors}, nil
}
// Run executes worker node provisioning and tries to join an existing cluster.
func (j *Join) Run(out io.Writer) error {
if j.cfg.ControlPlane != nil {
// Checks if the cluster configuration supports
// joining a new control plane instance and if all the necessary certificates are provided
if err := j.CheckIfReadyForAdditionalControlPlane(j.initCfg); err != nil {
if err := j.CheckIfReadyForAdditionalControlPlane(initCfg); err != nil {
// outputs the not ready for hosting a new control plane instance message
ctx := map[string]string{
"Error": err.Error(),
@ -364,11 +417,11 @@ func (j *Join) Run(out io.Writer) error {
// run kubeadm init preflight checks for checking all the prequisites
fmt.Printf("[join] Running pre-flight checks before initializing the new control plane instance\n")
preflight.RunInitMasterChecks(utilsexec.New(), j.initCfg, j.ignorePreflightErrors)
preflight.RunInitMasterChecks(utilsexec.New(), initCfg, j.ignorePreflightErrors)
// Prepares the node for hosting a new control plane instance by writing necessary
// kubeconfig files, and static pod manifests
if err := j.PrepareForHostingControlPlane(j.initCfg); err != nil {
if err := j.PrepareForHostingControlPlane(initCfg); err != nil {
return err
}
}
@ -379,21 +432,21 @@ func (j *Join) Run(out io.Writer) error {
// if the node is hosting a new control plane instance, since it uses static pods for the control plane,
// as soon as the kubelet starts it will take charge of creating control plane
// components on the node.
if err := j.BootstrapKubelet(); err != nil {
if err := j.BootstrapKubelet(tlsBootstrapCfg, initCfg); err != nil {
return err
}
// if the node is hosting a new control plane instance
if j.cfg.ControlPlane != nil {
// Completes the control plane setup
if err := j.PostInstallControlPlane(j.initCfg); err != nil {
if err := j.PostInstallControlPlane(initCfg); err != nil {
return err
}
// outputs the join control plane done template and exits
etcdMessage := ""
// in case of local etcd
if j.initCfg.Etcd.External == nil {
if initCfg.Etcd.External == nil {
etcdMessage = "* A new etcd member was added to the local/stacked etcd cluster."
}
@ -401,19 +454,19 @@ func (j *Join) Run(out io.Writer) error {
"KubeConfigPath": kubeadmconstants.GetAdminKubeConfigPath(),
"etcdMessage": etcdMessage,
}
joinControPlaneDoneTemp.Execute(out, ctx)
joinControPlaneDoneTemp.Execute(j.outputWriter, ctx)
return nil
}
// otherwise, if the node joined as a worker node;
// outputs the join done message and exits
fmt.Fprintf(out, joinWorkerNodeDoneMsg)
fmt.Fprintf(j.outputWriter, joinWorkerNodeDoneMsg)
return nil
}
// CheckIfReadyForAdditionalControlPlane ensures that the cluster is in a state that supports
// joining an additional control plane instance and if the node is ready to join
func (j *Join) CheckIfReadyForAdditionalControlPlane(initConfiguration *kubeadmapi.InitConfiguration) error {
func (j *joinData) CheckIfReadyForAdditionalControlPlane(initConfiguration *kubeadmapi.InitConfiguration) error {
// blocks if the cluster was created without a stable control plane endpoint
if initConfiguration.ControlPlaneEndpoint == "" {
return errors.New("unable to add a new control plane instance a cluster that doesn't have a stable controlPlaneEndpoint address")
@ -428,7 +481,7 @@ func (j *Join) CheckIfReadyForAdditionalControlPlane(initConfiguration *kubeadma
}
// PrepareForHostingControlPlane makes all preparation activities require for a node hosting a new control plane instance
func (j *Join) PrepareForHostingControlPlane(initConfiguration *kubeadmapi.InitConfiguration) error {
func (j *joinData) PrepareForHostingControlPlane(initConfiguration *kubeadmapi.InitConfiguration) error {
// Generate missing certificates (if any)
if err := certsphase.CreatePKIAssets(initConfiguration); err != nil {
@ -471,19 +524,19 @@ func (j *Join) PrepareForHostingControlPlane(initConfiguration *kubeadmapi.InitC
// BootstrapKubelet executes the kubelet TLS bootstrap process.
// This process is executed by the kubelet and completes with the node joining the cluster
// with a dedicates set of credentials as required by the node authorizer
func (j *Join) BootstrapKubelet() error {
func (j *joinData) BootstrapKubelet(tlsBootstrapCfg *clientcmdapi.Config, initConfiguration *kubeadmapi.InitConfiguration) error {
bootstrapKubeConfigFile := kubeadmconstants.GetBootstrapKubeletKubeConfigPath()
// Write the bootstrap kubelet config file or the TLS-Boostrapped kubelet config file down to disk
klog.V(1).Infoln("[join] writing bootstrap kubelet config file at", bootstrapKubeConfigFile)
if err := kubeconfigutil.WriteToDisk(bootstrapKubeConfigFile, j.tlsBootstrapCfg); err != nil {
if err := kubeconfigutil.WriteToDisk(bootstrapKubeConfigFile, tlsBootstrapCfg); err != nil {
return errors.Wrap(err, "couldn't save bootstrap-kubelet.conf to disk")
}
// Write the ca certificate to disk so kubelet can use it for authentication
cluster := j.tlsBootstrapCfg.Contexts[j.tlsBootstrapCfg.CurrentContext].Cluster
cluster := tlsBootstrapCfg.Contexts[tlsBootstrapCfg.CurrentContext].Cluster
if _, err := os.Stat(j.cfg.CACertPath); os.IsNotExist(err) {
if err := certutil.WriteCert(j.cfg.CACertPath, j.tlsBootstrapCfg.Clusters[cluster].CertificateAuthorityData); err != nil {
if err := certutil.WriteCert(j.cfg.CACertPath, tlsBootstrapCfg.Clusters[cluster].CertificateAuthorityData); err != nil {
return errors.Wrap(err, "couldn't save the CA certificate to disk")
}
}
@ -512,7 +565,7 @@ func (j *Join) BootstrapKubelet() error {
// register the joining node with the specified taints if the node
// is not a master. The markmaster phase will register the taints otherwise.
registerTaintsUsingFlags := j.cfg.ControlPlane == nil
if err := kubeletphase.WriteKubeletDynamicEnvFile(j.initCfg, registerTaintsUsingFlags, kubeadmconstants.KubeletRunDirectory); err != nil {
if err := kubeletphase.WriteKubeletDynamicEnvFile(initConfiguration, registerTaintsUsingFlags, kubeadmconstants.KubeletRunDirectory); err != nil {
return err
}
@ -544,7 +597,7 @@ func (j *Join) BootstrapKubelet() error {
}
// PostInstallControlPlane marks the new node as master and update the cluster status with information about current node
func (j *Join) PostInstallControlPlane(initConfiguration *kubeadmapi.InitConfiguration) error {
func (j *joinData) PostInstallControlPlane(initConfiguration *kubeadmapi.InitConfiguration) error {
kubeConfigFile := filepath.Join(kubeadmconstants.KubernetesDir, kubeadmconstants.AdminKubeConfigFileName)
client, err := kubeconfigutil.ClientSetFromFile(kubeConfigFile)

View File

@ -17,42 +17,29 @@ limitations under the License.
package cmd
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"testing"
"k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme"
kubeadmapiv1beta1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta1"
"k8s.io/kubernetes/cmd/kubeadm/app/cmd/options"
)
const (
testConfig = `apiVersion: v1
clusters:
- cluster:
certificate-authority-data:
server: localhost:9008
name: prod
contexts:
- context:
cluster: prod
namespace: default
user: default-service-account
name: default
current-context: default
kind: Config
preferences: {}
users:
- name: kubernetes-admin
user:
client-certificate-data:
client-key-data:
testJoinConfig = `apiVersion: kubeadm.k8s.io/v1beta1
kind: JoinConfiguration
discovery:
bootstrapToken:
token: abcdef.0123456789abcdef
apiServerEndpoint: 1.2.3.4:6443
unsafeSkipCAVerification: true
nodeRegistration:
criSocket: /run/containerd/containerd.sock
name: someName
`
)
func TestNewValidJoin(t *testing.T) {
func TestNewJoinData(t *testing.T) {
// create temp directory
tmpDir, err := ioutil.TempDir("", "kubeadm-join-test")
if err != nil {
@ -67,110 +54,192 @@ func TestNewValidJoin(t *testing.T) {
t.Errorf("Unable to create file %q: %v", configFilePath, err)
}
defer cfgFile.Close()
if _, err = cfgFile.WriteString(testJoinConfig); err != nil {
t.Fatalf("Unable to write file %q: %v", configFilePath, err)
}
testCases := []struct {
name string
skipPreFlight bool
cfgPath string
configToWrite string
ignorePreflightErrors []string
testJoinValidate bool
testJoinRun bool
cmdPersistentFlags map[string]string
nodeConfig *kubeadm.JoinConfiguration
expectedError bool
name string
args []string
flags map[string]string
validate func(*testing.T, *joinData)
expectError bool
}{
// Join data passed using flags
{
name: "invalid: missing config file",
skipPreFlight: true,
cfgPath: "missing-path-to-a-config",
expectedError: true,
name: "fails if no discovery method set",
expectError: true,
},
{
name: "invalid: incorrect config file",
skipPreFlight: true,
cfgPath: configFilePath,
configToWrite: "bad-config-contents",
expectedError: true,
},
{
name: "invalid: fail at preflight.RunJoinNodeChecks()",
skipPreFlight: false,
cfgPath: configFilePath,
configToWrite: testConfig,
expectedError: true,
},
{
name: "invalid: incorrect ignorePreflight argument",
skipPreFlight: true,
cfgPath: configFilePath,
configToWrite: testConfig,
ignorePreflightErrors: []string{"some-unsupported-preflight-arg"},
expectedError: true,
},
{
name: "invalid: fail Join.Validate() with wrong flags",
skipPreFlight: true,
cfgPath: configFilePath,
configToWrite: testConfig,
testJoinValidate: true,
cmdPersistentFlags: map[string]string{
"config": "some-config",
"node-name": "some-node-name",
name: "fails if both file and bootstrap discovery methods set",
args: []string{"1.2.3.4:6443"},
flags: map[string]string{
options.FileDiscovery: "https://foo",
options.TokenDiscovery: "abcdef.0123456789abcdef",
options.TokenDiscoverySkipCAHash: "true",
},
expectedError: true,
expectError: true,
},
{
name: "invalid: fail Join.Validate() with wrong node configuration",
skipPreFlight: true,
cfgPath: configFilePath,
configToWrite: testConfig,
testJoinValidate: true,
expectedError: true,
name: "pass if file discovery is set",
flags: map[string]string{
options.FileDiscovery: "https://foo",
},
validate: func(t *testing.T, data *joinData) {
// validate that file discovery settings are set into join data
if data.cfg.Discovery.File == nil || data.cfg.Discovery.File.KubeConfigPath != "https://foo" {
t.Errorf("Invalid data.cfg.Discovery.File")
}
},
},
{
name: "invalid: fail Join.Run() with invalid node config",
skipPreFlight: true,
cfgPath: configFilePath,
configToWrite: testConfig,
testJoinRun: true,
expectedError: true,
name: "pass if bootstrap discovery is set",
args: []string{"1.2.3.4:6443", "5.6.7.8:6443"},
flags: map[string]string{
options.TokenDiscovery: "abcdef.0123456789abcdef",
options.TokenDiscoverySkipCAHash: "true",
},
validate: func(t *testing.T, data *joinData) {
// validate that bootstrap discovery settings are set into join data
if data.cfg.Discovery.BootstrapToken == nil ||
data.cfg.Discovery.BootstrapToken.APIServerEndpoint != "1.2.3.4:6443" || //only first arg should be kept as APIServerEndpoint
data.cfg.Discovery.BootstrapToken.Token != "abcdef.0123456789abcdef" ||
data.cfg.Discovery.BootstrapToken.UnsafeSkipCAVerification != true {
t.Errorf("Invalid data.cfg.Discovery.BootstrapToken")
}
},
},
{
name: "--token sets TLSBootstrapToken and BootstrapToken.Token if unset",
args: []string{"1.2.3.4:6443"},
flags: map[string]string{
options.TokenStr: "abcdef.0123456789abcdef",
options.TokenDiscoverySkipCAHash: "true",
},
validate: func(t *testing.T, data *joinData) {
// validate that token sets both TLSBootstrapToken and BootstrapToken.Token into join data
if data.cfg.Discovery.TLSBootstrapToken != "abcdef.0123456789abcdef" ||
data.cfg.Discovery.BootstrapToken == nil ||
data.cfg.Discovery.BootstrapToken.Token != "abcdef.0123456789abcdef" {
t.Errorf("Invalid TLSBootstrapToken or BootstrapToken.Token")
}
},
},
{
name: "--token doesn't override TLSBootstrapToken and BootstrapToken.Token if set",
args: []string{"1.2.3.4:6443"},
flags: map[string]string{
options.TokenStr: "aaaaaa.0123456789aaaaaa",
options.TLSBootstrapToken: "abcdef.0123456789abcdef",
options.TokenDiscovery: "defghi.0123456789defghi",
options.TokenDiscoverySkipCAHash: "true",
},
validate: func(t *testing.T, data *joinData) {
// validate that TLSBootstrapToken and BootstrapToken.Token values are preserved into join data
if data.cfg.Discovery.TLSBootstrapToken != "abcdef.0123456789abcdef" ||
data.cfg.Discovery.BootstrapToken == nil ||
data.cfg.Discovery.BootstrapToken.Token != "defghi.0123456789defghi" {
t.Errorf("Invalid TLSBootstrapToken or BootstrapToken.Token")
}
},
},
{
name: "control plane setting are preserved if --control-plane flag is set",
flags: map[string]string{
options.ControlPlane: "true",
options.APIServerAdvertiseAddress: "1.2.3.4",
options.APIServerBindPort: "1234",
options.FileDiscovery: "https://foo", //required only to pass discovery validation
},
validate: func(t *testing.T, data *joinData) {
// validate that control plane attributes are set in join data
if data.cfg.ControlPlane == nil ||
data.cfg.ControlPlane.LocalAPIEndpoint.AdvertiseAddress != "1.2.3.4" ||
data.cfg.ControlPlane.LocalAPIEndpoint.BindPort != 1234 {
t.Errorf("Invalid ControlPlane")
}
},
},
{
name: "control plane setting are cleaned up if --control-plane flag is not set",
flags: map[string]string{
options.ControlPlane: "false",
options.APIServerAdvertiseAddress: "1.2.3.4",
options.APIServerBindPort: "1.2.3.4",
options.FileDiscovery: "https://foo", //required only to pass discovery validation
},
validate: func(t *testing.T, data *joinData) {
// validate that control plane attributes are unset in join data
if data.cfg.ControlPlane != nil {
t.Errorf("Invalid ControlPlane")
}
},
},
{
name: "fails if invalid preflight checks are provided",
flags: map[string]string{
options.IgnorePreflightErrors: "all,something-else",
},
expectError: true,
},
// Join data passed using config file
{
name: "Pass with config from file",
flags: map[string]string{
options.CfgPath: configFilePath,
},
},
{
name: "--cri-socket and --node-name flags override config from file",
flags: map[string]string{
options.CfgPath: configFilePath,
options.NodeCRISocket: "/var/run/crio/crio.sock",
options.NodeName: "anotherName",
},
validate: func(t *testing.T, data *joinData) {
// validate that cri-socket and node-name are overwritten
if data.cfg.NodeRegistration.CRISocket != "/var/run/crio/crio.sock" {
t.Errorf("Invalid NodeRegistration.CRISocket")
}
if data.cfg.NodeRegistration.Name != "anotherName" {
t.Errorf("Invalid NodeRegistration.Name")
}
},
},
{
name: "fail if mixedArguments are passed",
flags: map[string]string{
options.CfgPath: configFilePath,
options.APIServerAdvertiseAddress: "1.2.3.4",
},
expectError: true,
},
}
var out bytes.Buffer
cfg := &kubeadmapiv1beta1.JoinConfiguration{}
kubeadmscheme.Scheme.Default(cfg)
errorFormat := "Test case %q: NewValidJoin expected error: %v, saw: %v, error: %v"
for _, tc := range testCases {
if _, err = cfgFile.WriteString(tc.configToWrite); err != nil {
t.Fatalf("Unable to write file %q: %v", tc.cfgPath, err)
}
t.Run(tc.name, func(t *testing.T) {
// initialize an external join option and inject it to the join cmd
joinOptions := newJoinOptions()
cmd := NewCmdJoin(nil, joinOptions)
cmd := NewCmdJoin(&out)
if tc.cmdPersistentFlags != nil {
for key, value := range tc.cmdPersistentFlags {
cmd.PersistentFlags().Set(key, value)
// sets cmd flags (that will be reflected on the join options)
for f, v := range tc.flags {
cmd.Flags().Set(f, v)
}
}
join, err := NewValidJoin(cmd.PersistentFlags(), cfg, tc.cfgPath, tc.ignorePreflightErrors)
if tc.nodeConfig != nil {
join.cfg = tc.nodeConfig
}
// test Join.Run()
if err == nil && tc.testJoinRun {
err = join.Run(&out)
if (err != nil) != tc.expectedError {
t.Fatalf(errorFormat, tc.name, tc.expectedError, (err != nil), err)
// test newJoinData method
data, err := newJoinData(cmd, tc.args, joinOptions, nil)
if err != nil && !tc.expectError {
t.Fatalf("newJoinData returned unexpected error: %v", err)
}
// check error for NewValidJoin()
} else if (err != nil) != tc.expectedError {
t.Fatalf(errorFormat, tc.name, tc.expectedError, (err != nil), err)
}
if err == nil && tc.expectError {
t.Fatalf("newJoinData didn't return error when expected")
}
// exec additional validation on the returned value
if tc.validate != nil {
tc.validate(t, &data)
}
})
}
}

View File

@ -85,7 +85,7 @@ const CSROnly = "csr-only"
// CSRDir flag sets the location for CSRs and flags to be output
const CSRDir = "csr-dir"
// TokenStr flag sets the token
// TokenStr flags sets both the discovery-token and the tls-bootstrap-token when those values are not provided
const TokenStr = "token"
// TokenTTL flag sets the time to live for token
@ -99,3 +99,21 @@ const TokenGroups = "groups"
// TokenDescription flag sets the description of the token
const TokenDescription = "description"
// TLSBootstrapToken flag sets the token used to temporarily authenticate with the Kubernetes Master to submit a certificate signing request (CSR) for a locally created key pair
const TLSBootstrapToken = "tls-bootstrap-token"
// TokenDiscovery flag sets the token used to validate cluster information fetched from the API server (for token-based discovery)
const TokenDiscovery = "discovery-token"
// TokenDiscoveryCAHash flag instruct kubeadm to validate that the root CA public key matches this hash (for token-based discovery)
const TokenDiscoveryCAHash = "discovery-token-ca-cert-hash"
// TokenDiscoverySkipCAHash flag instruct kubeadm to skip CA hash verification (for token-based discovery)
const TokenDiscoverySkipCAHash = "discovery-token-unsafe-skip-ca-verification"
// FileDiscovery flag sets the file or URL from which to load cluster information (for file-based discovery)
const FileDiscovery = "discovery-file"
// ControlPlane flag instruct kubeadm to create a new control plane instance on this node
const ControlPlane = "experimental-control-plane"