From b4e665bad21c961464f14b1ee810398f898ccb36 Mon Sep 17 00:00:00 2001 From: Maciej Kwiek Date: Wed, 26 Apr 2017 16:32:17 +0200 Subject: [PATCH 1/2] Status checks if resource was upgraded All resources now have a Definition to which they need to conform in order to make an actual status check --- pkg/interfaces/interfaces.go | 7 +- pkg/mocks/daemonsets.go | 13 +++- pkg/resources/common.go | 54 +++++++++++-- pkg/resources/configmap.go | 44 ++++++++--- pkg/resources/configmap_test.go | 42 ++++++++++- pkg/resources/daemonset.go | 50 +++++++++--- pkg/resources/daemonset_test.go | 46 ++++++++++- pkg/resources/deployment.go | 49 +++++++++--- pkg/resources/deployment_test.go | 53 +++++++++++-- pkg/resources/flow.go | 7 +- pkg/resources/job.go | 50 +++++++++--- pkg/resources/job_test.go | 45 +++++++++++ pkg/resources/persistentvolumeclaim.go | 59 +++++++++++---- pkg/resources/persistentvolumeclaim_test.go | 45 +++++++++++ pkg/resources/petset.go | 50 +++++++++--- pkg/resources/petset_test.go | 55 ++++++++++++++ pkg/resources/pod.go | 48 +++++++++--- pkg/resources/pod_test.go | 45 +++++++++++ pkg/resources/replicaset.go | 58 ++++++++++---- pkg/resources/replicaset_test.go | 43 ++++++++++- pkg/resources/secrets.go | 42 +++++++++-- pkg/resources/secrets_test.go | 44 ++++++++++- pkg/resources/service.go | 66 ++++++++++++---- pkg/resources/service_test.go | 84 +++++++++++++++++---- pkg/resources/serviceaccount.go | 39 ++++++++-- pkg/resources/serviceaccount_test.go | 42 ++++++++++- pkg/resources/statefulset.go | 42 +++++++++-- pkg/resources/statefulset_test.go | 48 ++++++++++-- 28 files changed, 1080 insertions(+), 190 deletions(-) create mode 100644 pkg/resources/job_test.go create mode 100644 pkg/resources/persistentvolumeclaim_test.go create mode 100644 pkg/resources/petset_test.go create mode 100644 pkg/resources/pod_test.go diff --git a/pkg/interfaces/interfaces.go b/pkg/interfaces/interfaces.go index 1340d25..5b6e224 100644 --- a/pkg/interfaces/interfaces.go +++ b/pkg/interfaces/interfaces.go @@ -23,9 +23,10 @@ type ResourceStatus string // Possible ResourceStatus values const ( - ResourceReady ResourceStatus = "ready" - ResourceNotReady ResourceStatus = "not ready" - ResourceError ResourceStatus = "error" + ResourceReady ResourceStatus = "ready" + ResourceNotReady ResourceStatus = "not ready" + ResourceError ResourceStatus = "error" + ResourceWaitingForUpgrade ResourceStatus = "waiting for upgrade" ) // DefaultFlowName is the name of default flow (main dependency graph) diff --git a/pkg/mocks/daemonsets.go b/pkg/mocks/daemonsets.go index d101e44..d0adccd 100644 --- a/pkg/mocks/daemonsets.go +++ b/pkg/mocks/daemonsets.go @@ -14,7 +14,10 @@ package mocks -import extbeta1 "k8s.io/client-go/pkg/apis/extensions/v1beta1" +import ( + extbeta1 "k8s.io/client-go/pkg/apis/extensions/v1beta1" + "k8s.io/client-go/pkg/runtime" +) type daemonSetClient struct { } @@ -33,3 +36,11 @@ func MakeDaemonSet(name string) *extbeta1.DaemonSet { return daemonSet } + +func DaemonSets(names ...string) runtime.Object { + var daemonsets []extbeta1.DaemonSet + for _, name := range names { + daemonsets = append(daemonsets, *MakeDaemonSet(name)) + } + return &extbeta1.DaemonSetList{Items: daemonsets} +} diff --git a/pkg/resources/common.go b/pkg/resources/common.go index 796d81b..a49f7a2 100644 --- a/pkg/resources/common.go +++ b/pkg/resources/common.go @@ -21,12 +21,16 @@ import ( "strconv" "strings" + "k8s.io/client-go/pkg/api/v1" + appsbeta1 "k8s.io/client-go/pkg/apis/apps/v1beta1" + batchv1 "k8s.io/client-go/pkg/apis/batch/v1" + extbeta1 "k8s.io/client-go/pkg/apis/extensions/v1beta1" + "k8s.io/client-go/pkg/labels" + "github.com/Mirantis/k8s-AppController/pkg/client" + appsalpha1 "github.com/Mirantis/k8s-AppController/pkg/client/petsets/apis/apps/v1alpha1" "github.com/Mirantis/k8s-AppController/pkg/copier" "github.com/Mirantis/k8s-AppController/pkg/interfaces" - - "k8s.io/client-go/pkg/api/v1" - "k8s.io/client-go/pkg/labels" ) func init() { @@ -52,7 +56,8 @@ func init() { // Base is a base struct that contains data common for all resources type Base struct { - meta map[string]interface{} + Definition client.ResourceDefinition + meta map[string]interface{} } // Meta returns metadata parameter with given name, or empty string, @@ -71,6 +76,11 @@ func (b Base) Meta(paramName string) interface{} { return val } +// equalsToDefinition returns false since there is no definition to compare Base resource against. +func (b Base) equalsToDefinition(_ interface{}) bool { + return false +} + // KindToResourceTemplate is a map mapping kind strings to empty structs representing proper resources // structs implement interfaces.ResourceTemplate var KindToResourceTemplate = map[string]interfaces.ResourceTemplate{} @@ -154,7 +164,9 @@ func podsStateFromLabels(apiClient client.Interface, objLabels map[string]string resources := make([]interfaces.BaseResource, 0, len(pods.Items)) for _, pod := range pods.Items { p := pod - resources = append(resources, createNewPod(&p, apiClient.Pods(), nil)) + //resources = append(resources, createNewPod(&p, apiClient.Pods(), nil)) + def := MakeDefinition(&p) + resources = append(resources, createNewPod(def, apiClient.Pods())) } status, err := resourceListStatus(resources) @@ -215,3 +227,35 @@ func parametrizeResource(resource interface{}, context interfaces.GraphContext, return value }, append(replaceIn, "ObjectMeta")...) } + +func MakeDefinition(object interface{}) client.ResourceDefinition { + result := client.ResourceDefinition{} + + switch object.(type) { + case *v1.ConfigMap: + result.ConfigMap = object.(*v1.ConfigMap) + case *extbeta1.DaemonSet: + result.DaemonSet = object.(*extbeta1.DaemonSet) + case *extbeta1.Deployment: + result.Deployment = object.(*extbeta1.Deployment) + case *batchv1.Job: + result.Job = object.(*batchv1.Job) + case *v1.PersistentVolumeClaim: + result.PersistentVolumeClaim = object.(*v1.PersistentVolumeClaim) + case *appsalpha1.PetSet: + result.PetSet = object.(*appsalpha1.PetSet) + case *v1.Pod: + result.Pod = object.(*v1.Pod) + case *extbeta1.ReplicaSet: + result.ReplicaSet = object.(*extbeta1.ReplicaSet) + case *v1.Secret: + result.Secret = object.(*v1.Secret) + case *v1.Service: + result.Service = object.(*v1.Service) + case *v1.ServiceAccount: + result.ServiceAccount = object.(*v1.ServiceAccount) + case *appsbeta1.StatefulSet: + result.StatefulSet = object.(*appsbeta1.StatefulSet) + } + return result +} diff --git a/pkg/resources/configmap.go b/pkg/resources/configmap.go index da55c1e..49854dc 100644 --- a/pkg/resources/configmap.go +++ b/pkg/resources/configmap.go @@ -16,6 +16,7 @@ package resources import ( "log" + "reflect" "github.com/Mirantis/k8s-AppController/pkg/client" "github.com/Mirantis/k8s-AppController/pkg/interfaces" @@ -58,8 +59,21 @@ func (configMapTemplateFactory) Kind() string { // New returns configMap controller for new resource based on resource definition func (configMapTemplateFactory) New(def client.ResourceDefinition, c client.Interface, gc interfaces.GraphContext) interfaces.Resource { - cm := parametrizeResource(def.ConfigMap, gc, configMapParamFields).(*v1.ConfigMap) - return report.SimpleReporter{BaseResource: newConfigMap{Base: Base{def.Meta}, ConfigMap: cm, Client: c.ConfigMaps()}} + def.ConfigMap = parametrizeResource(def.ConfigMap, gc, configMapParamFields).(*v1.ConfigMap) + return createNewConfigMap(def, c) +} + +func createNewConfigMap(def client.ResourceDefinition, c client.Interface) interfaces.Resource { + return report.SimpleReporter{ + BaseResource: newConfigMap{ + Base: Base{ + Definition: def, + meta: def.Meta, + }, + ConfigMap: def.ConfigMap, + Client: c.ConfigMaps(), + }, + } } // NewExisting returns configMap controller for existing resource by its name @@ -76,18 +90,25 @@ func (c newConfigMap) Key() string { return configMapKey(c.ConfigMap.Name) } -func configMapStatus(c corev1.ConfigMapInterface, name string) (interfaces.ResourceStatus, error) { - _, err := c.Get(name) +// Status returns ConfigMap status. interfaces.ResourceReady means that its dependencies can be created +func (c newConfigMap) Status(meta map[string]string) (interfaces.ResourceStatus, error) { + cm, err := c.Client.Get(c.ConfigMap.Name) if err != nil { return interfaces.ResourceError, err } + if !c.equalsToDefinition(cm) { + return interfaces.ResourceWaitingForUpgrade, nil + } + return interfaces.ResourceReady, nil } -// Status returns configMap status. interfaces.ResourceReady means that its dependencies can be created -func (c newConfigMap) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return configMapStatus(c.Client, c.ConfigMap.Name) +// equalsToDefinition checks if definition in object is compatible with provided object +func (c newConfigMap) equalsToDefinition(configmap interface{}) bool { + cm := configmap.(*v1.ConfigMap) + + return reflect.DeepEqual(cm.Data, c.Definition.ConfigMap.Data) && reflect.DeepEqual(cm.ObjectMeta, c.Definition.ConfigMap.ObjectMeta) } // Create looks for DaemonSet in k8s and creates it if not present @@ -110,9 +131,14 @@ func (c existingConfigMap) Key() string { return configMapKey(c.Name) } -// Status returns configMap status. interfaces.ResourceReady means that its dependencies can be created +// Status returns ConfigMap status. interfaces.ResourceReady means that its dependencies can be created func (c existingConfigMap) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return configMapStatus(c.Client, c.Name) + _, err := c.Client.Get(c.Name) + if err != nil { + return interfaces.ResourceError, err + } + + return interfaces.ResourceReady, nil } // Create looks for existing configMap and returns an error if there is no such configMap in a cluster diff --git a/pkg/resources/configmap_test.go b/pkg/resources/configmap_test.go index 8bc91d5..8961d59 100644 --- a/pkg/resources/configmap_test.go +++ b/pkg/resources/configmap_test.go @@ -23,8 +23,13 @@ import ( // TestConfigMapSuccessCheck checks status of ready ConfigMap func TestConfigMapSuccessCheck(t *testing.T) { - c := mocks.NewClient(mocks.ConfigMaps("notfail")) - status, err := configMapStatus(c.ConfigMaps(), "notfail") + name := "notfail" + client := mocks.NewClient(mocks.ConfigMaps(name)) + + def := MakeDefinition(mocks.MakeConfigMap(name)) + tested := createNewConfigMap(def, client) + + status, err := tested.Status(nil) if err != nil { t.Error(err) @@ -37,8 +42,13 @@ func TestConfigMapSuccessCheck(t *testing.T) { // TestConfigMapFailCheck checks status of not existing ConfigMap func TestConfigMapFailCheck(t *testing.T) { - c := mocks.NewClient(mocks.ConfigMaps()) - status, err := configMapStatus(c.ConfigMaps(), "fail") + name := "fail" + client := mocks.NewClient() + + def := MakeDefinition(mocks.MakeConfigMap(name)) + tested := createNewConfigMap(def, client) + + status, err := tested.Status(nil) if err == nil { t.Error("error not found, expected error") @@ -48,3 +58,27 @@ func TestConfigMapFailCheck(t *testing.T) { t.Errorf("status should be `error`, is `%s` instead.", status) } } + +// TestConfigMapUpgraded tests status behaviour with resource definition differing from object in cluster +func TestConfigMapUpgraded(t *testing.T) { + name := "notfail" + client := mocks.NewClient(mocks.ConfigMaps(name)) + + def := MakeDefinition(mocks.MakeConfigMap(name)) + + //Make definition differ from client version + def.ConfigMap.ObjectMeta.Labels = map[string]string{ + "trolo": "lolo", + } + + tested := createNewConfigMap(def, client) + + status, err := tested.Status(nil) + + if err != nil { + t.Error("Error found, expected nil") + } + if status != interfaces.ResourceWaitingForUpgrade { + t.Errorf("Status should be `waiting for upgrade`, is `%s` instead.", status) + } +} diff --git a/pkg/resources/daemonset.go b/pkg/resources/daemonset.go index 8fadfac..a8a5047 100644 --- a/pkg/resources/daemonset.go +++ b/pkg/resources/daemonset.go @@ -16,6 +16,7 @@ package resources import ( "log" + "reflect" "github.com/Mirantis/k8s-AppController/pkg/client" "github.com/Mirantis/k8s-AppController/pkg/interfaces" @@ -58,8 +59,8 @@ func (daemonSetTemplateFactory) Kind() string { // New returns DaemonSets controller for new resource based on resource definition func (d daemonSetTemplateFactory) New(def client.ResourceDefinition, c client.Interface, gc interfaces.GraphContext) interfaces.Resource { - newDaemonSet := parametrizeResource(def.DaemonSet, gc, daemonSetParamFields).(*extbeta1.DaemonSet) - return report.SimpleReporter{BaseResource: DaemonSet{Base: Base{def.Meta}, DaemonSet: newDaemonSet, Client: c.DaemonSets()}} + def.DaemonSet = parametrizeResource(def.DaemonSet, gc, daemonSetParamFields).(*extbeta1.DaemonSet) + return createNewDaemonSet(def, c.DaemonSets()) } // NewExisting returns DaemonSets controller for existing resource by its name @@ -71,12 +72,8 @@ func daemonSetKey(name string) string { return "daemonset/" + name } -func daemonSetStatus(d v1beta1.DaemonSetInterface, name string) (interfaces.ResourceStatus, error) { - daemonSet, err := d.Get(name) - if err != nil { - return interfaces.ResourceError, err - } - if daemonSet.Status.CurrentNumberScheduled == daemonSet.Status.DesiredNumberScheduled { +func daemonSetStatus(d *extbeta1.DaemonSet) (interfaces.ResourceStatus, error) { + if d.Status.CurrentNumberScheduled == d.Status.DesiredNumberScheduled { return interfaces.ResourceReady, nil } return interfaces.ResourceNotReady, nil @@ -89,7 +86,22 @@ func (d DaemonSet) Key() string { // Status returns DaemonSet status. interfaces.ResourceReady means that its dependencies can be created func (d DaemonSet) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return daemonSetStatus(d.Client, d.DaemonSet.Name) + ds, err := d.Client.Get(d.DaemonSet.Name) + if err != nil { + return interfaces.ResourceError, err + } + + if !d.equalsToDefinition(ds) { + return interfaces.ResourceWaitingForUpgrade, nil + } + return daemonSetStatus(ds) +} + +// equalsToDefinition returns whether the resource has the same values as provided object +func (d DaemonSet) equalsToDefinition(daemonset interface{}) bool { + ds := daemonset.(*extbeta1.DaemonSet) + + return reflect.DeepEqual(ds.ObjectMeta, d.DaemonSet.ObjectMeta) && reflect.DeepEqual(ds.Spec, d.DaemonSet.Spec) } // Create looks for DaemonSet in k8s and creates it if not present @@ -107,6 +119,19 @@ func (d DaemonSet) Delete() error { return d.Client.Delete(d.DaemonSet.Name, &v1.DeleteOptions{}) } +func createNewDaemonSet(def client.ResourceDefinition, client v1beta1.DaemonSetInterface) interfaces.Resource { + return report.SimpleReporter{ + BaseResource: DaemonSet{ + Base: Base{ + Definition: def, + meta: def.Meta, + }, + DaemonSet: def.DaemonSet, + Client: client, + }, + } +} + // ExistingDaemonSet is a wrapper for K8s DaemonSet object which is deployed on a cluster before AppController type ExistingDaemonSet struct { Base @@ -121,7 +146,12 @@ func (d ExistingDaemonSet) Key() string { // Status returns DaemonSet status. interfaces.ResourceReady means that its dependencies can be created func (d ExistingDaemonSet) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return daemonSetStatus(d.Client, d.Name) + ds, err := d.Client.Get(d.Name) + if err != nil { + return interfaces.ResourceError, err + } + + return daemonSetStatus(ds) } // Create looks for existing DaemonSet and returns error if there is no such DaemonSet diff --git a/pkg/resources/daemonset_test.go b/pkg/resources/daemonset_test.go index 91319e4..95777ba 100644 --- a/pkg/resources/daemonset_test.go +++ b/pkg/resources/daemonset_test.go @@ -23,8 +23,14 @@ import ( // TestDaemonSetSuccessCheck check status for ready DaemonSet func TestDaemonSetSuccessCheck(t *testing.T) { - c := mocks.NewClient(mocks.MakeDaemonSet("not-fail")) - status, err := daemonSetStatus(c.DaemonSets(), "not-fail") + name := "not-fail" + + client := mocks.NewClient(mocks.DaemonSets(name)).DaemonSets() + + def := MakeDefinition(mocks.MakeDaemonSet(name)) + tested := createNewDaemonSet(def, client) + + status, err := tested.Status(nil) if err != nil { t.Error(err) @@ -36,8 +42,15 @@ func TestDaemonSetSuccessCheck(t *testing.T) { // TestDaemonSetFailCheck status of not ready daemonset func TestDaemonSetFailCheck(t *testing.T) { - c := mocks.NewClient(mocks.MakeDaemonSet("fail")) - status, err := daemonSetStatus(c.DaemonSets(), "fail") + name := "fail" + + client := mocks.NewClient(mocks.DaemonSets(name)).DaemonSets() + + def := MakeDefinition(mocks.MakeDaemonSet(name)) + tested := createNewDaemonSet(def, client) + + status, err := tested.Status(nil) + if err != nil { t.Error(err) } @@ -45,3 +58,28 @@ func TestDaemonSetFailCheck(t *testing.T) { t.Errorf("status should be not ready, is %s instead.", status) } } + +// TestDaemonSetUpgraded tests status behaviour with resource definition differing from object in cluster +func TestDaemonSetUpgraded(t *testing.T) { + name := "not-fail" + + client := mocks.NewClient(mocks.DaemonSets(name)).DaemonSets() + + def := MakeDefinition(mocks.MakeDaemonSet(name)) + + //Make definition differ from client version + def.DaemonSet.ObjectMeta.Labels = map[string]string{ + "trolo": "lolo", + } + + tested := createNewDaemonSet(def, client) + + status, err := tested.Status(nil) + + if err != nil { + t.Error("Error found, expected nil") + } + if status != interfaces.ResourceWaitingForUpgrade { + t.Errorf("Status should be `waiting for upgrade`, is `%s` instead.", status) + } +} diff --git a/pkg/resources/deployment.go b/pkg/resources/deployment.go index cd2cdea..69bf1dc 100644 --- a/pkg/resources/deployment.go +++ b/pkg/resources/deployment.go @@ -17,6 +17,7 @@ package resources import ( "errors" "log" + "reflect" "github.com/Mirantis/k8s-AppController/pkg/client" "github.com/Mirantis/k8s-AppController/pkg/interfaces" @@ -58,8 +59,22 @@ func (deploymentTemplateFactory) Kind() string { // New returns Deployment controller for new resource based on resource definition func (deploymentTemplateFactory) New(def client.ResourceDefinition, c client.Interface, gc interfaces.GraphContext) interfaces.Resource { - newDeployment := parametrizeResource(def.Deployment, gc, deploymentParamFields).(*extbeta1.Deployment) - return report.SimpleReporter{BaseResource: Deployment{Base: Base{def.Meta}, Deployment: newDeployment, Client: c.Deployments()}} + def.Deployment = parametrizeResource(def.Deployment, gc, deploymentParamFields).(*extbeta1.Deployment) + return createNewDeployment(def, c.Deployments()) +} + +func createNewDeployment(def client.ResourceDefinition, c v1beta1.DeploymentInterface) interfaces.Resource { + return report.SimpleReporter{ + BaseResource: Deployment{ + Base: Base{ + Definition: def, + meta: def.Meta, + }, + Deployment: def.Deployment, + Client: c, + }, + } + } // NewExisting returns Deployment controller for existing resource by its name @@ -71,12 +86,7 @@ func deploymentKey(name string) string { return "deployment/" + name } -func deploymentStatus(d v1beta1.DeploymentInterface, name string) (interfaces.ResourceStatus, error) { - deployment, err := d.Get(name) - if err != nil { - return interfaces.ResourceError, err - } - +func deploymentStatus(deployment *extbeta1.Deployment) (interfaces.ResourceStatus, error) { if deployment.Status.UpdatedReplicas >= *deployment.Spec.Replicas && deployment.Status.AvailableReplicas >= *deployment.Spec.Replicas { return interfaces.ResourceReady, nil } @@ -90,7 +100,22 @@ func (d Deployment) Key() string { // Status returns Deployment status. interfaces.ResourceReady means that its dependencies can be created func (d Deployment) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return deploymentStatus(d.Client, d.Deployment.Name) + deployment, err := d.Client.Get(d.Deployment.Name) + if err != nil { + return interfaces.ResourceError, err + } + + if !d.equalsToDefinition(deployment) { + return interfaces.ResourceWaitingForUpgrade, nil + } + + return deploymentStatus(deployment) +} + +func (d Deployment) equalsToDefinition(deployment interface{}) bool { + dep := deployment.(*extbeta1.Deployment) + + return reflect.DeepEqual(dep.ObjectMeta, d.Deployment.ObjectMeta) && reflect.DeepEqual(dep.Spec, d.Deployment.Spec) } // Create looks for the Deployment in K8s and creates it if not present @@ -131,7 +156,11 @@ func (d ExistingDeployment) Key() string { // Status returns Deployment status. interfaces.ResourceReady means that its dependencies can be created func (d ExistingDeployment) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return deploymentStatus(d.Client, d.Name) + deployment, err := d.Client.Get(d.Name) + if err != nil { + return interfaces.ResourceError, err + } + return deploymentStatus(deployment) } // Create looks for existing Deployment and returns error if there is no such Deployment diff --git a/pkg/resources/deployment_test.go b/pkg/resources/deployment_test.go index 66b8c67..5f71265 100644 --- a/pkg/resources/deployment_test.go +++ b/pkg/resources/deployment_test.go @@ -23,8 +23,14 @@ import ( // TestDeploymentSuccessCheck checks status of ready Deployment func TestDeploymentSuccessCheck(t *testing.T) { - c := mocks.NewClient(mocks.MakeDeployment("notfail")) - status, err := deploymentStatus(c.Deployments(), "notfail") + name := "notfail" + + client := mocks.NewClient(mocks.MakeDeployment(name)).Deployments() + + def := MakeDefinition(mocks.MakeDeployment(name)) + tested := createNewDeployment(def, client) + + status, err := tested.Status(nil) if err != nil { t.Error(err) @@ -37,8 +43,14 @@ func TestDeploymentSuccessCheck(t *testing.T) { // TestDeploymentFailUpdatedCheck checks status of not ready deployment func TestDeploymentFailUpdatedCheck(t *testing.T) { - c := mocks.NewClient(mocks.MakeDeployment("fail")) - status, err := deploymentStatus(c.Deployments(), "fail") + name := "fail" + + client := mocks.NewClient(mocks.MakeDeployment(name)).Deployments() + + def := MakeDefinition(mocks.MakeDeployment(name)) + tested := createNewDeployment(def, client) + + status, err := tested.Status(nil) if err != nil { t.Error(err) @@ -51,8 +63,14 @@ func TestDeploymentFailUpdatedCheck(t *testing.T) { // TestDeploymentFailAvailableCheck checks status of not ready deployment func TestDeploymentFailAvailableCheck(t *testing.T) { - c := mocks.NewClient(mocks.MakeDeployment("failav")) - status, err := deploymentStatus(c.Deployments(), "failav") + name := "failav" + + client := mocks.NewClient(mocks.MakeDeployment(name)).Deployments() + + def := MakeDefinition(mocks.MakeDeployment(name)) + tested := createNewDeployment(def, client) + + status, err := tested.Status(nil) if err != nil { t.Error(err) @@ -62,3 +80,26 @@ func TestDeploymentFailAvailableCheck(t *testing.T) { t.Errorf("status should be `not ready`, is `%s` instead.", status) } } + +// TestDeploymentUpgraded tests status behaviour with resource definition differing from object in cluster +func TestDeploymentUpgraded(t *testing.T) { + name := "notfail" + + client := mocks.NewClient(mocks.MakeDeployment(name)).Deployments() + + def := MakeDefinition(mocks.MakeDeployment(name)) + //Make definition differ from client version + def.Deployment.ObjectMeta.Labels = map[string]string{ + "trolo": "lolo", + } + tested := createNewDeployment(def, client) + + status, err := tested.Status(nil) + + if err != nil { + t.Error("Error found, expected nil") + } + if status != interfaces.ResourceWaitingForUpgrade { + t.Errorf("Status should be `waiting for upgrade`, is `%s` instead.", status) + } +} diff --git a/pkg/resources/flow.go b/pkg/resources/flow.go index 8be0b20..90bb051 100644 --- a/pkg/resources/flow.go +++ b/pkg/resources/flow.go @@ -58,9 +58,14 @@ func (flowTemplateFactory) New(def client.ResourceDefinition, c client.Interface depName = strings.Replace(deps[0].Name, deps[0].GenerateName, "", 1) } + def.Flow = newFlow + return report.SimpleReporter{ BaseResource: &flow{ - Base: Base{def.Meta}, + Base: Base{ + Definition: def, + meta: def.Meta, + }, flow: newFlow, context: gc, generatedName: fmt.Sprintf("%s-%s%s", newFlow.Name, depName, gc.GetArg("AC_NAME")), diff --git a/pkg/resources/job.go b/pkg/resources/job.go index 3b1e331..581dbb8 100644 --- a/pkg/resources/job.go +++ b/pkg/resources/job.go @@ -16,6 +16,7 @@ package resources import ( "log" + "reflect" "github.com/Mirantis/k8s-AppController/pkg/client" "github.com/Mirantis/k8s-AppController/pkg/interfaces" @@ -60,8 +61,8 @@ func (jobTemplateFactory) Kind() string { // New returns Job controller for new resource based on resource definition func (jobTemplateFactory) New(def client.ResourceDefinition, c client.Interface, gc interfaces.GraphContext) interfaces.Resource { - job := parametrizeResource(def.Job, gc, jobParamFields).(*v1.Job) - return createNewJob(job, c.Jobs(), def.Meta) + def.Job = parametrizeResource(def.Job, gc, jobParamFields).(*v1.Job) + return createNewJob(def, c.Jobs()) } // NewExisting returns Job controller for existing resource by its name @@ -69,12 +70,7 @@ func (jobTemplateFactory) NewExisting(name string, c client.Interface, gc interf return report.SimpleReporter{BaseResource: existingJob{Name: name, Client: c.Jobs()}} } -func jobStatus(j batchv1.JobInterface, name string) (interfaces.ResourceStatus, error) { - job, err := j.Get(name) - if err != nil { - return interfaces.ResourceError, err - } - +func jobStatus(job *v1.Job) (interfaces.ResourceStatus, error) { for _, cond := range job.Status.Conditions { if cond.Type == "Complete" && cond.Status == "True" { return interfaces.ResourceReady, nil @@ -91,7 +87,23 @@ func (j newJob) Key() string { // Status returns job status func (j newJob) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return jobStatus(j.Client, j.Job.Name) + job, err := j.Client.Get(j.Job.Name) + if err != nil { + return interfaces.ResourceError, err + } + + if !j.equalsToDefinition(job) { + return interfaces.ResourceWaitingForUpgrade, nil + } + + return jobStatus(job) +} + +// equalsToDefinition checks if definition in object is compatible with provided object +func (j newJob) equalsToDefinition(jobiface interface{}) bool { + job := jobiface.(*v1.Job) + + return reflect.DeepEqual(job.ObjectMeta, j.Job.ObjectMeta) && reflect.DeepEqual(job.Spec, j.Job.Spec) } // Create looks for the Job in k8s and creates it if not present @@ -109,8 +121,17 @@ func (j newJob) Delete() error { return j.Client.Delete(j.Job.Name, nil) } -func createNewJob(job *v1.Job, client batchv1.JobInterface, meta map[string]interface{}) interfaces.Resource { - return report.SimpleReporter{BaseResource: newJob{Base: Base{meta}, Job: job, Client: client}} +func createNewJob(def client.ResourceDefinition, client batchv1.JobInterface) interfaces.Resource { + return report.SimpleReporter{ + BaseResource: newJob{ + Base: Base{ + Definition: def, + meta: def.Meta, + }, + Job: def.Job, + Client: client, + }, + } } type existingJob struct { @@ -126,7 +147,12 @@ func (j existingJob) Key() string { // Status returns job status func (j existingJob) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return jobStatus(j.Client, j.Name) + job, err := j.Client.Get(j.Name) + if err != nil { + return interfaces.ResourceError, err + } + + return jobStatus(job) } // Create looks for existing Job and returns error if there is no such Job diff --git a/pkg/resources/job_test.go b/pkg/resources/job_test.go new file mode 100644 index 0000000..766ddde --- /dev/null +++ b/pkg/resources/job_test.go @@ -0,0 +1,45 @@ +// Copyright 2017 Mirantis +// +// 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 resources + +import ( + "testing" + + "github.com/Mirantis/k8s-AppController/pkg/interfaces" + "github.com/Mirantis/k8s-AppController/pkg/mocks" +) + +// TestJobUpgraded tests status behaviour with resource definition differing from object in cluster +func TestJobUpgraded(t *testing.T) { + name := "notfail" + + client := mocks.NewClient(mocks.MakeJob(name)).Jobs() + + def := MakeDefinition(mocks.MakeJob(name)) + //Make definition differ from client version + def.Job.ObjectMeta.Labels = map[string]string{ + "trolo": "lolo", + } + tested := createNewJob(def, client) + + status, err := tested.Status(nil) + + if err != nil { + t.Error("Error found, expected nil") + } + if status != interfaces.ResourceWaitingForUpgrade { + t.Errorf("Status should be `waiting for upgrade`, is `%s` instead.", status) + } +} diff --git a/pkg/resources/persistentvolumeclaim.go b/pkg/resources/persistentvolumeclaim.go index af69a17..defea7b 100644 --- a/pkg/resources/persistentvolumeclaim.go +++ b/pkg/resources/persistentvolumeclaim.go @@ -16,6 +16,7 @@ package resources import ( "log" + "reflect" "github.com/Mirantis/k8s-AppController/pkg/client" "github.com/Mirantis/k8s-AppController/pkg/interfaces" @@ -52,12 +53,22 @@ func (persistentVolumeClaimTemplateFactory) Kind() string { // New returns PVC controller for new resource based on resource definition func (persistentVolumeClaimTemplateFactory) New(def client.ResourceDefinition, c client.Interface, gc interfaces.GraphContext) interfaces.Resource { + def.PersistentVolumeClaim = parametrizeResource(def.PersistentVolumeClaim, gc, persistentVolumeClaimParamFields).(*v1.PersistentVolumeClaim) + return createNewPersistentVolumeClaim(def, c.PersistentVolumeClaims()) +} + +func createNewPersistentVolumeClaim(def client.ResourceDefinition, c corev1.PersistentVolumeClaimInterface) interfaces.Resource { return report.SimpleReporter{ BaseResource: newPersistentVolumeClaim{ - Base: Base{def.Meta}, - PersistentVolumeClaim: parametrizeResource(def.PersistentVolumeClaim, gc, persistentVolumeClaimParamFields).(*v1.PersistentVolumeClaim), - Client: c.PersistentVolumeClaims(), - }} + Base: Base{ + Definition: def, + meta: def.Meta, + }, + PersistentVolumeClaim: def.PersistentVolumeClaim, + Client: c, + }, + } + } // NewExisting returns PVC controller for existing resource by its name @@ -74,12 +85,7 @@ func (p newPersistentVolumeClaim) Key() string { return persistentVolumeClaimKey(p.PersistentVolumeClaim.Name) } -func persistentVolumeClaimStatus(p corev1.PersistentVolumeClaimInterface, name string) (interfaces.ResourceStatus, error) { - persistentVolumeClaim, err := p.Get(name) - if err != nil { - return interfaces.ResourceError, err - } - +func persistentVolumeClaimStatus(persistentVolumeClaim *v1.PersistentVolumeClaim) (interfaces.ResourceStatus, error) { if persistentVolumeClaim.Status.Phase == v1.ClaimBound { return interfaces.ResourceReady, nil } @@ -102,17 +108,33 @@ func (p newPersistentVolumeClaim) Delete() error { return p.Client.Delete(p.PersistentVolumeClaim.Name, &v1.DeleteOptions{}) } -// Status returns PVC status. -func (p newPersistentVolumeClaim) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return persistentVolumeClaimStatus(p.Client, p.PersistentVolumeClaim.Name) -} - type existingPersistentVolumeClaim struct { Base Name string Client corev1.PersistentVolumeClaimInterface } +// Status returns PVC status. +func (p newPersistentVolumeClaim) Status(meta map[string]string) (interfaces.ResourceStatus, error) { + pvc, err := p.Client.Get(p.PersistentVolumeClaim.Name) + if err != nil { + return interfaces.ResourceError, err + } + + if !p.equalsToDefinition(pvc) { + return interfaces.ResourceWaitingForUpgrade, nil + } + + return persistentVolumeClaimStatus(pvc) +} + +// equalsToDefinition returns whether the resource has the same values as provided object +func (p newPersistentVolumeClaim) equalsToDefinition(claim interface{}) bool { + pvc := claim.(*v1.PersistentVolumeClaim) + + return reflect.DeepEqual(pvc.ObjectMeta, p.PersistentVolumeClaim.ObjectMeta) && reflect.DeepEqual(pvc.Spec, p.PersistentVolumeClaim.Spec) +} + // Key returns the PersistentVolumeClaim object identifier func (p existingPersistentVolumeClaim) Key() string { return persistentVolumeClaimKey(p.Name) @@ -125,7 +147,12 @@ func (p existingPersistentVolumeClaim) Create() error { // Status returns PVC status. func (p existingPersistentVolumeClaim) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return persistentVolumeClaimStatus(p.Client, p.Name) + pvc, err := p.Client.Get(p.Name) + if err != nil { + return interfaces.ResourceError, err + } + + return persistentVolumeClaimStatus(pvc) } // Delete deletes persistentVolumeClaim from the cluster diff --git a/pkg/resources/persistentvolumeclaim_test.go b/pkg/resources/persistentvolumeclaim_test.go new file mode 100644 index 0000000..05fdd3b --- /dev/null +++ b/pkg/resources/persistentvolumeclaim_test.go @@ -0,0 +1,45 @@ +// Copyright 2017 Mirantis +// +// 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 resources + +import ( + "testing" + + "github.com/Mirantis/k8s-AppController/pkg/interfaces" + "github.com/Mirantis/k8s-AppController/pkg/mocks" +) + +// TestPVCUpgraded tests status behaviour with resource definition differing from object in cluster +func TestPVCUpgraded(t *testing.T) { + name := "notfail" + + client := mocks.NewClient(mocks.MakePersistentVolumeClaim(name)).PersistentVolumeClaims() + + def := MakeDefinition(mocks.MakePersistentVolumeClaim(name)) + //Make definition differ from client version + def.PersistentVolumeClaim.ObjectMeta.Labels = map[string]string{ + "trolo": "lolo", + } + tested := createNewPersistentVolumeClaim(def, client) + + status, err := tested.Status(nil) + + if err != nil { + t.Error("Error found, expected nil") + } + if status != interfaces.ResourceWaitingForUpgrade { + t.Errorf("Status should be `waiting for upgrade`, is `%s` instead.", status) + } +} diff --git a/pkg/resources/petset.go b/pkg/resources/petset.go index 9004a88..8c06549 100644 --- a/pkg/resources/petset.go +++ b/pkg/resources/petset.go @@ -16,6 +16,7 @@ package resources import ( "log" + "reflect" "github.com/Mirantis/k8s-AppController/pkg/client" appsalpha1 "github.com/Mirantis/k8s-AppController/pkg/client/petsets/apis/apps/v1alpha1" @@ -57,8 +58,8 @@ func (petSetTemplateFactory) Kind() string { // New returns PetSet controller for new resource based on resource definition func (petSetTemplateFactory) New(def client.ResourceDefinition, c client.Interface, gc interfaces.GraphContext) interfaces.Resource { - petSet := parametrizeResource(def.PetSet, gc, petSetParamFields).(*appsalpha1.PetSet) - return newPetSet(petSet, c.PetSets(), c, def.Meta) + def.PetSet = parametrizeResource(def.PetSet, gc, petSetParamFields).(*appsalpha1.PetSet) + return newPetSet(def, c) } // NewExisting returns PetSet controller for existing resource by its name @@ -66,12 +67,8 @@ func (petSetTemplateFactory) NewExisting(name string, c client.Interface, gc int return report.SimpleReporter{BaseResource: ExistingPetSet{Name: name, Client: c.PetSets(), APIClient: c}} } -func petsetStatus(p v1alpha1.PetSetInterface, name string, apiClient client.Interface) (interfaces.ResourceStatus, error) { +func petsetStatus(ps *appsalpha1.PetSet, apiClient client.Interface) (interfaces.ResourceStatus, error) { // Use label from petset spec to get needed pods - ps, err := p.Get(name) - if err != nil { - return interfaces.ResourceError, err - } return podsStateFromLabels(apiClient, ps.Spec.Template.ObjectMeta.Labels) } @@ -101,12 +98,36 @@ func (p PetSet) Delete() error { // Status returns PetSet status. interfaces.ResourceReady is regarded as sufficient for it's dependencies to be created. func (p PetSet) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return petsetStatus(p.Client, p.PetSet.Name, p.APIClient) + petset, err := p.Client.Get(p.PetSet.Name) + if err != nil { + return interfaces.ResourceError, err + } + + if !p.equalsToDefinition(petset) { + return interfaces.ResourceWaitingForUpgrade, nil + } + + return petsetStatus(petset, p.APIClient) } -// newPetSet is a constructor -func newPetSet(petset *appsalpha1.PetSet, client v1alpha1.PetSetInterface, apiClient client.Interface, meta map[string]interface{}) interfaces.Resource { - return report.SimpleReporter{BaseResource: PetSet{Base: Base{meta}, PetSet: petset, Client: client, APIClient: apiClient}} +func (p PetSet) equalsToDefinition(ps interface{}) bool { + petset := ps.(*appsalpha1.PetSet) + + return reflect.DeepEqual(petset.ObjectMeta, p.PetSet.ObjectMeta) && reflect.DeepEqual(petset.Spec, p.PetSet.Spec) +} + +func newPetSet(def client.ResourceDefinition, c client.Interface) interfaces.Resource { + return report.SimpleReporter{ + BaseResource: PetSet{ + Base: Base{ + Definition: def, + meta: def.Meta, + }, + PetSet: def.PetSet, + Client: c.PetSets(), + APIClient: c, + }, + } } // ExistingPetSet is a wrapper for K8s PetSet object which is meant to already be in a cluster bofer AppController execution @@ -129,7 +150,12 @@ func (p ExistingPetSet) Create() error { // Status returns PetSet status. interfaces.ResourceReady is regarded as sufficient for it's dependencies to be created. func (p ExistingPetSet) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return petsetStatus(p.Client, p.Name, p.APIClient) + petset, err := p.Client.Get(p.Name) + if err != nil { + return interfaces.ResourceError, err + } + + return petsetStatus(petset, p.APIClient) } // Delete deletes PetSet from the cluster diff --git a/pkg/resources/petset_test.go b/pkg/resources/petset_test.go new file mode 100644 index 0000000..36cbcdb --- /dev/null +++ b/pkg/resources/petset_test.go @@ -0,0 +1,55 @@ +// Copyright 2017 Mirantis +// +// 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 resources + +import ( + "testing" + + "github.com/Mirantis/k8s-AppController/pkg/interfaces" + "github.com/Mirantis/k8s-AppController/pkg/mocks" +) + +// TestPetSetUpgraded tests status behaviour with resource definition differing from object in cluster +func TestPetSetUpgraded(t *testing.T) { + t.Skip("Test skipped due to PetSets not being available in fake client anymore. Test is left for compile-time checks.") + name := "notfail" + + client := mocks.NewClient(mocks.MakePetSet(name)) + + def := MakeDefinition(mocks.MakePetSet(name)) + //Make definition differ from client version + def.PetSet.ObjectMeta.Labels = map[string]string{ + "trolo": "lolo", + } + tested := newPetSet(def, client) + + status, err := tested.Status(nil) + + if err != nil { + t.Error("Error found, expected nil") + } + if status != interfaces.ResourceWaitingForUpgrade { + t.Errorf("Status should be `waiting for upgrade`, is `%s` instead.", status) + } +} + +// TestPetSetStatus ensures PetSets have implemented comparison function +func TestPetSetStatus(t *testing.T) { + tested := PetSet{PetSet: mocks.MakePetSet("")} + + if !tested.equalsToDefinition(tested.PetSet) { + t.Errorf("Definition not equal to itself") + } +} diff --git a/pkg/resources/pod.go b/pkg/resources/pod.go index 915eb82..f4e8873 100644 --- a/pkg/resources/pod.go +++ b/pkg/resources/pod.go @@ -16,6 +16,7 @@ package resources import ( "log" + "reflect" "github.com/Mirantis/k8s-AppController/pkg/client" "github.com/Mirantis/k8s-AppController/pkg/interfaces" @@ -55,8 +56,8 @@ func (podTemplateFactory) Kind() string { // New returns Pod controller for new resource based on resource definition func (podTemplateFactory) New(def client.ResourceDefinition, c client.Interface, gc interfaces.GraphContext) interfaces.Resource { - pod := parametrizeResource(def.Pod, gc, podParamFields).(*v1.Pod) - return createNewPod(pod, c.Pods(), def.Meta) + def.Pod = parametrizeResource(def.Pod, gc, podParamFields).(*v1.Pod) + return createNewPod(def, c.Pods()) } // NewExisting returns Pod controller for existing resource by its name @@ -73,12 +74,7 @@ func (p newPod) Key() string { return podKey(p.Pod.Name) } -func podStatus(p corev1.PodInterface, name string) (interfaces.ResourceStatus, error) { - pod, err := p.Get(name) - if err != nil { - return interfaces.ResourceError, err - } - +func podStatus(pod *v1.Pod) (interfaces.ResourceStatus, error) { if pod.Status.Phase == "Succeeded" { return interfaces.ResourceReady, nil } @@ -117,11 +113,35 @@ func (p newPod) Delete() error { // Status returns pod status. It returns interfaces.ResourceReady if the pod is succeeded or running with succeeding readiness probe. func (p newPod) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return podStatus(p.Client, p.Pod.Name) + pod, err := p.Client.Get(p.Pod.Name) + if err != nil { + return interfaces.ResourceError, err + } + + if !p.equalsToDefinition(pod) { + return interfaces.ResourceWaitingForUpgrade, nil + } + return podStatus(pod) +} + +// equalsToDefinition checks if definition in object is compatible with provided object +func (p newPod) equalsToDefinition(podiface interface{}) bool { + pod := podiface.(*v1.Pod) + + return reflect.DeepEqual(pod.ObjectMeta, p.Pod.ObjectMeta) && reflect.DeepEqual(pod.Spec, p.Pod.Spec) } -func createNewPod(pod *v1.Pod, client corev1.PodInterface, meta map[string]interface{}) interfaces.Resource { - return report.SimpleReporter{BaseResource: newPod{Base: Base{meta}, Pod: pod, Client: client}} +func createNewPod(def client.ResourceDefinition, c corev1.PodInterface) interfaces.Resource { + return report.SimpleReporter{ + BaseResource: newPod{ + Base: Base{ + Definition: def, + meta: def.Meta, + }, + Pod: def.Pod, + Client: c, + }, + } } type existingPod struct { @@ -142,7 +162,11 @@ func (p existingPod) Create() error { // Status returns pod status. It returns interfaces.ResourceReady if the pod is succeeded or running with succeeding readiness probe. func (p existingPod) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return podStatus(p.Client, p.Name) + pod, err := p.Client.Get(p.Name) + if err != nil { + return interfaces.ResourceError, err + } + return podStatus(pod) } // Delete deletes pod from the cluster diff --git a/pkg/resources/pod_test.go b/pkg/resources/pod_test.go new file mode 100644 index 0000000..baf710b --- /dev/null +++ b/pkg/resources/pod_test.go @@ -0,0 +1,45 @@ +// Copyright 2017 Mirantis +// +// 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 resources + +import ( + "testing" + + "github.com/Mirantis/k8s-AppController/pkg/interfaces" + "github.com/Mirantis/k8s-AppController/pkg/mocks" +) + +// TestPodUpgraded tests status behaviour with resource definition differing from object in cluster +func TestPodUpgraded(t *testing.T) { + name := "notfail" + + client := mocks.NewClient(mocks.MakePod(name)).Pods() + + def := MakeDefinition(mocks.MakePod(name)) + //Make definition differ from client version + def.Pod.ObjectMeta.Labels = map[string]string{ + "trolo": "lolo", + } + tested := createNewPod(def, client) + + status, err := tested.Status(nil) + + if err != nil { + t.Error("Error found, expected nil") + } + if status != interfaces.ResourceWaitingForUpgrade { + t.Errorf("Status should be `waiting for upgrade`, is `%s` instead.", status) + } +} diff --git a/pkg/resources/replicaset.go b/pkg/resources/replicaset.go index 86643ee..efcd1d7 100644 --- a/pkg/resources/replicaset.go +++ b/pkg/resources/replicaset.go @@ -17,6 +17,7 @@ package resources import ( "fmt" "log" + "reflect" "github.com/Mirantis/k8s-AppController/pkg/client" "github.com/Mirantis/k8s-AppController/pkg/interfaces" @@ -59,8 +60,8 @@ func (replicaSetTemplateFactory) Kind() string { // New returns ReplicaSet controller for new resource based on resource definition func (replicaSetTemplateFactory) New(def client.ResourceDefinition, c client.Interface, gc interfaces.GraphContext) interfaces.Resource { - replicaSet := parametrizeResource(def.ReplicaSet, gc, replicaSetParamFields).(*extbeta1.ReplicaSet) - return createNewReplicaSet(replicaSet, c.ReplicaSets(), def.Meta) + def.ReplicaSet = parametrizeResource(def.ReplicaSet, gc, replicaSetParamFields).(*extbeta1.ReplicaSet) + return createNewReplicaSet(def, c.ReplicaSets()) } // NewExisting returns ReplicaSet controller for existing resource by its name @@ -68,12 +69,7 @@ func (replicaSetTemplateFactory) NewExisting(name string, c client.Interface, gc return existingReplicaSet{Name: name, Client: c.ReplicaSets()} } -func replicaSetStatus(r v1beta1.ReplicaSetInterface, name string, meta map[string]string) (interfaces.ResourceStatus, error) { - rs, err := r.Get(name) - if err != nil { - return interfaces.ResourceError, err - } - +func replicaSetStatus(rs *extbeta1.ReplicaSet, meta map[string]string) (interfaces.ResourceStatus, error) { successFactor, err := getPercentage(successFactorKey, meta) if err != nil { return interfaces.ResourceError, err @@ -145,18 +141,46 @@ func (r newReplicaSet) Delete() error { return r.Client.Delete(r.ReplicaSet.Name, nil) } +// GetDependencyReport returns a DependencyReport for this ReplicaSet +func (r newReplicaSet) GetDependencyReport(meta map[string]string) interfaces.DependencyReport { + return replicaSetReport(r.Client, r.ReplicaSet.Name, meta) +} + // Status returns ReplicaSet status based on provided meta. func (r newReplicaSet) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return replicaSetStatus(r.Client, r.ReplicaSet.Name, meta) + rs, err := r.Client.Get(r.ReplicaSet.Name) + if err != nil { + return interfaces.ResourceError, err + } + + if !r.equalsToDefinition(rs) { + return interfaces.ResourceWaitingForUpgrade, nil + } + return replicaSetStatus(rs, meta) } -// GetDependencyReport returns a DependencyReport for this ReplicaSet -func (r newReplicaSet) GetDependencyReport(meta map[string]string) interfaces.DependencyReport { - return replicaSetReport(r.Client, r.ReplicaSet.Name, meta) +// equalsToDefinition checks if definition in object is compatible with provided object +func (r newReplicaSet) equalsToDefinition(replicaSetiface interface{}) bool { + replicaSet := replicaSetiface.(*extbeta1.ReplicaSet) + + return reflect.DeepEqual(replicaSet.ObjectMeta, r.ReplicaSet.ObjectMeta) && reflect.DeepEqual(replicaSet.Spec, r.ReplicaSet.Spec) } -func createNewReplicaSet(replicaSet *extbeta1.ReplicaSet, client v1beta1.ReplicaSetInterface, meta map[string]interface{}) newReplicaSet { - return newReplicaSet{Base: Base{meta}, ReplicaSet: replicaSet, Client: client} +// StatusIsCacheable returns false if meta contains SuccessFactorKey +func (r newReplicaSet) StatusIsCacheable(meta map[string]string) bool { + _, ok := meta[successFactorKey] + return !ok +} + +func createNewReplicaSet(def client.ResourceDefinition, client v1beta1.ReplicaSetInterface) newReplicaSet { + return newReplicaSet{ + Base: Base{ + Definition: def, + meta: def.Meta, + }, + ReplicaSet: def.ReplicaSet, + Client: client, + } } type existingReplicaSet struct { @@ -177,7 +201,11 @@ func (r existingReplicaSet) Create() error { // Status returns ReplicaSet status based on provided meta. func (r existingReplicaSet) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return replicaSetStatus(r.Client, r.Name, meta) + rs, err := r.Client.Get(r.Name) + if err != nil { + return interfaces.ResourceError, err + } + return replicaSetStatus(rs, meta) } // Delete deletes ReplicaSet from the cluster diff --git a/pkg/resources/replicaset_test.go b/pkg/resources/replicaset_test.go index 8b17345..eab8ce7 100644 --- a/pkg/resources/replicaset_test.go +++ b/pkg/resources/replicaset_test.go @@ -22,8 +22,14 @@ import ( ) func TestSuccessCheck(t *testing.T) { - c := mocks.NewClient(mocks.MakeReplicaSet("notfail")) - status, err := replicaSetStatus(c.ReplicaSets(), "notfail", nil) + name := "notfail" + + client := mocks.NewClient(mocks.MakeReplicaSet(name)).ReplicaSets() + + def := MakeDefinition(mocks.MakeReplicaSet(name)) + tested := createNewReplicaSet(def, client) + + status, err := tested.Status(nil) if err != nil { t.Error(err) @@ -35,8 +41,14 @@ func TestSuccessCheck(t *testing.T) { } func TestFailCheck(t *testing.T) { - c := mocks.NewClient(mocks.MakeReplicaSet("fail")) - status, err := replicaSetStatus(c.ReplicaSets(), "fail", map[string]string{successFactorKey: "80"}) + name := "fail" + + client := mocks.NewClient(mocks.MakeReplicaSet(name)).ReplicaSets() + + def := MakeDefinition(mocks.MakeReplicaSet(name)) + tested := createNewReplicaSet(def, client) + + status, err := tested.Status(map[string]string{successFactorKey: "80"}) if err != nil { t.Error(err) @@ -46,3 +58,26 @@ func TestFailCheck(t *testing.T) { t.Errorf("status should be `not ready`, is `%s` instead.", status) } } + +// TestReplicaSetUpgraded tests status behaviour with resource definition differing from object in cluster +func TestReplicaSetUpgraded(t *testing.T) { + name := "notfail" + + client := mocks.NewClient(mocks.MakeReplicaSet(name)).ReplicaSets() + + def := MakeDefinition(mocks.MakeReplicaSet(name)) + //Make definition differ from client version + def.ReplicaSet.ObjectMeta.Labels = map[string]string{ + "trolo": "lolo", + } + tested := createNewReplicaSet(def, client) + + status, err := tested.Status(nil) + + if err != nil { + t.Error("Error found, expected nil") + } + if status != interfaces.ResourceWaitingForUpgrade { + t.Errorf("Status should be `waiting for upgrade`, is `%s` instead.", status) + } +} diff --git a/pkg/resources/secrets.go b/pkg/resources/secrets.go index f408c89..36a426b 100644 --- a/pkg/resources/secrets.go +++ b/pkg/resources/secrets.go @@ -16,6 +16,7 @@ package resources import ( "log" + "reflect" "github.com/Mirantis/k8s-AppController/pkg/client" "github.com/Mirantis/k8s-AppController/pkg/interfaces" @@ -59,8 +60,21 @@ func (secretTemplateFactory) Kind() string { // New returns Secret controller for new resource based on resource definition func (secretTemplateFactory) New(def client.ResourceDefinition, c client.Interface, gc interfaces.GraphContext) interfaces.Resource { - secret := parametrizeResource(def.Secret, gc, secretParamFields).(*v1.Secret) - return report.SimpleReporter{BaseResource: newSecret{Base: Base{def.Meta}, Secret: secret, Client: c.Secrets()}} + def.Secret = parametrizeResource(def.Secret, gc, secretParamFields).(*v1.Secret) + return createNewSecret(def, c.Secrets()) +} + +func createNewSecret(def client.ResourceDefinition, c corev1.SecretInterface) interfaces.Resource { + return report.SimpleReporter{ + BaseResource: newSecret{ + Base: Base{ + Definition: def, + meta: def.Meta, + }, + Secret: def.Secret, + Client: c, + }, + } } // NewExisting returns Secret controller for existing resource by its name @@ -82,18 +96,25 @@ func (s existingSecret) Key() string { return secretKey(s.Name) } -func secretStatus(s corev1.SecretInterface, name string) (interfaces.ResourceStatus, error) { - _, err := s.Get(name) +// Status returns interfaces.ResourceReady if the secret is available in cluster +func (s newSecret) Status(_ map[string]string) (interfaces.ResourceStatus, error) { + secret, err := s.Client.Get(s.Secret.Name) if err != nil { return interfaces.ResourceError, err } + if !s.equalsToDefinition(secret) { + return interfaces.ResourceWaitingForUpgrade, nil + } + return interfaces.ResourceReady, nil } -// Status returns interfaces.ResourceReady if the secret is available in cluster -func (s newSecret) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return secretStatus(s.Client, s.Secret.Name) +// equalsToDefinition checks if definition in object is compatible with provided object +func (s newSecret) equalsToDefinition(secretiface interface{}) bool { + secret := secretiface.(*v1.Secret) + + return reflect.DeepEqual(secret.ObjectMeta, s.Secret.ObjectMeta) && reflect.DeepEqual(secret.Data, s.Secret.Data) && reflect.DeepEqual(secret.StringData, s.Secret.StringData) && secret.Type == s.Secret.Type } // Create looks for the Secret in k8s and creates it if not present @@ -113,7 +134,12 @@ func (s newSecret) Delete() error { // Status returns interfaces.ResourceReady if the secret is available in cluster func (s existingSecret) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return secretStatus(s.Client, s.Name) + _, err := s.Client.Get(s.Name) + if err != nil { + return interfaces.ResourceError, err + } + + return interfaces.ResourceReady, nil } // Create looks for existing Secret and returns error if there is no such Secret diff --git a/pkg/resources/secrets_test.go b/pkg/resources/secrets_test.go index 82e29a0..c3b7e59 100644 --- a/pkg/resources/secrets_test.go +++ b/pkg/resources/secrets_test.go @@ -23,8 +23,14 @@ import ( // TestSecretSuccessCheck checks status of ready Secret func TestSecretSuccessCheck(t *testing.T) { - c := mocks.NewClient(mocks.MakeSecret("notfail")) - status, err := secretStatus(c.Secrets(), "notfail") + name := "notfail" + + client := mocks.NewClient(mocks.MakeSecret(name)).Secrets() + + def := MakeDefinition(mocks.MakeSecret(name)) + tested := createNewSecret(def, client) + + status, err := tested.Status(nil) if err != nil { t.Error(err) @@ -37,8 +43,15 @@ func TestSecretSuccessCheck(t *testing.T) { // TestSecretFailCheck checks status of not existing Secret func TestSecretFailCheck(t *testing.T) { - c := mocks.NewClient() - status, err := secretStatus(c.Secrets(), "fail") + name := "fail" + + client := mocks.NewClient().Secrets() + + secret := mocks.MakeSecret(name) + def := MakeDefinition(secret) + tested := createNewSecret(def, client) + + status, err := tested.Status(nil) if err == nil { t.Error("error not found, expected error") @@ -48,3 +61,26 @@ func TestSecretFailCheck(t *testing.T) { t.Errorf("status should be `error`, is `%s` instead.", status) } } + +// TestSecretUpgraded tests status behaviour with resource definition differing from object in cluster +func TestSecretUpgraded(t *testing.T) { + name := "notfail" + + client := mocks.NewClient(mocks.MakeSecret(name)).Secrets() + + def := MakeDefinition(mocks.MakeSecret(name)) + //Make definition differ from client version + def.Secret.ObjectMeta.Labels = map[string]string{ + "trolo": "lolo", + } + tested := createNewSecret(def, client) + + status, err := tested.Status(nil) + + if err != nil { + t.Error("Error found, expected nil") + } + if status != interfaces.ResourceWaitingForUpgrade { + t.Errorf("Status should be `waiting for upgrade`, is `%s` instead.", status) + } +} diff --git a/pkg/resources/service.go b/pkg/resources/service.go index 5c3fcb2..a00770f 100644 --- a/pkg/resources/service.go +++ b/pkg/resources/service.go @@ -16,6 +16,7 @@ package resources import ( "log" + "reflect" "github.com/Mirantis/k8s-AppController/pkg/client" "github.com/Mirantis/k8s-AppController/pkg/interfaces" @@ -56,8 +57,22 @@ func (serviceTemplateFactory) Kind() string { // New returns Service controller for new resource based on resource definition func (serviceTemplateFactory) New(def client.ResourceDefinition, c client.Interface, gc interfaces.GraphContext) interfaces.Resource { - service := parametrizeResource(def.Service, gc, serviceParamFields).(*v1.Service) - return report.SimpleReporter{BaseResource: newService{Base: Base{def.Meta}, Service: service, Client: c.Services(), APIClient: c}} + def.Service = parametrizeResource(def.Service, gc, serviceParamFields).(*v1.Service) + return createNewService(def, c) +} + +func createNewService(def client.ResourceDefinition, c client.Interface) interfaces.Resource { + return report.SimpleReporter{ + BaseResource: newService{ + Base: Base{ + Definition: def, + meta: def.Meta, + }, + Service: def.Service, + Client: c.Services(), + APIClient: c, + }, + } } // NewExisting returns Service controller for existing resource by its name @@ -65,13 +80,7 @@ func (serviceTemplateFactory) NewExisting(name string, c client.Interface, gc in return report.SimpleReporter{BaseResource: existingService{Name: name, Client: c.Services()}} } -func serviceStatus(s corev1.ServiceInterface, name string, apiClient client.Interface) (interfaces.ResourceStatus, error) { - service, err := s.Get(name) - - if err != nil { - return interfaces.ResourceError, err - } - +func serviceStatus(service *v1.Service, apiClient client.Interface) (interfaces.ResourceStatus, error) { log.Printf("Checking service status for selector %v", service.Spec.Selector) selector := labels.SelectorFromSet(service.Spec.Selector) @@ -91,13 +100,16 @@ func serviceStatus(s corev1.ServiceInterface, name string, apiClient client.Inte } resources := make([]interfaces.BaseResource, 0, len(pods.Items)+len(jobs.Items)+len(replicasets.Items)) for _, pod := range pods.Items { - resources = append(resources, createNewPod(&pod, apiClient.Pods(), nil)) + def := MakeDefinition(&pod) + resources = append(resources, createNewPod(def, apiClient.Pods())) } for _, j := range jobs.Items { - resources = append(resources, createNewJob(&j, apiClient.Jobs(), nil)) + def := MakeDefinition(&j) + resources = append(resources, createNewJob(def, apiClient.Jobs())) } for _, r := range replicasets.Items { - resources = append(resources, createNewReplicaSet(&r, apiClient.ReplicaSets(), nil)) + def := MakeDefinition(&r) + resources = append(resources, createNewReplicaSet(def, apiClient.ReplicaSets())) } if apiClient.IsEnabled(v1beta1.SchemeGroupVersion) { statefulsets, err := apiClient.StatefulSets().List(options) @@ -105,7 +117,8 @@ func serviceStatus(s corev1.ServiceInterface, name string, apiClient client.Inte return interfaces.ResourceError, err } for _, ps := range statefulsets.Items { - resources = append(resources, newStatefulSet(&ps, apiClient.StatefulSets(), apiClient, nil)) + def := MakeDefinition(&ps) + resources = append(resources, createNewStatefulSet(def, apiClient)) } } else { petsets, err := apiClient.PetSets().List(api.ListOptions{LabelSelector: selector}) @@ -113,7 +126,8 @@ func serviceStatus(s corev1.ServiceInterface, name string, apiClient client.Inte return interfaces.ResourceError, err } for _, ps := range petsets.Items { - resources = append(resources, newPetSet(&ps, apiClient.PetSets(), apiClient, nil)) + def := MakeDefinition(&ps) + resources = append(resources, createNewStatefulSet(def, apiClient)) } } status, err := resourceListStatus(resources) @@ -150,7 +164,22 @@ func (s newService) Delete() error { // Status returns Service Status. It is based on the status of all objects which match the service selector. If all of them are ready, the Service is considered ready. func (s newService) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return serviceStatus(s.Client, s.Service.Name, s.APIClient) + service, err := s.Client.Get(s.Service.Name) + if err != nil { + return interfaces.ResourceError, err + } + + if !s.equalsToDefinition(service) { + return interfaces.ResourceWaitingForUpgrade, nil + } + return serviceStatus(service, s.APIClient) +} + +// equalsToDefinition checks if definition in object is compatible with provided object +func (s newService) equalsToDefinition(serviceiface interface{}) bool { + service := serviceiface.(*v1.Service) + + return reflect.DeepEqual(service.ObjectMeta, s.Service.ObjectMeta) && reflect.DeepEqual(service.Spec, s.Service.Spec) } type existingService struct { @@ -172,7 +201,12 @@ func (s existingService) Create() error { // Status returns Service Status. It is based on the status of all objects which match the service selector. If all of them are ready, the Service is considered ready. func (s existingService) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return serviceStatus(s.Client, s.Name, s.APIClient) + service, err := s.Client.Get(s.Name) + + if err != nil { + return interfaces.ResourceError, err + } + return serviceStatus(service, s.APIClient) } // Delete deletes Service from the cluster diff --git a/pkg/resources/service_test.go b/pkg/resources/service_test.go index fabd900..9235274 100644 --- a/pkg/resources/service_test.go +++ b/pkg/resources/service_test.go @@ -24,8 +24,13 @@ import ( // TestCheckServiceStatusReady checks if the service status check is fine for healthy service func TestCheckServiceStatusReady(t *testing.T) { - c := mocks.NewClient(mocks.MakeService("success")) - status, err := serviceStatus(c.Services(), "success", c) + svc := mocks.MakeService("success") + c := mocks.NewClient(svc) + + def := MakeDefinition(svc) + tested := createNewService(def, c) + + status, err := tested.Status(nil) if err != nil { t.Errorf("%s", err) @@ -38,11 +43,19 @@ func TestCheckServiceStatusReady(t *testing.T) { // TestCheckServiceStatusPodNotReady tests if service which selects failed pods is not ready func TestCheckServiceStatusPodNotReady(t *testing.T) { - svc := mocks.MakeService("failedpod") - pod := mocks.MakePod("error") + svcName := "failedpod" + podName := "error" + svc := mocks.MakeService(svcName) + + pod := mocks.MakePod(podName) pod.Labels = svc.Spec.Selector + c := mocks.NewClient(svc, pod) - status, err := serviceStatus(c.Services(), "failedpod", c) + + def := MakeDefinition(svc) + tested := createNewService(def, c) + + status, err := tested.Status(nil) if err == nil { t.Fatal("error should be returned, got nil") @@ -57,13 +70,21 @@ func TestCheckServiceStatusPodNotReady(t *testing.T) { } } -// TestCheckServiceStatusJobNotReady tests if service which selects failed pods is not ready +// TestCheckServiceStatusJobNotReady tests if service which selects failed jobs is not ready func TestCheckServiceStatusJobNotReady(t *testing.T) { - svc := mocks.MakeService("failedjob") - job := mocks.MakeJob("error") + svcName := "failedjob" + jobName := "error" + svc := mocks.MakeService(svcName) + + job := mocks.MakeJob(jobName) job.Labels = svc.Spec.Selector + c := mocks.NewClient(svc, job) - status, err := serviceStatus(c.Services(), "failedjob", c) + + def := MakeDefinition(svc) + tested := createNewService(def, c) + + status, err := tested.Status(nil) if err == nil { t.Error("error should be returned, got nil") @@ -81,11 +102,19 @@ func TestCheckServiceStatusJobNotReady(t *testing.T) { // TestCheckServiceStatusReplicaSetNotReady tests if service which selects failed replicasets is not ready func TestCheckServiceStatusReplicaSetNotReady(t *testing.T) { - svc := mocks.MakeService("failedrc") - rc := mocks.MakeReplicaSet("fail") + svcName := "failedrc" + rsName := "fail" + svc := mocks.MakeService(svcName) + + rc := mocks.MakeReplicaSet(rsName) rc.Labels = svc.Spec.Selector + c := mocks.NewClient(svc, rc) - status, err := serviceStatus(c.Services(), "failedrc", c) + + def := MakeDefinition(svc) + tested := createNewService(def, c) + + status, err := tested.Status(nil) if err == nil { t.Error("error should be returned, got nil") @@ -124,7 +153,11 @@ func TestCheckServiceStatusOnPartialSelector(t *testing.T) { } c := mocks.NewClient(svc, rs, pod, job) - status, err := serviceStatus(c.Services(), "svc", c) + + def := MakeDefinition(svc) + tested := createNewService(def, c) + + status, err := tested.Status(nil) if err != nil { t.Fatalf("error should be nil, got %v", err) @@ -138,7 +171,7 @@ func TestCheckServiceStatusOnPartialSelector(t *testing.T) { } c.ReplicaSets().Create(rs2) - status, err = serviceStatus(c.Services(), "svc", c) + status, err = tested.Status(nil) if err == nil { t.Fatal("error should be returned, got nil") @@ -152,3 +185,26 @@ func TestCheckServiceStatusOnPartialSelector(t *testing.T) { t.Errorf("service should be `not ready`, is `%s` instead", status) } } + +// TestServiceUpgraded tests status behaviour with resource definition differing from object in cluster +func TestServiceUpgraded(t *testing.T) { + name := "notfail" + + client := mocks.NewClient(mocks.MakeService(name)) + + def := MakeDefinition(mocks.MakeService(name)) + //Make definition differ from client version + def.Service.ObjectMeta.Labels = map[string]string{ + "trolo": "lolo", + } + tested := createNewService(def, client) + + status, err := tested.Status(nil) + + if err != nil { + t.Error("Error found, expected nil") + } + if status != interfaces.ResourceWaitingForUpgrade { + t.Errorf("Status should be `waiting for upgrade`, is `%s` instead.", status) + } +} diff --git a/pkg/resources/serviceaccount.go b/pkg/resources/serviceaccount.go index 68a9196..d6900b6 100644 --- a/pkg/resources/serviceaccount.go +++ b/pkg/resources/serviceaccount.go @@ -16,6 +16,7 @@ package resources import ( "log" + "reflect" "github.com/Mirantis/k8s-AppController/pkg/client" "github.com/Mirantis/k8s-AppController/pkg/interfaces" @@ -59,9 +60,20 @@ func (serviceAccountTemplateFactory) Kind() string { // New returns ServiceAccount controller for new resource based on resource definition func (serviceAccountTemplateFactory) New(def client.ResourceDefinition, c client.Interface, gc interfaces.GraphContext) interfaces.Resource { - serviceAccount := parametrizeResource(def.ServiceAccount, gc, serviceAccountParamFields).(*v1.ServiceAccount) + def.ServiceAccount = parametrizeResource(def.ServiceAccount, gc, serviceAccountParamFields).(*v1.ServiceAccount) + return createNewServiceAccount(def, c.ServiceAccounts()) +} + +func createNewServiceAccount(def client.ResourceDefinition, c corev1.ServiceAccountInterface) interfaces.Resource { return report.SimpleReporter{ - BaseResource: newServiceAccount{Base: Base{def.Meta}, ServiceAccount: serviceAccount, Client: c.ServiceAccounts()}, + BaseResource: newServiceAccount{ + Base: Base{ + Definition: def, + meta: def.Meta, + }, + ServiceAccount: def.ServiceAccount, + Client: c, + }, } } @@ -79,18 +91,24 @@ func (c newServiceAccount) Key() string { return serviceAccountKey(c.ServiceAccount.Name) } -func serviceAccountStatus(c corev1.ServiceAccountInterface, name string) (interfaces.ResourceStatus, error) { - _, err := c.Get(name) +func (c newServiceAccount) Status(meta map[string]string) (interfaces.ResourceStatus, error) { + sa, err := c.Client.Get(c.ServiceAccount.Name) if err != nil { return interfaces.ResourceError, err } + if !c.equalsToDefinition(sa) { + return interfaces.ResourceWaitingForUpgrade, nil + } + return interfaces.ResourceReady, nil } -// Status returns ServiceAccount status -func (c newServiceAccount) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return serviceAccountStatus(c.Client, c.ServiceAccount.Name) +// equalsToDefinition checks if definition in object is compatible with provided object +func (c newServiceAccount) equalsToDefinition(serviceAccountiface interface{}) bool { + serviceAccount := serviceAccountiface.(*v1.ServiceAccount) + + return reflect.DeepEqual(serviceAccount.ObjectMeta, c.ServiceAccount.ObjectMeta) && reflect.DeepEqual(serviceAccount.Secrets, c.ServiceAccount.Secrets) && reflect.DeepEqual(serviceAccount.ImagePullSecrets, c.ServiceAccount.ImagePullSecrets) } // Create looks for the ServiceAccount in k8s and creates it if not present @@ -115,7 +133,12 @@ func (c existingServiceAccount) Key() string { // Status returns ServiceAccount status func (c existingServiceAccount) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return serviceAccountStatus(c.Client, c.Name) + _, err := c.Client.Get(c.Name) + if err != nil { + return interfaces.ResourceError, err + } + + return interfaces.ResourceReady, nil } // Create looks for existing ServiceAccount and returns error if there is no such ServiceAccount diff --git a/pkg/resources/serviceaccount_test.go b/pkg/resources/serviceaccount_test.go index 5bbc081..430381e 100644 --- a/pkg/resources/serviceaccount_test.go +++ b/pkg/resources/serviceaccount_test.go @@ -23,8 +23,14 @@ import ( // TestServiceAccountSuccessCheck checks status of ready ServiceAccount func TestServiceAccountSuccessCheck(t *testing.T) { - c := mocks.NewClient(mocks.ServiceAccounts("notfail")) - status, err := serviceAccountStatus(c.ServiceAccounts(), "notfail") + sa := mocks.MakeServiceAccount("notfail") + c := mocks.NewClient(sa).ServiceAccounts() + + def := MakeDefinition(sa) + + tested := createNewServiceAccount(def, c) + + status, err := tested.Status(nil) if err != nil { t.Error(err) @@ -37,8 +43,13 @@ func TestServiceAccountSuccessCheck(t *testing.T) { // TestServiceAccountFailCheck checks status of not existing ServiceAccount func TestServiceAccountFailCheck(t *testing.T) { - c := mocks.NewClient(mocks.ServiceAccounts()) - status, err := serviceAccountStatus(c.ServiceAccounts(), "fail") + c := mocks.NewClient(mocks.ServiceAccounts()).ServiceAccounts() + + def := MakeDefinition(mocks.MakeServiceAccount("")) + + tested := createNewServiceAccount(def, c) + + status, err := tested.Status(nil) if err == nil { t.Error("error not found, expected error") @@ -48,3 +59,26 @@ func TestServiceAccountFailCheck(t *testing.T) { t.Errorf("status should be `error`, is `%s` instead.", status) } } + +// TestServiceAccountUpgraded tests status behaviour with resource definition differing from object in cluster +func TestServiceAccountUpgraded(t *testing.T) { + name := "notfail" + + client := mocks.NewClient(mocks.MakeServiceAccount(name)).ServiceAccounts() + + def := MakeDefinition(mocks.MakeServiceAccount(name)) + //Make definition differ from client version + def.ServiceAccount.ObjectMeta.Labels = map[string]string{ + "trolo": "lolo", + } + tested := createNewServiceAccount(def, client) + + status, err := tested.Status(nil) + + if err != nil { + t.Error("Error found, expected nil") + } + if status != interfaces.ResourceWaitingForUpgrade { + t.Errorf("Status should be `waiting for upgrade`, is `%s` instead.", status) + } +} diff --git a/pkg/resources/statefulset.go b/pkg/resources/statefulset.go index e6f4c74..8a9e7ed 100644 --- a/pkg/resources/statefulset.go +++ b/pkg/resources/statefulset.go @@ -16,6 +16,7 @@ package resources import ( "log" + "reflect" "github.com/Mirantis/k8s-AppController/pkg/client" "github.com/Mirantis/k8s-AppController/pkg/interfaces" @@ -58,8 +59,8 @@ func (statefulSetTemplateFactory) Kind() string { // New returns StatefulSet controller for new resource based on resource definition func (statefulSetTemplateFactory) New(def client.ResourceDefinition, c client.Interface, gc interfaces.GraphContext) interfaces.Resource { - statefulSet := parametrizeResource(def.StatefulSet, gc, statefulSetParamFields).(*appsbeta1.StatefulSet) - return newStatefulSet(statefulSet, c.StatefulSets(), c, def.Meta) + def.StatefulSet = parametrizeResource(def.StatefulSet, gc, statefulSetParamFields).(*appsbeta1.StatefulSet) + return createNewStatefulSet(def, c) } // NewExisting returns StatefulSet controller for existing resource by its name @@ -102,11 +103,36 @@ func (p StatefulSet) Delete() error { // Status returns StatefulSet status. interfaces.ResourceReady is regarded as sufficient for it's dependencies to be created. func (p StatefulSet) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return statefulsetStatus(p.Client, p.StatefulSet.Name, p.APIClient) + ps, err := p.Client.Get(p.StatefulSet.Name) + if err != nil { + return interfaces.ResourceError, err + } + + if !p.equalsToDefinition(ps) { + return interfaces.ResourceWaitingForUpgrade, nil + } + return podsStateFromLabels(p.APIClient, ps.Spec.Template.ObjectMeta.Labels) +} + +// equalsToDefinition checks if definition in object is compatible with provided object +func (p StatefulSet) equalsToDefinition(statefulSetiface interface{}) bool { + statefulSet := statefulSetiface.(*appsbeta1.StatefulSet) + + return reflect.DeepEqual(statefulSet.ObjectMeta, p.StatefulSet.ObjectMeta) && reflect.DeepEqual(statefulSet.Spec, p.StatefulSet.Spec) } -func newStatefulSet(statefulset *appsbeta1.StatefulSet, client v1beta1.StatefulSetInterface, apiClient client.Interface, meta map[string]interface{}) interfaces.Resource { - return report.SimpleReporter{BaseResource: StatefulSet{Base: Base{meta}, StatefulSet: statefulset, Client: client, APIClient: apiClient}} +func createNewStatefulSet(def client.ResourceDefinition, apiClient client.Interface) interfaces.Resource { + return report.SimpleReporter{ + BaseResource: StatefulSet{ + Base: Base{ + Definition: def, + meta: def.Meta, + }, + StatefulSet: def.StatefulSet, + Client: apiClient.StatefulSets(), + APIClient: apiClient, + }, + } } // ExistingStatefulSet is a wrapper for K8s StatefulSet object which is meant to already be in a cluster before AppController execution @@ -129,7 +155,11 @@ func (p ExistingStatefulSet) Create() error { // Status returns StatefulSet status. interfaces.ResourceReady is regarded as sufficient for it's dependencies to be created. func (p ExistingStatefulSet) Status(meta map[string]string) (interfaces.ResourceStatus, error) { - return statefulsetStatus(p.Client, p.Name, p.APIClient) + ps, err := p.Client.Get(p.Name) + if err != nil { + return interfaces.ResourceError, err + } + return podsStateFromLabels(p.APIClient, ps.Spec.Template.ObjectMeta.Labels) } // Delete deletes StatefulSet from the cluster diff --git a/pkg/resources/statefulset_test.go b/pkg/resources/statefulset_test.go index 9474fad..2235b88 100644 --- a/pkg/resources/statefulset_test.go +++ b/pkg/resources/statefulset_test.go @@ -25,9 +25,15 @@ import ( // TestStatefulSetSuccessCheck checks status of ready StatefulSet func TestStatefulSetSuccessCheck(t *testing.T) { - c := mocks.NewClient(mocks.MakeStatefulSet("notfail")) - status, err := statefulsetStatus(c.StatefulSets(), "notfail", c) + name := "notfail" + ss := mocks.MakeStatefulSet(name) + client := mocks.NewClient(ss) + + def := MakeDefinition(ss) + tested := createNewStatefulSet(def, client) + + status, err := tested.Status(nil) if err != nil { t.Error(err) } @@ -39,11 +45,18 @@ func TestStatefulSetSuccessCheck(t *testing.T) { // TestStatefulSetFailCheck checks status of not ready statefulset func TestStatefulSetFailCheck(t *testing.T) { - ss := mocks.MakeStatefulSet("fail") - pod := mocks.MakePod("fail") + name := "fail" + ss := mocks.MakeStatefulSet(name) + + pod := mocks.MakePod(name) pod.Labels = ss.Spec.Template.ObjectMeta.Labels - c := mocks.NewClient(ss, pod) - status, err := statefulsetStatus(c.StatefulSets(), "fail", c) + + client := mocks.NewClient(ss, pod) + + def := MakeDefinition(ss) + tested := createNewStatefulSet(def, client) + + status, err := tested.Status(nil) expectedError := "resource pod/fail is not ready" if err.Error() != expectedError { @@ -68,3 +81,26 @@ func TestStatefulSetDisabledOn14Version(t *testing.T) { t.Errorf("%v expected to be disabled", v1beta1.SchemeGroupVersion) } } + +// TestStatefulSetUpgraded tests status behaviour with resource definition differing from object in cluster +func TestStatefulSetUpgraded(t *testing.T) { + name := "notfail" + + client := mocks.NewClient(mocks.MakeStatefulSet(name)) + + def := MakeDefinition(mocks.MakeStatefulSet(name)) + //Make definition differ from client version + def.StatefulSet.ObjectMeta.Labels = map[string]string{ + "trolo": "lolo", + } + tested := createNewStatefulSet(def, client) + + status, err := tested.Status(nil) + + if err != nil { + t.Error("Error found, expected nil") + } + if status != interfaces.ResourceWaitingForUpgrade { + t.Errorf("Status should be `waiting for upgrade`, is `%s` instead.", status) + } +} From ab64919aeaebc3b283197e6e3142e1dfbad919a0 Mon Sep 17 00:00:00 2001 From: Maciej Kwiek Date: Wed, 31 May 2017 14:43:50 +0200 Subject: [PATCH 2/2] Resource upgrade in added in scheduler --- pkg/interfaces/interfaces.go | 1 + pkg/mocks/countingresource.go | 5 +++++ pkg/mocks/resource.go | 5 +++++ pkg/resources/common.go | 5 +++++ pkg/resources/configmap.go | 6 ++++++ pkg/resources/daemonset.go | 6 ++++++ pkg/resources/deployment.go | 6 ++++++ pkg/resources/job.go | 6 ++++++ pkg/resources/persistentvolumeclaim.go | 6 ++++++ pkg/resources/petset.go | 6 ++++++ pkg/resources/pod.go | 6 ++++++ pkg/resources/replicaset.go | 6 ++++++ pkg/resources/secrets.go | 6 ++++++ pkg/resources/service.go | 6 ++++++ pkg/resources/serviceaccount.go | 6 ++++++ pkg/resources/statefulset.go | 6 ++++++ pkg/scheduler/scheduler.go | 8 ++++++++ 17 files changed, 96 insertions(+) diff --git a/pkg/interfaces/interfaces.go b/pkg/interfaces/interfaces.go index 5b6e224..048d1be 100644 --- a/pkg/interfaces/interfaces.go +++ b/pkg/interfaces/interfaces.go @@ -40,6 +40,7 @@ type BaseResource interface { Create() error Delete() error Meta(string) interface{} + UpdateFromDefinition() error } // DependencyReport is a report of a single dependency of a node in graph diff --git a/pkg/mocks/countingresource.go b/pkg/mocks/countingresource.go index 02175b1..9928f7c 100644 --- a/pkg/mocks/countingresource.go +++ b/pkg/mocks/countingresource.go @@ -55,6 +55,11 @@ func (c *CountingResource) Create() error { return nil } +// UpdateFromDefinition does nothing +func (c *CountingResource) UpdateFromDefinition() error { + return nil +} + // Delete does nothing func (c *CountingResource) Delete() error { return nil diff --git a/pkg/mocks/resource.go b/pkg/mocks/resource.go index 52e774a..6117d51 100644 --- a/pkg/mocks/resource.go +++ b/pkg/mocks/resource.go @@ -42,6 +42,11 @@ func (c *Resource) Create() error { return nil } +// UpdateFromDefinition does nothing +func (c *Resource) UpdateFromDefinition() error { + return nil +} + // Delete does nothing func (c *Resource) Delete() error { return nil diff --git a/pkg/resources/common.go b/pkg/resources/common.go index a49f7a2..9317e0e 100644 --- a/pkg/resources/common.go +++ b/pkg/resources/common.go @@ -81,6 +81,11 @@ func (b Base) equalsToDefinition(_ interface{}) bool { return false } +// UpdateFromDefinition is an empty implementation +func (b Base) UpdateFromDefinition() error { + return nil +} + // KindToResourceTemplate is a map mapping kind strings to empty structs representing proper resources // structs implement interfaces.ResourceTemplate var KindToResourceTemplate = map[string]interfaces.ResourceTemplate{} diff --git a/pkg/resources/configmap.go b/pkg/resources/configmap.go index 49854dc..e5564ff 100644 --- a/pkg/resources/configmap.go +++ b/pkg/resources/configmap.go @@ -121,6 +121,12 @@ func (c newConfigMap) Create() error { return nil } +// UpdateFromDefinition updates k8s object with the definition contents +func (c newConfigMap) UpdateFromDefinition() (err error) { + c.ConfigMap, err = c.Client.Update(c.Definition.ConfigMap) + return err +} + // Delete deletes configMap from the cluster func (c newConfigMap) Delete() error { return c.Client.Delete(c.ConfigMap.Name, &v1.DeleteOptions{}) diff --git a/pkg/resources/daemonset.go b/pkg/resources/daemonset.go index a8a5047..09c83d8 100644 --- a/pkg/resources/daemonset.go +++ b/pkg/resources/daemonset.go @@ -114,6 +114,12 @@ func (d DaemonSet) Create() error { return nil } +// UpdateFromDefinition updates k8s object with the definition contents +func (d DaemonSet) UpdateFromDefinition() (err error) { + d.DaemonSet, err = d.Client.Update(d.Definition.DaemonSet) + return err +} + // Delete deletes DaemonSet from the cluster func (d DaemonSet) Delete() error { return d.Client.Delete(d.DaemonSet.Name, &v1.DeleteOptions{}) diff --git a/pkg/resources/deployment.go b/pkg/resources/deployment.go index 69bf1dc..4108310 100644 --- a/pkg/resources/deployment.go +++ b/pkg/resources/deployment.go @@ -132,6 +132,12 @@ func (d Deployment) Create() error { return err } +// UpdateFromDefinition updates k8s object with the definition contents +func (d Deployment) UpdateFromDefinition() (err error) { + d.Deployment, err = d.Client.Update(d.Definition.Deployment) + return err +} + // Delete deletes Deployment from the cluster func (d Deployment) Delete() error { return d.Client.Delete(d.Deployment.Name, nil) diff --git a/pkg/resources/job.go b/pkg/resources/job.go index 581dbb8..b32e3e3 100644 --- a/pkg/resources/job.go +++ b/pkg/resources/job.go @@ -116,6 +116,12 @@ func (j newJob) Create() error { return nil } +// UpdateFromDefinition updates k8s object with the definition contents +func (j newJob) UpdateFromDefinition() (err error) { + j.Job, err = j.Client.Update(j.Definition.Job) + return err +} + // Delete deletes Job from the cluster func (j newJob) Delete() error { return j.Client.Delete(j.Job.Name, nil) diff --git a/pkg/resources/persistentvolumeclaim.go b/pkg/resources/persistentvolumeclaim.go index defea7b..3ce6ba1 100644 --- a/pkg/resources/persistentvolumeclaim.go +++ b/pkg/resources/persistentvolumeclaim.go @@ -103,6 +103,12 @@ func (p newPersistentVolumeClaim) Create() error { return nil } +// UpdateFromDefinition updates k8s object with the definition contents +func (p newPersistentVolumeClaim) UpdateFromDefinition() (err error) { + p.PersistentVolumeClaim, err = p.Client.Update(p.Definition.PersistentVolumeClaim) + return err +} + // Delete deletes persistentVolumeClaim from the cluster func (p newPersistentVolumeClaim) Delete() error { return p.Client.Delete(p.PersistentVolumeClaim.Name, &v1.DeleteOptions{}) diff --git a/pkg/resources/petset.go b/pkg/resources/petset.go index 8c06549..2ee34e4 100644 --- a/pkg/resources/petset.go +++ b/pkg/resources/petset.go @@ -91,6 +91,12 @@ func (p PetSet) Create() error { return nil } +// UpdateFromDefinition updates k8s object with the definition contents +func (p PetSet) UpdateFromDefinition() (err error) { + p.PetSet, err = p.Client.Update(p.Definition.PetSet) + return err +} + // Delete deletes PetSet from the cluster func (p PetSet) Delete() error { return p.Client.Delete(p.PetSet.Name, nil) diff --git a/pkg/resources/pod.go b/pkg/resources/pod.go index f4e8873..613a949 100644 --- a/pkg/resources/pod.go +++ b/pkg/resources/pod.go @@ -106,6 +106,12 @@ func (p newPod) Create() error { return nil } +// UpdateFromDefinition updates k8s object with the definition contents +func (p newPod) UpdateFromDefinition() (err error) { + p.Pod, err = p.Client.Update(p.Definition.Pod) + return err +} + // Delete deletes pod from the cluster func (p newPod) Delete() error { return p.Client.Delete(p.Pod.Name, nil) diff --git a/pkg/resources/replicaset.go b/pkg/resources/replicaset.go index efcd1d7..4d6b49e 100644 --- a/pkg/resources/replicaset.go +++ b/pkg/resources/replicaset.go @@ -136,6 +136,12 @@ func (r newReplicaSet) Create() error { return nil } +// UpdateFromDefinition updates k8s object with the definition contents +func (r newReplicaSet) UpdateFromDefinition() (err error) { + r.ReplicaSet, err = r.Client.Update(r.Definition.ReplicaSet) + return err +} + // Delete deletes ReplicaSet from the cluster func (r newReplicaSet) Delete() error { return r.Client.Delete(r.ReplicaSet.Name, nil) diff --git a/pkg/resources/secrets.go b/pkg/resources/secrets.go index 36a426b..4c9a038 100644 --- a/pkg/resources/secrets.go +++ b/pkg/resources/secrets.go @@ -127,6 +127,12 @@ func (s newSecret) Create() error { return nil } +// UpdateFromDefinition updates k8s object with the definition contents +func (s newSecret) UpdateFromDefinition() (err error) { + s.Secret, err = s.Client.Update(s.Definition.Secret) + return err +} + // Delete deletes Secret from the cluster func (s newSecret) Delete() error { return s.Client.Delete(s.Secret.Name, nil) diff --git a/pkg/resources/service.go b/pkg/resources/service.go index a00770f..3954a41 100644 --- a/pkg/resources/service.go +++ b/pkg/resources/service.go @@ -157,6 +157,12 @@ func (s newService) Create() error { return nil } +// UpdateFromDefinition updates k8s object with the definition contents +func (s newService) UpdateFromDefinition() (err error) { + s.Service, err = s.Client.Update(s.Definition.Service) + return err +} + // Delete deletes Service from the cluster func (s newService) Delete() error { return s.Client.Delete(s.Service.Name, nil) diff --git a/pkg/resources/serviceaccount.go b/pkg/resources/serviceaccount.go index d6900b6..2c95a7f 100644 --- a/pkg/resources/serviceaccount.go +++ b/pkg/resources/serviceaccount.go @@ -121,6 +121,12 @@ func (c newServiceAccount) Create() error { return nil } +// UpdateFromDefinition updates k8s object with the definition contents +func (c newServiceAccount) UpdateFromDefinition() (err error) { + c.ServiceAccount, err = c.Client.Update(c.Definition.ServiceAccount) + return err +} + // Delete deletes ServiceAccount from the cluster func (c newServiceAccount) Delete() error { return c.Client.Delete(c.ServiceAccount.Name, &v1.DeleteOptions{}) diff --git a/pkg/resources/statefulset.go b/pkg/resources/statefulset.go index 8a9e7ed..45b69fd 100644 --- a/pkg/resources/statefulset.go +++ b/pkg/resources/statefulset.go @@ -96,6 +96,12 @@ func (p StatefulSet) Create() error { return nil } +// UpdateFromDefinition updates k8s object with the definition contents +func (p StatefulSet) UpdateFromDefinition() (err error) { + p.StatefulSet, err = p.Client.Update(p.Definition.StatefulSet) + return err +} + // Delete deletes StatefulSet from the cluster func (p StatefulSet) Delete() error { return p.Client.Delete(p.StatefulSet.Name, nil) diff --git a/pkg/scheduler/scheduler.go b/pkg/scheduler/scheduler.go index 6322ed4..52f54e0 100644 --- a/pkg/scheduler/scheduler.go +++ b/pkg/scheduler/scheduler.go @@ -74,6 +74,7 @@ func (sr *ScheduledResource) RequestCreation(toCreate chan *ScheduledResource) b return false } +// it can update the resource if it doesn't match the definition func isResourceFinished(sr *ScheduledResource, ch chan error) bool { status, err := sr.Resource.Status(nil) if err != nil { @@ -84,6 +85,13 @@ func isResourceFinished(sr *ScheduledResource, ch chan error) bool { ch <- nil return true } + + if status == interfaces.ResourceWaitingForUpgrade { + err = sr.Resource.UpdateFromDefinition() + ch <- err + return true + } + return false }