mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Aggregation: Support apidiscovery.k8s.io/v2 (#91938)
This commit is contained in:
parent
675a58b680
commit
d6ce6aaf44
@ -16,7 +16,6 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/aggregator/apis/aggregation/v0alpha1"
|
||||
v0alpha1helper "github.com/grafana/grafana/pkg/aggregator/apis/aggregation/v0alpha1/helper"
|
||||
"github.com/grafana/grafana/pkg/aggregator/apiserver/scheme"
|
||||
clientset "github.com/grafana/grafana/pkg/aggregator/generated/clientset/versioned"
|
||||
informers "github.com/grafana/grafana/pkg/aggregator/generated/informers/externalversions"
|
||||
dataplaneservicerest "github.com/grafana/grafana/pkg/aggregator/registry/dataplaneservice/rest"
|
||||
@ -83,10 +82,7 @@ func (c completedConfig) NewWithDelegate(delegationTarget genericapiserver.Deleg
|
||||
return nil, err
|
||||
}
|
||||
|
||||
discoveryHandler := &apisProxyHandler{
|
||||
delegationTarget: delegationTarget,
|
||||
codecs: scheme.Codecs,
|
||||
}
|
||||
discoveryHandler := newApisProxyHandler(delegationTarget.UnprotectedHandler())
|
||||
genericServer.Handler.GoRestfulContainer.Filter(discoveryHandler.handle)
|
||||
|
||||
dataplaneServiceRegistrationControllerInitiated := make(chan struct{})
|
||||
|
@ -6,36 +6,44 @@ import (
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/emicklei/go-restful/v3"
|
||||
apidiscoveryv2 "k8s.io/api/apidiscovery/v2"
|
||||
apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apiserver/pkg/endpoints/discovery/aggregated"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
|
||||
aggregationv0alpha1api "github.com/grafana/grafana/pkg/aggregator/apis/aggregation/v0alpha1"
|
||||
)
|
||||
|
||||
var (
|
||||
discoveryGroup = metav1.APIGroup{
|
||||
Name: aggregationv0alpha1api.SchemeGroupVersion.Group,
|
||||
Versions: []metav1.GroupVersionForDiscovery{
|
||||
{
|
||||
GroupVersion: aggregationv0alpha1api.SchemeGroupVersion.String(),
|
||||
Version: aggregationv0alpha1api.SchemeGroupVersion.Version,
|
||||
},
|
||||
},
|
||||
PreferredVersion: metav1.GroupVersionForDiscovery{
|
||||
GroupVersion: aggregationv0alpha1api.SchemeGroupVersion.String(),
|
||||
Version: aggregationv0alpha1api.SchemeGroupVersion.Version,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// apisProxyHandler serves the `/apis` endpoint.
|
||||
type apisProxyHandler struct {
|
||||
delegationTarget genericapiserver.DelegationTarget
|
||||
codecs serializer.CodecFactory
|
||||
delegate http.Handler
|
||||
codecs serializer.CodecFactory
|
||||
}
|
||||
|
||||
func newApisProxyHandler(delegate http.Handler) *apisProxyHandler {
|
||||
scheme := runtime.NewScheme()
|
||||
metav1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"})
|
||||
|
||||
// TODO: keep the generic API server from wanting this
|
||||
unversioned := schema.GroupVersion{Group: "", Version: "v1"}
|
||||
scheme.AddUnversionedTypes(unversioned,
|
||||
&metav1.Status{},
|
||||
&metav1.APIVersions{},
|
||||
&metav1.APIGroupList{},
|
||||
&metav1.APIGroup{},
|
||||
&metav1.APIResourceList{},
|
||||
)
|
||||
utilruntime.Must(apidiscoveryv2.AddToScheme(scheme))
|
||||
utilruntime.Must(apidiscoveryv2beta1.AddToScheme(scheme))
|
||||
codecs := serializer.NewCodecFactory(scheme)
|
||||
return &apisProxyHandler{
|
||||
delegate: delegate,
|
||||
codecs: codecs,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *apisProxyHandler) handle(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
|
||||
@ -43,26 +51,89 @@ func (a *apisProxyHandler) handle(req *restful.Request, resp *restful.Response,
|
||||
chain.ProcessFilter(req, resp)
|
||||
return
|
||||
}
|
||||
|
||||
discoveryGroupList := &metav1.APIGroupList{
|
||||
Groups: []metav1.APIGroup{discoveryGroup},
|
||||
}
|
||||
|
||||
rw := httptest.NewRecorder()
|
||||
a.delegationTarget.UnprotectedHandler().ServeHTTP(rw, req.Request)
|
||||
|
||||
if rw.Code != http.StatusOK {
|
||||
http.Error(resp.ResponseWriter, rw.Body.String(), rw.Code)
|
||||
return
|
||||
}
|
||||
|
||||
proxiedGroups := metav1.APIGroupList{}
|
||||
if err := json.Unmarshal(rw.Body.Bytes(), &proxiedGroups); err != nil {
|
||||
http.Error(resp.ResponseWriter, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
discoveryGroupList.Groups = append(discoveryGroupList.Groups, proxiedGroups.Groups...)
|
||||
|
||||
responsewriters.WriteObjectNegotiated(a.codecs, negotiation.DefaultEndpointRestrictions, schema.GroupVersion{}, resp.ResponseWriter, req.Request, http.StatusOK, discoveryGroupList, false)
|
||||
apisHandlerWithAggregationSupport := aggregated.WrapAggregatedDiscoveryToHandler(a.v1handler(chain), a.v2handler(chain))
|
||||
apisHandlerWithAggregationSupport.ServeHTTP(resp.ResponseWriter, req.Request)
|
||||
}
|
||||
|
||||
func (a *apisProxyHandler) v2handler(chain *restful.FilterChain) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
clonedReq := req.Clone(req.Context())
|
||||
newReq := restful.NewRequest(clonedReq)
|
||||
rw := httptest.NewRecorder()
|
||||
newRes := restful.NewResponse(rw)
|
||||
|
||||
chain.ProcessFilter(newReq, newRes)
|
||||
if rw.Code != http.StatusOK {
|
||||
http.Error(w, rw.Body.String(), rw.Code)
|
||||
return
|
||||
}
|
||||
|
||||
v2Discovery := apidiscoveryv2.APIGroupDiscoveryList{}
|
||||
if err := json.Unmarshal(rw.Body.Bytes(), &v2Discovery); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
clonedReq = req.Clone(req.Context())
|
||||
rw = httptest.NewRecorder()
|
||||
|
||||
a.delegate.ServeHTTP(rw, clonedReq)
|
||||
if rw.Code != http.StatusOK {
|
||||
http.Error(w, rw.Body.String(), rw.Code)
|
||||
return
|
||||
}
|
||||
|
||||
proxiedDiscovery := apidiscoveryv2.APIGroupDiscoveryList{}
|
||||
if err := json.Unmarshal(rw.Body.Bytes(), &proxiedDiscovery); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
v2Discovery.Items = append(v2Discovery.Items, proxiedDiscovery.Items...)
|
||||
responsewriters.WriteObjectNegotiated(a.codecs, aggregated.DiscoveryEndpointRestrictions, schema.GroupVersion{
|
||||
Group: apidiscoveryv2.SchemeGroupVersion.Group,
|
||||
Version: apidiscoveryv2.SchemeGroupVersion.Version,
|
||||
}, w, req, http.StatusOK, &v2Discovery, true)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *apisProxyHandler) v1handler(chain *restful.FilterChain) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
clonedReq := req.Clone(req.Context())
|
||||
clonedReq.Header.Set("Accept", "application/json")
|
||||
newReq := restful.NewRequest(clonedReq)
|
||||
rw := httptest.NewRecorder()
|
||||
newRes := restful.NewResponse(rw)
|
||||
|
||||
chain.ProcessFilter(newReq, newRes)
|
||||
if rw.Code != http.StatusOK {
|
||||
http.Error(w, rw.Body.String(), rw.Code)
|
||||
return
|
||||
}
|
||||
|
||||
discoveryGroupList := metav1.APIGroupList{}
|
||||
if err := json.Unmarshal(rw.Body.Bytes(), &discoveryGroupList); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
clonedReq = req.Clone(req.Context())
|
||||
clonedReq.Header.Set("Accept", "application/json")
|
||||
rw = httptest.NewRecorder()
|
||||
|
||||
a.delegate.ServeHTTP(rw, clonedReq)
|
||||
if rw.Code != http.StatusOK {
|
||||
http.Error(w, rw.Body.String(), rw.Code)
|
||||
return
|
||||
}
|
||||
|
||||
proxiedGroups := metav1.APIGroupList{}
|
||||
if err := json.Unmarshal(rw.Body.Bytes(), &proxiedGroups); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
discoveryGroupList.Groups = append(discoveryGroupList.Groups, proxiedGroups.Groups...)
|
||||
responsewriters.WriteObjectNegotiated(a.codecs, negotiation.DefaultEndpointRestrictions, schema.GroupVersion{}, w, req, http.StatusOK, &discoveryGroupList, false)
|
||||
}
|
||||
}
|
||||
|
233
pkg/aggregator/apiserver/handler_apis_test.go
Normal file
233
pkg/aggregator/apiserver/handler_apis_test.go
Normal file
@ -0,0 +1,233 @@
|
||||
package apiserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/emicklei/go-restful/v3"
|
||||
"github.com/stretchr/testify/require"
|
||||
v2 "k8s.io/api/apidiscovery/v2"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/endpoints/discovery/aggregated"
|
||||
|
||||
aggregationv0alpha1api "github.com/grafana/grafana/pkg/aggregator/apis/aggregation/v0alpha1"
|
||||
)
|
||||
|
||||
func TestApisProxyHandler_Handle(t *testing.T) {
|
||||
v1Discovery := metav1.APIGroup{
|
||||
Name: aggregationv0alpha1api.SchemeGroupVersion.Group,
|
||||
Versions: []metav1.GroupVersionForDiscovery{
|
||||
{
|
||||
GroupVersion: aggregationv0alpha1api.SchemeGroupVersion.String(),
|
||||
Version: aggregationv0alpha1api.SchemeGroupVersion.Version,
|
||||
},
|
||||
},
|
||||
PreferredVersion: metav1.GroupVersionForDiscovery{
|
||||
GroupVersion: aggregationv0alpha1api.SchemeGroupVersion.String(),
|
||||
Version: aggregationv0alpha1api.SchemeGroupVersion.Version,
|
||||
},
|
||||
}
|
||||
fakeGroup := metav1.APIGroup{
|
||||
Name: "foo.example.com",
|
||||
Versions: []metav1.GroupVersionForDiscovery{
|
||||
{
|
||||
GroupVersion: "foo.example.com/v1",
|
||||
Version: "v1",
|
||||
},
|
||||
},
|
||||
PreferredVersion: metav1.GroupVersionForDiscovery{
|
||||
GroupVersion: "foo.example.com/v1",
|
||||
Version: "v1",
|
||||
},
|
||||
}
|
||||
fakeGroupList := metav1.APIGroupList{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "APIGroupList",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Groups: []metav1.APIGroup{fakeGroup},
|
||||
}
|
||||
|
||||
rm := aggregated.NewResourceManager("apis")
|
||||
v2GroupVersion := v2.APIVersionDiscovery{
|
||||
Version: "v0alpha1",
|
||||
Resources: []v2.APIResourceDiscovery{
|
||||
{
|
||||
Resource: "dataplaneservices",
|
||||
ResponseKind: &metav1.GroupVersionKind{
|
||||
Group: "aggregation.grafana.app",
|
||||
Version: "v0alpha1",
|
||||
Kind: "DataPlaneService",
|
||||
},
|
||||
Scope: v2.ScopeCluster,
|
||||
SingularResource: "dataplaneservice",
|
||||
Verbs: []string{"list", "get", "create", "update", "delete", "patch", "watch"},
|
||||
},
|
||||
},
|
||||
Freshness: v2.DiscoveryFreshnessCurrent,
|
||||
}
|
||||
rm.AddGroupVersion("aggregation.grafana.app", v2GroupVersion)
|
||||
|
||||
v2FakeGroupVersion := v2.APIVersionDiscovery{
|
||||
Version: "v1",
|
||||
Resources: []v2.APIResourceDiscovery{
|
||||
{
|
||||
Resource: "foos",
|
||||
ResponseKind: &metav1.GroupVersionKind{
|
||||
Group: "foo.example.com",
|
||||
Version: "v1",
|
||||
Kind: "Foo",
|
||||
},
|
||||
Scope: v2.ScopeNamespace,
|
||||
SingularResource: "foo",
|
||||
Verbs: []string{"list"},
|
||||
},
|
||||
},
|
||||
Freshness: v2.DiscoveryFreshnessStale,
|
||||
}
|
||||
|
||||
fakeRm := aggregated.NewResourceManager("apis")
|
||||
rm.AddGroupVersion("foo.example.com", v2FakeGroupVersion)
|
||||
|
||||
delegationTarget := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Header.Get("Accept") {
|
||||
case "application/json":
|
||||
err := json.NewEncoder(w).Encode(fakeGroupList)
|
||||
require.NoError(t, err)
|
||||
return
|
||||
case "application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList,application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList,application/json":
|
||||
fakeRm.ServeHTTP(w, r)
|
||||
return
|
||||
default:
|
||||
fmt.Printf("Accept: %s\n", r.Header.Get("Accept"))
|
||||
http.Error(w, "Bad request", http.StatusBadRequest)
|
||||
}
|
||||
})
|
||||
|
||||
handler := newApisProxyHandler(delegationTarget)
|
||||
|
||||
chain := &restful.FilterChain{
|
||||
Target: func(req *restful.Request, resp *restful.Response) {
|
||||
switch req.Request.URL.Path {
|
||||
case "/apis/aggregation.grafana.app/v0alpha1":
|
||||
_, err := resp.ResponseWriter.Write([]byte("v0alpha1"))
|
||||
require.NoError(t, err)
|
||||
return
|
||||
case "/apis":
|
||||
switch req.Request.Header.Get("Accept") {
|
||||
case "application/json":
|
||||
err := json.NewEncoder(resp.ResponseWriter).Encode(&metav1.APIGroupList{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "APIGroupList",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Groups: []metav1.APIGroup{v1Discovery},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return
|
||||
case "application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList,application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList,application/json":
|
||||
rm.ServeHTTP(resp.ResponseWriter, req.Request)
|
||||
return
|
||||
default:
|
||||
fmt.Printf("Accept: %s\n", req.Request.Header.Get("Accept"))
|
||||
http.Error(resp.ResponseWriter, "Bad request", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
default:
|
||||
http.Error(resp.ResponseWriter, "not found", http.StatusNotFound)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("should return the list of API groups in v1 format", func(t *testing.T) {
|
||||
req := &restful.Request{
|
||||
Request: httptest.NewRequest(http.MethodGet, "/apis", nil),
|
||||
}
|
||||
req.Request.Header.Set("Accept", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
resp := &restful.Response{
|
||||
ResponseWriter: rec,
|
||||
}
|
||||
|
||||
handler.handle(req, resp, chain)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode())
|
||||
require.NoError(t, resp.Error())
|
||||
require.Equal(t, "application/json", resp.Header().Get("Content-Type"))
|
||||
|
||||
expectedGroupList := metav1.APIGroupList{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "APIGroupList",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
Groups: append([]metav1.APIGroup{v1Discovery}, fakeGroupList.Groups...),
|
||||
}
|
||||
|
||||
actualGroupList := metav1.APIGroupList{}
|
||||
err := json.NewDecoder(rec.Body).Decode(&actualGroupList)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, expectedGroupList, actualGroupList)
|
||||
})
|
||||
|
||||
t.Run("should return the list of API groups in v2 format", func(t *testing.T) {
|
||||
req := &restful.Request{
|
||||
Request: httptest.NewRequest(http.MethodGet, "/apis", nil),
|
||||
}
|
||||
req.Request.Header.Set("Accept", "application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList,application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList,application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
resp := &restful.Response{
|
||||
ResponseWriter: rec,
|
||||
}
|
||||
|
||||
handler.handle(req, resp, chain)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode())
|
||||
require.Equal(t, "application/json;g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList", resp.Header().Get("Content-Type"))
|
||||
|
||||
expected := v2.APIGroupDiscoveryList{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "APIGroupDiscoveryList",
|
||||
APIVersion: v2.SchemeGroupVersion.String(),
|
||||
},
|
||||
ListMeta: metav1.ListMeta{},
|
||||
Items: []v2.APIGroupDiscovery{
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "aggregation.grafana.app",
|
||||
},
|
||||
Versions: []v2.APIVersionDiscovery{v2GroupVersion},
|
||||
},
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo.example.com",
|
||||
},
|
||||
Versions: []v2.APIVersionDiscovery{v2FakeGroupVersion},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
actual := v2.APIGroupDiscoveryList{}
|
||||
err := json.NewDecoder(rec.Body).Decode(&actual)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, expected, actual)
|
||||
})
|
||||
|
||||
t.Run("should handle the request if the path is not /apis", func(t *testing.T) {
|
||||
req := &restful.Request{
|
||||
Request: httptest.NewRequest(http.MethodGet, "/apis/aggregation.grafana.app/v0alpha1", nil),
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
resp := &restful.Response{
|
||||
ResponseWriter: rec,
|
||||
}
|
||||
|
||||
handler.handle(req, resp, chain)
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, "v0alpha1", rec.Body.String())
|
||||
})
|
||||
}
|
@ -9,6 +9,7 @@ require (
|
||||
github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435
|
||||
github.com/stretchr/testify v1.9.0
|
||||
go.opentelemetry.io/otel v1.28.0
|
||||
k8s.io/api v0.31.0
|
||||
k8s.io/apimachinery v0.31.0
|
||||
k8s.io/apiserver v0.31.0
|
||||
k8s.io/client-go v0.31.0
|
||||
@ -149,7 +150,6 @@ require (
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/api v0.31.0 // indirect
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
|
Loading…
Reference in New Issue
Block a user