Skip to content

Commit b1dae61

Browse files
committed
feat(cli): include status conditions in output of render command
Signed-off-by: Jared Watts <[email protected]>
1 parent 5ba0282 commit b1dae61

File tree

3 files changed

+87
-10
lines changed

3 files changed

+87
-10
lines changed

cmd/crank/render/cmd.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,15 @@ type Cmd struct {
4444
Functions string `arg:"" help:"A YAML file or directory of YAML files specifying the Composition Functions to use to render the XR." type:"path"`
4545

4646
// Flags. Keep them in alphabetical order.
47-
ContextFiles map[string]string `help:"Comma-separated context key-value pairs to pass to the Function pipeline. Values must be files containing JSON." mapsep:""`
48-
ContextValues map[string]string `help:"Comma-separated context key-value pairs to pass to the Function pipeline. Values must be JSON. Keys take precedence over --context-files." mapsep:""`
49-
IncludeFunctionResults bool `help:"Include informational and warning messages from Functions in the rendered output as resources of kind: Result." short:"r"`
50-
IncludeFullXR bool `help:"Include a direct copy of the input XR's spec and metadata fields in the rendered output." short:"x"`
51-
ObservedResources string `help:"A YAML file or directory of YAML files specifying the observed state of composed resources." placeholder:"PATH" short:"o" type:"path"`
52-
ExtraResources string `help:"A YAML file or directory of YAML files specifying extra resources to pass to the Function pipeline." placeholder:"PATH" short:"e" type:"path"`
53-
IncludeContext bool `help:"Include the context in the rendered output as a resource of kind: Context." short:"c"`
54-
FunctionCredentials string `help:"A YAML file or directory of YAML files specifying credentials to use for Functions to render the XR." placeholder:"PATH" type:"path"`
47+
ContextFiles map[string]string `help:"Comma-separated context key-value pairs to pass to the Function pipeline. Values must be files containing JSON." mapsep:""`
48+
ContextValues map[string]string `help:"Comma-separated context key-value pairs to pass to the Function pipeline. Values must be JSON. Keys take precedence over --context-files." mapsep:""`
49+
IncludeFunctionResults bool `help:"Include informational and warning messages from Functions in the rendered output as resources of kind: Result." short:"r"`
50+
IncludeFullXR bool `help:"Include a direct copy of the input XR's spec and metadata fields in the rendered output." short:"x"`
51+
IncludeStatusConditions bool `help:"Include the status conditions in the rendered output as a resource of kind: Condition." short:"s"`
52+
ObservedResources string `help:"A YAML file or directory of YAML files specifying the observed state of composed resources." placeholder:"PATH" short:"o" type:"path"`
53+
ExtraResources string `help:"A YAML file or directory of YAML files specifying extra resources to pass to the Function pipeline." placeholder:"PATH" short:"e" type:"path"`
54+
IncludeContext bool `help:"Include the context in the rendered output as a resource of kind: Context." short:"c"`
55+
FunctionCredentials string `help:"A YAML file or directory of YAML files specifying credentials to use for Functions to render the XR." placeholder:"PATH" type:"path"`
5556

5657
Timeout time.Duration `default:"1m" help:"How long to run before timing out."`
5758

@@ -270,6 +271,15 @@ func (c *Cmd) Run(k *kong.Context, log logging.Logger) error { //nolint:gocognit
270271
}
271272
}
272273

274+
if c.IncludeStatusConditions {
275+
for i := range out.Conditions {
276+
_, _ = fmt.Fprintln(k.Stdout, "---")
277+
if err := s.Encode(&out.Conditions[i], os.Stdout); err != nil {
278+
return errors.Wrap(err, "cannot marshal condition to YAML")
279+
}
280+
}
281+
}
282+
273283
if c.IncludeContext {
274284
_, _ = fmt.Fprintln(k.Stdout, "---")
275285
if err := s.Encode(out.Context, k.Stdout); err != nil {

cmd/crank/render/render.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ type Outputs struct {
8585
ComposedResources []composed.Unstructured
8686
Results []unstructured.Unstructured
8787
Context *unstructured.Unstructured
88+
Conditions []unstructured.Unstructured
8889

8990
// TODO(negz): Allow returning desired XR connection details. Maybe as a
9091
// Secret? Should we honor writeConnectionSecretToRef? What if secret stores
@@ -99,6 +100,15 @@ type RuntimeFunctionRunner struct {
99100
mx sync.Mutex
100101
}
101102

103+
// A CompositionTarget is the target of a composition event or condition.
104+
type CompositionTarget string
105+
106+
// Composition event and condition targets.
107+
const (
108+
CompositionTargetComposite CompositionTarget = "Composite"
109+
CompositionTargetCompositeAndClaim CompositionTarget = "CompositeAndClaim"
110+
)
111+
102112
// NewRuntimeFunctionRunner returns a FunctionRunner that runs functions
103113
// locally, using the runtime configured in their annotations (e.g. Docker). It
104114
// starts all the functions and creates gRPC connections when called.
@@ -207,6 +217,7 @@ func Render(ctx context.Context, log logging.Logger, in Inputs) (Outputs, error)
207217
d := &fnv1.State{}
208218

209219
results := make([]unstructured.Unstructured, 0)
220+
conditions := make([]unstructured.Unstructured, 0)
210221

211222
// The Function context starts empty.
212223
fctx := &structpb.Struct{Fields: map[string]*structpb.Value{}}
@@ -271,6 +282,29 @@ func Render(ctx context.Context, log logging.Logger, in Inputs) (Outputs, error)
271282
// We intentionally discard/ignore this after the last Function runs.
272283
fctx = rsp.GetContext()
273284

285+
for _, c := range rsp.GetConditions() {
286+
var status corev1.ConditionStatus
287+
switch c.GetStatus() {
288+
case fnv1.Status_STATUS_CONDITION_TRUE:
289+
status = corev1.ConditionTrue
290+
case fnv1.Status_STATUS_CONDITION_FALSE:
291+
status = corev1.ConditionFalse
292+
case fnv1.Status_STATUS_CONDITION_UNKNOWN, fnv1.Status_STATUS_CONDITION_UNSPECIFIED:
293+
status = corev1.ConditionUnknown
294+
}
295+
296+
conditions = append(conditions, unstructured.Unstructured{Object: map[string]any{
297+
"apiVersion": "render.crossplane.io/v1beta1",
298+
"kind": "Condition",
299+
"type": c.GetType(),
300+
"status": status,
301+
"lastTransitionTime": metav1.Now(),
302+
"reason": xpv1.ConditionReason(c.GetReason()),
303+
"message": c.GetMessage(),
304+
"target": convertTarget(c.GetTarget()),
305+
}})
306+
}
307+
274308
// Results of fatal severity stop the Composition process.
275309
for _, rs := range rsp.GetResults() {
276310
switch rs.GetSeverity() { //nolint:exhaustive // We intentionally have a broad default case.
@@ -340,7 +374,7 @@ func Render(ctx context.Context, log logging.Logger, in Inputs) (Outputs, error)
340374
xrCond.LastTransitionTime = metav1.NewTime(time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC))
341375
xr.SetConditions(xrCond)
342376

343-
out := Outputs{CompositeResource: xr, ComposedResources: desired, Results: results}
377+
out := Outputs{CompositeResource: xr, ComposedResources: desired, Results: results, Conditions: conditions}
344378
if fctx != nil {
345379
out.Context = &unstructured.Unstructured{Object: map[string]any{
346380
"apiVersion": "render.crossplane.io/v1beta1",
@@ -414,3 +448,10 @@ func (f *FilteringFetcher) Fetch(_ context.Context, rs *fnv1.ResourceSelector) (
414448

415449
return out, nil
416450
}
451+
452+
func convertTarget(t fnv1.Target) CompositionTarget {
453+
if t == fnv1.Target_TARGET_COMPOSITE_AND_CLAIM {
454+
return CompositionTargetCompositeAndClaim
455+
}
456+
return CompositionTargetComposite
457+
}

cmd/crank/render/render_test.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"io"
2222
"net"
2323
"testing"
24+
"time"
2425

2526
"github.com/google/go-cmp/cmp"
2627
"github.com/google/go-cmp/cmp/cmpopts"
@@ -32,7 +33,9 @@ import (
3233
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3334
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3435
"k8s.io/apimachinery/pkg/runtime"
36+
"k8s.io/utils/ptr"
3537

38+
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
3639
"github.com/crossplane/crossplane-runtime/pkg/logging"
3740
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composed"
3841
ucomposite "github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composite"
@@ -249,6 +252,15 @@ func TestRender(t *testing.T) {
249252
},
250253
},
251254
},
255+
Conditions: []*fnv1.Condition{
256+
{
257+
Type: "ProvisioningSuccess",
258+
Status: fnv1.Status_STATUS_CONDITION_TRUE,
259+
Reason: "Provisioned",
260+
Message: ptr.To("Provisioned successfully"),
261+
Target: fnv1.Target_TARGET_COMPOSITE_AND_CLAIM.Enum(),
262+
},
263+
},
252264
})
253265
listeners = append(listeners, lis)
254266

@@ -349,6 +361,20 @@ func TestRender(t *testing.T) {
349361
},
350362
},
351363
},
364+
Conditions: []unstructured.Unstructured{
365+
{
366+
Object: map[string]any{
367+
"apiVersion": "render.crossplane.io/v1beta1",
368+
"kind": "Condition",
369+
"type": "ProvisioningSuccess",
370+
"status": corev1.ConditionTrue,
371+
"reason": xpv1.ConditionReason("Provisioned"),
372+
"message": "Provisioned successfully",
373+
"target": CompositionTargetCompositeAndClaim,
374+
"lastTransitionTime": metav1.Now(),
375+
},
376+
},
377+
},
352378
},
353379
},
354380
},
@@ -745,7 +771,7 @@ func TestRender(t *testing.T) {
745771
t.Run(name, func(t *testing.T) {
746772
out, err := Render(tc.args.ctx, logging.NewNopLogger(), tc.args.in)
747773

748-
if diff := cmp.Diff(tc.want.out, out, cmpopts.EquateEmpty()); diff != "" {
774+
if diff := cmp.Diff(tc.want.out, out, cmpopts.EquateEmpty(), cmpopts.EquateApproxTime(time.Second)); diff != "" {
749775
t.Errorf("%s\nRender(...): -want, +got:\n%s", tc.reason, diff)
750776
}
751777
if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" {

0 commit comments

Comments
 (0)