diff --git a/PROJECT b/PROJECT index 6e69fa30de..7c3d4759a1 100644 --- a/PROJECT +++ b/PROJECT @@ -285,6 +285,18 @@ resources: webhooks: validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: victoriametrics.com + group: operator + kind: VMMCP + path: github.com/VictoriaMetrics/operator/api/operator/v1 + version: v1 + webhooks: + validation: true + webhookVersion: v1 - api: crdVersion: v1 namespaced: true diff --git a/api/client/informers/externalversions/generic.go b/api/client/informers/externalversions/generic.go index 0aea9b8b72..0a658b2ed5 100644 --- a/api/client/informers/externalversions/generic.go +++ b/api/client/informers/externalversions/generic.go @@ -62,6 +62,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Operator().V1().VLSingles().Informer()}, nil case v1.SchemeGroupVersion.WithResource("vmanomalies"): return &genericInformer{resource: resource.GroupResource(), informer: f.Operator().V1().VMAnomalies().Informer()}, nil + case v1.SchemeGroupVersion.WithResource("vmmcps"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Operator().V1().VMMCPs().Informer()}, nil case v1.SchemeGroupVersion.WithResource("vtagents"): return &genericInformer{resource: resource.GroupResource(), informer: f.Operator().V1().VTAgents().Informer()}, nil case v1.SchemeGroupVersion.WithResource("vtclusters"): diff --git a/api/client/informers/externalversions/operator/v1/interface.go b/api/client/informers/externalversions/operator/v1/interface.go index a91b1ae545..cbcf19341f 100644 --- a/api/client/informers/externalversions/operator/v1/interface.go +++ b/api/client/informers/externalversions/operator/v1/interface.go @@ -31,6 +31,8 @@ type Interface interface { VLSingles() TypedVLSingleInformer // VMAnomalies returns a VMAnomalyInformer. VMAnomalies() TypedVMAnomalyInformer + // VMMCPs returns a VMMCPInformer. + VMMCPs() TypedVMMCPInformer // VTAgents returns a VTAgentInformer. VTAgents() TypedVTAgentInformer // VTClusters returns a VTClusterInformer. @@ -70,6 +72,11 @@ func (v *version) VMAnomalies() TypedVMAnomalyInformer { return &vMAnomalyInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } +// VMMCPs returns a TypedVMMCPInformer. +func (v *version) VMMCPs() TypedVMMCPInformer { + return &vMMCPInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // VTAgents returns a TypedVTAgentInformer. func (v *version) VTAgents() TypedVTAgentInformer { return &vTAgentInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} diff --git a/api/client/informers/externalversions/operator/v1/vmmcp.go b/api/client/informers/externalversions/operator/v1/vmmcp.go new file mode 100644 index 0000000000..7bd6663cc4 --- /dev/null +++ b/api/client/informers/externalversions/operator/v1/vmmcp.go @@ -0,0 +1,207 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by informer-gen-v0.37. DO NOT EDIT. + +package v1 + +import ( + context "context" + time "time" + + internalinterfaces "github.com/VictoriaMetrics/operator/api/client/informers/externalversions/internalinterfaces" + operatorv1 "github.com/VictoriaMetrics/operator/api/client/listers/operator/v1" + versioned "github.com/VictoriaMetrics/operator/api/client/versioned" + apioperatorv1 "github.com/VictoriaMetrics/operator/api/operator/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// VMMCPInformer provides access to a shared informer and lister for +// VMMCPs. Prefer using the type-safe variant (see [TypedVMMCPInformer]). +type VMMCPInformer interface { + Informer() cache.SharedIndexInformer + Lister() operatorv1.VMMCPLister +} + +// TypedVMMCPInformer provides access to a shared informer and lister for +// VMMCPs, including the type-safe TypedInformer variant. +// It is a superset of VMMCPInformer. +type TypedVMMCPInformer interface { + Informer() cache.SharedIndexInformer + TypedInformer() VMMCPIndexInformer + Lister() operatorv1.VMMCPLister +} + +// VMMCPIndexInformer is a wrapper around the underlying [cache.SharedIndexInformer] +// with type-safe variants of several methods. +type VMMCPIndexInformer cache.TypedSharedIndexInformer[*apioperatorv1.VMMCP] + +// VMMCPHandlerFuncs is a specialization of [cache.TypedResourceEventHandlerFuncs] for VMMCP. +type VMMCPHandlerFuncs = cache.TypedResourceEventHandlerFuncs[*apioperatorv1.VMMCP] + +// VMMCPDetailedHandlerFuncs is a specialization of [cache.TypedResourceEventHandlerDetailedFuncs] for VMMCP. +type VMMCPDetailedHandlerFuncs = cache.TypedResourceEventHandlerDetailedFuncs[*apioperatorv1.VMMCP] + +// VMMCPFilteringHandler is a specialization of [cache.TypedFilteringResourceEventHandler] for VMMCP. +type VMMCPFilteringHandler = cache.TypedFilteringResourceEventHandler[*apioperatorv1.VMMCP] + +// VMMCPIndexers is a specialization of [cache.TypedIndexers] for VMMCP. +type VMMCPIndexers = cache.TypedIndexers[*apioperatorv1.VMMCP] + +// DeletedVMMCP is a specialization of [cache.DeletedObject] for VMMCP. +type DeletedVMMCP = cache.DeletedObject[*apioperatorv1.VMMCP] + +type vMMCPInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewVMMCPInformer constructs a new informer for VMMCP type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +// If you really need an independent one, prefer using the type-safe variant (see [NewTypedVMMCPInformer]). +func NewVMMCPInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewVMMCPInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers}) +} + +// NewTypedVMMCPInformer constructs a new informer for VMMCP type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewTypedVMMCPInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers VMMCPIndexers) VMMCPIndexInformer { + return NewTypedVMMCPInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.TypedIndexersToIndexers(indexers)}) +} + +// NewFilteredVMMCPInformer constructs a new informer for VMMCP type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +// If you really need an independent one, prefer using the type-safe variant (see [NewTypedFilteredVMMCPInformer]). +func NewFilteredVMMCPInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return NewTypedVMMCPInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers, TweakListOptions: tweakListOptions}) +} + +// NewTypedFilteredVMMCPInformer constructs a new informer for VMMCP type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewTypedFilteredVMMCPInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers VMMCPIndexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) VMMCPIndexInformer { + return NewTypedVMMCPInformerWithOptions(client, namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.TypedIndexersToIndexers(indexers), TweakListOptions: tweakListOptions}) +} + +// NewVMMCPInformerWithOptions constructs a new informer for VMMCP type with additional options. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +// If you really need an independent one, prefer using the type-safe variant (see [NewTypedVMMCPInformerWithOptions]). +func NewVMMCPInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) cache.SharedIndexInformer { + return NewTypedVMMCPInformerWithOptions(client, namespace, options) +} + +// NewTypedVMMCPInformerWithOptions constructs a new informer for VMMCP type with additional options. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewTypedVMMCPInformerWithOptions(client versioned.Interface, namespace string, options internalinterfaces.InformerOptions) VMMCPIndexInformer { + gvr := schema.GroupVersionResource{Group: "operator", Version: "v1", Resource: "vmmcps"} + identifier := options.InformerName.WithResource(gvr) + tweakListOptions := options.TweakListOptions + return cache.NewTypedSharedIndexInformer[*apioperatorv1.VMMCP](cache.NewSharedIndexInformerWithOptions( + cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ + ListFunc: func(opts metav1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&opts) + } + return client.OperatorV1().VMMCPs(namespace).List(context.Background(), opts) + }, + WatchFunc: func(opts metav1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&opts) + } + return client.OperatorV1().VMMCPs(namespace).Watch(context.Background(), opts) + }, + ListWithContextFunc: func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&opts) + } + return client.OperatorV1().VMMCPs(namespace).List(ctx, opts) + }, + WatchFuncWithContext: func(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&opts) + } + return client.OperatorV1().VMMCPs(namespace).Watch(ctx, opts) + }, + }, client), + &apioperatorv1.VMMCP{}, + cache.SharedIndexInformerOptions{ + ResyncPeriod: options.ResyncPeriod, + Indexers: options.Indexers, + Identifier: identifier, + }, + )) +} + +func (f *vMMCPInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewTypedVMMCPInformerWithOptions(client, f.namespace, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, InformerName: f.factory.InformerName(), TweakListOptions: f.tweakListOptions}) +} + +func (f *vMMCPInformer) Informer() cache.SharedIndexInformer { + return f.TypedInformer() +} + +func (f *vMMCPInformer) TypedInformer() VMMCPIndexInformer { + return cache.NewTypedSharedIndexInformer[*apioperatorv1.VMMCP](f.factory.InformerFor(&apioperatorv1.VMMCP{}, f.defaultInformer)) +} + +func (f *vMMCPInformer) Lister() operatorv1.VMMCPLister { + return operatorv1.NewVMMCPLister(f.Informer().GetIndexer()) +} + +// ToTypedVMMCPInformer converts an untyped informer into a TypedVMMCPInformer. +// +// WARNING: this conversion is only safe if the informer handles objects of type +// *VMMCP. If that is not the case, calling type-safe methods of the returned +// TypedVMMCPInformer leads to runtime panics. A safer alternative is to pass +// around a TypedVMMCPInformer instances that was obtained from a +// SharedInformerFactory. +func ToTypedVMMCPInformer(informer VMMCPInformer) TypedVMMCPInformer { + if informer, ok := informer.(TypedVMMCPInformer); ok { + return informer + } + return &vMMCPTypedInformerAdapter{informer} +} + +type vMMCPTypedInformerAdapter struct { + VMMCPInformer +} + +func (a *vMMCPTypedInformerAdapter) TypedInformer() VMMCPIndexInformer { + return cache.NewTypedSharedIndexInformer[*apioperatorv1.VMMCP](a.Informer()) +} + +// ToVMMCPIndexInformer converts an untyped informer into a VMMCPIndexInformer. +// +// WARNING: this conversion is only safe if the informer handles objects of type +// *VMMCP. If that is not the case, calling type-safe methods of the returned +// VMMCPIndexInformer leads to runtime panics. A safer alternative is to pass +// around a VMMCPIndexInformer instances that was obtained from a +// SharedInformerFactory. +func ToVMMCPIndexInformer(informer cache.SharedIndexInformer) VMMCPIndexInformer { + if informer, ok := informer.(VMMCPIndexInformer); ok { + return informer + } + return cache.NewTypedSharedIndexInformer[*apioperatorv1.VMMCP](informer) +} diff --git a/api/client/listers/operator/v1/expansion_generated.go b/api/client/listers/operator/v1/expansion_generated.go index ec2c2b9ea8..750fac4481 100644 --- a/api/client/listers/operator/v1/expansion_generated.go +++ b/api/client/listers/operator/v1/expansion_generated.go @@ -49,6 +49,14 @@ type VMAnomalyListerExpansion interface{} // VMAnomalyNamespaceLister. type VMAnomalyNamespaceListerExpansion interface{} +// VMMCPListerExpansion allows custom methods to be added to +// VMMCPLister. +type VMMCPListerExpansion interface{} + +// VMMCPNamespaceListerExpansion allows custom methods to be added to +// VMMCPNamespaceLister. +type VMMCPNamespaceListerExpansion interface{} + // VTAgentListerExpansion allows custom methods to be added to // VTAgentLister. type VTAgentListerExpansion interface{} diff --git a/api/client/listers/operator/v1/vmmcp.go b/api/client/listers/operator/v1/vmmcp.go new file mode 100644 index 0000000000..034358e8d2 --- /dev/null +++ b/api/client/listers/operator/v1/vmmcp.go @@ -0,0 +1,69 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by lister-gen-v0.37. DO NOT EDIT. + +package v1 + +import ( + operatorv1 "github.com/VictoriaMetrics/operator/api/operator/v1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// VMMCPLister helps list VMMCPs. +// All objects returned here must be treated as read-only. +type VMMCPLister interface { + // List lists all VMMCPs in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*operatorv1.VMMCP, err error) + // VMMCPs returns an object that can list and get VMMCPs. + VMMCPs(namespace string) VMMCPNamespaceLister + VMMCPListerExpansion +} + +// vMMCPLister implements the VMMCPLister interface. +type vMMCPLister struct { + listers.ResourceIndexer[*operatorv1.VMMCP] +} + +// NewVMMCPLister returns a new VMMCPLister. +func NewVMMCPLister(indexer cache.Indexer) VMMCPLister { + return &vMMCPLister{listers.New[*operatorv1.VMMCP](indexer, operatorv1.Resource("vmmcp"))} +} + +// VMMCPs returns an object that can list and get VMMCPs. +func (s *vMMCPLister) VMMCPs(namespace string) VMMCPNamespaceLister { + return vMMCPNamespaceLister{listers.NewNamespaced[*operatorv1.VMMCP](s.ResourceIndexer, namespace)} +} + +// VMMCPNamespaceLister helps list and get VMMCPs. +// All objects returned here must be treated as read-only. +type VMMCPNamespaceLister interface { + // List lists all VMMCPs in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*operatorv1.VMMCP, err error) + // Get retrieves the VMMCP from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*operatorv1.VMMCP, error) + VMMCPNamespaceListerExpansion +} + +// vMMCPNamespaceLister implements the VMMCPNamespaceLister +// interface. +type vMMCPNamespaceLister struct { + listers.ResourceIndexer[*operatorv1.VMMCP] +} diff --git a/api/client/versioned/typed/operator/v1/fake/fake_operator_client.go b/api/client/versioned/typed/operator/v1/fake/fake_operator_client.go index 5cc8a38780..66dfcde03c 100644 --- a/api/client/versioned/typed/operator/v1/fake/fake_operator_client.go +++ b/api/client/versioned/typed/operator/v1/fake/fake_operator_client.go @@ -43,6 +43,10 @@ func (c *FakeOperatorV1) VMAnomalies(namespace string) v1.VMAnomalyInterface { return newFakeVMAnomalies(c, namespace) } +func (c *FakeOperatorV1) VMMCPs(namespace string) v1.VMMCPInterface { + return newFakeVMMCPs(c, namespace) +} + func (c *FakeOperatorV1) VTAgents(namespace string) v1.VTAgentInterface { return newFakeVTAgents(c, namespace) } diff --git a/api/client/versioned/typed/operator/v1/fake/fake_vmmcp.go b/api/client/versioned/typed/operator/v1/fake/fake_vmmcp.go new file mode 100644 index 0000000000..10ff9c075c --- /dev/null +++ b/api/client/versioned/typed/operator/v1/fake/fake_vmmcp.go @@ -0,0 +1,47 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen-v0.37. DO NOT EDIT. + +package fake + +import ( + operatorv1 "github.com/VictoriaMetrics/operator/api/client/versioned/typed/operator/v1" + v1 "github.com/VictoriaMetrics/operator/api/operator/v1" + gentype "k8s.io/client-go/gentype" +) + +// fakeVMMCPs implements VMMCPInterface +type fakeVMMCPs struct { + *gentype.FakeClientWithList[*v1.VMMCP, *v1.VMMCPList] + Fake *FakeOperatorV1 +} + +func newFakeVMMCPs(fake *FakeOperatorV1, namespace string) operatorv1.VMMCPInterface { + return &fakeVMMCPs{ + gentype.NewFakeClientWithList[*v1.VMMCP, *v1.VMMCPList]( + fake.Fake, + namespace, + v1.SchemeGroupVersion.WithResource("vmmcps"), + v1.SchemeGroupVersion.WithKind("VMMCP"), + func() *v1.VMMCP { return &v1.VMMCP{} }, + func() *v1.VMMCPList { return &v1.VMMCPList{} }, + func(dst, src *v1.VMMCPList) { dst.ListMeta = src.ListMeta }, + func(list *v1.VMMCPList) []*v1.VMMCP { return gentype.ToPointerSlice(list.Items) }, + func(list *v1.VMMCPList, items []*v1.VMMCP) { list.Items = gentype.FromPointerSlice(items) }, + ), + fake, + } +} diff --git a/api/client/versioned/typed/operator/v1/generated_expansion.go b/api/client/versioned/typed/operator/v1/generated_expansion.go index 8a8e02a1b9..edb4cf56fa 100644 --- a/api/client/versioned/typed/operator/v1/generated_expansion.go +++ b/api/client/versioned/typed/operator/v1/generated_expansion.go @@ -25,6 +25,8 @@ type VLSingleExpansion interface{} type VMAnomalyExpansion interface{} +type VMMCPExpansion interface{} + type VTAgentExpansion interface{} type VTClusterExpansion interface{} diff --git a/api/client/versioned/typed/operator/v1/operator_client.go b/api/client/versioned/typed/operator/v1/operator_client.go index f857930775..b7ae468bc1 100644 --- a/api/client/versioned/typed/operator/v1/operator_client.go +++ b/api/client/versioned/typed/operator/v1/operator_client.go @@ -31,6 +31,7 @@ type OperatorV1Interface interface { VLClustersGetter VLSinglesGetter VMAnomaliesGetter + VMMCPsGetter VTAgentsGetter VTClustersGetter VTSinglesGetter @@ -57,6 +58,10 @@ func (c *OperatorV1Client) VMAnomalies(namespace string) VMAnomalyInterface { return newVMAnomalies(c, namespace) } +func (c *OperatorV1Client) VMMCPs(namespace string) VMMCPInterface { + return newVMMCPs(c, namespace) +} + func (c *OperatorV1Client) VTAgents(namespace string) VTAgentInterface { return newVTAgents(c, namespace) } diff --git a/api/client/versioned/typed/operator/v1/vmmcp.go b/api/client/versioned/typed/operator/v1/vmmcp.go new file mode 100644 index 0000000000..f7e7fcab44 --- /dev/null +++ b/api/client/versioned/typed/operator/v1/vmmcp.go @@ -0,0 +1,69 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen-v0.37. DO NOT EDIT. + +package v1 + +import ( + context "context" + + scheme "github.com/VictoriaMetrics/operator/api/client/versioned/scheme" + operatorv1 "github.com/VictoriaMetrics/operator/api/operator/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// VMMCPsGetter has a method to return a VMMCPInterface. +// A group's client should implement this interface. +type VMMCPsGetter interface { + VMMCPs(namespace string) VMMCPInterface +} + +// VMMCPInterface has methods to work with VMMCP resources. +type VMMCPInterface interface { + Create(ctx context.Context, vMMCP *operatorv1.VMMCP, opts metav1.CreateOptions) (*operatorv1.VMMCP, error) + Update(ctx context.Context, vMMCP *operatorv1.VMMCP, opts metav1.UpdateOptions) (*operatorv1.VMMCP, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, vMMCP *operatorv1.VMMCP, opts metav1.UpdateOptions) (*operatorv1.VMMCP, error) + Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error + Get(ctx context.Context, name string, opts metav1.GetOptions) (*operatorv1.VMMCP, error) + List(ctx context.Context, opts metav1.ListOptions) (*operatorv1.VMMCPList, error) + Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *operatorv1.VMMCP, err error) + VMMCPExpansion +} + +// vMMCPs implements VMMCPInterface +type vMMCPs struct { + *gentype.ClientWithList[*operatorv1.VMMCP, *operatorv1.VMMCPList] +} + +// newVMMCPs returns a VMMCPs +func newVMMCPs(c *OperatorV1Client, namespace string) *vMMCPs { + return &vMMCPs{ + gentype.NewClientWithList[*operatorv1.VMMCP, *operatorv1.VMMCPList]( + "vmmcps", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *operatorv1.VMMCP { return &operatorv1.VMMCP{} }, + func() *operatorv1.VMMCPList { return &operatorv1.VMMCPList{} }, + ), + } +} diff --git a/api/operator/v1/groupversion_info.go b/api/operator/v1/groupversion_info.go index 42002efd8d..52fa252f17 100644 --- a/api/operator/v1/groupversion_info.go +++ b/api/operator/v1/groupversion_info.go @@ -46,6 +46,7 @@ func addKnownTypes(scheme *runtime.Scheme) error { &VTCluster{}, &VTClusterList{}, &VMAnomaly{}, &VMAnomalyList{}, &VMAnomalyConfig{}, &VMAnomalyConfigList{}, + &VMMCP{}, &VMMCPList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/api/operator/v1/mcp_types.go b/api/operator/v1/mcp_types.go new file mode 100644 index 0000000000..b475097895 --- /dev/null +++ b/api/operator/v1/mcp_types.go @@ -0,0 +1,229 @@ +package v1 + +import ( + "fmt" + "strings" + "time" + + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/utils/ptr" + + vmv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1" +) + +// MCPServerMode defines transport of the MCP server +// +kubebuilder:validation:Enum=http;sse +type MCPServerMode string + +const ( + // MCPServerModeHTTP serves Streamable HTTP transport at /mcp path + MCPServerModeHTTP MCPServerMode = "http" + // MCPServerModeSSE serves Server-Sent Events transport at /sse and /message paths + MCPServerModeSSE MCPServerMode = "sse" +) + +const ( + // MCPLivenessPath defines path of the MCP server liveness check + MCPLivenessPath = "/health/liveness" + mcpReadinessPath = "/health/readiness" + mcpDefaultPort = "8080" +) + +// CommonMCPSpec defines the desired state of the MCP server, which is shared by CRDs of MCP servers +type CommonMCPSpec struct { + // ComponentVersion defines default image tag for the MCP server. + // It can be overwritten with image.tag value. + // +optional + ComponentVersion string `json:"componentVersion,omitempty"` + // PodMetadata configures Labels and Annotations which are propagated to the MCP server pods. + // +optional + PodMetadata *vmv1beta1.EmbeddedObjectMetadata `json:"podMetadata,omitempty"` + // ManagedMetadata defines metadata that will be added to the all objects + // created by operator for the given CustomResource + // +optional + ManagedMetadata *vmv1beta1.ManagedObjectsMetadata `json:"managedMetadata,omitempty"` + // ServiceAccountName is the name of the ServiceAccount to use to run the pods + // +optional + ServiceAccountName string `json:"serviceAccountName,omitempty"` + // ServiceSpec that will be added to the MCP server service spec + // +optional + ServiceSpec *vmv1beta1.AdditionalServiceSpec `json:"serviceSpec,omitempty"` + // ServiceScrapeSpec that will be added to the MCP server VMServiceScrape spec + // +optional + // +kubebuilder:validation:Type=object + // +kubebuilder:validation:Schemaless + // +kubebuilder:pruning:PreserveUnknownFields + ServiceScrapeSpec *vmv1beta1.VMServiceScrapeSpec `json:"serviceScrapeSpec,omitempty"` + // PodDisruptionBudget created by operator + // +optional + PodDisruptionBudget *vmv1beta1.EmbeddedPodDisruptionBudgetSpec `json:"podDisruptionBudget,omitempty"` + // UpdateStrategy - overrides default update strategy. + // +kubebuilder:validation:Enum=Recreate;RollingUpdate + // +optional + UpdateStrategy *appsv1.DeploymentStrategyType `json:"updateStrategy,omitempty"` + // RollingUpdate - overrides deployment update params. + // +optional + RollingUpdate *appsv1.RollingUpdateDeployment `json:"rollingUpdate,omitempty"` + // Configures vertical pod autoscaling. + // +optional + VPA *vmv1beta1.EmbeddedVPA `json:"vpa,omitempty"` + // NetworkPolicy defines network access rules for pods created by this CR. + // +optional + NetworkPolicy *vmv1beta1.EmbeddedNetworkPolicy `json:"networkPolicy,omitempty"` + + // ServerMode defines transport of the MCP server: + // http serves Streamable HTTP transport at /mcp path, + // sse serves Server-Sent Events transport at /sse and /message paths. + // Defaults to http. + // See https://modelcontextprotocol.io/specification/2025-03-26/basic/transports + // +optional + ServerMode MCPServerMode `json:"serverMode,omitempty"` + // DisabledTools defines names of MCP tools, which are not exposed to MCP clients. + // If it's not set, the MCP server uses its default list of disabled tools. + // +optional + DisabledTools []string `json:"disabledTools,omitempty"` + // HeartbeatInterval defines how often the MCP server sends heartbeats over Streamable HTTP connections, + // which keeps them from being closed by gateways and load balancers. + // Defaults to 30s. + // +kubebuilder:validation:Pattern:="[0-9]+(ms|s|m|h)" + // +optional + HeartbeatInterval string `json:"heartbeatInterval,omitempty"` + // LogLevel defines minimum log level of the MCP server. + // +kubebuilder:validation:Enum=debug;info;warn;error + // +optional + LogLevel string `json:"logLevel,omitempty"` + // LogFormat defines log output format of the MCP server. + // +kubebuilder:validation:Enum=text;json + // +optional + LogFormat string `json:"logFormat,omitempty"` + + vmv1beta1.CommonAppsParams `json:",inline,omitempty"` +} + +// validate checks if common params of the MCP server are correct +func (s *CommonMCPSpec) validate(prefixedName string) error { + if s.ServiceSpec != nil && s.ServiceSpec.Name == prefixedName { + return fmt.Errorf("spec.serviceSpec.Name cannot be equal to prefixed name=%q", prefixedName) + } + if len(s.ExtraArgs) > 0 { + return fmt.Errorf("spec.extraArgs is not supported, since the MCP server is configured with environment variables, use spec.extraEnvs instead") + } + if s.HeartbeatInterval != "" { + d, err := time.ParseDuration(s.HeartbeatInterval) + if err != nil { + return fmt.Errorf("cannot parse spec.heartbeatInterval=%q: %w", s.HeartbeatInterval, err) + } + if d < 0 { + return fmt.Errorf("spec.heartbeatInterval=%q cannot be negative", s.HeartbeatInterval) + } + } + if err := validateMCPListItems("disabledTools", s.DisabledTools); err != nil { + return err + } + if s.VPA != nil { + if err := s.VPA.Validate(); err != nil { + return err + } + } + // validates params of the embedded CommonAppsParams + return s.Validate() +} + +// validateMCPListItems checks items of the list - headers or tools, which is passed to the MCP server as a comma-separated env var value +func validateMCPListItems(field string, items []string) error { + for i, item := range items { + if strings.TrimSpace(item) == "" { + return fmt.Errorf("spec.%s[%d] cannot be empty", field, i) + } + if strings.Contains(item, ",") { + return fmt.Errorf("spec.%s[%d]=%q cannot contain commas", field, i, item) + } + } + return nil +} + +// validateMCPHeaders checks custom headers, which are passed to the MCP server as a comma-separated list of key=value pairs +func validateMCPHeaders(headers []string) error { + if err := validateMCPListItems("headers", headers); err != nil { + return err + } + for i, h := range headers { + k, v, ok := strings.Cut(h, "=") + if !ok || strings.TrimSpace(k) == "" || strings.TrimSpace(v) == "" { + return fmt.Errorf("spec.headers[%d]=%q must be in key=value format", i, h) + } + } + return nil +} + +func mcpSelectorLabels(appName, name string) map[string]string { + return map[string]string{ + "app.kubernetes.io/name": appName, + "app.kubernetes.io/instance": name, + "app.kubernetes.io/component": "monitoring", + "managed-by": "vm-operator", + } +} + +func (s *CommonMCPSpec) podLabels(selectorLabels map[string]string) map[string]string { + if s.PodMetadata == nil { + return selectorLabels + } + return labels.Merge(s.PodMetadata.Labels, selectorLabels) +} + +func (s *CommonMCPSpec) podAnnotations() map[string]string { + annotations := map[string]string{} + if s.PodMetadata != nil { + for annotation, value := range s.PodMetadata.Annotations { + annotations[annotation] = value + } + } + return annotations +} + +func (s *CommonMCPSpec) finalAnnotations() map[string]string { + var v map[string]string + if s.ManagedMetadata != nil { + v = labels.Merge(s.ManagedMetadata.Annotations, v) + } + return v +} + +func (s *CommonMCPSpec) resourceLabels(selectorLabels, input map[string]string) map[string]string { + var v map[string]string + if s.ManagedMetadata != nil { + v = s.ManagedMetadata.Labels + } + v = labels.Merge(v, input) + return labels.Merge(v, selectorLabels) +} + +func (s *CommonMCPSpec) serviceAccountName(prefixedName string) string { + if s.ServiceAccountName == "" { + return prefixedName + } + return s.ServiceAccountName +} + +func (s *CommonMCPSpec) asURL(prefixedName, namespace string, isExtra bool) string { + specPort := s.Port + if specPort == "" { + specPort = mcpDefaultPort + } + svcName, port := vmv1beta1.ResolveServiceURL(prefixedName, specPort, "http", s.ServiceSpec, isExtra) + return fmt.Sprintf("http://%s.%s.svc:%s", svcName, namespace, port) +} + +func mcpOwnerReference(tm metav1.TypeMeta, om metav1.ObjectMeta) metav1.OwnerReference { + return metav1.OwnerReference{ + APIVersion: tm.APIVersion, + Kind: tm.Kind, + Name: om.Name, + UID: om.UID, + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + } +} diff --git a/api/operator/v1/mcp_types_test.go b/api/operator/v1/mcp_types_test.go new file mode 100644 index 0000000000..a05ae306e7 --- /dev/null +++ b/api/operator/v1/mcp_types_test.go @@ -0,0 +1,150 @@ +package v1 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + vmv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1" +) + +func TestVMMCP_Validate(t *testing.T) { + f := func(spec VMMCPSpec, wantErr string) { + t.Helper() + cr := &VMMCP{ + ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, + Spec: spec, + } + err := cr.Validate() + if wantErr == "" { + assert.NoError(t, err) + } else { + assert.ErrorContains(t, err, wantErr) + } + } + secret := &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "vm-auth"}, + Key: "token", + } + + // entrypoint + f(VMMCPSpec{Entrypoint: "http://vmsingle-example:8428"}, "") + + // cloud API key + f(VMMCPSpec{CloudAPIKey: secret}, "") + + // neither entrypoint nor cloud API key + f(VMMCPSpec{}, "either spec.entrypoint or spec.cloudAPIKey must be set") + + // entrypoint without a host + f(VMMCPSpec{Entrypoint: "http://?"}, `spec.entrypoint="http://?" must have a host`) + f(VMMCPSpec{Entrypoint: "http://:8428"}, `spec.entrypoint="http://:8428" must have a host`) + + // both entrypoint and cloud API key + f(VMMCPSpec{ + Entrypoint: "http://vmsingle-example:8428", + CloudAPIKey: secret, + }, "cannot be set at the same time") + + // all supported params + f(VMMCPSpec{ + Entrypoint: "http://vmselect-example:8481", + InstanceType: VMMCPInstanceTypeCluster, + BearerToken: secret, + Headers: []string{"X-Scope=team-a", "X-Env=prod"}, + DefaultTenantID: "1:2", + PassthroughHeaders: []string{"X-Token"}, + DisableResources: true, + CommonMCPSpec: CommonMCPSpec{ + ServerMode: MCPServerModeSSE, + DisabledTools: []string{"export", "flags"}, + HeartbeatInterval: "1m30s", + LogLevel: "debug", + LogFormat: "json", + }, + }, "") + + // header without value + f(VMMCPSpec{ + Entrypoint: "http://vmsingle-example:8428", + Headers: []string{"X-Scope"}, + }, "must be in key=value format") + + // header with comma, which is used as headers separator + f(VMMCPSpec{ + Entrypoint: "http://vmsingle-example:8428", + Headers: []string{"X-Scope=a,b"}, + }, "cannot contain commas") + + // empty passthrough header + f(VMMCPSpec{ + Entrypoint: "http://vmsingle-example:8428", + PassthroughHeaders: []string{" "}, + }, "spec.passthroughHeaders[0] cannot be empty") + + // disabled tool with comma, which is used as tools separator + f(VMMCPSpec{ + Entrypoint: "http://vmsingle-example:8428", + CommonMCPSpec: CommonMCPSpec{ + DisabledTools: []string{"export,flags"}, + }, + }, "cannot contain commas") + + // incorrect heartbeat interval + f(VMMCPSpec{ + Entrypoint: "http://vmsingle-example:8428", + CommonMCPSpec: CommonMCPSpec{ + HeartbeatInterval: "10sec", + }, + }, "cannot parse spec.heartbeatInterval") + + // extra args aren't supported by the MCP server + f(VMMCPSpec{ + Entrypoint: "http://vmsingle-example:8428", + CommonMCPSpec: CommonMCPSpec{ + CommonAppsParams: vmv1beta1.CommonAppsParams{ + ExtraArgs: map[string]string{"loggerLevel": "INFO"}, + }, + }, + }, "spec.extraArgs is not supported") + + // additional service name is equal to prefixed name + f(VMMCPSpec{ + Entrypoint: "http://vmsingle-example:8428", + CommonMCPSpec: CommonMCPSpec{ + ServiceSpec: &vmv1beta1.AdditionalServiceSpec{ + EmbeddedObjectMetadata: vmv1beta1.EmbeddedObjectMetadata{Name: "vmmcp-test"}, + }, + }, + }, "spec.serviceSpec.Name cannot be equal to prefixed name") + + // preStop sleep exceeds termination grace period + f(VMMCPSpec{ + Entrypoint: "http://vmsingle-example:8428", + CommonMCPSpec: CommonMCPSpec{ + CommonAppsParams: vmv1beta1.CommonAppsParams{ + PreStopSleepSeconds: ptr.To[int32](30), + TerminationGracePeriodSeconds: ptr.To[int64](10), + }, + }, + }, "preStopSleepSeconds (30) must be less than terminationGracePeriodSeconds (10)") +} + +func TestMCP_Naming(t *testing.T) { + meta := metav1.ObjectMeta{Name: "example", Namespace: "monitoring"} + vm := &VMMCP{ObjectMeta: meta} + vm.Spec.Port = "8080" + + assert.Equal(t, "vmmcp-example", vm.PrefixedName()) + assert.Equal(t, "vmmcp", vm.SelectorLabels()["app.kubernetes.io/name"]) + assert.Equal(t, "http://vmmcp-example.monitoring.svc:8080", vm.AsURL(false)) + + // URL of the object without defaults, which is read from Kubernetes API, uses the default port + assert.Equal(t, "http://vmmcp-example.monitoring.svc:8080", (&VMMCP{ObjectMeta: meta}).AsURL(false)) + custom := &VMMCP{ObjectMeta: meta} + custom.Spec.Port = "8090" + assert.Equal(t, "http://vmmcp-example.monitoring.svc:8090", custom.AsURL(false)) +} diff --git a/api/operator/v1/vmmcp_types.go b/api/operator/v1/vmmcp_types.go new file mode 100644 index 0000000000..355f38c3ac --- /dev/null +++ b/api/operator/v1/vmmcp_types.go @@ -0,0 +1,307 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "encoding/json" + "fmt" + "net/url" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + vmv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1" +) + +// VMMCPInstanceType defines type of VictoriaMetrics instance, which is queried by the MCP server +// +kubebuilder:validation:Enum=single;cluster +type VMMCPInstanceType string + +const ( + // VMMCPInstanceTypeSingle defines single-node VictoriaMetrics instance + VMMCPInstanceTypeSingle VMMCPInstanceType = "single" + // VMMCPInstanceTypeCluster defines VictoriaMetrics cluster + VMMCPInstanceTypeCluster VMMCPInstanceType = "cluster" +) + +// VMMCPSpec defines the desired state of VMMCP +// +k8s:openapi-gen=true +type VMMCPSpec struct { + // Entrypoint defines root URL of VictoriaMetrics instance, which is queried by the MCP server: + // VMSingle URL (e.g. http://vmsingle-example.default.svc:8428) for single instance type + // or vmselect URL (e.g. http://vmselect-example.default.svc:8481) for cluster instance type. + // Either entrypoint or cloudAPIKey must be set. + // +kubebuilder:validation:Pattern:="^https?://.+$" + // +optional + Entrypoint string `json:"entrypoint,omitempty"` + // InstanceType defines type of VictoriaMetrics instance set at entrypoint. + // Defaults to single. + // +optional + InstanceType VMMCPInstanceType `json:"instanceType,omitempty"` + // BearerToken defines a Secret key with a token, which authenticates requests to entrypoint. + // +optional + BearerToken *corev1.SecretKeySelector `json:"bearerToken,omitempty"` + // Headers defines custom HTTP headers in key=value format, which are sent with requests to entrypoint. + // +optional + Headers []string `json:"headers,omitempty"` + // DefaultTenantID defines tenant for requests without explicit tenant to the cluster instance type + // in accountID or accountID:projectID format. + // Defaults to 0. + // +kubebuilder:validation:Pattern:="^[0-9]+(:[0-9]+)?$" + // +optional + DefaultTenantID string `json:"defaultTenantID,omitempty"` + // PassthroughHeaders defines names of HTTP headers, which are forwarded from MCP client requests to entrypoint. + // They override headers with the same names. + // It's useful for routing requests by headers with vmauth, + // see https://docs.victoriametrics.com/victoriametrics/vmauth/#routing-by-header + // +optional + PassthroughHeaders []string `json:"passthroughHeaders,omitempty"` + // CloudAPIKey defines a Secret key with VictoriaMetrics Cloud API key. + // If it's set, the MCP server works with VictoriaMetrics Cloud deployments instead of entrypoint. + // See https://docs.victoriametrics.com/victoriametrics-cloud/api/ + // +optional + CloudAPIKey *corev1.SecretKeySelector `json:"cloudAPIKey,omitempty"` + // DisableResources disables MCP resources with VictoriaMetrics documentation, + // while documentation tool keeps working. + // +optional + DisableResources bool `json:"disableResources,omitempty"` + + CommonMCPSpec `json:",inline"` +} + +// VMMCPStatus defines the observed state of VMMCP +type VMMCPStatus struct { + vmv1beta1.StatusMetadata `json:",inline"` + // +kubebuilder:validation:Schemaless + // +kubebuilder:pruning:PreserveUnknownFields + LastAppliedSpec *VMMCPSpec `json:"lastAppliedSpec,omitempty"` + // ParsingSpecError contents error with context if operator was failed to parse json object from kubernetes api server + ParsingSpecError string `json:"-" yaml:"-"` +} + +// GetStatusMetadata returns metadata for object status +func (cr *VMMCP) GetStatusMetadata() *vmv1beta1.StatusMetadata { + return &cr.Status.StatusMetadata +} + +// VMMCP is the MCP server for VictoriaMetrics, which provides AI assistants with access to VictoriaMetrics APIs and documentation. +// See https://github.com/VictoriaMetrics/mcp-victoriametrics +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +operator-sdk:gen-csv:customresourcedefinitions.displayName="VMMCP App" +// +operator-sdk:gen-csv:customresourcedefinitions.resources="Deployment,apps" +// +operator-sdk:gen-csv:customresourcedefinitions.resources="Service,v1" +// +genclient +// +k8s:openapi-gen=true +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:path=vmmcps,scope=Namespaced +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.updateStatus",description="Current status of update rollout" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +// VMMCP is the Schema for the API +type VMMCP struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec VMMCPSpec `json:"spec,omitempty"` + Status VMMCPStatus `json:"status,omitempty"` +} + +// GetStatus implements reconcile.ObjectWithDeepCopyAndStatus interface +func (cr *VMMCP) GetStatus() *VMMCPStatus { + return &cr.Status +} + +// DefaultStatusFields implements reconcile.ObjectWithDeepCopyAndStatus interface +func (cr *VMMCP) DefaultStatusFields(vs *VMMCPStatus) { +} + +// UnmarshalJSON implements json.Unmarshaler interface +func (cr *VMMCP) UnmarshalJSON(src []byte) error { + type pcr VMMCP + type shadow struct { + *pcr + Spec json.RawMessage `json:"spec"` + } + s := shadow{pcr: (*pcr)(cr)} + if err := json.Unmarshal(src, &s); err != nil { + return err + } + if len(s.Spec) > 0 { + if err := vmv1beta1.UnmarshalSpecStrict(s.Spec, &cr.Spec); err != nil { + cr.Status.ParsingSpecError = fmt.Sprintf("cannot parse VMMCPSpec: %s, err: %s", string(s.Spec), err) + } + } + return nil +} + +// +kubebuilder:object:root=true + +// VMMCPList contains a list of VMMCP +type VMMCPList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []VMMCP `json:"items"` +} + +// PodAnnotations returns annotations attached to the podMetadata +func (cr *VMMCP) PodAnnotations() map[string]string { + return cr.Spec.podAnnotations() +} + +// AsOwner returns owner references with current object as owner +func (cr *VMMCP) AsOwner() metav1.OwnerReference { + return mcpOwnerReference(cr.TypeMeta, cr.ObjectMeta) +} + +// ProbePath implements build.probeCRD interface +func (cr *VMMCP) ProbePath() string { + return mcpReadinessPath +} + +// ProbeScheme implements build.probeCRD interface +func (cr *VMMCP) ProbeScheme() string { + return string(corev1.URISchemeHTTP) +} + +// ProbePort implements build.probeCRD interface +func (cr *VMMCP) ProbePort() string { + return cr.Spec.Port +} + +// ProbeNeedLiveness implements build.probeCRD interface +func (cr *VMMCP) ProbeNeedLiveness() bool { + return true +} + +// UseProxyProtocol implements build.probeCRD interface +func (cr *VMMCP) UseProxyProtocol() bool { + return false +} + +// FinalAnnotations returns annotations to be applied for created objects +func (cr *VMMCP) FinalAnnotations() map[string]string { + return cr.Spec.finalAnnotations() +} + +// SelectorLabels returns unique labels for object +func (cr *VMMCP) SelectorLabels() map[string]string { + return mcpSelectorLabels("vmmcp", cr.Name) +} + +// PodLabels returns labels attached to the podMetadata +func (cr *VMMCP) PodLabels() map[string]string { + return cr.Spec.podLabels(cr.SelectorLabels()) +} + +// FinalLabels returns combination of selector and managed labels +func (cr *VMMCP) FinalLabels() map[string]string { + return cr.ResourceLabels(nil) +} + +// ResourceLabels returns combination of selector, resource and managed labels +func (cr *VMMCP) ResourceLabels(input map[string]string) map[string]string { + return cr.Spec.resourceLabels(cr.SelectorLabels(), input) +} + +// PrefixedName format name of the component with hard-coded prefix +func (cr *VMMCP) PrefixedName() string { + return fmt.Sprintf("vmmcp-%s", cr.Name) +} + +// GetMetricsPath returns prefixed path for metric requests +func (cr *VMMCP) GetMetricsPath() string { + return metricsPath +} + +// UseTLS returns true if TLS is enabled +func (cr *VMMCP) UseTLS() bool { + return false +} + +// GetExtraArgs returns additionally configured command-line arguments +func (cr *VMMCP) GetExtraArgs() map[string]string { + return cr.Spec.ExtraArgs +} + +// GetServiceScrape returns overrides for serviceScrape builder +func (cr *VMMCP) GetServiceScrape() *vmv1beta1.VMServiceScrapeSpec { + return cr.Spec.ServiceScrapeSpec +} + +// GetServiceAccountName returns service account name for components +func (cr *VMMCP) GetServiceAccountName() string { + return cr.Spec.serviceAccountName(cr.PrefixedName()) +} + +// IsOwnsServiceAccount checks if ServiceAccountName is set explicitly +func (cr *VMMCP) IsOwnsServiceAccount() bool { + return cr.Spec.ServiceAccountName == "" +} + +// GetAdditionalService returns AdditionalServiceSpec settings +func (cr *VMMCP) GetAdditionalService() *vmv1beta1.AdditionalServiceSpec { + return cr.Spec.ServiceSpec +} + +// AsURL returns URL for components access +func (cr *VMMCP) AsURL(isExtra bool) string { + return cr.Spec.asURL(cr.PrefixedName(), cr.Namespace, isExtra) +} + +// Paused checks if resource reconcile should be paused +func (cr *VMMCP) Paused() bool { + return cr.Spec.Paused +} + +// LastSpecUpdated compares spec with last applied spec stored, replaces old spec and returns true if it's updated +func (cr *VMMCP) LastSpecUpdated() bool { + updated := cr.Status.LastAppliedSpec == nil || !equality.Semantic.DeepEqual(&cr.Spec, cr.Status.LastAppliedSpec) + cr.Status.LastAppliedSpec = cr.Spec.DeepCopy() + return updated +} + +// Validate checks if spec is correct +func (cr *VMMCP) Validate() error { + if vmv1beta1.MustSkipCRValidation(cr) { + return nil + } + spec := &cr.Spec + switch { + case spec.Entrypoint == "" && spec.CloudAPIKey == nil: + return fmt.Errorf("either spec.entrypoint or spec.cloudAPIKey must be set") + case spec.Entrypoint != "" && spec.CloudAPIKey != nil: + return fmt.Errorf("spec.entrypoint and spec.cloudAPIKey cannot be set at the same time") + } + if spec.Entrypoint != "" { + // the CRD pattern checks only the scheme + u, err := url.Parse(spec.Entrypoint) + if err != nil { + return fmt.Errorf("incorrect spec.entrypoint syntax: %q: %w", spec.Entrypoint, err) + } + if u.Hostname() == "" { + return fmt.Errorf("spec.entrypoint=%q must have a host", spec.Entrypoint) + } + } + if err := validateMCPHeaders(spec.Headers); err != nil { + return err + } + if err := validateMCPListItems("passthroughHeaders", spec.PassthroughHeaders); err != nil { + return err + } + return spec.validate(cr.PrefixedName()) +} diff --git a/api/operator/v1/zz_generated.deepcopy.go b/api/operator/v1/zz_generated.deepcopy.go index 2fc12976a0..d69503087f 100644 --- a/api/operator/v1/zz_generated.deepcopy.go +++ b/api/operator/v1/zz_generated.deepcopy.go @@ -28,6 +28,72 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CommonMCPSpec) DeepCopyInto(out *CommonMCPSpec) { + *out = *in + if in.PodMetadata != nil { + in, out := &in.PodMetadata, &out.PodMetadata + *out = new(v1beta1.EmbeddedObjectMetadata) + (*in).DeepCopyInto(*out) + } + if in.ManagedMetadata != nil { + in, out := &in.ManagedMetadata, &out.ManagedMetadata + *out = new(v1beta1.ManagedObjectsMetadata) + (*in).DeepCopyInto(*out) + } + if in.ServiceSpec != nil { + in, out := &in.ServiceSpec, &out.ServiceSpec + *out = new(v1beta1.AdditionalServiceSpec) + (*in).DeepCopyInto(*out) + } + if in.ServiceScrapeSpec != nil { + in, out := &in.ServiceScrapeSpec, &out.ServiceScrapeSpec + *out = new(v1beta1.VMServiceScrapeSpec) + (*in).DeepCopyInto(*out) + } + if in.PodDisruptionBudget != nil { + in, out := &in.PodDisruptionBudget, &out.PodDisruptionBudget + *out = new(v1beta1.EmbeddedPodDisruptionBudgetSpec) + (*in).DeepCopyInto(*out) + } + if in.UpdateStrategy != nil { + in, out := &in.UpdateStrategy, &out.UpdateStrategy + *out = new(appsv1.DeploymentStrategyType) + **out = **in + } + if in.RollingUpdate != nil { + in, out := &in.RollingUpdate, &out.RollingUpdate + *out = new(appsv1.RollingUpdateDeployment) + (*in).DeepCopyInto(*out) + } + if in.VPA != nil { + in, out := &in.VPA, &out.VPA + *out = new(v1beta1.EmbeddedVPA) + (*in).DeepCopyInto(*out) + } + if in.NetworkPolicy != nil { + in, out := &in.NetworkPolicy, &out.NetworkPolicy + *out = new(v1beta1.EmbeddedNetworkPolicy) + (*in).DeepCopyInto(*out) + } + if in.DisabledTools != nil { + in, out := &in.DisabledTools, &out.DisabledTools + *out = make([]string, len(*in)) + copy(*out, *in) + } + in.CommonAppsParams.DeepCopyInto(&out.CommonAppsParams) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommonMCPSpec. +func (in *CommonMCPSpec) DeepCopy() *CommonMCPSpec { + if in == nil { + return nil + } + out := new(CommonMCPSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OAuth2) DeepCopyInto(out *OAuth2) { *out = *in @@ -1502,6 +1568,122 @@ func (in *VMAnomalyWritersSpec) DeepCopy() *VMAnomalyWritersSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VMMCP) DeepCopyInto(out *VMMCP) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VMMCP. +func (in *VMMCP) DeepCopy() *VMMCP { + if in == nil { + return nil + } + out := new(VMMCP) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VMMCP) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VMMCPList) DeepCopyInto(out *VMMCPList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]VMMCP, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VMMCPList. +func (in *VMMCPList) DeepCopy() *VMMCPList { + if in == nil { + return nil + } + out := new(VMMCPList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VMMCPList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VMMCPSpec) DeepCopyInto(out *VMMCPSpec) { + *out = *in + if in.BearerToken != nil { + in, out := &in.BearerToken, &out.BearerToken + *out = new(corev1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + if in.Headers != nil { + in, out := &in.Headers, &out.Headers + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.PassthroughHeaders != nil { + in, out := &in.PassthroughHeaders, &out.PassthroughHeaders + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.CloudAPIKey != nil { + in, out := &in.CloudAPIKey, &out.CloudAPIKey + *out = new(corev1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + in.CommonMCPSpec.DeepCopyInto(&out.CommonMCPSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VMMCPSpec. +func (in *VMMCPSpec) DeepCopy() *VMMCPSpec { + if in == nil { + return nil + } + out := new(VMMCPSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VMMCPStatus) DeepCopyInto(out *VMMCPStatus) { + *out = *in + in.StatusMetadata.DeepCopyInto(&out.StatusMetadata) + if in.LastAppliedSpec != nil { + in, out := &in.LastAppliedSpec, &out.LastAppliedSpec + *out = new(VMMCPSpec) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VMMCPStatus. +func (in *VMMCPStatus) DeepCopy() *VMMCPStatus { + if in == nil { + return nil + } + out := new(VMMCPStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VTAgent) DeepCopyInto(out *VTAgent) { *out = *in diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 1b474e3710..143c3eb63c 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -28,6 +28,7 @@ resources: - bases/operator.victoriametrics.com_vmdistributed.yaml - bases/operator.victoriametrics.com_vldistributed.yaml - bases/operator.victoriametrics.com_vmanomalyconfigs.yaml +- bases/operator.victoriametrics.com_vmmcps.yaml patches: - path: patches/common-array-fields-with-claimtemplates.yaml target: @@ -36,7 +37,7 @@ patches: - path: patches/common-array-fields.yaml target: kind: CustomResourceDefinition - name: ^(vlsingles|vmauths|vmsingles|vtsingles|vmalerts)\.operator\.victoriametrics\.com$ + name: ^(vlsingles|vmauths|vmsingles|vtsingles|vmalerts|vmmcps)\.operator\.victoriametrics\.com$ - path: patches/operator.victoriametrics.com_vmalerts.yaml target: kind: CustomResourceDefinition diff --git a/config/crd/overlay/crd.descriptionless.yaml b/config/crd/overlay/crd.descriptionless.yaml index 06f450f1b4..5d6066f946 100644 --- a/config/crd/overlay/crd.descriptionless.yaml +++ b/config/crd/overlay/crd.descriptionless.yaml @@ -30232,6 +30232,859 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + name: vmmcps.operator.victoriametrics.com +spec: + group: operator.victoriametrics.com + names: + kind: VMMCP + listKind: VMMCPList + plural: vmmcps + singular: vmmcp + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Current status of update rollout + jsonPath: .status.updateStatus + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + properties: + affinity: + type: object + x-kubernetes-preserve-unknown-fields: true + bearerToken: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + cloudAPIKey: + properties: + key: + type: string + name: + default: "" + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + componentVersion: + type: string + configMaps: + items: + type: string + type: array + containers: + items: + required: + - name + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + defaultTenantID: + pattern: ^[0-9]+(:[0-9]+)?$ + type: string + disableAutomountServiceAccountToken: + type: boolean + disableResources: + type: boolean + disableSelfServiceScrape: + type: boolean + disabledTools: + items: + type: string + type: array + dnsConfig: + type: object + x-kubernetes-preserve-unknown-fields: true + dnsPolicy: + type: string + enableServiceLinks: + type: boolean + entrypoint: + pattern: ^https?://.+$ + type: string + extraArgs: + additionalProperties: + type: string + type: object + extraEnvs: + items: + properties: + name: + type: string + value: + type: string + required: + - name + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + extraEnvsFrom: + items: + properties: + configMapRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + headers: + items: + type: string + type: array + heartbeatInterval: + pattern: '[0-9]+(ms|s|m|h)' + type: string + host_aliases: + items: + properties: + hostnames: + items: + type: string + type: array + x-kubernetes-list-type: atomic + ip: + type: string + required: + - ip + type: object + type: array + hostAliases: + items: + properties: + hostnames: + items: + type: string + type: array + x-kubernetes-list-type: atomic + ip: + type: string + required: + - ip + type: object + type: array + hostNetwork: + type: boolean + image: + properties: + pullPolicy: + type: string + repository: + type: string + tag: + type: string + type: object + imagePullSecrets: + items: + properties: + name: + default: "" + type: string + type: object + x-kubernetes-map-type: atomic + type: array + initContainers: + items: + required: + - name + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + instanceType: + enum: + - single + - cluster + type: string + livenessProbe: + type: object + x-kubernetes-preserve-unknown-fields: true + logFormat: + enum: + - text + - json + type: string + logLevel: + enum: + - debug + - info + - warn + - error + type: string + managedMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + type: object + minReadySeconds: + format: int32 + type: integer + networkPolicy: + properties: + egress: + items: + properties: + ports: + items: + properties: + endPort: + format: int32 + type: integer + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + protocol: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + to: + items: + properties: + ipBlock: + properties: + cidr: + type: string + except: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - cidr + type: object + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + podSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + ingress: + items: + properties: + from: + items: + properties: + ipBlock: + properties: + cidr: + type: string + except: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - cidr + type: object + namespaceSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + podSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + ports: + items: + properties: + endPort: + format: int32 + type: integer + port: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + protocol: + type: string + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + type: object + nodeSelector: + additionalProperties: + type: string + type: object + passthroughHeaders: + items: + type: string + type: array + paused: + type: boolean + podDisruptionBudget: + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + minAvailable: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + selectorLabels: + additionalProperties: + type: string + type: object + unhealthyPodEvictionPolicy: + enum: + - IfHealthyBudget + - AlwaysAllow + type: string + type: object + podMetadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + name: + type: string + type: object + port: + type: string + preStopSleepSeconds: + format: int32 + type: integer + priorityClassName: + type: string + readinessGates: + items: + properties: + conditionType: + type: string + required: + - conditionType + type: object + type: array + readinessProbe: + type: object + x-kubernetes-preserve-unknown-fields: true + replicaCount: + format: int32 + type: integer + resources: + properties: + claims: + items: + properties: + name: + type: string + request: + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: object + revisionHistoryLimitCount: + format: int32 + type: integer + rollingUpdate: + properties: + maxSurge: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + maxUnavailable: + anyOf: + - type: integer + - type: string + x-kubernetes-int-or-string: true + type: object + runtimeClassName: + type: string + schedulerName: + type: string + secrets: + items: + type: string + type: array + securityContext: + type: object + x-kubernetes-preserve-unknown-fields: true + serverMode: + enum: + - http + - sse + type: string + serviceAccountName: + type: string + serviceScrapeSpec: + type: object + x-kubernetes-preserve-unknown-fields: true + serviceSpec: + properties: + metadata: + properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + name: + type: string + type: object + spec: + type: object + x-kubernetes-preserve-unknown-fields: true + useAsDefault: + type: boolean + required: + - spec + type: object + startupProbe: + type: object + x-kubernetes-preserve-unknown-fields: true + terminationGracePeriodSeconds: + format: int64 + type: integer + tolerations: + items: + properties: + effect: + type: string + key: + type: string + operator: + type: string + tolerationSeconds: + format: int64 + type: integer + value: + type: string + type: object + type: array + topologySpreadConstraints: + items: + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + updateStrategy: + enum: + - Recreate + - RollingUpdate + type: string + useDefaultResources: + type: boolean + useStrictSecurity: + type: boolean + volumeMounts: + items: + properties: + bindMountOptions: + items: + type: string + type: array + x-kubernetes-list-type: set + mountPath: + type: string + mountPropagation: + type: string + name: + type: string + readOnly: + type: boolean + recursiveReadOnly: + type: string + subPath: + type: string + subPathExpr: + type: string + required: + - mountPath + - name + type: object + type: array + volumes: + items: + required: + - name + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + vpa: + properties: + recommenders: + items: + properties: + name: + type: string + required: + - name + type: object + type: array + resourcePolicy: + properties: + containerPolicies: + items: + properties: + containerName: + type: string + controlledResources: + items: + type: string + type: array + controlledValues: + enum: + - RequestsAndLimits + - RequestsOnly + type: string + maxAllowed: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + memoryAggregationIntervalCount: + format: int64 + minimum: 1 + type: integer + memoryAggregationIntervalSeconds: + format: int32 + minimum: 1 + type: integer + minAllowed: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + mode: + enum: + - Auto + - "Off" + type: string + oomBumpUpRatio: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + oomMinBumpUp: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + startupBoost: + properties: + cpu: + properties: + durationSeconds: + format: int32 + type: integer + factor: + format: int32 + type: integer + quantity: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + enum: + - Factor + - Quantity + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: factor is required when type is Factor + and forbidden otherwise + rule: (self.type == 'Factor') == has(self.factor) + - message: quantity is required when type is Quantity + and forbidden otherwise + rule: (self.type == 'Quantity') == has(self.quantity) + type: object + type: object + type: array + type: object + updatePolicy: + properties: + evictAfterOOMSeconds: + format: int32 + minimum: 1 + type: integer + evictionRequirements: + items: + properties: + changeRequirement: + enum: + - TargetHigherThanRequests + - TargetLowerThanRequests + type: string + resources: + items: + type: string + type: array + required: + - changeRequirement + - resources + type: object + type: array + minReplicas: + format: int32 + type: integer + updateMode: + enum: + - "Off" + - Initial + - Recreate + - InPlaceOrRecreate + - InPlace + - Auto + type: string + type: object + type: object + type: object + status: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + lastUpdateTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + type: string + required: + - lastTransitionTime + - lastUpdateTime + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + lastAppliedSpec: + x-kubernetes-preserve-unknown-fields: true + observedGeneration: + format: int64 + type: integer + reason: + type: string + updateStatus: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: name: vmnodescrapes.operator.victoriametrics.com spec: diff --git a/config/crd/overlay/crd.specless.yaml b/config/crd/overlay/crd.specless.yaml index a04066ef78..1431efe696 100644 --- a/config/crd/overlay/crd.specless.yaml +++ b/config/crd/overlay/crd.specless.yaml @@ -1381,6 +1381,98 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + name: vmmcps.operator.victoriametrics.com +spec: + group: operator.victoriametrics.com + names: + kind: VMMCP + listKind: VMMCPList + plural: vmmcps + singular: vmmcp + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Current status of update rollout + jsonPath: .status.updateStatus + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object + spec: + type: object + x-kubernetes-preserve-unknown-fields: true + status: + properties: + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + lastUpdateTime: + format: date-time + type: string + message: + maxLength: 32768 + type: string + observedGeneration: + format: int64 + minimum: 0 + type: integer + reason: + maxLength: 1024 + minLength: 1 + type: string + status: + enum: + - "True" + - "False" + - Unknown + type: string + type: + maxLength: 316 + type: string + required: + - lastTransitionTime + - lastUpdateTime + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + lastAppliedSpec: + x-kubernetes-preserve-unknown-fields: true + observedGeneration: + format: int64 + type: integer + reason: + type: string + updateStatus: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: name: vmnodescrapes.operator.victoriametrics.com spec: diff --git a/config/crd/overlay/crd.yaml b/config/crd/overlay/crd.yaml index 8d21dbf76d..d628162cdb 100644 --- a/config/crd/overlay/crd.yaml +++ b/config/crd/overlay/crd.yaml @@ -60312,6 +60312,1664 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + name: vmmcps.operator.victoriametrics.com +spec: + group: operator.victoriametrics.com + names: + kind: VMMCP + listKind: VMMCPList + plural: vmmcps + singular: vmmcp + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: Current status of update rollout + jsonPath: .status.updateStatus + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + VMMCP is the MCP server for VictoriaMetrics, which provides AI assistants with access to VictoriaMetrics APIs and documentation. + See https://github.com/VictoriaMetrics/mcp-victoriametrics + VMMCP is the Schema for the API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: VMMCPSpec defines the desired state of VMMCP + properties: + affinity: + description: Affinity If specified, the pod's scheduling constraints. + type: object + x-kubernetes-preserve-unknown-fields: true + bearerToken: + description: BearerToken defines a Secret key with a token, which + authenticates requests to entrypoint. + properties: + key: + description: The key of the secret to select from. Must be a + valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + cloudAPIKey: + description: |- + CloudAPIKey defines a Secret key with VictoriaMetrics Cloud API key. + If it's set, the MCP server works with VictoriaMetrics Cloud deployments instead of entrypoint. + See https://docs.victoriametrics.com/victoriametrics-cloud/api/ + properties: + key: + description: The key of the secret to select from. Must be a + valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + componentVersion: + description: |- + ComponentVersion defines default image tag for the MCP server. + It can be overwritten with image.tag value. + type: string + configMaps: + description: |- + ConfigMaps is a list of ConfigMaps in the same namespace as the Application + object, which shall be mounted into the Application container + at /etc/vm/configs/CONFIGMAP_NAME folder + items: + type: string + type: array + containers: + description: |- + Containers property allows to inject additions sidecars or to patch existing containers. + It can be useful for proxies, backup, etc. + items: + description: A single application container that you want to run + within a pod. + required: + - name + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + defaultTenantID: + description: |- + DefaultTenantID defines tenant for requests without explicit tenant to the cluster instance type + in accountID or accountID:projectID format. + Defaults to 0. + pattern: ^[0-9]+(:[0-9]+)?$ + type: string + disableAutomountServiceAccountToken: + description: |- + DisableAutomountServiceAccountToken whether to disable serviceAccount auto mount by Kubernetes + Operator will conditionally create volumes and volumeMounts for containers if it requires k8s API access. + For example, vmagent and vm-config-reloader requires k8s API access. + Operator creates volumes with name: "kube-api-access", which can be used as volumeMount for extraContainers if needed. + And also adds VolumeMounts at /var/run/secrets/kubernetes.io/serviceaccount. + type: boolean + disableResources: + description: |- + DisableResources disables MCP resources with VictoriaMetrics documentation, + while documentation tool keeps working. + type: boolean + disableSelfServiceScrape: + description: |- + DisableSelfServiceScrape controls creation of VMServiceScrape by operator + for the application. + Has priority over `VM_DISABLESELFSERVICESCRAPECREATION` operator env variable + type: boolean + disabledTools: + description: |- + DisabledTools defines names of MCP tools, which are not exposed to MCP clients. + If it's not set, the MCP server uses its default list of disabled tools. + items: + type: string + type: array + dnsConfig: + description: |- + Specifies the DNS parameters of a pod. + Parameters specified here will be merged to the generated DNS + configuration based on DNSPolicy. + type: object + x-kubernetes-preserve-unknown-fields: true + dnsPolicy: + description: DNSPolicy sets DNS policy for the pod + type: string + enableServiceLinks: + description: |- + EnableServiceLinks indicates whether information about services should be injected into pod's + environment variables, matching the syntax of Docker links. + Optional: Defaults to true. + type: boolean + entrypoint: + description: |- + Entrypoint defines root URL of VictoriaMetrics instance, which is queried by the MCP server: + VMSingle URL (e.g. http://vmsingle-example.default.svc:8428) for single instance type + or vmselect URL (e.g. http://vmselect-example.default.svc:8481) for cluster instance type. + Either entrypoint or cloudAPIKey must be set. + pattern: ^https?://.+$ + type: string + extraArgs: + additionalProperties: + type: string + description: |- + ExtraArgs that will be passed to the application container + for example remoteWrite.tmpDataPath: /tmp + type: object + extraEnvs: + description: ExtraEnvs that will be passed to the application container + items: + description: EnvVar represents an environment variable present in + a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + required: + - name + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + extraEnvsFrom: + description: |- + ExtraEnvsFrom defines source of env variables for the application container + could either be secret or configmap + items: + description: EnvFromSource represents the source of a set of ConfigMaps + or Secrets + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: |- + Optional text to prepend to the name of each environment variable. + May consist of any printable ASCII characters except '='. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + headers: + description: Headers defines custom HTTP headers in key=value format, + which are sent with requests to entrypoint. + items: + type: string + type: array + heartbeatInterval: + description: |- + HeartbeatInterval defines how often the MCP server sends heartbeats over Streamable HTTP connections, + which keeps them from being closed by gateways and load balancers. + Defaults to 30s. + pattern: '[0-9]+(ms|s|m|h)' + type: string + host_aliases: + description: |- + HostAliasesUnderScore provides mapping for ip and hostname, + that would be propagated to pod, + cannot be used with HostNetwork. + Has Priority over hostAliases field + items: + description: |- + HostAlias holds the mapping between IP and hostnames that will be injected as an entry in the + pod's hosts file. + properties: + hostnames: + description: Hostnames for the above IP address. + items: + type: string + type: array + x-kubernetes-list-type: atomic + ip: + description: IP address of the host file entry. + type: string + required: + - ip + type: object + type: array + hostAliases: + description: |- + HostAliases provides mapping for ip and hostname, + that would be propagated to pod, + cannot be used with HostNetwork. + items: + description: |- + HostAlias holds the mapping between IP and hostnames that will be injected as an entry in the + pod's hosts file. + properties: + hostnames: + description: Hostnames for the above IP address. + items: + type: string + type: array + x-kubernetes-list-type: atomic + ip: + description: IP address of the host file entry. + type: string + required: + - ip + type: object + type: array + hostNetwork: + description: HostNetwork controls whether the pod may use the node + network namespace + type: boolean + image: + description: |- + Image - docker image settings + if no specified operator uses default version from operator config + properties: + pullPolicy: + description: PullPolicy describes how to pull docker image + type: string + repository: + description: Repository contains name of docker image + it's repository + if needed + type: string + tag: + description: |- + Tag contains desired docker image version. + It may be a regular tag (e.g. "v1.96.0") or a digest + (e.g. "sha256:"); a digest is joined to the repository with "@" + instead of ":", allowing images to be pinned by digest. + type: string + type: object + imagePullSecrets: + description: |- + ImagePullSecrets An optional list of references to secrets in the same namespace + to use for pulling images from registries + see https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array + initContainers: + description: |- + InitContainers allows adding initContainers to the pod definition. + Any errors during the execution of an initContainer will lead to a restart of the Pod. + More info: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ + items: + description: A single application container that you want to run + within a pod. + required: + - name + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + instanceType: + description: |- + InstanceType defines type of VictoriaMetrics instance set at entrypoint. + Defaults to single. + enum: + - single + - cluster + type: string + livenessProbe: + description: LivenessProbe that will be added to CR pod + type: object + x-kubernetes-preserve-unknown-fields: true + logFormat: + description: LogFormat defines log output format of the MCP server. + enum: + - text + - json + type: string + logLevel: + description: LogLevel defines minimum log level of the MCP server. + enum: + - debug + - info + - warn + - error + type: string + managedMetadata: + description: |- + ManagedMetadata defines metadata that will be added to the all objects + created by operator for the given CustomResource + properties: + annotations: + additionalProperties: + type: string + description: |- + Annotations is an unstructured key value map stored with a resource that may be + set by external tools to store and retrieve arbitrary metadata. They are not + queryable and should be preserved when modifying objects. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations + type: object + labels: + additionalProperties: + type: string + description: |- + Labels Map of string keys and values that can be used to organize and categorize + (scope and select) objects. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels + type: object + type: object + minReadySeconds: + description: |- + MinReadySeconds defines a minimum number of seconds to wait before starting update next pod + if previous in healthy state + Has no effect for VLogs and VMSingle + format: int32 + type: integer + networkPolicy: + description: NetworkPolicy defines network access rules for pods created + by this CR. + properties: + egress: + description: Egress defines the list of egress rules applied to + pods selected by this CR. + items: + description: |- + NetworkPolicyEgressRule describes a particular set of traffic that is allowed out of pods + matched by a NetworkPolicySpec's podSelector. The traffic must match both ports and to. + This type is beta-level in 1.8 + properties: + ports: + description: |- + ports is a list of destination ports for outgoing traffic. + Each item in this list is combined using a logical OR. If this field is + empty or missing, this rule matches all ports (traffic not restricted by port). + If this field is present and contains at least one item, then this rule allows + traffic only if the traffic matches at least one port in the list. + items: + description: NetworkPolicyPort describes a port to allow + traffic on + properties: + endPort: + description: |- + endPort indicates that the range of ports from port to endPort if set, inclusive, + should be allowed by the policy. This field cannot be defined if the port field + is not defined or if the port field is defined as a named (string) port. + The endPort must be equal or greater than port. + format: int32 + type: integer + port: + anyOf: + - type: integer + - type: string + description: |- + port represents the port on the given protocol. This can either be a numerical or named + port on a pod. If this field is not provided, this matches all port names and + numbers. + If present, only traffic on the specified protocol AND port will be matched. + x-kubernetes-int-or-string: true + protocol: + description: |- + protocol represents the protocol (TCP, UDP, or SCTP) which traffic must match. + If not specified, this field defaults to TCP. + type: string + type: object + type: array + x-kubernetes-list-type: atomic + to: + description: |- + to is a list of destinations for outgoing traffic of pods selected for this rule. + Items in this list are combined using a logical OR operation. If this field is + empty or missing, this rule matches all destinations (traffic not restricted by + destination). If this field is present and contains at least one item, this rule + allows traffic only if the traffic matches at least one item in the to list. + items: + description: |- + NetworkPolicyPeer describes a peer to allow traffic to/from. Only certain combinations of + fields are allowed + properties: + ipBlock: + description: |- + ipBlock defines policy on a particular IPBlock. If this field is set then + neither of the other fields can be. + properties: + cidr: + description: |- + cidr is a string representing the IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + type: string + except: + description: |- + except is a slice of CIDRs that should not be included within an IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + Except values will be rejected if they are outside the cidr range + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - cidr + type: object + namespaceSelector: + description: |- + namespaceSelector selects namespaces using cluster-scoped labels. This field follows + standard label selector semantics; if present but empty, it selects all namespaces. + + If podSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the namespaces selected by namespaceSelector. + Otherwise it selects all pods in the namespaces selected by namespaceSelector. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + podSelector: + description: |- + podSelector is a label selector which selects pods. This field follows standard label + selector semantics; if present but empty, it selects all pods. + + If namespaceSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the Namespaces selected by NamespaceSelector. + Otherwise it selects the pods matching podSelector in the policy's own namespace. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + ingress: + description: |- + Ingress defines the list of ingress rules applied to pods selected by this CR. + Each rule allows traffic which matches both the from and ports sections. + items: + description: |- + NetworkPolicyIngressRule describes a particular set of traffic that is allowed to the pods + matched by a NetworkPolicySpec's podSelector. The traffic must match both ports and from. + properties: + from: + description: |- + from is a list of sources which should be able to access the pods selected for this rule. + Items in this list are combined using a logical OR operation. If this field is + empty or missing, this rule matches all sources (traffic not restricted by + source). If this field is present and contains at least one item, this rule + allows traffic only if the traffic matches at least one item in the from list. + items: + description: |- + NetworkPolicyPeer describes a peer to allow traffic to/from. Only certain combinations of + fields are allowed + properties: + ipBlock: + description: |- + ipBlock defines policy on a particular IPBlock. If this field is set then + neither of the other fields can be. + properties: + cidr: + description: |- + cidr is a string representing the IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + type: string + except: + description: |- + except is a slice of CIDRs that should not be included within an IPBlock + Valid examples are "192.168.1.0/24" or "2001:db8::/64" + Except values will be rejected if they are outside the cidr range + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - cidr + type: object + namespaceSelector: + description: |- + namespaceSelector selects namespaces using cluster-scoped labels. This field follows + standard label selector semantics; if present but empty, it selects all namespaces. + + If podSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the namespaces selected by namespaceSelector. + Otherwise it selects all pods in the namespaces selected by namespaceSelector. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + podSelector: + description: |- + podSelector is a label selector which selects pods. This field follows standard label + selector semantics; if present but empty, it selects all pods. + + If namespaceSelector is also set, then the NetworkPolicyPeer as a whole selects + the pods matching podSelector in the Namespaces selected by NamespaceSelector. + Otherwise it selects the pods matching podSelector in the policy's own namespace. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + type: object + type: array + x-kubernetes-list-type: atomic + ports: + description: |- + ports is a list of ports which should be made accessible on the pods selected for + this rule. Each item in this list is combined using a logical OR. If this field is + empty or missing, this rule matches all ports (traffic not restricted by port). + If this field is present and contains at least one item, then this rule allows + traffic only if the traffic matches at least one port in the list. + items: + description: NetworkPolicyPort describes a port to allow + traffic on + properties: + endPort: + description: |- + endPort indicates that the range of ports from port to endPort if set, inclusive, + should be allowed by the policy. This field cannot be defined if the port field + is not defined or if the port field is defined as a named (string) port. + The endPort must be equal or greater than port. + format: int32 + type: integer + port: + anyOf: + - type: integer + - type: string + description: |- + port represents the port on the given protocol. This can either be a numerical or named + port on a pod. If this field is not provided, this matches all port names and + numbers. + If present, only traffic on the specified protocol AND port will be matched. + x-kubernetes-int-or-string: true + protocol: + description: |- + protocol represents the protocol (TCP, UDP, or SCTP) which traffic must match. + If not specified, this field defaults to TCP. + type: string + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: array + type: object + nodeSelector: + additionalProperties: + type: string + description: NodeSelector Define which Nodes the Pods are scheduled + on. + type: object + passthroughHeaders: + description: |- + PassthroughHeaders defines names of HTTP headers, which are forwarded from MCP client requests to entrypoint. + They override headers with the same names. + It's useful for routing requests by headers with vmauth, + see https://docs.victoriametrics.com/victoriametrics/vmauth/#routing-by-header + items: + type: string + type: array + paused: + description: |- + Paused If set to true all actions on the underlying managed objects are not + going to be performed, except for delete actions. + type: boolean + podDisruptionBudget: + description: PodDisruptionBudget created by operator + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: |- + An eviction is allowed if at most "maxUnavailable" pods selected by + "selector" are unavailable after the eviction, i.e. even in absence of + the evicted pod. For example, one can prevent all voluntary evictions + by specifying 0. This is a mutually exclusive setting with "minAvailable". + x-kubernetes-int-or-string: true + minAvailable: + anyOf: + - type: integer + - type: string + description: |- + An eviction is allowed if at least "minAvailable" pods selected by + "selector" will still be available after the eviction, i.e. even in the + absence of the evicted pod. So for example you can prevent all voluntary + evictions by specifying "100%". + x-kubernetes-int-or-string: true + selectorLabels: + additionalProperties: + type: string + description: |- + replaces default labels selector generated by operator + it's useful when you need to create custom budget + type: object + unhealthyPodEvictionPolicy: + description: |- + UnhealthyPodEvictionPolicy defines the criteria for when unhealthy pods + + Valid policies are IfHealthyBudget and AlwaysAllow. + If no policy is specified, the default behavior will be used, + which corresponds to the IfHealthyBudget policy. + enum: + - IfHealthyBudget + - AlwaysAllow + type: string + type: object + podMetadata: + description: PodMetadata configures Labels and Annotations which are + propagated to the MCP server pods. + properties: + annotations: + additionalProperties: + type: string + description: |- + Annotations is an unstructured key value map stored with a resource that may be + set by external tools to store and retrieve arbitrary metadata. They are not + queryable and should be preserved when modifying objects. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations + type: object + labels: + additionalProperties: + type: string + description: |- + Labels Map of string keys and values that can be used to organize and categorize + (scope and select) objects. May match selectors of replication controllers + and services. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels + type: object + name: + description: |- + Name must be unique within a namespace. Is required when creating resources, although + some resources may allow a client to request the generation of an appropriate name + automatically. Name is primarily intended for creation idempotence and configuration + definition. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names + type: string + type: object + port: + description: Port listen address + type: string + preStopSleepSeconds: + description: |- + PreStopSleepSeconds defines the number of seconds to sleep in the preStop lifecycle hook. + It gives time for load balancers to remove the pod from rotation before the pod is terminated. + Defaults to 15 for applicable components. Set to 0 to disable. + format: int32 + type: integer + priorityClassName: + description: PriorityClassName class assigned to the Pods + type: string + readinessGates: + description: ReadinessGates defines pod readiness gates + items: + description: PodReadinessGate contains the reference to a pod condition + properties: + conditionType: + description: ConditionType refers to a condition in the pod's + condition list with matching type. + type: string + required: + - conditionType + type: object + type: array + readinessProbe: + description: ReadinessProbe that will be added to CR pod + type: object + x-kubernetes-preserve-unknown-fields: true + replicaCount: + description: ReplicaCount is the expected size of the Application. + format: int32 + type: integer + resources: + description: |- + Resources container resource request and limits, https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + if not defined default resources from operator config will be used + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This field depends on the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + revisionHistoryLimitCount: + description: |- + The number of old ReplicaSets to retain to allow rollback in deployment or + maximum number of revisions that will be maintained in the Deployment revision history. + Has no effect at StatefulSets + Defaults to 10. + format: int32 + type: integer + rollingUpdate: + description: RollingUpdate - overrides deployment update params. + properties: + maxSurge: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of pods that can be scheduled above the desired number of + pods. + Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). + This can not be 0 if MaxUnavailable is 0. + Absolute number is calculated from percentage by rounding up. + Defaults to 25%. + Example: when this is set to 30%, the new ReplicaSet can be scaled up immediately when + the rolling update starts, such that the total number of old and new pods do not exceed + 130% of desired pods. Once old pods have been killed, + new ReplicaSet can be scaled up further, ensuring that total number of pods running + at any time during the update is at most 130% of desired pods. + x-kubernetes-int-or-string: true + maxUnavailable: + anyOf: + - type: integer + - type: string + description: |- + The maximum number of pods that can be unavailable during the update. + Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). + Absolute number is calculated from percentage by rounding down. + This can not be 0 if MaxSurge is 0. + Defaults to 25%. + Example: when this is set to 30%, the old ReplicaSet can be scaled down to 70% of desired pods + immediately when the rolling update starts. Once new pods are ready, old ReplicaSet + can be scaled down further, followed by scaling up the new ReplicaSet, ensuring + that the total number of pods available at all times during the update is at + least 70% of desired pods. + x-kubernetes-int-or-string: true + type: object + runtimeClassName: + description: |- + RuntimeClassName - defines runtime class for kubernetes pod. + https://kubernetes.io/docs/concepts/containers/runtime-class/ + type: string + schedulerName: + description: SchedulerName - defines kubernetes scheduler name + type: string + secrets: + description: |- + Secrets is a list of Secrets in the same namespace as the Application + object, which shall be mounted into the Application container + at /etc/vm/secrets/SECRET_NAME folder + items: + type: string + type: array + securityContext: + description: |- + SecurityContext holds pod-level security attributes and common container settings. + This defaults to the default PodSecurityContext. + type: object + x-kubernetes-preserve-unknown-fields: true + serverMode: + description: |- + ServerMode defines transport of the MCP server: + http serves Streamable HTTP transport at /mcp path, + sse serves Server-Sent Events transport at /sse and /message paths. + Defaults to http. + See https://modelcontextprotocol.io/specification/2025-03-26/basic/transports + enum: + - http + - sse + type: string + serviceAccountName: + description: ServiceAccountName is the name of the ServiceAccount + to use to run the pods + type: string + serviceScrapeSpec: + description: ServiceScrapeSpec that will be added to the MCP server + VMServiceScrape spec + type: object + x-kubernetes-preserve-unknown-fields: true + serviceSpec: + description: ServiceSpec that will be added to the MCP server service + spec + properties: + metadata: + description: EmbeddedObjectMetadata defines objectMeta for additional + service. + properties: + annotations: + additionalProperties: + type: string + description: |- + Annotations is an unstructured key value map stored with a resource that may be + set by external tools to store and retrieve arbitrary metadata. They are not + queryable and should be preserved when modifying objects. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations + type: object + labels: + additionalProperties: + type: string + description: |- + Labels Map of string keys and values that can be used to organize and categorize + (scope and select) objects. May match selectors of replication controllers + and services. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels + type: object + name: + description: |- + Name must be unique within a namespace. Is required when creating resources, although + some resources may allow a client to request the generation of an appropriate name + automatically. Name is primarily intended for creation idempotence and configuration + definition. + Cannot be updated. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names + type: string + type: object + spec: + description: |- + ServiceSpec describes the attributes that a user creates on a service. + More info: https://kubernetes.io/docs/concepts/services-networking/service/ + type: object + x-kubernetes-preserve-unknown-fields: true + useAsDefault: + description: |- + UseAsDefault applies changes from given service definition to the main object Service + Changing from headless service to clusterIP or loadbalancer may break cross-component communication + type: boolean + required: + - spec + type: object + startupProbe: + description: StartupProbe that will be added to CR pod + type: object + x-kubernetes-preserve-unknown-fields: true + terminationGracePeriodSeconds: + description: TerminationGracePeriodSeconds period for container graceful + termination + format: int64 + type: integer + tolerations: + description: Tolerations If specified, the pod's tolerations. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators). + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array + topologySpreadConstraints: + description: |- + TopologySpreadConstraints embedded kubernetes pod configuration option, + controls how pods are spread across your cluster among failure-domains + such as regions, zones, nodes, and other user-defined topology domains + https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/ + items: + description: TopologySpreadConstraint specifies how to spread matching + pods among the given topology. + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + updateStrategy: + description: UpdateStrategy - overrides default update strategy. + enum: + - Recreate + - RollingUpdate + type: string + useDefaultResources: + description: |- + UseDefaultResources controls resource settings + By default, operator sets built-in resource requirements + type: boolean + useStrictSecurity: + description: |- + UseStrictSecurity enables strict security mode for component + it restricts disk writes access + uses non-root user out of the box + drops not needed security permissions + type: boolean + volumeMounts: + description: |- + VolumeMounts allows configuration of additional VolumeMounts on the output Deployment/StatefulSet definition. + VolumeMounts specified will be appended to other VolumeMounts in the Application container + items: + description: VolumeMount describes a mounting of a Volume within + a container. + properties: + bindMountOptions: + description: |- + bindMountOptions is the list of additional bind mount options to apply when + mounting this volume into the container. Allowed values are noexec, + nodev, and nosuid. These are Linux mount options and have no effect on + Windows nodes. + This field is not supported with image volumes. + This is an alpha field and requires enabling the VolumeBindMountOptions feature gate. + items: + type: string + type: array + x-kubernetes-list-type: set + mountPath: + description: Path within the container at which the volume should + be mounted. + type: string + mountPropagation: + description: |- + mountPropagation determines how mounts are propagated from the host + to container and the other way around. + When not set, MountPropagationNone is used. + This field is beta in 1.10. + When RecursiveReadOnly is set to IfPossible or to Enabled, MountPropagation must be None or unspecified + (which defaults to None). + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: |- + Mounted read-only if true, read-write otherwise (false or unspecified). + Defaults to false. + type: boolean + recursiveReadOnly: + description: |- + RecursiveReadOnly specifies whether read-only mounts should be handled + recursively. + + If ReadOnly is false, this field has no meaning and must be unspecified. + + If ReadOnly is true, and this field is set to Disabled, the mount is not made + recursively read-only. If this field is set to IfPossible, the mount is made + recursively read-only, if it is supported by the container runtime. If this + field is set to Enabled, the mount is made recursively read-only if it is + supported by the container runtime, otherwise the pod will not be started and + an error will be generated to indicate the reason. + + If this field is set to IfPossible or Enabled, MountPropagation must be set to + None (or be unspecified, which defaults to None). + + If this field is not specified, it is treated as an equivalent of Disabled. + type: string + subPath: + description: |- + Path within the volume from which the container's volume should be mounted. + Defaults to "" (volume's root). + type: string + subPathExpr: + description: |- + Expanded path within the volume from which the container's volume should be mounted. + Behaves similarly to SubPath but environment variable references $(VAR_NAME) are expanded using the container's environment. + Defaults to "" (volume's root). + SubPathExpr and SubPath are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + volumes: + description: |- + Volumes allows configuration of additional volumes on the output Deployment/StatefulSet definition. + Volumes specified will be appended to other volumes that are generated. + items: + description: Volume represents a named volume in a pod that may + be accessed by any container in the pod. + required: + - name + type: object + x-kubernetes-preserve-unknown-fields: true + type: array + vpa: + description: Configures vertical pod autoscaling. + properties: + recommenders: + description: Recommenders specifies custom VPA recommender names. + items: + description: |- + VerticalPodAutoscalerRecommenderSelector points to a specific Vertical Pod Autoscaler recommender. + In the future it might pass parameters to the recommender. + properties: + name: + description: Name of the recommender responsible for generating + recommendation for this object. + type: string + required: + - name + type: object + type: array + resourcePolicy: + description: ResourcePolicy controls how the autoscaler computes + recommended resources per container. + properties: + containerPolicies: + description: Per-container resource policies. + items: + description: |- + ContainerResourcePolicy controls how autoscaler computes the recommended + resources for a specific container. + properties: + containerName: + description: |- + Name of the container or DefaultContainerResourcePolicy, in which + case the policy is used by the containers that don't have their own + policy specified. + type: string + controlledResources: + description: |- + Specifies the type of recommendations that will be computed + (and possibly applied) by VPA. + If not specified, the default of [cpu, memory] will be used. + items: + description: ResourceName is the name identifying + various resources in a ResourceList. + type: string + type: array + controlledValues: + description: |- + Specifies which resource values should be controlled. + The default is "RequestsAndLimits". + enum: + - RequestsAndLimits + - RequestsOnly + type: string + maxAllowed: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Specifies the maximum amount of resources that will be recommended + for the container. The default is no maximum. + type: object + memoryAggregationIntervalCount: + description: |- + memoryAggregationIntervalCount is the number of consecutive + memoryAggregationIntervals which make up the memory aggregation window. + The total window length is: + MemoryAggregationIntervalSeconds * MemoryAggregationIntervalCount. + format: int64 + minimum: 1 + type: integer + memoryAggregationIntervalSeconds: + description: |- + memoryAggregationIntervalSeconds is the length of a single interval + (in seconds) for which the peak memory usage is computed. + Memory usage peaks are aggregated in multiples of this interval. + In other words, there is one memory usage sample per interval + (the maximum usage over that interval). + format: int32 + minimum: 1 + type: integer + minAllowed: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Specifies the minimal amount of resources that will be recommended + for the container. The default is no minimum. + type: object + mode: + description: Whether autoscaler is enabled for the container. + The default is "Auto". + enum: + - Auto + - "Off" + type: string + oomBumpUpRatio: + anyOf: + - type: integer + - type: string + description: oomBumpUpRatio is the ratio to increase + memory when OOM is detected. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + oomMinBumpUp: + anyOf: + - type: integer + - type: string + description: oomMinBumpUp is the minimum increase in + memory when OOM is detected. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + startupBoost: + description: |- + startupBoost specifies the startup boost policy for the container. + This overrides any pod-level startup boost policy. + The startup boost policy takes precedence over the rest of the fields in + this struct, except for ContainerName and ControlledValues. + properties: + cpu: + description: |- + cpu specifies the CPU startup boost policy. + If this field is not set, no startup boost is applied. + properties: + durationSeconds: + description: |- + durationSeconds indicates for how long to keep the pod boosted after it goes to Ready. + Defaults to 0. + format: int32 + type: integer + factor: + description: |- + factor specifies the factor to apply to the resource request. + This field is required when Type is "Factor". + format: int32 + type: integer + quantity: + anyOf: + - type: integer + - type: string + description: |- + quantity specifies the absolute resource quantity to be used as the + resource request and limit during the boost phase. + This field is required when Type is "Quantity". + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: + description: |- + type specifies the kind of boost to apply. + Supported values are: "Factor", "Quantity". + No startupboost will be applied for unrecognized values. + enum: + - Factor + - Quantity + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: factor is required when type is Factor + and forbidden otherwise + rule: (self.type == 'Factor') == has(self.factor) + - message: quantity is required when type is Quantity + and forbidden otherwise + rule: (self.type == 'Quantity') == has(self.quantity) + type: object + type: object + type: array + type: object + updatePolicy: + description: UpdatePolicy controls how the autoscaler applies + changes to pod resources. + properties: + evictAfterOOMSeconds: + description: |- + evictAfterOOMSeconds specifies the time in seconds to wait after an OOM event before + considering the pod for eviction. Pods that have OOMed in less than this time + since start will be evicted. + format: int32 + minimum: 1 + type: integer + evictionRequirements: + description: |- + EvictionRequirements is a list of EvictionRequirements that need to + evaluate to true in order for a Pod to be evicted. If more than one + EvictionRequirement is specified, all of them need to be fulfilled to allow eviction. + items: + description: |- + EvictionRequirement defines a single condition which needs to be true in + order to evict a Pod + properties: + changeRequirement: + description: EvictionChangeRequirement refers to the + relationship between the new target recommendation + for a Pod and its current requests, what kind of change + is necessary for the Pod to be evicted + enum: + - TargetHigherThanRequests + - TargetLowerThanRequests + type: string + resources: + description: |- + Resources is a list of one or more resources that the condition applies + to. If more than one resource is given, the EvictionRequirement is fulfilled + if at least one resource meets `changeRequirement`. + items: + description: ResourceName is the name identifying + various resources in a ResourceList. + type: string + type: array + required: + - changeRequirement + - resources + type: object + type: array + minReplicas: + description: |- + Minimal number of replicas which need to be alive for Updater to attempt + pod eviction (pending other checks like PDB). Only positive values are + allowed. Overrides global '--min-replicas' flag. + format: int32 + type: integer + updateMode: + description: |- + Controls when autoscaler applies changes to the pod resources. + The default is 'Recreate'. + enum: + - "Off" + - Initial + - Recreate + - InPlaceOrRecreate + - InPlace + - Auto + type: string + type: object + type: object + type: object + status: + description: VMMCPStatus defines the observed state of VMMCP + properties: + conditions: + description: 'Known .status.conditions.type are: "Available", "Progressing", + and "Degraded"' + items: + description: Condition defines status condition of the resource + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. + format: date-time + type: string + lastUpdateTime: + description: |- + LastUpdateTime is the last time of given type update. + This value is used for status TTL update and removal + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: Type of condition in CamelCase or in name.namespace.resource.victoriametrics.com/CamelCase. + maxLength: 316 + type: string + required: + - lastTransitionTime + - lastUpdateTime + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + lastAppliedSpec: + x-kubernetes-preserve-unknown-fields: true + observedGeneration: + description: |- + ObservedGeneration defines current generation picked by operator for the + reconcile + format: int64 + type: integer + reason: + description: Reason defines human readable error reason + type: string + updateStatus: + description: UpdateStatus defines a status for update rollout + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: name: vmnodescrapes.operator.victoriametrics.com spec: diff --git a/config/examples/vmmcp.yaml b/config/examples/vmmcp.yaml new file mode 100644 index 0000000000..e385587ee6 --- /dev/null +++ b/config/examples/vmmcp.yaml @@ -0,0 +1,14 @@ +apiVersion: operator.victoriametrics.com/v1 +kind: VMMCP +metadata: + name: example +spec: + # vmselect service of VMCluster named example + entrypoint: http://vmselect-example:8481 + instanceType: cluster + defaultTenantID: "0" + disabledTools: + - export + - flags + - test_rules + logFormat: json diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 3fe3a9e6a8..2bbcdd49fc 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -179,6 +179,11 @@ rules: - networking.k8s.io resources: - ingresses/finalizers + verbs: + - '*' +- apiGroups: + - networking.k8s.io + resources: - networkpolicies verbs: - '*' @@ -199,6 +204,7 @@ rules: - vmauths - vmclusters - vmdistributed + - vmmcps - vmpodscrapes - vmprobes - vmrules @@ -260,6 +266,7 @@ rules: - vmauths/status - vmclusters/status - vmdistributed/status + - vmmcps/status - vmnodescrapes/status - vmpodscrapes/status - vmprobes/status @@ -283,6 +290,7 @@ rules: - vldistributed/finalizers - vlsingles/finalizers - vmdistributed/finalizers + - vmmcps/finalizers - vtclusters/finalizers - vtsingles/finalizers verbs: diff --git a/config/samples/operator_v1_vmmcp.yaml b/config/samples/operator_v1_vmmcp.yaml new file mode 100644 index 0000000000..d102049e95 --- /dev/null +++ b/config/samples/operator_v1_vmmcp.yaml @@ -0,0 +1,9 @@ +apiVersion: operator.victoriametrics.com/v1 +kind: VMMCP +metadata: + labels: + app.kubernetes.io/name: victoriametrics-operator + app.kubernetes.io/managed-by: kustomize + name: dev +spec: + entrypoint: http://vmsingle-dev:8428 diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 22e5fc25ad..e4dc2202fa 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -384,3 +384,23 @@ webhooks: resources: - vmanomalies sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-operator-victoriametrics-com-v1-vmmcp + failurePolicy: Fail + name: vvmmcp.kb.io + rules: + - apiGroups: + - operator.victoriametrics.com + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - vmmcps + sideEffects: None diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 64eade65b5..353528528a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -13,6 +13,10 @@ aliases: ## tip +**Update note 1**: [vmmcp](https://docs.victoriametrics.com/operator/resources/vmmcp/): the `VMMCP` CRD must be installed and the operator's `ClusterRole` must be updated to grant `get`, `list`, `watch`, `create`, `update`, `patch` and `delete` on `operator.victoriametrics.com/vmmcps`, along with `vmmcps/status` and `vmmcps/finalizers`. See [config/rbac/role.yaml](https://github.com/VictoriaMetrics/operator/blob/master/config/rbac/role.yaml) for an example of these rules. The `VMMCP` controller is enabled by default and watches `VMMCP` objects at the cluster scope, even when no `VMMCP` objects exist. With an older `ClusterRole`, the operator logs `cannot list resource "vmmcps"` errors and shuts down once `-controller.cacheSyncTimeout` expires. Set `-controller.disableReconcileFor=VMMCP` to keep running without these permissions. See [#2739](https://github.com/VictoriaMetrics/operator/issues/2739). + +* FEATURE: [vmmcp](https://docs.victoriametrics.com/operator/resources/vmmcp/): add `VMMCP` CRD for [MCP server for VictoriaMetrics](https://github.com/VictoriaMetrics/mcp-victoriametrics), which gives AI assistants access to VictoriaMetrics data and documentation over [Model Context Protocol](https://modelcontextprotocol.io/). The default version is [v1.20.2](https://github.com/VictoriaMetrics/mcp-victoriametrics/releases/tag/v1.20.2) and can be changed with `VM_MCP_METRICS_VERSION` env variable. See [#2739](https://github.com/VictoriaMetrics/operator/issues/2739). + ## [v0.75.1](https://github.com/VictoriaMetrics/operator/releases/tag/v0.75.1) **Release date:** 21 October 2026 diff --git a/docs/api.md b/docs/api.md index acc5f0ef56..b54b11aa55 100644 --- a/docs/api.md +++ b/docs/api.md @@ -31,10 +31,78 @@ Package v1 contains API Schema definitions for the operator v1 API group - [VLSingle](#v1-vlsingle) - [VMAnomaly](#v1-vmanomaly) - [VMAnomalyConfig](#v1-vmanomalyconfig) +- [VMMCP](#v1-vmmcp) - [VTAgent](#v1-vtagent) - [VTCluster](#v1-vtcluster) - [VTSingle](#v1-vtsingle) +#### CommonMCPSpec {#v1-commonmcpspec} + + +CommonMCPSpec defines the desired state of the MCP server, which is shared by CRDs of MCP servers + +Appears in: [VMMCPSpec (v1)](#v1-vmmcpspec) + +| Field | Description | +| --- | --- | +| affinity#
_[Affinity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#affinity-v1-core)_ | _(Optional)_
Affinity If specified, the pod's scheduling constraints. | +| componentVersion#
_string_ | _(Optional)_
ComponentVersion defines default image tag for the MCP server.
It can be overwritten with image.tag value. | +| configMaps#
_string array_ | _(Optional)_
ConfigMaps is a list of ConfigMaps in the same namespace as the Application
object, which shall be mounted into the Application container
at /etc/vm/configs/CONFIGMAP_NAME folder | +| containers#
_[Container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#container-v1-core) array_ | _(Optional)_
Containers property allows to inject additions sidecars or to patch existing containers.
It can be useful for proxies, backup, etc. | +| disableAutomountServiceAccountToken#
_boolean_ | _(Optional)_
DisableAutomountServiceAccountToken whether to disable serviceAccount auto mount by Kubernetes
Operator will conditionally create volumes and volumeMounts for containers if it requires k8s API access.
For example, vmagent and vm-config-reloader requires k8s API access.
Operator creates volumes with name: "kube-api-access", which can be used as volumeMount for extraContainers if needed.
And also adds VolumeMounts at /var/run/secrets/kubernetes.io/serviceaccount.
Available from: v0.54.0 | +| disableSelfServiceScrape#
_boolean_ | _(Optional)_
DisableSelfServiceScrape controls creation of VMServiceScrape by operator
for the application.
Has priority over `VM_DISABLESELFSERVICESCRAPECREATION` operator env variable | +| disabledTools#
_string array_ | _(Optional)_
DisabledTools defines names of MCP tools, which are not exposed to MCP clients.
If it's not set, the MCP server uses its default list of disabled tools. | +| dnsConfig#
_[PodDNSConfig](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#poddnsconfig-v1-core)_ | _(Optional)_
Specifies the DNS parameters of a pod.
Parameters specified here will be merged to the generated DNS
configuration based on DNSPolicy. | +| dnsPolicy#
_[DNSPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#dnspolicy-v1-core)_ | _(Optional)_
DNSPolicy sets DNS policy for the pod | +| enableServiceLinks#
_boolean_ | _(Optional)_
EnableServiceLinks indicates whether information about services should be injected into pod's
environment variables, matching the syntax of Docker links.
Optional: Defaults to true. | +| extraArgs#
_object (keys:string, values:string)_ | _(Optional)_
ExtraArgs that will be passed to the application container
for example remoteWrite.tmpDataPath: /tmp | +| extraEnvs#
_[EnvVar](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#envvar-v1-core) array_ | _(Optional)_
ExtraEnvs that will be passed to the application container | +| extraEnvsFrom#
_[EnvFromSource](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#envfromsource-v1-core) array_ | _(Optional)_
ExtraEnvsFrom defines source of env variables for the application container
could either be secret or configmap | +| heartbeatInterval#
_string_ | _(Optional)_
HeartbeatInterval defines how often the MCP server sends heartbeats over Streamable HTTP connections,
which keeps them from being closed by gateways and load balancers.
Defaults to 30s. | +| hostAliases#
_[HostAlias](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#hostalias-v1-core) array_ | _(Optional)_
HostAliases provides mapping for ip and hostname,
that would be propagated to pod,
cannot be used with HostNetwork. | +| hostNetwork#
_boolean_ | _(Optional)_
HostNetwork controls whether the pod may use the node network namespace | +| host_aliases#
_[HostAlias](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#hostalias-v1-core) array_ | _(Optional)_
HostAliasesUnderScore provides mapping for ip and hostname,
that would be propagated to pod,
cannot be used with HostNetwork.
Has Priority over hostAliases field | +| image#
_[Image (v1beta1)](#v1beta1-image)_ | _(Optional)_
Image - docker image settings
if no specified operator uses default version from operator config | +| imagePullSecrets#
_[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#localobjectreference-v1-core) array_ | _(Optional)_
ImagePullSecrets An optional list of references to secrets in the same namespace
to use for pulling images from registries
see https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod | +| initContainers#
_[Container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#container-v1-core) array_ | _(Optional)_
InitContainers allows adding initContainers to the pod definition.
Any errors during the execution of an initContainer will lead to a restart of the Pod.
More info: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ | +| livenessProbe#
_[Probe](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#probe-v1-core)_ | _(Optional)_
LivenessProbe that will be added to CR pod | +| logFormat#
_string_ | _(Optional)_
LogFormat defines log output format of the MCP server. | +| logLevel#
_string_ | _(Optional)_
LogLevel defines minimum log level of the MCP server. | +| managedMetadata#
_[ManagedObjectsMetadata (v1beta1)](#v1beta1-managedobjectsmetadata)_ | _(Optional)_
ManagedMetadata defines metadata that will be added to the all objects
created by operator for the given CustomResource | +| minReadySeconds#
_integer_ | _(Optional)_
MinReadySeconds defines a minimum number of seconds to wait before starting update next pod
if previous in healthy state
Has no effect for VLogs and VMSingle | +| networkPolicy#
_[EmbeddedNetworkPolicy (v1beta1)](#v1beta1-embeddednetworkpolicy)_ | _(Optional)_
NetworkPolicy defines network access rules for pods created by this CR. | +| nodeSelector#
_object (keys:string, values:string)_ | _(Optional)_
NodeSelector Define which Nodes the Pods are scheduled on. | +| paused#
_boolean_ | _(Optional)_
Paused If set to true all actions on the underlying managed objects are not
going to be performed, except for delete actions. | +| podDisruptionBudget#
_[EmbeddedPodDisruptionBudgetSpec (v1beta1)](#v1beta1-embeddedpoddisruptionbudgetspec)_ | _(Optional)_
PodDisruptionBudget created by operator | +| podMetadata#
_[EmbeddedObjectMetadata (v1beta1)](#v1beta1-embeddedobjectmetadata)_ | _(Optional)_
PodMetadata configures Labels and Annotations which are propagated to the MCP server pods. | +| port#
_string_ | _(Optional)_
Port listen address | +| preStopSleepSeconds#
_integer_ | _(Optional)_
PreStopSleepSeconds defines the number of seconds to sleep in the preStop lifecycle hook.
It gives time for load balancers to remove the pod from rotation before the pod is terminated.
Defaults to 15 for applicable components. Set to 0 to disable. | +| priorityClassName#
_string_ | _(Optional)_
PriorityClassName class assigned to the Pods | +| readinessGates#
_[PodReadinessGate](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#podreadinessgate-v1-core) array_ | _(Required)_
ReadinessGates defines pod readiness gates | +| readinessProbe#
_[Probe](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#probe-v1-core)_ | _(Optional)_
ReadinessProbe that will be added to CR pod | +| replicaCount#
_integer_ | _(Optional)_
ReplicaCount is the expected size of the Application. | +| resources#
_[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#resourcerequirements-v1-core)_ | _(Optional)_
Resources container resource request and limits, https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
if not defined default resources from operator config will be used | +| revisionHistoryLimitCount#
_integer_ | _(Optional)_
The number of old ReplicaSets to retain to allow rollback in deployment or
maximum number of revisions that will be maintained in the Deployment revision history.
Has no effect at StatefulSets
Defaults to 10. | +| rollingUpdate#
_[RollingUpdateDeployment](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#rollingupdatedeployment-v1-apps)_ | _(Optional)_
RollingUpdate - overrides deployment update params. | +| runtimeClassName#
_string_ | _(Optional)_
RuntimeClassName - defines runtime class for kubernetes pod.
https://kubernetes.io/docs/concepts/containers/runtime-class/ | +| schedulerName#
_string_ | _(Optional)_
SchedulerName - defines kubernetes scheduler name | +| secrets#
_string array_ | _(Optional)_
Secrets is a list of Secrets in the same namespace as the Application
object, which shall be mounted into the Application container
at /etc/vm/secrets/SECRET_NAME folder | +| securityContext#
_[SecurityContext (v1beta1)](#v1beta1-securitycontext)_ | _(Optional)_
SecurityContext holds pod-level security attributes and common container settings.
This defaults to the default PodSecurityContext. | +| serverMode#
_[MCPServerMode (v1)](#v1-mcpservermode)_ | _(Optional)_
ServerMode defines transport of the MCP server:
http serves Streamable HTTP transport at /mcp path,
sse serves Server-Sent Events transport at /sse and /message paths.
Defaults to http.
See https://modelcontextprotocol.io/specification/2025-03-26/basic/transports | +| serviceAccountName#
_string_ | _(Optional)_
ServiceAccountName is the name of the ServiceAccount to use to run the pods | +| serviceScrapeSpec#
_[VMServiceScrapeSpec (v1beta1)](#v1beta1-vmservicescrapespec)_ | _(Optional)_
ServiceScrapeSpec that will be added to the MCP server VMServiceScrape spec | +| serviceSpec#
_[AdditionalServiceSpec (v1beta1)](#v1beta1-additionalservicespec)_ | _(Optional)_
ServiceSpec that will be added to the MCP server service spec | +| startupProbe#
_[Probe](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#probe-v1-core)_ | _(Optional)_
StartupProbe that will be added to CR pod | +| terminationGracePeriodSeconds#
_integer_ | _(Optional)_
TerminationGracePeriodSeconds period for container graceful termination | +| tolerations#
_[Toleration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#toleration-v1-core) array_ | _(Optional)_
Tolerations If specified, the pod's tolerations. | +| topologySpreadConstraints#
_[TopologySpreadConstraint](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#topologyspreadconstraint-v1-core) array_ | _(Optional)_
TopologySpreadConstraints embedded kubernetes pod configuration option,
controls how pods are spread across your cluster among failure-domains
such as regions, zones, nodes, and other user-defined topology domains
https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/ | +| updateStrategy#
_[DeploymentStrategyType](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#deploymentstrategytype-v1-apps)_ | _(Optional)_
UpdateStrategy - overrides default update strategy. | +| useDefaultResources#
_boolean_ | _(Optional)_
UseDefaultResources controls resource settings
By default, operator sets built-in resource requirements | +| useStrictSecurity#
_boolean_ | _(Optional)_
UseStrictSecurity enables strict security mode for component
it restricts disk writes access
uses non-root user out of the box
drops not needed security permissions | +| volumeMounts#
_[VolumeMount](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#volumemount-v1-core) array_ | _(Optional)_
VolumeMounts allows configuration of additional VolumeMounts on the output Deployment/StatefulSet definition.
VolumeMounts specified will be appended to other VolumeMounts in the Application container | +| volumes#
_[Volume](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#volume-v1-core) array_ | _(Optional)_
Volumes allows configuration of additional volumes on the output Deployment/StatefulSet definition.
Volumes specified will be appended to other volumes that are generated. | +| vpa#
_[EmbeddedVPA (v1beta1)](#v1beta1-embeddedvpa)_ | _(Optional)_
Configures vertical pod autoscaling. | + #### FieldsListString {#v1-fieldsliststring} @@ -45,6 +113,18 @@ FieldsListString represents list of json encoded strings Appears in: [SyslogTCPListener (v1)](#v1-syslogtcplistener), [SyslogUDPListener (v1)](#v1-syslogudplistener) +#### MCPServerMode {#v1-mcpservermode} + + +_Underlying type:_ _string_ + +MCPServerMode defines transport of the MCP server + +_Validation:_ +- Enum: [http sse] + +Appears in: [CommonMCPSpec (v1)](#v1-commonmcpspec), [VMMCPSpec (v1)](#v1-vmmcpspec) + #### OAuth2 {#v1-oauth2} @@ -878,6 +958,107 @@ Appears in: [VMAnomalySpec (v1)](#v1-vmanomalyspec) | timeout#
_string_ | _(Required)_
Timeout for the requests, passed as a string | | tlsConfig#
_[TLSConfig (v1beta1)](#v1beta1-tlsconfig)_ | _(Required)_
TLSConfig defines tls connection configuration | +#### VMMCP {#v1-vmmcp} + + +VMMCP is the MCP server for VictoriaMetrics, which provides AI assistants with access to VictoriaMetrics APIs and documentation. +See https://github.com/VictoriaMetrics/mcp-victoriametrics +VMMCP is the Schema for the API + +| Field | Description | +| --- | --- | +| apiVersion
_string_ | (Required)
`operator.victoriametrics.com/v1` | +| kind
_string_ | (Required)
`VMMCP` | +| metadata#
_[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#objectmeta-v1-meta)_ | _(Required)_
Refer to Kubernetes API documentation for fields of `metadata`. | +| spec#
_[VMMCPSpec (v1)](#v1-vmmcpspec)_ | _(Required)_
| + +#### VMMCPInstanceType {#v1-vmmcpinstancetype} + + +_Underlying type:_ _string_ + +VMMCPInstanceType defines type of VictoriaMetrics instance, which is queried by the MCP server + +_Validation:_ +- Enum: [single cluster] + +Appears in: [VMMCPSpec (v1)](#v1-vmmcpspec) + +#### VMMCPSpec {#v1-vmmcpspec} + + +VMMCPSpec defines the desired state of VMMCP + +Appears in: [VMMCP (v1)](#v1-vmmcp) + +| Field | Description | +| --- | --- | +| affinity#
_[Affinity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#affinity-v1-core)_ | _(Optional)_
Affinity If specified, the pod's scheduling constraints. | +| bearerToken#
_[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#secretkeyselector-v1-core)_ | _(Optional)_
BearerToken defines a Secret key with a token, which authenticates requests to entrypoint. | +| cloudAPIKey#
_[SecretKeySelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#secretkeyselector-v1-core)_ | _(Optional)_
CloudAPIKey defines a Secret key with VictoriaMetrics Cloud API key.
If it's set, the MCP server works with VictoriaMetrics Cloud deployments instead of entrypoint.
See https://docs.victoriametrics.com/victoriametrics-cloud/api/ | +| componentVersion#
_string_ | _(Optional)_
ComponentVersion defines default image tag for the MCP server.
It can be overwritten with image.tag value. | +| configMaps#
_string array_ | _(Optional)_
ConfigMaps is a list of ConfigMaps in the same namespace as the Application
object, which shall be mounted into the Application container
at /etc/vm/configs/CONFIGMAP_NAME folder | +| containers#
_[Container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#container-v1-core) array_ | _(Optional)_
Containers property allows to inject additions sidecars or to patch existing containers.
It can be useful for proxies, backup, etc. | +| defaultTenantID#
_string_ | _(Optional)_
DefaultTenantID defines tenant for requests without explicit tenant to the cluster instance type
in accountID or accountID:projectID format.
Defaults to 0. | +| disableAutomountServiceAccountToken#
_boolean_ | _(Optional)_
DisableAutomountServiceAccountToken whether to disable serviceAccount auto mount by Kubernetes
Operator will conditionally create volumes and volumeMounts for containers if it requires k8s API access.
For example, vmagent and vm-config-reloader requires k8s API access.
Operator creates volumes with name: "kube-api-access", which can be used as volumeMount for extraContainers if needed.
And also adds VolumeMounts at /var/run/secrets/kubernetes.io/serviceaccount.
Available from: v0.54.0 | +| disableResources#
_boolean_ | _(Optional)_
DisableResources disables MCP resources with VictoriaMetrics documentation,
while documentation tool keeps working. | +| disableSelfServiceScrape#
_boolean_ | _(Optional)_
DisableSelfServiceScrape controls creation of VMServiceScrape by operator
for the application.
Has priority over `VM_DISABLESELFSERVICESCRAPECREATION` operator env variable | +| disabledTools#
_string array_ | _(Optional)_
DisabledTools defines names of MCP tools, which are not exposed to MCP clients.
If it's not set, the MCP server uses its default list of disabled tools. | +| dnsConfig#
_[PodDNSConfig](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#poddnsconfig-v1-core)_ | _(Optional)_
Specifies the DNS parameters of a pod.
Parameters specified here will be merged to the generated DNS
configuration based on DNSPolicy. | +| dnsPolicy#
_[DNSPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#dnspolicy-v1-core)_ | _(Optional)_
DNSPolicy sets DNS policy for the pod | +| enableServiceLinks#
_boolean_ | _(Optional)_
EnableServiceLinks indicates whether information about services should be injected into pod's
environment variables, matching the syntax of Docker links.
Optional: Defaults to true. | +| entrypoint#
_string_ | _(Optional)_
Entrypoint defines root URL of VictoriaMetrics instance, which is queried by the MCP server:
VMSingle URL (e.g. http://vmsingle-example.default.svc:8428) for single instance type
or vmselect URL (e.g. http://vmselect-example.default.svc:8481) for cluster instance type.
Either entrypoint or cloudAPIKey must be set. | +| extraArgs#
_object (keys:string, values:string)_ | _(Optional)_
ExtraArgs that will be passed to the application container
for example remoteWrite.tmpDataPath: /tmp | +| extraEnvs#
_[EnvVar](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#envvar-v1-core) array_ | _(Optional)_
ExtraEnvs that will be passed to the application container | +| extraEnvsFrom#
_[EnvFromSource](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#envfromsource-v1-core) array_ | _(Optional)_
ExtraEnvsFrom defines source of env variables for the application container
could either be secret or configmap | +| headers#
_string array_ | _(Optional)_
Headers defines custom HTTP headers in key=value format, which are sent with requests to entrypoint. | +| heartbeatInterval#
_string_ | _(Optional)_
HeartbeatInterval defines how often the MCP server sends heartbeats over Streamable HTTP connections,
which keeps them from being closed by gateways and load balancers.
Defaults to 30s. | +| hostAliases#
_[HostAlias](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#hostalias-v1-core) array_ | _(Optional)_
HostAliases provides mapping for ip and hostname,
that would be propagated to pod,
cannot be used with HostNetwork. | +| hostNetwork#
_boolean_ | _(Optional)_
HostNetwork controls whether the pod may use the node network namespace | +| host_aliases#
_[HostAlias](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#hostalias-v1-core) array_ | _(Optional)_
HostAliasesUnderScore provides mapping for ip and hostname,
that would be propagated to pod,
cannot be used with HostNetwork.
Has Priority over hostAliases field | +| image#
_[Image (v1beta1)](#v1beta1-image)_ | _(Optional)_
Image - docker image settings
if no specified operator uses default version from operator config | +| imagePullSecrets#
_[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#localobjectreference-v1-core) array_ | _(Optional)_
ImagePullSecrets An optional list of references to secrets in the same namespace
to use for pulling images from registries
see https://kubernetes.io/docs/concepts/containers/images/#specifying-imagepullsecrets-on-a-pod | +| initContainers#
_[Container](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#container-v1-core) array_ | _(Optional)_
InitContainers allows adding initContainers to the pod definition.
Any errors during the execution of an initContainer will lead to a restart of the Pod.
More info: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ | +| instanceType#
_[VMMCPInstanceType (v1)](#v1-vmmcpinstancetype)_ | _(Optional)_
InstanceType defines type of VictoriaMetrics instance set at entrypoint.
Defaults to single. | +| livenessProbe#
_[Probe](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#probe-v1-core)_ | _(Optional)_
LivenessProbe that will be added to CR pod | +| logFormat#
_string_ | _(Optional)_
LogFormat defines log output format of the MCP server. | +| logLevel#
_string_ | _(Optional)_
LogLevel defines minimum log level of the MCP server. | +| managedMetadata#
_[ManagedObjectsMetadata (v1beta1)](#v1beta1-managedobjectsmetadata)_ | _(Optional)_
ManagedMetadata defines metadata that will be added to the all objects
created by operator for the given CustomResource | +| minReadySeconds#
_integer_ | _(Optional)_
MinReadySeconds defines a minimum number of seconds to wait before starting update next pod
if previous in healthy state
Has no effect for VLogs and VMSingle | +| networkPolicy#
_[EmbeddedNetworkPolicy (v1beta1)](#v1beta1-embeddednetworkpolicy)_ | _(Optional)_
NetworkPolicy defines network access rules for pods created by this CR. | +| nodeSelector#
_object (keys:string, values:string)_ | _(Optional)_
NodeSelector Define which Nodes the Pods are scheduled on. | +| passthroughHeaders#
_string array_ | _(Optional)_
PassthroughHeaders defines names of HTTP headers, which are forwarded from MCP client requests to entrypoint.
They override headers with the same names.
It's useful for routing requests by headers with vmauth,
see https://docs.victoriametrics.com/victoriametrics/vmauth/#routing-by-header | +| paused#
_boolean_ | _(Optional)_
Paused If set to true all actions on the underlying managed objects are not
going to be performed, except for delete actions. | +| podDisruptionBudget#
_[EmbeddedPodDisruptionBudgetSpec (v1beta1)](#v1beta1-embeddedpoddisruptionbudgetspec)_ | _(Optional)_
PodDisruptionBudget created by operator | +| podMetadata#
_[EmbeddedObjectMetadata (v1beta1)](#v1beta1-embeddedobjectmetadata)_ | _(Optional)_
PodMetadata configures Labels and Annotations which are propagated to the MCP server pods. | +| port#
_string_ | _(Optional)_
Port listen address | +| preStopSleepSeconds#
_integer_ | _(Optional)_
PreStopSleepSeconds defines the number of seconds to sleep in the preStop lifecycle hook.
It gives time for load balancers to remove the pod from rotation before the pod is terminated.
Defaults to 15 for applicable components. Set to 0 to disable. | +| priorityClassName#
_string_ | _(Optional)_
PriorityClassName class assigned to the Pods | +| readinessGates#
_[PodReadinessGate](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#podreadinessgate-v1-core) array_ | _(Required)_
ReadinessGates defines pod readiness gates | +| readinessProbe#
_[Probe](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#probe-v1-core)_ | _(Optional)_
ReadinessProbe that will be added to CR pod | +| replicaCount#
_integer_ | _(Optional)_
ReplicaCount is the expected size of the Application. | +| resources#
_[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#resourcerequirements-v1-core)_ | _(Optional)_
Resources container resource request and limits, https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
if not defined default resources from operator config will be used | +| revisionHistoryLimitCount#
_integer_ | _(Optional)_
The number of old ReplicaSets to retain to allow rollback in deployment or
maximum number of revisions that will be maintained in the Deployment revision history.
Has no effect at StatefulSets
Defaults to 10. | +| rollingUpdate#
_[RollingUpdateDeployment](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#rollingupdatedeployment-v1-apps)_ | _(Optional)_
RollingUpdate - overrides deployment update params. | +| runtimeClassName#
_string_ | _(Optional)_
RuntimeClassName - defines runtime class for kubernetes pod.
https://kubernetes.io/docs/concepts/containers/runtime-class/ | +| schedulerName#
_string_ | _(Optional)_
SchedulerName - defines kubernetes scheduler name | +| secrets#
_string array_ | _(Optional)_
Secrets is a list of Secrets in the same namespace as the Application
object, which shall be mounted into the Application container
at /etc/vm/secrets/SECRET_NAME folder | +| securityContext#
_[SecurityContext (v1beta1)](#v1beta1-securitycontext)_ | _(Optional)_
SecurityContext holds pod-level security attributes and common container settings.
This defaults to the default PodSecurityContext. | +| serverMode#
_[MCPServerMode (v1)](#v1-mcpservermode)_ | _(Optional)_
ServerMode defines transport of the MCP server:
http serves Streamable HTTP transport at /mcp path,
sse serves Server-Sent Events transport at /sse and /message paths.
Defaults to http.
See https://modelcontextprotocol.io/specification/2025-03-26/basic/transports | +| serviceAccountName#
_string_ | _(Optional)_
ServiceAccountName is the name of the ServiceAccount to use to run the pods | +| serviceScrapeSpec#
_[VMServiceScrapeSpec (v1beta1)](#v1beta1-vmservicescrapespec)_ | _(Optional)_
ServiceScrapeSpec that will be added to the MCP server VMServiceScrape spec | +| serviceSpec#
_[AdditionalServiceSpec (v1beta1)](#v1beta1-additionalservicespec)_ | _(Optional)_
ServiceSpec that will be added to the MCP server service spec | +| startupProbe#
_[Probe](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#probe-v1-core)_ | _(Optional)_
StartupProbe that will be added to CR pod | +| terminationGracePeriodSeconds#
_integer_ | _(Optional)_
TerminationGracePeriodSeconds period for container graceful termination | +| tolerations#
_[Toleration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#toleration-v1-core) array_ | _(Optional)_
Tolerations If specified, the pod's tolerations. | +| topologySpreadConstraints#
_[TopologySpreadConstraint](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#topologyspreadconstraint-v1-core) array_ | _(Optional)_
TopologySpreadConstraints embedded kubernetes pod configuration option,
controls how pods are spread across your cluster among failure-domains
such as regions, zones, nodes, and other user-defined topology domains
https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/ | +| updateStrategy#
_[DeploymentStrategyType](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#deploymentstrategytype-v1-apps)_ | _(Optional)_
UpdateStrategy - overrides default update strategy. | +| useDefaultResources#
_boolean_ | _(Optional)_
UseDefaultResources controls resource settings
By default, operator sets built-in resource requirements | +| useStrictSecurity#
_boolean_ | _(Optional)_
UseStrictSecurity enables strict security mode for component
it restricts disk writes access
uses non-root user out of the box
drops not needed security permissions | +| volumeMounts#
_[VolumeMount](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#volumemount-v1-core) array_ | _(Optional)_
VolumeMounts allows configuration of additional VolumeMounts on the output Deployment/StatefulSet definition.
VolumeMounts specified will be appended to other VolumeMounts in the Application container | +| volumes#
_[Volume](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.35/#volume-v1-core) array_ | _(Optional)_
Volumes allows configuration of additional volumes on the output Deployment/StatefulSet definition.
Volumes specified will be appended to other volumes that are generated. | +| vpa#
_[EmbeddedVPA (v1beta1)](#v1beta1-embeddedvpa)_ | _(Optional)_
Configures vertical pod autoscaling. | + #### VTAgent {#v1-vtagent} @@ -1840,7 +2021,7 @@ labels,selector, ports. if metadata.name is not defined, service will have format {{CRD_TYPE}}-{{CRD_NAME}}-additional-service. if UseAsDefault is set to true, changes applied to the main service without additional service creation -Appears in: [VLAgentSpec (v1)](#v1-vlagentspec), [VLInsert (v1)](#v1-vlinsert), [VLSelect (v1)](#v1-vlselect), [VLSingleSpec (v1)](#v1-vlsinglespec), [VLStorage (v1)](#v1-vlstorage), [VLogsSpec (v1beta1)](#v1beta1-vlogsspec), [VMAgentSpec (v1beta1)](#v1beta1-vmagentspec), [VMAlertSpec (v1beta1)](#v1beta1-vmalertspec), [VMAlertmanagerSpec (v1beta1)](#v1beta1-vmalertmanagerspec), [VMAuthLoadBalancerSpec (v1beta1)](#v1beta1-vmauthloadbalancerspec), [VMAuthSpec (v1beta1)](#v1beta1-vmauthspec), [VMInsert (v1beta1)](#v1beta1-vminsert), [VMSelect (v1beta1)](#v1beta1-vmselect), [VMSingleSpec (v1beta1)](#v1beta1-vmsinglespec), [VMStorage (v1beta1)](#v1beta1-vmstorage), [VTAgentSpec (v1)](#v1-vtagentspec), [VTInsert (v1)](#v1-vtinsert), [VTSelect (v1)](#v1-vtselect), [VTSingleSpec (v1)](#v1-vtsinglespec), [VTStorage (v1)](#v1-vtstorage) +Appears in: [CommonMCPSpec (v1)](#v1-commonmcpspec), [VLAgentSpec (v1)](#v1-vlagentspec), [VLInsert (v1)](#v1-vlinsert), [VLSelect (v1)](#v1-vlselect), [VLSingleSpec (v1)](#v1-vlsinglespec), [VLStorage (v1)](#v1-vlstorage), [VLogsSpec (v1beta1)](#v1beta1-vlogsspec), [VMAgentSpec (v1beta1)](#v1beta1-vmagentspec), [VMAlertSpec (v1beta1)](#v1beta1-vmalertspec), [VMAlertmanagerSpec (v1beta1)](#v1beta1-vmalertmanagerspec), [VMAuthLoadBalancerSpec (v1beta1)](#v1beta1-vmauthloadbalancerspec), [VMAuthSpec (v1beta1)](#v1beta1-vmauthspec), [VMInsert (v1beta1)](#v1beta1-vminsert), [VMMCPSpec (v1)](#v1-vmmcpspec), [VMSelect (v1beta1)](#v1beta1-vmselect), [VMSingleSpec (v1beta1)](#v1beta1-vmsinglespec), [VMStorage (v1beta1)](#v1beta1-vmstorage), [VTAgentSpec (v1)](#v1-vtagentspec), [VTInsert (v1)](#v1-vtinsert), [VTSelect (v1)](#v1-vtselect), [VTSingleSpec (v1)](#v1-vtsinglespec), [VTStorage (v1)](#v1-vtstorage) | Field | Description | | --- | --- | @@ -1987,7 +2168,7 @@ Appears in: [TLSClientConfig (v1beta1)](#v1beta1-tlsclientconfig), [TLSServerCon CommonAppsParams defines common params for deployment and statefulset specifications -Appears in: [VLAgentSpec (v1)](#v1-vlagentspec), [VLDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vldistributedzoneagentspec), [VLInsert (v1)](#v1-vlinsert), [VLSelect (v1)](#v1-vlselect), [VLSingleSpec (v1)](#v1-vlsinglespec), [VLStorage (v1)](#v1-vlstorage), [VLogsSpec (v1beta1)](#v1beta1-vlogsspec), [VMAgentSpec (v1beta1)](#v1beta1-vmagentspec), [VMAlertSpec (v1beta1)](#v1beta1-vmalertspec), [VMAlertmanagerSpec (v1beta1)](#v1beta1-vmalertmanagerspec), [VMAnomalySpec (v1)](#v1-vmanomalyspec), [VMAuthLoadBalancerSpec (v1beta1)](#v1beta1-vmauthloadbalancerspec), [VMAuthSpec (v1beta1)](#v1beta1-vmauthspec), [VMDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vmdistributedzoneagentspec), [VMInsert (v1beta1)](#v1beta1-vminsert), [VMSelect (v1beta1)](#v1beta1-vmselect), [VMSingleSpec (v1beta1)](#v1beta1-vmsinglespec), [VMStorage (v1beta1)](#v1beta1-vmstorage), [VTAgentSpec (v1)](#v1-vtagentspec), [VTInsert (v1)](#v1-vtinsert), [VTSelect (v1)](#v1-vtselect), [VTSingleSpec (v1)](#v1-vtsinglespec), [VTStorage (v1)](#v1-vtstorage) +Appears in: [CommonMCPSpec (v1)](#v1-commonmcpspec), [VLAgentSpec (v1)](#v1-vlagentspec), [VLDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vldistributedzoneagentspec), [VLInsert (v1)](#v1-vlinsert), [VLSelect (v1)](#v1-vlselect), [VLSingleSpec (v1)](#v1-vlsinglespec), [VLStorage (v1)](#v1-vlstorage), [VLogsSpec (v1beta1)](#v1beta1-vlogsspec), [VMAgentSpec (v1beta1)](#v1beta1-vmagentspec), [VMAlertSpec (v1beta1)](#v1beta1-vmalertspec), [VMAlertmanagerSpec (v1beta1)](#v1beta1-vmalertmanagerspec), [VMAnomalySpec (v1)](#v1-vmanomalyspec), [VMAuthLoadBalancerSpec (v1beta1)](#v1beta1-vmauthloadbalancerspec), [VMAuthSpec (v1beta1)](#v1beta1-vmauthspec), [VMDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vmdistributedzoneagentspec), [VMInsert (v1beta1)](#v1beta1-vminsert), [VMMCPSpec (v1)](#v1-vmmcpspec), [VMSelect (v1beta1)](#v1beta1-vmselect), [VMSingleSpec (v1beta1)](#v1beta1-vmsinglespec), [VMStorage (v1beta1)](#v1beta1-vmstorage), [VTAgentSpec (v1)](#v1-vtagentspec), [VTInsert (v1)](#v1-vtinsert), [VTSelect (v1)](#v1-vtselect), [VTSingleSpec (v1)](#v1-vtsinglespec), [VTStorage (v1)](#v1-vtstorage) | Field | Description | | --- | --- | @@ -2479,7 +2660,7 @@ Appears in: [VMAuthSpec (v1beta1)](#v1beta1-vmauthspec) EmbeddedNetworkPolicy defines configuration for a NetworkPolicy protecting pods owned by the CR. -Appears in: [VLAgentSpec (v1)](#v1-vlagentspec), [VLInsert (v1)](#v1-vlinsert), [VLSelect (v1)](#v1-vlselect), [VLSingleSpec (v1)](#v1-vlsinglespec), [VLStorage (v1)](#v1-vlstorage), [VMAgentSpec (v1beta1)](#v1beta1-vmagentspec), [VMAlertSpec (v1beta1)](#v1beta1-vmalertspec), [VMAlertmanagerSpec (v1beta1)](#v1beta1-vmalertmanagerspec), [VMAnomalySpec (v1)](#v1-vmanomalyspec), [VMAuthLoadBalancerSpec (v1beta1)](#v1beta1-vmauthloadbalancerspec), [VMAuthSpec (v1beta1)](#v1beta1-vmauthspec), [VMInsert (v1beta1)](#v1beta1-vminsert), [VMSelect (v1beta1)](#v1beta1-vmselect), [VMSingleSpec (v1beta1)](#v1beta1-vmsinglespec), [VMStorage (v1beta1)](#v1beta1-vmstorage), [VTAgentSpec (v1)](#v1-vtagentspec), [VTInsert (v1)](#v1-vtinsert), [VTSelect (v1)](#v1-vtselect), [VTSingleSpec (v1)](#v1-vtsinglespec), [VTStorage (v1)](#v1-vtstorage) +Appears in: [CommonMCPSpec (v1)](#v1-commonmcpspec), [VLAgentSpec (v1)](#v1-vlagentspec), [VLInsert (v1)](#v1-vlinsert), [VLSelect (v1)](#v1-vlselect), [VLSingleSpec (v1)](#v1-vlsinglespec), [VLStorage (v1)](#v1-vlstorage), [VMAgentSpec (v1beta1)](#v1beta1-vmagentspec), [VMAlertSpec (v1beta1)](#v1beta1-vmalertspec), [VMAlertmanagerSpec (v1beta1)](#v1beta1-vmalertmanagerspec), [VMAnomalySpec (v1)](#v1-vmanomalyspec), [VMAuthLoadBalancerSpec (v1beta1)](#v1beta1-vmauthloadbalancerspec), [VMAuthSpec (v1beta1)](#v1beta1-vmauthspec), [VMInsert (v1beta1)](#v1beta1-vminsert), [VMMCPSpec (v1)](#v1-vmmcpspec), [VMSelect (v1beta1)](#v1beta1-vmselect), [VMSingleSpec (v1beta1)](#v1beta1-vmsinglespec), [VMStorage (v1beta1)](#v1beta1-vmstorage), [VTAgentSpec (v1)](#v1-vtagentspec), [VTInsert (v1)](#v1-vtinsert), [VTSelect (v1)](#v1-vtselect), [VTSingleSpec (v1)](#v1-vtsinglespec), [VTStorage (v1)](#v1-vtstorage) | Field | Description | | --- | --- | @@ -2492,7 +2673,7 @@ Appears in: [VLAgentSpec (v1)](#v1-vlagentspec), [VLInsert (v1)](#v1-vlinsert), EmbeddedObjectMetadata contains a subset of the fields included in k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta Only fields which are relevant to embedded resources are included. -Appears in: [AdditionalServiceSpec (v1beta1)](#v1beta1-additionalservicespec), [EmbeddedHTTPRoute (v1beta1)](#v1beta1-embeddedhttproute), [EmbeddedIngress (v1beta1)](#v1beta1-embeddedingress), [EmbeddedPersistentVolumeClaim (v1beta1)](#v1beta1-embeddedpersistentvolumeclaim), [VLAgentSpec (v1)](#v1-vlagentspec), [VLDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vldistributedzoneagentspec), [VLInsert (v1)](#v1-vlinsert), [VLSelect (v1)](#v1-vlselect), [VLSingleSpec (v1)](#v1-vlsinglespec), [VLStorage (v1)](#v1-vlstorage), [VLogsSpec (v1beta1)](#v1beta1-vlogsspec), [VMAgentSpec (v1beta1)](#v1beta1-vmagentspec), [VMAlertSpec (v1beta1)](#v1beta1-vmalertspec), [VMAlertmanagerSpec (v1beta1)](#v1beta1-vmalertmanagerspec), [VMAnomalySpec (v1)](#v1-vmanomalyspec), [VMAuthLoadBalancerSpec (v1beta1)](#v1beta1-vmauthloadbalancerspec), [VMAuthSpec (v1beta1)](#v1beta1-vmauthspec), [VMDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vmdistributedzoneagentspec), [VMInsert (v1beta1)](#v1beta1-vminsert), [VMSelect (v1beta1)](#v1beta1-vmselect), [VMSingleSpec (v1beta1)](#v1beta1-vmsinglespec), [VMStorage (v1beta1)](#v1beta1-vmstorage), [VTAgentSpec (v1)](#v1-vtagentspec), [VTInsert (v1)](#v1-vtinsert), [VTSelect (v1)](#v1-vtselect), [VTSingleSpec (v1)](#v1-vtsinglespec), [VTStorage (v1)](#v1-vtstorage) +Appears in: [AdditionalServiceSpec (v1beta1)](#v1beta1-additionalservicespec), [CommonMCPSpec (v1)](#v1-commonmcpspec), [EmbeddedHTTPRoute (v1beta1)](#v1beta1-embeddedhttproute), [EmbeddedIngress (v1beta1)](#v1beta1-embeddedingress), [EmbeddedPersistentVolumeClaim (v1beta1)](#v1beta1-embeddedpersistentvolumeclaim), [VLAgentSpec (v1)](#v1-vlagentspec), [VLDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vldistributedzoneagentspec), [VLInsert (v1)](#v1-vlinsert), [VLSelect (v1)](#v1-vlselect), [VLSingleSpec (v1)](#v1-vlsinglespec), [VLStorage (v1)](#v1-vlstorage), [VLogsSpec (v1beta1)](#v1beta1-vlogsspec), [VMAgentSpec (v1beta1)](#v1beta1-vmagentspec), [VMAlertSpec (v1beta1)](#v1beta1-vmalertspec), [VMAlertmanagerSpec (v1beta1)](#v1beta1-vmalertmanagerspec), [VMAnomalySpec (v1)](#v1-vmanomalyspec), [VMAuthLoadBalancerSpec (v1beta1)](#v1beta1-vmauthloadbalancerspec), [VMAuthSpec (v1beta1)](#v1beta1-vmauthspec), [VMDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vmdistributedzoneagentspec), [VMInsert (v1beta1)](#v1beta1-vminsert), [VMMCPSpec (v1)](#v1-vmmcpspec), [VMSelect (v1beta1)](#v1beta1-vmselect), [VMSingleSpec (v1beta1)](#v1beta1-vmsinglespec), [VMStorage (v1beta1)](#v1beta1-vmstorage), [VTAgentSpec (v1)](#v1-vtagentspec), [VTInsert (v1)](#v1-vtinsert), [VTSelect (v1)](#v1-vtselect), [VTSingleSpec (v1)](#v1-vtsinglespec), [VTStorage (v1)](#v1-vtstorage) | Field | Description | | --- | --- | @@ -2516,7 +2697,7 @@ Appears in: [StorageSpec (v1beta1)](#v1beta1-storagespec) #### EmbeddedPodDisruptionBudgetSpec {#v1beta1-embeddedpoddisruptionbudgetspec} -Appears in: [VLAgentSpec (v1)](#v1-vlagentspec), [VLDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vldistributedzoneagentspec), [VLInsert (v1)](#v1-vlinsert), [VLSelect (v1)](#v1-vlselect), [VLStorage (v1)](#v1-vlstorage), [VMAgentSpec (v1beta1)](#v1beta1-vmagentspec), [VMAlertSpec (v1beta1)](#v1beta1-vmalertspec), [VMAlertmanagerSpec (v1beta1)](#v1beta1-vmalertmanagerspec), [VMAnomalySpec (v1)](#v1-vmanomalyspec), [VMAuthLoadBalancerSpec (v1beta1)](#v1beta1-vmauthloadbalancerspec), [VMAuthSpec (v1beta1)](#v1beta1-vmauthspec), [VMDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vmdistributedzoneagentspec), [VMInsert (v1beta1)](#v1beta1-vminsert), [VMSelect (v1beta1)](#v1beta1-vmselect), [VMStorage (v1beta1)](#v1beta1-vmstorage), [VTAgentSpec (v1)](#v1-vtagentspec), [VTInsert (v1)](#v1-vtinsert), [VTSelect (v1)](#v1-vtselect), [VTStorage (v1)](#v1-vtstorage) +Appears in: [CommonMCPSpec (v1)](#v1-commonmcpspec), [VLAgentSpec (v1)](#v1-vlagentspec), [VLDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vldistributedzoneagentspec), [VLInsert (v1)](#v1-vlinsert), [VLSelect (v1)](#v1-vlselect), [VLStorage (v1)](#v1-vlstorage), [VMAgentSpec (v1beta1)](#v1beta1-vmagentspec), [VMAlertSpec (v1beta1)](#v1beta1-vmalertspec), [VMAlertmanagerSpec (v1beta1)](#v1beta1-vmalertmanagerspec), [VMAnomalySpec (v1)](#v1-vmanomalyspec), [VMAuthLoadBalancerSpec (v1beta1)](#v1beta1-vmauthloadbalancerspec), [VMAuthSpec (v1beta1)](#v1beta1-vmauthspec), [VMDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vmdistributedzoneagentspec), [VMInsert (v1beta1)](#v1beta1-vminsert), [VMMCPSpec (v1)](#v1-vmmcpspec), [VMSelect (v1beta1)](#v1beta1-vmselect), [VMStorage (v1beta1)](#v1beta1-vmstorage), [VTAgentSpec (v1)](#v1-vtagentspec), [VTInsert (v1)](#v1-vtinsert), [VTSelect (v1)](#v1-vtselect), [VTStorage (v1)](#v1-vtstorage) | Field | Description | | --- | --- | @@ -2531,7 +2712,7 @@ Appears in: [VLAgentSpec (v1)](#v1-vlagentspec), [VLDistributedZoneAgentSpec (v1 EmbeddedVPA embeds VerticalPodAutoscaler spec v1. https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler -Appears in: [VLAgentSpec (v1)](#v1-vlagentspec), [VLDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vldistributedzoneagentspec), [VLInsert (v1)](#v1-vlinsert), [VLSelect (v1)](#v1-vlselect), [VLSingleSpec (v1)](#v1-vlsinglespec), [VLStorage (v1)](#v1-vlstorage), [VMAgentSpec (v1beta1)](#v1beta1-vmagentspec), [VMAlertSpec (v1beta1)](#v1beta1-vmalertspec), [VMAlertmanagerSpec (v1beta1)](#v1beta1-vmalertmanagerspec), [VMAnomalySpec (v1)](#v1-vmanomalyspec), [VMAuthSpec (v1beta1)](#v1beta1-vmauthspec), [VMInsert (v1beta1)](#v1beta1-vminsert), [VMSelect (v1beta1)](#v1beta1-vmselect), [VMSingleSpec (v1beta1)](#v1beta1-vmsinglespec), [VMStorage (v1beta1)](#v1beta1-vmstorage), [VTAgentSpec (v1)](#v1-vtagentspec), [VTInsert (v1)](#v1-vtinsert), [VTSelect (v1)](#v1-vtselect), [VTSingleSpec (v1)](#v1-vtsinglespec), [VTStorage (v1)](#v1-vtstorage) +Appears in: [CommonMCPSpec (v1)](#v1-commonmcpspec), [VLAgentSpec (v1)](#v1-vlagentspec), [VLDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vldistributedzoneagentspec), [VLInsert (v1)](#v1-vlinsert), [VLSelect (v1)](#v1-vlselect), [VLSingleSpec (v1)](#v1-vlsinglespec), [VLStorage (v1)](#v1-vlstorage), [VMAgentSpec (v1beta1)](#v1beta1-vmagentspec), [VMAlertSpec (v1beta1)](#v1beta1-vmalertspec), [VMAlertmanagerSpec (v1beta1)](#v1beta1-vmalertmanagerspec), [VMAnomalySpec (v1)](#v1-vmanomalyspec), [VMAuthSpec (v1beta1)](#v1beta1-vmauthspec), [VMInsert (v1beta1)](#v1beta1-vminsert), [VMMCPSpec (v1)](#v1-vmmcpspec), [VMSelect (v1beta1)](#v1beta1-vmselect), [VMSingleSpec (v1beta1)](#v1beta1-vmsinglespec), [VMStorage (v1beta1)](#v1beta1-vmstorage), [VTAgentSpec (v1)](#v1-vtagentspec), [VTInsert (v1)](#v1-vtinsert), [VTSelect (v1)](#v1-vtselect), [VTSingleSpec (v1)](#v1-vtsinglespec), [VTStorage (v1)](#v1-vtstorage) | Field | Description | | --- | --- | @@ -2810,7 +2991,7 @@ Appears in: [VMScrapeConfigSpec (v1beta1)](#v1beta1-vmscrapeconfigspec) Image defines docker image settings -Appears in: [CommonAppsParams (v1beta1)](#v1beta1-commonappsparams), [VLAgentSpec (v1)](#v1-vlagentspec), [VLDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vldistributedzoneagentspec), [VLInsert (v1)](#v1-vlinsert), [VLSelect (v1)](#v1-vlselect), [VLSingleSpec (v1)](#v1-vlsinglespec), [VLStorage (v1)](#v1-vlstorage), [VLogsSpec (v1beta1)](#v1beta1-vlogsspec), [VMAgentSpec (v1beta1)](#v1beta1-vmagentspec), [VMAlertSpec (v1beta1)](#v1beta1-vmalertspec), [VMAlertmanagerSpec (v1beta1)](#v1beta1-vmalertmanagerspec), [VMAnomalySpec (v1)](#v1-vmanomalyspec), [VMAuthLoadBalancerSpec (v1beta1)](#v1beta1-vmauthloadbalancerspec), [VMAuthSpec (v1beta1)](#v1beta1-vmauthspec), [VMBackup (v1beta1)](#v1beta1-vmbackup), [VMDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vmdistributedzoneagentspec), [VMInsert (v1beta1)](#v1beta1-vminsert), [VMSelect (v1beta1)](#v1beta1-vmselect), [VMSingleSpec (v1beta1)](#v1beta1-vmsinglespec), [VMStorage (v1beta1)](#v1beta1-vmstorage), [VTAgentSpec (v1)](#v1-vtagentspec), [VTInsert (v1)](#v1-vtinsert), [VTSelect (v1)](#v1-vtselect), [VTSingleSpec (v1)](#v1-vtsinglespec), [VTStorage (v1)](#v1-vtstorage) +Appears in: [CommonAppsParams (v1beta1)](#v1beta1-commonappsparams), [CommonMCPSpec (v1)](#v1-commonmcpspec), [VLAgentSpec (v1)](#v1-vlagentspec), [VLDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vldistributedzoneagentspec), [VLInsert (v1)](#v1-vlinsert), [VLSelect (v1)](#v1-vlselect), [VLSingleSpec (v1)](#v1-vlsinglespec), [VLStorage (v1)](#v1-vlstorage), [VLogsSpec (v1beta1)](#v1beta1-vlogsspec), [VMAgentSpec (v1beta1)](#v1beta1-vmagentspec), [VMAlertSpec (v1beta1)](#v1beta1-vmalertspec), [VMAlertmanagerSpec (v1beta1)](#v1beta1-vmalertmanagerspec), [VMAnomalySpec (v1)](#v1-vmanomalyspec), [VMAuthLoadBalancerSpec (v1beta1)](#v1beta1-vmauthloadbalancerspec), [VMAuthSpec (v1beta1)](#v1beta1-vmauthspec), [VMBackup (v1beta1)](#v1beta1-vmbackup), [VMDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vmdistributedzoneagentspec), [VMInsert (v1beta1)](#v1beta1-vminsert), [VMMCPSpec (v1)](#v1-vmmcpspec), [VMSelect (v1beta1)](#v1beta1-vmselect), [VMSingleSpec (v1beta1)](#v1beta1-vmsinglespec), [VMStorage (v1beta1)](#v1beta1-vmstorage), [VTAgentSpec (v1)](#v1-vtagentspec), [VTInsert (v1)](#v1-vtinsert), [VTSelect (v1)](#v1-vtselect), [VTSingleSpec (v1)](#v1-vtsinglespec), [VTStorage (v1)](#v1-vtstorage) | Field | Description | | --- | --- | @@ -3030,7 +3211,7 @@ Appears in: [Receiver (v1beta1)](#v1beta1-receiver) ManagedObjectsMetadata contains Labels and Annotations -Appears in: [VLAgentSpec (v1)](#v1-vlagentspec), [VLClusterSpec (v1)](#v1-vlclusterspec), [VLDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vldistributedzoneagentspec), [VLSingleSpec (v1)](#v1-vlsinglespec), [VLogsSpec (v1beta1)](#v1beta1-vlogsspec), [VMAgentSpec (v1beta1)](#v1beta1-vmagentspec), [VMAlertSpec (v1beta1)](#v1beta1-vmalertspec), [VMAlertmanagerSpec (v1beta1)](#v1beta1-vmalertmanagerspec), [VMAnomalySpec (v1)](#v1-vmanomalyspec), [VMAuthSpec (v1beta1)](#v1beta1-vmauthspec), [VMClusterSpec (v1beta1)](#v1beta1-vmclusterspec), [VMDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vmdistributedzoneagentspec), [VMSingleSpec (v1beta1)](#v1beta1-vmsinglespec), [VMUserSpec (v1beta1)](#v1beta1-vmuserspec), [VTAgentSpec (v1)](#v1-vtagentspec), [VTClusterSpec (v1)](#v1-vtclusterspec), [VTSingleSpec (v1)](#v1-vtsinglespec) +Appears in: [CommonMCPSpec (v1)](#v1-commonmcpspec), [VLAgentSpec (v1)](#v1-vlagentspec), [VLClusterSpec (v1)](#v1-vlclusterspec), [VLDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vldistributedzoneagentspec), [VLSingleSpec (v1)](#v1-vlsinglespec), [VLogsSpec (v1beta1)](#v1beta1-vlogsspec), [VMAgentSpec (v1beta1)](#v1beta1-vmagentspec), [VMAlertSpec (v1beta1)](#v1beta1-vmalertspec), [VMAlertmanagerSpec (v1beta1)](#v1beta1-vmalertmanagerspec), [VMAnomalySpec (v1)](#v1-vmanomalyspec), [VMAuthSpec (v1beta1)](#v1beta1-vmauthspec), [VMClusterSpec (v1beta1)](#v1beta1-vmclusterspec), [VMDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vmdistributedzoneagentspec), [VMMCPSpec (v1)](#v1-vmmcpspec), [VMSingleSpec (v1beta1)](#v1beta1-vmsinglespec), [VMUserSpec (v1beta1)](#v1beta1-vmuserspec), [VTAgentSpec (v1)](#v1-vtagentspec), [VTClusterSpec (v1)](#v1-vtclusterspec), [VTSingleSpec (v1)](#v1-vtsinglespec) | Field | Description | | --- | --- | @@ -3712,7 +3893,7 @@ Appears in: [OAuth2 (v1beta1)](#v1beta1-oauth2), [TLSConfig (v1beta1)](#v1beta1- SecurityContext extends PodSecurityContext with ContainerSecurityContext It allows to globally configure security params for pod and all containers -Appears in: [CommonAppsParams (v1beta1)](#v1beta1-commonappsparams), [VLAgentSpec (v1)](#v1-vlagentspec), [VLDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vldistributedzoneagentspec), [VLInsert (v1)](#v1-vlinsert), [VLSelect (v1)](#v1-vlselect), [VLSingleSpec (v1)](#v1-vlsinglespec), [VLStorage (v1)](#v1-vlstorage), [VLogsSpec (v1beta1)](#v1beta1-vlogsspec), [VMAgentSpec (v1beta1)](#v1beta1-vmagentspec), [VMAlertSpec (v1beta1)](#v1beta1-vmalertspec), [VMAlertmanagerSpec (v1beta1)](#v1beta1-vmalertmanagerspec), [VMAnomalySpec (v1)](#v1-vmanomalyspec), [VMAuthLoadBalancerSpec (v1beta1)](#v1beta1-vmauthloadbalancerspec), [VMAuthSpec (v1beta1)](#v1beta1-vmauthspec), [VMDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vmdistributedzoneagentspec), [VMInsert (v1beta1)](#v1beta1-vminsert), [VMSelect (v1beta1)](#v1beta1-vmselect), [VMSingleSpec (v1beta1)](#v1beta1-vmsinglespec), [VMStorage (v1beta1)](#v1beta1-vmstorage), [VTAgentSpec (v1)](#v1-vtagentspec), [VTInsert (v1)](#v1-vtinsert), [VTSelect (v1)](#v1-vtselect), [VTSingleSpec (v1)](#v1-vtsinglespec), [VTStorage (v1)](#v1-vtstorage) +Appears in: [CommonAppsParams (v1beta1)](#v1beta1-commonappsparams), [CommonMCPSpec (v1)](#v1-commonmcpspec), [VLAgentSpec (v1)](#v1-vlagentspec), [VLDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vldistributedzoneagentspec), [VLInsert (v1)](#v1-vlinsert), [VLSelect (v1)](#v1-vlselect), [VLSingleSpec (v1)](#v1-vlsinglespec), [VLStorage (v1)](#v1-vlstorage), [VLogsSpec (v1beta1)](#v1beta1-vlogsspec), [VMAgentSpec (v1beta1)](#v1beta1-vmagentspec), [VMAlertSpec (v1beta1)](#v1beta1-vmalertspec), [VMAlertmanagerSpec (v1beta1)](#v1beta1-vmalertmanagerspec), [VMAnomalySpec (v1)](#v1-vmanomalyspec), [VMAuthLoadBalancerSpec (v1beta1)](#v1beta1-vmauthloadbalancerspec), [VMAuthSpec (v1beta1)](#v1beta1-vmauthspec), [VMDistributedZoneAgentSpec (v1alpha1)](#v1alpha1-vmdistributedzoneagentspec), [VMInsert (v1beta1)](#v1beta1-vminsert), [VMMCPSpec (v1)](#v1-vmmcpspec), [VMSelect (v1beta1)](#v1beta1-vmselect), [VMSingleSpec (v1beta1)](#v1beta1-vmsinglespec), [VMStorage (v1beta1)](#v1beta1-vmstorage), [VTAgentSpec (v1)](#v1-vtagentspec), [VTInsert (v1)](#v1-vtinsert), [VTSelect (v1)](#v1-vtselect), [VTSingleSpec (v1)](#v1-vtsinglespec), [VTStorage (v1)](#v1-vtstorage) #### Sigv4Config {#v1beta1-sigv4config} @@ -5548,7 +5729,7 @@ result config will scrape service endpoints VMServiceScrapeSpec defines the desired state of VMServiceScrape -Appears in: [VLAgentSpec (v1)](#v1-vlagentspec), [VLInsert (v1)](#v1-vlinsert), [VLSelect (v1)](#v1-vlselect), [VLSingleSpec (v1)](#v1-vlsinglespec), [VLStorage (v1)](#v1-vlstorage), [VLogsSpec (v1beta1)](#v1beta1-vlogsspec), [VMAgentSpec (v1beta1)](#v1beta1-vmagentspec), [VMAlertSpec (v1beta1)](#v1beta1-vmalertspec), [VMAlertmanagerSpec (v1beta1)](#v1beta1-vmalertmanagerspec), [VMAnomalySpec (v1)](#v1-vmanomalyspec), [VMAuthLoadBalancerSpec (v1beta1)](#v1beta1-vmauthloadbalancerspec), [VMAuthSpec (v1beta1)](#v1beta1-vmauthspec), [VMInsert (v1beta1)](#v1beta1-vminsert), [VMSelect (v1beta1)](#v1beta1-vmselect), [VMServiceScrape (v1beta1)](#v1beta1-vmservicescrape), [VMSingleSpec (v1beta1)](#v1beta1-vmsinglespec), [VMStorage (v1beta1)](#v1beta1-vmstorage), [VTAgentSpec (v1)](#v1-vtagentspec), [VTInsert (v1)](#v1-vtinsert), [VTSelect (v1)](#v1-vtselect), [VTSingleSpec (v1)](#v1-vtsinglespec), [VTStorage (v1)](#v1-vtstorage) +Appears in: [CommonMCPSpec (v1)](#v1-commonmcpspec), [VLAgentSpec (v1)](#v1-vlagentspec), [VLInsert (v1)](#v1-vlinsert), [VLSelect (v1)](#v1-vlselect), [VLSingleSpec (v1)](#v1-vlsinglespec), [VLStorage (v1)](#v1-vlstorage), [VLogsSpec (v1beta1)](#v1beta1-vlogsspec), [VMAgentSpec (v1beta1)](#v1beta1-vmagentspec), [VMAlertSpec (v1beta1)](#v1beta1-vmalertspec), [VMAlertmanagerSpec (v1beta1)](#v1beta1-vmalertmanagerspec), [VMAnomalySpec (v1)](#v1-vmanomalyspec), [VMAuthLoadBalancerSpec (v1beta1)](#v1beta1-vmauthloadbalancerspec), [VMAuthSpec (v1beta1)](#v1beta1-vmauthspec), [VMInsert (v1beta1)](#v1beta1-vminsert), [VMMCPSpec (v1)](#v1-vmmcpspec), [VMSelect (v1beta1)](#v1beta1-vmselect), [VMServiceScrape (v1beta1)](#v1beta1-vmservicescrape), [VMSingleSpec (v1beta1)](#v1beta1-vmsinglespec), [VMStorage (v1beta1)](#v1beta1-vmstorage), [VTAgentSpec (v1)](#v1-vtagentspec), [VTInsert (v1)](#v1-vtinsert), [VTSelect (v1)](#v1-vtselect), [VTSingleSpec (v1)](#v1-vtsinglespec), [VTStorage (v1)](#v1-vtstorage) | Field | Description | | --- | --- | diff --git a/docs/configuration.md b/docs/configuration.md index a24b0cb62a..92eb4e6cc3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -579,6 +579,7 @@ VPA configuration is available for: - `VMCluster` components through `spec.vmselect.vpa`, `spec.vminsert.vpa` and `spec.vmstorage.vpa`. - `VLCluster` components through `spec.vlselect.vpa`, `spec.vlinsert.vpa` and `spec.vlstorage.vpa`. - `VTCluster` components through `spec.select.vpa`, `spec.insert.vpa` and `spec.storage.vpa`. +- `VMMCP` through `spec.vpa`. - `VMDistributed` components that expose `vpa` in their component spec. ### Configuration diff --git a/docs/env.md b/docs/env.md index 19e13026a3..da905b5ce3 100644 --- a/docs/env.md +++ b/docs/env.md @@ -4,6 +4,7 @@ | VM_LOGS_VERSION: `v1.53.0` #
Defines default image version for VictoriaLogs components: VLogs, VLAgent, VLSingle, VLCluster (vlselect/vlinsert/vlstorage). Used as the image tag when no explicit version is set in the CR spec. | | VM_ANOMALY_VERSION: `v1.31.1` #
Defines default image version for VMAnomaly. Used as the image tag when no explicit version is set in the CR spec. | | VM_TRACES_VERSION: `v0.12.0` #
Defines default image version for VictoriaTraces components: VTSingle, VTCluster (vtselect/vtinsert/vtstorage). Used as the image tag when no explicit version is set in the CR spec. | +| VM_MCP_METRICS_VERSION: `v1.20.2` #
Defines default image version for VMMCP (MCP server for VictoriaMetrics). Used as the image tag when no explicit version is set in the CR spec. | | VM_OPERATOR_VERSION: `v0.75.1` #
Defines the operator's own version. Used for config-reloader image tag interpolation. | | VM_GATEWAY_API_ENABLED: `false` #
Enables support for Kubernetes Gateway API. When enabled, operator manages HTTPRoute resources for VMAuth ingress configuration. | | VM_VPA_API_ENABLED: `false` #
Enables support for VerticalPodAutoscaler API. When enabled, operator can create and manage VPA objects for VM components. | @@ -113,6 +114,17 @@ | VM_VMANOMALYDEFAULT_RESOURCE_REQUEST_CPU: `50m` # | | VM_VMANOMALYDEFAULT_RESOURCE_REQUEST_EPHEMERAL_STORAGE: `unlimited` # | | VM_VMANOMALYDEFAULT_TERMINATION_GRACE_PERIOD_SECONDS: `30` # | +| VM_VMMCPDEFAULT_IMAGE: `ghcr.io/victoriametrics/mcp-victoriametrics` #
Default container image for VMMCP. | +| VM_VMMCPDEFAULT_VERSION: `${VM_MCP_METRICS_VERSION}` #
Default image version. Inherits VM_MCP_METRICS_VERSION if not set explicitly. | +| VM_VMMCPDEFAULT_PORT: `8080` #
Default HTTP listen port. | +| VM_VMMCPDEFAULT_USEDEFAULTRESOURCES: `true` #
Whether to apply default resource requests and limits. | +| VM_VMMCPDEFAULT_RESOURCE_LIMIT_MEM: `1024Mi` # | +| VM_VMMCPDEFAULT_RESOURCE_LIMIT_CPU: `500m` # | +| VM_VMMCPDEFAULT_RESOURCE_LIMIT_EPHEMERAL_STORAGE: `unlimited` # | +| VM_VMMCPDEFAULT_RESOURCE_REQUEST_MEM: `768Mi` # | +| VM_VMMCPDEFAULT_RESOURCE_REQUEST_CPU: `100m` # | +| VM_VMMCPDEFAULT_RESOURCE_REQUEST_EPHEMERAL_STORAGE: `unlimited` # | +| VM_VMMCPDEFAULT_TERMINATION_GRACE_PERIOD_SECONDS: `30` # | | VM_VMSINGLEDEFAULT_IMAGE: `victoriametrics/victoria-metrics` #
Default container image for VMSingle. | | VM_VMSINGLEDEFAULT_VERSION: `${VM_METRICS_VERSION}` #
Default image version. Inherits VM_METRICS_VERSION if not set explicitly. | | VM_VMSINGLEDEFAULT_PORT: `8429` #
Default HTTP listen port. | diff --git a/docs/resources/README.md b/docs/resources/README.md index 20acd18180..9efbfd368c 100644 --- a/docs/resources/README.md +++ b/docs/resources/README.md @@ -31,6 +31,7 @@ Metrics Operator introduces. - [VTSingle](https://docs.victoriametrics.com/operator/resources/vtsingle/) - [VTCluster](https://docs.victoriametrics.com/operator/resources/vtcluster/) - [VTAgent](https://docs.victoriametrics.com/operator/resources/vtagent/) +- [VMMCP](https://docs.victoriametrics.com/operator/resources/vmmcp/) Here is the scheme of relations between the custom resources: @@ -155,6 +156,7 @@ Every custom resource with deployable application has a fields for specifying ve - [Managing versions for VMAuth](https://docs.victoriametrics.com/operator/resources/vmauth/#version-management) - [Managing versions for VMCluster](https://docs.victoriametrics.com/operator/resources/vmcluster/#version-management) - [Managing versions for VMSingle](https://docs.victoriametrics.com/operator/resources/vmsingle/#version-management) +- [Managing versions for VMMCP](https://docs.victoriametrics.com/operator/resources/vmmcp/#version-management) ## Managing resources @@ -167,6 +169,7 @@ Every custom resource with deployable application has a fields and operator para - [Managing resources for VMAuth](https://docs.victoriametrics.com/operator/resources/vmauth/#resource-management) - [Managing resources for VMCluster](https://docs.victoriametrics.com/operator/resources/vmcluster/#resource-management) - [Managing resources for VMSingle](https://docs.victoriametrics.com/operator/resources/vmsingle/#resource-management) +- [Managing resources for VMMCP](https://docs.victoriametrics.com/operator/resources/vmmcp/#resource-management) ## High availability @@ -178,6 +181,7 @@ VictoriaMetrics operator support high availability for each component of the mon - [VMAlertmanager](https://docs.victoriametrics.com/operator/resources/vmalertmanager/#high-availability) - [VMAuth](https://docs.victoriametrics.com/operator/resources/vmauth/#high-availability) - [VMCluster](https://docs.victoriametrics.com/operator/resources/vmcluster/#high-availability) +- [VMMCP](https://docs.victoriametrics.com/operator/resources/vmmcp/#high-availability) In addition, these CRD support common features, that can be used to increase high availability - resources above have the following fields: diff --git a/docs/resources/vmmcp.md b/docs/resources/vmmcp.md new file mode 100644 index 0000000000..dfda47bc4b --- /dev/null +++ b/docs/resources/vmmcp.md @@ -0,0 +1,294 @@ +--- +weight: 25 +title: VMMCP +menu: + docs: + identifier: operator-cr-vmmcp + parent: operator-cr + weight: 25 +aliases: + - /operator/resources/vmmcp/ + - /operator/resources/vmmcp/index.html +tags: + - kubernetes + - metrics +--- +`VMMCP` represents [MCP server for VictoriaMetrics](https://github.com/VictoriaMetrics/mcp-victoriametrics). +The MCP server implements [Model Context Protocol](https://modelcontextprotocol.io/), which lets AI assistants and agents +query VictoriaMetrics, explore metrics, labels and alerting rules, analyze cardinality +and search in VictoriaMetrics documentation. + +For each `VMMCP` resource, the Operator deploys a `Deployment` with the MCP server in the same namespace +and adds `Service`, `ServiceAccount` and `VMServiceScrape` prefixed with `vmmcp-` and the name from `VMMCP.metadata.name`. +With `spec.serviceAccountName` the MCP server uses an existing `ServiceAccount` instead, +and `spec.disableSelfServiceScrape: true` disables the `VMServiceScrape`. + +## Specification + +You can see the full actual specification of the `VMMCP` resource in the **[API docs -> VMMCP](https://docs.victoriametrics.com/operator/api/#v1-vmmcp)**. + +The MCP server is configured with environment variables, so `spec.extraArgs` isn't supported. +If you can't find necessary field in the specification of the custom resource, +set [MCP server environment variables](https://github.com/VictoriaMetrics/mcp-victoriametrics#configuration) at `spec.extraEnvs`. +They override environment variables with the same names set by the Operator. + +Also, you can check out the [examples](https://docs.victoriametrics.com/operator/resources/vmmcp/#examples) section. + +## Connecting to VictoriaMetrics + +`spec.entrypoint` defines the root URL of VictoriaMetrics instance, which is queried by the MCP server, +and `spec.instanceType` defines its type: `single` (default) or `cluster`. + +For [VMSingle](https://docs.victoriametrics.com/operator/resources/vmsingle/) set the URL of its service: + +```yaml +apiVersion: operator.victoriametrics.com/v1 +kind: VMMCP +metadata: + name: example +spec: + entrypoint: http://vmsingle-example.default.svc:8428 +``` + +For [VMCluster](https://docs.victoriametrics.com/operator/resources/vmcluster/) set the URL of `vmselect` service and `cluster` instance type. +`spec.defaultTenantID` defines the tenant for requests, which don't specify it explicitly: + +```yaml +apiVersion: operator.victoriametrics.com/v1 +kind: VMMCP +metadata: + name: example +spec: + entrypoint: http://vmselect-example.default.svc:8481 + instanceType: cluster + defaultTenantID: "0" +``` + +If VictoriaMetrics requires authentication, for example when it's accessed via [VMAuth](https://docs.victoriametrics.com/operator/resources/vmauth/), +set a `Secret` key with a token at `spec.bearerToken` or custom headers in `key=value` format at `spec.headers`: + +```yaml +apiVersion: operator.victoriametrics.com/v1 +kind: VMMCP +metadata: + name: example +spec: + entrypoint: http://vmauth-example.default.svc:8427 + bearerToken: + name: vmmcp-token + key: token + headers: + - X-Scope=team-a +``` + +`spec.passthroughHeaders` defines names of headers, which are forwarded from MCP client requests to VictoriaMetrics. +It allows serving several VictoriaMetrics instances by one MCP server +with [routing by header in VMAuth](https://docs.victoriametrics.com/victoriametrics/vmauth/#routing-by-header). + +To work with [VictoriaMetrics Cloud](https://docs.victoriametrics.com/victoriametrics-cloud/) deployments, +set a `Secret` key with [API key](https://docs.victoriametrics.com/victoriametrics-cloud/api/) at `spec.cloudAPIKey` instead of `spec.entrypoint`: + +```yaml +apiVersion: operator.victoriametrics.com/v1 +kind: VMMCP +metadata: + name: cloud +spec: + cloudAPIKey: + name: vmcloud + key: api-key +``` + +## Connecting MCP clients + +By default, the MCP server uses Streamable HTTP transport, which is served at `/mcp` path of `vmmcp-` service on port `8080`. +Set `spec.serverMode: sse` for clients, which support only Server-Sent Events transport, it's served at `/sse` and `/message` paths. + +For example, to connect [Claude Code](https://docs.anthropic.com/en/docs/claude-code/mcp) from your machine, +forward the service port and add the MCP server: + +```sh +kubectl port-forward svc/vmmcp-example 8080:8080 +claude mcp add --transport http victoriametrics http://localhost:8080/mcp +``` + +See [MCP server docs](https://github.com/VictoriaMetrics/mcp-victoriametrics#setup-in-clients) for other clients. + +## Security + +The MCP server doesn't authenticate its clients, so anyone with network access to it can query VictoriaMetrics +with credentials of the MCP server. Don't expose the MCP server outside of the cluster as is. +Put [VMAuth](https://docs.victoriametrics.com/operator/resources/vmauth/) with +[VMUser](https://docs.victoriametrics.com/operator/resources/vmuser/) in front of it +and restrict access to its pods with `spec.networkPolicy`, as in the [VMMCP for VMCluster](https://docs.victoriametrics.com/operator/resources/vmmcp/#vmmcp-for-vmcluster) example: + +```yaml +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMAuth +metadata: + name: mcp +spec: + userSelector: + matchLabels: + app: mcp +--- +apiVersion: operator.victoriametrics.com/v1beta1 +kind: VMUser +metadata: + name: mcp + labels: + app: mcp +spec: + username: mcp + passwordRef: + name: vmmcp-users + key: password + targetRefs: + - static: + url: http://vmmcp-example.default.svc:8080 + paths: + - /mcp +``` + +## Tools and resources + +The MCP server disables some tools by default, see the [list of tools](https://github.com/VictoriaMetrics/mcp-victoriametrics#toolset). +`spec.disabledTools` replaces the default list of disabled tools. +To enable all the tools, set `MCP_DISABLED_TOOLS` environment variable to an empty value at `spec.extraEnvs`. + +The `documentation` tool searches in VictoriaMetrics documentation, which is built into the MCP server. +The MCP server loads the documentation index into memory at start: it takes about 700MiB of memory +and the start takes from 10 seconds with 1 CPU to more than a minute with 0.2 CPU. +Default resources and startup probe of `VMMCP` account for it. +Add `documentation` to `spec.disabledTools` to disable the tool, then the MCP server takes tens of MiB of memory and starts in a second. + +`spec.disableResources: true` disables MCP resources with documentation, while the `documentation` tool keeps working. + +## High availability + +The MCP server is stateless, so `VMMCP` can run several replicas with `spec.replicaCount` +and spread them across nodes with `spec.topologySpreadConstraints`, `spec.affinity` and `spec.podDisruptionBudget`. + +In `http` server mode, tool calls of a session can be served by any replica. +In `sse` server mode, MCP client messages must be served by the replica, which holds the client SSE connection, +so run a single replica or set `sessionAffinity: ClientIP` for the `vmmcp-` service. +`spec.serviceSpec` changes this service only with `useAsDefault: true`, otherwise it adds another service: + +```yaml +apiVersion: operator.victoriametrics.com/v1 +kind: VMMCP +metadata: + name: example +spec: + entrypoint: http://vmsingle-example.default.svc:8428 + serverMode: sse + replicaCount: 2 + serviceSpec: + useAsDefault: true + spec: + sessionAffinity: ClientIP +``` + +## Version management + +To set `VMMCP` version add `spec.image.tag` name from [releases](https://github.com/VictoriaMetrics/mcp-victoriametrics/releases): + +```yaml +apiVersion: operator.victoriametrics.com/v1 +kind: VMMCP +metadata: + name: example +spec: + image: + repository: ghcr.io/victoriametrics/mcp-victoriametrics + tag: v1.20.2 + pullPolicy: Always + # ... +``` + +The default version can be changed with `VM_MCP_METRICS_VERSION` operator parameter. +The MCP server image is published only to `ghcr.io`, so `VM_CONTAINERREGISTRY` operator parameter doesn't affect it. +Set `VM_VMMCPDEFAULT_IMAGE` operator parameter to use a mirror. + +Also, you can specify `imagePullSecrets` if you are pulling images from private repo: + +```yaml +apiVersion: operator.victoriametrics.com/v1 +kind: VMMCP +metadata: + name: example +spec: + imagePullSecrets: + - name: my-repo-secret + # ... +``` + +## Resource management + +You can specify resources for each `VMMCP` resource in the `spec` section of the `VMMCP` CRD. + +```yaml +apiVersion: operator.victoriametrics.com/v1 +kind: VMMCP +metadata: + name: resources-example +spec: + # ... + resources: + requests: + memory: "768Mi" + cpu: "100m" + limits: + memory: "1Gi" + cpu: "500m" + # ... +``` + +If these parameters are not specified, then, +by default all `VMMCP` pods have resource requests and limits from the default values of the following [operator parameters](https://docs.victoriametrics.com/operator/configuration/): + +- `VM_VMMCPDEFAULT_RESOURCE_LIMIT_MEM` - default memory limit for `VMMCP` pods, +- `VM_VMMCPDEFAULT_RESOURCE_LIMIT_CPU` - default CPU limit for `VMMCP` pods, +- `VM_VMMCPDEFAULT_RESOURCE_REQUEST_MEM` - default memory request for `VMMCP` pods, +- `VM_VMMCPDEFAULT_RESOURCE_REQUEST_CPU` - default CPU request for `VMMCP` pods. + +These default parameters will be used if: + +- `VM_VMMCPDEFAULT_USEDEFAULTRESOURCES` is set to `true` (default value), +- `VMMCP` CR doesn't have `resources` field in `spec` section. + +Field `resources` in `VMMCP` spec have higher priority than operator parameters. + +If you set `VM_VMMCPDEFAULT_USEDEFAULTRESOURCES` to `false` and don't specify `resources` in `VMMCP` CRD, +then `VMMCP` pods will be created without resource requests and limits. + +Also, you can specify requests without limits - in this case default values for limits will not be used. + +## Examples + +### VMMCP for VMCluster + +```yaml +apiVersion: operator.victoriametrics.com/v1 +kind: VMMCP +metadata: + name: example +spec: + entrypoint: http://vmselect-example.default.svc:8481 + instanceType: cluster + disabledTools: + - export + - flags + - test_rules + logFormat: json + # allows connections only from vmauth and scrapes of MCP server metrics by vmagent + networkPolicy: + ingress: + - from: + - podSelector: + matchLabels: + app.kubernetes.io/name: vmauth + - podSelector: + matchLabels: + app.kubernetes.io/name: vmagent +``` diff --git a/internal/config/config.go b/internal/config/config.go index 041c91a2a5..0462df39f5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -49,6 +49,8 @@ var ( "VM_ANOMALY_VERSION": "v1.31.1", // renovate: datasource=github-releases depName=VictoriaMetrics/VictoriaTraces "VM_TRACES_VERSION": "v0.12.0", + // renovate: datasource=github-releases depName=VictoriaMetrics/mcp-victoriametrics + "VM_MCP_METRICS_VERSION": "v1.20.2", // renovate: datasource=github-releases depName=VictoriaMetrics/operator "VM_OPERATOR_VERSION": getVersion("v0.75.1"), // renovate: datasource=github-releases depName=prometheus/alertmanager @@ -125,6 +127,9 @@ type BaseOperatorConf struct { // VTSingle, VTCluster (vtselect/vtinsert/vtstorage). // Used as the image tag when no explicit version is set in the CR spec. TracesVersion string `default:"${VM_TRACES_VERSION}" env:"VM_TRACES_VERSION,expand"` + // Defines default image version for VMMCP (MCP server for VictoriaMetrics). + // Used as the image tag when no explicit version is set in the CR spec. + MCPMetricsVersion string `default:"${VM_MCP_METRICS_VERSION}" env:"VM_MCP_METRICS_VERSION,expand"` // Defines the operator's own version. Used for config-reloader image tag interpolation. OperatorVersion string `default:"${VM_OPERATOR_VERSION}" env:"VM_OPERATOR_VERSION,expand"` // Enables support for Kubernetes Gateway API. @@ -396,6 +401,33 @@ type BaseOperatorConf struct { TerminationGracePeriodSeconds int64 `default:"30"` } `prefix:"VM_VMANOMALYDEFAULT_"` + // VMMCP defines default settings for VMMCP (MCP server for VictoriaMetrics) deployments. + VMMCP struct { + // Default container image for VMMCP. + Image string `default:"ghcr.io/victoriametrics/mcp-victoriametrics"` + // Default image version. Inherits VM_MCP_METRICS_VERSION if not set explicitly. + Version string `env:",expand" default:"${VM_MCP_METRICS_VERSION}"` + // Default HTTP listen port. + Port string `default:"8080"` + // Whether to apply default resource requests and limits. + UseDefaultResources bool `default:"true" env:"USEDEFAULTRESOURCES"` + // Default memory accounts for the in-memory index of VictoriaMetrics documentation, + // which is loaded unless the documentation tool is disabled. + Resource struct { + Limit struct { + Mem string `default:"1024Mi"` + Cpu string `default:"500m"` + EphemeralStorage string `default:"unlimited"` + } `prefix:"LIMIT_"` + Request struct { + Mem string `default:"768Mi"` + Cpu string `default:"100m"` + EphemeralStorage string `default:"unlimited"` + } `prefix:"REQUEST_"` + } `prefix:"RESOURCE_"` + TerminationGracePeriodSeconds int64 `default:"30"` + } `prefix:"VM_VMMCPDEFAULT_"` + // VMSingle defines default settings for VMSingle (single-node VictoriaMetrics) deployments. VMSingle struct { // Default container image for VMSingle. @@ -923,6 +955,9 @@ func (boc BaseOperatorConf) validate() error { if err := validateResource("vmanomaly", Resource(boc.VMAnomaly.Resource)); err != nil { return err } + if err := validateResource("vmmcp", Resource(boc.VMMCP.Resource)); err != nil { + return err + } if err := validateResource("vlsingle", Resource(boc.VLSingle.Resource)); err != nil { return err } diff --git a/internal/controller/operator/factory/build/defaults.go b/internal/controller/operator/factory/build/defaults.go index cabb0ec2a6..aac119e404 100644 --- a/internal/controller/operator/factory/build/defaults.go +++ b/internal/controller/operator/factory/build/defaults.go @@ -51,6 +51,7 @@ func AddDefaults(scheme *runtime.Scheme) { scheme.AddTypeDefaultingFunc(&vmv1.VTAgent{}, addVTAgentDefaults) scheme.AddTypeDefaultingFunc(&vmv1.VTCluster{}, addVTClusterDefaults) scheme.AddTypeDefaultingFunc(&vmv1.VMAnomaly{}, addVMAnomalyDefaults) + scheme.AddTypeDefaultingFunc(&vmv1.VMMCP{}, addVMMCPDefaults) scheme.AddTypeDefaultingFunc(&vmv1beta1.VMServiceScrape{}, addVMServiceScrapeDefaults) scheme.AddTypeDefaultingFunc(&vmv1alpha1.VMDistributed{}, addVMDistributedDefaults) scheme.AddTypeDefaultingFunc(&vmv1alpha1.VLDistributed{}, addVLDistributedDefaults) @@ -464,6 +465,37 @@ func addVMAnomalyDefaults(objI any) { } } +func addVMMCPDefaults(objI any) { + cr := objI.(*vmv1.VMMCP) + addDefaultMetadata(cr) + cv := config.ApplicationDefaults(getCfg().VMMCP) + addMCPDefaults(&cr.Spec.CommonMCPSpec, &cv) +} + +// addMCPDefaults adds defaults shared by CRDs of MCP servers +func addMCPDefaults(spec *vmv1.CommonMCPSpec, appDefaults *config.ApplicationDefaults) { + // the MCP server serves liveness checks at a separate path, + // while readiness path is used for all probes by default + if spec.LivenessProbe == nil { + spec.LivenessProbe = &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: vmv1.MCPLivenessPath, + }, + }, + } + } + // the MCP server starts listening only after loading the built-in documentation, + // which takes more than a minute with low CPU limits + if spec.StartupProbe == nil { + spec.StartupProbe = &corev1.Probe{ + FailureThreshold: 60, + } + } + cp := commonParams{tag: spec.ComponentVersion} + addDefaultsToCommonParams(&spec.CommonAppsParams, &cp, appDefaults) +} + func addVLSingleDefaults(objI any) { cr := objI.(*vmv1.VLSingle) c := getCfg() diff --git a/internal/controller/operator/factory/finalize/mcp.go b/internal/controller/operator/factory/finalize/mcp.go new file mode 100644 index 0000000000..b3a60f6e55 --- /dev/null +++ b/internal/controller/operator/factory/finalize/mcp.go @@ -0,0 +1,50 @@ +package finalize + +import ( + "context" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + policyv1 "k8s.io/api/policy/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + vpav1 "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + vmv1 "github.com/VictoriaMetrics/operator/api/operator/v1" + "github.com/VictoriaMetrics/operator/internal/config" +) + +// OnVMMCPDelete deletes all VMMCP related resources +func OnVMMCPDelete(ctx context.Context, rclient client.Client, cr *vmv1.VMMCP) error { + ns := cr.GetNamespace() + objMeta := metav1.ObjectMeta{ + Namespace: ns, + Name: cr.PrefixedName(), + } + objsToRemove := []client.Object{ + &appsv1.Deployment{ObjectMeta: objMeta}, + &corev1.Service{ObjectMeta: objMeta}, + &policyv1.PodDisruptionBudget{ObjectMeta: objMeta}, + &networkingv1.NetworkPolicy{ObjectMeta: objMeta}, + } + // a ServiceAccount set in spec.serviceAccountName isn't managed by the operator + if cr.IsOwnsServiceAccount() { + objsToRemove = append(objsToRemove, &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{ + Name: cr.GetServiceAccountName(), + Namespace: ns, + }}) + } + if cr.Spec.ServiceSpec != nil { + objsToRemove = append(objsToRemove, &corev1.Service{ObjectMeta: metav1.ObjectMeta{ + Name: cr.Spec.ServiceSpec.NameOrDefault(cr.PrefixedName()), + Namespace: ns, + }}) + } + if config.MustGetBaseConfig().VPAAPIEnabled { + objsToRemove = append(objsToRemove, &vpav1.VerticalPodAutoscaler{ObjectMeta: objMeta}) + } + objsToRemove = append(objsToRemove, cr) + deleteOwnerReferences := make([]bool, len(objsToRemove)) + return removeFinalizers(ctx, rclient, objsToRemove, deleteOwnerReferences, cr) +} diff --git a/internal/controller/operator/factory/finalize/mcp_test.go b/internal/controller/operator/factory/finalize/mcp_test.go new file mode 100644 index 0000000000..296dce7d57 --- /dev/null +++ b/internal/controller/operator/factory/finalize/mcp_test.go @@ -0,0 +1,92 @@ +package finalize + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + vmv1 "github.com/VictoriaMetrics/operator/api/operator/v1" + vmv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/k8stools" +) + +func TestOnVMMCPDelete(t *testing.T) { + ctx := context.TODO() + cr := &vmv1.VMMCP{ + TypeMeta: metav1.TypeMeta{APIVersion: "operator.victoriametrics.com/v1", Kind: "VMMCP"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Finalizers: []string{vmv1beta1.FinalizerName}, + OwnerReferences: []metav1.OwnerReference{ + {APIVersion: "v1", Kind: "ConfigMap", Name: "owner", UID: "owner-uid"}, + }, + }, + } + cr.Spec.ServiceSpec = &vmv1beta1.AdditionalServiceSpec{ + EmbeddedObjectMetadata: vmv1beta1.EmbeddedObjectMetadata{Name: "custom-service"}, + } + objMeta := func(name string) metav1.ObjectMeta { + return metav1.ObjectMeta{ + Name: name, + Namespace: "default", + Finalizers: []string{vmv1beta1.FinalizerName}, + OwnerReferences: []metav1.OwnerReference{cr.AsOwner()}, + } + } + objs := []client.Object{ + &appsv1.Deployment{ObjectMeta: objMeta("vmmcp-test")}, + &corev1.Service{ObjectMeta: objMeta("vmmcp-test")}, + &corev1.Service{ObjectMeta: objMeta("custom-service")}, + &corev1.ServiceAccount{ObjectMeta: objMeta("vmmcp-test")}, + &policyv1.PodDisruptionBudget{ObjectMeta: objMeta("vmmcp-test")}, + cr, + } + predefined := make([]runtime.Object, 0, len(objs)) + ownerRefs := make([][]metav1.OwnerReference, 0, len(objs)) + for _, o := range objs { + predefined = append(predefined, o.DeepCopyObject()) + ownerRefs = append(ownerRefs, o.GetOwnerReferences()) + } + cl := k8stools.GetTestClientWithObjects(predefined) + require.NoError(t, OnVMMCPDelete(ctx, cl, cr)) + + for i, o := range objs { + require.NoError(t, cl.Get(ctx, types.NamespacedName{Namespace: o.GetNamespace(), Name: o.GetName()}, o)) + assert.Empty(t, o.GetFinalizers(), "finalizers of %T %s must be removed", o, o.GetName()) + assert.Equal(t, ownerRefs[i], o.GetOwnerReferences(), "owner references of %T %s must be retained", o, o.GetName()) + } +} + +func TestOnVMMCPDeleteExternalServiceAccount(t *testing.T) { + ctx := context.TODO() + cr := &vmv1.VMMCP{ + TypeMeta: metav1.TypeMeta{APIVersion: "operator.victoriametrics.com/v1", Kind: "VMMCP"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + Finalizers: []string{vmv1beta1.FinalizerName}, + }, + } + cr.Spec.ServiceAccountName = "external" + // the VMMCP doesn't own the ServiceAccount set in spec.serviceAccountName, e.g. one shared with another VMMCP + sa := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{ + Name: "external", + Namespace: "default", + Finalizers: []string{vmv1beta1.FinalizerName}, + }} + cl := k8stools.GetTestClientWithObjects([]runtime.Object{cr.DeepCopy(), sa.DeepCopy()}) + require.NoError(t, OnVMMCPDelete(ctx, cl, cr)) + + require.NoError(t, cl.Get(ctx, types.NamespacedName{Namespace: "default", Name: "external"}, sa)) + assert.Equal(t, []string{vmv1beta1.FinalizerName}, sa.GetFinalizers(), "finalizers of a ServiceAccount, which the VMMCP doesn't own, must be kept") +} diff --git a/internal/controller/operator/factory/k8stools/test_helpers.go b/internal/controller/operator/factory/k8stools/test_helpers.go index 4d7d2cb284..34aca6d91f 100644 --- a/internal/controller/operator/factory/k8stools/test_helpers.go +++ b/internal/controller/operator/factory/k8stools/test_helpers.go @@ -77,6 +77,7 @@ func testGetScheme() *runtime.Scheme { &vmv1.VMAnomalyConfigList{}, &vmv1.VLAgentList{}, &vmv1.VTAgentList{}, + &vmv1.VMMCPList{}, &vmv1.VLSingle{}, &vmv1.VLCluster{}, &vmv1.VTSingle{}, @@ -85,6 +86,7 @@ func testGetScheme() *runtime.Scheme { &vmv1.VMAnomalyConfig{}, &vmv1.VLAgent{}, &vmv1.VTAgent{}, + &vmv1.VMMCP{}, ) s.AddKnownTypes(schema.GroupVersion(gwapiv1.GroupVersion), &gwapiv1.HTTPRouteList{}, @@ -141,6 +143,7 @@ func getTestClient(predefinedObjects []runtime.Object, fns *interceptor.Funcs) c &vmv1.VMAnomaly{}, &vmv1.VMAnomalyConfig{}, &vmv1.VLAgent{}, + &vmv1.VMMCP{}, &gwapiv1.HTTPRoute{}, &vpav1.VerticalPodAutoscaler{}, ). diff --git a/internal/controller/operator/factory/mcp/mcp.go b/internal/controller/operator/factory/mcp/mcp.go new file mode 100644 index 0000000000..c5387a7de4 --- /dev/null +++ b/internal/controller/operator/factory/mcp/mcp.go @@ -0,0 +1,420 @@ +package mcp + +import ( + "context" + "fmt" + "path" + "sort" + "strings" + + appsv1 "k8s.io/api/apps/v1" + autoscalingv1 "k8s.io/api/autoscaling/v1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + policyv1 "k8s.io/api/policy/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apimachinery/pkg/util/sets" + vpav1 "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + vmv1 "github.com/VictoriaMetrics/operator/api/operator/v1" + vmv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1" + "github.com/VictoriaMetrics/operator/internal/config" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/build" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/finalize" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/k8stools" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/reconcile" +) + +// mcpCR is implemented by CRDs of MCP servers +type mcpCR interface { + client.Object + PrefixedName() string + SelectorLabels() map[string]string + PodLabels() map[string]string + PodAnnotations() map[string]string + FinalLabels() map[string]string + FinalAnnotations() map[string]string + AsOwner() metav1.OwnerReference + GetServiceAccountName() string + IsOwnsServiceAccount() bool + GetAdditionalService() *vmv1beta1.AdditionalServiceSpec + GetServiceScrape() *vmv1beta1.VMServiceScrapeSpec + GetExtraArgs() map[string]string + GetMetricsPath() string + UseTLS() bool + ProbePath() string + ProbeScheme() string + ProbePort() string + ProbeNeedLiveness() bool + UseProxyProtocol() bool +} + +// server defines MCP server with settings specific to its CRD +type server struct { + cr mcpCR + spec *vmv1.CommonMCPSpec + // kind is used as container name and in error messages + kind string + // envs contains env vars, which configure access to the queried instance and MCP features specific to the server kind + envs []corev1.EnvVar +} + +func createOrUpdate(ctx context.Context, rclient client.Client, s, prevS *server) error { + cfg := config.MustGetBaseConfig() + if s.spec.VPA != nil && !cfg.VPAAPIEnabled { + return fmt.Errorf("spec.vpa is set but VM_VPA_API_ENABLED=true env var was not provided") + } + if prevS != nil { + if err := deleteOrphaned(ctx, rclient, s); err != nil { + return err + } + } + owner := s.cr.AsOwner() + if s.cr.IsOwnsServiceAccount() { + var prevSA *corev1.ServiceAccount + if prevS != nil { + prevSA = build.ServiceAccount(prevS.cr) + } + if err := reconcile.ServiceAccount(ctx, rclient, build.ServiceAccount(s.cr), prevSA, &owner); err != nil { + return fmt.Errorf("failed create service account: %w", err) + } + } + if err := createOrUpdateService(ctx, rclient, s, prevS); err != nil { + return err + } + if s.spec.PodDisruptionBudget != nil { + var prevPDB *policyv1.PodDisruptionBudget + if prevS != nil && prevS.spec.PodDisruptionBudget != nil { + prevPDB = build.PodDisruptionBudget(prevS.cr, prevS.spec.PodDisruptionBudget) + } + if err := reconcile.PDB(ctx, rclient, build.PodDisruptionBudget(s.cr, s.spec.PodDisruptionBudget), prevPDB, &owner); err != nil { + return fmt.Errorf("cannot update pod disruption budget for %s: %w", s.kind, err) + } + } + if err := createOrUpdateVPA(ctx, rclient, s, prevS); err != nil { + return fmt.Errorf("cannot create or update vpa for %s: %w", s.kind, err) + } + if s.spec.NetworkPolicy != nil { + var prevNP *networkingv1.NetworkPolicy + if prevS != nil && prevS.spec.NetworkPolicy != nil { + prevNP = build.NetworkPolicy(prevS.cr, prevS.spec.NetworkPolicy) + } + if err := reconcile.NetworkPolicy(ctx, rclient, build.NetworkPolicy(s.cr, s.spec.NetworkPolicy), prevNP, &owner); err != nil { + return fmt.Errorf("cannot update network policy for %s: %w", s.kind, err) + } + } + + var prevDeploy *appsv1.Deployment + if prevS != nil { + var err error + prevDeploy, err = newDeployment(prevS) + if err != nil { + return fmt.Errorf("cannot generate prev deploy spec: %w", err) + } + } + newDeploy, err := newDeployment(s) + if err != nil { + return fmt.Errorf("cannot generate new deploy for %s: %w", s.kind, err) + } + return reconcile.Deployment(ctx, rclient, newDeploy, prevDeploy, &owner, nil) +} + +func createOrUpdateVPA(ctx context.Context, rclient client.Client, s, prevS *server) error { + if s.spec.VPA == nil { + return nil + } + targetRef := autoscalingv1.CrossVersionObjectReference{ + Name: s.cr.PrefixedName(), + Kind: string(vmv1beta1.WorkloadKindDeployment), + APIVersion: "apps/v1", + } + newVPA := build.VPA(s.cr, targetRef, s.spec.VPA) + var prevVPA *vpav1.VerticalPodAutoscaler + if prevS != nil && prevS.spec.VPA != nil { + prevVPA = build.VPA(prevS.cr, targetRef, prevS.spec.VPA) + } + owner := s.cr.AsOwner() + return reconcile.VPA(ctx, rclient, newVPA, prevVPA, &owner) +} + +func newDeployment(s *server) (*appsv1.Deployment, error) { + podSpec, err := newPodSpec(s) + if err != nil { + return nil, err + } + strategy := appsv1.DeploymentStrategy{ + Type: appsv1.RollingUpdateDeploymentStrategyType, + } + if s.spec.UpdateStrategy != nil { + strategy.Type = *s.spec.UpdateStrategy + } + if strategy.Type == appsv1.RollingUpdateDeploymentStrategyType { + strategy.RollingUpdate = s.spec.RollingUpdate + } + depSpec := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: s.cr.PrefixedName(), + Namespace: s.cr.GetNamespace(), + Labels: s.cr.FinalLabels(), + Annotations: s.cr.FinalAnnotations(), + OwnerReferences: []metav1.OwnerReference{s.cr.AsOwner()}, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: s.cr.SelectorLabels(), + }, + Strategy: strategy, + Template: *podSpec, + }, + } + build.DeploymentAddCommonParams(depSpec, &s.spec.CommonAppsParams) + return depSpec, nil +} + +func newPodSpec(s *server) (*corev1.PodTemplateSpec, error) { + spec := s.spec + var volumes []corev1.Volume + var vmMounts []corev1.VolumeMount + volumes = append(volumes, spec.Volumes...) + vmMounts = append(vmMounts, spec.VolumeMounts...) + + for _, sn := range spec.Secrets { + volumes = append(volumes, corev1.Volume{ + Name: k8stools.SanitizeVolumeName("secret-" + sn), + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: sn, + }, + }, + }) + vmMounts = append(vmMounts, corev1.VolumeMount{ + Name: k8stools.SanitizeVolumeName("secret-" + sn), + ReadOnly: true, + MountPath: path.Join(vmv1beta1.SecretsDir, sn), + }) + } + + for _, c := range spec.ConfigMaps { + volumes = append(volumes, corev1.Volume{ + Name: k8stools.SanitizeVolumeName("configmap-" + c), + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: c, + }, + }, + }, + }) + vmMounts = append(vmMounts, corev1.VolumeMount{ + Name: k8stools.SanitizeVolumeName("configmap-" + c), + ReadOnly: true, + MountPath: path.Join(vmv1beta1.ConfigMapsDir, c), + }) + } + + container := corev1.Container{ + Name: s.kind, + Image: spec.Image.Reference(), + Ports: []corev1.ContainerPort{ + { + Name: "http", + Protocol: corev1.ProtocolTCP, + ContainerPort: intstr.Parse(spec.Port).IntVal, + }, + }, + VolumeMounts: vmMounts, + Resources: spec.Resources, + Env: buildEnvs(s), + EnvFrom: spec.ExtraEnvsFrom, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + ImagePullPolicy: spec.Image.PullPolicy, + } + build.Probe(&container, s.cr, &spec.CommonAppsParams) + build.Lifecycle(&container, &spec.CommonAppsParams) + + operatorContainers := []corev1.Container{container} + build.AddStrictSecuritySettingsToContainers(operatorContainers, &spec.CommonAppsParams) + containers, err := k8stools.MergePatchContainers(operatorContainers, spec.Containers) + if err != nil { + return nil, fmt.Errorf("cannot patch containers: %w", err) + } + + for i := range spec.TopologySpreadConstraints { + if spec.TopologySpreadConstraints[i].LabelSelector == nil { + spec.TopologySpreadConstraints[i].LabelSelector = &metav1.LabelSelector{ + MatchLabels: s.cr.SelectorLabels(), + } + } + } + + return &corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: s.cr.PodLabels(), + Annotations: s.cr.PodAnnotations(), + }, + Spec: corev1.PodSpec{ + Volumes: volumes, + InitContainers: spec.InitContainers, + Containers: containers, + ServiceAccountName: s.cr.GetServiceAccountName(), + }, + }, nil +} + +// buildEnvs returns env vars of the MCP server container. +// Env vars defined at spec.extraEnvs override env vars with the same names set by operator. +func buildEnvs(s *server) []corev1.EnvVar { + spec := s.spec + serverMode := spec.ServerMode + if serverMode == "" { + serverMode = vmv1.MCPServerModeHTTP + } + envs := []corev1.EnvVar{ + {Name: "MCP_LISTEN_ADDR", Value: fmt.Sprintf(":%s", spec.Port)}, + {Name: "MCP_SERVER_MODE", Value: string(serverMode)}, + } + if len(spec.DisabledTools) > 0 { + envs = append(envs, corev1.EnvVar{Name: "MCP_DISABLED_TOOLS", Value: strings.Join(spec.DisabledTools, ",")}) + } + if spec.HeartbeatInterval != "" { + envs = append(envs, corev1.EnvVar{Name: "MCP_HEARTBEAT_INTERVAL", Value: spec.HeartbeatInterval}) + } + if spec.LogLevel != "" { + envs = append(envs, corev1.EnvVar{Name: "MCP_LOG_LEVEL", Value: spec.LogLevel}) + } + if spec.LogFormat != "" { + envs = append(envs, corev1.EnvVar{Name: "MCP_LOG_FORMAT", Value: spec.LogFormat}) + } + envs = append(envs, s.envs...) + + overridden := sets.New[string]() + for _, e := range spec.ExtraEnvs { + overridden.Insert(e.Name) + } + envs = filterEnvs(envs, func(e corev1.EnvVar) bool { + return !overridden.Has(e.Name) + }) + sort.Slice(envs, func(i, j int) bool { + return envs[i].Name < envs[j].Name + }) + return append(envs, spec.ExtraEnvs...) +} + +func filterEnvs(envs []corev1.EnvVar, keep func(corev1.EnvVar) bool) []corev1.EnvVar { + dst := envs[:0] + for _, e := range envs { + if keep(e) { + dst = append(dst, e) + } + } + return dst +} + +// instanceEnvs returns env vars, which configure access to the instance queried by the MCP server. +// The prefix depends on the queried instance, e.g. VM for VMMCP. +func instanceEnvs(prefix, entrypoint string, bearerToken *corev1.SecretKeySelector, headers []string, defaultTenantID string) []corev1.EnvVar { + var envs []corev1.EnvVar + if entrypoint != "" { + envs = append(envs, corev1.EnvVar{Name: prefix + "_INSTANCE_ENTRYPOINT", Value: entrypoint}) + } + if bearerToken != nil { + envs = append(envs, corev1.EnvVar{ + Name: prefix + "_INSTANCE_BEARER_TOKEN", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: bearerToken, + }, + }) + } + if len(headers) > 0 { + envs = append(envs, corev1.EnvVar{Name: prefix + "_INSTANCE_HEADERS", Value: strings.Join(headers, ",")}) + } + if defaultTenantID != "" { + envs = append(envs, corev1.EnvVar{Name: prefix + "_DEFAULT_TENANT_ID", Value: defaultTenantID}) + } + return envs +} + +func passthroughHeadersEnvs(headers []string) []corev1.EnvVar { + if len(headers) == 0 { + return nil + } + return []corev1.EnvVar{{Name: "MCP_PASSTHROUGH_HEADERS", Value: strings.Join(headers, ",")}} +} + +func buildScrape(s *server, svc *corev1.Service) *vmv1beta1.VMServiceScrape { + if s == nil || svc == nil || ptr.Deref(s.spec.DisableSelfServiceScrape, false) { + return nil + } + return build.VMServiceScrape(svc, s.cr) +} + +func createOrUpdateService(ctx context.Context, rclient client.Client, s, prevS *server) error { + owner := s.cr.AsOwner() + var prevSvc, prevAdditionalSvc *corev1.Service + if prevS != nil { + prevSvc = build.Service(prevS.cr, prevS.spec.Port, nil) + prevAdditionalSvc = build.AdditionalServiceFromDefault(prevSvc, prevS.spec.ServiceSpec) + } + svc := build.Service(s.cr, s.spec.Port, nil) + if err := s.spec.ServiceSpec.IsSomeAndThen(func(ss *vmv1beta1.AdditionalServiceSpec) error { + additionalService := build.AdditionalServiceFromDefault(svc, ss) + if additionalService.Name == svc.Name { + return fmt.Errorf("%s additional service name: %q cannot be the same as crd.prefixedname: %q", s.kind, additionalService.Name, svc.Name) + } + if err := reconcile.Service(ctx, rclient, additionalService, prevAdditionalSvc, &owner); err != nil { + return fmt.Errorf("cannot reconcile additional service for %s: %w", s.kind, err) + } + return nil + }); err != nil { + return err + } + if err := reconcile.Service(ctx, rclient, svc, prevSvc, &owner); err != nil { + return fmt.Errorf("cannot reconcile service for %s: %w", s.kind, err) + } + if !ptr.Deref(s.spec.DisableSelfServiceScrape, false) { + svs := buildScrape(s, svc) + prevSvs := buildScrape(prevS, prevSvc) + if err := reconcile.VMServiceScrape(ctx, rclient, svs, prevSvs, &owner, false); err != nil { + return fmt.Errorf("cannot create serviceScrape for %s: %w", s.kind, err) + } + } + return nil +} + +func deleteOrphaned(ctx context.Context, rclient client.Client, s *server) error { + svcName := s.cr.PrefixedName() + keepServices := sets.New(svcName) + keepServicesScrapes := sets.New[string]() + if !ptr.Deref(s.spec.DisableSelfServiceScrape, false) { + keepServicesScrapes.Insert(svcName) + } + if s.spec.ServiceSpec != nil && !s.spec.ServiceSpec.UseAsDefault { + extraSvcName := s.spec.ServiceSpec.NameOrDefault(svcName) + keepServices.Insert(extraSvcName) + } + if err := finalize.RemoveOrphanedServices(ctx, rclient, s.cr, keepServices, true); err != nil { + return fmt.Errorf("cannot remove services: %w", err) + } + if err := finalize.RemoveOrphanedVMServiceScrapes(ctx, rclient, s.cr, keepServicesScrapes, true); err != nil { + return fmt.Errorf("cannot remove serviceScrapes: %w", err) + } + + objMeta := metav1.ObjectMeta{Name: s.cr.PrefixedName(), Namespace: s.cr.GetNamespace()} + var objsToRemove []client.Object + if s.spec.PodDisruptionBudget == nil { + objsToRemove = append(objsToRemove, &policyv1.PodDisruptionBudget{ObjectMeta: objMeta}) + } + if config.MustGetBaseConfig().VPAAPIEnabled && s.spec.VPA == nil { + objsToRemove = append(objsToRemove, &vpav1.VerticalPodAutoscaler{ObjectMeta: objMeta}) + } + if s.spec.NetworkPolicy == nil { + objsToRemove = append(objsToRemove, &networkingv1.NetworkPolicy{ObjectMeta: objMeta}) + } + if !s.cr.IsOwnsServiceAccount() { + objsToRemove = append(objsToRemove, &corev1.ServiceAccount{ObjectMeta: objMeta}) + } + return finalize.SafeDeleteWithFinalizer(ctx, rclient, objsToRemove, s.cr) +} diff --git a/internal/controller/operator/factory/mcp/mcp_test.go b/internal/controller/operator/factory/mcp/mcp_test.go new file mode 100644 index 0000000000..72fe581e5e --- /dev/null +++ b/internal/controller/operator/factory/mcp/mcp_test.go @@ -0,0 +1,335 @@ +package mcp + +import ( + "context" + "testing" + "testing/synctest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + policyv1 "k8s.io/api/policy/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + vmv1 "github.com/VictoriaMetrics/operator/api/operator/v1" + vmv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1" + "github.com/VictoriaMetrics/operator/internal/config" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/build" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/k8stools" +) + +func secretKey(name, key string) *corev1.SecretKeySelector { + return &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: name}, + Key: key, + } +} + +// reconcileMCP applies defaults to the given MCP server CR and reconciles it with fake client +func reconcileMCP(t *testing.T, rclient client.Client, cr client.Object) error { + t.Helper() + rclient.Scheme().Default(cr) + ctx := context.TODO() + var err error + synctest.Test(t, func(t *testing.T) { + switch cr := cr.(type) { + case *vmv1.VMMCP: + err = CreateOrUpdateVMMCP(ctx, rclient, cr) + default: + t.Fatalf("unexpected object type %T", cr) + } + }) + return err +} + +func newTestClient() client.Client { + fclient := k8stools.GetTestClientWithObjects(nil) + build.AddDefaults(fclient.Scheme()) + return fclient +} + +func getDeployment(t *testing.T, rclient client.Client, name string) *appsv1.Deployment { + t.Helper() + var dep appsv1.Deployment + require.NoError(t, rclient.Get(context.TODO(), types.NamespacedName{Namespace: "default", Name: name}, &dep)) + return &dep +} + +func TestCreateOrUpdateVMMCP(t *testing.T) { + rclient := newTestClient() + cr := &vmv1.VMMCP{ + ObjectMeta: metav1.ObjectMeta{Name: "base", Namespace: "default"}, + Spec: vmv1.VMMCPSpec{ + Entrypoint: "http://vmsingle-example.default.svc:8428", + }, + } + require.NoError(t, reconcileMCP(t, rclient, cr)) + ctx := context.TODO() + nsn := types.NamespacedName{Namespace: "default", Name: "vmmcp-base"} + + dep := getDeployment(t, rclient, nsn.Name) + assert.Equal(t, map[string]string{ + "app.kubernetes.io/name": "vmmcp", + "app.kubernetes.io/instance": "base", + "app.kubernetes.io/component": "monitoring", + "managed-by": "vm-operator", + }, dep.Spec.Selector.MatchLabels) + assert.Equal(t, appsv1.RollingUpdateDeploymentStrategyType, dep.Spec.Strategy.Type) + assert.Equal(t, "vmmcp-base", dep.Spec.Template.Spec.ServiceAccountName) + require.Len(t, dep.Spec.Template.Spec.Containers, 1) + c := dep.Spec.Template.Spec.Containers[0] + assert.Equal(t, "vmmcp", c.Name) + assert.Equal(t, "ghcr.io/victoriametrics/mcp-victoriametrics:"+config.MustGetBaseConfig().VMMCP.Version, c.Image) + assert.Empty(t, c.Args) + assert.Equal(t, []corev1.ContainerPort{{Name: "http", Protocol: corev1.ProtocolTCP, ContainerPort: 8080}}, c.Ports) + assert.Equal(t, []corev1.EnvVar{ + {Name: "MCP_LISTEN_ADDR", Value: ":8080"}, + {Name: "MCP_SERVER_MODE", Value: "http"}, + {Name: "VM_INSTANCE_ENTRYPOINT", Value: "http://vmsingle-example.default.svc:8428"}, + {Name: "VM_INSTANCE_TYPE", Value: "single"}, + }, c.Env) + + // probes + require.NotNil(t, c.ReadinessProbe) + assert.Equal(t, "/health/readiness", c.ReadinessProbe.HTTPGet.Path) + assert.Equal(t, intstr.Parse("8080"), c.ReadinessProbe.HTTPGet.Port) + require.NotNil(t, c.LivenessProbe) + assert.Equal(t, "/health/liveness", c.LivenessProbe.HTTPGet.Path) + assert.Equal(t, intstr.Parse("8080"), c.LivenessProbe.HTTPGet.Port) + require.NotNil(t, c.StartupProbe) + require.NotNil(t, c.StartupProbe.TCPSocket) + assert.Equal(t, intstr.Parse("8080"), c.StartupProbe.TCPSocket.Port) + assert.Equal(t, int32(60), c.StartupProbe.FailureThreshold) + + // default resources account for the documentation index + memLimit, memRequest := c.Resources.Limits[corev1.ResourceMemory], c.Resources.Requests[corev1.ResourceMemory] + assert.True(t, resource.MustParse("1024Mi").Equal(memLimit), "memory limit: %s", memLimit.String()) + assert.True(t, resource.MustParse("768Mi").Equal(memRequest), "memory request: %s", memRequest.String()) + + var svc corev1.Service + require.NoError(t, rclient.Get(ctx, nsn, &svc)) + require.Len(t, svc.Spec.Ports, 1) + assert.Equal(t, int32(8080), svc.Spec.Ports[0].Port) + assert.Equal(t, "http", svc.Spec.Ports[0].Name) + + var sa corev1.ServiceAccount + require.NoError(t, rclient.Get(ctx, nsn, &sa)) + + var vmss vmv1beta1.VMServiceScrape + require.NoError(t, rclient.Get(ctx, nsn, &vmss)) + require.Len(t, vmss.Spec.Endpoints, 1) + assert.Equal(t, "/metrics", vmss.Spec.Endpoints[0].Path) + assert.Equal(t, "http", vmss.Spec.Endpoints[0].Port) + + var pdb policyv1.PodDisruptionBudget + assert.True(t, k8serrors.IsNotFound(rclient.Get(ctx, nsn, &pdb)), "pdb must not be created by default") +} + +func TestCreateOrUpdateVMMCP_Envs(t *testing.T) { + f := func(spec vmv1.VMMCPSpec, want []corev1.EnvVar) { + t.Helper() + rclient := newTestClient() + cr := &vmv1.VMMCP{ + ObjectMeta: metav1.ObjectMeta{Name: "envs", Namespace: "default"}, + Spec: spec, + } + require.NoError(t, reconcileMCP(t, rclient, cr)) + dep := getDeployment(t, rclient, "vmmcp-envs") + assert.Equal(t, want, dep.Spec.Template.Spec.Containers[0].Env) + } + + // all MCP server params + f(vmv1.VMMCPSpec{ + Entrypoint: "http://vmselect-example.default.svc:8481", + InstanceType: vmv1.VMMCPInstanceTypeCluster, + BearerToken: secretKey("vm-auth", "token"), + Headers: []string{"X-Scope=team-a", "X-Env=prod"}, + DefaultTenantID: "1:2", + PassthroughHeaders: []string{"X-Token", "X-Access-Key"}, + DisableResources: true, + CommonMCPSpec: vmv1.CommonMCPSpec{ + ServerMode: vmv1.MCPServerModeSSE, + DisabledTools: []string{"export", "flags"}, + HeartbeatInterval: "1m", + LogLevel: "debug", + LogFormat: "json", + CommonAppsParams: vmv1beta1.CommonAppsParams{ + Port: "8090", + }, + }, + }, []corev1.EnvVar{ + {Name: "MCP_DISABLED_TOOLS", Value: "export,flags"}, + {Name: "MCP_DISABLE_RESOURCES", Value: "true"}, + {Name: "MCP_HEARTBEAT_INTERVAL", Value: "1m"}, + {Name: "MCP_LISTEN_ADDR", Value: ":8090"}, + {Name: "MCP_LOG_FORMAT", Value: "json"}, + {Name: "MCP_LOG_LEVEL", Value: "debug"}, + {Name: "MCP_PASSTHROUGH_HEADERS", Value: "X-Token,X-Access-Key"}, + {Name: "MCP_SERVER_MODE", Value: "sse"}, + {Name: "VM_DEFAULT_TENANT_ID", Value: "1:2"}, + {Name: "VM_INSTANCE_BEARER_TOKEN", ValueFrom: &corev1.EnvVarSource{SecretKeyRef: secretKey("vm-auth", "token")}}, + {Name: "VM_INSTANCE_ENTRYPOINT", Value: "http://vmselect-example.default.svc:8481"}, + {Name: "VM_INSTANCE_HEADERS", Value: "X-Scope=team-a,X-Env=prod"}, + {Name: "VM_INSTANCE_TYPE", Value: "cluster"}, + }) + + // VictoriaMetrics Cloud + f(vmv1.VMMCPSpec{ + CloudAPIKey: secretKey("vmc", "api-key"), + }, []corev1.EnvVar{ + {Name: "MCP_LISTEN_ADDR", Value: ":8080"}, + {Name: "MCP_SERVER_MODE", Value: "http"}, + {Name: "VMC_API_KEY", ValueFrom: &corev1.EnvVarSource{SecretKeyRef: secretKey("vmc", "api-key")}}, + }) + + // extra envs override env vars set by operator and keep their order + f(vmv1.VMMCPSpec{ + Entrypoint: "http://vmsingle-example.default.svc:8428", + CommonMCPSpec: vmv1.CommonMCPSpec{ + LogLevel: "info", + CommonAppsParams: vmv1beta1.CommonAppsParams{ + ExtraEnvs: []corev1.EnvVar{ + {Name: "MCP_LOG_LEVEL", Value: "debug"}, + {Name: "MCP_DISABLED_TOOLS", Value: ""}, + }, + }, + }, + }, []corev1.EnvVar{ + {Name: "MCP_LISTEN_ADDR", Value: ":8080"}, + {Name: "MCP_SERVER_MODE", Value: "http"}, + {Name: "VM_INSTANCE_ENTRYPOINT", Value: "http://vmsingle-example.default.svc:8428"}, + {Name: "VM_INSTANCE_TYPE", Value: "single"}, + {Name: "MCP_LOG_LEVEL", Value: "debug"}, + {Name: "MCP_DISABLED_TOOLS", Value: ""}, + }) +} + +func TestCreateOrUpdate_Deployment(t *testing.T) { + rclient := newTestClient() + cr := &vmv1.VMMCP{ + ObjectMeta: metav1.ObjectMeta{Name: "deploy", Namespace: "default"}, + Spec: vmv1.VMMCPSpec{ + Entrypoint: "http://vmsingle-example.default.svc:8428", + CommonMCPSpec: vmv1.CommonMCPSpec{ + UpdateStrategy: ptr.To(appsv1.RecreateDeploymentStrategyType), + // must be ignored for Recreate strategy, since it's rejected by Kubernetes + RollingUpdate: &appsv1.RollingUpdateDeployment{MaxSurge: ptr.To(intstr.FromInt32(2))}, + PodMetadata: &vmv1beta1.EmbeddedObjectMetadata{ + Labels: map[string]string{"team": "a"}, + Annotations: map[string]string{"note": "mcp"}, + }, + ServiceAccountName: "custom-sa", + CommonAppsParams: vmv1beta1.CommonAppsParams{ + ReplicaCount: ptr.To[int32](2), + TopologySpreadConstraints: []corev1.TopologySpreadConstraint{{ + MaxSkew: 1, + TopologyKey: "kubernetes.io/hostname", + WhenUnsatisfiable: corev1.ScheduleAnyway, + }}, + Secrets: []string{"tls"}, + ConfigMaps: []string{"extra"}, + }, + }, + }, + } + require.NoError(t, reconcileMCP(t, rclient, cr)) + dep := getDeployment(t, rclient, "vmmcp-deploy") + assert.Equal(t, appsv1.RecreateDeploymentStrategyType, dep.Spec.Strategy.Type) + assert.Nil(t, dep.Spec.Strategy.RollingUpdate) + assert.Equal(t, ptr.To[int32](2), dep.Spec.Replicas) + pod := dep.Spec.Template + assert.Equal(t, "a", pod.Labels["team"]) + assert.Equal(t, "mcp", pod.Annotations["note"]) + assert.Equal(t, "custom-sa", pod.Spec.ServiceAccountName) + require.Len(t, pod.Spec.TopologySpreadConstraints, 1) + assert.Equal(t, cr.SelectorLabels(), pod.Spec.TopologySpreadConstraints[0].LabelSelector.MatchLabels) + assert.Equal(t, []corev1.VolumeMount{ + {Name: "secret-tls", ReadOnly: true, MountPath: "/etc/vm/secrets/tls"}, + {Name: "configmap-extra", ReadOnly: true, MountPath: "/etc/vm/configs/extra"}, + }, pod.Spec.Containers[0].VolumeMounts) + + // the service account isn't created for the explicitly set serviceAccountName + var sa corev1.ServiceAccount + err := rclient.Get(context.TODO(), types.NamespacedName{Namespace: "default", Name: "vmmcp-deploy"}, &sa) + assert.True(t, k8serrors.IsNotFound(err)) +} + +func TestCreateOrUpdate_RemoveOptionalObjects(t *testing.T) { + rclient := newTestClient() + ctx := context.TODO() + nsn := types.NamespacedName{Namespace: "default", Name: "vmmcp-optional"} + cr := &vmv1.VMMCP{ + ObjectMeta: metav1.ObjectMeta{Name: "optional", Namespace: "default"}, + Spec: vmv1.VMMCPSpec{ + Entrypoint: "http://vmsingle-example.default.svc:8428", + CommonMCPSpec: vmv1.CommonMCPSpec{ + PodDisruptionBudget: &vmv1beta1.EmbeddedPodDisruptionBudgetSpec{ + MinAvailable: ptr.To(intstr.FromInt32(1)), + }, + ServiceSpec: &vmv1beta1.AdditionalServiceSpec{ + Spec: corev1.ServiceSpec{Type: corev1.ServiceTypeNodePort}, + }, + NetworkPolicy: &vmv1beta1.EmbeddedNetworkPolicy{}, + }, + }, + } + require.NoError(t, reconcileMCP(t, rclient, cr)) + var pdb policyv1.PodDisruptionBudget + require.NoError(t, rclient.Get(ctx, nsn, &pdb)) + assert.Equal(t, ptr.To(intstr.FromInt32(1)), pdb.Spec.MinAvailable) + var svc corev1.Service + require.NoError(t, rclient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "vmmcp-optional-additional-service"}, &svc)) + assert.Equal(t, corev1.ServiceTypeNodePort, svc.Spec.Type) + var np networkingv1.NetworkPolicy + require.NoError(t, rclient.Get(ctx, nsn, &np)) + + // remove optional objects from spec + cr.Status.LastAppliedSpec = cr.Spec.DeepCopy() + cr.Spec.PodDisruptionBudget = nil + cr.Spec.ServiceSpec = nil + cr.Spec.NetworkPolicy = nil + require.NoError(t, reconcileMCP(t, rclient, cr)) + assert.True(t, k8serrors.IsNotFound(rclient.Get(ctx, nsn, &pdb)), "pdb must be removed") + err := rclient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "vmmcp-optional-additional-service"}, &svc) + assert.True(t, k8serrors.IsNotFound(err), "additional service must be removed") + assert.True(t, k8serrors.IsNotFound(rclient.Get(ctx, nsn, &np)), "network policy must be removed") +} + +func TestCreateOrUpdate_Errors(t *testing.T) { + // vpa requires VPA API to be enabled + rclient := newTestClient() + cr := &vmv1.VMMCP{ + ObjectMeta: metav1.ObjectMeta{Name: "vpa", Namespace: "default"}, + Spec: vmv1.VMMCPSpec{ + Entrypoint: "http://vmsingle-example.default.svc:8428", + CommonMCPSpec: vmv1.CommonMCPSpec{ + VPA: &vmv1beta1.EmbeddedVPA{}, + }, + }, + } + assert.ErrorContains(t, reconcileMCP(t, rclient, cr), "VM_VPA_API_ENABLED=true") + + // paused CR isn't reconciled + rclient = newTestClient() + paused := &vmv1.VMMCP{ + ObjectMeta: metav1.ObjectMeta{Name: "paused", Namespace: "default"}, + Spec: vmv1.VMMCPSpec{ + Entrypoint: "http://vmsingle-example.default.svc:8428", + CommonMCPSpec: vmv1.CommonMCPSpec{ + CommonAppsParams: vmv1beta1.CommonAppsParams{Paused: true}, + }, + }, + } + require.NoError(t, reconcileMCP(t, rclient, paused)) + var dep appsv1.Deployment + err := rclient.Get(context.TODO(), types.NamespacedName{Namespace: "default", Name: "vmmcp-paused"}, &dep) + assert.True(t, k8serrors.IsNotFound(err)) +} diff --git a/internal/controller/operator/factory/mcp/vmmcp.go b/internal/controller/operator/factory/mcp/vmmcp.go new file mode 100644 index 0000000000..eff1e70c92 --- /dev/null +++ b/internal/controller/operator/factory/mcp/vmmcp.go @@ -0,0 +1,55 @@ +package mcp + +import ( + "context" + "strconv" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + vmv1 "github.com/VictoriaMetrics/operator/api/operator/v1" +) + +// CreateOrUpdateVMMCP performs an update for VMMCP resource +func CreateOrUpdateVMMCP(ctx context.Context, rclient client.Client, cr *vmv1.VMMCP) error { + if cr.Paused() { + return nil + } + var prevS *server + if cr.Status.LastAppliedSpec != nil { + prevCR := cr.DeepCopy() + prevCR.Spec = *cr.Status.LastAppliedSpec + prevS = newVMMCPServer(prevCR) + } + return createOrUpdate(ctx, rclient, newVMMCPServer(cr), prevS) +} + +func newVMMCPServer(cr *vmv1.VMMCP) *server { + spec := &cr.Spec + var envs []corev1.EnvVar + if spec.CloudAPIKey != nil { + envs = append(envs, corev1.EnvVar{ + Name: "VMC_API_KEY", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: spec.CloudAPIKey, + }, + }) + } else { + instanceType := spec.InstanceType + if instanceType == "" { + instanceType = vmv1.VMMCPInstanceTypeSingle + } + envs = append(envs, corev1.EnvVar{Name: "VM_INSTANCE_TYPE", Value: string(instanceType)}) + } + envs = append(envs, instanceEnvs("VM", spec.Entrypoint, spec.BearerToken, spec.Headers, spec.DefaultTenantID)...) + envs = append(envs, passthroughHeadersEnvs(spec.PassthroughHeaders)...) + if spec.DisableResources { + envs = append(envs, corev1.EnvVar{Name: "MCP_DISABLE_RESOURCES", Value: strconv.FormatBool(spec.DisableResources)}) + } + return &server{ + cr: cr, + spec: &spec.CommonMCPSpec, + kind: "vmmcp", + envs: envs, + } +} diff --git a/internal/controller/operator/objects_stat.go b/internal/controller/operator/objects_stat.go index f64be1e19b..3f6d0d9bdc 100644 --- a/internal/controller/operator/objects_stat.go +++ b/internal/controller/operator/objects_stat.go @@ -105,6 +105,7 @@ func newCollector() *objectCollector { "vmnodescrape", "vmpodscrape", "vmprobe", "vmscrapeconfig", "vmanomaly", "vlagent", "vtsingle", "vtcluster", "vmdistributed", "podmonitor", "prometheusrule", "servicemonitor", "alertmanagerconfig", "probe", "scrapeconfig", "vmanomalyconfig", "vldistributed", + "vmmcp", } for _, controller := range registeredObjects { oc.objectsByController[controller] = sets.New[string]() diff --git a/internal/controller/operator/vmmcp_controller.go b/internal/controller/operator/vmmcp_controller.go new file mode 100644 index 0000000000..02189d829b --- /dev/null +++ b/internal/controller/operator/vmmcp_controller.go @@ -0,0 +1,131 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package operator + +import ( + "context" + "fmt" + "strings" + + "github.com/go-logr/logr" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + policyv1 "k8s.io/api/policy/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" + vpav1 "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + vmv1 "github.com/VictoriaMetrics/operator/api/operator/v1" + vmv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1" + "github.com/VictoriaMetrics/operator/internal/config" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/finalize" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/logger" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/mcp" +) + +// VMMCPReconciler reconciles a VMMCP object +type VMMCPReconciler struct { + client.Client + Log logr.Logger + OriginScheme *runtime.Scheme + BaseConf *config.BaseOperatorConf + name string +} + +// Init implements crdController interface +func (r *VMMCPReconciler) Init(name string, rclient client.Client, l logr.Logger, sc *runtime.Scheme, cf *config.BaseOperatorConf) { + r.name = strings.ToLower(name) + r.Client = rclient + r.Log = l.WithName("controller." + name) + r.OriginScheme = sc + r.BaseConf = cf +} + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// +kubebuilder:rbac:groups=operator.victoriametrics.com,resources=vmmcps,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=operator.victoriametrics.com,resources=vmmcps/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=operator.victoriametrics.com,resources=vmmcps/finalizers,verbs=update +// +kubebuilder:rbac:groups="networking.k8s.io",resources=networkpolicies,verbs=get;list;watch;create;update;patch;delete + +func (r *VMMCPReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) { + l := r.Log.WithValues(r.name, req.Name, "namespace", req.Namespace) + ctx = logger.AddToContext(ctx, l) + var instance vmv1.VMMCP + + defer func() { + result, err = handleReconcileErrWithStatus(ctx, r.Client, &instance, result, err) + }() + + instance.Name, instance.Namespace = req.Name, req.Namespace + if err = r.Get(ctx, req.NamespacedName, &instance); err != nil { + err = newGetError(err) + return + } + + RegisterObjectStat(&instance, r.name) + if !instance.DeletionTimestamp.IsZero() { + err = finalize.OnVMMCPDelete(ctx, r.Client, &instance) + return + } + if instance.Status.ParsingSpecError != "" && !vmv1beta1.HasUnknownFields(instance.Status.ParsingSpecError) { + err = newParsingError(instance.Status.ParsingSpecError) + return + } + if err = finalize.AddFinalizer(ctx, r.Client, &instance); err != nil { + return + } + r.Client.Scheme().Default(&instance) + + result, err = reconcileAndTrackStatus(ctx, r.Client, instance.DeepCopy(), r.name, func() (ctrl.Result, error) { + if err := mcp.CreateOrUpdateVMMCP(ctx, r, &instance); err != nil { + return result, fmt.Errorf("failed create or update vmmcp: %w", err) + } + return result, nil + }) + + if err == nil { + result.RequeueAfter = r.BaseConf.ResyncAfterDuration() + } + + return +} + +// SetupWithManager sets up the controller with the Manager. +func (r *VMMCPReconciler) SetupWithManager(mgr ctrl.Manager) error { + b := ctrl.NewControllerManagedBy(mgr). + For(&vmv1.VMMCP{}). + Owns(&appsv1.Deployment{}). + Owns(&corev1.Service{}). + Owns(&corev1.ServiceAccount{}). + Owns(&policyv1.PodDisruptionBudget{}). + Owns(&networkingv1.NetworkPolicy{}). + Owns(&vmv1beta1.VMServiceScrape{}) + // the VPA CRD is installed only when VPA API support is enabled + if r.BaseConf.VPAAPIEnabled { + b = b.Owns(&vpav1.VerticalPodAutoscaler{}) + } + return b.WithOptions(getDefaultOptions()).Complete(r) +} + +// IsDisabled returns true if controller should be disabled +func (*VMMCPReconciler) IsDisabled(_ *config.BaseOperatorConf, _ sets.Set[string]) bool { + return false +} diff --git a/internal/manager/manager.go b/internal/manager/manager.go index 90d0f8dc23..49c9f6963d 100644 --- a/internal/manager/manager.go +++ b/internal/manager/manager.go @@ -437,6 +437,7 @@ func addWebhooks(mgr ctrl.Manager, baseConfig *config.BaseOperatorConf) error { webhookv1.SetupVTSingleWebhookWithManager, webhookv1.SetupVTAgentWebhookWithManager, webhookv1.SetupVTClusterWebhookWithManager, + webhookv1.SetupVMMCPWebhookWithManager, webhookv1beta1.SetupVMAlertmanagerWebhookWithManager, webhookv1beta1.SetupVMAlertmanagerConfigWebhookWithManager, webhookv1beta1.SetupVMAuthWebhookWithManager, @@ -554,6 +555,7 @@ var controllersByName = map[string]crdController{ "VTSingle": &vmcontroller.VTSingleReconciler{}, "VTAgent": &vmcontroller.VTAgentReconciler{}, "VTCluster": &vmcontroller.VTClusterReconciler{}, + "VMMCP": &vmcontroller.VMMCPReconciler{}, "VMAlertmanager": &vmcontroller.VMAlertmanagerReconciler{}, "VMAlert": &vmcontroller.VMAlertReconciler{}, "VMUser": &vmcontroller.VMUserReconciler{}, diff --git a/internal/webhook/operator/v1/vmmcp_webhook.go b/internal/webhook/operator/v1/vmmcp_webhook.go new file mode 100644 index 0000000000..c34918483a --- /dev/null +++ b/internal/webhook/operator/v1/vmmcp_webhook.go @@ -0,0 +1,68 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "context" + "errors" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + vmv1 "github.com/VictoriaMetrics/operator/api/operator/v1" + vmv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/build" +) + +// SetupVMMCPWebhookWithManager will setup the manager to manage the webhooks +func SetupVMMCPWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr, &vmv1.VMMCP{}). + WithValidator(&VMMCPCustomValidator{}). + Complete() +} + +// +kubebuilder:webhook:path=/validate-operator-victoriametrics-com-v1-vmmcp,mutating=false,failurePolicy=fail,sideEffects=None,groups=operator.victoriametrics.com,resources=vmmcps,verbs=create;update,versions=v1,name=vvmmcps-v1.kb.io,admissionReviewVersions=v1 +type VMMCPCustomValidator struct{} + +var _ admission.Validator[*vmv1.VMMCP] = (*VMMCPCustomValidator)(nil) + +// ValidateCreate implements admission.Validator so a webhook will be registered for the type +func (*VMMCPCustomValidator) ValidateCreate(_ context.Context, obj *vmv1.VMMCP) (admission.Warnings, error) { + if obj.Status.ParsingSpecError != "" { + return nil, errors.New(obj.Status.ParsingSpecError) + } + if err := obj.Validate(); err != nil { + return nil, err + } + return build.WarnOpenShiftSecurityContext(obj.Spec.SecurityContext), nil +} + +// ValidateUpdate implements admission.Validator so a webhook will be registered for the type +func (*VMMCPCustomValidator) ValidateUpdate(_ context.Context, _, newObj *vmv1.VMMCP) (admission.Warnings, error) { + if newObj.Status.ParsingSpecError != "" && !vmv1beta1.HasUnknownFields(newObj.Status.ParsingSpecError) { + return nil, errors.New(newObj.Status.ParsingSpecError) + } + if err := newObj.Validate(); err != nil { + return nil, err + } + return build.WarnOpenShiftSecurityContext(newObj.Spec.SecurityContext), nil +} + +// ValidateDelete implements admission.Validator so a webhook will be registered for the type +func (*VMMCPCustomValidator) ValidateDelete(_ context.Context, _ *vmv1.VMMCP) (admission.Warnings, error) { + return nil, nil +} diff --git a/test/e2e/mcp_test.go b/test/e2e/mcp_test.go new file mode 100644 index 0000000000..cb35d373d2 --- /dev/null +++ b/test/e2e/mcp_test.go @@ -0,0 +1,272 @@ +package e2e + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/rest" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + vmv1 "github.com/VictoriaMetrics/operator/api/operator/v1" + vmv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1" + "github.com/VictoriaMetrics/operator/internal/controller/operator/factory/finalize" +) + +// callMCPTool opens a session with the MCP server over Streamable HTTP transport via Kubernetes API server proxy, +// calls the given tool and returns the text content of its result +func callMCPTool(ctx context.Context, svcURL, tool string, args map[string]any) (string, error) { + proxyURL, err := buildServiceProxyURL(&k8sCfg, svcURL+"/mcp") + if err != nil { + return "", err + } + hc, err := rest.HTTPClientFor(&k8sCfg) + if err != nil { + return "", err + } + hc.Timeout = 10 * time.Second + post := func(sessionID string, msg map[string]any) (http.Header, []byte, error) { + data, err := json.Marshal(msg) + if err != nil { + return nil, nil, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, proxyURL, bytes.NewReader(data)) + if err != nil { + return nil, nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json, text/event-stream") + if sessionID != "" { + req.Header.Set("Mcp-Session-Id", sessionID) + } + resp, err := hc.Do(req) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, err + } + // notifications are accepted with 202 and no body + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted { + return nil, nil, fmt.Errorf("unexpected status code for %s request: %d, body: %s", msg["method"], resp.StatusCode, body) + } + return resp.Header, body, nil + } + + headers, _, err := post("", map[string]any{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": map[string]any{ + "protocolVersion": "2025-03-26", + "capabilities": map[string]any{}, + "clientInfo": map[string]any{"name": "vm-operator-e2e", "version": "v1"}, + }, + }) + if err != nil { + return "", err + } + sessionID := headers.Get("Mcp-Session-Id") + if sessionID == "" { + return "", fmt.Errorf("MCP server didn't return session id") + } + // the client must confirm the initialization before sending other requests + if _, _, err := post(sessionID, map[string]any{ + "jsonrpc": "2.0", + "method": "notifications/initialized", + }); err != nil { + return "", err + } + _, body, err := post(sessionID, map[string]any{ + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": map[string]any{"name": tool, "arguments": args}, + }) + if err != nil { + return "", err + } + var resp struct { + Result struct { + Content []struct { + Text string `json:"text"` + } `json:"content"` + IsError bool `json:"isError"` + } `json:"result"` + Error *struct { + Message string `json:"message"` + } `json:"error"` + } + if err := json.Unmarshal(body, &resp); err != nil { + return "", fmt.Errorf("cannot parse tools/call response %q: %w", body, err) + } + if resp.Error != nil { + return "", fmt.Errorf("tools/call failed: %s", resp.Error.Message) + } + var texts []string + for _, c := range resp.Result.Content { + texts = append(texts, c.Text) + } + text := strings.Join(texts, "\n") + if resp.Result.IsError { + return "", fmt.Errorf("tool %q returned error: %s", tool, text) + } + return text, nil +} + +// expectMCPChildObjectsDeleted checks that objects created for the MCP server are garbage collected after CR removal +func expectMCPChildObjectsDeleted(ctx context.Context, nsn types.NamespacedName) { + GinkgoHelper() + Eventually(func() error { + for _, obj := range []client.Object{ + &appsv1.Deployment{}, + &corev1.Service{}, + &corev1.ServiceAccount{}, + &vmv1beta1.VMServiceScrape{}, + &policyv1.PodDisruptionBudget{}, + } { + if err := k8sClient.Get(ctx, nsn, obj); !k8serrors.IsNotFound(err) { + return fmt.Errorf("%T %s must be deleted, got err: %v", obj, nsn, err) + } + } + return nil + }, eventualDeletionTimeout).ShouldNot(HaveOccurred()) +} + +//nolint:dupl,lll +var _ = Describe("test mcp Controllers", Label("mcp"), func() { + var ctx context.Context + namespace := fmt.Sprintf("default-%d", GinkgoParallelProcess()) + // the documentation tool loads an index of the built-in documentation at start, + // which slows down the MCP server start and takes most of its memory, so it's disabled in tests + disabledTools := []string{"documentation"} + resources := corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("10m"), + corev1.ResourceMemory: resource.MustParse("32Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("200m"), + corev1.ResourceMemory: resource.MustParse("128Mi"), + }, + } + + BeforeEach(func() { + ctx = context.Background() + }) + + It("should run VMMCP for VMSingle", Label("vm", "vmmcp"), func() { + backend := &vmv1beta1.VMSingle{ + ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "mcp-backend"}, + Spec: vmv1beta1.VMSingleSpec{ + RetentionPeriod: "1", + CommonAppsParams: vmv1beta1.CommonAppsParams{ + ReplicaCount: ptr.To[int32](1), + }, + }, + } + backendNSN := types.NamespacedName{Namespace: namespace, Name: backend.Name} + DeferCleanup(func() { + Expect(finalize.SafeDelete(ctx, k8sClient, backend)).ToNot(HaveOccurred()) + waitResourceDeleted(ctx, backendNSN, &vmv1beta1.VMSingleList{}) + }) + expectStatusAfterAction(ctx, &vmv1beta1.VMSingleList{}, backendNSN, eventualDeploymentAppReadyTimeout, func() { + Expect(k8sClient.Create(ctx, backend)).ToNot(HaveOccurred()) + }, vmv1beta1.UpdateStatusOperational) + + cr := &vmv1.VMMCP{ + ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "metrics"}, + Spec: vmv1.VMMCPSpec{ + Entrypoint: backend.AsURL(false), + InstanceType: vmv1.VMMCPInstanceTypeSingle, + CommonMCPSpec: vmv1.CommonMCPSpec{ + DisabledTools: disabledTools, + CommonAppsParams: vmv1beta1.CommonAppsParams{ + UseStrictSecurity: ptr.To(true), + Resources: resources, + }, + }, + }, + } + nsn := types.NamespacedName{Namespace: namespace, Name: cr.Name} + DeferCleanup(func() { + Expect(finalize.SafeDelete(ctx, k8sClient, cr)).ToNot(HaveOccurred()) + waitResourceDeleted(ctx, nsn, &vmv1.VMMCPList{}) + }) + expectStatusAfterAction(ctx, &vmv1.VMMCPList{}, nsn, eventualDeploymentAppReadyTimeout, func() { + Expect(k8sClient.Create(ctx, cr)).ToNot(HaveOccurred()) + }, vmv1beta1.UpdateStatusOperational) + + childNSN := types.NamespacedName{Namespace: namespace, Name: cr.PrefixedName()} + var dep appsv1.Deployment + Expect(k8sClient.Get(ctx, childNSN, &dep)).ToNot(HaveOccurred()) + Expect(dep.Spec.Template.Spec.Containers).To(HaveLen(1)) + Expect(dep.Spec.Template.Spec.Containers[0].SecurityContext).NotTo(BeNil()) + Expect(dep.Spec.Template.Spec.Containers[0].SecurityContext.RunAsNonRoot).To(Equal(ptr.To(true))) + Expect(k8sClient.Get(ctx, childNSN, &vmv1beta1.VMServiceScrape{})).ToNot(HaveOccurred()) + + By("querying VMSingle with MCP query tool") + Eventually(func() (string, error) { + return callMCPTool(ctx, cr.AsURL(false), "query", map[string]any{"query": "vm_app_version"}) + }, eventualDeploymentAppReadyTimeout).Should(ContainSubstring(`"status":"success"`)) + + By("updating MCP server settings") + expectStatusAfterAction(ctx, &vmv1.VMMCPList{}, nsn, eventualDeploymentAppReadyTimeout, func() { + Eventually(func() error { + var toUpdate vmv1.VMMCP + if err := k8sClient.Get(ctx, nsn, &toUpdate); err != nil { + return err + } + toUpdate.Spec.LogLevel = "debug" + toUpdate.Spec.PodDisruptionBudget = &vmv1beta1.EmbeddedPodDisruptionBudgetSpec{ + MaxUnavailable: ptr.To(intstr.FromInt32(1)), + } + return k8sClient.Update(ctx, &toUpdate) + }, eventualDeploymentAppReadyTimeout).ShouldNot(HaveOccurred()) + }, vmv1beta1.UpdateStatusOperational) + Expect(k8sClient.Get(ctx, childNSN, &dep)).ToNot(HaveOccurred()) + Expect(dep.Spec.Template.Spec.Containers[0].Env).To(ContainElement(corev1.EnvVar{Name: "MCP_LOG_LEVEL", Value: "debug"})) + Expect(k8sClient.Get(ctx, childNSN, &policyv1.PodDisruptionBudget{})).ToNot(HaveOccurred()) + + By("restoring a deleted child object") + // wait until the updated Deployment settles, since its status updates trigger reconciles too + var lastVersion string + Eventually(func() bool { + if err := k8sClient.Get(ctx, childNSN, &dep); err != nil { + return false + } + settled := dep.ResourceVersion == lastVersion + lastVersion = dep.ResourceVersion + return settled + }, eventualDeploymentAppReadyTimeout, 5*time.Second).Should(BeTrue()) + // the controller watches its child objects, so it restores them before the periodic resync + Expect(k8sClient.Delete(ctx, &policyv1.PodDisruptionBudget{ObjectMeta: metav1.ObjectMeta{Namespace: childNSN.Namespace, Name: childNSN.Name}})).ToNot(HaveOccurred()) + Eventually(func() error { + return k8sClient.Get(ctx, childNSN, &policyv1.PodDisruptionBudget{}) + }, 20*time.Second).ShouldNot(HaveOccurred()) + + By("deleting VMMCP") + Expect(finalize.SafeDelete(ctx, k8sClient, cr)).ToNot(HaveOccurred()) + waitResourceDeleted(ctx, nsn, &vmv1.VMMCPList{}) + expectMCPChildObjectsDeleted(ctx, childNSN) + }) + +})