Merge pull request #125629 from benluddy/cbor-rawextension

KEP-4222: Automatically transcode RawExtension between unstructured protocols.
This commit is contained in:
Kubernetes Prow Robot 2024-07-10 14:54:35 -07:00 committed by GitHub
commit 2d877b5259
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 625 additions and 24 deletions

View File

@ -137,25 +137,7 @@ func TestRoundTrip(t *testing.T) {
}
func TestRoundtripToUnstructured(t *testing.T) {
skipped := sets.New(
// TODO: Support cross-protocol RawExtension roundtrips.
schema.GroupVersionKind{Version: "v1", Kind: "List"},
schema.GroupVersionKind{Group: "apps", Version: "v1beta1", Kind: "ControllerRevision"},
schema.GroupVersionKind{Group: "apps", Version: "v1beta1", Kind: "ControllerRevisionList"},
schema.GroupVersionKind{Group: "apps", Version: "v1beta2", Kind: "ControllerRevision"},
schema.GroupVersionKind{Group: "apps", Version: "v1beta2", Kind: "ControllerRevisionList"},
schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "ControllerRevision"},
schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "ControllerRevisionList"},
schema.GroupVersionKind{Group: "admission.k8s.io", Version: "v1beta1", Kind: "AdmissionReview"},
schema.GroupVersionKind{Group: "admission.k8s.io", Version: "v1", Kind: "AdmissionReview"},
schema.GroupVersionKind{Group: "resource.k8s.io", Version: "v1alpha2", Kind: "ResourceClaim"},
schema.GroupVersionKind{Group: "resource.k8s.io", Version: "v1alpha2", Kind: "ResourceClaimList"},
schema.GroupVersionKind{Group: "resource.k8s.io", Version: "v1alpha2", Kind: "ResourceClaimParameters"},
schema.GroupVersionKind{Group: "resource.k8s.io", Version: "v1alpha2", Kind: "ResourceClaimParametersList"},
schema.GroupVersionKind{Group: "resource.k8s.io", Version: "v1alpha2", Kind: "ResourceClassParameters"},
schema.GroupVersionKind{Group: "resource.k8s.io", Version: "v1alpha2", Kind: "ResourceClassParametersList"},
)
skipped := sets.New[schema.GroupVersionKind]()
for gvk := range legacyscheme.Scheme.AllKnownTypes() {
if nonRoundTrippableTypes.Has(gvk.Kind) {
skipped.Insert(gvk)

View File

@ -18,16 +18,77 @@ package runtime
import (
"bytes"
"encoding/json"
"errors"
"fmt"
cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct"
"k8s.io/apimachinery/pkg/util/json"
)
// RawExtension intentionally avoids implementing value.UnstructuredConverter for now because the
// signature of ToUnstructured does not allow returning an error value in cases where the conversion
// is not possible (content type is unrecognized or bytes don't match content type).
func rawToUnstructured(raw []byte, contentType string) (interface{}, error) {
switch contentType {
case ContentTypeJSON:
var u interface{}
if err := json.Unmarshal(raw, &u); err != nil {
return nil, fmt.Errorf("failed to parse RawExtension bytes as JSON: %w", err)
}
return u, nil
case ContentTypeCBOR:
var u interface{}
if err := cbor.Unmarshal(raw, &u); err != nil {
return nil, fmt.Errorf("failed to parse RawExtension bytes as CBOR: %w", err)
}
return u, nil
default:
return nil, fmt.Errorf("cannot convert RawExtension with unrecognized content type to unstructured")
}
}
func (re RawExtension) guessContentType() string {
switch {
case bytes.HasPrefix(re.Raw, cborSelfDescribed):
return ContentTypeCBOR
case len(re.Raw) > 0:
switch re.Raw[0] {
case '\t', '\r', '\n', ' ', '{', '[', 'n', 't', 'f', '"', '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
// Prefixes for the four whitespace characters, objects, arrays, strings, numbers, true, false, and null.
return ContentTypeJSON
}
}
return ""
}
func (re *RawExtension) UnmarshalJSON(in []byte) error {
if re == nil {
return errors.New("runtime.RawExtension: UnmarshalJSON on nil pointer")
}
if !bytes.Equal(in, []byte("null")) {
re.Raw = append(re.Raw[0:0], in...)
if bytes.Equal(in, []byte("null")) {
return nil
}
re.Raw = append(re.Raw[0:0], in...)
return nil
}
var (
cborNull = []byte{0xf6}
cborSelfDescribed = []byte{0xd9, 0xd9, 0xf7}
)
func (re *RawExtension) UnmarshalCBOR(in []byte) error {
if re == nil {
return errors.New("runtime.RawExtension: UnmarshalCBOR on nil pointer")
}
if !bytes.Equal(in, cborNull) {
if !bytes.HasPrefix(in, cborSelfDescribed) {
// The self-described CBOR tag doesn't change the interpretation of the data
// item it encloses, but it is useful as a magic number. Its encoding is
// also what is used to implement the CBOR RecognizingDecoder.
re.Raw = append(re.Raw[:0], cborSelfDescribed...)
}
re.Raw = append(re.Raw, in...)
}
return nil
}
@ -46,6 +107,35 @@ func (re RawExtension) MarshalJSON() ([]byte, error) {
}
return []byte("null"), nil
}
// TODO: Check whether ContentType is actually JSON before returning it.
return re.Raw, nil
contentType := re.guessContentType()
if contentType == ContentTypeJSON {
return re.Raw, nil
}
u, err := rawToUnstructured(re.Raw, contentType)
if err != nil {
return nil, err
}
return json.Marshal(u)
}
func (re RawExtension) MarshalCBOR() ([]byte, error) {
if re.Raw == nil {
if re.Object != nil {
return cbor.Marshal(re.Object)
}
return cbor.Marshal(nil)
}
contentType := re.guessContentType()
if contentType == ContentTypeCBOR {
return re.Raw, nil
}
u, err := rawToUnstructured(re.Raw, contentType)
if err != nil {
return nil, err
}
return cbor.Marshal(u)
}

View File

@ -23,6 +23,9 @@ import (
"testing"
"k8s.io/apimachinery/pkg/runtime"
runtimetesting "k8s.io/apimachinery/pkg/runtime/testing"
"github.com/google/go-cmp/cmp"
)
func TestEmbeddedRawExtensionMarshal(t *testing.T) {
@ -111,3 +114,151 @@ func TestEmbeddedRawExtensionRoundTrip(t *testing.T) {
}
}
}
func TestRawExtensionMarshalUnstructured(t *testing.T) {
for _, tc := range []struct {
Name string
In runtime.RawExtension
WantCBOR []byte
ExpectedErrorCBOR string
WantJSON string
ExpectedErrorJSON string
}{
{
Name: "nil bytes and nil object",
In: runtime.RawExtension{},
WantCBOR: []byte{0xf6},
WantJSON: "null",
},
{
Name: "nil bytes and non-nil object",
In: runtime.RawExtension{Object: &runtimetesting.ExternalSimple{TestString: "foo"}},
WantCBOR: []byte("\xa1\x4atestString\x43foo"),
WantJSON: `{"testString":"foo"}`,
},
{
Name: "cbor bytes not enclosed in self-described tag",
In: runtime.RawExtension{Raw: []byte{0x43, 'f', 'o', 'o'}}, // 'foo'
ExpectedErrorCBOR: "cannot convert RawExtension with unrecognized content type to unstructured",
ExpectedErrorJSON: "cannot convert RawExtension with unrecognized content type to unstructured",
},
{
Name: "cbor bytes enclosed in self-described tag",
In: runtime.RawExtension{Raw: []byte{0xd9, 0xd9, 0xf7, 0x43, 'f', 'o', 'o'}}, // 55799('foo')
WantCBOR: []byte{0xd9, 0xd9, 0xf7, 0x43, 'f', 'o', 'o'}, // 55799('foo')
WantJSON: `"foo"`,
},
{
Name: "json bytes",
In: runtime.RawExtension{Raw: []byte(`"foo"`)},
WantCBOR: []byte{0x43, 'f', 'o', 'o'},
WantJSON: `"foo"`,
},
{
Name: "ambiguous bytes not enclosed in self-described cbor tag",
In: runtime.RawExtension{Raw: []byte{'0'}}, // CBOR -17 / JSON 0
WantCBOR: []byte{0x00},
WantJSON: `0`,
},
{
Name: "ambiguous bytes enclosed in self-described cbor tag",
In: runtime.RawExtension{Raw: []byte{0xd9, 0xd9, 0xf7, '0'}}, // 55799(-17)
WantCBOR: []byte{0xd9, 0xd9, 0xf7, '0'},
WantJSON: `-17`,
},
{
Name: "unrecognized bytes",
In: runtime.RawExtension{Raw: []byte{0xff}},
ExpectedErrorCBOR: "cannot convert RawExtension with unrecognized content type to unstructured",
ExpectedErrorJSON: "cannot convert RawExtension with unrecognized content type to unstructured",
},
{
Name: "invalid cbor with self-described cbor prefix",
In: runtime.RawExtension{Raw: []byte{0xd9, 0xd9, 0xf7, 0xff}},
WantCBOR: []byte{0xd9, 0xd9, 0xf7, 0xff}, // verbatim
ExpectedErrorJSON: `failed to parse RawExtension bytes as CBOR: cbor: unexpected "break" code`,
},
{
Name: "invalid json with json prefix",
In: runtime.RawExtension{Raw: []byte(`{{`)},
ExpectedErrorCBOR: `failed to parse RawExtension bytes as JSON: invalid character '{' looking for beginning of object key string`,
WantJSON: `{{`, // verbatim
},
} {
t.Run(tc.Name, func(t *testing.T) {
t.Run("CBOR", func(t *testing.T) {
got, err := tc.In.MarshalCBOR()
if err != nil {
if tc.ExpectedErrorCBOR == "" {
t.Fatalf("unexpected error: %v", err)
}
if msg := err.Error(); msg != tc.ExpectedErrorCBOR {
t.Fatalf("expected error %q but got %q", tc.ExpectedErrorCBOR, msg)
}
}
if diff := cmp.Diff(tc.WantCBOR, got); diff != "" {
t.Errorf("unexpected diff:\n%s", diff)
}
})
t.Run("JSON", func(t *testing.T) {
got, err := tc.In.MarshalJSON()
if err != nil {
if tc.ExpectedErrorJSON == "" {
t.Fatalf("unexpected error: %v", err)
}
if msg := err.Error(); msg != tc.ExpectedErrorJSON {
t.Fatalf("expected error %q but got %q", tc.ExpectedErrorJSON, msg)
}
}
if diff := cmp.Diff(tc.WantJSON, string(got)); diff != "" {
t.Errorf("unexpected diff:\n%s", diff)
}
})
})
}
}
func TestRawExtensionUnmarshalCBOR(t *testing.T) {
for _, tc := range []struct {
Name string
In []byte
Want runtime.RawExtension
}{
{
// From json.Unmarshaler: By convention, to approximate the behavior of
// Unmarshal itself, Unmarshalers implement UnmarshalJSON([]byte("null")) as
// a no-op.
Name: "no-op on null",
In: []byte{0xf6},
Want: runtime.RawExtension{},
},
{
Name: "input copied verbatim",
In: []byte{0xd9, 0xd9, 0xf7, 0x5f, 0x41, 'f', 0x42, 'o', 'o', 0xff}, // 55799(_ 'f' 'oo')
Want: runtime.RawExtension{
Raw: []byte{0xd9, 0xd9, 0xf7, 0x5f, 0x41, 'f', 0x42, 'o', 'o', 0xff}, // 55799(_ 'f' 'oo')
},
},
{
Name: "input enclosed in self-described tag if absent",
In: []byte{0x5f, 0x41, 'f', 0x42, 'o', 'o', 0xff}, // (_ 'f' 'oo')
Want: runtime.RawExtension{
Raw: []byte{0xd9, 0xd9, 0xf7, 0x5f, 0x41, 'f', 0x42, 'o', 'o', 0xff}, // 55799(_ 'f' 'oo')
},
},
} {
t.Run(tc.Name, func(t *testing.T) {
var got runtime.RawExtension
if err := got.UnmarshalCBOR(tc.In); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if diff := cmp.Diff(tc.Want, got); diff != "" {
t.Errorf("unexpected diff:\n%s", diff)
}
})
}
}

View File

@ -298,6 +298,12 @@ func (s *serializer) Decode(data []byte, gvk *schema.GroupVersionKind, into runt
if err != nil {
return nil, actual, err
}
// TODO: Make possible to disable this behavior.
if err := transcodeRawTypes(obj); err != nil {
return nil, actual, err
}
return obj, actual, strict
}

View File

@ -121,6 +121,18 @@ func (p *anyObject) UnmarshalCBOR(in []byte) error {
return modes.Decode.Unmarshal(in, &p.Value)
}
type structWithRawExtensionField struct {
Extension runtime.RawExtension `json:"extension"`
}
func (p structWithRawExtensionField) GetObjectKind() schema.ObjectKind {
return schema.EmptyObjectKind
}
func (structWithRawExtensionField) DeepCopyObject() runtime.Object {
panic("unimplemented")
}
func TestEncode(t *testing.T) {
for _, tc := range []struct {
name string
@ -264,6 +276,21 @@ func TestDecode(t *testing.T) {
}
},
},
{
name: "rawextension transcoded",
data: []byte{0xa1, 0x49, 'e', 'x', 't', 'e', 'n', 's', 'i', 'o', 'n', 0xa1, 0x41, 'a', 0x01},
gvk: &schema.GroupVersionKind{},
metaFactory: stubMetaFactory{gvk: &schema.GroupVersionKind{}},
typer: stubTyper{gvks: []schema.GroupVersionKind{{Group: "x", Version: "y", Kind: "z"}}},
into: &structWithRawExtensionField{},
expectedObj: &structWithRawExtensionField{Extension: runtime.RawExtension{Raw: []byte(`{"a":1}`)}},
expectedGVK: &schema.GroupVersionKind{Group: "x", Version: "y", Kind: "z"},
assertOnError: func(t *testing.T, err error) {
if err != nil {
t.Errorf("expected nil error, got: %v", err)
}
},
},
{
name: "strict mode strict error",
options: []Option{Strict(true)},

View File

@ -0,0 +1,218 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cbor
import (
"fmt"
"reflect"
"sync"
"k8s.io/apimachinery/pkg/runtime"
)
var sharedTranscoders transcoders
var rawTypeTranscodeFuncs = map[reflect.Type]func(reflect.Value) error{
reflect.TypeFor[runtime.RawExtension](): func(rv reflect.Value) error {
if !rv.CanAddr() {
return nil
}
re := rv.Addr().Interface().(*runtime.RawExtension)
if re.Raw == nil {
// When Raw is nil it encodes to null. Don't change nil Raw values during
// transcoding, they would have unmarshalled from JSON as nil too.
return nil
}
j, err := re.MarshalJSON()
if err != nil {
return fmt.Errorf("failed to transcode RawExtension to JSON: %w", err)
}
re.Raw = j
return nil
},
}
func transcodeRawTypes(v interface{}) error {
if v == nil {
return nil
}
rv := reflect.ValueOf(v)
return sharedTranscoders.getTranscoder(rv.Type()).fn(rv)
}
type transcoder struct {
fn func(rv reflect.Value) error
}
var noop = transcoder{
fn: func(reflect.Value) error {
return nil
},
}
type transcoders struct {
lock sync.RWMutex
m map[reflect.Type]**transcoder
}
func (ts *transcoders) getTranscoder(rt reflect.Type) transcoder {
ts.lock.RLock()
tpp, ok := ts.m[rt]
ts.lock.RUnlock()
if ok {
return **tpp
}
ts.lock.Lock()
defer ts.lock.Unlock()
tp := ts.getTranscoderLocked(rt)
return *tp
}
func (ts *transcoders) getTranscoderLocked(rt reflect.Type) *transcoder {
if tpp, ok := ts.m[rt]; ok {
// A transcoder for this type was cached while waiting to acquire the lock.
return *tpp
}
// Cache the transcoder now, before populating fn, so that circular references between types
// don't overflow the call stack.
t := new(transcoder)
if ts.m == nil {
ts.m = make(map[reflect.Type]**transcoder)
}
ts.m[rt] = &t
for rawType, fn := range rawTypeTranscodeFuncs {
if rt == rawType {
t = &transcoder{fn: fn}
return t
}
}
switch rt.Kind() {
case reflect.Array:
te := ts.getTranscoderLocked(rt.Elem())
rtlen := rt.Len()
if rtlen == 0 || te == &noop {
t = &noop
break
}
t.fn = func(rv reflect.Value) error {
for i := 0; i < rtlen; i++ {
if err := te.fn(rv.Index(i)); err != nil {
return err
}
}
return nil
}
case reflect.Interface:
// Any interface value might have a dynamic type involving RawExtension. It needs to
// be checked.
t.fn = func(rv reflect.Value) error {
if rv.IsNil() {
return nil
}
rv = rv.Elem()
// The interface element's type is dynamic so its transcoder can't be
// determined statically.
return ts.getTranscoder(rv.Type()).fn(rv)
}
case reflect.Map:
rtk := rt.Key()
tk := ts.getTranscoderLocked(rtk)
rte := rt.Elem()
te := ts.getTranscoderLocked(rte)
if tk == &noop && te == &noop {
t = &noop
break
}
t.fn = func(rv reflect.Value) error {
iter := rv.MapRange()
rvk := reflect.New(rtk).Elem()
rve := reflect.New(rte).Elem()
for iter.Next() {
rvk.SetIterKey(iter)
if err := tk.fn(rvk); err != nil {
return err
}
rve.SetIterValue(iter)
if err := te.fn(rve); err != nil {
return err
}
}
return nil
}
case reflect.Pointer:
te := ts.getTranscoderLocked(rt.Elem())
if te == &noop {
t = &noop
break
}
t.fn = func(rv reflect.Value) error {
if rv.IsNil() {
return nil
}
return te.fn(rv.Elem())
}
case reflect.Slice:
te := ts.getTranscoderLocked(rt.Elem())
if te == &noop {
t = &noop
break
}
t.fn = func(rv reflect.Value) error {
for i := 0; i < rv.Len(); i++ {
if err := te.fn(rv.Index(i)); err != nil {
return err
}
}
return nil
}
case reflect.Struct:
type fieldTranscoder struct {
Index int
Transcoder *transcoder
}
var fieldTranscoders []fieldTranscoder
for i := 0; i < rt.NumField(); i++ {
f := rt.Field(i)
tf := ts.getTranscoderLocked(f.Type)
if tf == &noop {
continue
}
fieldTranscoders = append(fieldTranscoders, fieldTranscoder{Index: i, Transcoder: tf})
}
if len(fieldTranscoders) == 0 {
t = &noop
break
}
t.fn = func(rv reflect.Value) error {
for _, ft := range fieldTranscoders {
if err := ft.Transcoder.fn(rv.Field(ft.Index)); err != nil {
return err
}
}
return nil
}
default:
t = &noop
}
return t
}

View File

@ -0,0 +1,126 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cbor
import (
"fmt"
"testing"
"k8s.io/apimachinery/pkg/runtime"
"github.com/google/go-cmp/cmp"
)
func TestTranscodeRawTypes(t *testing.T) {
for _, tc := range []struct {
In interface{}
Out interface{}
}{
{
In: nil,
Out: nil,
},
{
In: &runtime.RawExtension{Raw: []byte{0xd9, 0xd9, 0xf7, 0x07}},
Out: &runtime.RawExtension{Raw: []byte(`7`)},
},
{
In: &runtime.RawExtension{},
Out: &runtime.RawExtension{},
},
{
In: runtime.RawExtension{Raw: []byte{0xd9, 0xd9, 0xf7, 0x07}},
Out: runtime.RawExtension{Raw: []byte{0xd9, 0xd9, 0xf7, 0x07}}, // not addressable
},
{
In: &[...]runtime.RawExtension{{Raw: []byte{0xd9, 0xd9, 0xf7, 0x07}}, {Raw: []byte{0xd9, 0xd9, 0xf7, 0x08}}, {Raw: []byte{0xd9, 0xd9, 0xf7, 0x09}}},
Out: &[...]runtime.RawExtension{{Raw: []byte(`7`)}, {Raw: []byte(`8`)}, {Raw: []byte(`9`)}},
},
{
In: &[0]runtime.RawExtension{},
Out: &[0]runtime.RawExtension{},
},
{
In: &[]runtime.RawExtension{{Raw: []byte{0xd9, 0xd9, 0xf7, 0x07}}, {Raw: []byte{0xd9, 0xd9, 0xf7, 0x08}}, {Raw: []byte{0xd9, 0xd9, 0xf7, 0x09}}},
Out: &[]runtime.RawExtension{{Raw: []byte(`7`)}, {Raw: []byte(`8`)}, {Raw: []byte(`9`)}},
},
{
In: &[]runtime.RawExtension{},
Out: &[]runtime.RawExtension{},
},
{
In: &[]string{"foo"},
Out: &[]string{"foo"},
},
{
In: (*runtime.RawExtension)(nil),
Out: (*runtime.RawExtension)(nil),
},
{
In: &struct{ I fmt.Stringer }{I: &runtime.RawExtension{Raw: []byte{0xd9, 0xd9, 0xf7, 0x07}}},
Out: &struct{ I fmt.Stringer }{I: &runtime.RawExtension{Raw: []byte(`7`)}},
},
{
In: &struct{ I fmt.Stringer }{I: nil},
Out: &struct{ I fmt.Stringer }{I: nil},
},
{
In: &struct{ I int64 }{I: 7},
Out: &struct{ I int64 }{I: 7},
},
{
In: &struct {
E runtime.RawExtension
I int64
}{E: runtime.RawExtension{Raw: []byte{0xd9, 0xd9, 0xf7, 0x07}}, I: 7},
Out: &struct {
E runtime.RawExtension
I int64
}{E: runtime.RawExtension{Raw: []byte(`7`)}, I: 7},
},
{
In: &struct {
runtime.RawExtension
}{RawExtension: runtime.RawExtension{Raw: []byte{0xd9, 0xd9, 0xf7, 0x07}}},
Out: &struct {
runtime.RawExtension
}{RawExtension: runtime.RawExtension{Raw: []byte(`7`)}},
},
{
In: &map[string]string{"hello": "world"},
Out: &map[string]string{"hello": "world"},
},
{
In: &map[string]runtime.RawExtension{"hello": {Raw: []byte{0xd9, 0xd9, 0xf7, 0x07}}},
Out: &map[string]runtime.RawExtension{"hello": {Raw: []byte{0xd9, 0xd9, 0xf7, 0x07}}}, // not addressable
},
{
In: &map[string][]runtime.RawExtension{"hello": {{Raw: []byte{0xd9, 0xd9, 0xf7, 0x07}}}},
Out: &map[string][]runtime.RawExtension{"hello": {{Raw: []byte(`7`)}}},
},
} {
t.Run(fmt.Sprintf("%#v", tc.In), func(t *testing.T) {
if err := transcodeRawTypes(tc.In); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if diff := cmp.Diff(tc.Out, tc.In); diff != "" {
t.Errorf("unexpected diff:\n%s", diff)
}
})
}
}

View File

@ -46,6 +46,7 @@ const (
ContentTypeJSON string = "application/json"
ContentTypeYAML string = "application/yaml"
ContentTypeProtobuf string = "application/vnd.kubernetes.protobuf"
ContentTypeCBOR string = "application/cbor"
)
// RawExtension is used to hold extensions in external versions.