Skip to content

Commit

Permalink
channels: support targeting kubernetes versions
Browse files Browse the repository at this point in the history
  • Loading branch information
justinsb committed Apr 8, 2017
1 parent 343217e commit a7c2c55
Show file tree
Hide file tree
Showing 14 changed files with 418 additions and 140 deletions.
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ test:
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/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

crossbuild-nodeup:
Expand Down
14 changes: 6 additions & 8 deletions channels/cmd/channels/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,14 @@ package main

import (
"fmt"
"k8s.io/kops/channels/pkg/cmd"
"os"
)

func main() {
Execute()
}

// 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)
os.Exit(1)
f := &cmd.DefaultFactory{}
if err := cmd.Execute(f, os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "\n%v\n", err)
os.Exit(1)
}
}
11 changes: 11 additions & 0 deletions channels/pkg/api/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,15 @@ type AddonSpec struct {

// Manifest is the URL to the manifest that should be applied
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"`
}
29 changes: 28 additions & 1 deletion channels/pkg/channels/addon.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,50 @@ import (
"net/url"
)

// Addon is a wrapper around a single version of an addon
type Addon struct {
Name string
ChannelName string
ChannelLocation url.URL
Spec *api.AddonSpec
}

// AddonUpdate holds data about a proposed update to an addon
type AddonUpdate struct {
Name string
ExistingVersion *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 {
return &ChannelVersion{
Channel: &a.ChannelName,
Version: a.Spec.Version,
Id: a.Spec.Id,
}
}

Expand All @@ -67,7 +94,7 @@ func (a *Addon) GetRequiredUpdates(k8sClient kubernetes.Interface) (*AddonUpdate
return nil, err
}

if existingVersion != nil && !newVersion.Replaces(existingVersion) {
if existingVersion != nil && !newVersion.replaces(existingVersion) {
return nil, nil
}

Expand Down
42 changes: 30 additions & 12 deletions channels/pkg/channels/addons.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package channels

import (
"fmt"
"github.com/blang/semver"
"github.com/golang/glog"
"k8s.io/kops/channels/pkg/api"
"k8s.io/kops/upup/pkg/fi/utils"
Expand Down Expand Up @@ -58,28 +59,29 @@ func ParseAddons(name string, location *url.URL, data []byte) (*Addons, error) {
return &Addons{ChannelName: name, ChannelLocation: *location, APIObject: apiObject}, nil
}

func (a *Addons) GetCurrent() ([]*Addon, error) {
all, err := a.All()
func (a *Addons) GetCurrent(kubernetesVersion semver.Version) (*AddonMenu, error) {
all, err := a.wrapInAddons()
if err != nil {
return nil, err
}
specs := make(map[string]*Addon)

menu := NewAddonMenu()
for _, addon := range all {
if !addon.matches(kubernetesVersion) {
continue
}
name := addon.Name
existing := specs[name]
if existing == nil || addon.ChannelVersion().Replaces(existing.ChannelVersion()) {
specs[name] = addon

existing := menu.Addons[name]
if existing == nil || addon.ChannelVersion().replaces(existing.ChannelVersion()) {
menu.Addons[name] = addon
}
}

var addons []*Addon
for _, addon := range specs {
addons = append(addons, addon)
}
return addons, nil
return menu, nil
}

func (a *Addons) All() ([]*Addon, error) {
func (a *Addons) wrapInAddons() ([]*Addon, error) {
var addons []*Addon
for _, s := range a.APIObject.Spec.Addons {
name := a.APIObject.ObjectMeta.Name
Expand All @@ -98,3 +100,19 @@ func (a *Addons) All() ([]*Addon, error) {
}
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
}
154 changes: 154 additions & 0 deletions channels/pkg/channels/addons_test.go
Original file line number Diff line number Diff line change
@@ -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
}
23 changes: 20 additions & 3 deletions channels/pkg/channels/channel_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type Channel struct {
type ChannelVersion struct {
Version *string `json:"version,omitempty"`
Channel *string `json:"channel,omitempty"`
Id string `json:"id,omitempty"`
}

func stringValue(s *string) string {
Expand All @@ -48,7 +49,11 @@ func stringValue(s *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) {
Expand Down Expand Up @@ -91,7 +96,7 @@ func (c *Channel) AnnotationName() string {
return AnnotationPrefix + c.Name
}

func (c *ChannelVersion) Replaces(existing *ChannelVersion) bool {
func (c *ChannelVersion) replaces(existing *ChannelVersion) bool {
if existing.Version != nil {
if c.Version == nil {
return false
Expand All @@ -106,13 +111,25 @@ func (c *ChannelVersion) Replaces(existing *ChannelVersion) bool {
glog.Warningf("error parsing existing version %q", *existing.Version)
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")
if c.Version == nil {
return false
}

return true
}

Expand Down
Loading

0 comments on commit a7c2c55

Please sign in to comment.