Store channel in cluster, use it to determine version

This commit is contained in:
Justin Santa Barbara 2016-09-30 22:29:39 -04:00
parent 9697a45e39
commit 647618b755
11 changed files with 312 additions and 30 deletions

View File

@ -35,6 +35,9 @@ type CreateClusterCmd struct {
AdminAccess string
Networking string
AssociatePublicIP bool
// Channel is the location of the api.Channel to use for our defaults
Channel string
}
var createCluster CreateClusterCmd
@ -64,7 +67,7 @@ func init() {
cmd.Flags().StringVar(&createCluster.MasterZones, "master-zones", "", "Zones in which to run masters (must be an odd number)")
cmd.Flags().StringVar(&createCluster.Project, "project", "", "Project to use (must be set on GCE)")
cmd.Flags().StringVar(&createCluster.KubernetesVersion, "kubernetes-version", "", "Version of kubernetes to run (defaults to latest)")
cmd.Flags().StringVar(&createCluster.KubernetesVersion, "kubernetes-version", "", "Version of kubernetes to run (defaults to version in channel)")
cmd.Flags().StringVar(&createCluster.SSHPublicKey, "ssh-public-key", "~/.ssh/id_rsa.pub", "SSH public key to use")
@ -86,6 +89,8 @@ func init() {
cmd.Flags().StringVar(&createCluster.AdminAccess, "admin-access", "", "Restrict access to admin endpoints (SSH, HTTPS) to this CIDR. If not set, access will not be restricted by IP.")
cmd.Flags().BoolVar(&createCluster.AssociatePublicIP, "associate-public-ip", true, "Specify --associate-public-ip=[true|false] to enable/disable association of public IP for master ASG and nodes. Default is 'true'.")
cmd.Flags().StringVar(&createCluster.Channel, "channel", api.DefaultChannel, "Channel for default versions and configuration to use")
}
func (c *CreateClusterCmd) Run(args []string) error {
@ -138,6 +143,16 @@ func (c *CreateClusterCmd) Run(args []string) error {
}
cluster = &api.Cluster{}
channel, err := api.LoadChannel(c.Channel)
if err != nil {
return err
}
if channel.Spec.Cluster != nil {
cluster.Spec = *channel.Spec.Cluster
}
cluster.Spec.Channel = c.Channel
var instanceGroups []*api.InstanceGroup
cluster.Spec.Networking = &api.NetworkingSpec{}
@ -360,7 +375,7 @@ func (c *CreateClusterCmd) Run(args []string) error {
var fullInstanceGroups []*api.InstanceGroup
for _, group := range instanceGroups {
fullGroup, err := cloudup.PopulateInstanceGroupSpec(fullCluster, group)
fullGroup, err := cloudup.PopulateInstanceGroupSpec(fullCluster, group, channel)
if err != nil {
return err
}

View File

@ -43,6 +43,11 @@ func init() {
func (c *CreateInstanceGroupCmd) Run(groupName string) error {
_, cluster, err := rootCommand.Cluster()
channel, err := cloudup.ChannelForCluster(cluster)
if err != nil {
return err
}
instanceGroupStore, err := rootCommand.InstanceGroupRegistry()
if err != nil {
return err
@ -62,7 +67,7 @@ func (c *CreateInstanceGroupCmd) Run(groupName string) error {
ig.Name = groupName
ig.Spec.Role = api.InstanceGroupRoleNode
ig, err = cloudup.PopulateInstanceGroupSpec(cluster, ig)
ig, err = cloudup.PopulateInstanceGroupSpec(cluster, ig, channel)
if err != nil {
return err
}

View File

@ -56,6 +56,11 @@ func (c *EditInstanceGroupCmd) Run(groupName string) error {
}
}
channel, err := cloudup.ChannelForCluster(cluster)
if err != nil {
return err
}
registry, err := rootCommand.InstanceGroupRegistry()
if err != nil {
return err
@ -110,7 +115,7 @@ func (c *EditInstanceGroupCmd) Run(groupName string) error {
return err
}
fullGroup, err := cloudup.PopulateInstanceGroupSpec(cluster, newGroup)
fullGroup, err := cloudup.PopulateInstanceGroupSpec(cluster, newGroup, channel)
if err != nil {
return err
}

View File

@ -11,6 +11,9 @@ import (
type ConvertImportedCmd struct {
NewClusterName string
// Channel is the location of the api.Channel to use for our defaults
Channel string
}
var convertImported ConvertImportedCmd
@ -30,6 +33,7 @@ func init() {
toolboxCmd.AddCommand(cmd)
cmd.Flags().StringVar(&convertImported.NewClusterName, "newname", "", "new cluster name")
cmd.Flags().StringVar(&convertImported.Channel, "channel", api.DefaultChannel, "Channel to use for upgrade")
}
func (c *ConvertImportedCmd) Run() error {
@ -83,13 +87,20 @@ func (c *ConvertImportedCmd) Run() error {
return fmt.Errorf("error initializing AWS client: %v", err)
}
d := &kutil.ConvertKubeupCluster{}
d.NewClusterName = c.NewClusterName
d.OldClusterName = oldClusterName
d.Cloud = cloud
d.ClusterConfig = cluster
d.InstanceGroups = instanceGroups
d.ClusterRegistry = clusterRegistry
channel, err := api.LoadChannel(c.Channel)
if err != nil {
return err
}
d := &kutil.ConvertKubeupCluster{
NewClusterName: c.NewClusterName,
OldClusterName: oldClusterName,
Cloud: cloud,
ClusterConfig: cluster,
InstanceGroups: instanceGroups,
ClusterRegistry: clusterRegistry,
Channel: channel,
}
err = d.Upgrade()
if err != nil {

View File

@ -3,8 +3,10 @@ package main
import (
"fmt"
"github.com/golang/glog"
"github.com/spf13/cobra"
"k8s.io/kops/upup/pkg/api"
"k8s.io/kops/upup/pkg/fi"
"k8s.io/kops/upup/pkg/fi/cloudup"
"k8s.io/kops/util/pkg/tables"
"os"
@ -12,6 +14,8 @@ import (
type UpgradeClusterCmd struct {
Yes bool
Channel string
}
var upgradeCluster UpgradeClusterCmd
@ -30,6 +34,7 @@ func init() {
}
cmd.Flags().BoolVar(&upgradeCluster.Yes, "yes", false, "Apply update")
cmd.Flags().StringVar(&upgradeCluster.Channel, "channel", "", "Channel to use for upgrade")
upgradeCmd.AddCommand(cmd)
}
@ -65,24 +70,138 @@ func (c *UpgradeClusterCmd) Run(args []string) error {
return fmt.Errorf("upgrade is not for use with imported clusters (did you mean `kops toolbox convert-imported`?)")
}
latestKubernetesVersion, err := api.FindLatestKubernetesVersion()
if err != nil {
return err
channelLocation := c.Channel
if channelLocation == "" {
channelLocation = cluster.Spec.Channel
}
if channelLocation == "" {
channelLocation = api.DefaultChannel
}
var actions []*upgradeAction
if cluster.Spec.KubernetesVersion != latestKubernetesVersion {
if channelLocation != cluster.Spec.Channel {
actions = append(actions, &upgradeAction{
Item: "Cluster",
Property: "Channel",
Old: cluster.Spec.Channel,
New: channelLocation,
apply: func() {
cluster.Spec.Channel = channelLocation
},
})
}
channel, err := api.LoadChannel(channelLocation)
if err != nil {
return fmt.Errorf("error loading channel %q: %v", channelLocation, err)
}
channelClusterSpec := channel.Spec.Cluster
if channelClusterSpec == nil {
// Just to prevent too much nil handling
channelClusterSpec = &api.ClusterSpec{}
}
//latestKubernetesVersion, err := api.FindLatestKubernetesVersion()
//if err != nil {
// return err
//}
if channelClusterSpec.KubernetesVersion != "" && cluster.Spec.KubernetesVersion != channelClusterSpec.KubernetesVersion {
actions = append(actions, &upgradeAction{
Item: "Cluster",
Property: "KubernetesVersion",
Old: cluster.Spec.KubernetesVersion,
New: latestKubernetesVersion,
New: channelClusterSpec.KubernetesVersion,
apply: func() {
cluster.Spec.KubernetesVersion = latestKubernetesVersion
cluster.Spec.KubernetesVersion = channelClusterSpec.KubernetesVersion
},
})
}
// Prompt to upgrade addins?
// Prompt to upgrade to kubenet
if channelClusterSpec.Networking != nil {
clusterNetworking := cluster.Spec.Networking
if clusterNetworking == nil {
clusterNetworking = &api.NetworkingSpec{}
}
// TODO: make this less hard coded
if channelClusterSpec.Networking.Kubenet != nil && channelClusterSpec.Networking.Classic != nil {
actions = append(actions, &upgradeAction{
Item: "Cluster",
Property: "Networking",
Old: "classic",
New: "kubenet",
apply: func() {
cluster.Spec.Networking.Classic = nil
cluster.Spec.Networking.Kubenet = channelClusterSpec.Networking.Kubenet
},
})
}
}
cloud, err := cloudup.BuildCloud(cluster)
if err != nil {
return err
}
// Prompt to upgrade image
{
var matches []*api.ChannelImageSpec
for _, image := range channel.Spec.Images {
cloudProvider := image.Labels[api.ImageLabelCloudprovider]
if cloudProvider != string(cloud.ProviderID()) {
continue
}
matches = append(matches, image)
}
if len(matches) == 0 {
glog.Warningf("No matching images specified in channel; cannot prompt for upgrade")
} else if len(matches) != 1 {
glog.Warningf("Multiple matching images specified in channel; cannot prompt for upgrade")
} else {
for _, ig := range instanceGroups {
if ig.Spec.Image != matches[0].Name {
target := ig
actions = append(actions, &upgradeAction{
Item: "InstanceGroup/" + target.Name,
Property: "Image",
Old: target.Spec.Image,
New: matches[0].Name,
apply: func() {
target.Spec.Image = matches[0].Name
},
})
}
}
}
}
// Prompt to upgrade to overlayfs
if channelClusterSpec.Docker != nil {
if cluster.Spec.Docker == nil {
cluster.Spec.Docker = &api.DockerConfig{}
}
// TODO: make less hard-coded
if channelClusterSpec.Docker.Storage != nil {
dockerStorage := fi.StringValue(cluster.Spec.Docker.Storage)
if dockerStorage != fi.StringValue(channelClusterSpec.Docker.Storage) {
actions = append(actions, &upgradeAction{
Item: "Cluster",
Property: "Docker.Storage",
Old: dockerStorage,
New: fi.StringValue(channelClusterSpec.Docker.Storage),
apply: func() {
cluster.Spec.Docker.Storage = channelClusterSpec.Docker.Storage
},
})
}
}
}
if len(actions) == 0 {
// TODO: Allow --force option to force even if not needed?
// Note stderr - we try not to print to stdout if no update is needed
@ -141,6 +260,13 @@ func (c *UpgradeClusterCmd) Run(args []string) error {
return err
}
for _, g := range instanceGroups {
err := instanceGroupRegistry.Update(g)
if err != nil {
return fmt.Errorf("error writing InstanceGroup %q to registry: %v", g.Name, err)
}
}
err = clusterRegistry.WriteCompletedConfig(fullCluster)
if err != nil {
return fmt.Errorf("error writing completed cluster spec: %v", err)

68
upup/pkg/api/channel.go Normal file
View File

@ -0,0 +1,68 @@
package api
import (
"fmt"
"github.com/golang/glog"
"k8s.io/kops/util/pkg/vfs"
k8sapi "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/unversioned"
"net/url"
)
const DefaultChannelBase = "https://raw.githubusercontent.com/kubernetes/kops/master/channels/"
const DefaultChannel = "stable"
type Channel struct {
unversioned.TypeMeta `json:",inline"`
k8sapi.ObjectMeta `json:"metadata,omitempty"`
Spec ChannelSpec `json:"spec,omitempty"`
}
type ChannelSpec struct {
Images []*ChannelImageSpec `json:"images,omitempty"`
Cluster *ClusterSpec `json:"cluster,omitempty"`
}
const (
ImageLabelCloudprovider = "k8s.io/cloudprovider"
)
type ChannelImageSpec struct {
Labels map[string]string `json:"labels,omitempty"`
ProviderID string `json:"providerID,omitempty"`
Name string `json:"name,omitempty"`
}
// LoadChannel loads a Channel object from the specified VFS location
func LoadChannel(location string) (*Channel, error) {
u, err := url.Parse(location)
if err != nil {
return nil, fmt.Errorf("invalid channel: %q", location)
}
if !u.IsAbs() {
base, err := url.Parse(DefaultChannelBase)
if err != nil {
return nil, fmt.Errorf("invalid base channel location: %q", DefaultChannelBase)
}
u = base.ResolveReference(u)
}
resolved := u.String()
glog.V(2).Infof("Loading channel from %q", resolved)
channel := &Channel{}
channelBytes, err := vfs.Context.ReadFile(resolved)
if err != nil {
return nil, fmt.Errorf("error reading channel %q: %v", resolved, err)
}
err = ParseYaml(channelBytes, channel)
if err != nil {
return nil, fmt.Errorf("error parsing channel %q: %v", resolved, err)
}
glog.V(4).Info("Channel contents: %s", string(channelBytes))
return channel, nil
}

View File

@ -20,6 +20,9 @@ type Cluster struct {
}
type ClusterSpec struct {
// The Channel we are following
Channel string `json:"channel,omitempty"`
// The CloudProvider to use (aws or gce)
CloudProvider string `json:"cloudProvider,omitempty"`
@ -319,6 +322,10 @@ func (c *Cluster) FillDefaults() error {
c.Spec.Networking.Kubenet = &KubenetNetworkingSpec{}
}
if c.Spec.Channel == "" {
c.Spec.Channel = DefaultChannel
}
err := c.ensureKubernetesVersion()
if err != nil {
return err
@ -328,8 +335,20 @@ func (c *Cluster) FillDefaults() error {
}
// ensureKubernetesVersion populates KubernetesVersion, if it is not already set
// It will be populated with the latest stable kubernetes version
// It will be populated with the latest stable kubernetes version, or the version from the channel
func (c *Cluster) ensureKubernetesVersion() error {
if c.Spec.KubernetesVersion == "" {
if c.Spec.Channel != "" {
channel, err := LoadChannel(c.Spec.Channel)
if err != nil {
return err
}
if channel.Spec.Cluster.KubernetesVersion != "" {
c.Spec.KubernetesVersion = channel.Spec.Cluster.KubernetesVersion
}
}
}
if c.Spec.KubernetesVersion == "" {
latestVersion, err := FindLatestKubernetesVersion()
if err != nil {

View File

@ -573,6 +573,11 @@ func (c *ApplyClusterCmd) upgradeSpecs() error {
// return fmt.Errorf("error populating configuration: %v", err)
//}
channel, err := ChannelForCluster(c.Cluster)
if err != nil {
return err
}
fullCluster, err := PopulateClusterSpec(c.Cluster, c.ClusterRegistry)
if err != nil {
return err
@ -580,7 +585,7 @@ func (c *ApplyClusterCmd) upgradeSpecs() error {
c.Cluster = fullCluster
for i, g := range c.InstanceGroups {
fullGroup, err := PopulateInstanceGroupSpec(fullCluster, g)
fullGroup, err := PopulateInstanceGroupSpec(fullCluster, g, channel)
if err != nil {
return err
}
@ -589,3 +594,11 @@ func (c *ApplyClusterCmd) upgradeSpecs() error {
return nil
}
func ChannelForCluster(c *api.Cluster) (*api.Channel, error) {
channelLocation := c.Spec.Channel
if channelLocation == "" {
channelLocation = api.DefaultChannel
}
return api.LoadChannel(channelLocation)
}

View File

@ -16,7 +16,7 @@ const DefaultMasterMachineTypeGCE = "n1-standard-1"
// PopulateInstanceGroupSpec sets default values in the InstanceGroup
// The InstanceGroup is simpler than the cluster spec, so we just populate in place (like the rest of k8s)
func PopulateInstanceGroupSpec(cluster *api.Cluster, input *api.InstanceGroup) (*api.InstanceGroup, error) {
func PopulateInstanceGroupSpec(cluster *api.Cluster, input *api.InstanceGroup, channel *api.Channel) (*api.InstanceGroup, error) {
err := input.Validate(false)
if err != nil {
return nil, err
@ -52,7 +52,7 @@ func PopulateInstanceGroupSpec(cluster *api.Cluster, input *api.InstanceGroup) (
}
if ig.Spec.Image == "" {
ig.Spec.Image = defaultImage(cluster)
ig.Spec.Image = defaultImage(cluster, channel)
}
if ig.IsMaster() {
@ -126,13 +126,17 @@ func defaultMasterMachineType(cluster *api.Cluster) string {
}
// defaultImage returns the default Image, based on the cloudprovider
func defaultImage(cluster *api.Cluster) string {
// TODO: Use spec?
switch cluster.Spec.CloudProvider {
case "aws":
return "282335181503/k8s-1.3-debian-jessie-amd64-hvm-ebs-2016-06-18"
default:
glog.V(2).Infof("Cannot set default Image for CloudProvider=%q", cluster.Spec.CloudProvider)
return ""
func defaultImage(cluster *api.Cluster, channel *api.Channel) string {
if channel != nil {
for _, image := range channel.Spec.Images {
if image.ProviderID != cluster.Spec.CloudProvider {
continue
}
return image.Name
}
}
glog.Infof("Cannot set default Image for CloudProvider=%q", cluster.Spec.CloudProvider)
return ""
}

View File

@ -23,6 +23,9 @@ type ConvertKubeupCluster struct {
ClusterConfig *api.Cluster
InstanceGroups []*api.InstanceGroup
// Channel is the channel that we are upgrading to
Channel *api.Channel
}
func (x *ConvertKubeupCluster) Upgrade() error {
@ -49,6 +52,12 @@ func (x *ConvertKubeupCluster) Upgrade() error {
// Build completed cluster (force errors asap)
cluster.Name = newClusterName
// Set KubernetesVersion from channel
if x.Channel != nil && x.Channel.Spec.Cluster != nil && x.Channel.Spec.Cluster.KubernetesVersion != "" {
cluster.Spec.KubernetesVersion = x.Channel.Spec.Cluster.KubernetesVersion
}
err := cluster.PerformAssignments()
if err != nil {
return fmt.Errorf("error populating cluster defaults: %v", err)

View File

@ -42,6 +42,13 @@ func (x *ImportCluster) ImportAWSCluster() error {
cluster.Spec.KubeControllerManager = &api.KubeControllerManagerConfig{}
cluster.Spec.Channel = api.DefaultChannel
channel, err := api.LoadChannel(cluster.Spec.Channel)
if err != nil {
return err
}
masterGroup := &api.InstanceGroup{}
masterGroup.Spec.Role = api.InstanceGroupRoleMaster
masterGroup.Spec.MinSize = fi.Int(1)
@ -418,7 +425,7 @@ func (x *ImportCluster) ImportAWSCluster() error {
var fullInstanceGroups []*api.InstanceGroup
for _, ig := range instanceGroups {
full, err := cloudup.PopulateInstanceGroupSpec(cluster, ig)
full, err := cloudup.PopulateInstanceGroupSpec(cluster, ig, channel)
if err != nil {
return err
}