From 0a9d55cdcf4e7fcfd8d14b097ac9d8777c9f2059 Mon Sep 17 00:00:00 2001 From: "jesus m. rodriguez" Date: Thu, 23 Jul 2020 17:05:47 -0400 Subject: [PATCH] Remove leader and status which have moved to operator-lib. --- .../fragments/rm-pkg-leader-and-status.yaml | 15 + pkg/leader/doc.go | 54 ---- pkg/leader/leader.go | 185 ------------ pkg/status/conditions.go | 189 ------------- pkg/status/conditions_test.go | 266 ------------------ 5 files changed, 15 insertions(+), 694 deletions(-) create mode 100644 changelog/fragments/rm-pkg-leader-and-status.yaml delete mode 100644 pkg/leader/doc.go delete mode 100644 pkg/leader/leader.go delete mode 100644 pkg/status/conditions.go delete mode 100644 pkg/status/conditions_test.go diff --git a/changelog/fragments/rm-pkg-leader-and-status.yaml b/changelog/fragments/rm-pkg-leader-and-status.yaml new file mode 100644 index 0000000000..deac7ef889 --- /dev/null +++ b/changelog/fragments/rm-pkg-leader-and-status.yaml @@ -0,0 +1,15 @@ +# entries is a list of entries to include in +# release notes and/or the migration guide +entries: + - description: > + Removed `pkg/leader` and `pkg/status`. These are now part of + operator-lib. + + kind: "removal" + + breaking: true + + migration: + header: Removed `pkg/leader` and `pkg/status` + body: > + TBD diff --git a/pkg/leader/doc.go b/pkg/leader/doc.go deleted file mode 100644 index b88c30a2cc..0000000000 --- a/pkg/leader/doc.go +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/* -Package leader implements Leader For Life, a simple alternative to lease-based -leader election. - -Both the Leader For Life and lease-based approaches to leader election are -built on the concept that each candidate will attempt to create a resource with -the same GVK, namespace, and name. Whichever candidate succeeds becomes the -leader. The rest receive "already exists" errors and wait for a new -opportunity. - -Leases provide a way to indirectly observe whether the leader still exists. The -leader must periodically renew its lease, usually by updating a timestamp in -its lock record. If it fails to do so, it is presumed dead, and a new election -takes place. If the leader is in fact still alive but unreachable, it is -expected to gracefully step down. A variety of factors can cause a leader to -fail at updating its lease, but continue acting as the leader before succeeding -at stepping down. - -In the "leader for life" approach, a specific Pod is the leader. Once -established (by creating a lock record), the Pod is the leader until it is -destroyed. There is no possibility for multiple pods to think they are the -leader at the same time. The leader does not need to renew a lease, consider -stepping down, or do anything related to election activity once it becomes the -leader. - -The lock record in this case is a ConfigMap whose OwnerReference is set to the -Pod that is the leader. When the leader is destroyed, the ConfigMap gets -garbage-collected, enabling a different candidate Pod to become the leader. - -Leader for Life requires that all candidate Pods be in the same Namespace. It -uses the downwards API to determine the pod name, as hostname is not reliable. -You should run it configured with: - -env: - - name: POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name -*/ -package leader diff --git a/pkg/leader/leader.go b/pkg/leader/leader.go deleted file mode 100644 index 702d2c140e..0000000000 --- a/pkg/leader/leader.go +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright 2018 The Operator-SDK Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package leader - -import ( - "context" - "time" - - "github.com/operator-framework/operator-sdk/internal/util/k8sutil" - - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/wait" - crclient "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/config" - logf "sigs.k8s.io/controller-runtime/pkg/log" -) - -var log = logf.Log.WithName("leader") - -// maxBackoffInterval defines the maximum amount of time to wait between -// attempts to become the leader. -const maxBackoffInterval = time.Second * 16 - -// Become ensures that the current pod is the leader within its namespace. If -// run outside a cluster, it will skip leader election and return nil. It -// continuously tries to create a ConfigMap with the provided name and the -// current pod set as the owner reference. Only one can exist at a time with -// the same name, so the pod that successfully creates the ConfigMap is the -// leader. Upon termination of that pod, the garbage collector will delete the -// ConfigMap, enabling a different pod to become the leader. -func Become(ctx context.Context, lockName string) error { - log.Info("Trying to become the leader.") - - ns, err := k8sutil.GetOperatorNamespace() - if err != nil { - if err == k8sutil.ErrNoNamespace || err == k8sutil.ErrRunLocal { - log.Info("Skipping leader election; not running in a cluster.") - return nil - } - return err - } - - config, err := config.GetConfig() - if err != nil { - return err - } - - client, err := crclient.New(config, crclient.Options{}) - if err != nil { - return err - } - - owner, err := myOwnerRef(ctx, client, ns) - if err != nil { - return err - } - - // check for existing lock from this pod, in case we got restarted - existing := &corev1.ConfigMap{} - key := crclient.ObjectKey{Namespace: ns, Name: lockName} - err = client.Get(ctx, key, existing) - - switch { - case err == nil: - for _, existingOwner := range existing.GetOwnerReferences() { - if existingOwner.Name == owner.Name { - log.Info("Found existing lock with my name. I was likely restarted.") - log.Info("Continuing as the leader.") - return nil - } - log.Info("Found existing lock", "LockOwner", existingOwner.Name) - } - case apierrors.IsNotFound(err): - log.Info("No pre-existing lock was found.") - default: - log.Error(err, "Unknown error trying to get ConfigMap") - return err - } - - cm := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: lockName, - Namespace: ns, - OwnerReferences: []metav1.OwnerReference{*owner}, - }, - } - - // try to create a lock - backoff := time.Second - for { - err := client.Create(ctx, cm) - switch { - case err == nil: - log.Info("Became the leader.") - return nil - case apierrors.IsAlreadyExists(err): - // refresh the lock so we use current leader - key := crclient.ObjectKey{Namespace: ns, Name: lockName} - if err := client.Get(ctx, key, existing); err != nil { - log.Info("Leader lock configmap not found.") - continue // configmap got lost ... just wait a bit - } - - existingOwners := existing.GetOwnerReferences() - switch { - case len(existingOwners) != 1: - log.Info("Leader lock configmap must have exactly one owner reference.", "ConfigMap", existing) - case existingOwners[0].Kind != "Pod": - log.Info("Leader lock configmap owner reference must be a pod.", "OwnerReference", existingOwners[0]) - default: - leaderPod := &corev1.Pod{} - key = crclient.ObjectKey{Namespace: ns, Name: existingOwners[0].Name} - err = client.Get(ctx, key, leaderPod) - switch { - case apierrors.IsNotFound(err): - log.Info("Leader pod has been deleted, waiting for garbage collection to remove the lock.") - case err != nil: - return err - case isPodEvicted(*leaderPod) && leaderPod.GetDeletionTimestamp() == nil: - log.Info("Operator pod with leader lock has been evicted.", "leader", leaderPod.Name) - log.Info("Deleting evicted leader.") - // Pod may not delete immediately, continue with backoff - err := client.Delete(ctx, leaderPod) - if err != nil { - log.Error(err, "Leader pod could not be deleted.") - } - - default: - log.Info("Not the leader. Waiting.") - } - } - - select { - case <-time.After(wait.Jitter(backoff, .2)): - if backoff < maxBackoffInterval { - backoff *= 2 - } - continue - case <-ctx.Done(): - return ctx.Err() - } - default: - log.Error(err, "Unknown error creating ConfigMap") - return err - } - } -} - -// myOwnerRef returns an OwnerReference that corresponds to the pod in which -// this code is currently running. -// It expects the environment variable POD_NAME to be set by the downwards API -func myOwnerRef(ctx context.Context, client crclient.Client, ns string) (*metav1.OwnerReference, error) { - myPod, err := k8sutil.GetPod(ctx, client, ns) - if err != nil { - return nil, err - } - - owner := &metav1.OwnerReference{ - APIVersion: "v1", - Kind: "Pod", - Name: myPod.ObjectMeta.Name, - UID: myPod.ObjectMeta.UID, - } - return owner, nil -} - -func isPodEvicted(pod corev1.Pod) bool { - podFailed := pod.Status.Phase == corev1.PodFailed - podEvicted := pod.Status.Reason == "Evicted" - return podFailed && podEvicted -} diff --git a/pkg/status/conditions.go b/pkg/status/conditions.go deleted file mode 100644 index 070cf863ae..0000000000 --- a/pkg/status/conditions.go +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package status - -import ( - "encoding/json" - "sort" - - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - kubeclock "k8s.io/apimachinery/pkg/util/clock" -) - -// clock is used to set status condition timestamps. -// This variable makes it easier to test conditions. -var clock kubeclock.Clock = &kubeclock.RealClock{} - -// ConditionType is the type of the condition and is typically a CamelCased -// word or short phrase. -// -// Condition types should indicate state in the "abnormal-true" polarity. For -// example, if the condition indicates when a policy is invalid, the "is valid" -// case is probably the norm, so the condition should be called "Invalid". -type ConditionType string - -// ConditionReason is intended to be a one-word, CamelCase representation of -// the category of cause of the current status. It is intended to be used in -// concise output, such as one-line kubectl get output, and in summarizing -// occurrences of causes. -type ConditionReason string - -// Condition represents an observation of an object's state. Conditions are an -// extension mechanism intended to be used when the details of an observation -// are not a priori known or would not apply to all instances of a given Kind. -// -// Conditions should be added to explicitly convey properties that users and -// components care about rather than requiring those properties to be inferred -// from other observations. Once defined, the meaning of a Condition can not be -// changed arbitrarily - it becomes part of the API, and has the same -// backwards- and forwards-compatibility concerns of any other part of the API. -type Condition struct { - Type ConditionType `json:"type"` - Status corev1.ConditionStatus `json:"status"` - Reason ConditionReason `json:"reason,omitempty"` - Message string `json:"message,omitempty"` - LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` -} - -// IsTrue Condition whether the condition status is "True". -func (c Condition) IsTrue() bool { - return c.Status == corev1.ConditionTrue -} - -// IsFalse returns whether the condition status is "False". -func (c Condition) IsFalse() bool { - return c.Status == corev1.ConditionFalse -} - -// IsUnknown returns whether the condition status is "Unknown". -func (c Condition) IsUnknown() bool { - return c.Status == corev1.ConditionUnknown -} - -// DeepCopyInto copies in into out. -func (c *Condition) DeepCopyInto(cpy *Condition) { - *cpy = *c -} - -// Conditions is a set of Condition instances. -type Conditions []Condition - -// NewConditions initializes a set of conditions with the given list of -// conditions. -func NewConditions(conds ...Condition) Conditions { - conditions := Conditions{} - for _, c := range conds { - conditions.SetCondition(c) - } - return conditions -} - -// IsTrueFor searches the set of conditions for a condition with the given -// ConditionType. If found, it returns `condition.IsTrue()`. If not found, -// it returns false. -func (conditions Conditions) IsTrueFor(t ConditionType) bool { - for _, condition := range conditions { - if condition.Type == t { - return condition.IsTrue() - } - } - return false -} - -// IsFalseFor searches the set of conditions for a condition with the given -// ConditionType. If found, it returns `condition.IsFalse()`. If not found, -// it returns false. -func (conditions Conditions) IsFalseFor(t ConditionType) bool { - for _, condition := range conditions { - if condition.Type == t { - return condition.IsFalse() - } - } - return false -} - -// IsUnknownFor searches the set of conditions for a condition with the given -// ConditionType. If found, it returns `condition.IsUnknown()`. If not found, -// it returns true. -func (conditions Conditions) IsUnknownFor(t ConditionType) bool { - for _, condition := range conditions { - if condition.Type == t { - return condition.IsUnknown() - } - } - return true -} - -// SetCondition adds (or updates) the set of conditions with the given -// condition. It returns a boolean value indicating whether the set condition -// is new or was a change to the existing condition with the same type. -func (conditions *Conditions) SetCondition(newCond Condition) bool { - newCond.LastTransitionTime = metav1.Time{Time: clock.Now()} - - for i, condition := range *conditions { - if condition.Type == newCond.Type { - if condition.Status == newCond.Status { - newCond.LastTransitionTime = condition.LastTransitionTime - } - changed := condition.Status != newCond.Status || - condition.Reason != newCond.Reason || - condition.Message != newCond.Message - (*conditions)[i] = newCond - return changed - } - } - *conditions = append(*conditions, newCond) - return true -} - -// GetCondition searches the set of conditions for the condition with the given -// ConditionType and returns it. If the matching condition is not found, -// GetCondition returns nil. -func (conditions Conditions) GetCondition(t ConditionType) *Condition { - for _, condition := range conditions { - if condition.Type == t { - return &condition - } - } - return nil -} - -// RemoveCondition removes the condition with the given ConditionType from -// the conditions set. If no condition with that type is found, RemoveCondition -// returns without performing any action. If the passed condition type is not -// found in the set of conditions, RemoveCondition returns false. -func (conditions *Conditions) RemoveCondition(t ConditionType) bool { - if conditions == nil { - return false - } - for i, condition := range *conditions { - if condition.Type == t { - *conditions = append((*conditions)[:i], (*conditions)[i+1:]...) - return true - } - } - return false -} - -// MarshalJSON marshals the set of conditions as a JSON array, sorted by -// condition type. -func (conditions Conditions) MarshalJSON() ([]byte, error) { - conds := []Condition(conditions) - sort.Slice(conds, func(a, b int) bool { - return conds[a].Type < conds[b].Type - }) - return json.Marshal(conds) -} diff --git a/pkg/status/conditions_test.go b/pkg/status/conditions_test.go deleted file mode 100644 index 9e523cc7b5..0000000000 --- a/pkg/status/conditions_test.go +++ /dev/null @@ -1,266 +0,0 @@ -// Copyright 2020 The Operator-SDK Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package status - -import ( - "encoding/json" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/assert" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - kubeclock "k8s.io/apimachinery/pkg/util/clock" -) - -var ( - initTime time.Time - clockInterval time.Duration -) - -func init() { - loc, _ := time.LoadLocation("Local") - initTime = time.Date(2015, time.July, 11, 0, 1, 0, 0, loc) - clockInterval = time.Hour -} - -func initConditions(init ...Condition) Conditions { - // Use the same initial time for all initial conditions - clock = kubeclock.NewFakeClock(initTime) - conditions := Conditions{} - for _, c := range init { - conditions.SetCondition(c) - } - - // Use an incrementing clock for the rest of the test - clock = &kubeclock.IntervalClock{ - Time: initTime, - Duration: clockInterval, - } - - return conditions -} - -func generateCondition(t ConditionType, s corev1.ConditionStatus) Condition { - c := Condition{ - Type: t, - Status: s, - Reason: ConditionReason(fmt.Sprintf("My%s%s", t, s)), - Message: fmt.Sprintf("Condition %s is %s", t, s), - } - return c -} - -func withLastTransitionTime(c Condition, t time.Time) Condition { - c.LastTransitionTime = metav1.Time{Time: t} - return c -} - -func TestConditionDeepCopy(t *testing.T) { - a := generateCondition("A", corev1.ConditionTrue) - var aCopy Condition - a.DeepCopyInto(&aCopy) - if &a == &aCopy { - t.Errorf("Expected and actual point to the same object: %p %#v", &a, &a) - } - if &a.Status == &aCopy.Status { - t.Errorf("Expected and actual point to the same object: %p %#v", &a.Status, &a.Status) - } - if &a.Reason == &aCopy.Reason { - t.Errorf("Expected and actual point to the same object: %p %#v", &a.Reason, &a.Reason) - } - if &a.Message == &aCopy.Message { - t.Errorf("Expected and actual point to the same object: %p %#v", &a.Message, &a.Message) - } -} - -func TestConditionsSetEmpty(t *testing.T) { - conditions := initConditions() - - setCondition := generateCondition("A", corev1.ConditionTrue) - assert.True(t, conditions.SetCondition(setCondition)) - - expectedCondition := withLastTransitionTime(setCondition, initTime.Add(clockInterval)) - actualCondition := conditions.GetCondition(setCondition.Type) - assert.Equal(t, 1, len(conditions)) - assert.Equal(t, expectedCondition, *actualCondition) -} - -func TestConditionsSetNotExists(t *testing.T) { - conditions := initConditions(generateCondition("B", corev1.ConditionTrue)) - - setCondition := generateCondition("A", corev1.ConditionTrue) - assert.True(t, conditions.SetCondition(setCondition)) - - expectedCondition := withLastTransitionTime(setCondition, initTime.Add(clockInterval)) - actualCondition := conditions.GetCondition(expectedCondition.Type) - assert.Equal(t, 2, len(conditions)) - assert.Equal(t, expectedCondition, *actualCondition) -} - -func TestConditionsSetExistsIdentical(t *testing.T) { - existingCondition := generateCondition("A", corev1.ConditionTrue) - conditions := initConditions(existingCondition) - - setCondition := existingCondition - assert.False(t, conditions.SetCondition(setCondition)) - - expectedCondition := withLastTransitionTime(setCondition, initTime) - actualCondition := conditions.GetCondition(expectedCondition.Type) - assert.Equal(t, 1, len(conditions)) - assert.Equal(t, expectedCondition, *actualCondition) -} -func TestConditionsSetExistsDifferentReason(t *testing.T) { - existingCondition := generateCondition("A", corev1.ConditionTrue) - conditions := initConditions(existingCondition) - - setCondition := existingCondition - setCondition.Reason = "ChangedReason" - assert.True(t, conditions.SetCondition(setCondition)) - - expectedCondition := withLastTransitionTime(setCondition, initTime) - actualCondition := conditions.GetCondition(expectedCondition.Type) - assert.Equal(t, 1, len(conditions)) - assert.Equal(t, expectedCondition, *actualCondition) -} - -func TestConditionsSetExistsDifferentStatus(t *testing.T) { - existingCondition := generateCondition("A", corev1.ConditionTrue) - conditions := initConditions(existingCondition) - - setCondition := existingCondition - setCondition.Status = corev1.ConditionFalse - setCondition.Reason = "ChangedReason" - assert.True(t, conditions.SetCondition(setCondition)) - - expectedCondition := withLastTransitionTime(setCondition, initTime.Add(clockInterval)) - actualCondition := conditions.GetCondition(expectedCondition.Type) - assert.Equal(t, 1, len(conditions)) - assert.Equal(t, expectedCondition, *actualCondition) -} - -func TestConditionsGetNotExists(t *testing.T) { - conditions := initConditions(generateCondition("A", corev1.ConditionTrue)) - - actualCondition := conditions.GetCondition(ConditionType("B")) - assert.Nil(t, actualCondition) -} - -func TestConditionsRemoveFromNilConditions(t *testing.T) { - var conditions *Conditions = nil - assert.False(t, conditions.RemoveCondition(ConditionType("C"))) -} - -func TestConditionsRemoveNotExists(t *testing.T) { - conditions := initConditions( - generateCondition("A", corev1.ConditionTrue), - generateCondition("B", corev1.ConditionTrue), - ) - - assert.False(t, conditions.RemoveCondition(ConditionType("C"))) - a := conditions.GetCondition(ConditionType("A")) - b := conditions.GetCondition(ConditionType("B")) - assert.NotNil(t, a) - assert.NotNil(t, b) - assert.Equal(t, 2, len(conditions)) -} - -func TestConditionsRemoveExists(t *testing.T) { - conditions := initConditions( - generateCondition("A", corev1.ConditionTrue), - generateCondition("B", corev1.ConditionTrue), - ) - - assert.True(t, conditions.RemoveCondition(ConditionType("A"))) - a := conditions.GetCondition(ConditionType("A")) - b := conditions.GetCondition(ConditionType("B")) - assert.Nil(t, a) - assert.NotNil(t, b) - assert.Equal(t, 1, len(conditions)) -} - -func TestConditionsIsTrueFor(t *testing.T) { - conditions := NewConditions( - generateCondition("False", corev1.ConditionFalse), - generateCondition("True", corev1.ConditionTrue), - generateCondition("Unknown", corev1.ConditionUnknown), - ) - - assert.True(t, conditions.IsTrueFor(ConditionType("True"))) - assert.False(t, conditions.IsTrueFor(ConditionType("False"))) - assert.False(t, conditions.IsTrueFor(ConditionType("Unknown"))) - assert.False(t, conditions.IsTrueFor(ConditionType("DoesNotExist"))) -} - -func TestConditionsIsFalseFor(t *testing.T) { - conditions := NewConditions( - generateCondition("False", corev1.ConditionFalse), - generateCondition("True", corev1.ConditionTrue), - generateCondition("Unknown", corev1.ConditionUnknown), - ) - - assert.False(t, conditions.IsFalseFor(ConditionType("True"))) - assert.True(t, conditions.IsFalseFor(ConditionType("False"))) - assert.False(t, conditions.IsFalseFor(ConditionType("Unknown"))) - assert.False(t, conditions.IsFalseFor(ConditionType("DoesNotExist"))) -} - -func TestConditionsIsUnknownFor(t *testing.T) { - conditions := NewConditions( - generateCondition("False", corev1.ConditionFalse), - generateCondition("True", corev1.ConditionTrue), - generateCondition("Unknown", corev1.ConditionUnknown), - ) - - assert.False(t, conditions.IsUnknownFor(ConditionType("True"))) - assert.False(t, conditions.IsUnknownFor(ConditionType("False"))) - assert.True(t, conditions.IsUnknownFor(ConditionType("Unknown"))) - assert.True(t, conditions.IsUnknownFor(ConditionType("DoesNotExist"))) -} - -func TestConditionsMarshalUnmarshalJSON(t *testing.T) { - a := generateCondition("A", corev1.ConditionTrue) - b := generateCondition("B", corev1.ConditionTrue) - c := generateCondition("C", corev1.ConditionTrue) - d := generateCondition("D", corev1.ConditionTrue) - - // Insert conditions unsorted - conditions := initConditions(b, d, c, a) - - data, err := json.Marshal(conditions) - if err != nil { - t.Fatalf("Failed to marshal JSON: %s", err) - } - - // Test that conditions are in sorted order by type. - in := []Condition{} - err = json.Unmarshal(data, &in) - if err != nil { - t.Fatalf("Failed to unmarshal JSON: %s", err) - } - assert.Equal(t, a.Type, in[0].Type) - assert.Equal(t, b.Type, in[1].Type) - assert.Equal(t, c.Type, in[2].Type) - assert.Equal(t, d.Type, in[3].Type) - - // Test that the marshal/unmarshal cycle is lossless. - unmarshalConds := Conditions{} - err = json.Unmarshal(data, &unmarshalConds) - if err != nil { - t.Fatalf("Failed to unmarshal JSON: %s", err) - } - assert.Equal(t, conditions, unmarshalConds) -}