Skip to content

Commit

Permalink
add common ACK pkg/condition
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jaypipes committed Jun 20, 2021
1 parent 814ef67 commit 4db22b4
Show file tree
Hide file tree
Showing 6 changed files with 370 additions and 2 deletions.
5 changes: 5 additions & 0 deletions mocks/pkg/types/aws_resource.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions mocks/pkg/types/manages_conditions.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

113 changes: 113 additions & 0 deletions pkg/condition/condition.go
Original file line number Diff line number Diff line change
@@ -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)
}
188 changes: 188 additions & 0 deletions pkg/condition/condition_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
3 changes: 1 addition & 2 deletions pkg/types/aws_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions pkg/types/manages_conditions.go
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit 4db22b4

Please sign in to comment.