mirror of https://github.com/grafana/grafana
K8s: Refactor config/options for aggregation (#81739)
parent
7a17963ab9
commit
67b6be5515
@ -1,628 +0,0 @@ |
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Provenance-includes-location: https://github.com/kubernetes/kubernetes/blob/master/cmd/kube-apiserver/app/aggregator.go
|
||||
// Provenance-includes-license: Apache-2.0
|
||||
// Provenance-includes-copyright: The Kubernetes Authors.
|
||||
// Provenance-includes-location: https://github.com/kubernetes/kubernetes/blob/master/cmd/kube-apiserver/app/server.go
|
||||
// Provenance-includes-license: Apache-2.0
|
||||
// Provenance-includes-copyright: The Kubernetes Authors.
|
||||
// Provenance-includes-location: https://github.com/kubernetes/kubernetes/blob/master/pkg/controlplane/apiserver/apiextensions.go
|
||||
// Provenance-includes-license: Apache-2.0
|
||||
// Provenance-includes-copyright: The Kubernetes Authors.
|
||||
|
||||
package aggregator |
||||
|
||||
import ( |
||||
"crypto/tls" |
||||
"fmt" |
||||
"io" |
||||
"net" |
||||
"net/http" |
||||
"path" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
servicev0alpha1 "github.com/grafana/grafana/pkg/apis/service/v0alpha1" |
||||
serviceclientset "github.com/grafana/grafana/pkg/generated/clientset/versioned" |
||||
informersv0alpha1 "github.com/grafana/grafana/pkg/generated/informers/externalversions" |
||||
"github.com/grafana/grafana/pkg/registry/apis/service" |
||||
grafanaAPIServer "github.com/grafana/grafana/pkg/services/grafana-apiserver" |
||||
filestorage "github.com/grafana/grafana/pkg/services/grafana-apiserver/storage/file" |
||||
|
||||
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" |
||||
apiextensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver" |
||||
apiextensionsinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions" |
||||
apiextensionsopenapi "k8s.io/apiextensions-apiserver/pkg/generated/openapi" |
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" |
||||
"k8s.io/apimachinery/pkg/runtime" |
||||
"k8s.io/apimachinery/pkg/runtime/schema" |
||||
"k8s.io/apimachinery/pkg/runtime/serializer" |
||||
utilnet "k8s.io/apimachinery/pkg/util/net" |
||||
"k8s.io/apimachinery/pkg/util/sets" |
||||
"k8s.io/apiserver/pkg/endpoints/discovery/aggregated" |
||||
openapinamer "k8s.io/apiserver/pkg/endpoints/openapi" |
||||
genericfeatures "k8s.io/apiserver/pkg/features" |
||||
genericapiserver "k8s.io/apiserver/pkg/server" |
||||
"k8s.io/apiserver/pkg/server/healthz" |
||||
"k8s.io/apiserver/pkg/server/options" |
||||
"k8s.io/apiserver/pkg/server/resourceconfig" |
||||
utilfeature "k8s.io/apiserver/pkg/util/feature" |
||||
"k8s.io/apiserver/pkg/util/openapi" |
||||
"k8s.io/client-go/informers" |
||||
"k8s.io/client-go/kubernetes/fake" |
||||
"k8s.io/client-go/tools/cache" |
||||
"k8s.io/klog/v2" |
||||
v1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" |
||||
v1helper "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1/helper" |
||||
apiregistrationv1beta1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1" |
||||
aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver" |
||||
aggregatorscheme "k8s.io/kube-aggregator/pkg/apiserver/scheme" |
||||
apiregistrationclientset "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset" |
||||
apiregistrationclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/typed/apiregistration/v1" |
||||
apiregistrationInformers "k8s.io/kube-aggregator/pkg/client/informers/externalversions/apiregistration/v1" |
||||
"k8s.io/kube-aggregator/pkg/controllers/autoregister" |
||||
apiserver "k8s.io/kube-aggregator/pkg/controllers/status" |
||||
aggregatoropenapi "k8s.io/kube-aggregator/pkg/generated/openapi" |
||||
"k8s.io/kube-openapi/pkg/common" |
||||
) |
||||
|
||||
// AggregatorServerOptions contains the state for the aggregator apiserver
|
||||
type AggregatorServerOptions struct { |
||||
Builders []grafanaAPIServer.APIGroupBuilder |
||||
AlternateDNS []string |
||||
Config *Config |
||||
serviceResolver ServiceResolver |
||||
|
||||
sharedInformerFactory informersv0alpha1.SharedInformerFactory |
||||
|
||||
StdOut io.Writer |
||||
StdErr io.Writer |
||||
} |
||||
|
||||
func NewAggregatorServerOptions(out, errOut io.Writer, |
||||
options *options.RecommendedOptions, |
||||
extraConfig *ExtraConfig, |
||||
) (*AggregatorServerOptions, error) { |
||||
sharedConfig, err := initSharedConfig(options, aggregatorscheme.Codecs, nil) |
||||
if err != nil { |
||||
klog.Errorf("Error creating shared config: %s", err) |
||||
return nil, err |
||||
} |
||||
|
||||
sharedInformerFactory, err := initSharedInformerFactory(sharedConfig) |
||||
if err != nil { |
||||
klog.Errorf("Error creating shared informer factory: %s", err) |
||||
return nil, err |
||||
} |
||||
|
||||
serviceResolver, err := initServiceResolver(sharedInformerFactory) |
||||
if err != nil { |
||||
klog.Errorf("Error creating service resolver: %s", err) |
||||
return nil, err |
||||
} |
||||
|
||||
fakeInformers := informers.NewSharedInformerFactory(fake.NewSimpleClientset(), 10*time.Minute) |
||||
builders := []grafanaAPIServer.APIGroupBuilder{ |
||||
service.NewServiceAPIBuilder(), |
||||
} |
||||
|
||||
extensionsConfig, err := initApiExtensionsConfig(options, sharedConfig, fakeInformers, serviceResolver, extraConfig.DataPath) |
||||
if err != nil { |
||||
klog.Errorf("Error creating extensions config: %s", err) |
||||
return nil, err |
||||
} |
||||
|
||||
aggregatorConfig, err := initAggregatorConfig(options, sharedConfig, extraConfig, fakeInformers, builders, serviceResolver, extraConfig.DataPath) |
||||
if err != nil { |
||||
klog.Errorf("Error creating aggregator config: %s", err) |
||||
return nil, err |
||||
} |
||||
|
||||
return &AggregatorServerOptions{ |
||||
StdOut: out, |
||||
StdErr: errOut, |
||||
Builders: builders, |
||||
sharedInformerFactory: sharedInformerFactory, |
||||
serviceResolver: serviceResolver, |
||||
Config: &Config{ |
||||
Aggregator: aggregatorConfig, |
||||
ApiExtensions: extensionsConfig, |
||||
|
||||
SharedConfig: sharedConfig, |
||||
extraConfig: extraConfig, |
||||
}, |
||||
}, nil |
||||
} |
||||
|
||||
func (o *AggregatorServerOptions) LoadAPIGroupBuilders() error { |
||||
// Install schemas
|
||||
for _, b := range o.Builders { |
||||
if err := b.InstallSchema(aggregatorscheme.Scheme); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func initSharedConfig(options *options.RecommendedOptions, codecs serializer.CodecFactory, alternateDNS []string) (*genericapiserver.RecommendedConfig, error) { |
||||
if err := options.SecureServing.MaybeDefaultWithSelfSignedCerts( |
||||
"localhost", alternateDNS, []net.IP{net.IPv4(127, 0, 0, 1)}, |
||||
); err != nil { |
||||
return nil, fmt.Errorf("error creating self-signed certificates: %v", err) |
||||
} |
||||
|
||||
options.Authentication.RemoteKubeConfigFileOptional = true |
||||
options.Authorization.RemoteKubeConfigFileOptional = true |
||||
|
||||
options.Admission = nil |
||||
|
||||
if options.CoreAPI.CoreAPIKubeconfigPath == "" { |
||||
options.CoreAPI = nil |
||||
} |
||||
|
||||
serverConfig := genericapiserver.NewRecommendedConfig(codecs) |
||||
|
||||
// NOTE: AggregatedDiscoveryGroupManager in kube-apiserver is set up by controlplane APIServerConfig creation
|
||||
// Here, we adopt that one line in addition to what recommendedOptions gives us
|
||||
// Without it, CRDs work on API routes (and are registered in openapi) but not discoverable by kubectl
|
||||
serverConfig.AggregatedDiscoveryGroupManager = aggregated.NewResourceManager("apis") |
||||
|
||||
if options.CoreAPI == nil { |
||||
if err := modifiedApplyTo(options, serverConfig); err != nil { |
||||
return nil, err |
||||
} |
||||
} else { |
||||
if err := options.ApplyTo(serverConfig); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
return serverConfig, nil |
||||
} |
||||
|
||||
// A copy of ApplyTo in recommended.go, but for >= 0.28, server pkg in apiserver does a bit extra causing
|
||||
// a panic when CoreAPI is set to nil
|
||||
func modifiedApplyTo(options *options.RecommendedOptions, config *genericapiserver.RecommendedConfig) error { |
||||
if err := options.Etcd.ApplyTo(&config.Config); err != nil { |
||||
return err |
||||
} |
||||
if err := options.EgressSelector.ApplyTo(&config.Config); err != nil { |
||||
return err |
||||
} |
||||
if err := options.Traces.ApplyTo(config.Config.EgressSelector, &config.Config); err != nil { |
||||
return err |
||||
} |
||||
if err := options.SecureServing.ApplyTo(&config.Config.SecureServing, &config.Config.LoopbackClientConfig); err != nil { |
||||
return err |
||||
} |
||||
if err := options.Authentication.ApplyTo(&config.Config.Authentication, config.SecureServing, config.OpenAPIConfig); err != nil { |
||||
return err |
||||
} |
||||
if err := options.Authorization.ApplyTo(&config.Config.Authorization); err != nil { |
||||
return err |
||||
} |
||||
if err := options.Audit.ApplyTo(&config.Config); err != nil { |
||||
return err |
||||
} |
||||
|
||||
// TODO: determine whether we need flow control (API priority and fairness)
|
||||
//if err := options.Features.ApplyTo(&config.Config); err != nil {
|
||||
// return err
|
||||
//}
|
||||
|
||||
if err := options.CoreAPI.ApplyTo(config); err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err := options.ExtraAdmissionInitializers(config) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func getMergedOpenAPIDefinitions(builders []grafanaAPIServer.APIGroupBuilder, ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { |
||||
// Add OpenAPI specs for each group+version
|
||||
prerequisiteAPIs := grafanaAPIServer.GetOpenAPIDefinitions(builders)(ref) |
||||
aggregatorAPIs := aggregatoropenapi.GetOpenAPIDefinitions(ref) |
||||
|
||||
for k, v := range prerequisiteAPIs { |
||||
aggregatorAPIs[k] = v |
||||
} |
||||
|
||||
return aggregatorAPIs |
||||
} |
||||
|
||||
func initSharedInformerFactory(sharedConfig *genericapiserver.RecommendedConfig) (informersv0alpha1.SharedInformerFactory, error) { |
||||
serviceClient, err := serviceclientset.NewForConfig(sharedConfig.LoopbackClientConfig) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return informersv0alpha1.NewSharedInformerFactory( |
||||
serviceClient, |
||||
5*time.Minute, // this is effectively used as a refresh interval right now. Might want to do something nicer later on.
|
||||
), nil |
||||
} |
||||
|
||||
func initServiceResolver(factory informersv0alpha1.SharedInformerFactory) (apiserver.ServiceResolver, error) { |
||||
return NewExternalNameResolver(factory.Service().V0alpha1().ExternalNames().Lister()), nil |
||||
} |
||||
|
||||
func initApiExtensionsConfig(options *options.RecommendedOptions, |
||||
sharedConfig *genericapiserver.RecommendedConfig, |
||||
fakeInfomers informers.SharedInformerFactory, |
||||
serviceResolver apiserver.ServiceResolver, |
||||
dataPath string, |
||||
) (*apiextensionsapiserver.Config, error) { |
||||
// make a shallow copy to let us twiddle a few things
|
||||
// most of the config actually remains the same. We only need to mess with a couple items related to the particulars of the api extensions
|
||||
genericConfig := sharedConfig.Config |
||||
|
||||
genericConfig.PostStartHooks = map[string]genericapiserver.PostStartHookConfigEntry{} |
||||
genericConfig.RESTOptionsGetter = nil |
||||
|
||||
// copy the etcd options so we don't mutate originals.
|
||||
// we assume that the etcd options have been completed already. avoid messing with anything outside
|
||||
// of changes to StorageConfig as that may lead to unexpected behavior when the options are applied.
|
||||
etcdOptions := *options.Etcd |
||||
// this is where the true decodable levels come from.
|
||||
etcdOptions.StorageConfig.Codec = apiextensionsapiserver.Codecs.LegacyCodec(apiextensionsv1beta1.SchemeGroupVersion, v1.SchemeGroupVersion) |
||||
// prefer the more compact serialization (v1beta1) for storage until https://issue.k8s.io/82292 is resolved for objects whose v1 serialization is too big but whose v1beta1 serialization can be stored
|
||||
etcdOptions.StorageConfig.EncodeVersioner = runtime.NewMultiGroupVersioner(apiextensionsv1beta1.SchemeGroupVersion, schema.GroupKind{Group: apiextensionsv1beta1.GroupName}) |
||||
etcdOptions.SkipHealthEndpoints = true // avoid double wiring of health checks
|
||||
if err := etcdOptions.ApplyTo(&genericConfig); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
restOptionsGetter := filestorage.NewRESTOptionsGetter(path.Join(dataPath, "grafana-apiextensionsserver"), etcdOptions.StorageConfig) |
||||
genericConfig.RESTOptionsGetter = restOptionsGetter |
||||
|
||||
// NOTE: ignoring genericConfig.ResourceTransformers in crdOptionsGetter creation for now
|
||||
// crdOptionsGetter := apiextensionsoptions.NewCRDRESTOptionsGetter(etcdOptions, genericConfig.ResourceTransformers, )
|
||||
// The following is equivalent code to apiextensionsoptions.NewCRDRESTOptionsGetter with lesser dependencies
|
||||
crdEtcdOptions := etcdOptions |
||||
crdEtcdOptions.StorageConfig.Codec = unstructured.UnstructuredJSONScheme |
||||
crdEtcdOptions.StorageConfig.StorageObjectCountTracker = genericConfig.StorageObjectCountTracker |
||||
crdEtcdOptions.WatchCacheSizes = nil // this control is not provided for custom resources
|
||||
|
||||
// override MergedResourceConfig with apiextensions defaults and registry
|
||||
mergedResourceConfig, err := resourceconfig.MergeAPIResourceConfigs(apiextensionsapiserver.DefaultAPIResourceConfigSource(), nil, apiextensionsapiserver.Scheme) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
genericConfig.MergedResourceConfig = mergedResourceConfig |
||||
|
||||
genericConfig.OpenAPIV3Config = genericapiserver.DefaultOpenAPIV3Config(openapi.GetOpenAPIDefinitionsWithoutDisabledFeatures(apiextensionsopenapi.GetOpenAPIDefinitions), openapinamer.NewDefinitionNamer(apiextensionsapiserver.Scheme, apiextensionsapiserver.Scheme)) |
||||
|
||||
apiextensionsConfig := &apiextensionsapiserver.Config{ |
||||
GenericConfig: &genericapiserver.RecommendedConfig{ |
||||
Config: genericConfig, |
||||
SharedInformerFactory: fakeInfomers, |
||||
}, |
||||
ExtraConfig: apiextensionsapiserver.ExtraConfig{ |
||||
CRDRESTOptionsGetter: filestorage.NewRESTOptionsGetter(path.Join(dataPath, "grafana-apiextensionsserver"), crdEtcdOptions.StorageConfig), |
||||
// TODO: remove the hardcod when HA story is more developed
|
||||
MasterCount: 1, |
||||
// TODO: leaving AuthResolverWrapper unset doesn't impact basic operation of CRDs
|
||||
// AuthResolverWrapper: authResolverWrapper,
|
||||
ServiceResolver: serviceResolver, |
||||
}, |
||||
} |
||||
|
||||
// we need to clear the poststarthooks so we don't add them multiple times to all the servers (that fails)
|
||||
apiextensionsConfig.GenericConfig.PostStartHooks = map[string]genericapiserver.PostStartHookConfigEntry{} |
||||
|
||||
return apiextensionsConfig, nil |
||||
} |
||||
|
||||
func initAggregatorConfig(options *options.RecommendedOptions, |
||||
sharedConfig *genericapiserver.RecommendedConfig, |
||||
extra *ExtraConfig, |
||||
fakeInformers informers.SharedInformerFactory, |
||||
builders []grafanaAPIServer.APIGroupBuilder, |
||||
serviceResolver apiserver.ServiceResolver, |
||||
dataPath string, |
||||
) (*aggregatorapiserver.Config, error) { |
||||
// make a shallow copy to let us twiddle a few things
|
||||
// most of the config actually remains the same. We only need to mess with a couple items related to the particulars of the aggregator
|
||||
genericConfig := sharedConfig.Config |
||||
|
||||
genericConfig.PostStartHooks = map[string]genericapiserver.PostStartHookConfigEntry{} |
||||
genericConfig.RESTOptionsGetter = nil |
||||
// prevent generic API server from installing the OpenAPI handler. Aggregator server
|
||||
// has its own customized OpenAPI handler.
|
||||
genericConfig.SkipOpenAPIInstallation = true |
||||
mergedResourceConfig, err := resourceconfig.MergeAPIResourceConfigs(aggregatorapiserver.DefaultAPIResourceConfigSource(), nil, aggregatorscheme.Scheme) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
genericConfig.MergedResourceConfig = mergedResourceConfig |
||||
|
||||
getOpenAPIDefinitionsFunc := func() common.GetOpenAPIDefinitions { |
||||
return func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { |
||||
return getMergedOpenAPIDefinitions(builders, ref) |
||||
} |
||||
} |
||||
|
||||
namer := openapinamer.NewDefinitionNamer(aggregatorscheme.Scheme, apiextensionsapiserver.Scheme) |
||||
genericConfig.OpenAPIV3Config = genericapiserver.DefaultOpenAPIV3Config(getOpenAPIDefinitionsFunc(), namer) |
||||
genericConfig.OpenAPIV3Config.Info.Title = "Kubernetes" |
||||
genericConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(getOpenAPIDefinitionsFunc(), namer) |
||||
genericConfig.OpenAPIConfig.Info.Title = "Kubernetes" |
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StorageVersionAPI) && |
||||
utilfeature.DefaultFeatureGate.Enabled(genericfeatures.APIServerIdentity) { |
||||
// Add StorageVersionPrecondition handler to aggregator-apiserver.
|
||||
// The handler will block write requests to built-in resources until the
|
||||
// target resources' storage versions are up-to-date.
|
||||
genericConfig.BuildHandlerChainFunc = genericapiserver.BuildHandlerChainWithStorageVersionPrecondition |
||||
} |
||||
|
||||
// copy the etcd options so we don't mutate originals.
|
||||
// we assume that the etcd options have been completed already. avoid messing with anything outside
|
||||
// of changes to StorageConfig as that may lead to unexpected behavior when the options are applied.
|
||||
etcdOptions := *options.Etcd |
||||
etcdOptions.StorageConfig.Codec = aggregatorscheme.Codecs.LegacyCodec(v1.SchemeGroupVersion, |
||||
apiregistrationv1beta1.SchemeGroupVersion, |
||||
servicev0alpha1.SchemeGroupVersion) |
||||
etcdOptions.StorageConfig.EncodeVersioner = runtime.NewMultiGroupVersioner(v1.SchemeGroupVersion, |
||||
schema.GroupKind{Group: apiregistrationv1beta1.GroupName}, |
||||
schema.GroupKind{Group: servicev0alpha1.GROUP}) |
||||
etcdOptions.SkipHealthEndpoints = true // avoid double wiring of health checks
|
||||
if err := etcdOptions.ApplyTo(&genericConfig); err != nil { |
||||
return nil, err |
||||
} |
||||
genericConfig.RESTOptionsGetter = filestorage.NewRESTOptionsGetter(path.Join(dataPath, "grafana-aggregator"), etcdOptions.StorageConfig) |
||||
|
||||
genericConfig.DisabledPostStartHooks = genericConfig.DisabledPostStartHooks.Insert("apiservice-status-available-controller") |
||||
genericConfig.DisabledPostStartHooks = genericConfig.DisabledPostStartHooks.Insert("start-kube-aggregator-informers") |
||||
|
||||
aggregatorConfig := &aggregatorapiserver.Config{ |
||||
GenericConfig: &genericapiserver.RecommendedConfig{ |
||||
Config: genericConfig, |
||||
SharedInformerFactory: fakeInformers, |
||||
ClientConfig: genericConfig.LoopbackClientConfig, |
||||
}, |
||||
ExtraConfig: aggregatorapiserver.ExtraConfig{ |
||||
ProxyClientCertFile: extra.ProxyClientCertFile, |
||||
ProxyClientKeyFile: extra.ProxyClientKeyFile, |
||||
// NOTE: while ProxyTransport can be skipped in the configuration, it allows honoring
|
||||
// DISABLE_HTTP2, HTTPS_PROXY and NO_PROXY env vars as needed
|
||||
ProxyTransport: createProxyTransport(), |
||||
}, |
||||
} |
||||
|
||||
aggregatorConfig.ExtraConfig.ServiceResolver = serviceResolver |
||||
|
||||
// we need to clear the poststarthooks so we don't add them multiple times to all the servers (that fails)
|
||||
aggregatorConfig.GenericConfig.PostStartHooks = map[string]genericapiserver.PostStartHookConfigEntry{} |
||||
|
||||
return aggregatorConfig, nil |
||||
} |
||||
|
||||
func (o *AggregatorServerOptions) CreateAggregatorServer(delegateAPIServer genericapiserver.DelegationTarget, apiExtensionsInformers apiextensionsinformers.SharedInformerFactory) (*aggregatorapiserver.APIAggregator, error) { |
||||
completedConfig := o.Config.AggregatorComplete |
||||
aggregatorServer, err := completedConfig.NewWithDelegate(delegateAPIServer) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// create controllers for auto-registration
|
||||
apiRegistrationClient, err := apiregistrationclient.NewForConfig(completedConfig.GenericConfig.LoopbackClientConfig) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
autoRegistrationController := autoregister.NewAutoRegisterController(aggregatorServer.APIRegistrationInformers.Apiregistration().V1().APIServices(), apiRegistrationClient) |
||||
apiServices := apiServicesToRegister(delegateAPIServer, autoRegistrationController) |
||||
|
||||
crdRegistrationController := NewCRDRegistrationController( |
||||
apiExtensionsInformers.Apiextensions().V1().CustomResourceDefinitions(), |
||||
autoRegistrationController) |
||||
|
||||
// Imbue all builtin group-priorities onto the aggregated discovery
|
||||
if completedConfig.GenericConfig.AggregatedDiscoveryGroupManager != nil { |
||||
for gv, entry := range apiVersionPriorities { |
||||
completedConfig.GenericConfig.AggregatedDiscoveryGroupManager.SetGroupVersionPriority(metav1.GroupVersion(gv), int(entry.group), int(entry.version)) |
||||
} |
||||
} |
||||
|
||||
err = aggregatorServer.GenericAPIServer.AddPostStartHook("kube-apiserver-autoregistration", func(context genericapiserver.PostStartHookContext) error { |
||||
go crdRegistrationController.Run(5, context.StopCh) |
||||
go func() { |
||||
crdRegistrationController.WaitForInitialSync() |
||||
autoRegistrationController.Run(5, context.StopCh) |
||||
}() |
||||
return nil |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
err = aggregatorServer.GenericAPIServer.AddBootSequenceHealthChecks( |
||||
makeAPIServiceAvailableHealthCheck( |
||||
"autoregister-completion", |
||||
apiServices, |
||||
aggregatorServer.APIRegistrationInformers.Apiregistration().V1().APIServices(), |
||||
), |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
apiregistrationClient, err := apiregistrationclientset.NewForConfig(completedConfig.GenericConfig.LoopbackClientConfig) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
availableController, err := NewAvailableConditionController( |
||||
aggregatorServer.APIRegistrationInformers.Apiregistration().V1().APIServices(), |
||||
o.sharedInformerFactory.Service().V0alpha1().ExternalNames(), |
||||
apiregistrationClient.ApiregistrationV1(), |
||||
nil, |
||||
(func() ([]byte, []byte))(nil), |
||||
completedConfig.ExtraConfig.ServiceResolver, |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
aggregatorServer.GenericAPIServer.AddPostStartHookOrDie("apiservice-status-override-available-controller", func(context genericapiserver.PostStartHookContext) error { |
||||
// if we end up blocking for long periods of time, we may need to increase workers.
|
||||
go availableController.Run(5, context.StopCh) |
||||
return nil |
||||
}) |
||||
|
||||
aggregatorServer.GenericAPIServer.AddPostStartHookOrDie("start-grafana-aggregator-informers", func(context genericapiserver.PostStartHookContext) error { |
||||
o.sharedInformerFactory.Start(context.StopCh) |
||||
aggregatorServer.APIRegistrationInformers.Start(context.StopCh) |
||||
return nil |
||||
}) |
||||
|
||||
// Install the API Group+version
|
||||
for _, b := range o.Builders { |
||||
g, err := b.GetAPIGroupInfo(aggregatorscheme.Scheme, aggregatorscheme.Codecs, completedConfig.GenericConfig.RESTOptionsGetter) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if g == nil || len(g.PrioritizedVersions) < 1 { |
||||
continue |
||||
} |
||||
err = aggregatorServer.GenericAPIServer.InstallAPIGroup(g) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
return aggregatorServer, nil |
||||
} |
||||
|
||||
func makeAPIService(gv schema.GroupVersion) *v1.APIService { |
||||
apiServicePriority, ok := apiVersionPriorities[gv] |
||||
if !ok { |
||||
// if we aren't found, then we shouldn't register ourselves because it could result in a CRD group version
|
||||
// being permanently stuck in the APIServices list.
|
||||
klog.Infof("Skipping APIService creation for %v", gv) |
||||
return nil |
||||
} |
||||
return &v1.APIService{ |
||||
ObjectMeta: metav1.ObjectMeta{Name: gv.Version + "." + gv.Group}, |
||||
Spec: v1.APIServiceSpec{ |
||||
Group: gv.Group, |
||||
Version: gv.Version, |
||||
GroupPriorityMinimum: apiServicePriority.group, |
||||
VersionPriority: apiServicePriority.version, |
||||
}, |
||||
} |
||||
} |
||||
|
||||
// makeAPIServiceAvailableHealthCheck returns a healthz check that returns healthy
|
||||
// once all of the specified services have been observed to be available at least once.
|
||||
func makeAPIServiceAvailableHealthCheck(name string, apiServices []*v1.APIService, apiServiceInformer apiregistrationInformers.APIServiceInformer) healthz.HealthChecker { |
||||
// Track the auto-registered API services that have not been observed to be available yet
|
||||
pendingServiceNamesLock := &sync.RWMutex{} |
||||
pendingServiceNames := sets.NewString() |
||||
for _, service := range apiServices { |
||||
pendingServiceNames.Insert(service.Name) |
||||
} |
||||
|
||||
// When an APIService in the list is seen as available, remove it from the pending list
|
||||
handleAPIServiceChange := func(service *v1.APIService) { |
||||
pendingServiceNamesLock.Lock() |
||||
defer pendingServiceNamesLock.Unlock() |
||||
if !pendingServiceNames.Has(service.Name) { |
||||
return |
||||
} |
||||
if v1helper.IsAPIServiceConditionTrue(service, v1.Available) { |
||||
pendingServiceNames.Delete(service.Name) |
||||
} |
||||
} |
||||
|
||||
// Watch add/update events for APIServices
|
||||
_, _ = apiServiceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ |
||||
AddFunc: func(obj interface{}) { handleAPIServiceChange(obj.(*v1.APIService)) }, |
||||
UpdateFunc: func(old, new interface{}) { handleAPIServiceChange(new.(*v1.APIService)) }, |
||||
}) |
||||
|
||||
// Don't return healthy until the pending list is empty
|
||||
return healthz.NamedCheck(name, func(r *http.Request) error { |
||||
pendingServiceNamesLock.RLock() |
||||
defer pendingServiceNamesLock.RUnlock() |
||||
if pendingServiceNames.Len() > 0 { |
||||
return fmt.Errorf("missing APIService: %v", pendingServiceNames.List()) |
||||
} |
||||
return nil |
||||
}) |
||||
} |
||||
|
||||
// priority defines group priority that is used in discovery. This controls
|
||||
// group position in the kubectl output.
|
||||
type priority struct { |
||||
// group indicates the order of the group relative to other groups.
|
||||
group int32 |
||||
// version indicates the relative order of the version inside of its group.
|
||||
version int32 |
||||
} |
||||
|
||||
// The proper way to resolve this letting the aggregator know the desired group and version-within-group order of the underlying servers
|
||||
// is to refactor the genericapiserver.DelegationTarget to include a list of priorities based on which APIs were installed.
|
||||
// This requires the APIGroupInfo struct to evolve and include the concept of priorities and to avoid mistakes, the core storage map there needs to be updated.
|
||||
// That ripples out every bit as far as you'd expect, so for 1.7 we'll include the list here instead of being built up during storage.
|
||||
var apiVersionPriorities = map[schema.GroupVersion]priority{ |
||||
{Group: "", Version: "v1"}: {group: 18000, version: 1}, |
||||
// to my knowledge, nothing below here collides
|
||||
{Group: "admissionregistration.k8s.io", Version: "v1"}: {group: 16700, version: 15}, |
||||
{Group: "admissionregistration.k8s.io", Version: "v1beta1"}: {group: 16700, version: 12}, |
||||
{Group: "admissionregistration.k8s.io", Version: "v1alpha1"}: {group: 16700, version: 9}, |
||||
{Group: "apiextensions.k8s.io", Version: "v1"}: {group: 16700, version: 15}, |
||||
// Append a new group to the end of the list if unsure.
|
||||
// You can use min(existing group)-100 as the initial value for a group.
|
||||
// Version can be set to 9 (to have space around) for a new group.
|
||||
} |
||||
|
||||
func apiServicesToRegister(delegateAPIServer genericapiserver.DelegationTarget, registration autoregister.AutoAPIServiceRegistration) []*v1.APIService { |
||||
apiServices := []*v1.APIService{} |
||||
|
||||
for _, curr := range delegateAPIServer.ListedPaths() { |
||||
if curr == "/api/v1" { |
||||
apiService := makeAPIService(schema.GroupVersion{Group: "", Version: "v1"}) |
||||
registration.AddAPIServiceToSyncOnStart(apiService) |
||||
apiServices = append(apiServices, apiService) |
||||
continue |
||||
} |
||||
|
||||
if !strings.HasPrefix(curr, "/apis/") { |
||||
continue |
||||
} |
||||
// this comes back in a list that looks like /apis/rbac.authorization.k8s.io/v1alpha1
|
||||
tokens := strings.Split(curr, "/") |
||||
if len(tokens) != 4 { |
||||
continue |
||||
} |
||||
|
||||
apiService := makeAPIService(schema.GroupVersion{Group: tokens[2], Version: tokens[3]}) |
||||
if apiService == nil { |
||||
continue |
||||
} |
||||
registration.AddAPIServiceToSyncOnStart(apiService) |
||||
apiServices = append(apiServices, apiService) |
||||
} |
||||
|
||||
return apiServices |
||||
} |
||||
|
||||
// NOTE: below function imported from https://github.com/kubernetes/kubernetes/blob/master/cmd/kube-apiserver/app/server.go#L197
|
||||
// createProxyTransport creates the dialer infrastructure to connect to the api servers.
|
||||
func createProxyTransport() *http.Transport { |
||||
// NOTE: We don't set proxyDialerFn but the below SetTransportDefaults will
|
||||
// See https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apimachinery/pkg/util/net/http.go#L109
|
||||
var proxyDialerFn utilnet.DialFunc |
||||
// Proxying to services is IP-based... don't expect to be able to verify the hostname
|
||||
proxyTLSClientConfig := &tls.Config{InsecureSkipVerify: true} |
||||
proxyTransport := utilnet.SetTransportDefaults(&http.Transport{ |
||||
DialContext: proxyDialerFn, |
||||
TLSClientConfig: proxyTLSClientConfig, |
||||
}) |
||||
return proxyTransport |
||||
} |
||||
@ -1,57 +0,0 @@ |
||||
package aggregator |
||||
|
||||
import ( |
||||
"github.com/spf13/pflag" |
||||
apiextensionsapiserver "k8s.io/apiextensions-apiserver/pkg/apiserver" |
||||
genericapiserver "k8s.io/apiserver/pkg/server" |
||||
"k8s.io/apiserver/pkg/server/options" |
||||
aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver" |
||||
) |
||||
|
||||
type ExtraConfig struct { |
||||
ProxyClientCertFile string |
||||
ProxyClientKeyFile string |
||||
|
||||
DataPath string |
||||
} |
||||
|
||||
type Config struct { |
||||
Aggregator *aggregatorapiserver.Config |
||||
ApiExtensions *apiextensionsapiserver.Config |
||||
|
||||
AggregatorComplete aggregatorapiserver.CompletedConfig |
||||
ApiExtensionsComplete apiextensionsapiserver.CompletedConfig |
||||
|
||||
recommendedOptions *options.RecommendedOptions |
||||
SharedConfig *genericapiserver.RecommendedConfig |
||||
extraConfig *ExtraConfig |
||||
} |
||||
|
||||
func (c *Config) AddFlags(fs *pflag.FlagSet) { |
||||
if c == nil { |
||||
return |
||||
} |
||||
|
||||
c.recommendedOptions.AddFlags(fs) |
||||
} |
||||
|
||||
func (c *Config) Complete() { |
||||
if c == nil { |
||||
return |
||||
} |
||||
|
||||
c.ApiExtensionsComplete = c.ApiExtensions.Complete() |
||||
c.AggregatorComplete = c.Aggregator.Complete() |
||||
} |
||||
|
||||
func (ec *ExtraConfig) AddFlags(fs *pflag.FlagSet) { |
||||
if ec == nil { |
||||
return |
||||
} |
||||
|
||||
fs.StringVar(&ec.ProxyClientCertFile, "proxy-client-cert-file", ec.ProxyClientCertFile, |
||||
"path to proxy client cert file") |
||||
|
||||
fs.StringVar(&ec.ProxyClientKeyFile, "proxy-client-key-file", ec.ProxyClientKeyFile, |
||||
"path to proxy client cert file") |
||||
} |
||||
@ -1,214 +0,0 @@ |
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Provenance-includes-location: https://github.com/kubernetes/kubernetes/blob/master/pkg/controlplane/controller/crdregistration/crdregistration_controller.go
|
||||
// Provenance-includes-license: Apache-2.0
|
||||
// Provenance-includes-copyright: The Kubernetes Authors.
|
||||
|
||||
package aggregator |
||||
|
||||
import ( |
||||
"fmt" |
||||
"time" |
||||
|
||||
"k8s.io/klog/v2" |
||||
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" |
||||
crdinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions/apiextensions/v1" |
||||
crdlisters "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/v1" |
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
||||
"k8s.io/apimachinery/pkg/labels" |
||||
"k8s.io/apimachinery/pkg/runtime/schema" |
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime" |
||||
"k8s.io/apimachinery/pkg/util/wait" |
||||
"k8s.io/client-go/tools/cache" |
||||
"k8s.io/client-go/util/workqueue" |
||||
v1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" |
||||
) |
||||
|
||||
// AutoAPIServiceRegistration is an interface which callers can re-declare locally and properly cast to for
|
||||
// adding and removing APIServices
|
||||
type AutoAPIServiceRegistration interface { |
||||
// AddAPIServiceToSync adds an API service to auto-register.
|
||||
AddAPIServiceToSync(in *v1.APIService) |
||||
// RemoveAPIServiceToSync removes an API service to auto-register.
|
||||
RemoveAPIServiceToSync(name string) |
||||
} |
||||
|
||||
type crdRegistrationController struct { |
||||
crdLister crdlisters.CustomResourceDefinitionLister |
||||
crdSynced cache.InformerSynced |
||||
|
||||
apiServiceRegistration AutoAPIServiceRegistration |
||||
|
||||
syncHandler func(groupVersion schema.GroupVersion) error |
||||
|
||||
syncedInitialSet chan struct{} |
||||
|
||||
// queue is where incoming work is placed to de-dup and to allow "easy" rate limited requeues on errors
|
||||
// this is actually keyed by a groupVersion
|
||||
queue workqueue.RateLimitingInterface |
||||
} |
||||
|
||||
// NewCRDRegistrationController returns a controller which will register CRD GroupVersions with the auto APIService registration
|
||||
// controller so they automatically stay in sync.
|
||||
func NewCRDRegistrationController(crdinformer crdinformers.CustomResourceDefinitionInformer, apiServiceRegistration AutoAPIServiceRegistration) *crdRegistrationController { |
||||
c := &crdRegistrationController{ |
||||
crdLister: crdinformer.Lister(), |
||||
crdSynced: crdinformer.Informer().HasSynced, |
||||
apiServiceRegistration: apiServiceRegistration, |
||||
syncedInitialSet: make(chan struct{}), |
||||
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "crd_autoregistration_controller"), |
||||
} |
||||
c.syncHandler = c.handleVersionUpdate |
||||
|
||||
_, _ = crdinformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ |
||||
AddFunc: func(obj interface{}) { |
||||
cast := obj.(*apiextensionsv1.CustomResourceDefinition) |
||||
c.enqueueCRD(cast) |
||||
}, |
||||
UpdateFunc: func(oldObj, newObj interface{}) { |
||||
// Enqueue both old and new object to make sure we remove and add appropriate API services.
|
||||
// The working queue will resolve any duplicates and only changes will stay in the queue.
|
||||
c.enqueueCRD(oldObj.(*apiextensionsv1.CustomResourceDefinition)) |
||||
c.enqueueCRD(newObj.(*apiextensionsv1.CustomResourceDefinition)) |
||||
}, |
||||
DeleteFunc: func(obj interface{}) { |
||||
cast, ok := obj.(*apiextensionsv1.CustomResourceDefinition) |
||||
if !ok { |
||||
tombstone, ok := obj.(cache.DeletedFinalStateUnknown) |
||||
if !ok { |
||||
klog.V(2).Infof("Couldn't get object from tombstone %#v", obj) |
||||
return |
||||
} |
||||
cast, ok = tombstone.Obj.(*apiextensionsv1.CustomResourceDefinition) |
||||
if !ok { |
||||
klog.V(2).Infof("Tombstone contained unexpected object: %#v", obj) |
||||
return |
||||
} |
||||
} |
||||
c.enqueueCRD(cast) |
||||
}, |
||||
}) |
||||
|
||||
return c |
||||
} |
||||
|
||||
func (c *crdRegistrationController) Run(workers int, stopCh <-chan struct{}) { |
||||
defer utilruntime.HandleCrash() |
||||
// make sure the work queue is shutdown which will trigger workers to end
|
||||
defer c.queue.ShutDown() |
||||
|
||||
klog.Infof("Starting crd-autoregister controller") |
||||
defer klog.Infof("Shutting down crd-autoregister controller") |
||||
|
||||
// wait for your secondary caches to fill before starting your work
|
||||
if !cache.WaitForNamedCacheSync("crd-autoregister", stopCh, c.crdSynced) { |
||||
return |
||||
} |
||||
|
||||
// process each item in the list once
|
||||
if crds, err := c.crdLister.List(labels.Everything()); err != nil { |
||||
utilruntime.HandleError(err) |
||||
} else { |
||||
for _, crd := range crds { |
||||
for _, version := range crd.Spec.Versions { |
||||
if err := c.syncHandler(schema.GroupVersion{Group: crd.Spec.Group, Version: version.Name}); err != nil { |
||||
utilruntime.HandleError(err) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
close(c.syncedInitialSet) |
||||
|
||||
// start up your worker threads based on workers. Some controllers have multiple kinds of workers
|
||||
for i := 0; i < workers; i++ { |
||||
// runWorker will loop until "something bad" happens. The .Until will then rekick the worker
|
||||
// after one second
|
||||
go wait.Until(c.runWorker, time.Second, stopCh) |
||||
} |
||||
|
||||
// wait until we're told to stop
|
||||
<-stopCh |
||||
} |
||||
|
||||
// WaitForInitialSync blocks until the initial set of CRD resources has been processed
|
||||
func (c *crdRegistrationController) WaitForInitialSync() { |
||||
<-c.syncedInitialSet |
||||
} |
||||
|
||||
func (c *crdRegistrationController) runWorker() { |
||||
// hot loop until we're told to stop. processNextWorkItem will automatically wait until there's work
|
||||
// available, so we don't worry about secondary waits
|
||||
for c.processNextWorkItem() { |
||||
} |
||||
} |
||||
|
||||
// processNextWorkItem deals with one key off the queue. It returns false when it's time to quit.
|
||||
func (c *crdRegistrationController) processNextWorkItem() bool { |
||||
// pull the next work item from queue. It should be a key we use to lookup something in a cache
|
||||
key, quit := c.queue.Get() |
||||
if quit { |
||||
return false |
||||
} |
||||
// you always have to indicate to the queue that you've completed a piece of work
|
||||
defer c.queue.Done(key) |
||||
|
||||
// do your work on the key. This method will contains your "do stuff" logic
|
||||
err := c.syncHandler(key.(schema.GroupVersion)) |
||||
if err == nil { |
||||
// if you had no error, tell the queue to stop tracking history for your key. This will
|
||||
// reset things like failure counts for per-item rate limiting
|
||||
c.queue.Forget(key) |
||||
return true |
||||
} |
||||
|
||||
// there was a failure so be sure to report it. This method allows for pluggable error handling
|
||||
// which can be used for things like cluster-monitoring
|
||||
utilruntime.HandleError(fmt.Errorf("%v failed with : %v", key, err)) |
||||
// since we failed, we should requeue the item to work on later. This method will add a backoff
|
||||
// to avoid hotlooping on particular items (they're probably still not going to work right away)
|
||||
// and overall controller protection (everything I've done is broken, this controller needs to
|
||||
// calm down or it can starve other useful work) cases.
|
||||
c.queue.AddRateLimited(key) |
||||
|
||||
return true |
||||
} |
||||
|
||||
func (c *crdRegistrationController) enqueueCRD(crd *apiextensionsv1.CustomResourceDefinition) { |
||||
for _, version := range crd.Spec.Versions { |
||||
c.queue.Add(schema.GroupVersion{Group: crd.Spec.Group, Version: version.Name}) |
||||
} |
||||
} |
||||
|
||||
func (c *crdRegistrationController) handleVersionUpdate(groupVersion schema.GroupVersion) error { |
||||
apiServiceName := groupVersion.Version + "." + groupVersion.Group |
||||
|
||||
// check all CRDs. There shouldn't that many, but if we have problems later we can index them
|
||||
crds, err := c.crdLister.List(labels.Everything()) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
for _, crd := range crds { |
||||
if crd.Spec.Group != groupVersion.Group { |
||||
continue |
||||
} |
||||
for _, version := range crd.Spec.Versions { |
||||
if version.Name != groupVersion.Version || !version.Served { |
||||
continue |
||||
} |
||||
|
||||
c.apiServiceRegistration.AddAPIServiceToSync(&v1.APIService{ |
||||
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName}, |
||||
Spec: v1.APIServiceSpec{ |
||||
Group: groupVersion.Group, |
||||
Version: groupVersion.Version, |
||||
GroupPriorityMinimum: 1000, // CRDs should have relatively low priority
|
||||
VersionPriority: 100, // CRDs will be sorted by kube-like versions like any other APIService with the same VersionPriority
|
||||
}, |
||||
}) |
||||
return nil |
||||
} |
||||
} |
||||
|
||||
c.apiServiceRegistration.RemoveAPIServiceToSync(apiServiceName) |
||||
return nil |
||||
} |
||||
@ -1,51 +0,0 @@ |
||||
# grafana aggregator |
||||
|
||||
The `aggregator` command in this binary is our equivalent of what kube-apiserver does for aggregation using |
||||
the `kube-aggregator` pkg. Here, we enable only select controllers that are useful for aggregation in a Grafana |
||||
cloud context. In future, Grafana microservices (and even plugins) will run as separate API servers |
||||
hosting each their own APIs (with specific Group/Versions). The `aggregator` component here shall act similar to what |
||||
`kube-apiserver` does: doing healthchecks for `APIService` objects registered against it and acting as a proxy for |
||||
the specified `GroupVersion` therein. |
||||
|
||||
## How to get started |
||||
|
||||
1. Generate the PKI using `openssl` (for development purposes, we will use the CN of `system:masters`): |
||||
```shell |
||||
./hack/make-aggregator-pki.sh |
||||
``` |
||||
2. Start the aggregator: |
||||
```shell |
||||
# This will generate the kubeconfig which you can use in the extension apiservers for |
||||
# enforcing delegate authnz under $PWD/data/grafana-apiserver/aggregator.kubeconfig |
||||
go run ./pkg/cmd/grafana aggregator --secure-port 8443 \ |
||||
--proxy-client-cert-file $PWD/data/grafana-aggregator/client.crt \ |
||||
--proxy-client-key-file $PWD/data/grafana-aggregator/client.key |
||||
``` |
||||
3. Apply the manifests: |
||||
```shell |
||||
export KUBECONFIG=$PWD/data/grafana-apiserver/aggregator.kubeconfig |
||||
kubectl apply -k ./pkg/cmd/grafana/apiserver/deploy/aggregator-test |
||||
# SAMPLE OUTPUT |
||||
# apiservice.apiregistration.k8s.io/v0alpha1.example.grafana.app created |
||||
# externalname.service.grafana.app/example-apiserver created |
||||
|
||||
kubectl get apiservice |
||||
# SAMPLE OUTPUT |
||||
# NAME SERVICE AVAILABLE AGE |
||||
# v0alpha1.example.grafana.app grafana/example-apiserver False (FailedDiscoveryCheck) 29m |
||||
``` |
||||
4. In another tab, start the example microservice that will be aggregated by the parent apiserver: |
||||
```shell |
||||
go run ./pkg/cmd/grafana apiserver example.grafana.app \ |
||||
--kubeconfig $PWD/data/grafana-aggregator/aggregator.kubeconfig \ |
||||
--secure-port 7443 \ |
||||
--client-ca-file=$PWD/data/grafana-aggregator/ca.crt |
||||
``` |
||||
5. Check `APIService` again: |
||||
```shell |
||||
export KUBECONFIG=$PWD/data/grafana-apiserver/aggregator.kubeconfig |
||||
kubectl get apiservice |
||||
# SAMPLE OUTPUT |
||||
# NAME SERVICE AVAILABLE AGE |
||||
# v0alpha1.example.grafana.app grafana/example-apiserver True 30m |
||||
``` |
||||
@ -0,0 +1,148 @@ |
||||
package datasource |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend" |
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
||||
|
||||
common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" |
||||
"github.com/grafana/grafana/pkg/apis/datasource/v0alpha1" |
||||
"github.com/grafana/grafana/pkg/infra/appcontext" |
||||
"github.com/grafana/grafana/pkg/plugins" |
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" |
||||
"github.com/grafana/grafana/pkg/services/datasources" |
||||
) |
||||
|
||||
type QuerierFactoryFunc func(ctx context.Context, ri common.ResourceInfo, pj plugins.JSONData) (Querier, error) |
||||
|
||||
type QuerierProvider interface { |
||||
Querier(ctx context.Context, ri common.ResourceInfo, pj plugins.JSONData) (Querier, error) |
||||
} |
||||
|
||||
type DefaultQuerierProvider struct { |
||||
factory QuerierFactoryFunc |
||||
} |
||||
|
||||
func ProvideDefaultQuerierProvider(pluginClient plugins.Client, dsService datasources.DataSourceService, |
||||
dsCache datasources.CacheService) *DefaultQuerierProvider { |
||||
return NewQuerierProvider(func(ctx context.Context, ri common.ResourceInfo, pj plugins.JSONData) (Querier, error) { |
||||
return NewDefaultQuerier(ri, pj, pluginClient, dsService, dsCache), nil |
||||
}) |
||||
} |
||||
|
||||
func NewQuerierProvider(factory QuerierFactoryFunc) *DefaultQuerierProvider { |
||||
return &DefaultQuerierProvider{ |
||||
factory: factory, |
||||
} |
||||
} |
||||
|
||||
func (p *DefaultQuerierProvider) Querier(ctx context.Context, ri common.ResourceInfo, pj plugins.JSONData) (Querier, error) { |
||||
return p.factory(ctx, ri, pj) |
||||
} |
||||
|
||||
// Querier is the interface that wraps the Query method.
|
||||
type Querier interface { |
||||
// Query runs the query on behalf of the user in context.
|
||||
Query(ctx context.Context, query *backend.QueryDataRequest) (*backend.QueryDataResponse, error) |
||||
// Health checks the health of the plugin.
|
||||
Health(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) |
||||
// Resource gets a resource plugin.
|
||||
Resource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error |
||||
// Datasource gets all data source plugins (with elevated permissions).
|
||||
Datasource(ctx context.Context, name string) (*v0alpha1.DataSourceConnection, error) |
||||
// Datasources lists all data sources (with elevated permissions).
|
||||
Datasources(ctx context.Context) (*v0alpha1.DataSourceConnectionList, error) |
||||
} |
||||
|
||||
type DefaultQuerier struct { |
||||
connectionResourceInfo common.ResourceInfo |
||||
pluginJSON plugins.JSONData |
||||
pluginClient plugins.Client |
||||
dsService datasources.DataSourceService |
||||
dsCache datasources.CacheService |
||||
} |
||||
|
||||
func NewDefaultQuerier( |
||||
connectionResourceInfo common.ResourceInfo, |
||||
pluginJSON plugins.JSONData, |
||||
pluginClient plugins.Client, |
||||
dsService datasources.DataSourceService, |
||||
dsCache datasources.CacheService, |
||||
) *DefaultQuerier { |
||||
return &DefaultQuerier{ |
||||
connectionResourceInfo: connectionResourceInfo, |
||||
pluginJSON: pluginJSON, |
||||
pluginClient: pluginClient, |
||||
dsService: dsService, |
||||
dsCache: dsCache, |
||||
} |
||||
} |
||||
|
||||
func (q *DefaultQuerier) Query(ctx context.Context, query *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { |
||||
_, err := request.NamespaceInfoFrom(ctx, true) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return q.pluginClient.QueryData(ctx, query) |
||||
} |
||||
|
||||
func (q *DefaultQuerier) Resource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { |
||||
_, err := request.NamespaceInfoFrom(ctx, true) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return q.pluginClient.CallResource(ctx, req, sender) |
||||
} |
||||
|
||||
func (q *DefaultQuerier) Health(ctx context.Context, query *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { |
||||
_, err := request.NamespaceInfoFrom(ctx, true) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return q.pluginClient.CheckHealth(ctx, query) |
||||
} |
||||
|
||||
func (q *DefaultQuerier) Datasource(ctx context.Context, name string) (*v0alpha1.DataSourceConnection, error) { |
||||
info, err := request.NamespaceInfoFrom(ctx, true) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
user, err := appcontext.User(ctx) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
ds, err := q.dsCache.GetDatasourceByUID(ctx, name, user, false) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return asConnection(ds, info.Value) |
||||
} |
||||
|
||||
func (q *DefaultQuerier) Datasources(ctx context.Context) (*v0alpha1.DataSourceConnectionList, error) { |
||||
info, err := request.NamespaceInfoFrom(ctx, true) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
ds, err := q.dsService.GetDataSourcesByType(ctx, &datasources.GetDataSourcesByTypeQuery{ |
||||
OrgID: info.OrgID, |
||||
Type: q.pluginJSON.ID, |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return asConnectionList(q.connectionResourceInfo.TypeMeta(), ds, info.Value) |
||||
} |
||||
|
||||
func asConnectionList(typeMeta metav1.TypeMeta, dss []*datasources.DataSource, ns string) (*v0alpha1.DataSourceConnectionList, error) { |
||||
result := &v0alpha1.DataSourceConnectionList{ |
||||
Items: []v0alpha1.DataSourceConnection{}, |
||||
} |
||||
for _, ds := range dss { |
||||
v, _ := asConnection(ds, ns) |
||||
result.Items = append(result.Items, *v) |
||||
} |
||||
|
||||
return result, nil |
||||
} |
||||
@ -0,0 +1,285 @@ |
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Provenance-includes-location: https://github.com/kubernetes/kubernetes/blob/master/cmd/kube-apiserver/app/aggregator.go
|
||||
// Provenance-includes-license: Apache-2.0
|
||||
// Provenance-includes-copyright: The Kubernetes Authors.
|
||||
// Provenance-includes-location: https://github.com/kubernetes/kubernetes/blob/master/cmd/kube-apiserver/app/server.go
|
||||
// Provenance-includes-license: Apache-2.0
|
||||
// Provenance-includes-copyright: The Kubernetes Authors.
|
||||
// Provenance-includes-location: https://github.com/kubernetes/kubernetes/blob/master/pkg/controlplane/apiserver/apiextensions.go
|
||||
// Provenance-includes-license: Apache-2.0
|
||||
// Provenance-includes-copyright: The Kubernetes Authors.
|
||||
|
||||
package aggregator |
||||
|
||||
import ( |
||||
"crypto/tls" |
||||
"fmt" |
||||
"net/http" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
||||
"k8s.io/apimachinery/pkg/runtime/schema" |
||||
utilnet "k8s.io/apimachinery/pkg/util/net" |
||||
"k8s.io/apimachinery/pkg/util/sets" |
||||
genericapiserver "k8s.io/apiserver/pkg/server" |
||||
"k8s.io/apiserver/pkg/server/healthz" |
||||
"k8s.io/client-go/informers" |
||||
"k8s.io/client-go/kubernetes/fake" |
||||
"k8s.io/client-go/tools/cache" |
||||
"k8s.io/klog/v2" |
||||
v1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" |
||||
v1helper "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1/helper" |
||||
aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver" |
||||
apiregistrationclientset "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset" |
||||
apiregistrationclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/typed/apiregistration/v1" |
||||
apiregistrationInformers "k8s.io/kube-aggregator/pkg/client/informers/externalversions/apiregistration/v1" |
||||
"k8s.io/kube-aggregator/pkg/controllers/autoregister" |
||||
|
||||
serviceclientset "github.com/grafana/grafana/pkg/generated/clientset/versioned" |
||||
informersv0alpha1 "github.com/grafana/grafana/pkg/generated/informers/externalversions" |
||||
"github.com/grafana/grafana/pkg/services/apiserver/options" |
||||
) |
||||
|
||||
func CreateAggregatorConfig(commandOptions *options.Options, sharedConfig genericapiserver.RecommendedConfig) (*aggregatorapiserver.Config, informersv0alpha1.SharedInformerFactory, error) { |
||||
// Create a fake clientset and informers for the k8s v1 API group.
|
||||
// These are not used in grafana's aggregator because v1 APIs are not available.
|
||||
fakev1Informers := informers.NewSharedInformerFactory(fake.NewSimpleClientset(), 10*time.Minute) |
||||
|
||||
serviceClient, err := serviceclientset.NewForConfig(sharedConfig.LoopbackClientConfig) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
sharedInformerFactory := informersv0alpha1.NewSharedInformerFactory( |
||||
serviceClient, |
||||
5*time.Minute, // this is effectively used as a refresh interval right now. Might want to do something nicer later on.
|
||||
) |
||||
serviceResolver := NewExternalNameResolver(sharedInformerFactory.Service().V0alpha1().ExternalNames().Lister()) |
||||
|
||||
aggregatorConfig := &aggregatorapiserver.Config{ |
||||
GenericConfig: &genericapiserver.RecommendedConfig{ |
||||
Config: sharedConfig.Config, |
||||
SharedInformerFactory: fakev1Informers, |
||||
ClientConfig: sharedConfig.LoopbackClientConfig, |
||||
}, |
||||
ExtraConfig: aggregatorapiserver.ExtraConfig{ |
||||
ProxyClientCertFile: commandOptions.AggregatorOptions.ProxyClientCertFile, |
||||
ProxyClientKeyFile: commandOptions.AggregatorOptions.ProxyClientKeyFile, |
||||
// NOTE: while ProxyTransport can be skipped in the configuration, it allows honoring
|
||||
// DISABLE_HTTP2, HTTPS_PROXY and NO_PROXY env vars as needed
|
||||
ProxyTransport: createProxyTransport(), |
||||
ServiceResolver: serviceResolver, |
||||
}, |
||||
} |
||||
|
||||
if err := commandOptions.AggregatorOptions.ApplyTo(aggregatorConfig, commandOptions.RecommendedOptions.Etcd, commandOptions.StorageOptions.DataPath); err != nil { |
||||
return nil, nil, err |
||||
} |
||||
|
||||
return aggregatorConfig, sharedInformerFactory, nil |
||||
} |
||||
|
||||
func CreateAggregatorServer(aggregatorConfig *aggregatorapiserver.Config, sharedInformerFactory informersv0alpha1.SharedInformerFactory, delegateAPIServer genericapiserver.DelegationTarget) (*aggregatorapiserver.APIAggregator, error) { |
||||
completedConfig := aggregatorConfig.Complete() |
||||
aggregatorServer, err := completedConfig.NewWithDelegate(delegateAPIServer) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// create controllers for auto-registration
|
||||
apiRegistrationClient, err := apiregistrationclient.NewForConfig(completedConfig.GenericConfig.LoopbackClientConfig) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
autoRegistrationController := autoregister.NewAutoRegisterController(aggregatorServer.APIRegistrationInformers.Apiregistration().V1().APIServices(), apiRegistrationClient) |
||||
apiServices := apiServicesToRegister(delegateAPIServer, autoRegistrationController) |
||||
|
||||
// Imbue all builtin group-priorities onto the aggregated discovery
|
||||
if completedConfig.GenericConfig.AggregatedDiscoveryGroupManager != nil { |
||||
for gv, entry := range APIVersionPriorities { |
||||
completedConfig.GenericConfig.AggregatedDiscoveryGroupManager.SetGroupVersionPriority(metav1.GroupVersion(gv), int(entry.Group), int(entry.Version)) |
||||
} |
||||
} |
||||
|
||||
err = aggregatorServer.GenericAPIServer.AddPostStartHook("grafana-apiserver-autoregistration", func(context genericapiserver.PostStartHookContext) error { |
||||
go func() { |
||||
autoRegistrationController.Run(5, context.StopCh) |
||||
}() |
||||
return nil |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
err = aggregatorServer.GenericAPIServer.AddBootSequenceHealthChecks( |
||||
makeAPIServiceAvailableHealthCheck( |
||||
"autoregister-completion", |
||||
apiServices, |
||||
aggregatorServer.APIRegistrationInformers.Apiregistration().V1().APIServices(), |
||||
), |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
apiregistrationClient, err := apiregistrationclientset.NewForConfig(completedConfig.GenericConfig.LoopbackClientConfig) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
availableController, err := NewAvailableConditionController( |
||||
aggregatorServer.APIRegistrationInformers.Apiregistration().V1().APIServices(), |
||||
sharedInformerFactory.Service().V0alpha1().ExternalNames(), |
||||
apiregistrationClient.ApiregistrationV1(), |
||||
nil, |
||||
(func() ([]byte, []byte))(nil), |
||||
completedConfig.ExtraConfig.ServiceResolver, |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
aggregatorServer.GenericAPIServer.AddPostStartHookOrDie("apiservice-status-override-available-controller", func(context genericapiserver.PostStartHookContext) error { |
||||
// if we end up blocking for long periods of time, we may need to increase workers.
|
||||
go availableController.Run(5, context.StopCh) |
||||
return nil |
||||
}) |
||||
|
||||
aggregatorServer.GenericAPIServer.AddPostStartHookOrDie("start-grafana-aggregator-informers", func(context genericapiserver.PostStartHookContext) error { |
||||
sharedInformerFactory.Start(context.StopCh) |
||||
aggregatorServer.APIRegistrationInformers.Start(context.StopCh) |
||||
return nil |
||||
}) |
||||
|
||||
return aggregatorServer, nil |
||||
} |
||||
|
||||
func makeAPIService(gv schema.GroupVersion) *v1.APIService { |
||||
apiServicePriority, ok := APIVersionPriorities[gv] |
||||
if !ok { |
||||
// if we aren't found, then we shouldn't register ourselves because it could result in a CRD group version
|
||||
// being permanently stuck in the APIServices list.
|
||||
klog.Infof("Skipping APIService creation for %v", gv) |
||||
return nil |
||||
} |
||||
return &v1.APIService{ |
||||
ObjectMeta: metav1.ObjectMeta{Name: gv.Version + "." + gv.Group}, |
||||
Spec: v1.APIServiceSpec{ |
||||
Group: gv.Group, |
||||
Version: gv.Version, |
||||
GroupPriorityMinimum: apiServicePriority.Group, |
||||
VersionPriority: apiServicePriority.Version, |
||||
}, |
||||
} |
||||
} |
||||
|
||||
// makeAPIServiceAvailableHealthCheck returns a healthz check that returns healthy
|
||||
// once all of the specified services have been observed to be available at least once.
|
||||
func makeAPIServiceAvailableHealthCheck(name string, apiServices []*v1.APIService, apiServiceInformer apiregistrationInformers.APIServiceInformer) healthz.HealthChecker { |
||||
// Track the auto-registered API services that have not been observed to be available yet
|
||||
pendingServiceNamesLock := &sync.RWMutex{} |
||||
pendingServiceNames := sets.NewString() |
||||
for _, service := range apiServices { |
||||
pendingServiceNames.Insert(service.Name) |
||||
} |
||||
|
||||
// When an APIService in the list is seen as available, remove it from the pending list
|
||||
handleAPIServiceChange := func(service *v1.APIService) { |
||||
pendingServiceNamesLock.Lock() |
||||
defer pendingServiceNamesLock.Unlock() |
||||
if !pendingServiceNames.Has(service.Name) { |
||||
return |
||||
} |
||||
if v1helper.IsAPIServiceConditionTrue(service, v1.Available) { |
||||
pendingServiceNames.Delete(service.Name) |
||||
} |
||||
} |
||||
|
||||
// Watch add/update events for APIServices
|
||||
_, _ = apiServiceInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ |
||||
AddFunc: func(obj interface{}) { handleAPIServiceChange(obj.(*v1.APIService)) }, |
||||
UpdateFunc: func(old, new interface{}) { handleAPIServiceChange(new.(*v1.APIService)) }, |
||||
}) |
||||
|
||||
// Don't return healthy until the pending list is empty
|
||||
return healthz.NamedCheck(name, func(r *http.Request) error { |
||||
pendingServiceNamesLock.RLock() |
||||
defer pendingServiceNamesLock.RUnlock() |
||||
if pendingServiceNames.Len() > 0 { |
||||
return fmt.Errorf("missing APIService: %v", pendingServiceNames.List()) |
||||
} |
||||
return nil |
||||
}) |
||||
} |
||||
|
||||
// Priority defines group Priority that is used in discovery. This controls
|
||||
// group position in the kubectl output.
|
||||
type Priority struct { |
||||
// Group indicates the order of the Group relative to other groups.
|
||||
Group int32 |
||||
// Version indicates the relative order of the Version inside of its group.
|
||||
Version int32 |
||||
} |
||||
|
||||
// APIVersionPriorities are the proper way to resolve this letting the aggregator know the desired group and version-within-group order of the underlying servers
|
||||
// is to refactor the genericapiserver.DelegationTarget to include a list of priorities based on which APIs were installed.
|
||||
// This requires the APIGroupInfo struct to evolve and include the concept of priorities and to avoid mistakes, the core storage map there needs to be updated.
|
||||
// That ripples out every bit as far as you'd expect, so for 1.7 we'll include the list here instead of being built up during storage.
|
||||
var APIVersionPriorities = map[schema.GroupVersion]Priority{ |
||||
{Group: "", Version: "v1"}: {Group: 18000, Version: 1}, |
||||
// to my knowledge, nothing below here collides
|
||||
{Group: "admissionregistration.k8s.io", Version: "v1"}: {Group: 16700, Version: 15}, |
||||
{Group: "admissionregistration.k8s.io", Version: "v1beta1"}: {Group: 16700, Version: 12}, |
||||
{Group: "admissionregistration.k8s.io", Version: "v1alpha1"}: {Group: 16700, Version: 9}, |
||||
// Append a new group to the end of the list if unsure.
|
||||
// You can use min(existing group)-100 as the initial value for a group.
|
||||
// Version can be set to 9 (to have space around) for a new group.
|
||||
} |
||||
|
||||
func apiServicesToRegister(delegateAPIServer genericapiserver.DelegationTarget, registration autoregister.AutoAPIServiceRegistration) []*v1.APIService { |
||||
apiServices := []*v1.APIService{} |
||||
|
||||
for _, curr := range delegateAPIServer.ListedPaths() { |
||||
if curr == "/api/v1" { |
||||
apiService := makeAPIService(schema.GroupVersion{Group: "", Version: "v1"}) |
||||
registration.AddAPIServiceToSyncOnStart(apiService) |
||||
apiServices = append(apiServices, apiService) |
||||
continue |
||||
} |
||||
|
||||
if !strings.HasPrefix(curr, "/apis/") { |
||||
continue |
||||
} |
||||
// this comes back in a list that looks like /apis/rbac.authorization.k8s.io/v1alpha1
|
||||
tokens := strings.Split(curr, "/") |
||||
if len(tokens) != 4 { |
||||
continue |
||||
} |
||||
|
||||
apiService := makeAPIService(schema.GroupVersion{Group: tokens[2], Version: tokens[3]}) |
||||
if apiService == nil { |
||||
continue |
||||
} |
||||
registration.AddAPIServiceToSyncOnStart(apiService) |
||||
apiServices = append(apiServices, apiService) |
||||
} |
||||
|
||||
return apiServices |
||||
} |
||||
|
||||
// NOTE: below function imported from https://github.com/kubernetes/kubernetes/blob/master/cmd/kube-apiserver/app/server.go#L197
|
||||
// createProxyTransport creates the dialer infrastructure to connect to the api servers.
|
||||
func createProxyTransport() *http.Transport { |
||||
// NOTE: We don't set proxyDialerFn but the below SetTransportDefaults will
|
||||
// See https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apimachinery/pkg/util/net/http.go#L109
|
||||
var proxyDialerFn utilnet.DialFunc |
||||
// Proxying to services is IP-based... don't expect to be able to verify the hostname
|
||||
proxyTLSClientConfig := &tls.Config{InsecureSkipVerify: true} |
||||
proxyTransport := utilnet.SetTransportDefaults(&http.Transport{ |
||||
DialContext: proxyDialerFn, |
||||
TLSClientConfig: proxyTLSClientConfig, |
||||
}) |
||||
return proxyTransport |
||||
} |
||||
@ -0,0 +1,48 @@ |
||||
package apiserver |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net" |
||||
"path/filepath" |
||||
"strconv" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/options" |
||||
"github.com/grafana/grafana/pkg/services/featuremgmt" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
) |
||||
|
||||
func applyGrafanaConfig(cfg *setting.Cfg, features featuremgmt.FeatureToggles, o *options.Options) { |
||||
defaultLogLevel := 0 |
||||
ip := net.ParseIP(cfg.HTTPAddr) |
||||
apiURL := cfg.AppURL |
||||
port, err := strconv.Atoi(cfg.HTTPPort) |
||||
if err != nil { |
||||
port = 3000 |
||||
} |
||||
|
||||
if cfg.Env == setting.Dev { |
||||
defaultLogLevel = 10 |
||||
port = 6443 |
||||
ip = net.ParseIP("127.0.0.1") |
||||
apiURL = fmt.Sprintf("https://%s:%d", ip, port) |
||||
} |
||||
|
||||
host := fmt.Sprintf("%s:%d", ip, port) |
||||
|
||||
o.RecommendedOptions.Etcd.StorageConfig.Transport.ServerList = cfg.SectionWithEnvOverrides("grafana-apiserver").Key("etcd_servers").Strings(",") |
||||
|
||||
o.RecommendedOptions.SecureServing.BindAddress = ip |
||||
o.RecommendedOptions.SecureServing.BindPort = port |
||||
o.RecommendedOptions.Authentication.RemoteKubeConfigFileOptional = true |
||||
o.RecommendedOptions.Authorization.RemoteKubeConfigFileOptional = true |
||||
|
||||
o.RecommendedOptions.Admission = nil |
||||
o.RecommendedOptions.CoreAPI = nil |
||||
|
||||
o.StorageOptions.StorageType = options.StorageType(cfg.SectionWithEnvOverrides("grafana-apiserver").Key("storage_type").MustString(string(options.StorageTypeLegacy))) |
||||
o.StorageOptions.DataPath = filepath.Join(cfg.DataPath, "grafana-apiserver") |
||||
o.ExtraOptions.DevMode = features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerEnsureKubectlAccess) |
||||
o.ExtraOptions.ExternalAddress = host |
||||
o.ExtraOptions.APIURL = apiURL |
||||
o.ExtraOptions.Verbosity = defaultLogLevel |
||||
} |
||||
@ -0,0 +1,112 @@ |
||||
package options |
||||
|
||||
import ( |
||||
"github.com/spf13/pflag" |
||||
v1 "k8s.io/api/apps/v1" |
||||
"k8s.io/apimachinery/pkg/runtime" |
||||
"k8s.io/apimachinery/pkg/runtime/schema" |
||||
openapinamer "k8s.io/apiserver/pkg/endpoints/openapi" |
||||
genericfeatures "k8s.io/apiserver/pkg/features" |
||||
genericapiserver "k8s.io/apiserver/pkg/server" |
||||
"k8s.io/apiserver/pkg/server/options" |
||||
"k8s.io/apiserver/pkg/server/resourceconfig" |
||||
utilfeature "k8s.io/apiserver/pkg/util/feature" |
||||
apiregistrationv1beta1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1" |
||||
aggregatorapiserver "k8s.io/kube-aggregator/pkg/apiserver" |
||||
aggregatorscheme "k8s.io/kube-aggregator/pkg/apiserver/scheme" |
||||
aggregatoropenapi "k8s.io/kube-aggregator/pkg/generated/openapi" |
||||
"k8s.io/kube-openapi/pkg/common" |
||||
|
||||
servicev0alpha1 "github.com/grafana/grafana/pkg/apis/service/v0alpha1" |
||||
filestorage "github.com/grafana/grafana/pkg/services/apiserver/storage/file" |
||||
) |
||||
|
||||
// AggregatorServerOptions contains the state for the aggregator apiserver
|
||||
type AggregatorServerOptions struct { |
||||
AlternateDNS []string |
||||
ProxyClientCertFile string |
||||
ProxyClientKeyFile string |
||||
} |
||||
|
||||
func NewAggregatorServerOptions() *AggregatorServerOptions { |
||||
return &AggregatorServerOptions{} |
||||
} |
||||
|
||||
func (o *AggregatorServerOptions) getMergedOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { |
||||
aggregatorAPIs := aggregatoropenapi.GetOpenAPIDefinitions(ref) |
||||
return aggregatorAPIs |
||||
} |
||||
|
||||
func (o *AggregatorServerOptions) AddFlags(fs *pflag.FlagSet) { |
||||
if o == nil { |
||||
return |
||||
} |
||||
|
||||
fs.StringVar(&o.ProxyClientCertFile, "proxy-client-cert-file", o.ProxyClientCertFile, |
||||
"path to proxy client cert file") |
||||
|
||||
fs.StringVar(&o.ProxyClientKeyFile, "proxy-client-key-file", o.ProxyClientKeyFile, |
||||
"path to proxy client cert file") |
||||
} |
||||
|
||||
func (o *AggregatorServerOptions) Validate() []error { |
||||
if o == nil { |
||||
return nil |
||||
} |
||||
|
||||
// TODO: do we need to validate anything here?
|
||||
return nil |
||||
} |
||||
|
||||
func (o *AggregatorServerOptions) ApplyTo(aggregatorConfig *aggregatorapiserver.Config, etcdOpts *options.EtcdOptions, dataPath string) error { |
||||
genericConfig := aggregatorConfig.GenericConfig |
||||
|
||||
genericConfig.PostStartHooks = map[string]genericapiserver.PostStartHookConfigEntry{} |
||||
genericConfig.RESTOptionsGetter = nil |
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StorageVersionAPI) && |
||||
utilfeature.DefaultFeatureGate.Enabled(genericfeatures.APIServerIdentity) { |
||||
// Add StorageVersionPrecondition handler to aggregator-apiserver.
|
||||
// The handler will block write requests to built-in resources until the
|
||||
// target resources' storage versions are up-to-date.
|
||||
genericConfig.BuildHandlerChainFunc = genericapiserver.BuildHandlerChainWithStorageVersionPrecondition |
||||
} |
||||
|
||||
// copy the etcd options so we don't mutate originals.
|
||||
// we assume that the etcd options have been completed already. avoid messing with anything outside
|
||||
// of changes to StorageConfig as that may lead to unexpected behavior when the options are applied.
|
||||
etcdOptions := *etcdOpts |
||||
etcdOptions.StorageConfig.Codec = aggregatorscheme.Codecs.LegacyCodec(v1.SchemeGroupVersion, |
||||
apiregistrationv1beta1.SchemeGroupVersion, |
||||
servicev0alpha1.SchemeGroupVersion) |
||||
etcdOptions.StorageConfig.EncodeVersioner = runtime.NewMultiGroupVersioner(v1.SchemeGroupVersion, |
||||
schema.GroupKind{Group: apiregistrationv1beta1.GroupName}, |
||||
schema.GroupKind{Group: servicev0alpha1.GROUP}) |
||||
etcdOptions.SkipHealthEndpoints = true // avoid double wiring of health checks
|
||||
if err := etcdOptions.ApplyTo(&genericConfig.Config); err != nil { |
||||
return err |
||||
} |
||||
// override the RESTOptionsGetter to use the file storage options getter
|
||||
aggregatorConfig.GenericConfig.RESTOptionsGetter = filestorage.NewRESTOptionsGetter(dataPath, etcdOptions.StorageConfig) |
||||
|
||||
// prevent generic API server from installing the OpenAPI handler. Aggregator server has its own customized OpenAPI handler.
|
||||
genericConfig.SkipOpenAPIInstallation = true |
||||
mergedResourceConfig, err := resourceconfig.MergeAPIResourceConfigs(aggregatorapiserver.DefaultAPIResourceConfigSource(), nil, aggregatorscheme.Scheme) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
genericConfig.MergedResourceConfig = mergedResourceConfig |
||||
|
||||
namer := openapinamer.NewDefinitionNamer(aggregatorscheme.Scheme) |
||||
genericConfig.OpenAPIV3Config = genericapiserver.DefaultOpenAPIV3Config(o.getMergedOpenAPIDefinitions, namer) |
||||
genericConfig.OpenAPIV3Config.Info.Title = "Kubernetes" |
||||
genericConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(o.getMergedOpenAPIDefinitions, namer) |
||||
genericConfig.OpenAPIConfig.Info.Title = "Kubernetes" |
||||
genericConfig.PostStartHooks = map[string]genericapiserver.PostStartHookConfigEntry{} |
||||
|
||||
// These hooks use v1 informers, which are not available in the grafana aggregator.
|
||||
genericConfig.DisabledPostStartHooks = genericConfig.DisabledPostStartHooks.Insert("apiservice-status-available-controller") |
||||
genericConfig.DisabledPostStartHooks = genericConfig.DisabledPostStartHooks.Insert("start-kube-aggregator-informers") |
||||
|
||||
return nil |
||||
} |
||||
@ -0,0 +1,46 @@ |
||||
package options |
||||
|
||||
import ( |
||||
"strconv" |
||||
|
||||
"github.com/go-logr/logr" |
||||
"github.com/spf13/pflag" |
||||
genericapiserver "k8s.io/apiserver/pkg/server" |
||||
"k8s.io/component-base/logs" |
||||
"k8s.io/klog/v2" |
||||
) |
||||
|
||||
type ExtraOptions struct { |
||||
DevMode bool |
||||
ExternalAddress string |
||||
APIURL string |
||||
Verbosity int |
||||
} |
||||
|
||||
func NewExtraOptions() *ExtraOptions { |
||||
return &ExtraOptions{ |
||||
DevMode: false, |
||||
Verbosity: 0, |
||||
} |
||||
} |
||||
|
||||
func (o *ExtraOptions) AddFlags(fs *pflag.FlagSet) { |
||||
fs.BoolVar(&o.DevMode, "grafana-apiserver-dev-mode", o.DevMode, "Enable dev mode") |
||||
fs.StringVar(&o.ExternalAddress, "grafana-apiserver-host", o.ExternalAddress, "Host") |
||||
fs.StringVar(&o.APIURL, "grafana-apiserver-api-url", o.APIURL, "API URL") |
||||
fs.IntVar(&o.Verbosity, "verbosity", o.Verbosity, "Verbosity") |
||||
} |
||||
|
||||
func (o *ExtraOptions) Validate() []error { |
||||
return nil |
||||
} |
||||
|
||||
func (o *ExtraOptions) ApplyTo(c *genericapiserver.RecommendedConfig) error { |
||||
logger := logr.New(newLogAdapter(o.Verbosity)) |
||||
klog.SetLoggerWithOptions(logger, klog.ContextualLogger(true)) |
||||
if _, err := logs.GlogSetter(strconv.Itoa(o.Verbosity)); err != nil { |
||||
logger.Error(err, "failed to set log level") |
||||
} |
||||
c.ExternalAddress = o.ExternalAddress |
||||
return nil |
||||
} |
||||
@ -1,4 +1,4 @@ |
||||
package grafanaapiserver |
||||
package options |
||||
|
||||
import ( |
||||
"strings" |
||||
@ -0,0 +1,126 @@ |
||||
package options |
||||
|
||||
import ( |
||||
"net" |
||||
|
||||
"github.com/spf13/pflag" |
||||
"k8s.io/apimachinery/pkg/runtime" |
||||
"k8s.io/apiserver/pkg/endpoints/discovery/aggregated" |
||||
genericapiserver "k8s.io/apiserver/pkg/server" |
||||
genericoptions "k8s.io/apiserver/pkg/server/options" |
||||
) |
||||
|
||||
const defaultEtcdPathPrefix = "/registry/grafana.app" |
||||
|
||||
type Options struct { |
||||
RecommendedOptions *genericoptions.RecommendedOptions |
||||
AggregatorOptions *AggregatorServerOptions |
||||
StorageOptions *StorageOptions |
||||
ExtraOptions *ExtraOptions |
||||
} |
||||
|
||||
func NewOptions(codec runtime.Codec) *Options { |
||||
return &Options{ |
||||
RecommendedOptions: genericoptions.NewRecommendedOptions( |
||||
defaultEtcdPathPrefix, |
||||
codec, |
||||
), |
||||
AggregatorOptions: NewAggregatorServerOptions(), |
||||
StorageOptions: NewStorageOptions(), |
||||
ExtraOptions: NewExtraOptions(), |
||||
} |
||||
} |
||||
|
||||
func (o *Options) AddFlags(fs *pflag.FlagSet) { |
||||
o.RecommendedOptions.AddFlags(fs) |
||||
o.AggregatorOptions.AddFlags(fs) |
||||
o.StorageOptions.AddFlags(fs) |
||||
o.ExtraOptions.AddFlags(fs) |
||||
} |
||||
|
||||
func (o *Options) Validate() []error { |
||||
if errs := o.ExtraOptions.Validate(); len(errs) != 0 { |
||||
return errs |
||||
} |
||||
|
||||
if errs := o.StorageOptions.Validate(); len(errs) != 0 { |
||||
return errs |
||||
} |
||||
|
||||
if errs := o.AggregatorOptions.Validate(); len(errs) != 0 { |
||||
return errs |
||||
} |
||||
|
||||
if errs := o.RecommendedOptions.SecureServing.Validate(); len(errs) != 0 { |
||||
return errs |
||||
} |
||||
|
||||
if errs := o.RecommendedOptions.Authentication.Validate(); len(errs) != 0 { |
||||
return errs |
||||
} |
||||
|
||||
if o.StorageOptions.StorageType == StorageTypeEtcd { |
||||
if errs := o.RecommendedOptions.Etcd.Validate(); len(errs) != 0 { |
||||
return errs |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (o *Options) ApplyTo(serverConfig *genericapiserver.RecommendedConfig) error { |
||||
serverConfig.AggregatedDiscoveryGroupManager = aggregated.NewResourceManager("apis") |
||||
|
||||
if err := o.ExtraOptions.ApplyTo(serverConfig); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if !o.ExtraOptions.DevMode { |
||||
o.RecommendedOptions.SecureServing.Listener = newFakeListener() |
||||
} |
||||
|
||||
if err := o.RecommendedOptions.SecureServing.ApplyTo(&serverConfig.SecureServing, &serverConfig.LoopbackClientConfig); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err := o.RecommendedOptions.Authentication.ApplyTo(&serverConfig.Authentication, serverConfig.SecureServing, serverConfig.OpenAPIConfig); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if !o.ExtraOptions.DevMode { |
||||
if err := serverConfig.SecureServing.Listener.Close(); err != nil { |
||||
return err |
||||
} |
||||
serverConfig.SecureServing = nil |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
type fakeListener struct { |
||||
server net.Conn |
||||
client net.Conn |
||||
} |
||||
|
||||
func newFakeListener() *fakeListener { |
||||
server, client := net.Pipe() |
||||
return &fakeListener{ |
||||
server: server, |
||||
client: client, |
||||
} |
||||
} |
||||
|
||||
func (f *fakeListener) Accept() (net.Conn, error) { |
||||
return f.server, nil |
||||
} |
||||
|
||||
func (f *fakeListener) Close() error { |
||||
if err := f.client.Close(); err != nil { |
||||
return err |
||||
} |
||||
return f.server.Close() |
||||
} |
||||
|
||||
func (f *fakeListener) Addr() net.Addr { |
||||
return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 3000, Zone: ""} |
||||
} |
||||
@ -0,0 +1,51 @@ |
||||
package options |
||||
|
||||
import ( |
||||
"fmt" |
||||
|
||||
"github.com/spf13/pflag" |
||||
genericapiserver "k8s.io/apiserver/pkg/server" |
||||
"k8s.io/apiserver/pkg/server/options" |
||||
) |
||||
|
||||
type StorageType string |
||||
|
||||
const ( |
||||
StorageTypeFile StorageType = "file" |
||||
StorageTypeEtcd StorageType = "etcd" |
||||
StorageTypeLegacy StorageType = "legacy" |
||||
StorageTypeUnified StorageType = "unified" |
||||
StorageTypeUnifiedGrpc StorageType = "unified-grpc" |
||||
) |
||||
|
||||
type StorageOptions struct { |
||||
StorageType StorageType |
||||
DataPath string |
||||
} |
||||
|
||||
func NewStorageOptions() *StorageOptions { |
||||
return &StorageOptions{ |
||||
StorageType: StorageTypeLegacy, |
||||
} |
||||
} |
||||
|
||||
func (o *StorageOptions) AddFlags(fs *pflag.FlagSet) { |
||||
fs.StringVar((*string)(&o.StorageType), "grafana-apiserver-storage-type", string(o.StorageType), "Storage type") |
||||
fs.StringVar((*string)(&o.StorageType), "grafana-apiserver-storage-path", string(o.StorageType), "Storage path for file storage") |
||||
} |
||||
|
||||
func (o *StorageOptions) Validate() []error { |
||||
errs := []error{} |
||||
switch o.StorageType { |
||||
case StorageTypeFile, StorageTypeEtcd, StorageTypeLegacy, StorageTypeUnified, StorageTypeUnifiedGrpc: |
||||
// no-op
|
||||
default: |
||||
errs = append(errs, fmt.Errorf("--grafana-apiserver-storage-type must be one of %s, %s, %s, %s, %s", StorageTypeFile, StorageTypeEtcd, StorageTypeLegacy, StorageTypeUnified, StorageTypeUnifiedGrpc)) |
||||
} |
||||
return errs |
||||
} |
||||
|
||||
func (o *StorageOptions) ApplyTo(serverConfig *genericapiserver.RecommendedConfig, etcdOptions *options.EtcdOptions) error { |
||||
// TODO: move storage setup here
|
||||
return nil |
||||
} |
||||
@ -1,13 +1,15 @@ |
||||
package grafanaapiserver |
||||
package apiserver |
||||
|
||||
import ( |
||||
"github.com/google/wire" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder" |
||||
) |
||||
|
||||
var WireSet = wire.NewSet( |
||||
ProvideService, |
||||
wire.Bind(new(RestConfigProvider), new(*service)), |
||||
wire.Bind(new(Service), new(*service)), |
||||
wire.Bind(new(APIRegistrar), new(*service)), |
||||
wire.Bind(new(DirectRestConfigProvider), new(*service)), |
||||
wire.Bind(new(builder.APIRegistrar), new(*service)), |
||||
) |
||||
@ -1,60 +0,0 @@ |
||||
package grafanaapiserver |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net" |
||||
"path/filepath" |
||||
"strconv" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
) |
||||
|
||||
type config struct { |
||||
enabled bool |
||||
devMode bool |
||||
|
||||
ip net.IP |
||||
port int |
||||
host string |
||||
apiURL string |
||||
|
||||
storageType StorageType |
||||
|
||||
etcdServers []string |
||||
dataPath string |
||||
|
||||
logLevel int |
||||
} |
||||
|
||||
func newConfig(cfg *setting.Cfg, features featuremgmt.FeatureToggles) *config { |
||||
defaultLogLevel := 0 |
||||
ip := net.ParseIP(cfg.HTTPAddr) |
||||
apiURL := cfg.AppURL |
||||
port, err := strconv.Atoi(cfg.HTTPPort) |
||||
if err != nil { |
||||
port = 3000 |
||||
} |
||||
|
||||
if cfg.Env == setting.Dev { |
||||
defaultLogLevel = 10 |
||||
port = 6443 |
||||
ip = net.ParseIP("127.0.0.1") |
||||
apiURL = fmt.Sprintf("https://%s:%d", ip, port) |
||||
} |
||||
|
||||
host := fmt.Sprintf("%s:%d", ip, port) |
||||
|
||||
return &config{ |
||||
enabled: true, |
||||
devMode: features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerEnsureKubectlAccess), |
||||
dataPath: filepath.Join(cfg.DataPath, "grafana-apiserver"), |
||||
ip: ip, |
||||
port: port, |
||||
host: host, |
||||
logLevel: cfg.SectionWithEnvOverrides("grafana-apiserver").Key("log_level").MustInt(defaultLogLevel), |
||||
etcdServers: cfg.SectionWithEnvOverrides("grafana-apiserver").Key("etcd_servers").Strings(","), |
||||
storageType: StorageType(cfg.SectionWithEnvOverrides("grafana-apiserver").Key("storage_type").MustString(string(StorageTypeLegacy))), |
||||
apiURL: apiURL, |
||||
} |
||||
} |
||||
@ -1,40 +0,0 @@ |
||||
package grafanaapiserver |
||||
|
||||
import ( |
||||
"net" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt" |
||||
"github.com/grafana/grafana/pkg/setting" |
||||
) |
||||
|
||||
func TestNewConfig(t *testing.T) { |
||||
cfg := setting.NewCfg() |
||||
cfg.Env = setting.Prod |
||||
cfg.DataPath = "/tmp/grafana" |
||||
cfg.HTTPAddr = "10.0.0.1" |
||||
cfg.HTTPPort = "4000" |
||||
cfg.AppURL = "http://test:4000" |
||||
|
||||
section := cfg.Raw.Section("grafana-apiserver") |
||||
section.Key("log_level").SetValue("5") |
||||
section.Key("etcd_servers").SetValue("http://localhost:2379") |
||||
|
||||
actual := newConfig(cfg, featuremgmt.WithFeatures()) |
||||
|
||||
expected := &config{ |
||||
enabled: true, |
||||
devMode: false, |
||||
storageType: StorageTypeLegacy, |
||||
etcdServers: []string{"http://localhost:2379"}, |
||||
apiURL: "http://test:4000", |
||||
ip: net.ParseIP("10.0.0.1"), |
||||
port: 4000, |
||||
host: "10.0.0.1:4000", |
||||
dataPath: "/tmp/grafana/grafana-apiserver", |
||||
logLevel: 5, |
||||
} |
||||
require.Equal(t, expected, actual) |
||||
} |
||||
Loading…
Reference in new issue