Skip to content

Commit bc16c87

Browse files
authored
Merge pull request #129 from dalton-hill-0/claim-events
Composition Function Events and Status Conditions
2 parents 54fac4d + 9de5311 commit bc16c87

File tree

8 files changed

+1085
-213
lines changed

8 files changed

+1085
-213
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ NPROCS ?= 1
2424
GO_TEST_PARALLEL := $(shell echo $$(( $(NPROCS) / 2 )))
2525

2626
GO_LDFLAGS += -X $(GO_PROJECT)/pkg/version.Version=$(VERSION)
27-
GO_SUBDIRS += proto
27+
GO_SUBDIRS += errors proto resource response request
2828
GO111MODULE = on
2929
GOLANGCILINT_VERSION = 1.55.2
3030
GO_LINT_ARGS ?= "--fix"

proto/v1beta1/run_function.pb.go

Lines changed: 469 additions & 173 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

proto/v1beta1/run_function.proto

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ message RunFunctionResponse {
124124

125125
// Requirements that must be satisfied for this Function to run successfully.
126126
Requirements requirements = 5;
127+
128+
// Status Conditions to be applied to the Composite Resource and sometimes the
129+
// Claim.
130+
repeated Condition conditions = 6;
127131
}
128132

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

143147
// ResourceSelector selects a group of resources, either by name or by label.
144148
message ResourceSelector {
149+
// API version of resources to select.
145150
string api_version = 1;
151+
152+
// Kind of resources to select.
146153
string kind = 2;
147154

155+
// Resources to match.
148156
oneof match {
157+
// Match the resource with this name.
149158
string match_name = 3;
159+
160+
// Match all resources with these labels.
150161
MatchLabels match_labels = 4;
151162
}
152163
}
@@ -239,6 +250,12 @@ message Result {
239250

240251
// Human-readable details about the result.
241252
string message = 2;
253+
254+
// Optional PascalCase, machine-readable reason for this result.
255+
optional string reason = 3;
256+
257+
// The resources this result targets.
258+
optional Target target = 4;
242259
}
243260

244261
// Severity of Function results.
@@ -259,3 +276,52 @@ enum Severity {
259276
// with the composite resource.
260277
SEVERITY_NORMAL = 3;
261278
}
279+
280+
// Target of Function results.
281+
enum Target {
282+
// If the target is unspecified, the result targets the composite resource.
283+
TARGET_UNSPECIFIED = 0;
284+
285+
// Target the composite resource. Results that target the composite resource
286+
// should include detailed, advanced information.
287+
TARGET_COMPOSITE = 1;
288+
289+
// Target the composite and the claim. Results that target the composite and
290+
// the claim should include only end-user friendly information.
291+
TARGET_COMPOSITE_AND_CLAIM = 2;
292+
}
293+
294+
// A Status Condition to be applied to the Composite Resource and sometimes the
295+
// Claim. For detailed information on proper usage of Conditions, please see
296+
// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties.
297+
message Condition {
298+
// Type of condition in CamelCase or in foo.example.com/CamelCase.
299+
string type = 1;
300+
301+
// Status of the condition.
302+
Status status = 2;
303+
304+
// Reason contains a programmatic identifier indicating the reason for the
305+
// condition's last transition. Producers of specific condition types may
306+
// define expected values and meanings for this field, and whether the values
307+
// are considered a guaranteed API. The value should be a CamelCase string.
308+
// This field may not be empty.
309+
string reason = 3;
310+
311+
// Message is a human readable message indicating details about the
312+
// transition. This may be an empty string.
313+
optional string message = 4;
314+
315+
// The resources this condition targets.
316+
optional Target target = 5;
317+
}
318+
319+
enum Status {
320+
STATUS_CONDITION_UNSPECIFIED = 0;
321+
322+
STATUS_CONDITION_UNKNOWN = 1;
323+
324+
STATUS_CONDITION_TRUE = 2;
325+
326+
STATUS_CONDITION_FALSE = 3;
327+
}

response/condition.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
Copyright 2024 The Crossplane Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package response
18+
19+
import (
20+
"github.com/crossplane/function-sdk-go/proto/v1beta1"
21+
)
22+
23+
// ConditionOption allows further customization of the condition.
24+
type ConditionOption struct {
25+
condition *v1beta1.Condition
26+
}
27+
28+
// ConditionTrue will create a condition with the status of true and add the
29+
// condition to the supplied RunFunctionResponse.
30+
func ConditionTrue(rsp *v1beta1.RunFunctionResponse, typ, reason string) *ConditionOption {
31+
return newCondition(rsp, typ, reason, v1beta1.Status_STATUS_CONDITION_TRUE)
32+
}
33+
34+
// ConditionFalse will create a condition with the status of false and add the
35+
// condition to the supplied RunFunctionResponse.
36+
func ConditionFalse(rsp *v1beta1.RunFunctionResponse, typ, reason string) *ConditionOption {
37+
return newCondition(rsp, typ, reason, v1beta1.Status_STATUS_CONDITION_FALSE)
38+
}
39+
40+
// ConditionUnknown will create a condition with the status of unknown and add
41+
// the condition to the supplied RunFunctionResponse.
42+
func ConditionUnknown(rsp *v1beta1.RunFunctionResponse, typ, reason string) *ConditionOption {
43+
return newCondition(rsp, typ, reason, v1beta1.Status_STATUS_CONDITION_UNKNOWN)
44+
}
45+
46+
func newCondition(rsp *v1beta1.RunFunctionResponse, typ, reason string, s v1beta1.Status) *ConditionOption {
47+
if rsp.GetConditions() == nil {
48+
rsp.Conditions = make([]*v1beta1.Condition, 0, 1)
49+
}
50+
c := &v1beta1.Condition{
51+
Type: typ,
52+
Status: s,
53+
Reason: reason,
54+
Target: v1beta1.Target_TARGET_COMPOSITE.Enum(),
55+
}
56+
rsp.Conditions = append(rsp.GetConditions(), c)
57+
return &ConditionOption{condition: c}
58+
}
59+
60+
// TargetComposite updates the condition to target the composite resource.
61+
func (c *ConditionOption) TargetComposite() *ConditionOption {
62+
c.condition.Target = v1beta1.Target_TARGET_COMPOSITE.Enum()
63+
return c
64+
}
65+
66+
// TargetCompositeAndClaim updates the condition to target both the composite
67+
// resource and claim.
68+
func (c *ConditionOption) TargetCompositeAndClaim() *ConditionOption {
69+
c.condition.Target = v1beta1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum()
70+
return c
71+
}
72+
73+
// WithMessage adds the message to the condition.
74+
func (c *ConditionOption) WithMessage(message string) *ConditionOption {
75+
c.condition.Message = &message
76+
return c
77+
}

response/condition_test.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
/*
2+
Copyright 2024 The Crossplane Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package response_test
18+
19+
import (
20+
"testing"
21+
22+
"github.com/google/go-cmp/cmp"
23+
"google.golang.org/protobuf/testing/protocmp"
24+
"k8s.io/utils/ptr"
25+
26+
"github.com/crossplane/function-sdk-go/proto/v1beta1"
27+
"github.com/crossplane/function-sdk-go/response"
28+
)
29+
30+
// Condition types.
31+
const (
32+
typeDatabaseReady = "DatabaseReady"
33+
)
34+
35+
// Condition reasons.
36+
const (
37+
reasonAvailable = "ReasonAvailable"
38+
reasonCreating = "ReasonCreating"
39+
reasonPriorFailure = "ReasonPriorFailure"
40+
reasonUnauthorized = "ReasonUnauthorized"
41+
)
42+
43+
func TestCondition(t *testing.T) {
44+
type testFn func(*v1beta1.RunFunctionResponse)
45+
type args struct {
46+
fns []testFn
47+
}
48+
type want struct {
49+
conditions []*v1beta1.Condition
50+
}
51+
cases := map[string]struct {
52+
reason string
53+
args args
54+
want want
55+
}{
56+
"CreateBasicRecords": {
57+
reason: "Correctly adds conditions to the response.",
58+
args: args{
59+
fns: []testFn{
60+
func(rsp *v1beta1.RunFunctionResponse) {
61+
response.ConditionTrue(rsp, typeDatabaseReady, reasonAvailable)
62+
},
63+
func(rsp *v1beta1.RunFunctionResponse) {
64+
response.ConditionFalse(rsp, typeDatabaseReady, reasonCreating)
65+
},
66+
func(rsp *v1beta1.RunFunctionResponse) {
67+
response.ConditionUnknown(rsp, typeDatabaseReady, reasonPriorFailure)
68+
},
69+
},
70+
},
71+
want: want{
72+
conditions: []*v1beta1.Condition{
73+
{
74+
Type: typeDatabaseReady,
75+
Status: v1beta1.Status_STATUS_CONDITION_TRUE,
76+
Reason: reasonAvailable,
77+
Target: v1beta1.Target_TARGET_COMPOSITE.Enum(),
78+
},
79+
{
80+
Type: typeDatabaseReady,
81+
Status: v1beta1.Status_STATUS_CONDITION_FALSE,
82+
Reason: reasonCreating,
83+
Target: v1beta1.Target_TARGET_COMPOSITE.Enum(),
84+
},
85+
{
86+
Type: typeDatabaseReady,
87+
Status: v1beta1.Status_STATUS_CONDITION_UNKNOWN,
88+
Reason: reasonPriorFailure,
89+
Target: v1beta1.Target_TARGET_COMPOSITE.Enum(),
90+
},
91+
},
92+
},
93+
},
94+
"SetTargets": {
95+
reason: "Correctly sets targets on condition and adds it to the response.",
96+
args: args{
97+
fns: []testFn{
98+
func(rsp *v1beta1.RunFunctionResponse) {
99+
response.ConditionTrue(rsp, typeDatabaseReady, reasonAvailable).TargetComposite()
100+
},
101+
func(rsp *v1beta1.RunFunctionResponse) {
102+
response.ConditionTrue(rsp, typeDatabaseReady, reasonAvailable).TargetCompositeAndClaim()
103+
},
104+
},
105+
},
106+
want: want{
107+
conditions: []*v1beta1.Condition{
108+
{
109+
Type: typeDatabaseReady,
110+
Status: v1beta1.Status_STATUS_CONDITION_TRUE,
111+
Reason: reasonAvailable,
112+
Target: v1beta1.Target_TARGET_COMPOSITE.Enum(),
113+
},
114+
{
115+
Type: typeDatabaseReady,
116+
Status: v1beta1.Status_STATUS_CONDITION_TRUE,
117+
Reason: reasonAvailable,
118+
Target: v1beta1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum(),
119+
},
120+
},
121+
},
122+
},
123+
"SetMessage": {
124+
reason: "Correctly sets message on condition and adds it to the response.",
125+
args: args{
126+
fns: []testFn{
127+
func(rsp *v1beta1.RunFunctionResponse) {
128+
response.ConditionTrue(rsp, typeDatabaseReady, reasonAvailable).WithMessage("a test message")
129+
},
130+
},
131+
},
132+
want: want{
133+
conditions: []*v1beta1.Condition{
134+
{
135+
Type: typeDatabaseReady,
136+
Status: v1beta1.Status_STATUS_CONDITION_TRUE,
137+
Reason: reasonAvailable,
138+
Target: v1beta1.Target_TARGET_COMPOSITE.Enum(),
139+
Message: ptr.To("a test message"),
140+
},
141+
},
142+
},
143+
},
144+
"ChainOptions": {
145+
reason: "Can chain condition options together.",
146+
args: args{
147+
fns: []testFn{
148+
func(rsp *v1beta1.RunFunctionResponse) {
149+
response.ConditionTrue(rsp, typeDatabaseReady, reasonAvailable).
150+
WithMessage("a test message").
151+
TargetCompositeAndClaim()
152+
},
153+
func(rsp *v1beta1.RunFunctionResponse) {
154+
response.ConditionTrue(rsp, typeDatabaseReady, reasonAvailable).
155+
TargetCompositeAndClaim().
156+
WithMessage("a test message")
157+
},
158+
},
159+
},
160+
want: want{
161+
conditions: []*v1beta1.Condition{
162+
{
163+
Type: typeDatabaseReady,
164+
Status: v1beta1.Status_STATUS_CONDITION_TRUE,
165+
Reason: reasonAvailable,
166+
Target: v1beta1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum(),
167+
Message: ptr.To("a test message"),
168+
},
169+
{
170+
Type: typeDatabaseReady,
171+
Status: v1beta1.Status_STATUS_CONDITION_TRUE,
172+
Reason: reasonAvailable,
173+
Target: v1beta1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum(),
174+
Message: ptr.To("a test message"),
175+
},
176+
},
177+
},
178+
},
179+
}
180+
for name, tc := range cases {
181+
t.Run(name, func(t *testing.T) {
182+
rsp := &v1beta1.RunFunctionResponse{}
183+
for _, f := range tc.args.fns {
184+
f(rsp)
185+
}
186+
187+
if diff := cmp.Diff(tc.want.conditions, rsp.GetConditions(), protocmp.Transform()); diff != "" {
188+
t.Errorf("\n%s\nFrom(...): -want, +got:\n%s", tc.reason, diff)
189+
}
190+
191+
})
192+
}
193+
}

0 commit comments

Comments
 (0)