refactor: use existing AsStruct/AsStateFunctions and some tests
Signed-off-by: Philippe Scorsolini <p.scorsolini@gmail.com>
This commit is contained in:
parent
ed421931a4
commit
6322178f03
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
Loading…
Reference in New Issue