channels: support targeting kubernetes versions

This commit is contained in:
Justin Santa Barbara 2017-04-06 15:43:38 -04:00
parent 343217eb8a
commit a7c2c554e1
14 changed files with 424 additions and 146 deletions

View File

@ -111,7 +111,8 @@ test:
go test k8s.io/kops/protokube/... -args -v=1 -logtostderr go test k8s.io/kops/protokube/... -args -v=1 -logtostderr
go test k8s.io/kops/dns-controller/pkg/... -args -v=1 -logtostderr go test k8s.io/kops/dns-controller/pkg/... -args -v=1 -logtostderr
go test k8s.io/kops/cmd/... -args -v=1 -logtostderr go test k8s.io/kops/cmd/... -args -v=1 -logtostderr
go test k8s.io/kops/tests/... -args -v=1 -logtostderr go test k8s.io/kops/cmd/... -args -v=1 -logtostderr
go test k8s.io/kops/channels/... -args -v=1 -logtostderr
go test k8s.io/kops/util/... -args -v=1 -logtostderr go test k8s.io/kops/util/... -args -v=1 -logtostderr
crossbuild-nodeup: crossbuild-nodeup:

View File

@ -18,16 +18,14 @@ package main
import ( import (
"fmt" "fmt"
"k8s.io/kops/channels/pkg/cmd"
"os" "os"
) )
func main() { func main() {
Execute() f := &cmd.DefaultFactory{}
} if err := cmd.Execute(f, os.Stdout); err != nil {
// exitWithError will terminate execution with an error result
// It prints the error to stderr and exits with a non-zero exit code
func exitWithError(err error) {
fmt.Fprintf(os.Stderr, "\n%v\n", err) fmt.Fprintf(os.Stderr, "\n%v\n", err)
os.Exit(1) os.Exit(1)
} }
}

View File

@ -47,4 +47,15 @@ type AddonSpec struct {
// Manifest is the URL to the manifest that should be applied // Manifest is the URL to the manifest that should be applied
Manifest *string `json:"manifest,omitempty"` Manifest *string `json:"manifest,omitempty"`
// KubernetesVersion is a semver version range on which this version of the addon can be applied
KubernetesVersion string `json:"kubernetesVersion,omitempty"`
// Id is an optional value which can be used to force a refresh even if the Version matches
// This is useful for when we have two manifests expressing the same addon version for two
// different kubernetes api versions. For example, we might label the 1.5 version "k8s-1.5"
// and the 1.6 version "k8s-1.6". Both would have the same Version, determined by the
// version of the software we are packaging. But we always want to reinstall when we
// switch kubernetes versions.
Id string `json:"id,omitempty"`
} }

View File

@ -25,6 +25,7 @@ import (
"net/url" "net/url"
) )
// Addon is a wrapper around a single version of an addon
type Addon struct { type Addon struct {
Name string Name string
ChannelName string ChannelName string
@ -32,16 +33,42 @@ type Addon struct {
Spec *api.AddonSpec Spec *api.AddonSpec
} }
// AddonUpdate holds data about a proposed update to an addon
type AddonUpdate struct { type AddonUpdate struct {
Name string Name string
ExistingVersion *ChannelVersion ExistingVersion *ChannelVersion
NewVersion *ChannelVersion NewVersion *ChannelVersion
} }
// AddonMenu is a collection of addons, with helpers for computing the latest versions
type AddonMenu struct {
Addons map[string]*Addon
}
func NewAddonMenu() *AddonMenu {
return &AddonMenu{
Addons: make(map[string]*Addon),
}
}
func (m *AddonMenu) Merge(o *AddonMenu) {
for k, v := range o.Addons {
existing := m.Addons[k]
if existing == nil {
m.Addons[k] = v
} else {
if existing.ChannelVersion().replaces(v.ChannelVersion()) {
m.Addons[k] = v
}
}
}
}
func (a *Addon) ChannelVersion() *ChannelVersion { func (a *Addon) ChannelVersion() *ChannelVersion {
return &ChannelVersion{ return &ChannelVersion{
Channel: &a.ChannelName, Channel: &a.ChannelName,
Version: a.Spec.Version, Version: a.Spec.Version,
Id: a.Spec.Id,
} }
} }
@ -67,7 +94,7 @@ func (a *Addon) GetRequiredUpdates(k8sClient kubernetes.Interface) (*AddonUpdate
return nil, err return nil, err
} }
if existingVersion != nil && !newVersion.Replaces(existingVersion) { if existingVersion != nil && !newVersion.replaces(existingVersion) {
return nil, nil return nil, nil
} }

View File

@ -18,6 +18,7 @@ package channels
import ( import (
"fmt" "fmt"
"github.com/blang/semver"
"github.com/golang/glog" "github.com/golang/glog"
"k8s.io/kops/channels/pkg/api" "k8s.io/kops/channels/pkg/api"
"k8s.io/kops/upup/pkg/fi/utils" "k8s.io/kops/upup/pkg/fi/utils"
@ -58,28 +59,29 @@ func ParseAddons(name string, location *url.URL, data []byte) (*Addons, error) {
return &Addons{ChannelName: name, ChannelLocation: *location, APIObject: apiObject}, nil return &Addons{ChannelName: name, ChannelLocation: *location, APIObject: apiObject}, nil
} }
func (a *Addons) GetCurrent() ([]*Addon, error) { func (a *Addons) GetCurrent(kubernetesVersion semver.Version) (*AddonMenu, error) {
all, err := a.All() all, err := a.wrapInAddons()
if err != nil { if err != nil {
return nil, err return nil, err
} }
specs := make(map[string]*Addon)
menu := NewAddonMenu()
for _, addon := range all { for _, addon := range all {
if !addon.matches(kubernetesVersion) {
continue
}
name := addon.Name name := addon.Name
existing := specs[name]
if existing == nil || addon.ChannelVersion().Replaces(existing.ChannelVersion()) { existing := menu.Addons[name]
specs[name] = addon if existing == nil || addon.ChannelVersion().replaces(existing.ChannelVersion()) {
menu.Addons[name] = addon
} }
} }
var addons []*Addon return menu, nil
for _, addon := range specs {
addons = append(addons, addon)
}
return addons, nil
} }
func (a *Addons) All() ([]*Addon, error) { func (a *Addons) wrapInAddons() ([]*Addon, error) {
var addons []*Addon var addons []*Addon
for _, s := range a.APIObject.Spec.Addons { for _, s := range a.APIObject.Spec.Addons {
name := a.APIObject.ObjectMeta.Name name := a.APIObject.ObjectMeta.Name
@ -98,3 +100,19 @@ func (a *Addons) All() ([]*Addon, error) {
} }
return addons, nil return addons, nil
} }
func (s *Addon) matches(kubernetesVersion semver.Version) bool {
if s.Spec.KubernetesVersion != "" {
versionRange, err := semver.ParseRange(s.Spec.KubernetesVersion)
if err != nil {
glog.Warningf("unable to parse KubernetesVersion %q; skipping", s.Spec.KubernetesVersion)
return false
}
if !versionRange(kubernetesVersion) {
glog.V(4).Infof("Skipping version range %q that does not match current version %s", s.Spec.KubernetesVersion, kubernetesVersion)
return false
}
}
return true
}

View File

@ -0,0 +1,154 @@
/*
Copyright 2016 The Kubernetes Authors.
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 channels
import (
"github.com/blang/semver"
"k8s.io/kops/channels/pkg/api"
"testing"
)
func Test_Filtering(t *testing.T) {
grid := []struct {
Input api.AddonSpec
KubernetesVersion string
Expected bool
}{
{
Input: api.AddonSpec{
KubernetesVersion: ">=1.6.0",
},
KubernetesVersion: "1.6.0",
Expected: true,
},
{
Input: api.AddonSpec{
KubernetesVersion: "<1.6.0",
},
KubernetesVersion: "1.6.0",
Expected: false,
},
{
Input: api.AddonSpec{
KubernetesVersion: ">=1.6.0",
},
KubernetesVersion: "1.5.9",
Expected: false,
},
{
Input: api.AddonSpec{
KubernetesVersion: ">=1.4.0 <1.6.0",
},
KubernetesVersion: "1.5.9",
Expected: true,
},
{
Input: api.AddonSpec{
KubernetesVersion: ">=1.4.0 <1.6.0",
},
KubernetesVersion: "1.6.0",
Expected: false,
},
}
for _, g := range grid {
k8sVersion := semver.MustParse(g.KubernetesVersion)
addon := &Addon{
Spec: &g.Input,
}
actual := addon.matches(k8sVersion)
if actual != g.Expected {
t.Errorf("unexpected result from %v, %s. got %v", g.Input.KubernetesVersion, g.KubernetesVersion, actual)
}
}
}
func Test_Replacement(t *testing.T) {
grid := []struct {
Old *ChannelVersion
New *ChannelVersion
Replaces bool
}{
// With no id, update iff newer semver
{
Old: &ChannelVersion{Version: s("1.0.0"), Id: ""},
New: &ChannelVersion{Version: s("1.0.0"), Id: ""},
Replaces: false,
},
{
Old: &ChannelVersion{Version: s("1.0.0"), Id: ""},
New: &ChannelVersion{Version: s("1.0.1"), Id: ""},
Replaces: true,
},
{
Old: &ChannelVersion{Version: s("1.0.1"), Id: ""},
New: &ChannelVersion{Version: s("1.0.0"), Id: ""},
Replaces: false,
},
{
Old: &ChannelVersion{Version: s("1.1.0"), Id: ""},
New: &ChannelVersion{Version: s("1.1.1"), Id: ""},
Replaces: true,
},
{
Old: &ChannelVersion{Version: s("1.1.1"), Id: ""},
New: &ChannelVersion{Version: s("1.1.0"), Id: ""},
Replaces: false,
},
// With id, update if different id and same version, otherwise follow semver
{
Old: &ChannelVersion{Version: s("1.0.0"), Id: "a"},
New: &ChannelVersion{Version: s("1.0.0"), Id: "a"},
Replaces: false,
},
{
Old: &ChannelVersion{Version: s("1.0.0"), Id: "a"},
New: &ChannelVersion{Version: s("1.0.0"), Id: "b"},
Replaces: true,
},
{
Old: &ChannelVersion{Version: s("1.0.0"), Id: "b"},
New: &ChannelVersion{Version: s("1.0.0"), Id: "a"},
Replaces: true,
},
{
Old: &ChannelVersion{Version: s("1.0.0"), Id: "a"},
New: &ChannelVersion{Version: s("1.0.1"), Id: "a"},
Replaces: true,
},
{
Old: &ChannelVersion{Version: s("1.0.0"), Id: "a"},
New: &ChannelVersion{Version: s("1.0.1"), Id: "a"},
Replaces: true,
},
{
Old: &ChannelVersion{Version: s("1.0.0"), Id: "a"},
New: &ChannelVersion{Version: s("1.0.1"), Id: "a"},
Replaces: true,
},
}
for _, g := range grid {
actual := g.New.replaces(g.Old)
if actual != g.Replaces {
t.Errorf("unexpected result from %v -> %v, expect %t. actual %v", g.Old, g.New, g.Replaces, actual)
}
}
}
func s(v string) *string {
return &v
}

View File

@ -38,6 +38,7 @@ type Channel struct {
type ChannelVersion struct { type ChannelVersion struct {
Version *string `json:"version,omitempty"` Version *string `json:"version,omitempty"`
Channel *string `json:"channel,omitempty"` Channel *string `json:"channel,omitempty"`
Id string `json:"id,omitempty"`
} }
func stringValue(s *string) string { func stringValue(s *string) string {
@ -48,7 +49,11 @@ func stringValue(s *string) string {
} }
func (c *ChannelVersion) String() string { func (c *ChannelVersion) String() string {
return "Version=" + stringValue(c.Version) + " Channel=" + stringValue(c.Channel) s := "Version=" + stringValue(c.Version) + " Channel=" + stringValue(c.Channel)
if c.Id != "" {
s += " Id=" + c.Id
}
return s
} }
func ParseChannelVersion(s string) (*ChannelVersion, error) { func ParseChannelVersion(s string) (*ChannelVersion, error) {
@ -91,7 +96,7 @@ func (c *Channel) AnnotationName() string {
return AnnotationPrefix + c.Name return AnnotationPrefix + c.Name
} }
func (c *ChannelVersion) Replaces(existing *ChannelVersion) bool { func (c *ChannelVersion) replaces(existing *ChannelVersion) bool {
if existing.Version != nil { if existing.Version != nil {
if c.Version == nil { if c.Version == nil {
return false return false
@ -106,13 +111,25 @@ func (c *ChannelVersion) Replaces(existing *ChannelVersion) bool {
glog.Warningf("error parsing existing version %q", *existing.Version) glog.Warningf("error parsing existing version %q", *existing.Version)
return true return true
} }
return cVersion.GT(existingVersion) if cVersion.LT(existingVersion) {
return false
} else if cVersion.GT(existingVersion) {
return true
} else {
// Same version; check ids
if c.Id == existing.Id {
return false
} else {
glog.V(4).Infof("Channels had same version %q but different ids (%q vs %q); will replace", c.Version, c.Id, existing.Id)
}
}
} }
glog.Warningf("ChannelVersion did not have a version; can't perform real version check") glog.Warningf("ChannelVersion did not have a version; can't perform real version check")
if c.Version == nil { if c.Version == nil {
return false return false
} }
return true return true
} }

View File

@ -14,18 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package main package cmd
import ( import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"io"
) )
// applyCmd represents the apply command func NewCmdApply(f Factory, out io.Writer) *cobra.Command {
var applyCmd = &cobra.Command{ cmd := &cobra.Command{
Use: "apply", Use: "apply",
Short: "apply resources from a channel", Short: "apply resources from a channel",
} }
func init() { // create subcommands
rootCommand.AddCommand(applyCmd) cmd.AddCommand(NewCmdApplyChannel(f, out))
return cmd
} }

View File

@ -14,11 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package main package cmd
import ( import (
"fmt" "fmt"
"github.com/blang/semver"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"io"
"k8s.io/kops/channels/pkg/channels" "k8s.io/kops/channels/pkg/channels"
"k8s.io/kops/util/pkg/tables" "k8s.io/kops/util/pkg/tables"
"net/url" "net/url"
@ -26,38 +28,54 @@ import (
"strings" "strings"
) )
type ApplyChannelCmd struct { type ApplyChannelOptions struct {
Yes bool Yes bool
Files []string Files []string
} }
var applyChannel ApplyChannelCmd func NewCmdApplyChannel(f Factory, out io.Writer) *cobra.Command {
var options ApplyChannelOptions
func init() {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "channel", Use: "channel",
Short: "Apply channel", Short: "Apply channel",
Run: func(cmd *cobra.Command, args []string) { RunE: func(cmd *cobra.Command, args []string) error {
err := applyChannel.Run(args) return RunApplyChannel(f, out, &options, args)
if err != nil {
exitWithError(err)
}
}, },
} }
cmd.Flags().BoolVar(&applyChannel.Yes, "yes", false, "Apply update") cmd.Flags().BoolVar(&options.Yes, "yes", false, "Apply update")
cmd.Flags().StringSliceVar(&applyChannel.Files, "f", []string{}, "Apply from a local file") cmd.Flags().StringSliceVar(&options.Files, "f", []string{}, "Apply from a local file")
applyCmd.AddCommand(cmd) return cmd
} }
func (c *ApplyChannelCmd) Run(args []string) error { func RunApplyChannel(f Factory, out io.Writer, options *ApplyChannelOptions, args []string) error {
k8sClient, err := rootCommand.KubernetesClient() k8sClient, err := f.KubernetesClient()
if err != nil { if err != nil {
return err return err
} }
var addons []*channels.Addon kubernetesVersionInfo, err := k8sClient.Discovery().ServerVersion()
if err != nil {
return fmt.Errorf("error querying kubernetes version: %v", err)
}
//kubernetesVersion, err := semver.Parse(kubernetesVersionInfo.Major + "." + kubernetesVersionInfo.Minor + ".0")
//if err != nil {
// return fmt.Errorf("cannot parse kubernetes version %q", kubernetesVersionInfo.Major+"."+kubernetesVersionInfo.Minor + ".0")
//}
kubernetesVersion, err := semver.ParseTolerant(kubernetesVersionInfo.GitVersion)
if err != nil {
return fmt.Errorf("cannot parse kubernetes version %q", kubernetesVersionInfo.GitVersion)
}
// Remove Pre and Patch, as they make semver comparisons impractical
kubernetesVersion.Pre = nil
menu := channels.NewAddonMenu()
for _, name := range args { for _, name := range args {
location, err := url.Parse(name) location, err := url.Parse(name)
if err != nil { if err != nil {
@ -80,14 +98,14 @@ func (c *ApplyChannelCmd) Run(args []string) error {
return fmt.Errorf("error loading channel %q: %v", location, err) return fmt.Errorf("error loading channel %q: %v", location, err)
} }
current, err := o.GetCurrent() current, err := o.GetCurrent(kubernetesVersion)
if err != nil { if err != nil {
return fmt.Errorf("error processing latest versions in %q: %v", location, err) return fmt.Errorf("error processing latest versions in %q: %v", location, err)
} }
addons = append(addons, current...) menu.Merge(current)
} }
for _, f := range c.Files { for _, f := range options.Files {
location, err := url.Parse(f) location, err := url.Parse(f)
if err != nil { if err != nil {
return fmt.Errorf("unable to parse argument %q as url", f) return fmt.Errorf("unable to parse argument %q as url", f)
@ -108,16 +126,16 @@ func (c *ApplyChannelCmd) Run(args []string) error {
return fmt.Errorf("error loading file %q: %v", f, err) return fmt.Errorf("error loading file %q: %v", f, err)
} }
current, err := o.GetCurrent() current, err := o.GetCurrent(kubernetesVersion)
if err != nil { if err != nil {
return fmt.Errorf("error processing latest versions in %q: %v", f, err) return fmt.Errorf("error processing latest versions in %q: %v", f, err)
} }
addons = append(addons, current...) menu.Merge(current)
} }
var updates []*channels.AddonUpdate var updates []*channels.AddonUpdate
var needUpdates []*channels.Addon var needUpdates []*channels.Addon
for _, addon := range addons { for _, addon := range menu.Addons {
// TODO: Cache lookups to prevent repeated lookups? // TODO: Cache lookups to prevent repeated lookups?
update, err := addon.GetRequiredUpdates(k8sClient) update, err := addon.GetRequiredUpdates(k8sClient)
if err != nil { if err != nil {
@ -165,7 +183,7 @@ func (c *ApplyChannelCmd) Run(args []string) error {
} }
} }
if !c.Yes { if !options.Yes {
fmt.Printf("\nMust specify --yes to update\n") fmt.Printf("\nMust specify --yes to update\n")
return nil return nil
} }
@ -178,7 +196,7 @@ func (c *ApplyChannelCmd) Run(args []string) error {
// Could have been a concurrent request // Could have been a concurrent request
if update != nil { if update != nil {
if update.NewVersion.Version != nil { if update.NewVersion.Version != nil {
fmt.Printf("Updated %q to %v\n", update.Name, *update.NewVersion.Version) fmt.Printf("Updated %q to %s\n", update.Name, *update.NewVersion.Version)
} else { } else {
fmt.Printf("Updated %q\n", update.Name) fmt.Printf("Updated %q\n", update.Name)
} }

View File

@ -0,0 +1,59 @@
/*
Copyright 2017 The Kubernetes Authors.
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 cmd
import (
"fmt"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
type Factory interface {
KubernetesClient() (kubernetes.Interface, error)
}
type DefaultFactory struct {
kubernetesClient kubernetes.Interface
}
var _ Factory = &DefaultFactory{}
func (f *DefaultFactory) KubernetesClient() (kubernetes.Interface, error) {
if f.kubernetesClient == nil {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig
configOverrides := &clientcmd.ConfigOverrides{
ClusterDefaults: clientcmd.ClusterDefaults,
}
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
config, err := kubeConfig.ClientConfig()
if err != nil {
return nil, fmt.Errorf("cannot load kubecfg settings: %v", err)
}
k8sClient, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, fmt.Errorf("cannot build kube client: %v", err)
}
f.kubernetesClient = k8sClient
}
return f.kubernetesClient, nil
}

View File

@ -14,36 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package main package cmd
import ( import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"io"
) )
// GetCmd represents the get command func NewCmdGet(f Factory, out io.Writer) *cobra.Command {
type GetCmd struct { cmd := &cobra.Command{
output string
cobraCommand *cobra.Command
}
var getCmd = GetCmd{
cobraCommand: &cobra.Command{
Use: "get", Use: "get",
SuggestFor: []string{"list"}, SuggestFor: []string{"list"},
Short: "list or get objects", Short: "list or get objects",
},
} }
const ( // create subcommands
OutputYaml = "yaml" cmd.AddCommand(NewCmdGetAddons(f, out))
OutputTable = "table"
)
func init() { return cmd
cmd := getCmd.cobraCommand
rootCommand.AddCommand(cmd)
cmd.PersistentFlags().StringVarP(&getCmd.output, "output", "o", OutputTable, "output format. One of: table, yaml")
} }

View File

@ -14,11 +14,28 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package main /*
Copyright 2017 The Kubernetes Authors.
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 cmd
import ( import (
"fmt" "fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"io"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/pkg/api/v1" "k8s.io/client-go/pkg/api/v1"
"k8s.io/kops/channels/pkg/channels" "k8s.io/kops/channels/pkg/channels"
@ -26,26 +43,23 @@ import (
"os" "os"
) )
type GetAddonsCmd struct { type GetAddonsOptions struct {
} }
var getAddonsCmd GetAddonsCmd func NewCmdGetAddons(f Factory, out io.Writer) *cobra.Command {
var options GetAddonsOptions
func init() {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "addons", Use: "addons",
Aliases: []string{"addon"}, Aliases: []string{"addon"},
Short: "get addons", Short: "get addons",
Long: `List or get addons.`, Long: `List or get addons.`,
Run: func(cmd *cobra.Command, args []string) { RunE: func(cmd *cobra.Command, args []string) error {
err := getAddonsCmd.Run(args) return RunGetAddons(f, out, &options)
if err != nil {
exitWithError(err)
}
}, },
} }
getCmd.cobraCommand.AddCommand(cmd) return cmd
} }
type addonInfo struct { type addonInfo struct {
@ -54,8 +68,8 @@ type addonInfo struct {
Namespace *v1.Namespace Namespace *v1.Namespace
} }
func (c *GetAddonsCmd) Run(args []string) error { func RunGetAddons(f Factory, out io.Writer, options *GetAddonsOptions) error {
k8sClient, err := rootCommand.KubernetesClient() k8sClient, err := f.KubernetesClient()
if err != nil { if err != nil {
return err return err
} }

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package main package cmd
import ( import (
goflag "flag" goflag "flag"
@ -22,48 +22,44 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"k8s.io/client-go/kubernetes" "io"
"k8s.io/client-go/tools/clientcmd"
) )
type RootCmd struct { type CmdRootOptions struct {
configFile string configFile string
cobraCommand *cobra.Command
} }
var rootCommand = RootCmd{ func Execute(f Factory, out io.Writer) error {
cobraCommand: &cobra.Command{
Use: "channels",
Short: "channels applies software from a channel",
},
}
func Execute() {
goflag.Set("logtostderr", "true")
goflag.CommandLine.Parse([]string{})
if err := rootCommand.cobraCommand.Execute(); err != nil {
exitWithError(err)
}
}
func init() {
cobra.OnInitialize(initConfig) cobra.OnInitialize(initConfig)
cmd := rootCommand.cobraCommand cmd := NewCmdRoot(f, out)
goflag.Set("logtostderr", "true")
goflag.CommandLine.Parse([]string{})
return cmd.Execute()
}
func NewCmdRoot(f Factory, out io.Writer) *cobra.Command {
options := &CmdRootOptions{}
cmd := &cobra.Command{
Use: "channels",
Short: "channels applies software from a channel",
}
cmd.PersistentFlags().AddGoFlagSet(goflag.CommandLine) cmd.PersistentFlags().AddGoFlagSet(goflag.CommandLine)
cmd.PersistentFlags().StringVar(&rootCommand.configFile, "config", "", "config file (default is $HOME/.channels.yaml)") cmd.PersistentFlags().StringVar(&options.configFile, "config", "", "config file (default is $HOME/.channels.yaml)")
// create subcommands
cmd.AddCommand(NewCmdApply(f, out))
cmd.AddCommand(NewCmdGet(f, out))
return cmd
} }
// initConfig reads in config file and ENV variables if set. // initConfig reads in config file and ENV variables if set.
func initConfig() { func initConfig() {
if rootCommand.configFile != "" {
// enable ability to specify config file via flag
viper.SetConfigFile(rootCommand.configFile)
}
viper.SetConfigName(".channels") // name of config file (without extension) viper.SetConfigName(".channels") // name of config file (without extension)
viper.AddConfigPath("$HOME") // adding home directory as first search path viper.AddConfigPath("$HOME") // adding home directory as first search path
viper.AutomaticEnv() // read in environment variables that match viper.AutomaticEnv() // read in environment variables that match
@ -73,28 +69,3 @@ func initConfig() {
fmt.Println("Using config file:", viper.ConfigFileUsed()) fmt.Println("Using config file:", viper.ConfigFileUsed())
} }
} }
func (c *RootCmd) AddCommand(cmd *cobra.Command) {
c.cobraCommand.AddCommand(cmd)
}
func (c *RootCmd) KubernetesClient() (kubernetes.Interface, error) {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig
configOverrides := &clientcmd.ConfigOverrides{
ClusterDefaults: clientcmd.ClusterDefaults,
}
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
config, err := kubeConfig.ClientConfig()
if err != nil {
return nil, fmt.Errorf("cannot load kubecfg settings: %v", err)
}
k8sClient, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, fmt.Errorf("cannot build kube client: %v", err)
}
return k8sClient, err
}

View File

@ -2,6 +2,7 @@ k8s.io/kops
k8s.io/kops/channels/cmd/channels k8s.io/kops/channels/cmd/channels
k8s.io/kops/channels/pkg/api k8s.io/kops/channels/pkg/api
k8s.io/kops/channels/pkg/channels k8s.io/kops/channels/pkg/channels
k8s.io/kops/channels/pkg/cmd
k8s.io/kops/cloudmock/aws/mockautoscaling k8s.io/kops/cloudmock/aws/mockautoscaling
k8s.io/kops/cloudmock/aws/mockec2 k8s.io/kops/cloudmock/aws/mockec2
k8s.io/kops/cloudmock/aws/mockroute53 k8s.io/kops/cloudmock/aws/mockroute53