diff --git a/pkg/apimachinery/announced/announced.go b/pkg/apimachinery/announced/announced.go new file mode 100644 index 00000000000..bb1982a6498 --- /dev/null +++ b/pkg/apimachinery/announced/announced.go @@ -0,0 +1,107 @@ +/* +Copyright 2016 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 announced contains tools for announcing API group factories. This is +// distinct from registration (in the 'registered' package) in that it's safe +// to announce every possible group linked in, but only groups requested at +// runtime should be registered. This package contains both a registry, and +// factory code (which was formerly copy-pasta in every install package). +package announced + +import ( + "fmt" + + "k8s.io/kubernetes/pkg/apimachinery/registered" + "k8s.io/kubernetes/pkg/runtime" +) + +var ( + DefaultGroupFactoryRegistry = make(APIGroupFactoryRegistry) + + // These functions will announce your group or version. + AnnounceGroupVersion = DefaultGroupFactoryRegistry.AnnounceGroupVersion + AnnounceGroup = DefaultGroupFactoryRegistry.AnnounceGroup +) + +// APIGroupFactoryRegistry allows for groups and versions to announce themselves, +// which simply makes them available and doesn't take other actions. Later, +// users of the registry can select which groups and versions they'd actually +// like to register with an APIRegistrationManager. +// +// (Right now APIRegistrationManager has separate 'registration' and 'enabled' +// concepts-- APIGroupFactory is going to take over the former function; +// they will overlap untill the refactoring is finished.) +// +// The key is the group name. After initialization, this should be treated as +// read-only. It is implemented as a map from group name to group factory, and +// it is safe to use this knowledge to manually pick out groups to register +// (e.g., for testing). +type APIGroupFactoryRegistry map[string]*GroupMetaFactory + +func (gar APIGroupFactoryRegistry) group(groupName string) *GroupMetaFactory { + gmf, ok := gar[groupName] + if !ok { + gmf = &GroupMetaFactory{VersionArgs: map[string]*GroupVersionFactoryArgs{}} + gar[groupName] = gmf + } + return gmf +} + +// AnnounceGroupVersion adds the particular arguments for this group version to the group factory. +func (gar APIGroupFactoryRegistry) AnnounceGroupVersion(gvf *GroupVersionFactoryArgs) error { + gmf := gar.group(gvf.GroupName) + if _, ok := gmf.VersionArgs[gvf.VersionName]; ok { + return fmt.Errorf("version %q in group %q has already been announced", gvf.VersionName, gvf.GroupName) + } + gmf.VersionArgs[gvf.VersionName] = gvf + return nil +} + +// AnnounceGroup adds the group-wide arguments to the group factory. +func (gar APIGroupFactoryRegistry) AnnounceGroup(args *GroupMetaFactoryArgs) error { + gmf := gar.group(args.GroupName) + if gmf.GroupArgs != nil { + return fmt.Errorf("group %q has already been announced", args.GroupName) + } + gmf.GroupArgs = args + return nil +} + +// RegisterAndEnableAll throws every factory at the specified API registration +// manager, and lets it decide which to register. (If you want to do this a la +// cart, you may look through gar itself-- it's just a map.) +func (gar APIGroupFactoryRegistry) RegisterAndEnableAll(m *registered.APIRegistrationManager, scheme *runtime.Scheme) error { + for groupName, gmf := range gar { + if err := gmf.Register(m); err != nil { + return fmt.Errorf("error registering %v: %v", groupName, err) + } + if err := gmf.Enable(m, scheme); err != nil { + return fmt.Errorf("error enabling %v: %v", groupName, err) + } + } + return nil +} + +// AnnouncePreconstructedFactory announces a factory which you've manually assembled. +// You may call this instead of calling AnnounceGroup and AnnounceGroupVersion. +func (gar APIGroupFactoryRegistry) AnnouncePreconstructedFactory(gmf *GroupMetaFactory) error { + name := gmf.GroupArgs.GroupName + if _, exists := gar[name]; exists { + return fmt.Errorf("the group %q has already been announced.", name) + } + gar[name] = gmf + return nil +} diff --git a/pkg/apimachinery/announced/announced_test.go b/pkg/apimachinery/announced/announced_test.go new file mode 100644 index 00000000000..b79523fef5e --- /dev/null +++ b/pkg/apimachinery/announced/announced_test.go @@ -0,0 +1,66 @@ +/* +Copyright 2016 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 announced + +import ( + "reflect" + "testing" + + "k8s.io/kubernetes/pkg/util/sets" +) + +func TestFactoryRegistry(t *testing.T) { + regA := make(APIGroupFactoryRegistry) + regB := make(APIGroupFactoryRegistry) + + if err := regA.AnnounceGroup(&GroupMetaFactoryArgs{ + GroupName: "foo", + VersionPreferenceOrder: []string{"v2", "v1"}, + ImportPrefix: "pkg/apis/foo", + RootScopedKinds: sets.NewString("namespaces"), + }); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if err := regA.AnnounceGroupVersion(&GroupVersionFactoryArgs{ + GroupName: "foo", + VersionName: "v1", + }); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if err := regA.AnnounceGroupVersion(&GroupVersionFactoryArgs{ + GroupName: "foo", + VersionName: "v2", + }); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if err := regB.AnnouncePreconstructedFactory(NewGroupMetaFactory( + &GroupMetaFactoryArgs{ + GroupName: "foo", + VersionPreferenceOrder: []string{"v2", "v1"}, + ImportPrefix: "pkg/apis/foo", + RootScopedKinds: sets.NewString("namespaces"), + }, + VersionToSchemeFunc{"v1": nil, "v2": nil}, + )); err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if !reflect.DeepEqual(regA, regB) { + t.Errorf("Expected both ways of registering to be equivalent, but they were not.\n\n%#v\n\n%#v\n", regA, regB) + } +} diff --git a/pkg/apimachinery/announced/group_factory.go b/pkg/apimachinery/announced/group_factory.go new file mode 100644 index 00000000000..6ec507c20af --- /dev/null +++ b/pkg/apimachinery/announced/group_factory.go @@ -0,0 +1,249 @@ +/* +Copyright 2016 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 announced + +import ( + "fmt" + + "github.com/golang/glog" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/meta" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/apimachinery" + "k8s.io/kubernetes/pkg/apimachinery/registered" + "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util/sets" +) + +type SchemeFunc func(*runtime.Scheme) error +type VersionToSchemeFunc map[string]SchemeFunc + +// GroupVersionFactoryArgs contains all the per-version parts of a GroupMetaFactory. +type GroupVersionFactoryArgs struct { + GroupName string + VersionName string + + AddToScheme SchemeFunc +} + +// GroupMetaFactoryArgs contains the group-level args of a GroupMetaFactory. +type GroupMetaFactoryArgs struct { + GroupName string + VersionPreferenceOrder []string + ImportPrefix string + + RootScopedKinds sets.String // nil is allowed + IgnoredKinds sets.String // nil is allowed + + // May be nil if there are no internal objects. + AddInternalObjectsToScheme SchemeFunc +} + +// NewGroupMetaFactory builds the args for you. This is for if you're +// constructing a factory all at once and not using the registry. +func NewGroupMetaFactory(groupArgs *GroupMetaFactoryArgs, versions VersionToSchemeFunc) *GroupMetaFactory { + gmf := &GroupMetaFactory{ + GroupArgs: groupArgs, + VersionArgs: map[string]*GroupVersionFactoryArgs{}, + } + for v, f := range versions { + gmf.VersionArgs[v] = &GroupVersionFactoryArgs{ + GroupName: groupArgs.GroupName, + VersionName: v, + AddToScheme: f, + } + } + return gmf +} + +// Announce adds this Group factory to the global factory registry. It should +// only be called if you constructed the GroupMetaFactory yourself via +// NewGroupMetadFactory. +// Note that this will panic on an error, since it's expected that you'll be +// calling this at initialization time and any error is a result of a +// programmer importing the wrong set of packages. If this assumption doesn't +// work for you, just call DefaultGroupFactoryRegistry.AnnouncePreconstructedFactory +// yourself. +func (gmf *GroupMetaFactory) Announce() *GroupMetaFactory { + if err := DefaultGroupFactoryRegistry.AnnouncePreconstructedFactory(gmf); err != nil { + panic(err) + } + return gmf +} + +// GroupMetaFactory has the logic for actually assembling and registering a group. +// +// There are two ways of obtaining one of these. +// 1. You can announce your group and versions separately, and then let the +// GroupFactoryRegistry assemble this object for you. (This allows group and +// versions to be imported separately, without referencing each other, to +// keep import trees small.) +// 2. You can call NewGroupMetaFactory(), which is mostly a drop-in replacement +// for the old, bad way of doing things. You can then call .Announce() to +// announce your constructed factory to any code that would like to do +// things the new, better way. +// +// Note that GroupMetaFactory actually does construct GroupMeta objects, but +// currently it does so in a way that's very entangled with an +// APIRegistrationManager. It's a TODO item to cleanly separate that interface. +type GroupMetaFactory struct { + GroupArgs *GroupMetaFactoryArgs + // map of version name to version factory + VersionArgs map[string]*GroupVersionFactoryArgs + + // assembled by Register() + prioritizedVersionList []unversioned.GroupVersion +} + +// Register constructs the finalized prioritized version list and sanity checks +// the announced group & versions. Then it calls register. +func (gmf *GroupMetaFactory) Register(m *registered.APIRegistrationManager) error { + if gmf.GroupArgs == nil { + return fmt.Errorf("partially announced groups are not allowed, only got versions: %#v", gmf.VersionArgs) + } + if len(gmf.VersionArgs) == 0 { + return fmt.Errorf("group %v announced but no versions announced", gmf.GroupArgs.GroupName) + } + + pvSet := sets.NewString(gmf.GroupArgs.VersionPreferenceOrder...) + if pvSet.Len() != len(gmf.GroupArgs.VersionPreferenceOrder) { + return fmt.Errorf("preference order for group %v has duplicates: %v", gmf.GroupArgs.GroupName, gmf.GroupArgs.VersionPreferenceOrder) + } + prioritizedVersions := []unversioned.GroupVersion{} + for _, v := range gmf.GroupArgs.VersionPreferenceOrder { + prioritizedVersions = append( + prioritizedVersions, + unversioned.GroupVersion{ + Group: gmf.GroupArgs.GroupName, + Version: v, + }, + ) + } + + // Go through versions that weren't explicitly prioritized. + unprioritizedVersions := []unversioned.GroupVersion{} + for _, v := range gmf.VersionArgs { + if v.GroupName != gmf.GroupArgs.GroupName { + return fmt.Errorf("found %v/%v in group %v?", v.GroupName, v.VersionName, gmf.GroupArgs.GroupName) + } + if pvSet.Has(v.VersionName) { + pvSet.Delete(v.VersionName) + continue + } + unprioritizedVersions = append(unprioritizedVersions, unversioned.GroupVersion{Group: v.GroupName, Version: v.VersionName}) + } + if len(unprioritizedVersions) > 1 { + glog.Warningf("group %v has multiple unprioritized versions: %#v. They will have an arbitrary preference order!", gmf.GroupArgs.GroupName, unprioritizedVersions) + } + if pvSet.Len() != 0 { + return fmt.Errorf("group %v has versions in the priority list that were never announced: %s", gmf.GroupArgs.GroupName, pvSet) + } + prioritizedVersions = append(prioritizedVersions, unprioritizedVersions...) + m.RegisterVersions(prioritizedVersions) + gmf.prioritizedVersionList = prioritizedVersions + return nil +} + +func (gmf *GroupMetaFactory) newRESTMapper(scheme *runtime.Scheme, externalVersions []unversioned.GroupVersion, groupMeta *apimachinery.GroupMeta) meta.RESTMapper { + // the list of kinds that are scoped at the root of the api hierarchy + // if a kind is not enumerated here, it is assumed to have a namespace scope + rootScoped := sets.NewString() + if gmf.GroupArgs.RootScopedKinds != nil { + rootScoped = gmf.GroupArgs.RootScopedKinds + } + ignoredKinds := sets.NewString() + if gmf.GroupArgs.IgnoredKinds != nil { + ignoredKinds = gmf.GroupArgs.IgnoredKinds + } + + return api.NewDefaultRESTMapperFromScheme( + externalVersions, + groupMeta.InterfacesFor, + gmf.GroupArgs.ImportPrefix, + ignoredKinds, + rootScoped, + scheme, + ) +} + +// Enable enables group versions that are allowed, adds methods to the scheme, etc. +func (gmf *GroupMetaFactory) Enable(m *registered.APIRegistrationManager, scheme *runtime.Scheme) error { + externalVersions := []unversioned.GroupVersion{} + for _, v := range gmf.prioritizedVersionList { + if !m.IsAllowedVersion(v) { + continue + } + externalVersions = append(externalVersions, v) + if err := m.EnableVersions(v); err != nil { + return err + } + gmf.VersionArgs[v.Version].AddToScheme(scheme) + } + if len(externalVersions) == 0 { + glog.V(4).Infof("No version is registered for group %v", gmf.GroupArgs.GroupName) + return nil + } + + if gmf.GroupArgs.AddInternalObjectsToScheme != nil { + gmf.GroupArgs.AddInternalObjectsToScheme(scheme) + } + + preferredExternalVersion := externalVersions[0] + accessor := meta.NewAccessor() + + groupMeta := &apimachinery.GroupMeta{ + GroupVersion: preferredExternalVersion, + GroupVersions: externalVersions, + SelfLinker: runtime.SelfLinker(accessor), + } + for _, v := range externalVersions { + gvf := gmf.VersionArgs[v.Version] + if err := groupMeta.AddVersionInterfaces( + unversioned.GroupVersion{Group: gvf.GroupName, Version: gvf.VersionName}, + &meta.VersionInterfaces{ + ObjectConvertor: scheme, + MetadataAccessor: accessor, + }, + ); err != nil { + return err + } + } + groupMeta.InterfacesFor = groupMeta.DefaultInterfacesFor + groupMeta.RESTMapper = gmf.newRESTMapper(scheme, externalVersions, groupMeta) + + if err := m.RegisterGroup(*groupMeta); err != nil { + return err + } + return nil +} + +// RegisterAndEnable is provided only to allow this code to get added in multiple steps. +// It's really bad that this is called in init() methods, but supporting this +// temporarily lets us do the change incrementally. +func (gmf *GroupMetaFactory) RegisterAndEnable() error { + if err := gmf.Register(registered.DefaultAPIRegistrationManager); err != nil { + return err + } + if err := gmf.Enable(registered.DefaultAPIRegistrationManager, api.Scheme); err != nil { + return err + } + + // TODO: find a sane way to register the rest mappers. + api.RegisterRESTMapper(registered.GroupOrDie(gmf.GroupArgs.GroupName).RESTMapper) + return nil +} diff --git a/pkg/apimachinery/registered/registered.go b/pkg/apimachinery/registered/registered.go index 16923a36d37..d6380ac39ac 100644 --- a/pkg/apimachinery/registered/registered.go +++ b/pkg/apimachinery/registered/registered.go @@ -35,7 +35,14 @@ var ( DefaultAPIRegistrationManager = NewOrDie(os.Getenv("KUBE_API_VERSIONS")) ) -// APIRegistrationManager +// APIRegistrationManager provides the concept of what API groups are enabled. +// +// TODO: currently, it also provides a "registered" concept. But it's wrong to +// have both concepts in the same object. Therefore the "announced" package is +// going to take over the registered concept. After all the install packages +// are switched to using the announce package instead of this package, then we +// can combine the registered/enabled concepts in this object. Simplifying this +// isn't easy right now because there are so many callers of this package. type APIRegistrationManager struct { // registeredGroupVersions stores all API group versions for which RegisterGroup is called. registeredVersions map[unversioned.GroupVersion]struct{} diff --git a/pkg/apimachinery/types.go b/pkg/apimachinery/types.go index d42323651ac..c9146080a24 100644 --- a/pkg/apimachinery/types.go +++ b/pkg/apimachinery/types.go @@ -58,7 +58,7 @@ type GroupMeta struct { InterfacesByVersion map[unversioned.GroupVersion]*meta.VersionInterfaces } -// InterfacesFor returns the default Codec and ResourceVersioner for a given version +// DefaultInterfacesFor returns the default Codec and ResourceVersioner for a given version // string, or an error if the version is not known. // TODO: Remove the "Default" prefix. func (gm *GroupMeta) DefaultInterfacesFor(version unversioned.GroupVersion) (*meta.VersionInterfaces, error) { @@ -68,10 +68,12 @@ func (gm *GroupMeta) DefaultInterfacesFor(version unversioned.GroupVersion) (*me return nil, fmt.Errorf("unsupported storage version: %s (valid: %v)", version, gm.GroupVersions) } -// Adds the given version to the group. Only call during init, after that -// GroupMeta objects should be immutable. Not thread safe. +// AddVersionInterfaces adds the given version to the group. Only call during +// init, after that GroupMeta objects should be immutable. Not thread safe. // (If you use this, be sure to set .InterfacesFor = .DefaultInterfacesFor) -func (gm *GroupMeta) AddVersion(version unversioned.GroupVersion, interfaces *meta.VersionInterfaces) error { +// TODO: remove the "Interfaces" suffix and make this also maintain the +// .GroupVersions member. +func (gm *GroupMeta) AddVersionInterfaces(version unversioned.GroupVersion, interfaces *meta.VersionInterfaces) error { if e, a := gm.GroupVersion.Group, version.Group; a != e { return fmt.Errorf("got a version in group %v, but am in group %v", a, e) } @@ -79,6 +81,13 @@ func (gm *GroupMeta) AddVersion(version unversioned.GroupVersion, interfaces *me gm.InterfacesByVersion = make(map[unversioned.GroupVersion]*meta.VersionInterfaces) } gm.InterfacesByVersion[version] = interfaces - gm.GroupVersions = append(gm.GroupVersions, version) - return nil + + // TODO: refactor to make the below error not possible, this function + // should *set* GroupVersions rather than depend on it. + for _, v := range gm.GroupVersions { + if v == version { + return nil + } + } + return fmt.Errorf("added a version interface without the corresponding version %v being in the list %#v", version, gm.GroupVersions) } diff --git a/pkg/apimachinery/types_test.go b/pkg/apimachinery/types_test.go index 7cf0e918abe..be5cf4261b8 100644 --- a/pkg/apimachinery/types_test.go +++ b/pkg/apimachinery/types_test.go @@ -1,5 +1,5 @@ /* -Copyright 2016 The Kubernetes Authors All rights reserved. +Copyright 2016 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. @@ -28,12 +28,12 @@ func TestAdd(t *testing.T) { Group: "test", Version: "v1", }, - GroupVersions: []unversioned.GroupVersion{{"test", "v1"}}, + GroupVersions: []unversioned.GroupVersion{{Group: "test", Version: "v1"}}, } - gm.AddVersion(unversioned.GroupVersion{"test", "v1"}, nil) + gm.AddVersionInterfaces(unversioned.GroupVersion{Group: "test", Version: "v1"}, nil) if e, a := 1, len(gm.InterfacesByVersion); e != a { - t.Errorf("expected %v, got %v") + t.Errorf("expected %v, got %v", e, a) } // GroupVersions is unchanged