Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions api/operator/v1/cluster_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ func TestVTCluster_AvailableStorageNodeIDs(t *testing.T) {
cr := &VTCluster{
Spec: VTClusterSpec{
Storage: &VTStorage{
CommonAppsParams: vmv1beta1.CommonAppsParams{
ReplicaCount: ptr.To(int32(5)),
StandardAppsParams: vmv1beta1.StandardAppsParams{
CommonAppsParams: vmv1beta1.CommonAppsParams{
ReplicaCount: ptr.To(int32(5)),
},
},
MaintenanceSelectNodeIDs: []int32{1, 3},
MaintenanceInsertNodeIDs: []int32{0, 4},
Expand All @@ -38,7 +40,9 @@ func TestVTCluster_AvailableStorageNodeIDs(t *testing.T) {
f(&VTCluster{
Spec: VTClusterSpec{
Storage: &VTStorage{
CommonAppsParams: vmv1beta1.CommonAppsParams{ReplicaCount: ptr.To(int32(3))},
StandardAppsParams: vmv1beta1.StandardAppsParams{
CommonAppsParams: vmv1beta1.CommonAppsParams{ReplicaCount: ptr.To(int32(3))},
},
},
},
}, vmv1beta1.ClusterComponentSelect, []int32{0, 1, 2})
Expand All @@ -54,8 +58,10 @@ func TestVLCluster_AvailableStorageNodeIDs(t *testing.T) {
cr := &VLCluster{
Spec: VLClusterSpec{
VLStorage: &VLStorage{
CommonAppsParams: vmv1beta1.CommonAppsParams{
ReplicaCount: ptr.To(int32(5)),
StandardAppsParams: vmv1beta1.StandardAppsParams{
CommonAppsParams: vmv1beta1.CommonAppsParams{
ReplicaCount: ptr.To(int32(5)),
},
},
MaintenanceSelectNodeIDs: []int32{1, 3},
MaintenanceInsertNodeIDs: []int32{0, 4},
Expand All @@ -73,7 +79,9 @@ func TestVLCluster_AvailableStorageNodeIDs(t *testing.T) {
f(&VLCluster{
Spec: VLClusterSpec{
VLStorage: &VLStorage{
CommonAppsParams: vmv1beta1.CommonAppsParams{ReplicaCount: ptr.To(int32(3))},
StandardAppsParams: vmv1beta1.StandardAppsParams{
CommonAppsParams: vmv1beta1.CommonAppsParams{ReplicaCount: ptr.To(int32(3))},
},
},
},
}, vmv1beta1.ClusterComponentSelect, []int32{0, 1, 2})
Expand Down
36 changes: 31 additions & 5 deletions api/operator/v1/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ import (
"strconv"

corev1 "k8s.io/api/core/v1"

vmv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1"
)

const (
healthPath = "/health"
metricsPath = "/metrics"
// OTLPGRPCPortName is the Service/container port name generated for the OTLP gRPC listener.
OTLPGRPCPortName = "otlp-grpc"
)

// TLSServerConfig defines VictoriaMetrics TLS configuration for the application's server
Expand Down Expand Up @@ -51,18 +55,40 @@ type OTLPGRPCSpec struct {
TLSConfig *TLSServerConfig `json:"tlsConfig,omitempty"`
}

// Validate checks that ListenPort doesn't collide with httpPort, the port already used for
// the component's main HTTP listener.
func (g *OTLPGRPCSpec) Validate(httpPort string) error {
// Validate checks that ListenPort doesn't collide with httpPort or any configured HTTPListener,
// and that no HTTPListener is named OTLPGRPCPortName.
func (g *OTLPGRPCSpec) Validate(httpPort string, listeners []vmv1beta1.HTTPListener) error {
Comment thread
AndrewChubatiuk marked this conversation as resolved.

@cubic-dev-ai cubic-dev-ai Bot Sep 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Changing this exported method from Validate(httpPort string) to a two-argument signature breaks external Go callers using the existing API. Preserve the one-argument method and add a listener-aware helper, or use a backward-compatible variadic form.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api/operator/v1/common.go, line 60:

<comment>Changing this exported method from `Validate(httpPort string)` to a two-argument signature breaks external Go callers using the existing API. Preserve the one-argument method and add a listener-aware helper, or use a backward-compatible variadic form.</comment>

<file context>
@@ -51,18 +55,40 @@ type OTLPGRPCSpec struct {
-func (g *OTLPGRPCSpec) Validate(httpPort string) error {
+// Validate checks that ListenPort doesn't collide with httpPort or any configured HTTPListener,
+// and that no HTTPListener is named OTLPGRPCPortName.
+func (g *OTLPGRPCSpec) Validate(httpPort string, listeners []vmv1beta1.HTTPListener) error {
 	if g == nil {
 		return nil
</file context>
Fix with cubic

if g == nil {
return nil
}
if strconv.Itoa(int(g.ListenPort)) == httpPort {
return fmt.Errorf("spec.grpcSpec.listenPort=%d must not be equal to the HTTP listen port=%s", g.ListenPort, httpPort)
if len(listeners) == 0 {
if portsEqual(strconv.Itoa(int(g.ListenPort)), httpPort) {
return fmt.Errorf("spec.grpcSpec.listenPort=%d must not be equal to the HTTP listen port=%s", g.ListenPort, httpPort)
}
return nil
}
for i := range listeners {
if listeners[i].Name == OTLPGRPCPortName {
return fmt.Errorf("httpListeners[%d].name cannot be %q while spec.grpcSpec is enabled, since it collides with the generated OTLP gRPC port name", i, OTLPGRPCPortName)
}
if port := listeners[i].AddrPort(); portsEqual(strconv.Itoa(int(g.ListenPort)), port) {
return fmt.Errorf("spec.grpcSpec.listenPort=%d must not be equal to httpListeners[%d].addr port=%s", g.ListenPort, i, port)
}
}
return nil
}

// portsEqual compares two port strings numerically when both parse as integers,
// falling back to a plain string comparison otherwise.
func portsEqual(a, b string) bool {
ai, aerr := strconv.Atoi(a)
bi, berr := strconv.Atoi(b)
if aerr != nil || berr != nil {
return a == b
}
return ai == bi
}

// OAuth2 defines OAuth2 configuration parameters
// with optional references to secrets with corresponding sensitive values
type OAuth2 struct {
Expand Down
36 changes: 30 additions & 6 deletions api/operator/v1/common_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package v1

import "testing"
import (
"testing"

vmv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1"
)

func TestOTLPGRPCSpec_Validate(t *testing.T) {
f := func(g *OTLPGRPCSpec, httpPort string, wantErr bool) {
f := func(g *OTLPGRPCSpec, httpPort string, listeners []vmv1beta1.HTTPListener, wantErr bool) {
t.Helper()
err := g.Validate(httpPort)
err := g.Validate(httpPort, listeners)
if wantErr && err == nil {
t.Fatalf("expected error, got nil")
}
Expand All @@ -15,11 +19,31 @@ func TestOTLPGRPCSpec_Validate(t *testing.T) {
}

// nil spec is always valid
f(nil, "10429", false)
f(nil, "10429", nil, false)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

// distinct ports
f(&OTLPGRPCSpec{ListenPort: 4317}, "10429", false)
f(&OTLPGRPCSpec{ListenPort: 4317}, "10429", nil, false)

// colliding with the HTTP port
f(&OTLPGRPCSpec{ListenPort: 10429}, "10429", true)
f(&OTLPGRPCSpec{ListenPort: 10429}, "10429", nil, true)

// a listener named like the generated OTLP gRPC port collides
f(&OTLPGRPCSpec{ListenPort: 4317}, "10429", []vmv1beta1.HTTPListener{
Comment thread
AndrewChubatiuk marked this conversation as resolved.
{Name: "otlp-grpc", Addr: ":10429"},
}, true)

// a listener with a different name is fine
f(&OTLPGRPCSpec{ListenPort: 4317}, "10429", []vmv1beta1.HTTPListener{
{Name: "http", Addr: ":10429"},
}, false)

// a listener's own port colliding with the gRPC port is invalid, even with a different name
f(&OTLPGRPCSpec{ListenPort: 4317}, "10429", []vmv1beta1.HTTPListener{
{Name: "http", Addr: ":4317"},
}, true)

// nil grpcSpec is always valid, regardless of listener names
f(nil, "10429", []vmv1beta1.HTTPListener{
{Name: "otlp-grpc", Addr: ":4317"},
}, false)
}
42 changes: 21 additions & 21 deletions api/operator/v1/vlagent_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package v1
import (
"encoding/json"
"fmt"
"strings"

appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -107,8 +106,8 @@ type VLAgentSpec struct {
UseLegacyNaming bool `json:"useLegacyNaming,omitempty"`
// Configures vertical pod autoscaling.
// +optional
VPA *vmv1beta1.EmbeddedVPA `json:"vpa,omitempty"`
vmv1beta1.CommonAppsParams `json:",inline,omitempty"`
VPA *vmv1beta1.EmbeddedVPA `json:"vpa,omitempty"`
vmv1beta1.StandardAppsParams `json:",inline,omitempty"`
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
}

type VLAgentK8sCollector struct {
Expand Down Expand Up @@ -171,6 +170,9 @@ func (cr *VLAgent) Validate() error {
if cr.Spec.ServiceSpec != nil && cr.Spec.ServiceSpec.Name == cr.PrefixedName() {
return fmt.Errorf("spec.serviceSpec.Name cannot be equal to prefixed name=%q", cr.PrefixedName())
}
if err := cr.Spec.SyslogSpec.ValidateNoListenerNameCollision(cr.Spec.HTTPListeners); err != nil {
return err
}
if len(cr.Spec.RemoteWrite) == 0 {
return fmt.Errorf("spec.remoteWrite cannot be empty array, provide at least one remoteWrite")
}
Expand Down Expand Up @@ -209,7 +211,7 @@ func (cr *VLAgent) Validate() error {

// UseProxyProtocol implements build.probeCRD interface
func (cr *VLAgent) UseProxyProtocol() bool {
return vmv1beta1.UseProxyProtocol(cr.Spec.ExtraArgs)
return cr.Spec.UseProxyProtocol()
}

// VLAgentRemoteWriteSettings - defines global settings for all remoteWrite urls.
Expand Down Expand Up @@ -460,7 +462,12 @@ func (cr *VLAgent) GetMetricsPath() string {

// UseTLS returns true if TLS is enabled
func (cr *VLAgent) UseTLS() bool {
return vmv1beta1.UseTLS(cr.Spec.ExtraArgs)
return cr.Spec.UseTLS()
}

// PrimaryPortName returns the Service port name generated for the primary listener.
func (cr *VLAgent) PrimaryPortName() string {
return cr.Spec.PrimaryPortName()
}

// ExtraArgs returns additionally configured command-line arguments
Expand All @@ -486,31 +493,24 @@ func (cr *VLAgent) IsOwnsServiceAccount() bool {
return cr.Spec.ServiceAccountName == ""
}

// Params implements build.scrapeBuilder and urlBuilder interfaces
func (cr *VLAgent) Params(vmv1beta1.ParamsKind) *vmv1beta1.StandardAppsParams {
return &cr.Spec.StandardAppsParams
}

// AsURL - returns url for http access
func (cr *VLAgent) AsURL(isExtra bool) string {
specPort := cr.Spec.Port
if specPort == "" {
specPort = "9429"
func (cr *VLAgent) AsURL(nsn vmv1beta1.NamespacedName) (string, error) {
if nsn.ListenerName != "" && cr.Spec.ByName(nsn.ListenerName) == nil {
return "", fmt.Errorf("listenerName=%q not found at VLAgent=%q httpListeners", nsn.ListenerName, cr.Name)
}
svcName, port := vmv1beta1.ResolveServiceURL(cr.PrefixedName(), specPort, "http", cr.Spec.ServiceSpec, isExtra)
return fmt.Sprintf("%s://%s.%s.svc:%s", vmv1beta1.HTTPProtoFromFlags(cr.Spec.ExtraArgs), svcName, cr.Namespace, port)
return vmv1beta1.BuildServiceURL(cr, nsn)
}

// ProbePath implements build.probeCRD interface
func (cr *VLAgent) ProbePath() string {
return vmv1beta1.BuildPathWithPrefixFlag(cr.Spec.ExtraArgs, healthPath)
}

// ProbeScheme implements build.probeCRD interface
func (cr *VLAgent) ProbeScheme() string {
return strings.ToUpper(vmv1beta1.HTTPProtoFromFlags(cr.Spec.ExtraArgs))
}

// ProbePort implements build.probeCRD interface
func (cr *VLAgent) ProbePort() string {
return cr.Spec.Port
}

func (cr *VLAgent) GetRBACName() string {
return fmt.Sprintf("monitoring:%s:vlagent-%s", cr.Namespace, cr.Name)
}
Expand Down
Loading