refactor: use existing AsStruct/AsStateFunctions and some tests

Signed-off-by: Philippe Scorsolini <p.scorsolini@gmail.com>
This commit is contained in:
Philippe Scorsolini 2023-10-12 20:11:44 +02:00
parent ed421931a4
commit 6322178f03
4 changed files with 165 additions and 108 deletions

View File

@ -12,11 +12,12 @@ import (
"github.com/crossplane/crossplane-runtime/pkg/meta"
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composed"
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composite"
ucomposite "github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composite"
fnv1beta1 "github.com/crossplane/crossplane/apis/apiextensions/fn/proto/v1beta1"
apiextensionsv1 "github.com/crossplane/crossplane/apis/apiextensions/v1"
pkgv1beta1 "github.com/crossplane/crossplane/apis/pkg/v1beta1"
"github.com/crossplane/crossplane/internal/controller/apiextensions/composite"
)
// Wait for the server to be ready before sending RPCs. Notably this gives
@ -39,7 +40,7 @@ const (
// Inputs contains all inputs to the render process.
type Inputs struct {
CompositeResource *composite.Unstructured
CompositeResource *ucomposite.Unstructured
Composition *apiextensionsv1.Composition
Functions []pkgv1beta1.Function
ObservedResources []composed.Unstructured
@ -50,7 +51,7 @@ type Inputs struct {
// Outputs contains all outputs from the render process.
type Outputs struct {
CompositeResource *composite.Unstructured
CompositeResource *ucomposite.Unstructured
ComposedResources []composed.Unstructured
Results []unstructured.Unstructured
@ -88,15 +89,19 @@ func Render(ctx context.Context, in Inputs) (Outputs, error) { //nolint:gocyclo
conns[fn.GetName()] = conn
}
observed := map[string]composed.Unstructured{}
for _, cd := range in.ObservedResources {
observed := composite.ComposedResourceStates{}
for i, cd := range in.ObservedResources {
name := cd.GetAnnotations()[AnnotationKeyCompositionResourceName]
observed[name] = cd
observed[composite.ResourceName(name)] = composite.ComposedResourceState{
Resource: &in.ObservedResources[i],
ConnectionDetails: nil, // We don't support passing in observed connection details.
Ready: false,
}
}
// TODO(negz): Support passing in optional observed connection details for
// both the XR and composed resources.
o, err := AsState(in.CompositeResource, observed)
o, err := composite.AsState(in.CompositeResource, nil, observed)
if err != nil {
return Outputs{}, errors.Wrap(err, "cannot build observed composite and composed resources for RunFunctionRequest")
}
@ -152,16 +157,16 @@ func Render(ctx context.Context, in Inputs) (Outputs, error) { //nolint:gocyclo
desired := make([]composed.Unstructured, 0, len(d.GetResources()))
for name, dr := range d.GetResources() {
cd := composed.New()
if err := FromStruct(cd, dr.GetResource()); err != nil {
if err := composite.FromStruct(cd, dr.GetResource()); err != nil {
return Outputs{}, errors.Wrapf(err, "cannot unmarshal desired composed resource %q", name)
}
// If this desired resource state pertains to an existing composed
// resource we want to maintain its name and namespace.
or, ok := observed[name]
or, ok := observed[composite.ResourceName(name)]
if ok {
cd.SetNamespace(or.GetNamespace())
cd.SetName(or.GetName())
cd.SetNamespace(or.Resource.GetNamespace())
cd.SetName(or.Resource.GetName())
}
// Set standard composed resource metadata that is derived from the XR.
@ -172,8 +177,8 @@ func Render(ctx context.Context, in Inputs) (Outputs, error) { //nolint:gocyclo
desired = append(desired, *cd)
}
xr := composite.New()
if err := FromStruct(xr, d.GetComposite().GetResource()); err != nil {
xr := ucomposite.New()
if err := composite.FromStruct(xr, d.GetComposite().GetResource()); err != nil {
return Outputs{}, errors.Wrap(err, "cannot render desired composite resource")
}

View File

@ -207,10 +207,14 @@ func (r *RuntimeDocker) Start(ctx context.Context) (RuntimeContext, error) { //n
return RuntimeContext{Target: addr, Stop: stop}, nil
}
type pullClient interface {
ImagePull(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error)
}
// PullImage pulls the supplied image using the supplied client. It blocks until
// the image has either finished pulling or hit an error.
func PullImage(ctx context.Context, c *client.Client, image string) error {
out, err := c.ImagePull(ctx, image, types.ImagePullOptions{})
func PullImage(ctx context.Context, p pullClient, image string) error {
out, err := p.ImagePull(ctx, image, types.ImagePullOptions{})
if err != nil {
return err
}

View File

@ -0,0 +1,141 @@
package render
import (
"context"
"io"
"testing"
"github.com/docker/docker/api/types"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "github.com/crossplane/crossplane/apis/pkg/v1"
"github.com/crossplane/crossplane/apis/pkg/v1beta1"
)
type mockPullClient struct {
MockPullImage func(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error)
}
func (m *mockPullClient) ImagePull(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) {
return m.MockPullImage(ctx, ref, options)
}
var _ pullClient = &mockPullClient{}
func TestGetRuntimeDocker(t *testing.T) {
type args struct {
fn v1beta1.Function
}
type want struct {
rd *RuntimeDocker
err error
}
cases := map[string]struct {
reason string
args args
want want
}{
"SuccessAllSet": {
reason: "should return a RuntimeDocker with all fields set according to the supplied Function's annotations",
args: args{
fn: v1beta1.Function{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
AnnotationKeyRuntimeDockerCleanup: string(AnnotationValueRuntimeDockerCleanupOrphan),
AnnotationKeyRuntimeDockerPullPolicy: string(AnnotationValueRuntimeDockerPullPolicyAlways),
AnnotationKeyRuntimeDockerImage: "test-image-from-annotation",
},
},
Spec: v1beta1.FunctionSpec{
PackageSpec: v1.PackageSpec{
Package: "test-package",
},
},
},
},
want: want{
rd: &RuntimeDocker{
Image: "test-image-from-annotation",
Stop: false,
PullPolicy: AnnotationValueRuntimeDockerPullPolicyAlways,
},
},
},
"SuccessDefaults": {
reason: "should return a RuntimeDocker with default fields set if no annotation are set",
args: args{
fn: v1beta1.Function{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{},
},
Spec: v1beta1.FunctionSpec{
PackageSpec: v1.PackageSpec{
Package: "test-package",
},
},
},
},
want: want{
rd: &RuntimeDocker{
Image: "test-package",
Stop: true,
PullPolicy: AnnotationValueRuntimeDockerPullPolicyIfNotPresent,
},
},
},
"ErrorUnknownAnnotationValueCleanup": {
reason: "should return an error if the supplied Function has an unknown cleanup annotation value",
args: args{
fn: v1beta1.Function{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
AnnotationKeyRuntimeDockerCleanup: "wrong",
},
},
Spec: v1beta1.FunctionSpec{
PackageSpec: v1.PackageSpec{
Package: "test-package",
},
},
},
},
want: want{
err: cmpopts.AnyError,
},
},
"ErrorUnknownAnnotationPullPolicy": {
reason: "should return an error if the supplied Function has an unknown pull policy annotation value",
args: args{
fn: v1beta1.Function{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
AnnotationKeyRuntimeDockerPullPolicy: "wrong",
},
},
Spec: v1beta1.FunctionSpec{
PackageSpec: v1.PackageSpec{
Package: "test-package",
},
},
},
},
want: want{
err: cmpopts.AnyError,
},
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
rd, err := GetRuntimeDocker(tc.args.fn)
if diff := cmp.Diff(tc.want.rd, rd); diff != "" {
t.Errorf("\n%s\nGetRuntimeDocker(...): -want, +got:\n%s", tc.reason, diff)
}
if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" {
t.Errorf("\n%s\nGetRuntimeDocker(...): -want error, +got error:\n%s", tc.reason, diff)
}
})
}
}

View File

@ -1,93 +0,0 @@
package render
import (
"encoding/json"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/structpb"
kunstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/crossplane/crossplane-runtime/pkg/errors"
"github.com/crossplane/crossplane-runtime/pkg/resource"
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured"
"github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composed"
"github.com/crossplane/crossplane/apis/apiextensions/fn/proto/v1beta1"
)
// TODO(negz): We have similar functions in c/c composition_functions.go and in
// c/function-sdk-go. Perhaps everything should import from function-sdk-go?
// AsState builds state for a RunFunctionRequest from the XR and composed
// resources.
func AsState(xr resource.Composite, cds map[string]composed.Unstructured) (*v1beta1.State, error) {
r, err := AsStruct(xr)
if err != nil {
return nil, errors.Wrap(err, "cannot convert composite resource to google.proto.Struct")
}
oxr := &v1beta1.Resource{Resource: r}
ocds := make(map[string]*v1beta1.Resource)
for name, cd := range cds {
cd := cd // Pin range variable so we can take its address.
r, err := AsStruct(&cd)
if err != nil {
return nil, errors.Wrapf(err, "cannot convert composed resource %q to google.proto.Struct", name)
}
ocds[name] = &v1beta1.Resource{Resource: r}
}
return &v1beta1.State{Composite: oxr, Resources: ocds}, nil
}
// AsStruct converts the supplied object to a protocol buffer Struct well-known
// type.
func AsStruct(o runtime.Object) (*structpb.Struct, error) {
// If the supplied object is *Unstructured we don't need to round-trip.
if u, ok := o.(*kunstructured.Unstructured); ok {
s, err := structpb.NewStruct(u.Object)
return s, errors.Wrapf(err, "cannot create google.proto.Struct from %T", u)
}
// If the supplied object wraps *Unstructured we don't need to round-trip.
if w, ok := o.(unstructured.Wrapper); ok {
s, err := structpb.NewStruct(w.GetUnstructured().Object)
return s, errors.Wrapf(err, "cannot create google.proto.Struct from %T", w)
}
// Fall back to a JSON round-trip.
b, err := json.Marshal(o)
if err != nil {
return nil, errors.Wrap(err, "cannot marshal JSON")
}
s := &structpb.Struct{}
return s, errors.Wrap(s.UnmarshalJSON(b), "cannot unmarshal JSON")
}
// FromStruct populates the supplied object with content loaded from the Struct.
func FromStruct(o client.Object, s *structpb.Struct) error {
// If the supplied object is *Unstructured we don't need to round-trip.
if u, ok := o.(*kunstructured.Unstructured); ok {
u.Object = s.AsMap()
return nil
}
// If the supplied object wraps *Unstructured we don't need to round-trip.
if w, ok := o.(unstructured.Wrapper); ok {
w.GetUnstructured().Object = s.AsMap()
return nil
}
// Fall back to a JSON round-trip.
b, err := protojson.Marshal(s)
if err != nil {
return errors.Wrap(err, "cannot marshal google.proto.Struct to JSON")
}
return errors.Wrap(json.Unmarshal(b, o), "cannot unmarshal JSON")
}