Skip to content
This repository has been archived by the owner on Nov 1, 2022. It is now read-only.

Commit

Permalink
Merge pull request #1117 from weaveworks/feature/careful-sync-order
Browse files Browse the repository at this point in the history
Sort resources using dependency order before applying
  • Loading branch information
squaremo authored Jun 26, 2018
2 parents b10e0bb + a211ea3 commit 335c768
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 43 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ build/.%.done: docker/Dockerfile.%
-f build/docker/$*/Dockerfile.$* ./build/docker/$*
touch $@

build/.flux.done: build/fluxd build/kubectl docker/ssh_config
build/.flux.done: build/fluxd build/kubectl docker/ssh_config docker/kubeconfig
build/.helm-operator.done: build/helm-operator build/kubectl docker/ssh_config

build/fluxd: $(FLUXD_DEPS)
Expand Down
36 changes: 7 additions & 29 deletions cluster/kubernetes/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ type extendedClient struct {

// --- internal types for keeping track of syncing

type metadata struct {
Name string `yaml:"name"`
Namespace string `yaml:"namespace"`
}

type apiObject struct {
resource.Resource
Kind string `yaml:"kind"`
Metadata struct {
Name string `yaml:"name"`
Namespace string `yaml:"namespace"`
} `yaml:"metadata"`
Kind string `yaml:"kind"`
Metadata metadata `yaml:"metadata"`
}

// A convenience for getting an minimal object from some bytes.
Expand Down Expand Up @@ -99,30 +101,6 @@ func isAddon(obj k8sObject) bool {

// --- /add ons

type changeSet struct {
nsObjs map[string][]*apiObject
noNsObjs map[string][]*apiObject
}

func makeChangeSet() changeSet {
return changeSet{
nsObjs: make(map[string][]*apiObject),
noNsObjs: make(map[string][]*apiObject),
}
}

func (c *changeSet) stage(cmd string, o *apiObject) {
if o.hasNamespace() {
c.nsObjs[cmd] = append(c.nsObjs[cmd], o)
} else {
c.noNsObjs[cmd] = append(c.noNsObjs[cmd], o)
}
}

type Applier interface {
apply(log.Logger, changeSet) cluster.SyncError
}

// Cluster is a handle to a Kubernetes API server.
// (Typically, this code is deployed into the same cluster.)
type Cluster struct {
Expand Down
88 changes: 76 additions & 12 deletions cluster/kubernetes/release.go → cluster/kubernetes/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,34 @@ import (
"fmt"
"io"
"os/exec"
"sort"
"strings"
"time"

rest "k8s.io/client-go/rest"

"github.com/go-kit/kit/log"
"github.com/pkg/errors"
"github.com/weaveworks/flux/cluster"
rest "k8s.io/client-go/rest"
)

type changeSet struct {
objs map[string][]*apiObject
}

func makeChangeSet() changeSet {
return changeSet{objs: make(map[string][]*apiObject)}
}

func (c *changeSet) stage(cmd string, o *apiObject) {
c.objs[cmd] = append(c.objs[cmd], o)
}

// Applier is something that will apply a changeset to the cluster.
type Applier interface {
apply(log.Logger, changeSet) cluster.SyncError
}

type Kubectl struct {
exe string
config *rest.Config
Expand Down Expand Up @@ -52,9 +71,50 @@ func (c *Kubectl) connectArgs() []string {
return args
}

// rankOfKind returns an int denoting the position of the given kind
// in the partial ordering of Kubernetes resources, according to which
// kinds depend on which (derived by hand).
func rankOfKind(kind string) int {
switch kind {
// Namespaces answer to NOONE
case "Namespace":
return 0
// These don't go in namespaces; or do, but don't depend on anything else
case "ServiceAccount", "ClusterRole", "Role", "PersistentVolume", "Service":
return 1
// These depend on something above, but not each other
case "ResourceQuota", "LimitRange", "Secret", "ConfigMap", "RoleBinding", "ClusterRoleBinding", "PersistentVolumeClaim", "Ingress":
return 2
// Same deal, next layer
case "DaemonSet", "Deployment", "ReplicationController", "ReplicaSet", "Job", "CronJob", "StatefulSet":
return 3
// Assumption: anything not mentioned isn't depended _upon_, so
// can come last.
default:
return 4
}
}

type applyOrder []*apiObject

func (objs applyOrder) Len() int {
return len(objs)
}

func (objs applyOrder) Swap(i, j int) {
objs[i], objs[j] = objs[j], objs[i]
}

func (objs applyOrder) Less(i, j int) bool {
ranki, rankj := rankOfKind(objs[i].Kind), rankOfKind(objs[j].Kind)
if ranki == rankj {
return objs[i].Metadata.Name < objs[j].Metadata.Name
}
return ranki < rankj
}

func (c *Kubectl) apply(logger log.Logger, cs changeSet) (errs cluster.SyncError) {
f := func(m map[string][]*apiObject, cmd string, args ...string) {
objs := m[cmd]
f := func(objs []*apiObject, cmd string, args ...string) {
if len(objs) == 0 {
return
}
Expand All @@ -70,15 +130,19 @@ func (c *Kubectl) apply(logger log.Logger, cs changeSet) (errs cluster.SyncError
}
}

// When deleting resources we must ensure any resource in a non-default
// namespace is deleted before the namespace that it is in. Since namespace
// resources don't specify a namespace, this ordering guarantees that.
f(cs.nsObjs, "delete")
f(cs.noNsObjs, "delete", "--namespace", "default")
// Likewise, when applying resources we must ensure the namespace is applied
// first, so we run the commands the other way round.
f(cs.noNsObjs, "apply", "--namespace", "default")
f(cs.nsObjs, "apply")
// When deleting objects, the only real concern is that we don't
// try to delete things that have already been deleted by
// Kubernete's GC -- most notably, resources in a namespace which
// is also being deleted. GC does not have the dependency ranking,
// but we can use it as a shortcut to avoid the above problem at
// least.
objs := cs.objs["delete"]
sort.Sort(sort.Reverse(applyOrder(objs)))
f(objs, "delete")

objs = cs.objs["apply"]
sort.Sort(applyOrder(objs))
f(objs, "apply")
return errs
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kubernetes

import (
"sort"
"testing"

"github.com/go-kit/kit/log"
Expand All @@ -15,7 +16,7 @@ type mockApplier struct {
}

func (m *mockApplier) apply(_ log.Logger, c changeSet) cluster.SyncError {
if len(c.nsObjs) != 0 || len(c.noNsObjs) != 0 {
if len(c.objs) != 0 {
m.commandRun = true
}
return nil
Expand Down Expand Up @@ -79,3 +80,33 @@ func TestSyncMalformed(t *testing.T) {
t.Error("expected no commands run")
}
}

// TestApplyOrder checks that applyOrder works as expected.
func TestApplyOrder(t *testing.T) {
objs := []*apiObject{
{
Kind: "Deployment",
Metadata: metadata{
Name: "deploy",
},
},
{
Kind: "Secret",
Metadata: metadata{
Name: "secret",
},
},
{
Kind: "Namespace",
Metadata: metadata{
Name: "namespace",
},
},
}
sort.Sort(applyOrder(objs))
for i, name := range []string{"namespace", "secret", "deploy"} {
if objs[i].Metadata.Name != name {
t.Errorf("Expected %q at position %d, got %q", name, i, objs[i].Metadata.Name)
}
}
}
1 change: 1 addition & 0 deletions docker/Dockerfile.flux
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ RUN ssh-keyscan github.com gitlab.com bitbucket.org >> /etc/ssh/ssh_known_hosts
# Add default SSH config, which points at the private key we'll mount
COPY ./ssh_config /etc/ssh/ssh_config

COPY ./kubeconfig /root/.kube/config
COPY ./kubectl /usr/local/bin/
COPY ./fluxd /usr/local/bin/

Expand Down
12 changes: 12 additions & 0 deletions docker/kubeconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: v1
clusters: []
contexts:
- context:
cluster: ""
namespace: default
user: ""
name: default
current-context: default
kind: Config
preferences: {}
users: []

0 comments on commit 335c768

Please sign in to comment.