Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Robert Kwolek
2020-08-20 21:03:43 +02:00
67 changed files with 2418 additions and 307 deletions
+1
View File
@@ -9,4 +9,5 @@ const (
GlooProvider string = "gloo"
NGINXProvider string = "nginx"
KubernetesProvider string = "kubernetes"
SkipperProvider string = "skipper"
)
+58 -62
View File
@@ -5,6 +5,7 @@ import (
"crypto/sha256"
"encoding/json"
"fmt"
"strings"
"go.uber.org/zap"
corev1 "k8s.io/api/core/v1"
@@ -29,6 +30,8 @@ type ConfigRefType string
const (
ConfigRefMap ConfigRefType = "configmap"
ConfigRefSecret ConfigRefType = "secret"
configTrackingDisabledAnnotationKey = "flagger.app/config-tracking"
)
// ConfigRef holds the reference to a tracked Kubernetes ConfigMap or Secret
@@ -50,6 +53,10 @@ func checksum(data interface{}) string {
return fmt.Sprintf("%x", hashBytes[:8])
}
func configIsDisabled(annotations map[string]string) bool {
return strings.HasPrefix(annotations[configTrackingDisabledAnnotationKey], "disable")
}
// getRefFromConfigMap transforms a Kubernetes ConfigMap into a ConfigRef
// and computes the checksum of the ConfigMap data
func (ct *ConfigTracker) getRefFromConfigMap(name string, namespace string) (*ConfigRef, error) {
@@ -58,6 +65,10 @@ func (ct *ConfigTracker) getRefFromConfigMap(name string, namespace string) (*Co
return nil, fmt.Errorf("configmap %s.%s get query error: %w", name, namespace, err)
}
if configIsDisabled(config.GetAnnotations()) {
return nil, nil
}
return &ConfigRef{
Name: config.Name,
Type: ConfigRefMap,
@@ -82,6 +93,10 @@ func (ct *ConfigTracker) getRefFromSecret(name string, namespace string) (*Confi
return nil, nil
}
if configIsDisabled(secret.GetAnnotations()) {
return nil, nil
}
return &ConfigRef{
Name: secret.Name,
Type: ConfigRefSecret,
@@ -92,23 +107,22 @@ func (ct *ConfigTracker) getRefFromSecret(name string, namespace string) (*Confi
// GetTargetConfigs scans the target deployment for Kubernetes ConfigMaps and Secretes
// and returns a list of config references
func (ct *ConfigTracker) GetTargetConfigs(cd *flaggerv1.Canary) (map[string]ConfigRef, error) {
res := make(map[string]ConfigRef)
targetName := cd.Spec.TargetRef.Name
var vs []corev1.Volume
var cs []corev1.Container
switch cd.Spec.TargetRef.Kind {
case "Deployment":
targetDep, err := ct.KubeClient.AppsV1().Deployments(cd.Namespace).Get(context.TODO(), targetName, metav1.GetOptions{})
if err != nil {
return res, fmt.Errorf("deployment %s.%s get query error: %w", targetName, cd.Namespace, err)
return nil, fmt.Errorf("deployment %s.%s get query error: %w", targetName, cd.Namespace, err)
}
vs = targetDep.Spec.Template.Spec.Volumes
cs = targetDep.Spec.Template.Spec.Containers
case "DaemonSet":
targetDae, err := ct.KubeClient.AppsV1().DaemonSets(cd.Namespace).Get(context.TODO(), targetName, metav1.GetOptions{})
if err != nil {
return res, fmt.Errorf("daemonset %s.%s get query error: %w", targetName, cd.Namespace, err)
return nil, fmt.Errorf("daemonset %s.%s get query error: %w", targetName, cd.Namespace, err)
}
vs = targetDae.Spec.Template.Spec.Volumes
cs = targetDae.Spec.Template.Spec.Containers
@@ -116,48 +130,31 @@ func (ct *ConfigTracker) GetTargetConfigs(cd *flaggerv1.Canary) (map[string]Conf
return nil, fmt.Errorf("TargetRef.Kind invalid: %s", cd.Spec.TargetRef.Kind)
}
type void struct{}
var member void
secretNames := map[string]void{}
configMapNames := map[string]void{}
// scan volumes
for _, volume := range vs {
if cmv := volume.ConfigMap; cmv != nil {
config, err := ct.getRefFromConfigMap(cmv.Name, cd.Namespace)
if err != nil {
ct.Logger.Errorf("getRefFromConfigMap failed: %v", err)
continue
}
res[config.GetName()] = *config
name := cmv.Name
configMapNames[name] = member
}
if sv := volume.Secret; sv != nil {
secret, err := ct.getRefFromSecret(sv.SecretName, cd.Namespace)
if err != nil {
ct.Logger.Errorf("getRefFromSecret failed: %v", err)
continue
}
if secret != nil {
res[secret.GetName()] = *secret
}
name := sv.SecretName
secretNames[name] = member
}
if projected := volume.Projected; projected != nil {
for _, source := range projected.Sources {
if cmv := source.ConfigMap; cmv != nil {
config, err := ct.getRefFromConfigMap(cmv.Name, cd.Namespace)
if err != nil {
ct.Logger.Errorf("getRefFromConfigMap failed: %v", err)
continue
}
res[config.GetName()] = *config
name := cmv.Name
configMapNames[name] = member
}
if sv := source.Secret; sv != nil {
secret, err := ct.getRefFromSecret(sv.Name, cd.Namespace)
if err != nil {
ct.Logger.Errorf("getRefFromSecret failed: %v", err)
continue
}
if secret != nil {
res[secret.GetName()] = *secret
}
name := sv.Name
secretNames[name] = member
}
}
}
@@ -170,22 +167,10 @@ func (ct *ConfigTracker) GetTargetConfigs(cd *flaggerv1.Canary) (map[string]Conf
switch {
case env.ValueFrom.ConfigMapKeyRef != nil:
name := env.ValueFrom.ConfigMapKeyRef.LocalObjectReference.Name
config, err := ct.getRefFromConfigMap(name, cd.Namespace)
if err != nil {
ct.Logger.Errorf("getRefFromConfigMap failed: %v", err)
continue
}
res[config.GetName()] = *config
configMapNames[name] = member
case env.ValueFrom.SecretKeyRef != nil:
name := env.ValueFrom.SecretKeyRef.LocalObjectReference.Name
secret, err := ct.getRefFromSecret(name, cd.Namespace)
if err != nil {
ct.Logger.Errorf("getRefFromSecret failed: %v", err)
continue
}
if secret != nil {
res[secret.GetName()] = *secret
}
secretNames[name] = member
}
}
}
@@ -194,26 +179,37 @@ func (ct *ConfigTracker) GetTargetConfigs(cd *flaggerv1.Canary) (map[string]Conf
switch {
case envFrom.ConfigMapRef != nil:
name := envFrom.ConfigMapRef.LocalObjectReference.Name
config, err := ct.getRefFromConfigMap(name, cd.Namespace)
if err != nil {
ct.Logger.Errorf("getRefFromConfigMap failed %v", err)
continue
}
res[config.GetName()] = *config
configMapNames[name] = member
case envFrom.SecretRef != nil:
name := envFrom.SecretRef.LocalObjectReference.Name
secret, err := ct.getRefFromSecret(name, cd.Namespace)
if err != nil {
ct.Logger.Errorf("getRefFromSecret failed %v", err)
continue
}
if secret != nil {
res[secret.GetName()] = *secret
}
secretNames[name] = member
}
}
}
res := make(map[string]ConfigRef)
for configMapName := range configMapNames {
config, err := ct.getRefFromConfigMap(configMapName, cd.Namespace)
if err != nil {
ct.Logger.Errorf("getRefFromConfigMap failed: %v", err)
continue
}
if config != nil {
res[config.GetName()] = *config
}
}
for secretName := range secretNames {
secret, err := ct.getRefFromSecret(secretName, cd.Namespace)
if err != nil {
ct.Logger.Errorf("getRefFromSecret failed: %v", err)
continue
}
if secret != nil {
res[secret.GetName()] = *secret
}
}
return res, nil
}
+124 -3
View File
@@ -10,6 +10,19 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestConfigIsDisabled(t *testing.T) {
for _, c := range []struct {
annotations map[string]string
exp bool
}{
{annotations: map[string]string{configTrackingDisabledAnnotationKey: "disable"}, exp: true},
{annotations: map[string]string{"app": "disable"}, exp: false},
{annotations: map[string]string{}, exp: false},
} {
assert.Equal(t, configIsDisabled(c.annotations), c.exp)
}
}
func TestConfigTracker_ConfigMaps(t *testing.T) {
t.Run("deployment", func(t *testing.T) {
mocks := newDeploymentFixture()
@@ -46,6 +59,33 @@ func TestConfigTracker_ConfigMaps(t *testing.T) {
if assert.NoError(t, err) {
assert.Equal(t, configMapProjected.Data["color"], configPrimaryProjected.Data["color"])
}
_, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-tracker-enabled", metav1.GetOptions{})
assert.NoError(t, err)
_, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-tracker-enabled-primary", metav1.GetOptions{})
assert.NoError(t, err)
_, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-tracker-disabled", metav1.GetOptions{})
assert.NoError(t, err)
_, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-tracker-disabled-primary", metav1.GetOptions{})
assert.Error(t, err)
var trackedVolPresent, originalVolPresent bool
for _, vol := range depPrimary.Spec.Template.Spec.Volumes {
if vol.ConfigMap != nil {
switch vol.ConfigMap.Name {
case "podinfo-config-tracker-enabled":
assert.Fail(t, "primary Deployment does not contain a volume for config-tracked configmap %q", vol.ConfigMap.Name)
case "podinfo-config-tracker-enabled-primary":
trackedVolPresent = true
case "podinfo-config-tracker-disabled":
originalVolPresent = true
case "podinfo-config-tracker-disabled-primary":
assert.Fail(t, "primary Deployment incorrectly contains a volume for a copy of an untracked configmap %q", vol.ConfigMap.Name)
}
}
}
assert.True(t, trackedVolPresent, "Volume for primary copy of config-tracked configmap should be present")
assert.True(t, originalVolPresent, "Volume for original configmap with disabled tracking should be present")
})
t.Run("daemonset", func(t *testing.T) {
@@ -56,10 +96,10 @@ func TestConfigTracker_ConfigMaps(t *testing.T) {
err := mocks.controller.Initialize(mocks.canary)
require.NoError(t, err)
depPrimary, err := mocks.kubeClient.AppsV1().DaemonSets("default").Get(context.TODO(), "podinfo-primary", metav1.GetOptions{})
daePrimary, err := mocks.kubeClient.AppsV1().DaemonSets("default").Get(context.TODO(), "podinfo-primary", metav1.GetOptions{})
require.NoError(t, err)
configPrimaryVolName := depPrimary.Spec.Template.Spec.Volumes[0].VolumeSource.ConfigMap.LocalObjectReference.Name
configPrimaryVolName := daePrimary.Spec.Template.Spec.Volumes[0].VolumeSource.ConfigMap.LocalObjectReference.Name
assert.Equal(t, "podinfo-config-vol-primary", configPrimaryVolName)
configPrimary, err := mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-env-primary", metav1.GetOptions{})
@@ -77,13 +117,40 @@ func TestConfigTracker_ConfigMaps(t *testing.T) {
assert.Equal(t, configMap.Data["color"], configPrimaryVol.Data["color"])
}
configProjectedName := depPrimary.Spec.Template.Spec.Volumes[2].VolumeSource.Projected.Sources[0].ConfigMap.Name
configProjectedName := daePrimary.Spec.Template.Spec.Volumes[2].VolumeSource.Projected.Sources[0].ConfigMap.Name
assert.Equal(t, "podinfo-config-projected-primary", configProjectedName)
configPrimaryProjected, err := mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-vol-primary", metav1.GetOptions{})
if assert.NoError(t, err) {
assert.Equal(t, configMapProjected.Data["color"], configPrimaryProjected.Data["color"])
}
_, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-tracker-enabled", metav1.GetOptions{})
assert.NoError(t, err)
_, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-tracker-enabled-primary", metav1.GetOptions{})
assert.NoError(t, err)
_, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-tracker-disabled", metav1.GetOptions{})
assert.NoError(t, err)
_, err = mocks.kubeClient.CoreV1().ConfigMaps("default").Get(context.TODO(), "podinfo-config-tracker-disabled-primary", metav1.GetOptions{})
assert.Error(t, err)
var trackedVolPresent, originalVolPresent bool
for _, vol := range daePrimary.Spec.Template.Spec.Volumes {
if vol.ConfigMap != nil {
switch vol.ConfigMap.Name {
case "podinfo-config-tracker-enabled":
assert.Fail(t, "primary Deployment does not contain a volume for config-tracked configmap %q", vol.ConfigMap.Name)
case "podinfo-config-tracker-enabled-primary":
trackedVolPresent = true
case "podinfo-config-tracker-disabled":
originalVolPresent = true
case "podinfo-config-tracker-disabled-primary":
assert.Fail(t, "primary Deployment incorrectly contains a volume for a copy of an untracked configmap %q", vol.ConfigMap.Name)
}
}
}
assert.True(t, trackedVolPresent, "Volume for primary copy of config-tracked configmap should be present")
assert.True(t, originalVolPresent, "Volume for original configmap with disabled tracking should be present")
})
}
@@ -123,6 +190,33 @@ func TestConfigTracker_Secrets(t *testing.T) {
if assert.NoError(t, err) {
assert.Equal(t, string(secretProjected.Data["apiKey"]), string(secretPrimaryProjected.Data["apiKey"]))
}
_, err = mocks.kubeClient.CoreV1().Secrets("default").Get(context.TODO(), "podinfo-secret-tracker-enabled", metav1.GetOptions{})
assert.NoError(t, err)
_, err = mocks.kubeClient.CoreV1().Secrets("default").Get(context.TODO(), "podinfo-secret-tracker-enabled-primary", metav1.GetOptions{})
assert.NoError(t, err)
_, err = mocks.kubeClient.CoreV1().Secrets("default").Get(context.TODO(), "podinfo-secret-tracker-disabled", metav1.GetOptions{})
assert.NoError(t, err)
_, err = mocks.kubeClient.CoreV1().Secrets("default").Get(context.TODO(), "podinfo-secret-tracker-disabled-primary", metav1.GetOptions{})
assert.Error(t, err)
var trackedVolPresent, originalVolPresent bool
for _, vol := range depPrimary.Spec.Template.Spec.Volumes {
if vol.Secret != nil {
switch vol.Secret.SecretName {
case "podinfo-secret-tracker-enabled":
assert.Fail(t, "primary Deployment does not contain a volume for config-tracked secret %q", vol.Secret.SecretName)
case "podinfo-secret-tracker-enabled-primary":
trackedVolPresent = true
case "podinfo-secret-tracker-disabled":
originalVolPresent = true
case "podinfo-secret-tracker-disabled-primary":
assert.Fail(t, "primary Deployment incorrectly contains a volume for a copy of an untracked secret %q", vol.Secret.SecretName)
}
}
}
assert.True(t, trackedVolPresent, "Volume for primary copy of config-tracked secret should be present")
assert.True(t, originalVolPresent, "Volume for original secret with disabled tracking should be present")
})
t.Run("daemonset", func(t *testing.T) {
@@ -160,5 +254,32 @@ func TestConfigTracker_Secrets(t *testing.T) {
if assert.NoError(t, err) {
assert.Equal(t, string(secretProjected.Data["apiKey"]), string(secretPrimaryProjected.Data["apiKey"]))
}
_, err = mocks.kubeClient.CoreV1().Secrets("default").Get(context.TODO(), "podinfo-secret-tracker-enabled", metav1.GetOptions{})
assert.NoError(t, err)
_, err = mocks.kubeClient.CoreV1().Secrets("default").Get(context.TODO(), "podinfo-secret-tracker-enabled-primary", metav1.GetOptions{})
assert.NoError(t, err)
_, err = mocks.kubeClient.CoreV1().Secrets("default").Get(context.TODO(), "podinfo-secret-tracker-disabled", metav1.GetOptions{})
assert.NoError(t, err)
_, err = mocks.kubeClient.CoreV1().Secrets("default").Get(context.TODO(), "podinfo-secret-tracker-disabled-primary", metav1.GetOptions{})
assert.Error(t, err)
var trackedVolPresent, originalVolPresent bool
for _, vol := range daePrimary.Spec.Template.Spec.Volumes {
if vol.Secret != nil {
switch vol.Secret.SecretName {
case "podinfo-secret-tracker-enabled":
assert.Fail(t, "primary Deployment does not contain a volume for config-tracked secret %q", vol.Secret.SecretName)
case "podinfo-secret-tracker-enabled-primary":
trackedVolPresent = true
case "podinfo-secret-tracker-disabled":
originalVolPresent = true
case "podinfo-secret-tracker-disabled-primary":
assert.Fail(t, "primary Deployment incorrectly contains a volume for a copy of an untracked secret %q", vol.Secret.SecretName)
}
}
}
assert.True(t, trackedVolPresent, "Volume for primary copy of config-tracked secret should be present")
assert.True(t, originalVolPresent, "Volume for original secret with disabled tracking should be present")
})
}
+190
View File
@@ -35,10 +35,14 @@ func newDaemonSetFixture() daemonSetControllerFixture {
newDaemonSetControllerTestConfigMapEnv(),
newDaemonSetControllerTestConfigMapVol(),
newDaemonSetControllerTestConfigProjected(),
newDaemonSetControllerTestConfigMapTrackerEnabled(),
newDaemonSetControllerTestConfigMapTrackerDisabled(),
newDaemonSetControllerTestSecret(),
newDaemonSetControllerTestSecretEnv(),
newDaemonSetControllerTestSecretVol(),
newDaemonSetControllerTestSecretProjected(),
newDaemonSetControllerTestSecretTrackerEnabled(),
newDaemonSetControllerTestSecretTrackerDisabled(),
)
logger, _ := logger.NewLogger("debug")
@@ -130,6 +134,42 @@ func newDaemonSetControllerTestConfigMapVol() *corev1.ConfigMap {
}
}
func newDaemonSetControllerTestConfigMapTrackerEnabled() *corev1.ConfigMap {
return &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()},
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "podinfo-config-tracker-enabled",
Annotations: map[string]string{
"unrelated-annotation-1": ":)",
"flagger.app/config-tracking": "enabled",
"unrelated-annotation-2": "<3",
},
},
Data: map[string]string{
"color": "red",
},
}
}
func newDaemonSetControllerTestConfigMapTrackerDisabled() *corev1.ConfigMap {
return &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()},
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "podinfo-config-tracker-disabled",
Annotations: map[string]string{
"unrelated-annotation-1": "c:",
"flagger.app/config-tracking": "disabled",
"unrelated-annotation-2": "^-^",
},
},
Data: map[string]string{
"color": "red",
},
}
}
func newDaemonSetControllerTestSecret() *corev1.Secret {
return &corev1.Secret{
TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()},
@@ -186,6 +226,44 @@ func newDaemonSetControllerTestSecretVol() *corev1.Secret {
}
}
func newDaemonSetControllerTestSecretTrackerEnabled() *corev1.Secret {
return &corev1.Secret{
TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()},
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "podinfo-secret-tracker-enabled",
Annotations: map[string]string{
"unrelated-annotation-1": ":)",
"flagger.app/config-tracking": "enabled",
"unrelated-annotation-2": "<3",
},
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{
"apiKey": []byte("test"),
},
}
}
func newDaemonSetControllerTestSecretTrackerDisabled() *corev1.Secret {
return &corev1.Secret{
TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()},
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "podinfo-secret-tracker-disabled",
Annotations: map[string]string{
"unrelated-annotation-1": "c:",
"flagger.app/config-tracking": "disabled",
"unrelated-annotation-2": "^-^",
},
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{
"apiKey": []byte("test"),
},
}
}
func newDaemonSetControllerTestCanary() *flaggerv1.Canary {
cd := &flaggerv1.Canary{
TypeMeta: metav1.TypeMeta{APIVersion: flaggerv1.SchemeGroupVersion.String()},
@@ -297,6 +375,26 @@ func newDaemonSetControllerTestPodInfo() *appsv1.DaemonSet {
MountPath: "/etc/podinfo/secret",
ReadOnly: true,
},
{
Name: "config-tracker-enabled",
MountPath: "/etc/podinfo/config-tracker-enabled",
ReadOnly: true,
},
{
Name: "config-tracker-disabled",
MountPath: "/etc/podinfo/config-tracker-disabled",
ReadOnly: true,
},
{
Name: "secret-tracker-enabled",
MountPath: "/etc/podinfo/secret-tracker-enabled",
ReadOnly: true,
},
{
Name: "secret-tracker-disabled",
MountPath: "/etc/podinfo/secret-tracker-disabled",
ReadOnly: true,
},
},
},
},
@@ -354,6 +452,42 @@ func newDaemonSetControllerTestPodInfo() *appsv1.DaemonSet {
},
},
},
{
Name: "config-tracker-enabled",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "podinfo-config-tracker-enabled",
},
},
},
},
{
Name: "config-tracker-disabled",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "podinfo-config-tracker-disabled",
},
},
},
},
{
Name: "secret-tracker-enabled",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: "podinfo-secret-tracker-enabled",
},
},
},
{
Name: "secret-tracker-disabled",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: "podinfo-secret-tracker-disabled",
},
},
},
},
},
},
@@ -441,6 +575,26 @@ func newDaemonSetControllerTestPodInfoV2() *appsv1.DaemonSet {
MountPath: "/etc/podinfo/secret",
ReadOnly: true,
},
{
Name: "config-tracker-enabled",
MountPath: "/etc/podinfo/config-tracker-enabled",
ReadOnly: true,
},
{
Name: "config-tracker-disabled",
MountPath: "/etc/podinfo/config-tracker-disabled",
ReadOnly: true,
},
{
Name: "secret-tracker-enabled",
MountPath: "/etc/podinfo/secret-tracker-enabled",
ReadOnly: true,
},
{
Name: "secret-tracker-disabled",
MountPath: "/etc/podinfo/secret-tracker-disabled",
ReadOnly: true,
},
},
},
},
@@ -498,6 +652,42 @@ func newDaemonSetControllerTestPodInfoV2() *appsv1.DaemonSet {
},
},
},
{
Name: "config-tracker-enabled",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "podinfo-config-tracker-enabled",
},
},
},
},
{
Name: "config-tracker-disabled",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "podinfo-config-tracker-disabled",
},
},
},
},
{
Name: "secret-tracker-enabled",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: "podinfo-secret-tracker-enabled",
},
},
},
{
Name: "secret-tracker-disabled",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: "podinfo-secret-tracker-disabled",
},
},
},
},
},
},
+190
View File
@@ -64,10 +64,14 @@ func newDeploymentFixture() deploymentControllerFixture {
newDeploymentControllerTestConfigMapEnv(),
newDeploymentControllerTestConfigMapVol(),
newDeploymentControllerTestConfigProjected(),
newDeploymentControllerTestConfigMapTrackerEnabled(),
newDeploymentControllerTestConfigMapTrackerDisabled(),
newDeploymentControllerTestSecret(),
newDeploymentControllerTestSecretEnv(),
newDeploymentControllerTestSecretVol(),
newDeploymentControllerTestSecretProjected(),
newDeploymentControllerTestSecretTrackerEnabled(),
newDeploymentControllerTestSecretTrackerDisabled(),
)
logger, _ := logger.NewLogger("debug")
@@ -146,6 +150,42 @@ func newDeploymentControllerTestConfigMapEnv() *corev1.ConfigMap {
}
}
func newDeploymentControllerTestConfigMapTrackerEnabled() *corev1.ConfigMap {
return &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()},
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "podinfo-config-tracker-enabled",
Annotations: map[string]string{
"unrelated-annotation-1": ":)",
"flagger.app/config-tracking": "enabled",
"unrelated-annotation-2": "<3",
},
},
Data: map[string]string{
"color": "red",
},
}
}
func newDeploymentControllerTestConfigMapTrackerDisabled() *corev1.ConfigMap {
return &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()},
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "podinfo-config-tracker-disabled",
Annotations: map[string]string{
"unrelated-annotation-1": "c:",
"flagger.app/config-tracking": "disabled",
"unrelated-annotation-2": "^-^",
},
},
Data: map[string]string{
"color": "red",
},
}
}
func newDeploymentControllerTestConfigMapVol() *corev1.ConfigMap {
return &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()},
@@ -215,6 +255,44 @@ func newDeploymentControllerTestSecretVol() *corev1.Secret {
}
}
func newDeploymentControllerTestSecretTrackerEnabled() *corev1.Secret {
return &corev1.Secret{
TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()},
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "podinfo-secret-tracker-enabled",
Annotations: map[string]string{
"unrelated-annotation-1": ":)",
"flagger.app/config-tracking": "enabled",
"unrelated-annotation-2": "<3",
},
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{
"apiKey": []byte("test"),
},
}
}
func newDeploymentControllerTestSecretTrackerDisabled() *corev1.Secret {
return &corev1.Secret{
TypeMeta: metav1.TypeMeta{APIVersion: corev1.SchemeGroupVersion.String()},
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "podinfo-secret-tracker-disabled",
Annotations: map[string]string{
"unrelated-annotation-1": "c:",
"flagger.app/config-tracking": "disabled",
"unrelated-annotation-2": "^-^",
},
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{
"apiKey": []byte("test"),
},
}
}
func newDeploymentControllerTestCanary() *flaggerv1.Canary {
cd := &flaggerv1.Canary{
TypeMeta: metav1.TypeMeta{APIVersion: flaggerv1.SchemeGroupVersion.String()},
@@ -337,6 +415,26 @@ func newDeploymentControllerTest() *appsv1.Deployment {
MountPath: "/etc/podinfo/secret",
ReadOnly: true,
},
{
Name: "config-tracker-enabled",
MountPath: "/etc/podinfo/config-tracker-enabled",
ReadOnly: true,
},
{
Name: "config-tracker-disabled",
MountPath: "/etc/podinfo/config-tracker-disabled",
ReadOnly: true,
},
{
Name: "secret-tracker-enabled",
MountPath: "/etc/podinfo/secret-tracker-enabled",
ReadOnly: true,
},
{
Name: "secret-tracker-disabled",
MountPath: "/etc/podinfo/secret-tracker-disabled",
ReadOnly: true,
},
},
},
},
@@ -394,6 +492,42 @@ func newDeploymentControllerTest() *appsv1.Deployment {
},
},
},
{
Name: "config-tracker-enabled",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "podinfo-config-tracker-enabled",
},
},
},
},
{
Name: "config-tracker-disabled",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "podinfo-config-tracker-disabled",
},
},
},
},
{
Name: "secret-tracker-enabled",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: "podinfo-secret-tracker-enabled",
},
},
},
{
Name: "secret-tracker-disabled",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: "podinfo-secret-tracker-disabled",
},
},
},
},
},
},
@@ -482,6 +616,26 @@ func newDeploymentControllerTestV2() *appsv1.Deployment {
MountPath: "/etc/podinfo/secret",
ReadOnly: true,
},
{
Name: "config-tracker-enabled",
MountPath: "/etc/podinfo/config-tracker-enabled",
ReadOnly: true,
},
{
Name: "config-tracker-disabled",
MountPath: "/etc/podinfo/config-tracker-disabled",
ReadOnly: true,
},
{
Name: "secret-tracker-enabled",
MountPath: "/etc/podinfo/secret-tracker-enabled",
ReadOnly: true,
},
{
Name: "secret-tracker-disabled",
MountPath: "/etc/podinfo/secret-tracker-disabled",
ReadOnly: true,
},
},
},
},
@@ -539,6 +693,42 @@ func newDeploymentControllerTestV2() *appsv1.Deployment {
},
},
},
{
Name: "config-tracker-enabled",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "podinfo-config-tracker-enabled",
},
},
},
},
{
Name: "config-tracker-disabled",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: "podinfo-config-tracker-disabled",
},
},
},
},
{
Name: "secret-tracker-enabled",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: "podinfo-secret-tracker-enabled",
},
},
},
{
Name: "secret-tracker-disabled",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: "podinfo-secret-tracker-disabled",
},
},
},
},
},
},
+4
View File
@@ -56,6 +56,10 @@ func (factory Factory) Observer(provider string) Interface {
return &HttpObserver{
client: factory.Client,
}
case provider == flaggerv1.SkipperProvider:
return &SkipperObserver{
client: factory.Client,
}
default:
return &IstioObserver{
client: factory.Client,
+82
View File
@@ -0,0 +1,82 @@
package observers
import (
"fmt"
"regexp"
"time"
flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
"github.com/weaveworks/flagger/pkg/logger"
"github.com/weaveworks/flagger/pkg/metrics/providers"
)
const routePattern = `{{- $route := printf "kube(ew)?_%s__%s_canary__.*__%s_canary(_[0-9]+)?" namespace ingress service }}`
var skipperQueries = map[string]string{
"request-success-rate": routePattern + `
sum(rate(skipper_response_duration_seconds_bucket{route=~"{{ $route }}",code!~"5..",le="+Inf"}[{{ interval }}])) /
sum(rate(skipper_response_duration_seconds_bucket{route=~"{{ $route }}",le="+Inf"}[{{ interval }}])) * 100`,
"request-duration": routePattern + `
sum(rate(skipper_serve_route_duration_seconds_sum{route=~"{{ $route }}"}[{{ interval }}])) /
sum(rate(skipper_serve_route_duration_seconds_count{route=~"{{ $route }}"}[{{ interval }}])) * 1000`,
}
// SkipperObserver Implementation for Skipper (https://github.com/zalando/skipper)
type SkipperObserver struct {
client providers.Interface
}
// GetRequestSuccessRate return value for Skipper Request Success Rate
func (ob *SkipperObserver) GetRequestSuccessRate(model flaggerv1.MetricTemplateModel) (float64, error) {
model = encodeModelForSkipper(model)
query, err := RenderQuery(skipperQueries["request-success-rate"], model)
if err != nil {
return 0, fmt.Errorf("rendering query failed: %w", err)
}
logger, _ := logger.NewLoggerWithEncoding("debug", "json")
logger.Debugf("GetRequestSuccessRate: %s", query)
value, err := ob.client.RunQuery(query)
if err != nil {
return 0, fmt.Errorf("running query failed: %w", err)
}
return value, nil
}
// GetRequestDuration return value for Skipper Request Duration
func (ob *SkipperObserver) GetRequestDuration(model flaggerv1.MetricTemplateModel) (time.Duration, error) {
model = encodeModelForSkipper(model)
query, err := RenderQuery(skipperQueries["request-duration"], model)
if err != nil {
return 0, fmt.Errorf("rendering query failed: %w", err)
}
logger, _ := logger.NewLoggerWithEncoding("debug", "json")
logger.Debugf("GetRequestDuration: %s", query)
value, err := ob.client.RunQuery(query)
if err != nil {
return 0, fmt.Errorf("running query failed: %w", err)
}
ms := time.Duration(int64(value)) * time.Millisecond
return ms, nil
}
// encodeModelForSkipper replaces non word character in model with underscore to match route names
// https://github.com/zalando/skipper/blob/dd70bd65e7f99cfb5dd6b6f71885d9fe3b2707f6/dataclients/kubernetes/ingress.go#L101
func encodeModelForSkipper(model flaggerv1.MetricTemplateModel) flaggerv1.MetricTemplateModel {
nonWord := regexp.MustCompile(`\W`)
model.Ingress = nonWord.ReplaceAllString(model.Ingress, "_")
model.Name = nonWord.ReplaceAllString(model.Name, "_")
model.Namespace = nonWord.ReplaceAllString(model.Namespace, "_")
model.Service = nonWord.ReplaceAllString(model.Service, "_")
model.Target = nonWord.ReplaceAllString(model.Target, "_")
return model
}
+97
View File
@@ -0,0 +1,97 @@
package observers
import (
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
"github.com/weaveworks/flagger/pkg/metrics/providers"
)
func TestSkipperObserver_GetRequestSuccessRate(t *testing.T) {
t.Run("ok", func(t *testing.T) {
expected := ` sum(rate(skipper_response_duration_seconds_bucket{route=~"kube(ew)?_skipper__skipper_ingress_canary__.*__backend_canary(_[0-9]+)?",code!~"5..",le="+Inf"}[1m])) / sum(rate(skipper_response_duration_seconds_bucket{route=~"kube(ew)?_skipper__skipper_ingress_canary__.*__backend_canary(_[0-9]+)?",le="+Inf"}[1m])) * 100`
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
promql := r.URL.Query()["query"][0]
assert.Equal(t, expected, promql)
json := `{"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1,"100"]}]}}`
w.Write([]byte(json))
}))
defer ts.Close()
client, err := providers.NewPrometheusProvider(flaggerv1.MetricTemplateProvider{
Type: "prometheus",
Address: ts.URL,
SecretRef: nil,
}, nil)
require.NoError(t, err)
observer := &SkipperObserver{client: client}
val, err := observer.GetRequestSuccessRate(flaggerv1.MetricTemplateModel{
Namespace: "skipper",
Interval: "1m",
Service: "backend",
Ingress: "skipper-ingress",
})
require.NoError(t, err)
assert.Equal(t, float64(100), val)
})
t.Run("no values", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json := `{"status":"success","data":{"resultType":"vector","result":[]}}`
w.Write([]byte(json))
}))
defer ts.Close()
client, err := providers.NewPrometheusProvider(flaggerv1.MetricTemplateProvider{
Type: "prometheus",
Address: ts.URL,
SecretRef: nil,
}, nil)
require.NoError(t, err)
observer := &SkipperObserver{client: client}
_, err = observer.GetRequestSuccessRate(flaggerv1.MetricTemplateModel{})
require.True(t, errors.Is(err, providers.ErrNoValuesFound))
})
}
func TestSkipperObserver_GetRequestDuration(t *testing.T) {
expected := ` sum(rate(skipper_serve_route_duration_seconds_sum{route=~"kube(ew)?_skipper__skipper_ingress_canary__.*__backend_canary(_[0-9]+)?"}[1m])) / sum(rate(skipper_serve_route_duration_seconds_count{route=~"kube(ew)?_skipper__skipper_ingress_canary__.*__backend_canary(_[0-9]+)?"}[1m])) * 1000`
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
promql := r.URL.Query()["query"][0]
assert.Equal(t, expected, promql)
json := `{"status":"success","data":{"resultType":"vector","result":[{"metric":{},"value":[1,"100"]}]}}`
w.Write([]byte(json))
}))
defer ts.Close()
client, err := providers.NewPrometheusProvider(flaggerv1.MetricTemplateProvider{
Type: "prometheus",
Address: ts.URL,
SecretRef: nil,
}, nil)
require.NoError(t, err)
observer := &SkipperObserver{client: client}
val, err := observer.GetRequestDuration(flaggerv1.MetricTemplateModel{
Namespace: "skipper",
Interval: "1m",
Service: "backend",
Ingress: "skipper-ingress",
})
require.NoError(t, err)
assert.Equal(t, 100*time.Millisecond, val)
}
+5
View File
@@ -122,6 +122,11 @@ func (factory *Factory) MeshRouter(provider string, labelSelector string) Interf
kubeClient: factory.kubeClient,
annotationsPrefix: factory.ingressAnnotationsPrefix,
}
case provider == flaggerv1.SkipperProvider:
return &SkipperRouter{
logger: factory.logger,
kubeClient: factory.kubeClient,
}
case provider == flaggerv1.KubernetesProvider:
return &NopRouter{}
default:
-3
View File
@@ -125,9 +125,6 @@ func (c *KubernetesDefaultRouter) reconcileService(canary *flaggerv1.Canary, nam
metadata.Annotations = make(map[string]string)
}
c.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
Debugw(fmt.Sprintf("Creating Service %s", name), "metadata", metadata, "service_configuration", canary.Spec.Service)
// create service if it doesn't exists
svc, err := c.kubeClient.CoreV1().Services(canary.Namespace).Get(context.TODO(), name, metav1.GetOptions{})
if errors.IsNotFound(err) {
+235
View File
@@ -0,0 +1,235 @@
package router
import (
"context"
"encoding/json"
"fmt"
"github.com/google/go-cmp/cmp"
"go.uber.org/zap"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/kubernetes"
flaggerv1 "github.com/weaveworks/flagger/pkg/apis/flagger/v1beta1"
)
/*
Skipper Principles:
* if only one backend has a weight, only one backend will get 100% traffic
* if two of three or more backends have a weight, only those two should get traffic.
* if two backends don't have any weight, it's undefined and right now they get equal amount of traffic.
* weights can be int or float, but always treated as a ratio.
Implementation:
* apex Ingress is immutable
* new canary Ingress contains two paths for primary and canary service
* canary Ingress manages weights on primary & canary service, hence no traffic to apex service
*/
const (
skipperpredicateAnnotationKey = "zalando.org/skipper-predicate"
skipperBackendWeightsAnnotationKey = "zalando.org/backend-weights"
canaryPatternf = "%s-canary"
canaryRouteWeight = "Weight(100)"
canaryRouteDisable = "False()"
)
type SkipperRouter struct {
kubeClient kubernetes.Interface
logger *zap.SugaredLogger
}
// Reconcile creates or updates the ingresses
func (skp *SkipperRouter) Reconcile(canary *flaggerv1.Canary) error {
if canary.Spec.IngressRef == nil || canary.Spec.IngressRef.Name == "" {
return fmt.Errorf("ingress selector is empty")
}
apexSvcName, primarySvcName, canarySvcName := canary.GetServiceNames()
apexIngressName, canaryIngressName := skp.getIngressNames(canary.Spec.IngressRef.Name)
// retrieving apex ingress
apexIngress, err := skp.kubeClient.NetworkingV1beta1().Ingresses(canary.Namespace).Get(
context.TODO(), apexIngressName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("apexIngress %s.%s get query error: %w", apexIngressName, canary.Namespace, err)
}
// building the canary ingress from apex
iClone := apexIngress.DeepCopy()
for x := range iClone.Spec.Rules {
rule := &iClone.Spec.Rules[x] // ref not value
for y := range rule.HTTP.Paths {
path := &rule.HTTP.Paths[y] // ref not value
if path.Backend.ServiceName == apexSvcName {
// flipping to primary service
path.Backend.ServiceName = primarySvcName
// adding second canary service
canaryBackend := path.DeepCopy()
canaryBackend.Backend.ServiceName = canarySvcName
rule.HTTP.Paths = append(rule.HTTP.Paths, *canaryBackend)
}
}
}
if apexIngress.DeepCopy() == iClone {
return fmt.Errorf("backend %s not found in ingress %s", apexSvcName, apexIngressName)
}
iClone.Annotations = skp.makeAnnotations(iClone.Annotations, map[string]int{primarySvcName: 100, canarySvcName: 0})
iClone.Name = canaryIngressName
iClone.Namespace = canary.Namespace
iClone.OwnerReferences = []metav1.OwnerReference{
*metav1.NewControllerRef(canary, schema.GroupVersionKind{
Group: flaggerv1.SchemeGroupVersion.Group,
Version: flaggerv1.SchemeGroupVersion.Version,
Kind: flaggerv1.CanaryKind,
}),
}
// search for existence
canaryIngress, err := skp.kubeClient.NetworkingV1beta1().Ingresses(canary.Namespace).Get(
context.TODO(), canaryIngressName, metav1.GetOptions{})
// new ingress
if errors.IsNotFound(err) {
// Let K8s set this. Otherwise K8s API complains with "resourceVersion should not be set on objects to be created"
iClone.ObjectMeta.ResourceVersion = ""
_, err := skp.kubeClient.NetworkingV1beta1().Ingresses(canary.Namespace).Create(context.TODO(), iClone, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("ingress %s.%s create error: %w", iClone.Name, iClone.Namespace, err)
}
skp.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
Infof("Ingress %s.%s created", iClone.GetName(), canary.Namespace)
return nil
} else if err != nil {
return fmt.Errorf("ingress %s.%s query error: %w", canaryIngressName, canary.Namespace, err)
}
// existant, updating
if cmp.Diff(iClone.Spec, canaryIngress.Spec) != "" {
ingressClone := canaryIngress.DeepCopy()
ingressClone.Spec = iClone.Spec
ingressClone.Annotations = iClone.Annotations
_, err := skp.kubeClient.NetworkingV1beta1().Ingresses(canary.Namespace).Update(context.TODO(), ingressClone, metav1.UpdateOptions{})
if err != nil {
return fmt.Errorf("ingress %s.%s update error: %w", canaryIngressName, ingressClone.Namespace, err)
}
skp.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
Infof("Ingress %s updated", canaryIngressName)
}
return nil
}
func (skp *SkipperRouter) GetRoutes(canary *flaggerv1.Canary) (primaryWeight, canaryWeight int, mirrored bool, err error) {
_, primarySvcName, canarySvcName := canary.GetServiceNames()
_, canaryIngressName := skp.getIngressNames(canary.Spec.IngressRef.Name)
canaryIngress, err := skp.kubeClient.NetworkingV1beta1().Ingresses(canary.Namespace).Get(context.TODO(), canaryIngressName, metav1.GetOptions{})
if err != nil {
err = fmt.Errorf("ingress %s.%s get query error: %w", canaryIngressName, canary.Namespace, err)
return
}
weights, err := skp.backendWeights(canaryIngress.Annotations)
if err != nil {
err = fmt.Errorf("ingress %s.%s get backendWeights error: %w", canaryIngressName, canary.Namespace, err)
return
}
var ok bool
primaryWeight, ok = weights[primarySvcName]
if !ok {
err = fmt.Errorf("ingress %s.%s could not get weights[primarySvcName]", canaryIngressName, canary.Namespace)
return
}
canaryWeight, ok = weights[canarySvcName]
if !ok {
err = fmt.Errorf("ingress %s.%s could not get weights[canarySvcName]", canaryIngressName, canary.Namespace)
return
}
mirrored = false
skp.logger.With("GetRoutes", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
Debugf("GetRoutes primaryWeight: %d, canaryWeight: %d", primaryWeight, canaryWeight)
return
}
func (skp *SkipperRouter) SetRoutes(canary *flaggerv1.Canary, primaryWeight, canaryWeight int, _ bool) (err error) {
_, primarySvcName, canarySvcName := canary.GetServiceNames()
_, canaryIngressName := skp.getIngressNames(canary.Spec.IngressRef.Name)
canaryIngress, err := skp.kubeClient.NetworkingV1beta1().Ingresses(canary.Namespace).Get(context.TODO(), canaryIngressName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("ingress %s.%s get query error: %w", canaryIngressName, canary.Namespace, err)
}
iClone := canaryIngress.DeepCopy()
// TODO: A/B testing
// Canary
iClone.Annotations = skp.makeAnnotations(iClone.Annotations, map[string]int{
primarySvcName: primaryWeight,
canarySvcName: canaryWeight,
})
// Disable the canary-ingress route after the canary process
if canaryWeight == 0 {
iClone.Annotations[skipperpredicateAnnotationKey] = canaryRouteDisable
}
_, err = skp.kubeClient.NetworkingV1beta1().Ingresses(canary.Namespace).Update(
context.TODO(), iClone, metav1.UpdateOptions{})
if err != nil {
return fmt.Errorf("ingress %s.%s update error %w", iClone.Name, iClone.Namespace, err)
}
skp.logger.With("SetRoutes", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
Debugf("primaryWeight: %d, canaryWeight: %d", primaryWeight, canaryWeight)
return err
}
func (skp *SkipperRouter) Finalize(canary *flaggerv1.Canary) error {
gracePeriodSeconds := int64(2)
_, canaryIngressName := skp.getIngressNames(canary.Spec.IngressRef.Name)
skp.logger.With("deleteCanaryIngress", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
Debugf("Deleting Canary Ingress: %s", canaryIngressName)
err := skp.kubeClient.NetworkingV1beta1().Ingresses(canary.Namespace).Delete(
context.TODO(), canaryIngressName, metav1.DeleteOptions{GracePeriodSeconds: &gracePeriodSeconds})
if err != nil {
return fmt.Errorf("ingress %s.%s unable to remove canary ingress: %w", canaryIngressName, canary.Namespace, err)
}
return nil
}
func (skp *SkipperRouter) makeAnnotations(annotations map[string]string, backendWeights map[string]int) map[string]string {
b, err := json.Marshal(backendWeights)
if err != nil {
skp.logger.Errorf("Skipper:makeAnnotations: unable to marshal backendWeights %w", err)
return annotations
}
annotations[skipperBackendWeightsAnnotationKey] = string(b)
// adding more weight to canary route solves traffic bypassing through apexIngress
annotations[skipperpredicateAnnotationKey] = canaryRouteWeight
return annotations
}
// parse backend-weights annotation if it exists
func (skp *SkipperRouter) backendWeights(annotation map[string]string) (backendWeights map[string]int, err error) {
backends, ok := annotation[skipperBackendWeightsAnnotationKey]
if ok {
err = json.Unmarshal([]byte(backends), &backendWeights)
} else {
err = errors.NewNotFound(schema.GroupResource{Group: "Skipper Canary Ingress", Resource: "Annotation"},
skipperBackendWeightsAnnotationKey)
}
return
}
// getIngressNames returns the primary and canary Kubernetes Ingress names
func (skp *SkipperRouter) getIngressNames(name string) (apexName, canaryName string) {
return name, fmt.Sprintf(canaryPatternf, name)
}
+107
View File
@@ -0,0 +1,107 @@
package router
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestSkipperRouter_Reconcile(t *testing.T) {
assert := assert.New(t)
mocks := newFixture(nil)
for _, tt := range []struct {
name string
mocks func() fixture
wantErr bool
}{
{
"creating new canary ingress w/ default settings",
func() fixture { return mocks },
false,
}, {
"updating existing canary ingress",
func() fixture {
ti := newTestIngress()
ti.Annotations["something"] = "changed"
_, err := mocks.kubeClient.NetworkingV1beta1().Ingresses("default").Update(
context.TODO(), ti, metav1.UpdateOptions{})
assert.NoError(err)
return mocks
},
false,
},
} {
tt := tt
t.Run(tt.name, func(t *testing.T) {
mocks := tt.mocks()
router := &SkipperRouter{
kubeClient: mocks.kubeClient,
logger: mocks.logger,
}
assert.NoError(router.Reconcile(mocks.ingressCanary))
canaryName := fmt.Sprintf("%s-canary", mocks.ingressCanary.Spec.IngressRef.Name)
inCanary, err := router.kubeClient.NetworkingV1beta1().Ingresses("default").Get(
context.TODO(), canaryName, metav1.GetOptions{})
assert.NoError(err)
// test initialisation
assert.JSONEq(`{ "podinfo-primary": 100, "podinfo-canary": 0 }`, inCanary.Annotations["zalando.org/backend-weights"])
assert.Equal("podinfo-primary", inCanary.Spec.Rules[0].HTTP.Paths[0].Backend.ServiceName, "backend flipped over")
assert.Equal("podinfo-canary", inCanary.Spec.Rules[0].HTTP.Paths[1].Backend.ServiceName, "backend flipped over")
assert.Len(inCanary.Spec.Rules[0].HTTP.Paths, 2)
inApex, err := router.kubeClient.NetworkingV1beta1().Ingresses("default").Get(
context.TODO(), mocks.ingressCanary.Spec.IngressRef.Name, metav1.GetOptions{})
assert.NoError(err)
assert.Equal(inCanary.Spec.Rules[0].HTTP.Paths[0].Backend.ServicePort,
inApex.Spec.Rules[0].HTTP.Paths[0].Backend.ServicePort, "canary backend not cloned")
assert.Equal(inCanary.Spec.Rules[0].HTTP.Paths[0].Backend.ServicePort,
inCanary.Spec.Rules[0].HTTP.Paths[1].Backend.ServicePort, "canary backend not cloned")
})
}
}
func TestSkipperRouter_GetSetRoutes(t *testing.T) {
assert := assert.New(t)
mocks := newFixture(nil)
router := &SkipperRouter{logger: mocks.logger, kubeClient: mocks.kubeClient}
assert.NoError(router.Reconcile(mocks.ingressCanary))
p, c, m, err := router.GetRoutes(mocks.ingressCanary)
assert.NoError(err)
assert.Equal(100, p)
assert.Equal(0, c)
assert.Equal(false, m)
tests := []struct {
name string
primary, canary int
}{
{name: "0%", primary: 100, canary: 0},
{name: "10%", primary: 90, canary: 10},
{name: "20%", primary: 80, canary: 20},
{name: "30%", primary: 70, canary: 30},
{name: "85%", primary: 15, canary: 85},
{name: "100%", primary: 0, canary: 100},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert.NoError(router.SetRoutes(mocks.ingressCanary, tt.primary, tt.canary, false))
inCanary, err := router.kubeClient.NetworkingV1beta1().Ingresses("default").Get(
context.TODO(), fmt.Sprintf("%s-canary", mocks.ingressCanary.Spec.IngressRef.Name), metav1.GetOptions{})
assert.NoError(err)
assert.JSONEq(fmt.Sprintf(`{"podinfo-primary": %d,"podinfo-canary": %d}`, tt.primary, tt.canary),
inCanary.Annotations["zalando.org/backend-weights"])
p, c, m, err = router.GetRoutes(mocks.ingressCanary)
assert.NoError(err)
assert.Equal(tt.primary, p)
assert.Equal(tt.canary, c)
assert.Equal(false, m)
})
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
package version
var VERSION = "1.0.0"
var VERSION = "1.1.0"
var REVISION = "unknown"