Skip to content

Commit 4addd41

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

File tree

3 files changed

+50
-72
lines changed

3 files changed

+50
-72
lines changed

cmd/crank/render/cmd.go

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,14 @@ 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-
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"`
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"`
5655

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

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

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-
283273
if c.IncludeContext {
284274
_, _ = fmt.Fprintln(k.Stdout, "---")
285275
if err := s.Encode(out.Context, k.Stdout); err != nil {

cmd/crank/render/render.go

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

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

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-
112102
// NewRuntimeFunctionRunner returns a FunctionRunner that runs functions
113103
// locally, using the runtime configured in their annotations (e.g. Docker). It
114104
// starts all the functions and creates gRPC connections when called.
@@ -217,7 +207,7 @@ func Render(ctx context.Context, log logging.Logger, in Inputs) (Outputs, error)
217207
d := &fnv1.State{}
218208

219209
results := make([]unstructured.Unstructured, 0)
220-
conditions := make([]unstructured.Unstructured, 0)
210+
conditions := make([]xpv1.Condition, 0)
221211

222212
// The Function context starts empty.
223213
fctx := &structpb.Struct{Fields: map[string]*structpb.Value{}}
@@ -293,16 +283,13 @@ func Render(ctx context.Context, log logging.Logger, in Inputs) (Outputs, error)
293283
status = corev1.ConditionUnknown
294284
}
295285

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-
}})
286+
conditions = append(conditions, xpv1.Condition{
287+
Type: xpv1.ConditionType(c.GetType()),
288+
Status: status,
289+
LastTransitionTime: conditionTime(),
290+
Reason: xpv1.ConditionReason(c.GetReason()),
291+
Message: c.GetMessage(),
292+
})
306293
}
307294

308295
// Results of fatal severity stop the Composition process.
@@ -369,12 +356,21 @@ func Render(ctx context.Context, log logging.Logger, in Inputs) (Outputs, error)
369356
if len(unready) > 0 {
370357
xrCond = xpv1.Creating().WithMessage(fmt.Sprintf("Unready resources: %s", resource.StableNAndSomeMore(resource.DefaultFirstN, unready)))
371358
}
372-
// lastTransitionTime would just be noise, but we can't drop it as it's a
373-
// required field and null is not allowed, so we set a random time.
374-
xrCond.LastTransitionTime = metav1.NewTime(time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC))
359+
xrCond.LastTransitionTime = conditionTime()
375360
xr.SetConditions(xrCond)
376361

377-
out := Outputs{CompositeResource: xr, ComposedResources: desired, Results: results, Conditions: conditions}
362+
for _, c := range conditions {
363+
if xpv1.IsSystemConditionType(c.Type) {
364+
// Do not let users update system conditions.
365+
continue
366+
}
367+
// update the XR with the current condition. If it targets the claim, we
368+
// could also set it on the claim here, but we don't support Claims in
369+
// render yet.
370+
xr.SetConditions(c)
371+
}
372+
373+
out := Outputs{CompositeResource: xr, ComposedResources: desired, Results: results}
378374
if fctx != nil {
379375
out.Context = &unstructured.Unstructured{Object: map[string]any{
380376
"apiVersion": "render.crossplane.io/v1beta1",
@@ -449,9 +445,8 @@ func (f *FilteringFetcher) Fetch(_ context.Context, rs *fnv1.ResourceSelector) (
449445
return out, nil
450446
}
451447

452-
func convertTarget(t fnv1.Target) CompositionTarget {
453-
if t == fnv1.Target_TARGET_COMPOSITE_AND_CLAIM {
454-
return CompositionTargetCompositeAndClaim
455-
}
456-
return CompositionTargetComposite
448+
func conditionTime() metav1.Time {
449+
// lastTransitionTime for conditions would just be noise, but we can't drop it
450+
// as it's a required field and null is not allowed, so we set a random time.
451+
return metav1.NewTime(time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC))
457452
}

cmd/crank/render/render_test.go

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

2625
"github.com/google/go-cmp/cmp"
2726
"github.com/google/go-cmp/cmp/cmpopts"
@@ -35,7 +34,6 @@ import (
3534
"k8s.io/apimachinery/pkg/runtime"
3635
"k8s.io/utils/ptr"
3736

38-
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
3937
"github.com/crossplane/crossplane-runtime/pkg/logging"
4038
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composed"
4139
ucomposite "github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composite"
@@ -292,13 +290,22 @@ func TestRender(t *testing.T) {
292290
},
293291
"status": {
294292
"widgets": 9001,
295-
"conditions": [{
296-
"lastTransitionTime": "2024-01-01T00:00:00Z",
297-
"type": "Ready",
298-
"status": "False",
299-
"reason": "Creating",
300-
"message": "Unready resources: a-cool-resource, b-cool-resource"
301-
}]
293+
"conditions": [
294+
{
295+
"lastTransitionTime": "2024-01-01T00:00:00Z",
296+
"type": "Ready",
297+
"status": "False",
298+
"reason": "Creating",
299+
"message": "Unready resources: a-cool-resource, b-cool-resource"
300+
},
301+
{
302+
"lastTransitionTime": "2024-01-01T00:00:00Z",
303+
"type": "ProvisioningSuccess",
304+
"status": "True",
305+
"reason": "Provisioned",
306+
"message": "Provisioned successfully"
307+
}
308+
]
302309
}
303310
}`),
304311
},
@@ -361,20 +368,6 @@ func TestRender(t *testing.T) {
361368
},
362369
},
363370
},
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-
},
378371
},
379372
},
380373
},
@@ -771,7 +764,7 @@ func TestRender(t *testing.T) {
771764
t.Run(name, func(t *testing.T) {
772765
out, err := Render(tc.args.ctx, logging.NewNopLogger(), tc.args.in)
773766

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

0 commit comments

Comments
 (0)