Add a simple ToUnstructured method for converting our types to unstructured. (#900)

This adds a ToUnstructured method to complement the FromUnstructured method
we have had for some time.  The ToUnstructured method is effectively scoped
to Knative-style types since we expect it to implement both kmeta.Accessor
and kmeta.OwnerRefable in order to ensure that the TypeMeta is always
properly populated.
This commit is contained in:
Matt Moore 2019-11-26 10:05:21 -08:00 committed by Knative Prow Robot
parent 47b250654a
commit 9f41433241
3 changed files with 124 additions and 20 deletions

View File

@ -18,16 +18,49 @@ package duck
import (
"encoding/json"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"knative.dev/pkg/kmeta"
)
// Marshallable is implementated by the Unstructured K8s types.
type Marshalable interface {
MarshalJSON() ([]byte, error)
// OneOfOurs is the union of our Accessor interface and the OwnerRefable interface
// that is implemented by our resources that implement the kmeta.Accessor.
type OneOfOurs interface {
kmeta.Accessor
kmeta.OwnerRefable
}
// ToUnstructured takes an instance of a OneOfOurs compatible type and
// converts it to unstructured.Unstructured. We take OneOfOurs in place
// or runtime.Object because sometimes we get resources that do not have their
// TypeMeta populated but that is required for unstructured.Unstructured to
// deserialize things, so we leverage our content-agnostic GroupVersionKind()
// method to populate this as-needed (in a copy, so that we don't modify the
// informer's copy, if that is what we are passed).
func ToUnstructured(desired OneOfOurs) (*unstructured.Unstructured, error) {
// If the TypeMeta is not populated, then unmarshalling will fail, so ensure
// the TypeMeta is populated. See also EnsureTypeMeta.
if gvk := desired.GroupVersionKind(); gvk.Version == "" || gvk.Kind == "" {
gvk = desired.GetGroupVersionKind()
desired = desired.DeepCopyObject().(OneOfOurs)
desired.SetGroupVersionKind(gvk)
}
// Convert desired to unstructured.Unstructured
b, err := json.Marshal(desired)
if err != nil {
return nil, err
}
ud := &unstructured.Unstructured{}
if err := json.Unmarshal(b, ud); err != nil {
return nil, err
}
return ud, nil
}
// FromUnstructured takes unstructured object from (say from client-go/dynamic) and
// converts it into our duck types.
func FromUnstructured(obj Marshalable, target interface{}) error {
func FromUnstructured(obj json.Marshaler, target interface{}) error {
// Use the unstructured marshaller to ensure it's proper JSON
raw, err := obj.MarshalJSON()
if err != nil {

View File

@ -17,18 +17,20 @@ limitations under the License.
package duck
import (
"reflect"
"testing"
"encoding/json"
"github.com/google/go-cmp/cmp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
. "knative.dev/pkg/testing"
)
func TestFromUnstructuredFooable(t *testing.T) {
tcs := []struct {
name string
in Marshalable
in json.Marshaler
want FooStatus
wantError error
}{{
@ -70,23 +72,86 @@ func TestFromUnstructuredFooable(t *testing.T) {
want: FooStatus{},
wantError: nil,
}}
for _, tc := range tcs {
raw, err := json.Marshal(tc.in)
if err != nil {
panic("failed to marshal")
}
t.Run(tc.name, func(t *testing.T) {
raw, err := json.Marshal(tc.in)
if err != nil {
t.Fatalf("failed to marshal: %v", err)
}
t.Logf("Marshalled : %s", string(raw))
t.Logf("Marshalled: %s", string(raw))
got := Foo{}
err = FromUnstructured(tc.in, &got)
if err != tc.wantError {
t.Errorf("Unexpected error for %q: %v", string(tc.name), err)
continue
}
got := Foo{}
if err := FromUnstructured(tc.in, &got); err != tc.wantError {
t.Fatalf("FromUnstructured() = %v", err)
}
if !reflect.DeepEqual(tc.want, got.Status) {
t.Errorf("Decode(%q) want: %+v\ngot: %+v", string(tc.name), tc.want, got)
}
if !cmp.Equal(tc.want, got.Status) {
t.Errorf("ToUnstructured (-want, +got) = %s", cmp.Diff(tc.want, got.Status))
}
})
}
}
func TestToUnstructured(t *testing.T) {
tests := []struct {
name string
in OneOfOurs
want *unstructured.Unstructured
wantError error
}{{
name: "missing TypeMeta",
in: &Resource{
ObjectMeta: metav1.ObjectMeta{
Name: "blah",
},
},
want: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "pkg.knative.dev/v2",
"kind": "Resource",
"metadata": map[string]interface{}{
"creationTimestamp": nil,
"name": "blah",
},
"spec": map[string]interface{}{},
},
},
}, {
name: "with TypeMeta",
in: &Resource{
TypeMeta: metav1.TypeMeta{
APIVersion: "apps/v1",
Kind: "Deployment",
},
ObjectMeta: metav1.ObjectMeta{
Name: "blah",
},
},
want: &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"creationTimestamp": nil,
"name": "blah",
},
"spec": map[string]interface{}{},
},
},
}}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := ToUnstructured(tc.in)
if err != tc.wantError {
t.Fatalf("ToUnstructured() = %v", err)
}
if !cmp.Equal(tc.want, got) {
t.Errorf("ToUnstructured (-want, +got) = %s", cmp.Diff(tc.want, got))
}
})
}
}

View File

@ -22,6 +22,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"knative.dev/pkg/apis"
)
@ -49,6 +50,11 @@ type ResourceSpec struct {
FieldThatsImmutableWithDefault string `json:"fieldThatsImmutableWithDefault,omitempty"`
}
// GetGroupVersionKind returns the GroupVersionKind.
func (r *Resource) GetGroupVersionKind() schema.GroupVersionKind {
return SchemeGroupVersion.WithKind("Resource")
}
// GetUntypedSpec returns the spec of the resource.
func (r *Resource) GetUntypedSpec() interface{} {
return r.Spec