Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Composition Function Events and Status Conditions #129

Merged
merged 1 commit into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ NPROCS ?= 1
GO_TEST_PARALLEL := $(shell echo $$(( $(NPROCS) / 2 )))

GO_LDFLAGS += -X $(GO_PROJECT)/pkg/version.Version=$(VERSION)
GO_SUBDIRS += proto
GO_SUBDIRS += errors proto resource response request
GO111MODULE = on
GOLANGCILINT_VERSION = 1.55.2
GO_LINT_ARGS ?= "--fix"
Expand Down
642 changes: 469 additions & 173 deletions proto/v1beta1/run_function.pb.go

Large diffs are not rendered by default.

66 changes: 66 additions & 0 deletions proto/v1beta1/run_function.proto
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ message RunFunctionResponse {

// Requirements that must be satisfied for this Function to run successfully.
Requirements requirements = 5;

// Status Conditions to be applied to the Composite Resource and sometimes the
// Claim.
repeated Condition conditions = 6;
}

// RequestMeta contains metadata pertaining to a RunFunctionRequest.
Expand All @@ -142,11 +146,18 @@ message Requirements {

// ResourceSelector selects a group of resources, either by name or by label.
message ResourceSelector {
// API version of resources to select.
string api_version = 1;

// Kind of resources to select.
string kind = 2;

// Resources to match.
oneof match {
// Match the resource with this name.
string match_name = 3;

// Match all resources with these labels.
MatchLabels match_labels = 4;
}
}
Expand Down Expand Up @@ -239,6 +250,12 @@ message Result {

// Human-readable details about the result.
string message = 2;

// Optional PascalCase, machine-readable reason for this result.
optional string reason = 3;

// The resources this result targets.
optional Target target = 4;
}

// Severity of Function results.
Expand All @@ -259,3 +276,52 @@ enum Severity {
// with the composite resource.
SEVERITY_NORMAL = 3;
}

// Target of Function results.
enum Target {
// If the target is unspecified, the result targets the composite resource.
TARGET_UNSPECIFIED = 0;

// Target the composite resource. Results that target the composite resource
// should include detailed, advanced information.
TARGET_COMPOSITE = 1;

// Target the composite and the claim. Results that target the composite and
// the claim should include only end-user friendly information.
TARGET_COMPOSITE_AND_CLAIM = 2;
}

// A Status Condition to be applied to the Composite Resource and sometimes the
// Claim. For detailed information on proper usage of Conditions, please see
// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties.
message Condition {
// Type of condition in CamelCase or in foo.example.com/CamelCase.
string type = 1;

// Status of the condition.
Status status = 2;

// Reason contains a programmatic identifier indicating the reason for the
// condition's last transition. Producers of specific condition types may
// define expected values and meanings for this field, and whether the values
// are considered a guaranteed API. The value should be a CamelCase string.
// This field may not be empty.
string reason = 3;

// Message is a human readable message indicating details about the
// transition. This may be an empty string.
optional string message = 4;

// The resources this condition targets.
optional Target target = 5;
}

enum Status {
STATUS_CONDITION_UNSPECIFIED = 0;

STATUS_CONDITION_UNKNOWN = 1;

STATUS_CONDITION_TRUE = 2;

STATUS_CONDITION_FALSE = 3;
}
77 changes: 77 additions & 0 deletions response/condition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
Copyright 2024 The Crossplane 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 response

import (
"github.com/crossplane/function-sdk-go/proto/v1beta1"
)

// ConditionOption allows further customization of the condition.
type ConditionOption struct {
condition *v1beta1.Condition
}

// ConditionTrue will create a condition with the status of true and add the
// condition to the supplied RunFunctionResponse.
func ConditionTrue(rsp *v1beta1.RunFunctionResponse, typ, reason string) *ConditionOption {
return newCondition(rsp, typ, reason, v1beta1.Status_STATUS_CONDITION_TRUE)
}

// ConditionFalse will create a condition with the status of false and add the
// condition to the supplied RunFunctionResponse.
func ConditionFalse(rsp *v1beta1.RunFunctionResponse, typ, reason string) *ConditionOption {
return newCondition(rsp, typ, reason, v1beta1.Status_STATUS_CONDITION_FALSE)
}

// ConditionUnknown will create a condition with the status of unknown and add
// the condition to the supplied RunFunctionResponse.
func ConditionUnknown(rsp *v1beta1.RunFunctionResponse, typ, reason string) *ConditionOption {
return newCondition(rsp, typ, reason, v1beta1.Status_STATUS_CONDITION_UNKNOWN)
}

func newCondition(rsp *v1beta1.RunFunctionResponse, typ, reason string, s v1beta1.Status) *ConditionOption {
if rsp.GetConditions() == nil {
rsp.Conditions = make([]*v1beta1.Condition, 0, 1)
}
c := &v1beta1.Condition{
Type: typ,
Status: s,
Reason: reason,
Target: v1beta1.Target_TARGET_COMPOSITE.Enum(),
}
rsp.Conditions = append(rsp.GetConditions(), c)
return &ConditionOption{condition: c}
}

// TargetComposite updates the condition to target the composite resource.
func (c *ConditionOption) TargetComposite() *ConditionOption {
c.condition.Target = v1beta1.Target_TARGET_COMPOSITE.Enum()
return c
}

// TargetCompositeAndClaim updates the condition to target both the composite
// resource and claim.
func (c *ConditionOption) TargetCompositeAndClaim() *ConditionOption {
c.condition.Target = v1beta1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum()
return c
}

// WithMessage adds the message to the condition.
func (c *ConditionOption) WithMessage(message string) *ConditionOption {
c.condition.Message = &message
return c
}
193 changes: 193 additions & 0 deletions response/condition_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
Copyright 2024 The Crossplane 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 response_test

import (
"testing"

"github.com/google/go-cmp/cmp"
"google.golang.org/protobuf/testing/protocmp"
"k8s.io/utils/ptr"

"github.com/crossplane/function-sdk-go/proto/v1beta1"
"github.com/crossplane/function-sdk-go/response"
)

// Condition types.
const (
typeDatabaseReady = "DatabaseReady"
)

// Condition reasons.
const (
reasonAvailable = "ReasonAvailable"
reasonCreating = "ReasonCreating"
reasonPriorFailure = "ReasonPriorFailure"
reasonUnauthorized = "ReasonUnauthorized"
)

func TestCondition(t *testing.T) {
type testFn func(*v1beta1.RunFunctionResponse)
type args struct {
fns []testFn
}
type want struct {
conditions []*v1beta1.Condition
}
cases := map[string]struct {
reason string
args args
want want
}{
"CreateBasicRecords": {
reason: "Correctly adds conditions to the response.",
args: args{
fns: []testFn{
func(rsp *v1beta1.RunFunctionResponse) {
response.ConditionTrue(rsp, typeDatabaseReady, reasonAvailable)
},
func(rsp *v1beta1.RunFunctionResponse) {
response.ConditionFalse(rsp, typeDatabaseReady, reasonCreating)
},
func(rsp *v1beta1.RunFunctionResponse) {
response.ConditionUnknown(rsp, typeDatabaseReady, reasonPriorFailure)
},
},
},
want: want{
conditions: []*v1beta1.Condition{
{
Type: typeDatabaseReady,
Status: v1beta1.Status_STATUS_CONDITION_TRUE,
Reason: reasonAvailable,
Target: v1beta1.Target_TARGET_COMPOSITE.Enum(),
},
{
Type: typeDatabaseReady,
Status: v1beta1.Status_STATUS_CONDITION_FALSE,
Reason: reasonCreating,
Target: v1beta1.Target_TARGET_COMPOSITE.Enum(),
},
{
Type: typeDatabaseReady,
Status: v1beta1.Status_STATUS_CONDITION_UNKNOWN,
Reason: reasonPriorFailure,
Target: v1beta1.Target_TARGET_COMPOSITE.Enum(),
},
},
},
},
"SetTargets": {
reason: "Correctly sets targets on condition and adds it to the response.",
args: args{
fns: []testFn{
func(rsp *v1beta1.RunFunctionResponse) {
response.ConditionTrue(rsp, typeDatabaseReady, reasonAvailable).TargetComposite()
},
func(rsp *v1beta1.RunFunctionResponse) {
response.ConditionTrue(rsp, typeDatabaseReady, reasonAvailable).TargetCompositeAndClaim()
},
},
},
want: want{
conditions: []*v1beta1.Condition{
{
Type: typeDatabaseReady,
Status: v1beta1.Status_STATUS_CONDITION_TRUE,
Reason: reasonAvailable,
Target: v1beta1.Target_TARGET_COMPOSITE.Enum(),
},
{
Type: typeDatabaseReady,
Status: v1beta1.Status_STATUS_CONDITION_TRUE,
Reason: reasonAvailable,
Target: v1beta1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum(),
},
},
},
},
"SetMessage": {
reason: "Correctly sets message on condition and adds it to the response.",
args: args{
fns: []testFn{
func(rsp *v1beta1.RunFunctionResponse) {
response.ConditionTrue(rsp, typeDatabaseReady, reasonAvailable).WithMessage("a test message")
},
},
},
want: want{
conditions: []*v1beta1.Condition{
{
Type: typeDatabaseReady,
Status: v1beta1.Status_STATUS_CONDITION_TRUE,
Reason: reasonAvailable,
Target: v1beta1.Target_TARGET_COMPOSITE.Enum(),
Message: ptr.To("a test message"),
},
},
},
},
"ChainOptions": {
reason: "Can chain condition options together.",
args: args{
fns: []testFn{
func(rsp *v1beta1.RunFunctionResponse) {
response.ConditionTrue(rsp, typeDatabaseReady, reasonAvailable).
WithMessage("a test message").
TargetCompositeAndClaim()
},
func(rsp *v1beta1.RunFunctionResponse) {
response.ConditionTrue(rsp, typeDatabaseReady, reasonAvailable).
TargetCompositeAndClaim().
WithMessage("a test message")
},
},
},
want: want{
conditions: []*v1beta1.Condition{
{
Type: typeDatabaseReady,
Status: v1beta1.Status_STATUS_CONDITION_TRUE,
Reason: reasonAvailable,
Target: v1beta1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum(),
Message: ptr.To("a test message"),
},
{
Type: typeDatabaseReady,
Status: v1beta1.Status_STATUS_CONDITION_TRUE,
Reason: reasonAvailable,
Target: v1beta1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum(),
Message: ptr.To("a test message"),
},
},
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
rsp := &v1beta1.RunFunctionResponse{}
for _, f := range tc.args.fns {
f(rsp)
}

if diff := cmp.Diff(tc.want.conditions, rsp.GetConditions(), protocmp.Transform()); diff != "" {
t.Errorf("\n%s\nFrom(...): -want, +got:\n%s", tc.reason, diff)
}

})
}
}
Loading
Loading