Skip to content

Commit

Permalink
Implement initial OpenAPI v3 support for virtual workspaces
Browse files Browse the repository at this point in the history
Signed-off-by: Marko Mudrinić <mudrinic.mare@gmail.com>
  • Loading branch information
xmudrii committed Feb 5, 2025
1 parent 37065ca commit 4d4d225
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 135 deletions.
4 changes: 0 additions & 4 deletions pkg/virtual/framework/dynamic/apidefinition/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/handlers"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/kube-openapi/pkg/spec3"

dynamiccontext "github.com/kcp-dev/kcp/pkg/virtual/framework/dynamic/context"
apisv1alpha1 "github.com/kcp-dev/kcp/sdk/apis/apis/v1alpha1"
Expand All @@ -35,9 +34,6 @@ type APIDefinition interface {
// GetAPIResourceSchema returns the API schema this definition serves.
GetAPIResourceSchema() *apisv1alpha1.APIResourceSchema

// GetOpenAPIV3Schema fetches OpenAPI V3 Schema
GetOpenAPIV3Spec() *spec3.OpenAPI

// GetClusterName provides the name of the logical cluster where the resource specification comes from.
GetClusterName() logicalcluster.Name

Expand Down
10 changes: 9 additions & 1 deletion pkg/virtual/framework/dynamic/apiserver/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ import (
"k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/endpoints/discovery"
openapinamer "k8s.io/apiserver/pkg/endpoints/openapi"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/util/openapi"
generatedopenapi "k8s.io/kubernetes/pkg/generated/openapi"

virtualcontext "github.com/kcp-dev/kcp/pkg/virtual/framework/context"
"github.com/kcp-dev/kcp/pkg/virtual/framework/dynamic/apidefinition"
Expand Down Expand Up @@ -177,7 +180,12 @@ func (c completedConfig) New(virtualWorkspaceName string, delegationTarget gener

s.GenericAPIServer.Handler.GoRestfulContainer.Add(discovery.NewLegacyRootAPIHandler(c.GenericConfig.DiscoveryAddresses, s.GenericAPIServer.Serializer, "/api").WebService())

openAPIHandler := newOpenAPIHandler(s.APISetRetriever, delegateHandler)
getOpenAPIDefinitions := openapi.GetOpenAPIDefinitionsWithoutDisabledFeatures(generatedopenapi.GetOpenAPIDefinitions)
namer := openapinamer.NewDefinitionNamer(scheme)
openapiv3Config := genericapiserver.DefaultOpenAPIV3Config(getOpenAPIDefinitions, namer)
openapiv3Config.Info.Title = "Kubernetes"

openAPIHandler := newOpenAPIHandler(s.APISetRetriever, s.GenericAPIServer.Handler.GoRestfulContainer, openapiv3Config, delegateHandler)

s.GenericAPIServer.Handler.NonGoRestfulMux.Handle("/apis", crdHandler)
s.GenericAPIServer.Handler.NonGoRestfulMux.HandlePrefix("/apis/", crdHandler)
Expand Down
85 changes: 0 additions & 85 deletions pkg/virtual/framework/dynamic/apiserver/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ limitations under the License.
package apiserver

import (
"context"
"fmt"
"net/http"
"sort"
Expand All @@ -34,7 +33,6 @@ import (
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/kube-openapi/pkg/handler3"
"k8s.io/kubernetes/pkg/controlplane/apiserver/miniaggregator"

"github.com/kcp-dev/kcp/pkg/virtual/framework/dynamic/apidefinition"
Expand Down Expand Up @@ -332,86 +330,3 @@ func sortGroupDiscoveryByKubeAwareVersion(gd []metav1.GroupVersionForDiscovery)
return version.CompareKubeAwareVersionStrings(gd[i].Version, gd[j].Version) > 0
})
}

type openAPIHandler struct {
apiSetRetriever apidefinition.APIDefinitionSetGetter
delegate http.Handler

openAPIV3Service *handler3.OpenAPIService
}

func newOpenAPIHandler(apiSetRetriever apidefinition.APIDefinitionSetGetter, delegate http.Handler) *openAPIHandler {
openAPIV3Service := handler3.NewOpenAPIService()

return &openAPIHandler{
apiSetRetriever: apiSetRetriever,
delegate: delegate,

openAPIV3Service: openAPIV3Service,
}
}

func (h *openAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// handle /openapi -> do nothing
// handle /openapi/v2 -> do nothing for now. TODO: Implement. Actually, let OpenAPIV2 die. Ref: https://github.com/kcp-dev/kcp/pull/3059#discussion_r1424317153
// handle /openapi/v3 -> discovery endpoint
// handle /openapi/v3/apis/<group>/<version> -> serve OpenAPIV3Schema for <group>/<version>

pathParts := splitPath(r.URL.Path)

// if request doesn't start with /openapi return
// this check is a safety measure
if len(pathParts) == 1 && pathParts[0] != "openapi" {
h.delegate.ServeHTTP(w, r)
return
}

if len(pathParts) == 1 && pathParts[0] == "openapi" {
return
}

if len(pathParts) == 2 && pathParts[1] == "v2" { // handle /openapi/v2
// TODO: Implement OpenAPI V2 handler. Actually, let OpenAPIV2 die. Ref: https://github.com/kcp-dev/kcp/pull/3059#discussion_r1424317153
return
} else if len(pathParts) == 2 && pathParts[1] == "v3" { // handle /openapi/v3
if err := h.fetchAndUpdate(r.Context(), ""); err != nil {
responsewriters.ErrorNegotiated(
apierrors.NewInternalError(fmt.Errorf("unable to set: %w", err)),
errorCodecs, schema.GroupVersion{},
w, r)
}

h.openAPIV3Service.HandleDiscovery(w, r)
return
} else if len(pathParts) == 5 && pathParts[1] == "v3" && pathParts[2] == "apis" {
requestedGroup := pathParts[3]
if err := h.fetchAndUpdate(r.Context(), requestedGroup); err != nil {
responsewriters.ErrorNegotiated(
apierrors.NewInternalError(fmt.Errorf("unable to set: %w", err)),
errorCodecs, schema.GroupVersion{},
w, r)
}

h.openAPIV3Service.HandleGroupVersion(w, r)
} else {
h.delegate.ServeHTTP(w, r)
return
}
}

func (h *openAPIHandler) fetchAndUpdate(ctx context.Context, group string) error {
apiDomainKey := dyncamiccontext.APIDomainKeyFrom(ctx)
apiSet, _, err := h.apiSetRetriever.GetAPIDefinitionSet(ctx, apiDomainKey)
if err != nil {
return err
}

for gvr, apiDefinition := range apiSet {
if group == "" || (group != "" && group == gvr.Group) {
spec := apiDefinition.GetOpenAPIV3Spec()
h.openAPIV3Service.UpdateGroupVersion(gvr.Group, spec)
}
}

return nil
}
5 changes: 0 additions & 5 deletions pkg/virtual/framework/dynamic/apiserver/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import (
apirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
utilopenapi "k8s.io/apiserver/pkg/util/openapi"
"k8s.io/kube-openapi/pkg/spec3"
"sigs.k8s.io/yaml"

"github.com/kcp-dev/kcp/pkg/virtual/framework/dynamic/apidefinition"
Expand All @@ -59,7 +58,6 @@ type mockedAPIDefinition struct {
apiResourceSchema *apisv1alpha1.APIResourceSchema
store rest.Storage
subresourcesStores map[string]rest.Storage
openAPIV3Spec *spec3.OpenAPI
}

var _ apidefinition.APIDefinition = (*mockedAPIDefinition)(nil)
Expand All @@ -73,9 +71,6 @@ func (apiDef *mockedAPIDefinition) GetClusterName() logicalcluster.Name {
func (apiDef *mockedAPIDefinition) GetStorage() rest.Storage {
return apiDef.store
}
func (apiDef *mockedAPIDefinition) GetOpenAPIV3Spec() *spec3.OpenAPI {
return apiDef.openAPIV3Spec
}
func (apiDef *mockedAPIDefinition) GetSubResourceStorage(subresource string) rest.Storage {
return apiDef.subresourcesStores[subresource]
}
Expand Down
Loading

0 comments on commit 4d4d225

Please sign in to comment.