Skip to content

Commit 9d90184

Browse files
Support --write-flags on openshift start node
This acts like --write-config, but instead outputs the flags that the specified --config file would pass to the Kubelet. This prepares us to stop calling openshift start node and instead direct invoke the Kubelet. Remove the unsupported kubelet binary code because we are no longer calling ourself. Also move a small chunk of flag specialization code into a more appropriate place.
1 parent 0592af3 commit 9d90184

File tree

6 files changed

+111
-65
lines changed

6 files changed

+111
-65
lines changed

contrib/completions/bash/openshift

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

contrib/completions/zsh/openshift

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/cmd/server/api/validation/node.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ func ValidateNodeConfig(config *api.NodeConfig, fldPath *field.Path) ValidationR
2424
func ValidateInClusterNodeConfig(config *api.NodeConfig, fldPath *field.Path) ValidationResults {
2525
validationResults := ValidationResults{}
2626

27-
if len(config.NodeName) == 0 {
27+
hasBootstrapConfig := len(config.KubeletArguments["bootstrap-kubeconfig"]) > 0
28+
if len(config.NodeName) == 0 && !hasBootstrapConfig {
2829
validationResults.AddErrors(field.Required(fldPath.Child("nodeName"), ""))
2930
}
3031
if len(config.NodeIP) > 0 {
@@ -42,7 +43,9 @@ func ValidateInClusterNodeConfig(config *api.NodeConfig, fldPath *field.Path) Va
4243
validationResults.AddErrors(ValidateHostPort(config.DNSBindAddress, fldPath.Child("dnsBindAddress"))...)
4344
}
4445
if len(config.DNSIP) > 0 {
45-
validationResults.AddErrors(ValidateSpecifiedIP(config.DNSIP, fldPath.Child("dnsIP"))...)
46+
if !hasBootstrapConfig || config.DNSIP != "0.0.0.0" {
47+
validationResults.AddErrors(ValidateSpecifiedIP(config.DNSIP, fldPath.Child("dnsIP"))...)
48+
}
4649
}
4750
for i, nameserver := range config.DNSNameservers {
4851
validationResults.AddErrors(ValidateSpecifiedIPPort(nameserver, fldPath.Child("dnsNameservers").Index(i))...)

pkg/cmd/server/kubernetes/node/options/options.go

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -115,32 +115,23 @@ func ComputeKubeletFlagsAsMap(startingArgs map[string][]string, options configap
115115
}
116116
}
117117

118-
// we sometimes have different clusterdns options. I really don't understand why
119-
externalKubeClient, _, err := configapi.GetExternalKubeClient(options.MasterKubeConfig, options.MasterClientConnectionOverrides)
120-
if err != nil {
121-
return nil, err
122-
}
123-
args["cluster-dns"] = getClusterDNS(externalKubeClient, args["cluster-dns"])
124-
125-
return args, nil
126-
}
127-
128-
func KubeletArgsMapToArgs(argsMap map[string][]string) []string {
129-
args := []string{}
130-
for key, value := range argsMap {
131-
for _, token := range value {
132-
args = append(args, fmt.Sprintf("--%s=%v", key, token))
118+
// default cluster-dns to the master's DNS if possible, but only if we can reach the master
119+
// TODO: this exists to support legacy cases where the node defaulted to the master's DNS.
120+
// we can remove this when we drop support for master DNS when CoreDNS is in use everywhere.
121+
if len(args["cluster-dns"]) == 0 {
122+
if externalKubeClient, _, err := configapi.GetExternalKubeClient(options.MasterKubeConfig, options.MasterClientConnectionOverrides); err == nil {
123+
args["cluster-dns"] = getClusterDNS(externalKubeClient, args["cluster-dns"])
133124
}
134125
}
135126

136127
// there is a special case. If you set `--cgroups-per-qos=false` and `--enforce-node-allocatable` is
137128
// an empty string, `--enforce-node-allocatable=""` needs to be explicitly set
138129
// cgroups-per-qos defaults to true
139-
if cgroupArg, enforceAllocatable := argsMap["cgroups-per-qos"], argsMap["enforce-node-allocatable"]; len(cgroupArg) == 1 && cgroupArg[0] == "false" && len(enforceAllocatable) == 0 {
140-
args = append(args, `--enforce-node-allocatable=`)
130+
if cgroupArg, enforceAllocatable := args["cgroups-per-qos"], args["enforce-node-allocatable"]; len(cgroupArg) == 1 && cgroupArg[0] == "false" && len(enforceAllocatable) == 0 {
131+
args["enforce-node-allocatable"] = []string{""}
141132
}
142133

143-
return args
134+
return args, nil
144135
}
145136

146137
func setIfUnset(cmdLineArgs map[string][]string, key string, value ...string) {

pkg/cmd/server/start/node_args.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ type NodeArgs struct {
5252
// Components is the set of enabled components.
5353
Components *utilflags.ComponentFlag
5454

55+
// WriteFlagsOnly will print flags to run the Kubelet from the provided arguments rather than launching
56+
// the Kubelet itself.
57+
WriteFlagsOnly bool
58+
5559
// NodeName is the hostname to identify this node with the master.
5660
NodeName string
5761

pkg/cmd/server/start/start_node.go

Lines changed: 89 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import (
77
"os"
88
"os/exec"
99
"path/filepath"
10+
"regexp"
11+
"sort"
12+
"strconv"
1013
"strings"
1114
"syscall"
1215

@@ -91,6 +94,7 @@ func NewCommandStartNode(basename string, out, errout io.Writer) (*cobra.Command
9194
BindImageFormatArgs(options.NodeArgs.ImageFormatArgs, flags, "")
9295
BindKubeConnectionArgs(options.NodeArgs.KubeConnectionArgs, flags, "")
9396

97+
flags.BoolVar(&options.NodeArgs.WriteFlagsOnly, "write-flags", false, "When this is specified only the arguments necessary to start the Kubelet will be output.")
9498
flags.StringVar(&options.NodeArgs.BootstrapConfigName, "bootstrap-config-name", options.NodeArgs.BootstrapConfigName, "On startup, the node will request a client cert from the master and get its config from this config map in the openshift-node namespace (experimental).")
9599

96100
// autocompletion hints
@@ -172,12 +176,20 @@ func (o NodeOptions) Validate(args []string) error {
172176
if o.IsRunFromConfig() {
173177
return errors.New("--config may not be set if you're only writing the config")
174178
}
179+
if o.NodeArgs.WriteFlagsOnly {
180+
return errors.New("--write-config and --write-flags are mutually exclusive")
181+
}
175182
}
176183

177184
// if we are starting up using a config file, run no validations here
178-
if len(o.NodeArgs.BootstrapConfigName) > 0 && !o.IsRunFromConfig() {
179-
if err := o.NodeArgs.Validate(); err != nil {
180-
return err
185+
if len(o.NodeArgs.BootstrapConfigName) > 0 {
186+
if o.NodeArgs.WriteFlagsOnly {
187+
return errors.New("--write-flags is mutually exclusive with --bootstrap-config-name")
188+
}
189+
if !o.IsRunFromConfig() {
190+
if err := o.NodeArgs.Validate(); err != nil {
191+
return err
192+
}
181193
}
182194
}
183195

@@ -201,7 +213,7 @@ func (o NodeOptions) StartNode() error {
201213
return err
202214
}
203215

204-
if o.IsWriteConfigOnly() {
216+
if o.NodeArgs.WriteFlagsOnly || o.IsWriteConfigOnly() {
205217
return nil
206218
}
207219

@@ -224,6 +236,17 @@ func (o NodeOptions) RunNode() error {
224236
if addr := o.NodeArgs.ListenArg.ListenAddr; addr.Provided {
225237
nodeConfig.ServingInfo.BindAddress = addr.HostPort(o.NodeArgs.ListenArg.ListenAddr.DefaultPort)
226238
}
239+
// do a local resolution of node config DNS IP, supports bootstrapping cases
240+
if nodeConfig.DNSIP == "0.0.0.0" {
241+
glog.V(4).Infof("Defaulting to the DNSIP config to the node's IP")
242+
nodeConfig.DNSIP = nodeConfig.NodeIP
243+
// TODO: the Kubelet should do this defaulting (to the IP it recognizes)
244+
if len(nodeConfig.DNSIP) == 0 {
245+
if ip, err := cmdutil.DefaultLocalIP4(); err == nil {
246+
nodeConfig.DNSIP = ip.String()
247+
}
248+
}
249+
}
227250

228251
var validationResults validation.ValidationResults
229252
switch {
@@ -256,11 +279,11 @@ func (o NodeOptions) RunNode() error {
256279
return nil
257280
}
258281

259-
if err := StartNode(*nodeConfig, o.NodeArgs.Components); err != nil {
260-
return err
282+
if o.NodeArgs.WriteFlagsOnly {
283+
return WriteKubeletFlags(*nodeConfig)
261284
}
262285

263-
return nil
286+
return StartNode(*nodeConfig, o.NodeArgs.Components)
264287
}
265288

266289
// resolveNodeConfig creates a new configuration on disk by reading from the master, reads
@@ -371,41 +394,13 @@ func (o NodeOptions) IsRunFromConfig() bool {
371394
}
372395

373396
// execKubelet attempts to call execve() for the kubelet with the configuration defined
374-
// in server passed as flags. If the binary is not the same as the current file and
375-
// the environment variable OPENSHIFT_ALLOW_UNSUPPORTED_KUBELET is unset, the method
376-
// will return an error. The returned boolean indicates whether fallback to in-process
377-
// is allowed.
378-
func execKubelet(kubeletArgs []string) (bool, error) {
379-
// verify the Kubelet binary to use
397+
// in server passed as flags.
398+
func execKubelet(kubeletArgs []string) error {
380399
path := "kubelet"
381-
requireSameBinary := true
382-
if newPath := os.Getenv("OPENSHIFT_ALLOW_UNSUPPORTED_KUBELET"); len(newPath) > 0 {
383-
requireSameBinary = false
384-
path = newPath
385-
}
386400
kubeletPath, err := exec.LookPath(path)
387401
if err != nil {
388-
return requireSameBinary, err
389-
}
390-
kubeletFile, err := os.Stat(kubeletPath)
391-
if err != nil {
392-
return requireSameBinary, err
393-
}
394-
thisPath, err := exec.LookPath(os.Args[0])
395-
if err != nil {
396-
return true, err
397-
}
398-
thisFile, err := os.Stat(thisPath)
399-
if err != nil {
400-
return true, err
401-
}
402-
if !os.SameFile(thisFile, kubeletFile) {
403-
if requireSameBinary {
404-
return true, fmt.Errorf("binary at %q is not the same file as %q, cannot execute", thisPath, kubeletPath)
405-
}
406-
glog.Warningf("UNSUPPORTED: Executing a different Kubelet than the current binary is not supported: %s", kubeletPath)
402+
return err
407403
}
408-
409404
// convert current settings to flags
410405
args := append([]string{kubeletPath}, kubeletArgs...)
411406
for i := glog.Level(10); i > 0; i-- {
@@ -426,28 +421,77 @@ func execKubelet(kubeletArgs []string) (bool, error) {
426421
break
427422
}
428423
}
424+
// execve the child process, replacing this process
429425
glog.V(3).Infof("Exec %s %s", kubeletPath, strings.Join(args, " "))
430-
return false, syscall.Exec(kubeletPath, args, os.Environ())
426+
return syscall.Exec(kubeletPath, args, os.Environ())
431427
}
432428

429+
// safeArgRegexp matches only characters that are known safe. DO NOT add to this list
430+
// without fully considering whether that new character can be used to break shell escaping
431+
// rules.
432+
var safeArgRegexp = regexp.MustCompile(`^[\da-zA-Z\-=_\.,/\:]+$`)
433+
434+
// shellEscapeArg quotes an argument if it contains characters that my cause a shell
435+
// interpreter to split the single argument into multiple.
436+
func shellEscapeArg(s string) string {
437+
if safeArgRegexp.MatchString(s) {
438+
return s
439+
}
440+
return strconv.Quote(s)
441+
}
442+
443+
// argsMapToArgs writes a sorted list of arguments for flags. It does not escape the
444+
// flag values.
445+
func argsMapToArgs(argsMap map[string][]string) []string {
446+
var keys []string
447+
for key := range argsMap {
448+
keys = append(keys, key)
449+
}
450+
sort.Strings(keys)
451+
452+
var args []string
453+
for _, key := range keys {
454+
for _, token := range argsMap[key] {
455+
args = append(args, fmt.Sprintf("--%s=%v", key, token))
456+
}
457+
}
458+
return args
459+
}
460+
461+
// WriteKubeletFlags writes the correct set of flags to start a Kubelet from the provided node config to
462+
// stdout, instead of launching anything.
463+
func WriteKubeletFlags(nodeConfig configapi.NodeConfig) error {
464+
kubeletFlagsAsMap, err := nodeoptions.ComputeKubeletFlagsAsMap(nodeConfig.KubeletArguments, nodeConfig)
465+
if err != nil {
466+
return fmt.Errorf("cannot create kubelet args: %v", err)
467+
}
468+
kubeletArgs := argsMapToArgs(kubeletFlagsAsMap)
469+
if err := nodeoptions.CheckFlags(kubeletArgs); err != nil {
470+
return err
471+
}
472+
var outputArgs []string
473+
for _, s := range kubeletArgs {
474+
outputArgs = append(outputArgs, shellEscapeArg(s))
475+
}
476+
fmt.Println(strings.Join(outputArgs, " "))
477+
return nil
478+
}
479+
480+
// StartNode launches the node processes.
433481
func StartNode(nodeConfig configapi.NodeConfig, components *utilflags.ComponentFlag) error {
434482
kubeletFlagsAsMap, err := nodeoptions.ComputeKubeletFlagsAsMap(nodeConfig.KubeletArguments, nodeConfig)
435483
if err != nil {
436484
return fmt.Errorf("cannot create kubelet args: %v", err)
437485
}
438-
kubeletArgs := nodeoptions.KubeletArgsMapToArgs(kubeletFlagsAsMap)
486+
kubeletArgs := argsMapToArgs(kubeletFlagsAsMap)
439487
if err := nodeoptions.CheckFlags(kubeletArgs); err != nil {
440488
return err
441489
}
442490

443491
// as a step towards decomposing OpenShift into Kubernetes components, perform an execve
444492
// to launch the Kubelet instead of loading in-process
445493
if components.Calculated().Equal(sets.NewString(ComponentKubelet)) {
446-
ok, err := execKubelet(kubeletArgs)
447-
if !ok {
448-
return err
449-
}
450-
if err != nil {
494+
if err := execKubelet(kubeletArgs); err != nil {
451495
utilruntime.HandleError(fmt.Errorf("Unable to call exec on kubelet, continuing with normal startup: %v", err))
452496
}
453497
}

0 commit comments

Comments
 (0)