From e1dd6b297c9e90abe19622c24447a25d421f3204 Mon Sep 17 00:00:00 2001 From: Joe Beda Date: Fri, 23 Jan 2015 14:09:52 -0800 Subject: [PATCH] Create new hyperkube package. This is the start of an uber-binary that can morph into any server. Eventually we'll want this to be able to launch multiple servers from a single command line. --- pkg/hyperkube/doc.go | 30 ++++++ pkg/hyperkube/hyperkube.go | 184 ++++++++++++++++++++++++++++++++ pkg/hyperkube/hyperkube_test.go | 143 +++++++++++++++++++++++++ pkg/hyperkube/server.go | 73 +++++++++++++ pkg/util/pflag_import.go | 4 +- pkg/util/template.go | 48 +++++++++ pkg/util/template_test.go | 61 +++++++++++ 7 files changed, 542 insertions(+), 1 deletion(-) create mode 100644 pkg/hyperkube/doc.go create mode 100644 pkg/hyperkube/hyperkube.go create mode 100644 pkg/hyperkube/hyperkube_test.go create mode 100644 pkg/hyperkube/server.go create mode 100644 pkg/util/template.go create mode 100644 pkg/util/template_test.go diff --git a/pkg/hyperkube/doc.go b/pkg/hyperkube/doc.go new file mode 100644 index 00000000000..dd8ad4095da --- /dev/null +++ b/pkg/hyperkube/doc.go @@ -0,0 +1,30 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package hyperkube is a framework for kubernetes server components. It +// allows us to combine all of the kubernetes server components into a single +// binary where the user selects which components to run in any individual +// process. +// +// Currently, only one server component can be run at once. As such there is +// no need to harmonize flags or identify logs across the various servers. In +// the future we will support launching and running many servers -- either by +// managing processes or running in-proc. +// +// This package is inspired by https://github.com/spf13/cobra. However, as +// the eventual goal is to run *multiple* servers from one call, a new package +// was needed. +package hyperkube diff --git a/pkg/hyperkube/hyperkube.go b/pkg/hyperkube/hyperkube.go new file mode 100644 index 00000000000..accfe7176be --- /dev/null +++ b/pkg/hyperkube/hyperkube.go @@ -0,0 +1,184 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package hyperkube + +import ( + "errors" + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "path" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + + "github.com/spf13/pflag" +) + +// HyperKube represents a single binary that can morph/manage into multiple +// servers. +type HyperKube struct { + Name string // The executable name, used for help and soft-link invocation + Long string // A long description of the binary. It will be world wrapped before output. + + servers []Server + baseFlags *pflag.FlagSet + out io.Writer + helpFlagVal bool +} + +// AddServer adds a server to the HyperKube object. +func (hk *HyperKube) AddServer(s *Server) { + hk.servers = append(hk.servers, *s) + hk.servers[len(hk.servers)-1].hk = hk +} + +// FindServer will find a specific server named name. +func (hk *HyperKube) FindServer(name string) (*Server, error) { + for _, s := range hk.servers { + if s.Name() == name { + return &s, nil + } + } + return nil, fmt.Errorf("Server not found: %s", name) +} + +// Servers returns a list of all of the registred servers +func (hk *HyperKube) Servers() []Server { + return hk.servers +} + +// Flags returns a flagset for "global" flags. +func (hk *HyperKube) Flags() *pflag.FlagSet { + if hk.baseFlags == nil { + hk.baseFlags = pflag.NewFlagSet(hk.Name, pflag.ContinueOnError) + hk.baseFlags.SetOutput(ioutil.Discard) + hk.baseFlags.BoolVarP(&hk.helpFlagVal, "help", "h", false, "help for "+hk.Name) + + // These will add all of the "global" flags (defined with both the + // flag and pflag packages) to the new flag set we have. + util.AddFlagSetToPFlagSet(flag.CommandLine, hk.baseFlags) + util.AddPFlagSetToPFlagSet(pflag.CommandLine, hk.baseFlags) + + } + return hk.baseFlags +} + +// Out returns the io.Writer that is used for all usage/error information +func (hk *HyperKube) Out() io.Writer { + if hk.out == nil { + hk.out = os.Stderr + } + return hk.out +} + +// SetOut sets the output writer for all usage/error information +func (hk *HyperKube) SetOut(w io.Writer) { + hk.out = w +} + +// Print is a convenience method to Print to the defined output +func (hk *HyperKube) Print(i ...interface{}) { + fmt.Fprint(hk.Out(), i...) +} + +// Println is a convenience method to Println to the defined output +func (hk *HyperKube) Println(i ...interface{}) { + str := fmt.Sprintln(i...) + hk.Print(str) +} + +// Printf is a convenience method to Printf to the defined output +func (hk *HyperKube) Printf(format string, i ...interface{}) { + str := fmt.Sprintf(format, i...) + hk.Print(str) +} + +// Run the server. This will pick the appropriate server and run it. +func (hk *HyperKube) Run(args []string) error { + // If we are called directly, parse all flags up to the first real + // argument. That should be the server to run. + baseCommand := path.Base(args[0]) + serverName := baseCommand + if serverName == hk.Name { + args = args[1:] + + baseFlags := hk.Flags() + baseFlags.SetInterspersed(false) // Only parse flags up to the next real command + err := baseFlags.Parse(args) + if err != nil || hk.helpFlagVal { + if err != nil { + hk.Println("Error:", err) + } + hk.Usage() + return err + } + args = baseFlags.Args() + if len(args) > 0 && len(args[0]) > 0 { + serverName = args[0] + baseCommand = baseCommand + " " + serverName + args = args[1:] + } else { + err = errors.New("No server specified") + hk.Println("Error", err) + hk.Usage() + return err + } + } + + s, err := hk.FindServer(serverName) + if err != nil { + hk.Println("Error:", err) + hk.Usage() + return err + } + + util.AddPFlagSetToPFlagSet(hk.Flags(), s.Flags()) + err = s.Flags().Parse(args) + if err != nil || hk.helpFlagVal { + if err != nil { + hk.Println("Error:", err) + } + s.Usage() + return err + } + + util.InitLogs() + err = s.Run(s, s.Flags().Args()) + if err != nil { + hk.Println("Error:", err) + } + + return err +} + +// Usage will write out a summary for all servers that this binary supports. +func (hk *HyperKube) Usage() { + tt := `{{if .Long}}{{.Long | trim | wrap ""}} +{{end}}Usage + + {{.Name}} [flags] + +Servers +{{range .Servers}} + {{.Name}} +{{.Long | trim | wrap " "}}{{end}} +Call '{{.Name}} --help' for help on a specific server. +` + util.ExecuteTemplate(hk.Out(), tt, hk) +} diff --git a/pkg/hyperkube/hyperkube_test.go b/pkg/hyperkube/hyperkube_test.go new file mode 100644 index 00000000000..c88336c0537 --- /dev/null +++ b/pkg/hyperkube/hyperkube_test.go @@ -0,0 +1,143 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package hyperkube + +import ( + "bytes" + "errors" + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +type result struct { + err error + output string +} + +func testServer(n string) *Server { + return &Server{ + SimpleUsage: n, + Long: fmt.Sprintf("A simple server named %s", n), + Run: func(s *Server, args []string) error { + s.hk.Printf("%s Run\n", s.Name()) + return nil + }, + } +} +func testServerError(n string) *Server { + return &Server{ + SimpleUsage: n, + Long: fmt.Sprintf("A simple server named %s that returns an error", n), + Run: func(s *Server, args []string) error { + s.hk.Printf("%s Run\n", s.Name()) + return errors.New("Server returning error") + }, + } +} + +func runFull(t *testing.T, args string) *result { + buf := new(bytes.Buffer) + hk := HyperKube{ + Name: "hyperkube", + Long: "hyperkube is an all-in-one server binary.", + } + hk.SetOut(buf) + + hk.AddServer(testServer("test1")) + hk.AddServer(testServer("test2")) + hk.AddServer(testServer("test3")) + hk.AddServer(testServerError("test-error")) + + a := strings.Split(args, " ") + t.Logf("Running full with args: %q", a) + err := hk.Run(a) + + r := &result{err, buf.String()} + t.Logf("Result err: %v, output: %q", r.err, r.output) + + return r +} + +func TestRun(t *testing.T) { + x := runFull(t, "hyperkube test1") + assert.Contains(t, x.output, "test1 Run") + assert.NoError(t, x.err) +} + +func TestLinkRun(t *testing.T) { + x := runFull(t, "test1") + assert.Contains(t, x.output, "test1 Run") + assert.NoError(t, x.err) +} + +func TestTopNoArgs(t *testing.T) { + x := runFull(t, "hyperkube") + assert.EqualError(t, x.err, "No server specified") +} + +func TestBadServer(t *testing.T) { + x := runFull(t, "hyperkube bad-server") + assert.EqualError(t, x.err, "Server not found: bad-server") + assert.Contains(t, x.output, "Usage") +} + +func TestTopHelp(t *testing.T) { + x := runFull(t, "hyperkube --help") + assert.NoError(t, x.err) + assert.Contains(t, x.output, "all-in-one") + assert.Contains(t, x.output, "A simple server named test1") +} + +func TestTopFlags(t *testing.T) { + x := runFull(t, "hyperkube --help test1") + assert.NoError(t, x.err) + assert.Contains(t, x.output, "all-in-one") + assert.Contains(t, x.output, "A simple server named test1") + assert.NotContains(t, x.output, "test1 Run") +} + +func TestTopFlagsBad(t *testing.T) { + x := runFull(t, "hyperkube --bad-flag") + assert.EqualError(t, x.err, "unknown flag: --bad-flag") + assert.Contains(t, x.output, "all-in-one") + assert.Contains(t, x.output, "A simple server named test1") +} + +func TestServerHelp(t *testing.T) { + x := runFull(t, "hyperkube test1 --help") + assert.NoError(t, x.err) + assert.Contains(t, x.output, "A simple server named test1") + assert.Contains(t, x.output, "--help=false: help for hyperkube") + assert.NotContains(t, x.output, "test1 Run") +} + +func TestServerFlagsBad(t *testing.T) { + x := runFull(t, "hyperkube test1 --bad-flag") + assert.EqualError(t, x.err, "unknown flag: --bad-flag") + assert.Contains(t, x.output, "A simple server named test1") + assert.Contains(t, x.output, "--help=false: help for hyperkube") + assert.NotContains(t, x.output, "test1 Run") +} + +func TestServerError(t *testing.T) { + x := runFull(t, "hyperkube test-error") + assert.Contains(t, x.output, "test-error Run") + assert.EqualError(t, x.err, "Server returning error") +} diff --git a/pkg/hyperkube/server.go b/pkg/hyperkube/server.go new file mode 100644 index 00000000000..706df1e1921 --- /dev/null +++ b/pkg/hyperkube/server.go @@ -0,0 +1,73 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package hyperkube + +import ( + "io/ioutil" + "strings" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + + "github.com/spf13/pflag" +) + +type serverRunFunc func(s *Server, args []string) error + +// Server describes a server that this binary can morph into. +type Server struct { + SimpleUsage string // One line description of the server. + Long string // Longer free form description of the server + Run serverRunFunc // Run the server. This is not expected to return. + + flags *pflag.FlagSet // Flags for the command (and all dependents) + name string + hk *HyperKube +} + +// FullUsage returns the full usage string including all of the flags. +func (s *Server) Usage() error { + tt := `{{if .Long}}{{.Long | trim | wrap ""}} +{{end}}Usage: + {{.SimpleUsage}} [flags] + +Available Flags: +{{.Flags.FlagUsages}}` + + return util.ExecuteTemplate(s.hk.Out(), tt, s) +} + +// Name returns the name of the command as derived from the usage line. +func (s *Server) Name() string { + if s.name != "" { + return s.name + } + name := s.SimpleUsage + i := strings.Index(name, " ") + if i >= 0 { + name = name[:i] + } + return name +} + +// Flags returns a flagset for this server +func (s *Server) Flags() *pflag.FlagSet { + if s.flags == nil { + s.flags = pflag.NewFlagSet(s.Name(), pflag.ContinueOnError) + s.flags.SetOutput(ioutil.Discard) + } + return s.flags +} diff --git a/pkg/util/pflag_import.go b/pkg/util/pflag_import.go index e45d88ef23c..fcef3175de0 100644 --- a/pkg/util/pflag_import.go +++ b/pkg/util/pflag_import.go @@ -80,7 +80,9 @@ func (v *flagValueWrapper) IsBoolFlag() bool { // Imports a 'flag.Flag' into a 'pflag.FlagSet'. The "short" option is unset // and the type is inferred using reflection. func AddFlagToPFlagSet(f *flag.Flag, fs *pflag.FlagSet) { - fs.Var(wrapFlagValue(f.Value), f.Name, f.Usage) + if fs.Lookup(f.Name) == nil { + fs.Var(wrapFlagValue(f.Value), f.Name, f.Usage) + } } // Adds all of the flags in a 'flag.FlagSet' package flags to a 'pflag.FlagSet'. diff --git a/pkg/util/template.go b/pkg/util/template.go new file mode 100644 index 00000000000..f79b817c479 --- /dev/null +++ b/pkg/util/template.go @@ -0,0 +1,48 @@ +/* +Copyright 2015 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "bytes" + "go/doc" + "io" + "strings" + "text/template" +) + +func wrap(indent string, s string) string { + var buf bytes.Buffer + doc.ToText(&buf, s, indent, indent+" ", 80-len(indent)) + return buf.String() +} + +// ExecuteTemplate executes templateText with data and output written to w. +func ExecuteTemplate(w io.Writer, templateText string, data interface{}) error { + t := template.New("top") + t.Funcs(template.FuncMap{ + "trim": strings.TrimSpace, + "wrap": wrap, + }) + template.Must(t.Parse(templateText)) + return t.Execute(w, data) +} + +func ExecuteTemplateToString(templateText string, data interface{}) (string, error) { + b := bytes.Buffer{} + err := ExecuteTemplate(&b, templateText, data) + return b.String(), err +} diff --git a/pkg/util/template_test.go b/pkg/util/template_test.go new file mode 100644 index 00000000000..707ee2f7926 --- /dev/null +++ b/pkg/util/template_test.go @@ -0,0 +1,61 @@ +/* +Copyright 2015 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestWrap(t *testing.T) { + tt := `before ->{{.Long | wrap "**"}}<- after` + data := struct { + Long string + }{ + `Hodor, hodor; hodor hodor; +hodor hodor... Hodor hodor hodor? Hodor. Hodor hodor hodor hodor... +Hodor hodor hodor; hodor hodor hodor! Hodor, hodor. Hodor. Hodor, +HODOR hodor, hodor hodor; hodor hodor; hodor HODOR hodor, hodor hodor? +Hodor. Hodor hodor - hodor hodor. Hodor hodor HODOR! Hodor hodor - hodor... +Hodor hodor HODOR hodor, hodor hodor hodor! Hodor, hodor... Hodor hodor +hodor hodor hodor hodor! Hodor, hodor; hodor hodor. Hodor.`, + } + output, _ := ExecuteTemplateToString(tt, data) + t.Logf("%q", output) + + assert.Equal(t, `before ->**Hodor, hodor; hodor hodor; hodor hodor... Hodor hodor hodor? Hodor. Hodor +**hodor hodor hodor... Hodor hodor hodor; hodor hodor hodor! Hodor, hodor. +**Hodor. Hodor, HODOR hodor, hodor hodor; hodor hodor; hodor HODOR hodor, hodor +**hodor? Hodor. Hodor hodor - hodor hodor. Hodor hodor HODOR! Hodor hodor - +**hodor... Hodor hodor HODOR hodor, hodor hodor hodor! Hodor, hodor... Hodor +**hodor hodor hodor hodor hodor! Hodor, hodor; hodor hodor. Hodor. +<- after`, output) +} + +func TestTrim(t *testing.T) { + tt := `before ->{{.Messy | trim }}<- after` + data := struct { + Messy string + }{ + "\t stuff\n \r ", + } + output, _ := ExecuteTemplateToString(tt, data) + t.Logf("%q", output) + + assert.Equal(t, `before ->stuff<- after`, output) +}