Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix spec changes detection #446

Merged
merged 3 commits into from
Feb 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ jobs:
- run:
name: Verify code gen
command: make test-codegen
- run:
name: Release notes
command: make release-notes
- save_cache:
key: go-mod-v3-{{ checksum "go.sum" }}
paths:
Expand Down Expand Up @@ -71,6 +68,7 @@ jobs:
- restore_cache:
keys:
- go-mod-v3-{{ checksum "go.sum" }}
- run: make release-notes
- run: test/goreleaser.sh

e2e-istio-testing:
Expand Down
3 changes: 1 addition & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,7 @@ release-set: fmt version-set helm-package

release-notes:
cd /tmp && GH_REL_URL="https://github.com/buchanae/github-release-notes/releases/download/0.2.0/github-release-notes-linux-amd64-0.2.0.tar.gz" && \
curl -sSL $${GH_REL_URL} | tar xz && sudo mv github-release-notes /usr/local/bin/ && \
github-release-notes -org weaveworks -repo flagger -since-latest-release
curl -sSL $${GH_REL_URL} | tar xz && sudo mv github-release-notes /usr/local/bin/

reset-test:
kubectl delete -f ./artifacts/namespaces
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ go 1.13

require (
github.com/Masterminds/semver/v3 v3.0.3
github.com/davecgh/go-spew v1.1.1
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef // indirect
github.com/google/go-cmp v0.3.0
github.com/googleapis/gnostic v0.2.0 // indirect
github.com/imdario/mergo v0.3.7 // indirect
github.com/mitchellh/hashstructure v1.0.0
github.com/pkg/errors v0.8.1
github.com/prometheus/client_golang v1.0.0
go.uber.org/atomic v1.3.2 // indirect
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,6 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/hashstructure v1.0.0 h1:ZkRJX1CyOoTkar7p/mLS5TZU4nJ1Rn/F8u9dGS02Q3Y=
github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
Expand Down
152 changes: 128 additions & 24 deletions pkg/canary/deployment_controller_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package canary

import (
"k8s.io/apimachinery/pkg/api/errors"
"testing"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
Expand Down Expand Up @@ -39,29 +41,6 @@ func TestCanaryDeployer_Sync(t *testing.T) {
}
}

func TestCanaryDeployer_IsNewSpec(t *testing.T) {
mocks := newFixture()
err := mocks.deployer.Initialize(mocks.canary, true)
if err != nil {
t.Fatal(err.Error())
}

dep2 := newTestDeploymentV2()
_, err = mocks.kubeClient.AppsV1().Deployments("default").Update(dep2)
if err != nil {
t.Fatal(err.Error())
}

isNew, err := mocks.deployer.HasTargetChanged(mocks.canary)
if err != nil {
t.Fatal(err.Error())
}

if !isNew {
t.Errorf("Got %v wanted %v", isNew, true)
}
}

func TestCanaryDeployer_Promote(t *testing.T) {
mocks := newFixture()
err := mocks.deployer.Initialize(mocks.canary, true)
Expand Down Expand Up @@ -272,3 +251,128 @@ func TestCanaryDeployer_NoConfigTracking(t *testing.T) {
t.Errorf("Got config name %v wanted %v", configName, "podinfo-config-vol")
}
}

func TestCanaryDeployer_HasTargetChanged(t *testing.T) {
mocks := newFixture()
err := mocks.deployer.Initialize(mocks.canary, true)
if err != nil {
t.Fatal(err.Error())
}

// save last applied hash
canary, err := mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get("podinfo", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}
err = mocks.deployer.SyncStatus(canary, flaggerv1.CanaryStatus{Phase: flaggerv1.CanaryPhaseInitializing})
if err != nil {
t.Fatal(err.Error())
}

// save last promoted hash
canary, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get("podinfo", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}
err = mocks.deployer.SetStatusPhase(canary, flaggerv1.CanaryPhaseInitialized)
if err != nil {
t.Fatal(err.Error())
}

dep, err := mocks.kubeClient.AppsV1().Deployments("default").Get("podinfo", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}

depClone := dep.DeepCopy()
depClone.Spec.Template.Spec.Containers[0].Resources = corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: *resource.NewQuantity(100, resource.DecimalExponent),
},
}

// update pod spec
_, err = mocks.kubeClient.AppsV1().Deployments("default").Update(depClone)
if err != nil {
t.Fatal(err.Error())
}

canary, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get("podinfo", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}

// detect change in last applied spec
isNew, err := mocks.deployer.HasTargetChanged(canary)
if err != nil {
t.Fatal(err.Error())
}
if !isNew {
t.Errorf("Got %v wanted %v", isNew, true)
}

// save hash
err = mocks.deployer.SyncStatus(canary, flaggerv1.CanaryStatus{Phase: flaggerv1.CanaryPhaseProgressing})
if err != nil {
t.Fatal(err.Error())
}

dep, err = mocks.kubeClient.AppsV1().Deployments("default").Get("podinfo", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}

depClone = dep.DeepCopy()
depClone.Spec.Template.Spec.Containers[0].Resources = corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: *resource.NewQuantity(1000, resource.DecimalExponent),
},
}

// update pod spec
_, err = mocks.kubeClient.AppsV1().Deployments("default").Update(depClone)
if err != nil {
t.Fatal(err.Error())
}

canary, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get("podinfo", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}

// ignore change as hash should be the same with last promoted
isNew, err = mocks.deployer.HasTargetChanged(canary)
if err != nil {
t.Fatal(err.Error())
}
if isNew {
t.Errorf("Got %v wanted %v", isNew, false)
}

depClone = dep.DeepCopy()
depClone.Spec.Template.Spec.Containers[0].Resources = corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: *resource.NewQuantity(600, resource.DecimalExponent),
},
}

// update pod spec
_, err = mocks.kubeClient.AppsV1().Deployments("default").Update(depClone)
if err != nil {
t.Fatal(err.Error())
}

canary, err = mocks.flaggerClient.FlaggerV1beta1().Canaries("default").Get("podinfo", metav1.GetOptions{})
if err != nil {
t.Fatal(err.Error())
}

// detect change
isNew, err = mocks.deployer.HasTargetChanged(canary)
if err != nil {
t.Fatal(err.Error())
}
if !isNew {
t.Errorf("Got %v wanted %v", isNew, true)
}
}
6 changes: 6 additions & 0 deletions pkg/canary/fixture.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
appsv1 "k8s.io/api/apps/v1"
hpav2 "k8s.io/api/autoscaling/v2beta1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
Expand Down Expand Up @@ -244,6 +245,11 @@ func newTestDeployment() *appsv1.Deployment {
"./podinfo",
"--port=9898",
},
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: *resource.NewQuantity(1000, resource.DecimalExponent),
},
},
Args: nil,
WorkingDir: "",
Ports: []corev1.ContainerPort{
Expand Down
33 changes: 26 additions & 7 deletions pkg/canary/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,48 @@ package canary

import (
"fmt"
"hash/fnv"

"github.com/davecgh/go-spew/spew"
"k8s.io/apimachinery/pkg/util/rand"

"github.com/mitchellh/hashstructure"
flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
)

// hasSpecChanged computes the hash of the spec and compares it with the
// last applied spec, if the last applied hash is different but not equal
// to last promoted one the it returns true
func hasSpecChanged(cd *flaggerv1.Canary, spec interface{}) (bool, error) {
if cd.Status.LastAppliedSpec == "" {
return true, nil
}

newHash, err := hashstructure.Hash(spec, nil)
if err != nil {
return false, fmt.Errorf("hash error %v", err)
}
newHash := computeHash(spec)

// do not trigger a canary deployment on manual rollback
if cd.Status.LastPromotedSpec == fmt.Sprintf("%d", newHash) {
if cd.Status.LastPromotedSpec == newHash {
return false, nil
}

if cd.Status.LastAppliedSpec != fmt.Sprintf("%d", newHash) {
if cd.Status.LastAppliedSpec != newHash {
return true, nil
}

return false, nil
}

// computeHash returns a hash value calculated from a spec using the spew library
// which follows pointers and prints actual values of the nested objects
// ensuring the hash does not change when a pointer changes.
func computeHash(spec interface{}) string {
hasher := fnv.New32a()
printer := spew.ConfigState{
Indent: " ",
SortKeys: true,
DisableMethods: true,
SpewKeys: true,
}
printer.Fprintf(hasher, "%#v", spec)

return rand.SafeEncodeString(fmt.Sprint(hasher.Sum32()))
}
10 changes: 3 additions & 7 deletions pkg/canary/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"fmt"
"strings"

"github.com/mitchellh/hashstructure"
ex "github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -36,13 +35,10 @@ func (c *DeploymentController) SyncStatus(cd *flaggerv1.Canary, status flaggerv1
}

func syncCanaryStatus(flaggerClient clientset.Interface, cd *flaggerv1.Canary, status flaggerv1.CanaryStatus, canaryResource interface{}, setAll func(cdCopy *flaggerv1.Canary)) error {
hash, err := hashstructure.Hash(canaryResource, nil)
if err != nil {
return ex.Wrap(err, "SyncStatus hash error")
}
hash := computeHash(canaryResource)

firstTry := true
err = retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
err := retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) {
var selErr error
if !firstTry {
cd, selErr = flaggerClient.FlaggerV1beta1().Canaries(cd.Namespace).Get(cd.GetName(), metav1.GetOptions{})
Expand All @@ -56,7 +52,7 @@ func syncCanaryStatus(flaggerClient clientset.Interface, cd *flaggerv1.Canary, s
cdCopy.Status.CanaryWeight = status.CanaryWeight
cdCopy.Status.FailedChecks = status.FailedChecks
cdCopy.Status.Iterations = status.Iterations
cdCopy.Status.LastAppliedSpec = fmt.Sprintf("%d", hash)
cdCopy.Status.LastAppliedSpec = hash
cdCopy.Status.LastTransitionTime = metav1.Now()
setAll(cdCopy)

Expand Down
4 changes: 0 additions & 4 deletions test/goreleaser.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ set -e

TAR_FILE="/tmp/goreleaser.tar.gz"
RELEASES_URL="https://github.com/goreleaser/goreleaser/releases"
GH_REL_DIR="github-release-notes-linux-amd64-0.2.0"
GH_REL_URL="https://github.com/buchanae/github-release-notes/releases/download/0.2.0/${GH_REL_DIR}.tar.gz"
test -z "$TMPDIR" && TMPDIR="$(mktemp -d)"

last_version() {
Expand All @@ -23,8 +21,6 @@ download() {
rm -f "$TAR_FILE"
curl -s -L -o "$TAR_FILE" \
"$RELEASES_URL/download/$VERSION/goreleaser_$(uname -s)_$(uname -m).tar.gz"

curl -sSL ${GH_REL_URL} | tar xz && sudo mv ${GH_REL_DIR}/github-release-notes /usr/local/bin/ && rm -rf ${GH_REL_DIR}
}

download
Expand Down