From 4db22b445ad9845fb2dcbabb6e50b7731ed75d4d Mon Sep 17 00:00:00 2001 From: Jay Pipes Date: Sun, 20 Jun 2021 14:21:59 -0400 Subject: [PATCH] add common ACK pkg/condition Adds common resource condition functions to a new `pkg/condition`: - `Synced()` returns the resource's Condition of type ConditionTypeResourceSynced, or nil if the condition isn't found. - `Terminal()` returns the resource's Condition of type ConditionTypeTerminal, or nil if the condition isn't found. - `FirstOfType()` returns the first Condition of the specified type, or nil if a condition of the type isn't found on the resource. - `AllOfType()` returns a slice of `Condition` having the specified condition type. - `SetSynced()` ensures that a Condition of type ConditionTypeResourceSynced is present in the resource's Conditions collection and has a specified status, message and reason - `SetTerminal()` ensures that a Condition of type ConditionTypeResourceTerminal is present in the resource's Conditions collection and has a specified status, message and reason IMPORTANT COMPATIBILITY NOTE: Note that the `pkg/types.AWSResource` interface has been modified to be a composition of a new `pkg/types.ManagesConditions` interface which adds a new `ReplaceConditions()` method that overwrites a resource's Conditions collection. This is a backwards-incompatible change since the `pkg/types.AWSResource` implementations generated by `aws-controllers-k8s/code-generator` (the `pkg/{RESOURCE/resource.go` files...}`) will not include this new `ReplaceConditions` implementation and thus won't compile if you attempt to regenerate a controller using the ACK runtime after this patch is merged. A corresponding patch to the code-generator is coming that adds the `ReplaceCondiions` implementation. --- mocks/pkg/types/aws_resource.go | 5 + mocks/pkg/types/manages_conditions.go | 35 +++++ pkg/condition/condition.go | 113 ++++++++++++++++ pkg/condition/condition_test.go | 188 ++++++++++++++++++++++++++ pkg/types/aws_resource.go | 3 +- pkg/types/manages_conditions.go | 28 ++++ 6 files changed, 370 insertions(+), 2 deletions(-) create mode 100644 mocks/pkg/types/manages_conditions.go create mode 100644 pkg/condition/condition.go create mode 100644 pkg/condition/condition_test.go create mode 100644 pkg/types/manages_conditions.go diff --git a/mocks/pkg/types/aws_resource.go b/mocks/pkg/types/aws_resource.go index 8d89f66..968f5de 100644 --- a/mocks/pkg/types/aws_resource.go +++ b/mocks/pkg/types/aws_resource.go @@ -80,6 +80,11 @@ func (_m *AWSResource) MetaObject() v1.Object { return r0 } +// ReplaceConditions provides a mock function with given fields: _a0 +func (_m *AWSResource) ReplaceConditions(_a0 []*v1alpha1.Condition) { + _m.Called(_a0) +} + // RuntimeMetaObject provides a mock function with given fields: func (_m *AWSResource) RuntimeMetaObject() types.RuntimeMetaObject { ret := _m.Called() diff --git a/mocks/pkg/types/manages_conditions.go b/mocks/pkg/types/manages_conditions.go new file mode 100644 index 0000000..00f1b82 --- /dev/null +++ b/mocks/pkg/types/manages_conditions.go @@ -0,0 +1,35 @@ +// Code generated by mockery v2.2.2. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + + v1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" +) + +// ManagesConditions is an autogenerated mock type for the ManagesConditions type +type ManagesConditions struct { + mock.Mock +} + +// Conditions provides a mock function with given fields: +func (_m *ManagesConditions) Conditions() []*v1alpha1.Condition { + ret := _m.Called() + + var r0 []*v1alpha1.Condition + if rf, ok := ret.Get(0).(func() []*v1alpha1.Condition); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*v1alpha1.Condition) + } + } + + return r0 +} + +// ReplaceConditions provides a mock function with given fields: _a0 +func (_m *ManagesConditions) ReplaceConditions(_a0 []*v1alpha1.Condition) { + _m.Called(_a0) +} diff --git a/pkg/condition/condition.go b/pkg/condition/condition.go new file mode 100644 index 0000000..f70ba33 --- /dev/null +++ b/pkg/condition/condition.go @@ -0,0 +1,113 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file 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 condition + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + acktypes "github.com/aws-controllers-k8s/runtime/pkg/types" +) + +// Synced returns the Condition in the resource's Conditions collection that is +// of type ConditionTypeResourceSynced. If no such condition is found, returns +// nil. +func Synced(subject acktypes.ManagesConditions) *ackv1alpha1.Condition { + return FirstOfType(subject, ackv1alpha1.ConditionTypeResourceSynced) +} + +// Terminal returns the Condition in the resource's Conditions collection that +// is of type ConditionTypeTerminal. If no such condition is found, returns +// nil. +func Terminal(subject acktypes.ManagesConditions) *ackv1alpha1.Condition { + return FirstOfType(subject, ackv1alpha1.ConditionTypeTerminal) +} + +// FirstOfType returns the first Condition in the resource's Conditions +// collection of the supplied type. If no such condition is found, returns nil. +func FirstOfType( + subject acktypes.ManagesConditions, + condType ackv1alpha1.ConditionType, +) *ackv1alpha1.Condition { + for _, condition := range subject.Conditions() { + if condition.Type == condType { + return condition + } + } + return nil +} + +// AllOfType returns a slice of Conditions in the resource's Conditions +// collection of the supplied type. +func AllOfType( + subject acktypes.ManagesConditions, + condType ackv1alpha1.ConditionType, +) []*ackv1alpha1.Condition { + res := []*ackv1alpha1.Condition{} + for _, condition := range subject.Conditions() { + if condition.Type == condType { + res = append(res, condition) + } + } + return res +} + +// SetSynced sets the resource's Condition of type ConditionTypeResourceSynced +// to the supplied status, optional message and reason. +func SetSynced( + subject acktypes.ManagesConditions, + status corev1.ConditionStatus, + message *string, + reason *string, +) { + allConds := subject.Conditions() + var c *ackv1alpha1.Condition + if c = Synced(subject); c == nil { + c = &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeResourceSynced, + } + allConds = append(allConds, c) + } + now := metav1.Now() + c.LastTransitionTime = &now + c.Status = status + c.Message = message + c.Reason = reason + subject.ReplaceConditions(allConds) +} + +// SetTerminal sets the resource's Condition of type ConditionTypeTerminal to +// the supplied status, optional message and reason. +func SetTerminal( + subject acktypes.ManagesConditions, + status corev1.ConditionStatus, + message *string, + reason *string, +) { + allConds := subject.Conditions() + var c *ackv1alpha1.Condition + if c = Terminal(subject); c == nil { + c = &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeTerminal, + } + allConds = append(allConds, c) + } + now := metav1.Now() + c.LastTransitionTime = &now + c.Status = status + c.Message = message + c.Reason = reason + subject.ReplaceConditions(allConds) +} diff --git a/pkg/condition/condition_test.go b/pkg/condition/condition_test.go new file mode 100644 index 0000000..ad81a00 --- /dev/null +++ b/pkg/condition/condition_test.go @@ -0,0 +1,188 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file 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 condition_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" + ackcond "github.com/aws-controllers-k8s/runtime/pkg/condition" + corev1 "k8s.io/api/core/v1" + + ackmocks "github.com/aws-controllers-k8s/runtime/mocks/pkg/types" +) + +func TestConditionGetters(t *testing.T) { + assert := assert.New(t) + + conds := []*ackv1alpha1.Condition{} + + r := &ackmocks.AWSResource{} + r.On("Conditions").Return(conds) + + got := ackcond.Synced(r) + assert.Nil(got) + + got = ackcond.Terminal(r) + assert.Nil(got) + + conds = append(conds, &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeResourceSynced, + Status: corev1.ConditionFalse, + }) + + r = &ackmocks.AWSResource{} + r.On("Conditions").Return(conds) + + got = ackcond.Synced(r) + assert.NotNil(got) + + got = ackcond.Terminal(r) + assert.Nil(got) + + conds = append(conds, &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeTerminal, + Status: corev1.ConditionFalse, + }) + + r = &ackmocks.AWSResource{} + r.On("Conditions").Return(conds) + + got = ackcond.Synced(r) + assert.NotNil(got) + + got = ackcond.Terminal(r) + assert.NotNil(got) + + gotAll := ackcond.AllOfType(r, ackv1alpha1.ConditionTypeAdvisory) + assert.Empty(gotAll) + + msg1 := "advice 1" + conds = append(conds, &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeAdvisory, + Status: corev1.ConditionTrue, + Message: &msg1, + }) + + msg2 := "advice 2" + conds = append(conds, &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeAdvisory, + Status: corev1.ConditionTrue, + Message: &msg2, + }) + + r = &ackmocks.AWSResource{} + r.On("Conditions").Return(conds) + + gotAll = ackcond.AllOfType(r, ackv1alpha1.ConditionTypeAdvisory) + assert.NotEmpty(gotAll) + assert.Equal(len(gotAll), 2) +} + +func TestConditionSetters(t *testing.T) { + r := &ackmocks.AWSResource{} + r.On("Conditions").Return([]*ackv1alpha1.Condition{}) + + // Ensure that if there is no synced condition, it gets added... + r.On( + "ReplaceConditions", + mock.MatchedBy(func(subject []*ackv1alpha1.Condition) bool { + if len(subject) != 1 { + return false + } + // We need to ignore timestamps for LastTransitionTime in our argument + // assertions... + return (subject[0].Type == ackv1alpha1.ConditionTypeResourceSynced && + subject[0].Status == corev1.ConditionTrue && + subject[0].Message == nil && + subject[0].Reason == nil) + }), + ) + + ackcond.SetSynced(r, corev1.ConditionTrue, nil, nil) + + // Ensure that SetSynced doesn't overwrite any other conditions... + r = &ackmocks.AWSResource{} + r.On("Conditions").Return( + []*ackv1alpha1.Condition{ + &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeTerminal, + Status: corev1.ConditionTrue, + }, + }, + ) + r.On( + "ReplaceConditions", + mock.MatchedBy(func(subject []*ackv1alpha1.Condition) bool { + if len(subject) != 2 { + return false + } + return (subject[0].Type == ackv1alpha1.ConditionTypeTerminal && + subject[0].Status == corev1.ConditionTrue && + subject[1].Type == ackv1alpha1.ConditionTypeResourceSynced && + subject[1].Status == corev1.ConditionFalse) + }), + ) + + ackcond.SetSynced(r, corev1.ConditionFalse, nil, nil) + + // Ensure that SetSynced overwrites an existing synced condition... + r = &ackmocks.AWSResource{} + r.On("Conditions").Return( + []*ackv1alpha1.Condition{ + &ackv1alpha1.Condition{ + Type: ackv1alpha1.ConditionTypeResourceSynced, + Status: corev1.ConditionFalse, + }, + }, + ) + r.On( + "ReplaceConditions", + mock.MatchedBy(func(subject []*ackv1alpha1.Condition) bool { + if len(subject) != 1 { + return false + } + return (subject[0].Type == ackv1alpha1.ConditionTypeResourceSynced && + subject[0].Status == corev1.ConditionTrue) + }), + ) + + ackcond.SetSynced(r, corev1.ConditionTrue, nil, nil) + + msg1 := "message 1" + reason1 := "reason 1" + + // Ensure that if there is no terminal condition, it gets added... + r = &ackmocks.AWSResource{} + r.On("Conditions").Return([]*ackv1alpha1.Condition{}) + r.On( + "ReplaceConditions", + mock.MatchedBy(func(subject []*ackv1alpha1.Condition) bool { + if len(subject) != 1 { + return false + } + // We need to ignore timestamps for LastTransitionTime in our argument + // assertions... + return (subject[0].Type == ackv1alpha1.ConditionTypeTerminal && + subject[0].Status == corev1.ConditionTrue && + subject[0].Message == &msg1 && + subject[0].Reason == &reason1) + }), + ) + + ackcond.SetTerminal(r, corev1.ConditionTrue, &msg1, &reason1) +} diff --git a/pkg/types/aws_resource.go b/pkg/types/aws_resource.go index c17e6d8..0908782 100644 --- a/pkg/types/aws_resource.go +++ b/pkg/types/aws_resource.go @@ -32,12 +32,11 @@ type RuntimeMetaObject interface { // AWSResource represents a custom resource object in the Kubernetes API that // corresponds to a resource in an AWS service API. type AWSResource interface { + ManagesConditions // Identifiers returns an AWSResourceIdentifiers object containing various // identifying information, including the AWS account ID that owns the // resource, the resource's AWS Resource Name (ARN) Identifiers() AWSResourceIdentifiers - // Conditions returns the ACK Conditions collection for the AWSResource - Conditions() []*ackv1alpha1.Condition // IsBeingDeleted returns true if the Kubernetes resource has a non-zero // deletion timestamp IsBeingDeleted() bool diff --git a/pkg/types/manages_conditions.go b/pkg/types/manages_conditions.go new file mode 100644 index 0000000..7071035 --- /dev/null +++ b/pkg/types/manages_conditions.go @@ -0,0 +1,28 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"). You may +// not use this file except in compliance with the License. A copy of the +// License is located at +// +// http://aws.amazon.com/apache2.0/ +// +// or in the "license" file accompanying this file. This file 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 types + +import ( + ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1" +) + +// ManagesConditions describes a thing that can set and retrieve Condition +// objects. +type ManagesConditions interface { + // Conditions returns the ACK Conditions collection for the AWSResource + Conditions() []*ackv1alpha1.Condition + // ReplaceConditions replaces the resource's set of Condition structs with + // the supplied slice of Conditions. + ReplaceConditions([]*ackv1alpha1.Condition) +}