Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support legacy scheme resources as CRDs #7

Open
wants to merge 3 commits into
base: streamline
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions pkg/controlplane/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ import (
"strings"
"time"

"github.com/emicklei/go-restful"
extensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/authorization/union"
"k8s.io/apiserver/pkg/endpoints/discovery"
openapinamer "k8s.io/apiserver/pkg/endpoints/openapi"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
genericapiserver "k8s.io/apiserver/pkg/server"
Expand Down Expand Up @@ -112,6 +114,14 @@ func CreateServerChain(completedOptions completedServerRunOptions, stopCh <-chan
return nil, err
}

kubeAPIServer.GenericAPIServer.Handler.GoRestfulContainer.Filter(func(req *restful.Request, res *restful.Response, chain *restful.FilterChain){
if discovery.IsAPIContributed(req.Request.URL.Path) {
apiExtensionsServer.GenericAPIServer.Handler.NonGoRestfulMux.ServeHTTP(res.ResponseWriter, req.Request)
} else {
chain.ProcessFilter(req, res)
}
})

return aggregatorServer, nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/util/webhook"
"k8s.io/kubernetes/pkg/api/legacyscheme"

"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
Expand All @@ -48,7 +49,11 @@ var (
func ValidateCustomResourceDefinition(obj *apiextensions.CustomResourceDefinition, requestGV schema.GroupVersion) field.ErrorList {
nameValidationFn := func(name string, prefix bool) []string {
ret := genericvalidation.NameIsDNSSubdomain(name, prefix)
requiredName := obj.Spec.Names.Plural + "." + obj.Spec.Group
group := obj.Spec.Group
if group == "" {
group = "core"
}
requiredName := obj.Spec.Names.Plural + "." + group
if name != requiredName {
ret = append(ret, fmt.Sprintf(`must be spec.names.plural+"."+spec.group`))
}
Expand Down Expand Up @@ -171,8 +176,12 @@ func validateCustomResourceDefinitionVersion(version *apiextensions.CustomResour
func validateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefinitionSpec, opts validationOptions, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}

if len(spec.Group) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("group"), ""))
// HACK: Relax naming constraints when registering legacy schema resources through CRDs
// for the KCP scenario
if legacyscheme.Scheme.IsGroupRegistered(spec.Group) {
// No error: these are legacy schema kubernetes types
// that are not added in the controlplane schema
// and that we want to move up to the KCP as CRDs
} else if errs := utilvalidation.IsDNS1123Subdomain(spec.Group); len(errs) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("group"), spec.Group, strings.Join(errs, ",")))
} else if len(strings.Split(spec.Group, ".")) < 2 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
}
s.GenericAPIServer.Handler.NonGoRestfulMux.Handle("/apis", crdHandler)
s.GenericAPIServer.Handler.NonGoRestfulMux.HandlePrefix("/apis/", crdHandler)
// HACK: Added to allow serving core resources registered through CRDs (for the KCP scenario)
s.GenericAPIServer.Handler.NonGoRestfulMux.HandlePrefix("/api/v1/", crdHandler)

crdController := NewDiscoveryController(s.Informers.Apiextensions().V1().CustomResourceDefinitions(), versionDiscoveryHandler, groupDiscoveryHandler)
namingController := status.NewNamingConditionController(s.Informers.Apiextensions().V1().CustomResourceDefinitions(), crdClient.ApiextensionsV1())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"time"

"k8s.io/klog"
"k8s.io/kubernetes/pkg/api/legacyscheme"

autoscaling "k8s.io/api/autoscaling/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -104,11 +105,18 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error {
// If there is any Served version, that means the group should show up in discovery
foundGroup = true

// HACK: support the case when we add core resources through CRDs (KCP scenario)
groupVersion := crd.Spec.Group + "/" + v.Name
if crd.Spec.Group == "" {
groupVersion = v.Name
}

gv := metav1.GroupVersion{Group: crd.Spec.Group, Version: v.Name}

if !versionsForDiscoveryMap[gv] {
versionsForDiscoveryMap[gv] = true
apiVersionsForDiscovery = append(apiVersionsForDiscovery, metav1.GroupVersionForDiscovery{
GroupVersion: crd.Spec.Group + "/" + v.Name,
GroupVersion: groupVersion,
Version: v.Name,
})
}
Expand Down Expand Up @@ -167,30 +175,46 @@ func (c *DiscoveryController) sync(version schema.GroupVersion) error {
}
}

if !foundGroup {
c.groupHandler.unsetDiscovery(version.Group)
c.versionHandler.unsetDiscovery(version)
return nil
}

sortGroupDiscoveryByKubeAwareVersion(apiVersionsForDiscovery)

apiGroup := metav1.APIGroup{
Name: version.Group,
Versions: apiVersionsForDiscovery,
// the preferred versions for a group is the first item in
// apiVersionsForDiscovery after it put in the right ordered
PreferredVersion: apiVersionsForDiscovery[0],
resourceListerFunc := discovery.APIResourceListerFunc(func() []metav1.APIResource {
return apiResourcesForDiscovery
})

// HACK: if we are adding resources in legacy scheme group through CRDs (KCP scenario)
// then do not expose the CRD `APIResource`s in their own CRD-related group`,
// But instead add them in the existing legacy schema group
if legacyscheme.Scheme.IsGroupRegistered(version.Group) {
if !foundGroup || !foundVersion{
delete(discovery.ContributedResources, version)
}

discovery.ContributedResources[version] = resourceListerFunc
}
c.groupHandler.setDiscovery(version.Group, discovery.NewAPIGroupHandler(Codecs, apiGroup))

if !foundVersion {
if !foundGroup {
c.groupHandler.unsetDiscovery(version.Group)
c.versionHandler.unsetDiscovery(version)
return nil
}
c.versionHandler.setDiscovery(version, discovery.NewAPIVersionHandler(Codecs, version, discovery.APIResourceListerFunc(func() []metav1.APIResource {
return apiResourcesForDiscovery
})))

if version.Group != "" {
// If we don't add resources in the core API group
apiGroup := metav1.APIGroup{
Name: version.Group,
Versions: apiVersionsForDiscovery,
// the preferred versions for a group is the first item in
// apiVersionsForDiscovery after it put in the right ordered
PreferredVersion: apiVersionsForDiscovery[0],
}
c.groupHandler.setDiscovery(version.Group, discovery.NewAPIGroupHandler(Codecs, apiGroup))

if !foundVersion {
c.versionHandler.unsetDiscovery(version)
return nil
}
c.versionHandler.setDiscovery(version, discovery.NewAPIVersionHandler(Codecs, version, resourceListerFunc))
}

return nil
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import (
"k8s.io/apiextensions-apiserver/pkg/crdserverscheme"
"k8s.io/apiextensions-apiserver/pkg/registry/customresource"
"k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor"
"k8s.io/kubernetes/pkg/api/legacyscheme"

apiequality "k8s.io/apimachinery/pkg/api/equality"
apierrors "k8s.io/apimachinery/pkg/api/errors"
Expand All @@ -69,6 +70,7 @@ import (
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
"k8s.io/apiserver/pkg/endpoints/metrics"
"k8s.io/apiserver/pkg/endpoints/openapi"
apirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/registry/generic"
Expand Down Expand Up @@ -257,6 +259,10 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}

crdName := requestInfo.Resource + "." + requestInfo.APIGroup
// HACK: support the case when we add core resources through CRDs (KCP scenario)
if requestInfo.APIGroup == "" {
crdName = crdName + "core"
}
crd, err := r.crdLister.Get(crdName)
if apierrors.IsNotFound(err) {
if !r.hasSynced() {
Expand Down Expand Up @@ -331,6 +337,9 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
string(types.JSONPatchType),
string(types.MergePatchType),
}
if legacyscheme.Scheme.IsGroupRegistered(requestInfo.APIGroup) {
supportedTypes = append(supportedTypes, string(types.StrategicMergePatchType))
}
if utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) {
supportedTypes = append(supportedTypes, string(types.ApplyPatchType))
}
Expand Down Expand Up @@ -764,12 +773,16 @@ func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string) (*crd
table,
)

selfLinkPrefixPrefix := path.Join("apis", crd.Spec.Group, v.Name)
if crd.Spec.Group == "" {
selfLinkPrefixPrefix = path.Join("api", v.Name)
}
selfLinkPrefix := ""
switch crd.Spec.Scope {
case apiextensionsv1.ClusterScoped:
selfLinkPrefix = "/" + path.Join("apis", crd.Spec.Group, v.Name) + "/" + crd.Status.AcceptedNames.Plural + "/"
selfLinkPrefix = "/" + selfLinkPrefixPrefix + "/" + crd.Status.AcceptedNames.Plural + "/"
case apiextensionsv1.NamespaceScoped:
selfLinkPrefix = "/" + path.Join("apis", crd.Spec.Group, v.Name, "namespaces") + "/"
selfLinkPrefix = "/" + selfLinkPrefixPrefix + "/namespaces/"
}

clusterScoped := crd.Spec.Scope == apiextensionsv1.ClusterScoped
Expand All @@ -791,6 +804,10 @@ func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string) (*crd
standardSerializers = append(standardSerializers, s)
}

modelsByGKV, err := openapi.GetModelsByGKV(openAPIModels)
if err != nil {
klog.V(2).Infof("The CRD cannot gather openapi models by GKV: %v", err)
}
requestScopes[v.Name] = &handlers.RequestScope{
Namer: handlers.ContextBasedNaming{
SelfLinker: meta.NewAccessor(),
Expand Down Expand Up @@ -822,6 +839,8 @@ func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string) (*crd
Authorizer: r.authorizer,

MaxRequestBodyBytes: r.maxRequestBodyBytes,

OpenapiModels: modelsByGKV,
}
if utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) {
reqScope := *requestScopes[v.Name]
Expand Down Expand Up @@ -1247,7 +1266,7 @@ func buildOpenAPIModelsForApply(staticOpenAPISpec *spec.Swagger, crd *apiextensi

specs := []*spec.Swagger{}
for _, v := range crd.Spec.Versions {
s, err := builder.BuildSwagger(crd, v.Name, builder.Options{V2: false, StripDefaults: true, StripValueValidation: true, StripNullable: true, AllowNonStructural: true})
s, err := builder.BuildSwagger(crd, v.Name, builder.Options{V2: false, StripDefaults: true, StripValueValidation: true, StripNullable: true, AllowNonStructural: false})
if err != nil {
return nil, err
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,22 @@ func (x *Extensions) toGoOpenAPI(ret *spec.Schema) {
}
if len(x.XListMapKeys) > 0 {
ret.VendorExtensible.AddExtension("x-kubernetes-list-map-keys", x.XListMapKeys)
ret.VendorExtensible.AddExtension("x-kubernetes-patch-merge-key", x.XListMapKeys[0])
}
if x.XListType != nil {
ret.VendorExtensible.AddExtension("x-kubernetes-list-type", *x.XListType)
if *x.XListType == "map" || *x.XListType == "set" {
ret.VendorExtensible.AddExtension("x-kubernetes-patch-strategy", "merge")
}
if *x.XListType == "atomic" {
ret.VendorExtensible.AddExtension("x-kubernetes-patch-strategy", "replace")
}
}
if x.XMapType != nil {
ret.VendorExtensible.AddExtension("x-kubernetes-map-type", *x.XMapType)
if *x.XMapType == "atomic" {
ret.VendorExtensible.AddExtension("x-kubernetes-patch-strategy", "replace")
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import (
openapibuilder "k8s.io/kube-openapi/pkg/builder"
"k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/util"
"k8s.io/kubernetes/pkg/api/legacyscheme"
)

const (
Expand Down Expand Up @@ -104,6 +105,9 @@ func BuildSwagger(crd *apiextensionsv1.CustomResourceDefinition, version string,
if opts.AllowNonStructural || len(structuralschema.ValidateStructural(nil, ss)) == 0 {
schema = ss

// This adds ValueValidation fields (anyOf, allOf) which may be stripped below if opts.StripValueValidation is true
schema = schema.Unfold()

if opts.StripDefaults {
schema = schema.StripDefaults()
}
Expand All @@ -113,8 +117,6 @@ func BuildSwagger(crd *apiextensionsv1.CustomResourceDefinition, version string,
if opts.StripNullable {
schema = schema.StripNullable()
}

schema = schema.Unfold()
}
}
}
Expand Down Expand Up @@ -142,11 +144,17 @@ func BuildSwagger(crd *apiextensionsv1.CustomResourceDefinition, version string,
scale := &v1.Scale{}

routes := make([]*restful.RouteBuilder, 0)
root := fmt.Sprintf("/apis/%s/%s/%s", b.group, b.version, b.plural)
// HACK: support the case when we add core resources through CRDs (KCP scenario)
rootPrefix := fmt.Sprintf("/apis/%s/%s", b.group, b.version)
if b.group == "" {
rootPrefix = fmt.Sprintf("/api/%s", b.version)
}

root := fmt.Sprintf("%s/%s", rootPrefix, b.plural)

if b.namespaced {
routes = append(routes, b.buildRoute(root, "", "GET", "list", "list", sampleList).Operation("list"+b.kind+"ForAllNamespaces"))
root = fmt.Sprintf("/apis/%s/%s/namespaces/{namespace}/%s", b.group, b.version, b.plural)
root = fmt.Sprintf("%s/namespaces/{namespace}/%s", rootPrefix, b.plural)
}
routes = append(routes, b.buildRoute(root, "", "GET", "list", "list", sampleList))
routes = append(routes, b.buildRoute(root, "", "POST", "post", "create", sample).Reads(sample))
Expand Down Expand Up @@ -195,9 +203,21 @@ type CRDCanonicalTypeNamer struct {
kind string
}

// HACK: support the case when we add core or other legacy scheme resources through CRDs (KCP scenario)
func packagePrefix(group string) string {
if !strings.Contains(group, ".") &&
legacyscheme.Scheme.IsGroupRegistered(group) {
if group == "" {
group = "core"
}
return "k8s.io/api/" + group
}
return group
}

// OpenAPICanonicalTypeName returns canonical type name for given CRD
func (c *CRDCanonicalTypeNamer) OpenAPICanonicalTypeName() string {
return fmt.Sprintf("%s/%s.%s", c.group, c.version, c.kind)
return fmt.Sprintf("%s/%s.%s", packagePrefix(c.group), c.version, c.kind)
}

// builder contains validation schema and basic naming information for a CRD in
Expand Down Expand Up @@ -452,7 +472,7 @@ func addTypeMetaProperties(s *spec.Schema) {

// buildListSchema builds the list kind schema for the CRD
func (b *builder) buildListSchema() *spec.Schema {
name := definitionPrefix + util.ToRESTFriendlyName(fmt.Sprintf("%s/%s/%s", b.group, b.version, b.kind))
name := definitionPrefix + util.ToRESTFriendlyName(fmt.Sprintf("%s/%s/%s", packagePrefix(b.group), b.version, b.kind))
doc := fmt.Sprintf("List of %s. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md", b.plural)
s := new(spec.Schema).WithDescription(fmt.Sprintf("%s is a list of %s", b.listKind, b.kind)).
WithRequired("items").
Expand Down Expand Up @@ -489,14 +509,19 @@ func (b *builder) getOpenAPIConfig() *common.Config {
GetOperationIDAndTags: openapi.GetOperationIDAndTags,
GetDefinitionName: func(name string) (string, spec.Extensions) {
buildDefinitions.Do(buildDefinitionsFunc)
// HACK: support the case when we add core or other legacy scheme resources through CRDs (KCP scenario)
parts := strings.Split(name, "/")
if len(parts) == 2 {
name = packagePrefix(parts[0])
}
return namer.GetDefinitionName(name)
},
GetDefinitions: func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
def := generatedopenapi.GetOpenAPIDefinitions(ref)
def[fmt.Sprintf("%s/%s.%s", b.group, b.version, b.kind)] = common.OpenAPIDefinition{
def[fmt.Sprintf("%s/%s.%s", packagePrefix(b.group), b.version, b.kind)] = common.OpenAPIDefinition{
Schema: *b.schema,
}
def[fmt.Sprintf("%s/%s.%s", b.group, b.version, b.listKind)] = common.OpenAPIDefinition{
def[fmt.Sprintf("%s/%s.%s", packagePrefix(b.group), b.version, b.listKind)] = common.OpenAPIDefinition{
Schema: *b.listSchema,
}
return def
Expand All @@ -505,14 +530,16 @@ func (b *builder) getOpenAPIConfig() *common.Config {
}

func newBuilder(crd *apiextensionsv1.CustomResourceDefinition, version string, schema *structuralschema.Structural, v2 bool) *builder {
group := crd.Spec.Group
// HACK: support the case when we add core resources through CRDs (KCP scenario)
b := &builder{
schema: &spec.Schema{
SchemaProps: spec.SchemaProps{Type: []string{"object"}},
},
listSchema: &spec.Schema{},
ws: &restful.WebService{},

group: crd.Spec.Group,
group: group,
version: version,
kind: crd.Spec.Names.Kind,
listKind: crd.Spec.Names.ListKind,
Expand Down
Loading