From 323dd39d2dab91d3a7bcac925ed7e9e5349a7694 Mon Sep 17 00:00:00 2001 From: Jan Simon Date: Mon, 30 Jun 2025 16:37:51 +0200 Subject: [PATCH 01/50] feat: influxdb cloud dedicated support --- chronograf.go | 6 + influx/authorization.go | 16 + influx/cloud_dedicated.go | 145 ++++++ influx/databases.go | 7 + influx/influx.go | 54 +- kv/internal/internal.go | 8 + kv/internal/internal.pb.go | 603 ++++++++++++----------- kv/internal/internal.proto | 4 + server/sources.go | 31 +- ui/src/shared/constants/index.ts | 1 + ui/src/sources/components/SourceStep.tsx | 105 +++- ui/src/types/sources.ts | 4 + 12 files changed, 671 insertions(+), 313 deletions(-) create mode 100644 influx/cloud_dedicated.go diff --git a/chronograf.go b/chronograf.go index e55baf0f99..32d1aaff70 100644 --- a/chronograf.go +++ b/chronograf.go @@ -99,6 +99,8 @@ const ( InfluxRelay = "influx-relay" // InfluxDBv2 is Influx DB 2.x with Token authentication InfluxDBv2 = "influx-v2" + // InfluxDBCloudDedicated is InfluxDB Cloud Dedicated with Account ID, Cluster ID, Management and DB Token + InfluxDBCloudDedicated = "influx-cloud-dedicated" ) // TSDBStatus represents the current status of a time series database @@ -243,6 +245,10 @@ type Source struct { Username string `json:"username,omitempty"` // Username is the username to connect to the source Password string `json:"password,omitempty"` // Password is in CLEARTEXT SharedSecret string `json:"sharedSecret,omitempty"` // ShareSecret is the optional signing secret for Influx JWT authorization + ClusterID string `json:"clusterId,omitempty"` // ClusterID is the cluster ID for InfluxDB Cloud Dedicated sources + AccountID string `json:"accountId,omitempty"` // AccountID is the account ID for InfluxDB Cloud Dedicated sources + ManagementToken string `json:"managementToken,omitempty"` // ManagementToken is the management token for InfluxDB Cloud Dedicated sources + DatabaseToken string `json:"databaseToken,omitempty"` // DatabaseToken is the database token for InfluxDB Cloud Dedicated sources URL string `json:"url"` // URL are the connections to the source MetaURL string `json:"metaUrl,omitempty"` // MetaURL is the url for the meta node InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` // InsecureSkipVerify as true means any certificate presented by the source is accepted. diff --git a/influx/authorization.go b/influx/authorization.go index de03a995e8..60523cbc6e 100644 --- a/influx/authorization.go +++ b/influx/authorization.go @@ -23,6 +23,12 @@ func (n *NoAuthorization) Set(req *http.Request) error { return nil } // DefaultAuthorization creates either a shared JWT builder, basic auth or Noop or Token authentication func DefaultAuthorization(src *chronograf.Source) Authorizer { + // Use Bearer Token authentication for InfluxDB Cloud + if src.Type == chronograf.InfluxDBCloudDedicated { + return &BearerToken{ + Token: src.DatabaseToken, + } + } // Use Token authentication for InfluxDB v2 if src.Type == chronograf.InfluxDBv2 { return &TokenAuth{ @@ -71,6 +77,16 @@ func (a *TokenAuth) Set(r *http.Request) error { return nil } +// BearerToken adds `Authorization: Bearer ` to the request header, where the token is in non-JWT format. +type BearerToken struct { + Token string +} + +func (a *BearerToken) Set(r *http.Request) error { + r.Header.Set("Authorization", "Bearer "+a.Token) + return nil +} + // BearerJWT is the default Bearer for InfluxDB type BearerJWT struct { Username string diff --git a/influx/cloud_dedicated.go b/influx/cloud_dedicated.go new file mode 100644 index 0000000000..16abfa97d4 --- /dev/null +++ b/influx/cloud_dedicated.go @@ -0,0 +1,145 @@ +package influx + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/influxdata/chronograf" + "github.com/influxdata/chronograf/util" +) + +type cdDatabase struct { + Name string `json:"name,omitempty"` +} + +type cdListDatabasesError struct { + Code string `json:"code,omitempty"` + Message string `json:"message,omitempty"` +} + +// validateCloudDedicatedAuth checks both the management endpoint and the database endpoint to validate authentication. +func (c *Client) validateCloudDedicatedAuth(ctx context.Context) error { + var req *http.Request + var err error + + // Call list databases on management api. + if req, _, err = c.newListDatabasesRequestForCloudDedicated(ctx); err != nil { + return fmt.Errorf("management authentication failed: %w", err) + } + if err = c.executeRequest(err, req); err != nil { + return fmt.Errorf("management authentication failed: %w", err) + } + + // Call dummy query on query api. + if req, err = c.newDummyQueryRequestForCloudDedicated(ctx); err != nil { + return fmt.Errorf("database authentication failed: %w", err) + } + if err = c.executeRequest(err, req); err != nil { + return fmt.Errorf("database authentication failed: %w", err) + } + + return nil +} + +// listDatabasesForCloudDedicated list databases of InfluxDB Cloud Dedicated using the management api. +func (c *Client) listDatabasesForCloudDedicated(ctx context.Context) ([]chronograf.Database, error) { + // Prepare request. + req, logs, err := c.newListDatabasesRequestForCloudDedicated(ctx) + if err != nil { + return nil, err + } + + // Do request. + hc := &http.Client{} + hc.Transport = SharedTransport(c.InsecureSkipVerify) + resp, err := hc.Do(req) + if err != nil { + if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { + return nil, chronograf.ErrUpstreamTimeout + } + return nil, err + } + defer resp.Body.Close() + + // Handle non-OK status. + if resp.StatusCode != http.StatusOK { + var errorResponse cdListDatabasesError + dec := json.NewDecoder(resp.Body) + _ = dec.Decode(&errorResponse) + return nil, fmt.Errorf("received status code %d from server: err: %s", resp.StatusCode, errorResponse.Message) + } + + // Decode response. + var databases []cdDatabase + dec := json.NewDecoder(resp.Body) + decErr := dec.Decode(&databases) + if decErr != nil { + logs.WithField("influx_status", resp.StatusCode). + Error("Error parsing results from influxdb: err:", decErr) + return nil, decErr + } + + // Convert response. + result := make([]chronograf.Database, len(databases)) + for i, database := range databases { + result[i] = chronograf.Database{Name: database.Name} + } + return result, nil +} + +// newListDatabasesRequestForCloudDedicated constructs a new http.Request for listing databases in InfluxDB Cloud Dedicated. +func (c *Client) newListDatabasesRequestForCloudDedicated(ctx context.Context) (*http.Request, chronograf.Logger, error) { + req, err := http.NewRequest("GET", util.AppendPath(c.MgmtURL, "/databases").String(), nil) + if err != nil { + return nil, nil, err + } + req = req.WithContext(ctx) + logs := c.Logger. + WithField("component", "proxy"). + WithField("host", req.Host) + logs.Debug(req.URL.Path) + + if c.MgmtAuthorizer != nil { + if err := c.MgmtAuthorizer.Set(req); err != nil { + logs.Error("Error setting authorization header ", err) + return nil, nil, err + } + } + + return req, logs, err +} + +// newDummyQueryRequestForCloudDedicated constructs a http.Request to call a dummy query in InfluxDB Cloud Dedicated. +func (c *Client) newDummyQueryRequestForCloudDedicated(ctx context.Context) (*http.Request, error) { + u, err := url.Parse(c.URL.String()) + if err != nil { + return nil, err + } + u = util.AppendPath(u, "/query") + + form := url.Values{} + form.Set("q", "SELECT * FROM dummy") + req, err := http.NewRequest("POST", u.String(), strings.NewReader(form.Encode())) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + logs := c.Logger. + WithField("component", "proxy"). + WithField("host", req.Host) + logs.Debug("/query") + + if c.Authorizer != nil { + if err := c.Authorizer.Set(req); err != nil { + logs.Error("Error setting authorization header ", err) + return nil, err + } + } + return req, err +} diff --git a/influx/databases.go b/influx/databases.go index 6560e55e69..9b2b2bf791 100644 --- a/influx/databases.go +++ b/influx/databases.go @@ -11,6 +11,9 @@ import ( // AllDB returns all databases from within Influx func (c *Client) AllDB(ctx context.Context) ([]chronograf.Database, error) { + if c.SrcType == chronograf.InfluxDBCloudDedicated { + return c.listDatabasesForCloudDedicated(ctx) + } return c.showDatabases(ctx) } @@ -42,6 +45,10 @@ func (c *Client) DropDB(ctx context.Context, db string) error { // AllRP returns all the retention policies for a specific database func (c *Client) AllRP(ctx context.Context, db string) ([]chronograf.RetentionPolicy, error) { + if c.SrcType == chronograf.InfluxDBCloudDedicated { + // Data retention in InfluxDB 3 is configured differently, on database level. + return []chronograf.RetentionPolicy{}, nil + } return c.showRetentionPolicies(ctx, db) } diff --git a/influx/influx.go b/influx/influx.go index bd9d6c246f..efdf83c7c8 100644 --- a/influx/influx.go +++ b/influx/influx.go @@ -40,7 +40,10 @@ func init() { type Client struct { URL *url.URL Authorizer Authorizer + MgmtURL *url.URL // (optional) URL for management API + MgmtAuthorizer Authorizer // (optional) Authorizer for management API InsecureSkipVerify bool + SrcType string Logger chronograf.Logger } @@ -168,8 +171,13 @@ func (c *Client) ValidateAuth(ctx context.Context, src *chronograf.Source) error ctx, cancel := context.WithTimeout(ctx, 15*time.Second) defer cancel() + // Cloud Dedicated: + if src.Type == chronograf.InfluxDBCloudDedicated { + return c.validateCloudDedicatedAuth(ctx) + } + // v2: use flux query if src.Type == chronograf.InfluxDBv2 { - return c.validateAuthFlux(ctx, src) + return c.validateV2Auth(ctx, src) } // v1: use InfluxQL if _, err := c.Query(ctx, chronograf.Query{Command: "SHOW DATABASES"}); err != nil { @@ -178,8 +186,8 @@ func (c *Client) ValidateAuth(ctx context.Context, src *chronograf.Source) error return nil } -// validateAuthFlux uses Flux query to validate token authentication -func (c *Client) validateAuthFlux(ctx context.Context, src *chronograf.Source) error { +// validateV2Auth uses Flux query to validate token authentication +func (c *Client) validateV2Auth(ctx context.Context, src *chronograf.Source) error { u, err := url.Parse(c.URL.String()) if err != nil { return err @@ -210,11 +218,15 @@ func (c *Client) validateAuthFlux(ctx context.Context, src *chronograf.Source) e } } + return c.executeRequest(err, req) +} + +func (c *Client) executeRequest(err error, req *http.Request) error { hc := &http.Client{} hc.Transport = SharedTransport(c.InsecureSkipVerify) resp, err := hc.Do(req) if err != nil { - if err == context.DeadlineExceeded || err == context.Canceled { + if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { return chronograf.ErrUpstreamTimeout } return err @@ -245,6 +257,20 @@ func (c *Client) Connect(ctx context.Context, src *chronograf.Source) error { } c.URL = u + + // InfluxDB Cloud Dedicated also provides a management API. + if src.Type == chronograf.InfluxDBCloudDedicated { + mgmtUrl := fmt.Sprintf("https://console.influxdata.com/api/v0/accounts/%s/clusters/%s", src.AccountID, src.ClusterID) + if u, err = url.Parse(mgmtUrl); err != nil { + return err + } + + c.MgmtURL = u + c.MgmtAuthorizer = &BearerToken{ + Token: src.ManagementToken, + } + } + c.SrcType = src.Type return nil } @@ -331,11 +357,18 @@ func (c *Client) ping(u *url.URL) (string, string, error) { return "", "", err } - version := resp.Header.Get("X-Influxdb-Build") - if version == "ENT" { - return version, chronograf.InfluxEnterprise, nil + builds := resp.Header.Values("X-Influxdb-Build") + isCloud2 := false + for _, build := range builds { + if build == "ENT" { + return build, chronograf.InfluxEnterprise, nil + } + if build == "cloud2" { + isCloud2 = true + } } - version = resp.Header.Get("X-Influxdb-Version") + + version := resp.Header.Get("X-Influxdb-Version") if strings.Contains(version, "-c") { return version, chronograf.InfluxEnterprise, nil } else if strings.Contains(version, "relay") { @@ -347,6 +380,11 @@ func (c *Client) ping(u *url.URL) (string, string, error) { version = version[1:] } + if isCloud2 { + // TODO: improve this, other influxdb v3 version could also return "cloud2" + return version, chronograf.InfluxDBCloudDedicated, nil + } + return version, chronograf.InfluxDB, nil } diff --git a/kv/internal/internal.go b/kv/internal/internal.go index 70cc068f79..b5b3e61fdc 100644 --- a/kv/internal/internal.go +++ b/kv/internal/internal.go @@ -48,6 +48,10 @@ func MarshalSource(s chronograf.Source) ([]byte, error) { Role: s.Role, DefaultRP: s.DefaultRP, Version: s.Version, + ClusterID: s.ClusterID, + AccountID: s.AccountID, + ManagementToken: s.ManagementToken, + DatabaseToken: s.DatabaseToken, }) } @@ -73,6 +77,10 @@ func UnmarshalSource(data []byte, s *chronograf.Source) error { s.Role = pb.Role s.DefaultRP = pb.DefaultRP s.Version = pb.Version + s.ClusterID = pb.ClusterID + s.AccountID = pb.AccountID + s.ManagementToken = pb.ManagementToken + s.DatabaseToken = pb.DatabaseToken return nil } diff --git a/kv/internal/internal.pb.go b/kv/internal/internal.pb.go index 15ce31b5ee..135b3535b3 100644 --- a/kv/internal/internal.pb.go +++ b/kv/internal/internal.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.27.1 -// protoc v3.17.3 +// protoc-gen-go v1.34.1 +// protoc v5.29.3 // source: internal.proto package internal @@ -40,6 +40,10 @@ type Source struct { Role string `protobuf:"bytes,13,opt,name=Role,proto3" json:"Role,omitempty"` // Role is the name of the miniumum role that a user must possess to access the resource DefaultRP string `protobuf:"bytes,14,opt,name=DefaultRP,proto3" json:"DefaultRP,omitempty"` // DefaultRP is the default retention policy used in database queries to this source Version string `protobuf:"bytes,15,opt,name=Version,proto3" json:"Version,omitempty"` // Version of the InfluxDB or Unknown + ClusterID string `protobuf:"bytes,16,opt,name=ClusterID,proto3" json:"ClusterID,omitempty"` // Cluster ID of an InfluxDB Cloud Dedicated source + AccountID string `protobuf:"bytes,17,opt,name=AccountID,proto3" json:"AccountID,omitempty"` // Account ID of an InfluxDB Cloud Dedicated source + ManagementToken string `protobuf:"bytes,18,opt,name=ManagementToken,proto3" json:"ManagementToken,omitempty"` // Management token of an InfluxDB Cloud Dedicated source + DatabaseToken string `protobuf:"bytes,19,opt,name=DatabaseToken,proto3" json:"DatabaseToken,omitempty"` // Database token of an InfluxDB Cloud Dedicated source } func (x *Source) Reset() { @@ -179,6 +183,34 @@ func (x *Source) GetVersion() string { return "" } +func (x *Source) GetClusterID() string { + if x != nil { + return x.ClusterID + } + return "" +} + +func (x *Source) GetAccountID() string { + if x != nil { + return x.AccountID + } + return "" +} + +func (x *Source) GetManagementToken() string { + if x != nil { + return x.ManagementToken + } + return "" +} + +func (x *Source) GetDatabaseToken() string { + if x != nil { + return x.DatabaseToken + } + return "" +} + type Dashboard struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2418,7 +2450,7 @@ var File_internal_proto protoreflect.FileDescriptor var file_internal_proto_rawDesc = []byte{ 0x0a, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x12, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x22, 0x9e, 0x03, 0x0a, 0x06, 0x53, + 0x12, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x22, 0xaa, 0x04, 0x0a, 0x06, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, @@ -2444,286 +2476,295 @@ var file_internal_proto_rawDesc = []byte{ 0x6f, 0x6c, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x52, 0x50, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x52, 0x50, 0x12, 0x18, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0f, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0xb4, 0x01, 0x0a, 0x09, - 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, - 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, - 0x64, 0x43, 0x65, 0x6c, 0x6c, 0x52, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x30, 0x0a, 0x09, - 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x12, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x52, 0x09, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x12, 0x22, - 0x0a, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x22, 0x92, 0x05, 0x0a, 0x0d, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, - 0x43, 0x65, 0x6c, 0x6c, 0x12, 0x0c, 0x0a, 0x01, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x01, 0x78, 0x12, 0x0c, 0x0a, 0x01, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x79, - 0x12, 0x0c, 0x0a, 0x01, 0x77, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x77, 0x12, 0x0c, - 0x0a, 0x01, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x68, 0x12, 0x29, 0x0a, 0x07, - 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x07, - 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, - 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, - 0x35, 0x0a, 0x04, 0x61, 0x78, 0x65, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, - 0x72, 0x64, 0x43, 0x65, 0x6c, 0x6c, 0x2e, 0x41, 0x78, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x04, 0x61, 0x78, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x06, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x73, - 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x2e, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x52, 0x06, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x73, 0x12, - 0x28, 0x0a, 0x06, 0x6c, 0x65, 0x67, 0x65, 0x6e, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x10, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x4c, 0x65, 0x67, 0x65, 0x6e, - 0x64, 0x52, 0x06, 0x6c, 0x65, 0x67, 0x65, 0x6e, 0x64, 0x12, 0x3a, 0x0a, 0x0c, 0x74, 0x61, 0x62, - 0x6c, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x16, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x54, 0x61, 0x62, 0x6c, 0x65, - 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0c, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x4f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x3c, 0x0a, 0x0c, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x61, 0x62, 0x6c, 0x65, - 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x0c, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, - 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x46, 0x6f, 0x72, - 0x6d, 0x61, 0x74, 0x12, 0x3d, 0x0a, 0x0d, 0x64, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x50, 0x6c, - 0x61, 0x63, 0x65, 0x73, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x44, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x50, 0x6c, 0x61, - 0x63, 0x65, 0x73, 0x52, 0x0d, 0x64, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x50, 0x6c, 0x61, 0x63, - 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x6f, 0x74, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x6e, 0x6f, 0x74, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x6f, 0x74, 0x65, 0x56, 0x69, - 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, - 0x6e, 0x6f, 0x74, 0x65, 0x56, 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x1a, 0x47, - 0x0a, 0x09, 0x41, 0x78, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x41, 0x78, 0x69, 0x73, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x47, 0x0a, 0x0d, 0x44, 0x65, 0x63, 0x69, 0x6d, - 0x61, 0x6c, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x73, 0x45, 0x6e, - 0x66, 0x6f, 0x72, 0x63, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, - 0x45, 0x6e, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x67, 0x69, - 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x64, 0x69, 0x67, 0x69, 0x74, 0x73, - 0x22, 0xbc, 0x01, 0x0a, 0x0c, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x76, 0x65, 0x72, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x54, 0x69, 0x6d, - 0x65, 0x41, 0x78, 0x69, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x76, 0x65, 0x72, - 0x74, 0x69, 0x63, 0x61, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x41, 0x78, 0x69, 0x73, 0x12, 0x30, 0x0a, - 0x06, 0x73, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x61, 0x62, - 0x6c, 0x65, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x06, 0x73, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x12, - 0x1a, 0x0a, 0x08, 0x77, 0x72, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x77, 0x72, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x26, 0x0a, 0x0e, 0x66, - 0x69, 0x78, 0x46, 0x69, 0x72, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x0e, 0x66, 0x69, 0x78, 0x46, 0x69, 0x72, 0x73, 0x74, 0x43, 0x6f, 0x6c, - 0x75, 0x6d, 0x6e, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x22, - 0x70, 0x0a, 0x0e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x65, 0x6c, - 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x4e, 0x61, 0x6d, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, - 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, - 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x69, 0x73, 0x69, 0x62, - 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, - 0x65, 0x22, 0x67, 0x0a, 0x05, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, - 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x10, - 0x0a, 0x03, 0x48, 0x65, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x48, 0x65, 0x78, - 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x3e, 0x0a, 0x06, 0x4c, 0x65, - 0x67, 0x65, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x4f, 0x72, 0x69, 0x65, - 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x4f, - 0x72, 0x69, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xb2, 0x01, 0x0a, 0x04, 0x41, - 0x78, 0x69, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x42, 0x6f, 0x75, - 0x6e, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x03, 0x52, 0x0c, 0x6c, 0x65, 0x67, 0x61, 0x63, - 0x79, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x6f, 0x75, 0x6e, 0x64, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x12, - 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x16, 0x0a, - 0x06, 0x73, 0x75, 0x66, 0x66, 0x69, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, - 0x75, 0x66, 0x66, 0x69, 0x78, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x61, 0x73, 0x65, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x62, 0x61, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x61, - 0x6c, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x22, - 0xdb, 0x01, 0x0a, 0x08, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, - 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x19, 0x0a, 0x08, - 0x74, 0x65, 0x6d, 0x70, 0x5f, 0x76, 0x61, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, - 0x74, 0x65, 0x6d, 0x70, 0x56, 0x61, 0x72, 0x12, 0x2f, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, - 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, - 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, - 0x65, 0x6c, 0x12, 0x2d, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x54, 0x65, 0x6d, - 0x70, 0x6c, 0x61, 0x74, 0x65, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, - 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x44, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x44, 0x22, 0x67, 0x0a, - 0x0d, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, - 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, - 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, - 0x63, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, - 0x63, 0x74, 0x65, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0xb5, 0x01, 0x0a, 0x0d, 0x54, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, - 0x61, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, - 0x6e, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x64, 0x62, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, - 0x64, 0x62, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, - 0x72, 0x70, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x67, 0x5f, 0x6b, 0x65, 0x79, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x67, 0x4b, 0x65, 0x79, 0x12, 0x1b, 0x0a, - 0x09, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x6c, - 0x75, 0x78, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x6c, 0x75, 0x78, 0x22, 0xb0, - 0x02, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, + 0x28, 0x09, 0x52, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x43, + 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x44, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x49, 0x44, 0x12, 0x1c, 0x0a, 0x09, 0x41, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x28, 0x0a, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0f, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, + 0x73, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x61, 0x73, 0x68, + 0x62, 0x6f, 0x61, 0x72, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x05, 0x63, 0x65, 0x6c, + 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2e, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x43, 0x65, 0x6c, + 0x6c, 0x52, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x30, 0x0a, 0x09, 0x74, 0x65, 0x6d, 0x70, + 0x6c, 0x61, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x52, + 0x09, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x4f, 0x72, + 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x92, + 0x05, 0x0a, 0x0d, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x43, 0x65, 0x6c, 0x6c, + 0x12, 0x0c, 0x0a, 0x01, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x78, 0x12, 0x0c, + 0x0a, 0x01, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x79, 0x12, 0x0c, 0x0a, 0x01, + 0x77, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x77, 0x12, 0x0c, 0x0a, 0x01, 0x68, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x68, 0x12, 0x29, 0x0a, 0x07, 0x71, 0x75, 0x65, 0x72, + 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x07, 0x71, 0x75, 0x65, 0x72, + 0x69, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, + 0x44, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x35, 0x0a, 0x04, 0x61, + 0x78, 0x65, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x43, 0x65, + 0x6c, 0x6c, 0x2e, 0x41, 0x78, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x61, 0x78, + 0x65, 0x73, 0x12, 0x27, 0x0a, 0x06, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x73, 0x18, 0x0a, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x43, 0x6f, + 0x6c, 0x6f, 0x72, 0x52, 0x06, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x73, 0x12, 0x28, 0x0a, 0x06, 0x6c, + 0x65, 0x67, 0x65, 0x6e, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x4c, 0x65, 0x67, 0x65, 0x6e, 0x64, 0x52, 0x06, 0x6c, + 0x65, 0x67, 0x65, 0x6e, 0x64, 0x12, 0x3a, 0x0a, 0x0c, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x4f, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x4f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x52, 0x0c, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x12, 0x3c, 0x0a, 0x0c, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x65, 0x6c, + 0x64, 0x52, 0x0c, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, + 0x1e, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x0e, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, + 0x3d, 0x0a, 0x0d, 0x64, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x73, + 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2e, 0x44, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x52, + 0x0d, 0x64, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x12, 0x12, + 0x0a, 0x04, 0x6e, 0x6f, 0x74, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x6f, + 0x74, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x6f, 0x74, 0x65, 0x56, 0x69, 0x73, 0x69, 0x62, 0x69, + 0x6c, 0x69, 0x74, 0x79, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x6f, 0x74, 0x65, + 0x56, 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x1a, 0x47, 0x0a, 0x09, 0x41, 0x78, + 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2e, 0x41, 0x78, 0x69, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x22, 0x47, 0x0a, 0x0d, 0x44, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x50, 0x6c, + 0x61, 0x63, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x73, 0x45, 0x6e, 0x66, 0x6f, 0x72, 0x63, + 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, 0x45, 0x6e, 0x66, 0x6f, + 0x72, 0x63, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x67, 0x69, 0x74, 0x73, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x64, 0x69, 0x67, 0x69, 0x74, 0x73, 0x22, 0xbc, 0x01, 0x0a, + 0x0c, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2a, 0x0a, + 0x10, 0x76, 0x65, 0x72, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x41, 0x78, 0x69, + 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x76, 0x65, 0x72, 0x74, 0x69, 0x63, 0x61, + 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x41, 0x78, 0x69, 0x73, 0x12, 0x30, 0x0a, 0x06, 0x73, 0x6f, 0x72, + 0x74, 0x42, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, + 0x65, 0x6c, 0x64, 0x52, 0x06, 0x73, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x77, + 0x72, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, + 0x72, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x26, 0x0a, 0x0e, 0x66, 0x69, 0x78, 0x46, 0x69, + 0x72, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0e, 0x66, 0x69, 0x78, 0x46, 0x69, 0x72, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x4a, + 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x22, 0x70, 0x0a, 0x0e, 0x52, + 0x65, 0x6e, 0x61, 0x6d, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x22, 0x0a, + 0x0c, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0c, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x4e, 0x61, 0x6d, + 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, + 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x22, 0x67, 0x0a, + 0x05, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x48, 0x65, + 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x48, 0x65, 0x78, 0x12, 0x12, 0x0a, 0x04, + 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x3e, 0x0a, 0x06, 0x4c, 0x65, 0x67, 0x65, 0x6e, 0x64, + 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x4f, 0x72, 0x69, 0x65, 0x6e, 0x74, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x4f, 0x72, 0x69, 0x65, 0x6e, + 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xb2, 0x01, 0x0a, 0x04, 0x41, 0x78, 0x69, 0x73, 0x12, + 0x22, 0x0a, 0x0c, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x03, 0x52, 0x0c, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x42, 0x6f, 0x75, + 0x6e, 0x64, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x06, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6c, + 0x61, 0x62, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, + 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x75, 0x66, + 0x66, 0x69, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x75, 0x66, 0x66, 0x69, + 0x78, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x61, 0x73, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x62, 0x61, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x22, 0xdb, 0x01, 0x0a, 0x08, + 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, + 0x5f, 0x76, 0x61, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x74, 0x65, 0x6d, 0x70, + 0x56, 0x61, 0x72, 0x12, 0x2f, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x54, + 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, + 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x2d, + 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, + 0x65, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x1a, 0x0a, + 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x44, 0x22, 0x67, 0x0a, 0x0d, 0x54, 0x65, 0x6d, + 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x22, 0xb5, 0x01, 0x0a, 0x0d, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x51, + 0x75, 0x65, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x0e, + 0x0a, 0x02, 0x64, 0x62, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x64, 0x62, 0x12, 0x0e, + 0x0a, 0x02, 0x72, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x72, 0x70, 0x12, 0x20, + 0x0a, 0x0b, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x67, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x74, 0x61, 0x67, 0x4b, 0x65, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x65, + 0x6c, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, + 0x65, 0x6c, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x6c, 0x75, 0x78, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x6c, 0x75, 0x78, 0x22, 0xb0, 0x02, 0x0a, 0x06, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x55, 0x73, 0x65, + 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x55, 0x73, 0x65, + 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, + 0x64, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x55, 0x52, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x72, 0x63, 0x49, 0x44, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x05, 0x53, 0x72, 0x63, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x41, 0x63, 0x74, + 0x69, 0x76, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x76, + 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x12, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, + 0x65, 0x53, 0x6b, 0x69, 0x70, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x12, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x53, 0x6b, 0x69, 0x70, 0x56, + 0x65, 0x72, 0x69, 0x66, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x4a, 0x53, 0x4f, 0x4e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4a, 0x53, 0x4f, 0x4e, 0x22, 0x9e, 0x01, + 0x0a, 0x06, 0x4c, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x20, 0x0a, 0x0b, 0x41, 0x70, 0x70, 0x6c, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x41, + 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x4d, 0x65, + 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x4d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x05, + 0x43, 0x65, 0x6c, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x43, 0x65, 0x6c, 0x6c, 0x52, 0x05, 0x43, 0x65, 0x6c, + 0x6c, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x74, 0x6f, 0x66, 0x6c, 0x6f, 0x77, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x41, 0x75, 0x74, 0x6f, 0x66, 0x6c, 0x6f, 0x77, 0x22, 0xca, + 0x02, 0x0a, 0x04, 0x43, 0x65, 0x6c, 0x6c, 0x12, 0x0c, 0x0a, 0x01, 0x78, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x01, 0x78, 0x12, 0x0c, 0x0a, 0x01, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x01, 0x79, 0x12, 0x0c, 0x0a, 0x01, 0x77, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, + 0x77, 0x12, 0x0c, 0x0a, 0x01, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x68, 0x12, + 0x29, 0x0a, 0x07, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x0f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x51, 0x75, 0x65, 0x72, + 0x79, 0x52, 0x07, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x12, 0x0c, 0x0a, 0x01, 0x69, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x01, 0x69, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, + 0x79, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x03, 0x52, 0x07, 0x79, + 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x79, 0x6c, 0x61, 0x62, 0x65, 0x6c, + 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x79, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, + 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x12, 0x2c, 0x0a, 0x04, 0x61, 0x78, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x43, 0x65, + 0x6c, 0x6c, 0x2e, 0x41, 0x78, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x61, 0x78, + 0x65, 0x73, 0x1a, 0x47, 0x0a, 0x09, 0x41, 0x78, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, + 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x0e, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x41, 0x78, 0x69, 0x73, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x8b, 0x02, 0x0a, 0x05, + 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, + 0x0e, 0x0a, 0x02, 0x44, 0x42, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x44, 0x42, 0x12, + 0x0e, 0x0a, 0x02, 0x52, 0x50, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x52, 0x50, 0x12, + 0x1a, 0x0a, 0x08, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x08, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x57, + 0x68, 0x65, 0x72, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x57, 0x68, 0x65, + 0x72, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x25, 0x0a, 0x05, 0x52, 0x61, 0x6e, + 0x67, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, + 0x12, 0x16, 0x0a, 0x06, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x53, 0x68, 0x69, 0x66, + 0x74, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x68, 0x69, 0x66, 0x74, 0x52, 0x06, 0x53, + 0x68, 0x69, 0x66, 0x74, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x22, 0x51, 0x0a, 0x09, 0x54, 0x69, 0x6d, + 0x65, 0x53, 0x68, 0x69, 0x66, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, + 0x55, 0x6e, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x55, 0x6e, 0x69, 0x74, + 0x12, 0x1a, 0x0a, 0x08, 0x51, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x51, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0x33, 0x0a, 0x05, + 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x55, 0x70, 0x70, 0x65, 0x72, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x55, 0x70, 0x70, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x4c, + 0x6f, 0x77, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x4c, 0x6f, 0x77, 0x65, + 0x72, 0x22, 0x5d, 0x0a, 0x09, 0x41, 0x6c, 0x65, 0x72, 0x74, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x0e, + 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, + 0x0a, 0x04, 0x4a, 0x53, 0x4f, 0x4e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4a, 0x53, + 0x4f, 0x4e, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x72, 0x63, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x05, 0x53, 0x72, 0x63, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x4b, 0x61, 0x70, 0x61, + 0x49, 0x44, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4b, 0x61, 0x70, 0x61, 0x49, 0x44, + 0x22, 0xa4, 0x01, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, - 0x08, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x61, 0x73, - 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x61, 0x73, - 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x72, 0x63, 0x49, 0x44, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x53, 0x72, 0x63, 0x49, 0x44, 0x12, 0x16, 0x0a, - 0x06, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x41, - 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4f, 0x72, 0x67, - 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x12, 0x49, 0x6e, 0x73, - 0x65, 0x63, 0x75, 0x72, 0x65, 0x53, 0x6b, 0x69, 0x70, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x18, - 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x53, - 0x6b, 0x69, 0x70, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, - 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x22, 0x0a, - 0x0c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4a, 0x53, 0x4f, 0x4e, 0x18, 0x0b, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4a, 0x53, 0x4f, - 0x4e, 0x22, 0x9e, 0x01, 0x0a, 0x06, 0x4c, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x12, 0x0e, 0x0a, 0x02, - 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x20, 0x0a, 0x0b, - 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, - 0x0a, 0x0b, 0x4d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0b, 0x4d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x12, 0x24, 0x0a, 0x05, 0x43, 0x65, 0x6c, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x0e, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x43, 0x65, 0x6c, 0x6c, 0x52, - 0x05, 0x43, 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x74, 0x6f, 0x66, 0x6c, - 0x6f, 0x77, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x41, 0x75, 0x74, 0x6f, 0x66, 0x6c, - 0x6f, 0x77, 0x22, 0xca, 0x02, 0x0a, 0x04, 0x43, 0x65, 0x6c, 0x6c, 0x12, 0x0c, 0x0a, 0x01, 0x78, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x78, 0x12, 0x0c, 0x0a, 0x01, 0x79, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x79, 0x12, 0x0c, 0x0a, 0x01, 0x77, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x01, 0x77, 0x12, 0x0c, 0x0a, 0x01, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x01, 0x68, 0x12, 0x29, 0x0a, 0x07, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x18, 0x05, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, - 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x07, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x12, 0x0c, - 0x0a, 0x01, 0x69, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x01, 0x69, 0x12, 0x12, 0x0a, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x12, 0x18, 0x0a, 0x07, 0x79, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, - 0x03, 0x52, 0x07, 0x79, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x79, 0x6c, - 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x79, 0x6c, 0x61, - 0x62, 0x65, 0x6c, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0a, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2c, 0x0a, 0x04, 0x61, 0x78, 0x65, 0x73, - 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x2e, 0x43, 0x65, 0x6c, 0x6c, 0x2e, 0x41, 0x78, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x04, 0x61, 0x78, 0x65, 0x73, 0x1a, 0x47, 0x0a, 0x09, 0x41, 0x78, 0x65, 0x73, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, - 0x41, 0x78, 0x69, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, - 0x8b, 0x02, 0x0a, 0x05, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x43, 0x6f, 0x6d, - 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x43, 0x6f, 0x6d, 0x6d, - 0x61, 0x6e, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x44, 0x42, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x02, 0x44, 0x42, 0x12, 0x0e, 0x0a, 0x02, 0x52, 0x50, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x02, 0x52, 0x50, 0x12, 0x1a, 0x0a, 0x08, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x73, 0x18, - 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x73, 0x12, - 0x16, 0x0a, 0x06, 0x57, 0x68, 0x65, 0x72, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x06, 0x57, 0x68, 0x65, 0x72, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x25, 0x0a, - 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x05, 0x52, - 0x61, 0x6e, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x08, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x2b, 0x0a, 0x06, - 0x53, 0x68, 0x69, 0x66, 0x74, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x68, 0x69, 0x66, - 0x74, 0x52, 0x06, 0x53, 0x68, 0x69, 0x66, 0x74, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, - 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x22, 0x51, 0x0a, - 0x09, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x68, 0x69, 0x66, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x4c, 0x61, - 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, - 0x12, 0x12, 0x0a, 0x04, 0x55, 0x6e, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x55, 0x6e, 0x69, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x51, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x51, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, - 0x22, 0x33, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x55, 0x70, 0x70, - 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x55, 0x70, 0x70, 0x65, 0x72, 0x12, - 0x14, 0x0a, 0x05, 0x4c, 0x6f, 0x77, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, - 0x4c, 0x6f, 0x77, 0x65, 0x72, 0x22, 0x5d, 0x0a, 0x09, 0x41, 0x6c, 0x65, 0x72, 0x74, 0x52, 0x75, - 0x6c, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, - 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4a, 0x53, 0x4f, 0x4e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x4a, 0x53, 0x4f, 0x4e, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x72, 0x63, 0x49, 0x44, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x53, 0x72, 0x63, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, - 0x4b, 0x61, 0x70, 0x61, 0x49, 0x44, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4b, 0x61, - 0x70, 0x61, 0x49, 0x44, 0x22, 0xa4, 0x01, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x0e, 0x0a, - 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, - 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, - 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, - 0x06, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x53, - 0x63, 0x68, 0x65, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x05, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x05, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, - 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x05, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x53, - 0x75, 0x70, 0x65, 0x72, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0a, 0x53, 0x75, 0x70, 0x65, 0x72, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x22, 0x3e, 0x0a, 0x04, 0x52, - 0x6f, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0xa5, 0x01, 0x0a, 0x07, - 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x12, 0x32, 0x0a, 0x14, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x50, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, - 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, + 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x63, 0x68, + 0x65, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x53, 0x63, 0x68, 0x65, 0x6d, + 0x65, 0x12, 0x24, 0x0a, 0x05, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x0e, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x52, 0x6f, 0x6c, 0x65, + 0x52, 0x05, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x53, 0x75, 0x70, 0x65, 0x72, + 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x53, 0x75, 0x70, + 0x65, 0x72, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x22, 0x3e, 0x0a, 0x04, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x22, 0x54, 0x0a, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x44, 0x65, 0x66, 0x61, 0x75, - 0x6c, 0x74, 0x52, 0x6f, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x44, 0x65, - 0x66, 0x61, 0x75, 0x6c, 0x74, 0x52, 0x6f, 0x6c, 0x65, 0x22, 0x32, 0x0a, 0x06, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x28, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x14, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x41, 0x75, 0x74, - 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x04, 0x41, 0x75, 0x74, 0x68, 0x22, 0x3c, 0x0a, - 0x0a, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2e, 0x0a, 0x12, 0x53, - 0x75, 0x70, 0x65, 0x72, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x4e, 0x65, 0x77, 0x55, 0x73, 0x65, 0x72, - 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x53, 0x75, 0x70, 0x65, 0x72, 0x41, 0x64, - 0x6d, 0x69, 0x6e, 0x4e, 0x65, 0x77, 0x55, 0x73, 0x65, 0x72, 0x73, 0x22, 0x75, 0x0a, 0x12, 0x4f, - 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x12, 0x26, 0x0a, 0x0e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x4f, 0x72, 0x67, 0x61, 0x6e, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x37, 0x0a, 0x09, 0x4c, 0x6f, 0x67, - 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, - 0x65, 0x72, 0x22, 0x46, 0x0a, 0x0f, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x33, 0x0a, 0x07, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x2e, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x43, 0x6f, 0x6c, 0x75, 0x6d, - 0x6e, 0x52, 0x07, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x22, 0x79, 0x0a, 0x0f, 0x4c, 0x6f, - 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x12, 0x12, 0x0a, - 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, - 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x08, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x0a, - 0x09, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x18, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x43, 0x6f, 0x6c, 0x75, - 0x6d, 0x6e, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x09, 0x45, 0x6e, 0x63, 0x6f, - 0x64, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x4e, 0x0a, 0x0e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x45, - 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3d, 0x0a, 0x09, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, - 0x66, 0x6f, 0x12, 0x18, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, - 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x43, 0x6f, - 0x6d, 0x6d, 0x69, 0x74, 0x42, 0x0c, 0x5a, 0x0a, 0x2e, 0x3b, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0xa5, 0x01, 0x0a, 0x07, 0x4d, 0x61, 0x70, 0x70, + 0x69, 0x6e, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, + 0x16, 0x0a, 0x06, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x12, 0x32, 0x0a, 0x14, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4f, + 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x49, + 0x44, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x4f, + 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, + 0x54, 0x0a, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, + 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, + 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x52, 0x6f, + 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, + 0x74, 0x52, 0x6f, 0x6c, 0x65, 0x22, 0x32, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x28, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x04, 0x41, 0x75, 0x74, 0x68, 0x22, 0x3c, 0x0a, 0x0a, 0x41, 0x75, 0x74, + 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2e, 0x0a, 0x12, 0x53, 0x75, 0x70, 0x65, 0x72, + 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x4e, 0x65, 0x77, 0x55, 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x12, 0x53, 0x75, 0x70, 0x65, 0x72, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x4e, + 0x65, 0x77, 0x55, 0x73, 0x65, 0x72, 0x73, 0x22, 0x75, 0x0a, 0x12, 0x4f, 0x72, 0x67, 0x61, 0x6e, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x26, 0x0a, + 0x0e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x37, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, + 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2e, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x09, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x22, 0x46, + 0x0a, 0x0f, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x33, 0x0a, 0x07, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x4c, 0x6f, + 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x52, 0x07, 0x43, + 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x22, 0x79, 0x0a, 0x0f, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, + 0x77, 0x65, 0x72, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, + 0x08, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x08, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x09, 0x45, 0x6e, 0x63, + 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x45, 0x6e, + 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x09, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, + 0x73, 0x22, 0x4e, 0x0a, 0x0e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x45, 0x6e, 0x63, 0x6f, 0x64, + 0x69, 0x6e, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, + 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, + 0x65, 0x22, 0x3d, 0x0a, 0x09, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x18, + 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x43, 0x6f, 0x6d, 0x6d, + 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, + 0x42, 0x0c, 0x5a, 0x0a, 0x2e, 0x3b, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/kv/internal/internal.proto b/kv/internal/internal.proto index c12a9a2e36..2f822b3de9 100644 --- a/kv/internal/internal.proto +++ b/kv/internal/internal.proto @@ -18,6 +18,10 @@ message Source { string Role = 13; // Role is the name of the miniumum role that a user must possess to access the resource string DefaultRP = 14; // DefaultRP is the default retention policy used in database queries to this source string Version = 15; // Version of the InfluxDB or Unknown + string ClusterID = 16; // Cluster ID of an InfluxDB Cloud Dedicated source + string AccountID = 17; // Account ID of an InfluxDB Cloud Dedicated source + string ManagementToken = 18; // Management token of an InfluxDB Cloud Dedicated source + string DatabaseToken = 19; // Database token of an InfluxDB Cloud Dedicated source } message Dashboard { diff --git a/server/sources.go b/server/sources.go index 6908e37d28..132f24ce0f 100644 --- a/server/sources.go +++ b/server/sources.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/google/uuid" "github.com/influxdata/chronograf/enterprise" "github.com/influxdata/chronograf/flux" @@ -77,6 +78,10 @@ func hasFlux(ctx context.Context, src chronograf.Source) (bool, error) { if src.Version == "" /* v2 OSS reports no version */ || strings.HasPrefix(src.Version, "2.") { return src.Type == chronograf.InfluxDBv2 && src.Username != "", nil } + if src.Type == chronograf.InfluxDBCloudDedicated { + // InfluxDB 3 doesn't support Flux. + return false, nil + } url, err := url.ParseRequestURI(src.URL) if err != nil { @@ -222,8 +227,8 @@ func (s *Service) tsdbVersion(ctx context.Context, src *chronograf.Source) (stri } func (s *Service) tsdbType(ctx context.Context, src *chronograf.Source) (string, error) { - if src.Type == chronograf.InfluxDBv2 { - return chronograf.InfluxDBv2, nil // v2 selected by the user + if src.Type == chronograf.InfluxDBv2 || src.Type == chronograf.InfluxDBCloudDedicated { + return src.Type, nil // type selected by the user } cli := &influx.Client{ Logger: s.Logger, @@ -490,6 +495,7 @@ func ValidSourceRequest(s *chronograf.Source, defaultOrgID string) error { if s.Type != "" { if s.Type != chronograf.InfluxDB && s.Type != chronograf.InfluxDBv2 && + s.Type != chronograf.InfluxDBCloudDedicated && s.Type != chronograf.InfluxEnterprise && s.Type != chronograf.InfluxRelay { return fmt.Errorf("invalid source type %s", s.Type) @@ -508,6 +514,27 @@ func ValidSourceRequest(s *chronograf.Source, defaultOrgID string) error { return fmt.Errorf("invalid URL; no URL scheme defined") } + if s.Type == chronograf.InfluxDBCloudDedicated { + if len(s.ClusterID) == 0 { + return fmt.Errorf("cluster ID required") + } + if _, err := uuid.Parse(s.ClusterID); err != nil { + return fmt.Errorf("cluster ID is not a valid UUID") + } + if len(s.AccountID) == 0 { + return fmt.Errorf("account ID required") + } + if _, err := uuid.Parse(s.AccountID); err != nil { + return fmt.Errorf("account ID is not a valid UUID") + } + if len(s.ManagementToken) == 0 { + return fmt.Errorf("management token required") + } + if len(s.DatabaseToken) == 0 { + return fmt.Errorf("database token required") + } + } + return nil } diff --git a/ui/src/shared/constants/index.ts b/ui/src/shared/constants/index.ts index 201da8d672..5293eddf37 100644 --- a/ui/src/shared/constants/index.ts +++ b/ui/src/shared/constants/index.ts @@ -526,6 +526,7 @@ export const QUERY_BUILDER_LIST_ITEM_HEIGHT = 28 export const SOURCE_TYPE_INFLUX_V1 = 'influx' export const SOURCE_TYPE_INFLUX_V2 = 'influx-v2' +export const SOURCE_TYPE_INFLUX_CLOUD_DEDICATED = 'influx-cloud-dedicated' export enum DataType { flux = 'flux', diff --git a/ui/src/sources/components/SourceStep.tsx b/ui/src/sources/components/SourceStep.tsx index 782e916797..4e60bb0b69 100644 --- a/ui/src/sources/components/SourceStep.tsx +++ b/ui/src/sources/components/SourceStep.tsx @@ -31,6 +31,7 @@ import { import {insecureSkipVerifyText} from 'src/shared/copy/tooltipText' import { DEFAULT_SOURCE, + SOURCE_TYPE_INFLUX_CLOUD_DEDICATED, SOURCE_TYPE_INFLUX_V2, SOURCE_TYPE_INFLUX_V1, } from 'src/shared/constants' @@ -43,6 +44,8 @@ import {NextReturn} from 'src/types/wizard' const isNewSource = (source: Partial) => !source.id const isV2Auth = (source: Partial) => source.type && source.type === SOURCE_TYPE_INFLUX_V2 +const isCD = (source: Partial) => + source.type && source.type === SOURCE_TYPE_INFLUX_CLOUD_DEDICATED interface Props { notify: typeof notifyAction @@ -105,6 +108,7 @@ class SourceStep extends PureComponent { const {source} = this.state const {isUsingAuth, onBoarding} = this.props const sourceIsV2 = isV2Auth(source) + const sourceIsCD = isCD(source) return ( <> @@ -123,22 +127,55 @@ class SourceStep extends PureComponent { onChange={this.onChangeInput('name')} testId="connection-name--input" /> - - + {!sourceIsCD && ( + <> + + + + )} + + {/* InfluxDB Cloud Dedicated fields */} + {sourceIsCD && ( + <> + + + + + + )} + { )} {!onBoarding && ( { /> )} + {this.isHTTPS && ( @@ -236,18 +279,36 @@ class SourceStep extends PureComponent { this.setState({source: {...source, [key]: value}}) setError(false) } - private changeAuth = (v2: boolean) => { + private changeSourceType = (type: string, version: string) => { const {source} = this.state this.setState({ source: { ...source, username: '', password: '', - type: v2 ? SOURCE_TYPE_INFLUX_V2 : SOURCE_TYPE_INFLUX_V1, - version: v2 ? '2.x' : '1.x', + clusterId: '', + accountId: '', + managementToken: '', + databaseToken: '', + type, + version, }, }) } + private changeV2Auth = (v2: boolean) => { + if (v2) { + this.changeSourceType(SOURCE_TYPE_INFLUX_V2, '2.x') + } else { + this.changeSourceType(SOURCE_TYPE_INFLUX_V1, '1.x') + } + } + private changeCD = (cd: boolean) => { + if (cd) { + this.changeSourceType(SOURCE_TYPE_INFLUX_CLOUD_DEDICATED, 'cloud') + } else { + this.changeSourceType(SOURCE_TYPE_INFLUX_V1, '1.x') + } + } private handleSubmitUrl = (url: string) => this.detectServerType({url}) private handleSubmitUsername = (username: string) => diff --git a/ui/src/types/sources.ts b/ui/src/types/sources.ts index 8962e31d89..7429ee404d 100644 --- a/ui/src/types/sources.ts +++ b/ui/src/types/sources.ts @@ -16,6 +16,10 @@ export interface Source { username?: string password?: string sharedSecret?: string + clusterId?: string + accountId?: string + managementToken?: string + databaseToken?: string url: string metaUrl?: string insecureSkipVerify: boolean From 651c8a93a063f20066dcb6ae846284718676fb70 Mon Sep 17 00:00:00 2001 From: Jan Simon Date: Wed, 2 Jul 2025 11:14:44 +0200 Subject: [PATCH 02/50] test: add tests for cloud dedicated source --- kv/internal/internal_test.go | 126 ++++++++++++----------- server/sources_test.go | 187 +++++++++++++++++++++++++++++++++++ 2 files changed, 257 insertions(+), 56 deletions(-) diff --git a/kv/internal/internal_test.go b/kv/internal/internal_test.go index eb4cca903e..5be85f4632 100644 --- a/kv/internal/internal_test.go +++ b/kv/internal/internal_test.go @@ -10,67 +10,81 @@ import ( ) func TestMarshalSource(t *testing.T) { - v := chronograf.Source{ - ID: 12, - Name: "Fountain of Truth", - Type: "influx", - Username: "docbrown", - Password: "1 point twenty-one g1g@w@tts", - URL: "http://twin-pines.mall.io:8086", - MetaURL: "http://twin-pines.meta.io:8086", - Default: true, - Telegraf: "telegraf", + tests := []struct { + name string + src chronograf.Source + }{ + { + name: "Source with Password", + src: chronograf.Source{ + ID: 12, + Name: "Fountain of Truth", + Type: "influx", + Username: "docbrown", + Password: "1 point twenty-one g1g@w@tts", + URL: "http://twin-pines.mall.io:8086", + MetaURL: "http://twin-pines.meta.io:8086", + Default: true, + Telegraf: "telegraf", + }, + }, + { + name: "Source with Shared Secret", + src: chronograf.Source{ + ID: 12, + Name: "Fountain of Truth", + Type: "influx", + Username: "docbrown", + SharedSecret: "hunter2s", + URL: "http://twin-pines.mall.io:8086", + MetaURL: "http://twin-pines.meta.io:8086", + Default: true, + Telegraf: "telegraf", + }, + }, + { + name: "Source for Cloud Dedicated", + src: chronograf.Source{ + ID: 12, + Name: "Fountain of Truth", + Type: "influx-cloud-dedicated", + ClusterID: "3F762A1F-B609-4E7A-9657-8F0A39C27A58", + AccountID: "27F924B3-FF40-47B1-B587-3AB980B87EF4", + ManagementToken: "mgmt-token", + DatabaseToken: "database-token", + URL: "http://twin-pines.mall.io:8086", + MetaURL: "http://twin-pines.meta.io:8086", + Default: true, + Telegraf: "telegraf", + }, + }, } - var vv chronograf.Source - if buf, err := internal.MarshalSource(v); err != nil { - t.Fatal(err) - } else if err := internal.UnmarshalSource(buf, &vv); err != nil { - t.Fatal(err) - } else if !reflect.DeepEqual(v, vv) { - t.Fatalf("source protobuf copy error: got %#v, expected %#v", vv, v) - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := tt.src - // Test if the new insecureskipverify works - v.InsecureSkipVerify = true - if buf, err := internal.MarshalSource(v); err != nil { - t.Fatal(err) - } else if err := internal.UnmarshalSource(buf, &vv); err != nil { - t.Fatal(err) - } else if !reflect.DeepEqual(v, vv) { - t.Fatalf("source protobuf copy error: got %#v, expected %#v", vv, v) - } -} -func TestMarshalSourceWithSecret(t *testing.T) { - v := chronograf.Source{ - ID: 12, - Name: "Fountain of Truth", - Type: "influx", - Username: "docbrown", - SharedSecret: "hunter2s", - URL: "http://twin-pines.mall.io:8086", - MetaURL: "http://twin-pines.meta.io:8086", - Default: true, - Telegraf: "telegraf", - } + // Test initial marshal/unmarshal. + var vv chronograf.Source + if buf, err := internal.MarshalSource(v); err != nil { + t.Fatalf("failed to marshal source: %v", err) + } else if err := internal.UnmarshalSource(buf, &vv); err != nil { + t.Fatalf("failed to unmarshal source: %v", err) + } else if !reflect.DeepEqual(v, vv) { + t.Fatalf("source protobuf copy error: got %#v, expected %#v", vv, v) + } - var vv chronograf.Source - if buf, err := internal.MarshalSource(v); err != nil { - t.Fatal(err) - } else if err := internal.UnmarshalSource(buf, &vv); err != nil { - t.Fatal(err) - } else if !reflect.DeepEqual(v, vv) { - t.Fatalf("source protobuf copy error: got %#v, expected %#v", vv, v) - } + // Test with InsecureSkipVerify set to true. + v.InsecureSkipVerify = true - // Test if the new insecureskipverify works - v.InsecureSkipVerify = true - if buf, err := internal.MarshalSource(v); err != nil { - t.Fatal(err) - } else if err := internal.UnmarshalSource(buf, &vv); err != nil { - t.Fatal(err) - } else if !reflect.DeepEqual(v, vv) { - t.Fatalf("source protobuf copy error: got %#v, expected %#v", vv, v) + if buf, err := internal.MarshalSource(v); err != nil { + t.Fatalf("failed to marshal source with InsecureSkipVerify: %v", err) + } else if err := internal.UnmarshalSource(buf, &vv); err != nil { + t.Fatalf("failed to unmarshal source with InsecureSkipVerify: %v", err) + } else if !reflect.DeepEqual(v, vv) { + t.Fatalf("source protobuf copy error with InsecureSkipVerify: got %#v, expected %#v", vv, v) + } + }) } } diff --git a/server/sources_test.go b/server/sources_test.go index 8d1dc874dc..77f2354a3d 100644 --- a/server/sources_test.go +++ b/server/sources_test.go @@ -150,6 +150,193 @@ func Test_ValidSourceRequest(t *testing.T) { }, }, }, + { + name: "support InfluxDB Cloud Dedicated type", + args: args{ + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBCloudDedicated, + Username: "", + Password: "", + SharedSecret: "supersecret", + ClusterID: "3F762A1F-B609-4E7A-9657-8F0A39C27A58", + AccountID: "27F924B3-FF40-47B1-B587-3AB980B87EF4", + ManagementToken: "mgmt-token", + DatabaseToken: "database-token", + URL: "http://www.any.url.com", + MetaURL: "http://www.so.meta.com", + InsecureSkipVerify: true, + Default: true, + Telegraf: "telegraf", + }, + }, + wants: wants{ + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBCloudDedicated, + Username: "", + Password: "", + SharedSecret: "supersecret", + ClusterID: "3F762A1F-B609-4E7A-9657-8F0A39C27A58", + AccountID: "27F924B3-FF40-47B1-B587-3AB980B87EF4", + ManagementToken: "mgmt-token", + DatabaseToken: "database-token", + URL: "http://www.any.url.com", + MetaURL: "http://www.so.meta.com", + InsecureSkipVerify: true, + Default: true, + Telegraf: "telegraf", + }, + }, + }, + { + name: "InfluxDB Cloud Dedicated - missing cluster ID", + args: args{ + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBCloudDedicated, + Username: "", + Password: "", + SharedSecret: "supersecret", + AccountID: "27F924B3-FF40-47B1-B587-3AB980B87EF4", + ManagementToken: "mgmt-token", + DatabaseToken: "database-token", + URL: "http://www.any.url.com", + MetaURL: "http://www.so.meta.com", + InsecureSkipVerify: true, + Default: true, + Telegraf: "telegraf", + }, + }, + wants: wants{ + err: fmt.Errorf("cluster ID required"), + }, + }, + { + name: "InfluxDB Cloud Dedicated - invalid cluster ID", + args: args{ + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBCloudDedicated, + Username: "", + Password: "", + SharedSecret: "supersecret", + ClusterID: "not-a-uuid", + AccountID: "27F924B3-FF40-47B1-B587-3AB980B87EF4", + ManagementToken: "mgmt-token", + DatabaseToken: "database-token", + URL: "http://www.any.url.com", + MetaURL: "http://www.so.meta.com", + InsecureSkipVerify: true, + Default: true, + Telegraf: "telegraf", + }, + }, + wants: wants{ + err: fmt.Errorf("cluster ID is not a valid UUID"), + }, + }, + { + name: "InfluxDB Cloud Dedicated - missing account ID", + args: args{ + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBCloudDedicated, + Username: "", + Password: "", + SharedSecret: "supersecret", + ClusterID: "3F762A1F-B609-4E7A-9657-8F0A39C27A58", + ManagementToken: "mgmt-token", + DatabaseToken: "database-token", + URL: "http://www.any.url.com", + MetaURL: "http://www.so.meta.com", + InsecureSkipVerify: true, + Default: true, + Telegraf: "telegraf", + }, + }, + wants: wants{ + err: fmt.Errorf("account ID required"), + }, + }, + { + name: "InfluxDB Cloud Dedicated - invalid account ID", + args: args{ + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBCloudDedicated, + Username: "", + Password: "", + SharedSecret: "supersecret", + ClusterID: "3F762A1F-B609-4E7A-9657-8F0A39C27A58", + AccountID: "not-a-uuid", + ManagementToken: "mgmt-token", + DatabaseToken: "database-token", + URL: "http://www.any.url.com", + MetaURL: "http://www.so.meta.com", + InsecureSkipVerify: true, + Default: true, + Telegraf: "telegraf", + }, + }, + wants: wants{ + err: fmt.Errorf("account ID is not a valid UUID"), + }, + }, + { + name: "InfluxDB Cloud Dedicated - missing management token", + args: args{ + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBCloudDedicated, + Username: "", + Password: "", + SharedSecret: "supersecret", + ClusterID: "3F762A1F-B609-4E7A-9657-8F0A39C27A58", + AccountID: "27F924B3-FF40-47B1-B587-3AB980B87EF4", + DatabaseToken: "database-token", + URL: "http://www.any.url.com", + MetaURL: "http://www.so.meta.com", + InsecureSkipVerify: true, + Default: true, + Telegraf: "telegraf", + }, + }, + wants: wants{ + err: fmt.Errorf("management token required"), + }, + }, + { + name: "InfluxDB Cloud Dedicated - missing database token", + args: args{ + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBCloudDedicated, + Username: "", + Password: "", + SharedSecret: "supersecret", + ClusterID: "3F762A1F-B609-4E7A-9657-8F0A39C27A58", + AccountID: "27F924B3-FF40-47B1-B587-3AB980B87EF4", + ManagementToken: "mgmt-token", + URL: "http://www.any.url.com", + MetaURL: "http://www.so.meta.com", + InsecureSkipVerify: true, + Default: true, + Telegraf: "telegraf", + }, + }, + wants: wants{ + err: fmt.Errorf("database token required"), + }, + }, { name: "bad url", args: args{ From ceceeab984eb05ff2cd3e35fd407b4af14b96448 Mon Sep 17 00:00:00 2001 From: Jan Simon Date: Wed, 2 Jul 2025 13:56:26 +0200 Subject: [PATCH 03/50] feat(server): allow to setup influxdb cloud dedicated on CLI --- server/builders.go | 43 +++++++++++++++++++++++++++++-------------- server/server.go | 33 ++++++++++++++++++++------------- 2 files changed, 49 insertions(+), 27 deletions(-) diff --git a/server/builders.go b/server/builders.go index 0c78c13314..6383ff1467 100644 --- a/server/builders.go +++ b/server/builders.go @@ -112,11 +112,14 @@ type SourcesBuilder interface { // MultiSourceBuilder implements SourcesBuilder type MultiSourceBuilder struct { - InfluxDBURL string - InfluxDBUsername string - InfluxDBPassword string - InfluxDBOrg string - InfluxDBToken string + InfluxDBURL string + InfluxDBUsername string + InfluxDBPassword string + InfluxDBOrg string + InfluxDBToken string + InfluxDBMgmtToken string + InfluxDBClusterID string + InfluxDBAccountID string Logger chronograf.Logger ID chronograf.ID @@ -132,7 +135,15 @@ func (fs *MultiSourceBuilder) Build(db chronograf.SourcesStore) (*multistore.Sou if fs.InfluxDBURL != "" { var influxdbType, username, password string - if fs.InfluxDBOrg == "" || fs.InfluxDBToken == "" { + var clusterID, accountID, mgmtToken, dbToken string + if fs.InfluxDBClusterID != "" && fs.InfluxDBAccountID != "" && fs.InfluxDBToken != "" && fs.InfluxDBMgmtToken != "" { + // InfluxDB Cloud Dedicated + influxdbType = chronograf.InfluxDBCloudDedicated + clusterID = fs.InfluxDBClusterID + accountID = fs.InfluxDBAccountID + mgmtToken = fs.InfluxDBMgmtToken + dbToken = fs.InfluxDBToken + } else if fs.InfluxDBOrg == "" || fs.InfluxDBToken == "" { // v1 InfluxDB username = fs.InfluxDBUsername password = fs.InfluxDBPassword @@ -146,14 +157,18 @@ func (fs *MultiSourceBuilder) Build(db chronograf.SourcesStore) (*multistore.Sou influxStore := &memdb.SourcesStore{ Source: &chronograf.Source{ - ID: 0, - Name: fs.InfluxDBURL, - Type: influxdbType, - Username: username, - Password: password, - URL: fs.InfluxDBURL, - Default: true, - Version: "unknown", // a real version is re-fetched at runtime; use "unknown" version as a fallback, empty version would imply OSS 2.x + ID: 0, + Name: fs.InfluxDBURL, + Type: influxdbType, + Username: username, + Password: password, + ClusterID: clusterID, + AccountID: accountID, + ManagementToken: mgmtToken, + DatabaseToken: dbToken, + URL: fs.InfluxDBURL, + Default: true, + Version: "unknown", // a real version is re-fetched at runtime; use "unknown" version as a fallback, empty version would imply OSS 2.x }} stores = append([]chronograf.SourcesStore{influxStore}, stores...) } diff --git a/server/server.go b/server/server.go index 153898f502..d07cf42ac4 100644 --- a/server/server.go +++ b/server/server.go @@ -58,11 +58,14 @@ type Server struct { Cert flags.Filename `long:"cert" description:"Path to PEM encoded public key certificate. " env:"TLS_CERTIFICATE"` Key flags.Filename `long:"key" description:"Path to private key associated with given certificate. " env:"TLS_PRIVATE_KEY"` - InfluxDBURL string `long:"influxdb-url" description:"Location of your InfluxDB instance" env:"INFLUXDB_URL"` - InfluxDBUsername string `long:"influxdb-username" description:"Username for your InfluxDB instance" env:"INFLUXDB_USERNAME"` - InfluxDBPassword string `long:"influxdb-password" description:"Password for your InfluxDB instance" env:"INFLUXDB_PASSWORD"` - InfluxDBOrg string `long:"influxdb-org" description:"Organization for your InfluxDB v2 instance" env:"INFLUXDB_ORG"` - InfluxDBToken string `long:"influxdb-token" description:"Token for your InfluxDB v2 instance" env:"INFLUXDB_TOKEN"` + InfluxDBURL string `long:"influxdb-url" description:"Location of your InfluxDB instance" env:"INFLUXDB_URL"` + InfluxDBUsername string `long:"influxdb-username" description:"Username for your InfluxDB instance" env:"INFLUXDB_USERNAME"` + InfluxDBPassword string `long:"influxdb-password" description:"Password for your InfluxDB instance" env:"INFLUXDB_PASSWORD"` + InfluxDBOrg string `long:"influxdb-org" description:"Organization for your InfluxDB v2 instance" env:"INFLUXDB_ORG"` + InfluxDBToken string `long:"influxdb-token" description:"Token for your InfluxDB v2 or Cloud Dedicated instance" env:"INFLUXDB_TOKEN"` + InfluxDBMgmtToken string `long:"influxdb-mgmt-token" description:"Management token for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_MGMT_TOKEN"` + InfluxDBClusterID string `long:"influxdb-cluster-id" description:"Cluster ID for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_CLUSTER_ID"` + InfluxDBAccountID string `long:"influxdb-account-id" description:"Account ID for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_ACCOUNT_ID"` KapacitorURL string `long:"kapacitor-url" description:"Location of your Kapacitor instance" env:"KAPACITOR_URL"` KapacitorUsername string `long:"kapacitor-username" description:"Username of your Kapacitor instance" env:"KAPACITOR_USERNAME"` @@ -542,14 +545,18 @@ func (s *Server) newBuilders(logger chronograf.Logger) builders { Path: s.ResourcesPath, }, Sources: &MultiSourceBuilder{ - InfluxDBURL: s.InfluxDBURL, - InfluxDBUsername: s.InfluxDBUsername, - InfluxDBPassword: s.InfluxDBPassword, - InfluxDBOrg: s.InfluxDBOrg, - InfluxDBToken: s.InfluxDBToken, - Logger: logger, - ID: idgen.NewTime(), - Path: s.ResourcesPath, + InfluxDBURL: s.InfluxDBURL, + InfluxDBUsername: s.InfluxDBUsername, + InfluxDBPassword: s.InfluxDBPassword, + InfluxDBOrg: s.InfluxDBOrg, + InfluxDBToken: s.InfluxDBToken, + InfluxDBMgmtToken: s.InfluxDBMgmtToken, + InfluxDBClusterID: s.InfluxDBClusterID, + InfluxDBAccountID: s.InfluxDBAccountID, + + Logger: logger, + ID: idgen.NewTime(), + Path: s.ResourcesPath, }, Kapacitors: &MultiKapacitorBuilder{ KapacitorURL: s.KapacitorURL, From 3b04e41a7da6c64149a47dc41446738375df47c5 Mon Sep 17 00:00:00 2001 From: Jan Simon Date: Thu, 3 Jul 2025 13:44:00 +0200 Subject: [PATCH 04/50] feat(ui): make databases page read-only --- influx/databases.go | 16 ++++++++++++---- ui/src/admin/components/DatabaseManager.js | 3 +++ ui/src/admin/components/DatabaseRow.js | 3 +++ ui/src/admin/components/DatabaseTable.js | 7 ++++++- ui/src/admin/components/DatabaseTableHeader.js | 6 ++++++ .../containers/influxdb/DatabaseManagerPage.tsx | 5 ++++- 6 files changed, 34 insertions(+), 6 deletions(-) diff --git a/influx/databases.go b/influx/databases.go index 9b2b2bf791..694e9ef9e1 100644 --- a/influx/databases.go +++ b/influx/databases.go @@ -45,10 +45,6 @@ func (c *Client) DropDB(ctx context.Context, db string) error { // AllRP returns all the retention policies for a specific database func (c *Client) AllRP(ctx context.Context, db string) ([]chronograf.RetentionPolicy, error) { - if c.SrcType == chronograf.InfluxDBCloudDedicated { - // Data retention in InfluxDB 3 is configured differently, on database level. - return []chronograf.RetentionPolicy{}, nil - } return c.showRetentionPolicies(ctx, db) } @@ -68,6 +64,10 @@ func (c *Client) getRP(ctx context.Context, db, rp string) (chronograf.Retention // CreateRP creates a retention policy for a specific database func (c *Client) CreateRP(ctx context.Context, db string, rp *chronograf.RetentionPolicy) (*chronograf.RetentionPolicy, error) { + if c.SrcType == chronograf.InfluxDBCloudDedicated { + // Data retention in InfluxDB 3 is configured differently, on database level. + return nil, fmt.Errorf("retention policies not supported in InfluxDB Cloud Dedicated") + } query := fmt.Sprintf(`CREATE RETENTION POLICY "%s" ON "%s" DURATION %s REPLICATION %d`, rp.Name, db, rp.Duration, rp.Replication) if len(rp.ShardDuration) != 0 { query = fmt.Sprintf(`%s SHARD DURATION %s`, query, rp.ShardDuration) @@ -95,6 +95,10 @@ func (c *Client) CreateRP(ctx context.Context, db string, rp *chronograf.Retenti // UpdateRP updates a specific retention policy for a specific database func (c *Client) UpdateRP(ctx context.Context, db string, rp string, upd *chronograf.RetentionPolicy) (*chronograf.RetentionPolicy, error) { + if c.SrcType == chronograf.InfluxDBCloudDedicated { + // Data retention in InfluxDB 3 is configured differently, on database level. + return nil, fmt.Errorf("retention policies not supported in InfluxDB Cloud Dedicated") + } var buffer bytes.Buffer buffer.WriteString(fmt.Sprintf(`ALTER RETENTION POLICY "%s" ON "%s"`, rp, db)) if len(upd.Duration) > 0 { @@ -147,6 +151,10 @@ func (c *Client) UpdateRP(ctx context.Context, db string, rp string, upd *chrono // DropRP removes a specific retention policy for a specific database func (c *Client) DropRP(ctx context.Context, db string, rp string) error { + if c.SrcType == chronograf.InfluxDBCloudDedicated { + // Data retention in InfluxDB 3 is configured differently, on database level. + return fmt.Errorf("retention policies not supported in InfluxDB Cloud Dedicated") + } _, err := c.Query(ctx, chronograf.Query{ Command: fmt.Sprintf(`DROP RETENTION POLICY "%s" ON "%s"`, rp, db), DB: db, diff --git a/ui/src/admin/components/DatabaseManager.js b/ui/src/admin/components/DatabaseManager.js index 273f34899c..8e9c3ed73a 100644 --- a/ui/src/admin/components/DatabaseManager.js +++ b/ui/src/admin/components/DatabaseManager.js @@ -7,6 +7,7 @@ import FancyScrollbar from 'src/shared/components/FancyScrollbar' const DatabaseManager = ({ databases, isRFDisplayed, + isDBReadOnly, isAddDBDisabled, addDatabase, onEditDatabase, @@ -52,6 +53,7 @@ const DatabaseManager = ({ key={db.links.self} database={db} isRFDisplayed={isRFDisplayed} + isDBReadOnly={isDBReadOnly} onEditDatabase={onEditDatabase} onKeyDownDatabase={onKeyDownDatabase} onCancelDatabase={onCancelDatabase} @@ -82,6 +84,7 @@ DatabaseManager.propTypes = { databases: arrayOf(shape()), addDatabase: func, isRFDisplayed: bool, + isDBReadOnly: bool, isAddDBDisabled: bool, onEditDatabase: func, onKeyDownDatabase: func, diff --git a/ui/src/admin/components/DatabaseRow.js b/ui/src/admin/components/DatabaseRow.js index ed568a4605..c817f1cc9f 100644 --- a/ui/src/admin/components/DatabaseRow.js +++ b/ui/src/admin/components/DatabaseRow.js @@ -129,6 +129,7 @@ class DatabaseRow extends Component { database, onDelete, isDeletable, + isEditable, isRFDisplayed, } = this.props const {isEditing} = this.state @@ -233,6 +234,7 @@ class DatabaseRow extends Component { +
+ {this.props.options.map(option => ( +
this.selectOption(option.value)} + > + {option.label} +
+ ))} +
+ + {subtext && {subtext}} + + ) + } + + private get buttonText(): string { + const {placeholder = 'Select option...'} = this.props + return this.selectedOption ? this.selectedOption.label : placeholder + } + + private get selectedOption(): DropdownOption | undefined { + return this.props.options.find(opt => opt.value === this.props.value) + } + + private toggleDropdown = (e: React.MouseEvent) => { + e.stopPropagation() + + if (!this.state.isOpen && this.dropdownRef.current) { + // Calculate position for fixed positioning to avoid overflow issues + const rect = this.dropdownRef.current.getBoundingClientRect() + const menuStyle: React.CSSProperties = { + position: 'fixed', + top: `${rect.bottom + 4}px`, // Include the 4px margin + left: `${rect.left}px`, + width: `${rect.width}px`, + zIndex: 10000, + } + // Set position first, then make visible in next tick + this.setState({menuStyle}, () => { + requestAnimationFrame(() => { + this.setState({isOpen: true}) + }) + }) + } else { + this.setState({isOpen: false, menuStyle: undefined}) + } + } + + private selectOption = (value: string) => { + this.props.onChange(value) + this.setState({isOpen: false, menuStyle: undefined}) + } + + private handleClickOutside = (event: MouseEvent) => { + if ( + this.dropdownRef.current && + !this.dropdownRef.current.contains(event.target as Node) + ) { + this.setState({isOpen: false, menuStyle: undefined}) + } + } + + private handleScroll = () => { + // Close dropdown when scrolling to prevent position mismatch + if (this.state.isOpen) { + this.setState({isOpen: false, menuStyle: undefined}) + } + } +} + +export default ErrorHandling(WizardDropdown) diff --git a/ui/src/shared/constants/index.ts b/ui/src/shared/constants/index.ts index 5293eddf37..b3bca3310e 100644 --- a/ui/src/shared/constants/index.ts +++ b/ui/src/shared/constants/index.ts @@ -525,6 +525,8 @@ export const MIN_SIZE = 0 export const QUERY_BUILDER_LIST_ITEM_HEIGHT = 28 export const SOURCE_TYPE_INFLUX_V1 = 'influx' +export const SOURCE_TYPE_INFLUX_ENTERPRISE = 'influx-enterprise' +export const SOURCE_TYPE_INFLUX_RELAY = 'influx-relay' export const SOURCE_TYPE_INFLUX_V2 = 'influx-v2' export const SOURCE_TYPE_INFLUX_CLOUD_DEDICATED = 'influx-cloud-dedicated' diff --git a/ui/src/sources/components/SourceStep.tsx b/ui/src/sources/components/SourceStep.tsx index f33bf9a094..1b27425382 100644 --- a/ui/src/sources/components/SourceStep.tsx +++ b/ui/src/sources/components/SourceStep.tsx @@ -8,6 +8,7 @@ import _ from 'lodash' import {ErrorHandling} from 'src/shared/decorators/errors' import WizardTextInput from 'src/reusable_ui/components/wizard/WizardTextInput' import WizardCheckbox from 'src/reusable_ui/components/wizard/WizardCheckbox' +import WizardDropdown from 'src/reusable_ui/components/wizard/WizardDropdown' // Actions import { @@ -34,6 +35,8 @@ import { SOURCE_TYPE_INFLUX_CLOUD_DEDICATED, SOURCE_TYPE_INFLUX_V2, SOURCE_TYPE_INFLUX_V1, + SOURCE_TYPE_INFLUX_ENTERPRISE, + SOURCE_TYPE_INFLUX_RELAY, } from 'src/shared/constants' import {SUPERADMIN_ROLE} from 'src/auth/roles' @@ -42,10 +45,6 @@ import {Source, Me} from 'src/types' import {NextReturn} from 'src/types/wizard' const isNewSource = (source: Partial) => !source.id -const isV2Auth = (source: Partial) => - source.type && source.type === SOURCE_TYPE_INFLUX_V2 -const isCD = (source: Partial) => - source.type && source.type === SOURCE_TYPE_INFLUX_CLOUD_DEDICATED interface Props { notify: typeof notifyAction @@ -60,6 +59,7 @@ interface Props { interface State { source: Partial + serverType?: string // server type dropdown value } class SourceStep extends PureComponent { @@ -68,8 +68,10 @@ class SourceStep extends PureComponent { } constructor(props: Props) { super(props) + const source = this.props.source || DEFAULT_SOURCE this.state = { - source: this.props.source || DEFAULT_SOURCE, + source, + serverType: this.getServerTypeFromSource(source), } } @@ -107,12 +109,34 @@ class SourceStep extends PureComponent { public render() { const {source} = this.state const {isUsingAuth, onBoarding} = this.props - const sourceIsV2 = isV2Auth(source) - const sourceIsCD = isCD(source) + const sourceIsV2 = this.state.serverType === SOURCE_TYPE_INFLUX_V2 + const sourceIsCD = + this.state.serverType === SOURCE_TYPE_INFLUX_CLOUD_DEDICATED return ( <> {isUsingAuth && onBoarding && this.authIndicator} + { testId="default-connection--checkbox" /> )} - - {this.isHTTPS && ( { }, }) } - private changeV2Auth = (v2: boolean) => { - if (v2) { - this.changeSourceType(SOURCE_TYPE_INFLUX_V2, '2.x') - } else { - this.changeSourceType(SOURCE_TYPE_INFLUX_V1, '1.x') - } - } - private changeCD = (cd: boolean) => { - if (cd) { - this.changeSourceType(SOURCE_TYPE_INFLUX_CLOUD_DEDICATED, 'cloud') - } else { - this.changeSourceType(SOURCE_TYPE_INFLUX_V1, '1.x') - } - } private handleSubmitUrl = (url: string) => this.detectServerType({url}) private handleSubmitUsername = (username: string) => @@ -363,6 +361,40 @@ class SourceStep extends PureComponent { const {source} = this.state return _.get(source, 'type', '').includes('enterprise') } + + private getServerTypeFromSource = ( + source: Partial + ): string | undefined => { + if (source.type === SOURCE_TYPE_INFLUX_V2) { + return SOURCE_TYPE_INFLUX_V2 + } else if (source.type === SOURCE_TYPE_INFLUX_CLOUD_DEDICATED) { + return SOURCE_TYPE_INFLUX_CLOUD_DEDICATED + } else if ( + source.type === SOURCE_TYPE_INFLUX_V1 || + source.type === SOURCE_TYPE_INFLUX_ENTERPRISE || + source.type === SOURCE_TYPE_INFLUX_RELAY + ) { + // All 3 source subtypes are displayed as v1 + return SOURCE_TYPE_INFLUX_V1 + } + return undefined + } + + private handleServerTypeChange = (value: string) => { + this.setState({serverType: value}) + + switch (value) { + case SOURCE_TYPE_INFLUX_V2: + this.changeSourceType(SOURCE_TYPE_INFLUX_V2, '2.x') + break + case SOURCE_TYPE_INFLUX_CLOUD_DEDICATED: + this.changeSourceType(SOURCE_TYPE_INFLUX_CLOUD_DEDICATED, 'cloud') + break + case SOURCE_TYPE_INFLUX_V1: + default: + this.changeSourceType(SOURCE_TYPE_INFLUX_V1, '1.x') + } + } } const mdtp = { diff --git a/ui/src/style/chronograf.scss b/ui/src/style/chronograf.scss index 7eb4b1edfe..3118418a56 100644 --- a/ui/src/style/chronograf.scss +++ b/ui/src/style/chronograf.scss @@ -103,6 +103,7 @@ @import '../reusable_ui/components/wizard/WizardFullScreen.scss'; @import '../reusable_ui/components/wizard/WizardOverlay.scss'; @import '../reusable_ui/components/wizard/WizardCheckbox.scss'; +@import '../reusable_ui/components/wizard/WizardDropdown.scss'; @import '../reusable_ui/components/wizard/ProgressConnector.scss'; @import '../reusable_ui/components/wizard/WizardProgressBar.scss'; @import '../reusable_ui/components/wizard/WizardStep.scss'; From 4e85504a468e9203c346b96a64ff3cad3c9c97eb Mon Sep 17 00:00:00 2001 From: Jan Simon Date: Wed, 24 Sep 2025 21:33:38 +0200 Subject: [PATCH 21/50] feat: add InfluxDB 3 Core support --- chronograf.go | 8 +- influx/authorization.go | 4 +- influx/cloud_dedicated.go | 28 +- influx/databases.go | 12 +- influx/influx.go | 61 ++-- influx/influx_v3.go | 276 ++++++++++++++++++ influx/permissions.go | 16 + kv/internal/internal.pb.go | 2 +- kv/internal/internal.proto | 2 +- server/builders.go | 5 + server/sources.go | 13 +- server/sources_test.go | 46 ++- .../influxdb/AdminInfluxDBScopedPage.tsx | 6 +- .../influxdb/AdminInfluxDBTabbedPage.tsx | 18 +- .../influxdb/DatabaseManagerPage.tsx | 5 +- ui/src/shared/constants/index.ts | 1 + ui/src/sources/components/SourceStep.tsx | 66 +++-- 17 files changed, 499 insertions(+), 70 deletions(-) create mode 100644 influx/influx_v3.go diff --git a/chronograf.go b/chronograf.go index 3e785ac389..878a96f2ed 100644 --- a/chronograf.go +++ b/chronograf.go @@ -99,10 +99,16 @@ const ( InfluxRelay = "influx-relay" // InfluxDBv2 is Influx DB 2.x with Token authentication InfluxDBv2 = "influx-v2" + // InfluxDBv3 is InfluxDB 3 Core + InfluxDBv3Core = "influx-v3-core" // InfluxDBCloudDedicated is InfluxDB Cloud Dedicated with Account ID, Cluster ID, Management and DB Token InfluxDBCloudDedicated = "influx-cloud-dedicated" ) +func IsV3SrcType(srcType string) bool { + return srcType == InfluxDBv3Core || srcType == InfluxDBCloudDedicated +} + // TSDBStatus represents the current status of a time series database type TSDBStatus interface { // Connect will connect to the time series using the information in `Source`. @@ -248,7 +254,7 @@ type Source struct { ClusterID string `json:"clusterId,omitempty"` // ClusterID is the cluster ID for InfluxDB Cloud Dedicated sources AccountID string `json:"accountId,omitempty"` // AccountID is the account ID for InfluxDB Cloud Dedicated sources ManagementToken string `json:"managementToken,omitempty"` // ManagementToken is the management token for InfluxDB Cloud Dedicated sources - DatabaseToken string `json:"databaseToken,omitempty"` // DatabaseToken is the database token for InfluxDB Cloud Dedicated sources + DatabaseToken string `json:"databaseToken,omitempty"` // DatabaseToken is the database token for InfluxDB Cloud Dedicated or other InfluxDB 3 sources TagsCSVPath string `json:"tagsCSVPath,omitempty"` // TagsCSVPath is the path to a directory containing CSV files (per db) with tags for InfluxDB Cloud Dedicated sources URL string `json:"url"` // URL are the connections to the source MetaURL string `json:"metaUrl,omitempty"` // MetaURL is the url for the meta node diff --git a/influx/authorization.go b/influx/authorization.go index 60523cbc6e..ebeef8b50b 100644 --- a/influx/authorization.go +++ b/influx/authorization.go @@ -23,8 +23,8 @@ func (n *NoAuthorization) Set(req *http.Request) error { return nil } // DefaultAuthorization creates either a shared JWT builder, basic auth or Noop or Token authentication func DefaultAuthorization(src *chronograf.Source) Authorizer { - // Use Bearer Token authentication for InfluxDB Cloud - if src.Type == chronograf.InfluxDBCloudDedicated { + // Use Bearer Token authentication for InfluxDB 3 types + if chronograf.IsV3SrcType(src.Type) { return &BearerToken{ Token: src.DatabaseToken, } diff --git a/influx/cloud_dedicated.go b/influx/cloud_dedicated.go index dc8ee64520..7ebd251437 100644 --- a/influx/cloud_dedicated.go +++ b/influx/cloud_dedicated.go @@ -35,7 +35,9 @@ type cdListDatabasesError struct { Message string `json:"message,omitempty"` } -const timeCondition = "time > now() - 1d" +// TODO simon: make this expression configurable via environment variable +// const timeCondition = "time > now() - 1d" +const timeCondition = "time > 0" var timeExpr = mustParseExpr(timeCondition) @@ -110,7 +112,11 @@ func (c *Client) showDatabasesForCloudDedicated(ctx context.Context) (chronograf } // Convert response. - return constructShowDatabasesResponse(databases), nil + dbNames := make([]string, len(databases)) + for i, db := range databases { + dbNames[i] = db.Name + } + return constructShowDatabasesResponse(dbNames), nil } // newListDatabasesRequestForCloudDedicated constructs a new http.Request for listing databases in InfluxDB Cloud Dedicated. @@ -136,10 +142,10 @@ func (c *Client) newListDatabasesRequestForCloudDedicated(ctx context.Context) ( } // constructShowDatabasesResponse constructs a chronograf.Response containing database names formatted as query result data. -func constructShowDatabasesResponse(databases []cdDatabase) chronograf.Response { - values := make([][]interface{}, len(databases)) - for i, db := range databases { - values[i] = []interface{}{db.Name} +func constructShowDatabasesResponse(dbNames []string) chronograf.Response { + values := make([][]interface{}, len(dbNames)) + for i, dbName := range dbNames { + values[i] = []interface{}{dbName} } response := fakeInfluxResponse{ @@ -243,7 +249,7 @@ func (c *Client) handleShowTagValues(q *chronograf.Query, logs chronograf.Logger func parseShowTagValuesStatement(query string) (*influxql.ShowTagValuesStatement, error) { stmt, err := influxql.ParseStatement(query) if err != nil { - return nil, fmt.Errorf("failed to parse statement: %w", err) + return nil, fmt.Errorf("parsing error: %w", err) } showStmt, ok := stmt.(*influxql.ShowTagValuesStatement) if !ok { @@ -256,7 +262,7 @@ func parseShowTagValuesStatement(query string) (*influxql.ShowTagValuesStatement func parseShowTagKeysStatement(query string) (*influxql.ShowTagKeysStatement, error) { stmt, err := influxql.ParseStatement(query) if err != nil { - return nil, fmt.Errorf("failed to parse statement: %w", err) + return nil, fmt.Errorf("parsing error: %w", err) } showStmt, ok := stmt.(*influxql.ShowTagKeysStatement) if !ok { @@ -266,11 +272,12 @@ func parseShowTagKeysStatement(query string) (*influxql.ShowTagKeysStatement, er } // appendTimeCondition appends a default "WHERE time > now() - 1d" clause to the provided SHOW TAG VALUES statement if no time condition exists. -func appendTimeCondition(showStmt *influxql.ShowTagValuesStatement) { +// Returns true if the statement was modified. +func appendTimeCondition(showStmt *influxql.ShowTagValuesStatement) bool { // Check if there's already a time condition in the WHERE clause if showStmt.Condition != nil && hasTimeCondition(showStmt.Condition) { // Already has a time condition, do nothing - return + return false } // Add or modify the WHERE clause @@ -285,6 +292,7 @@ func appendTimeCondition(showStmt *influxql.ShowTagValuesStatement) { RHS: timeExpr, } } + return true } // hasTimeCondition recursively checks if an InfluxQL expression contains a reference to the "time" field. diff --git a/influx/databases.go b/influx/databases.go index fdeb56a8b0..a05703238d 100644 --- a/influx/databases.go +++ b/influx/databases.go @@ -61,9 +61,9 @@ func (c *Client) getRP(ctx context.Context, db, rp string) (chronograf.Retention // CreateRP creates a retention policy for a specific database func (c *Client) CreateRP(ctx context.Context, db string, rp *chronograf.RetentionPolicy) (*chronograf.RetentionPolicy, error) { - if c.SrcType == chronograf.InfluxDBCloudDedicated { + if c.isV3SrcType() { // Data retention in InfluxDB 3 is configured differently, on database level. - return nil, fmt.Errorf("retention policies not supported in InfluxDB Cloud Dedicated") + return nil, fmt.Errorf("retention policies not supported in InfluxDB 3") } query := fmt.Sprintf(`CREATE RETENTION POLICY "%s" ON "%s" DURATION %s REPLICATION %d`, rp.Name, db, rp.Duration, rp.Replication) if len(rp.ShardDuration) != 0 { @@ -92,9 +92,9 @@ func (c *Client) CreateRP(ctx context.Context, db string, rp *chronograf.Retenti // UpdateRP updates a specific retention policy for a specific database func (c *Client) UpdateRP(ctx context.Context, db string, rp string, upd *chronograf.RetentionPolicy) (*chronograf.RetentionPolicy, error) { - if c.SrcType == chronograf.InfluxDBCloudDedicated { + if c.isV3SrcType() { // Data retention in InfluxDB 3 is configured differently, on database level. - return nil, fmt.Errorf("retention policies not supported in InfluxDB Cloud Dedicated") + return nil, fmt.Errorf("retention policies not supported in InfluxDB 3") } var buffer bytes.Buffer buffer.WriteString(fmt.Sprintf(`ALTER RETENTION POLICY "%s" ON "%s"`, rp, db)) @@ -148,9 +148,9 @@ func (c *Client) UpdateRP(ctx context.Context, db string, rp string, upd *chrono // DropRP removes a specific retention policy for a specific database func (c *Client) DropRP(ctx context.Context, db string, rp string) error { - if c.SrcType == chronograf.InfluxDBCloudDedicated { + if c.isV3SrcType() { // Data retention in InfluxDB 3 is configured differently, on database level. - return fmt.Errorf("retention policies not supported in InfluxDB Cloud Dedicated") + return fmt.Errorf("retention policies not supported in InfluxDB 3") } _, err := c.Query(ctx, chronograf.Query{ Command: fmt.Sprintf(`DROP RETENTION POLICY "%s" ON "%s"`, rp, db), diff --git a/influx/influx.go b/influx/influx.go index 6e2a5388cc..1861cd9b20 100644 --- a/influx/influx.go +++ b/influx/influx.go @@ -135,16 +135,6 @@ func (c *Client) query(u *url.URL, q chronograf.Query) (chronograf.Response, err return nil, decErr } - // If we don't have an error in our json response, and didn't get statusOK - // then send back an error - if resp.StatusCode != http.StatusOK && response.Err != "" { - logs. - WithField("influx_status", resp.StatusCode). - Error("Received non-200 response from influxdb") - - return &response, fmt.Errorf("received status code %d from server", - resp.StatusCode) - } return &response, nil } @@ -186,7 +176,15 @@ func (c *Client) Query(ctx context.Context, q chronograf.Query) (chronograf.Resp resps := make(chan (result)) go func() { - resp, err := c.query(c.URL, q) + var resp chronograf.Response + var err error + if c.SrcType == chronograf.InfluxDBv3Core { + // v3 Core + resp, err = c.queryV3(c.URL, q) + } else { + // v1, v2, v3 Cloud Dedicated + resp, err = c.query(c.URL, q) + } resps <- result{resp, err} }() @@ -203,7 +201,7 @@ func (c *Client) ValidateAuth(ctx context.Context, src *chronograf.Source) error ctx, cancel := context.WithTimeout(ctx, 15*time.Second) defer cancel() - // Cloud Dedicated: + // v3 Cloud Dedicated: if src.Type == chronograf.InfluxDBCloudDedicated { return c.validateCloudDedicatedAuth(ctx) } @@ -211,7 +209,7 @@ func (c *Client) ValidateAuth(ctx context.Context, src *chronograf.Source) error if src.Type == chronograf.InfluxDBv2 { return c.validateV2Auth(ctx, src) } - // v1: use InfluxQL + // v1, v3 Core: use InfluxQL if _, err := c.Query(ctx, chronograf.Query{Command: "SHOW DATABASES"}); err != nil { return err } @@ -355,6 +353,10 @@ func (c *Client) pingTimeout(ctx context.Context) (string, string, error) { } } +type v3PingRespBody struct { + Version string `json:"version"` +} + type pingResult struct { Version string Type string @@ -390,11 +392,28 @@ func (c *Client) ping(u *url.URL) (string, string, error) { return "", "", err } - if resp.StatusCode != http.StatusNoContent { - var err = fmt.Errorf("%s", string(body)) + if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK { + var err = errors.New(string(body)) return "", "", err } + if c.SrcType == chronograf.InfluxDBv3Core { + // Read the version from the body + if len(body) == 0 { + return "", "", fmt.Errorf("empty ping response body") + } + var b v3PingRespBody + err := json.Unmarshal(body, &b) + if err != nil { + return "", "", fmt.Errorf("failed to parse ping response body: %w", err) + } + if b.Version == "" { + return "", "", fmt.Errorf("missing version in ping response body") + } + return b.Version, c.SrcType, nil + } + + // Check the `X-Influxdb-Build` header builds := resp.Header.Values("X-Influxdb-Build") isCloud2 := false for _, build := range builds { @@ -406,12 +425,16 @@ func (c *Client) ping(u *url.URL) (string, string, error) { } } + // Read the version from the `X-Influxdb-Version` header version := resp.Header.Get("X-Influxdb-Version") - if strings.Contains(version, "-c") { - return version, chronograf.InfluxEnterprise, nil - } else if strings.Contains(version, "relay") { - return version, chronograf.InfluxRelay, nil + if version != "" { + if strings.Contains(version, "-c") { + return version, chronograf.InfluxEnterprise, nil + } else if strings.Contains(version, "relay") { + return version, chronograf.InfluxRelay, nil + } } + // Strip v prefix from version, some older '1.x' versions and also // InfluxDB 2.2.0 return version in format vx.x.x if strings.HasPrefix(version, "v") { diff --git a/influx/influx_v3.go b/influx/influx_v3.go new file mode 100644 index 0000000000..ce4fd98210 --- /dev/null +++ b/influx/influx_v3.go @@ -0,0 +1,276 @@ +package influx + +import ( + "encoding/json" + "fmt" + "io" + "math/rand" + "net/http" + "net/url" + "strings" + + "github.com/influxdata/chronograf" + "github.com/influxdata/chronograf/util" + "github.com/influxdata/influxdb/influxql" +) + +// v3Database represents a database entry from InfluxDB v3 API response to `SHOW DATABASES` +type v3Database struct { + Database string `json:"iox::database"` + Deleted bool `json:"deleted"` +} + +// v3RetentionPolicy represents a retention policy entry from InfluxDB v3 API response to `SHOW RETENTION POLICIES` +type v3RetentionPolicy struct { + Database string `json:"iox::database"` + Name string `json:"name"` +} + +func (c *Client) isV3SrcType() bool { + return chronograf.IsV3SrcType(c.SrcType) +} + +// queryV3 executes InfluxQL queries against InfluxDB v3 server +func (c *Client) queryV3(u *url.URL, q chronograf.Query) (chronograf.Response, error) { + // Parse query + cmd := q.Command + stmt, err := influxql.ParseStatement(cmd) + if err != nil { + return nil, fmt.Errorf("parsing error: %w", err) + } + + // Select query endpoint + var path string + switch stmt.(type) { + case *influxql.ShowDatabasesStatement, + *influxql.ShowRetentionPoliciesStatement: + // `SHOW DATABASES` and `SHOW RETENTION POLICIES` are queried on the v3 query endpoint + path = "/api/v3/query_influxql" + default: + // v1 compatibility query endpoint + path = "/query" + } + + // Prepare request + u = util.AppendPath(u, path) + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + logs := c.Logger. + WithField("queryID", rand.Uint32()). + WithField("component", "proxy") + logs. + WithField("host", req.Host). + WithField("path", path). + WithField("command", cmd). + WithField("db", q.DB). + Debug("query") + + // If the database is not specified, then get it from the query + if q.DB == "" { + if db := parseDatabaseNameFromStatement(stmt); db != "" { + q.DB = db + logs.WithField("db", q.DB).Debug("database parsed from query") + } + } + + // Clear retention policies from queries since they're not supported in v3 queries + if clearRetentionPolicies(stmt) { + cmd = stmt.String() + logs.WithField("command", cmd).Debug("retention policies cleared from query") + } + + switch s := stmt.(type) { + case *influxql.ShowTagValuesStatement: + // Ensure time condition is added to `SHOW TAG VALUES` queries + if appendTimeCondition(s) { + cmd = stmt.String() + logs.WithField("command", cmd).Debug("time condition added to SHOW TAG VALUES query") + } + } + + // Query parameters + params := req.URL.Query() + params.Set("q", cmd) + params.Set("db", q.DB) + req.URL.RawQuery = params.Encode() + + // Authorization + if c.Authorizer != nil { + if err := c.Authorizer.Set(req); err != nil { + logs.Error("Error setting authorization header ", err) + return nil, err + } + } + + // Do the request + hc := &http.Client{} + hc.Transport = SharedTransport(c.InsecureSkipVerify) + resp, err := hc.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + b, _ := io.ReadAll(resp.Body) + bodyString := string(b) + logs.Debug("JSON response from InfluxDB: ", bodyString) + + if resp.StatusCode != http.StatusOK { + if resp.StatusCode == http.StatusInternalServerError { + // Shorten the error message to the point + errPrefix := "error in InfluxQL statement:" + if strings.HasPrefix(bodyString, errPrefix) { + return nil, fmt.Errorf("%s", bodyString[len(errPrefix):]) + } + } + return nil, fmt.Errorf("received status code %d: %s", resp.StatusCode, bodyString) + } + + switch stmt.(type) { + case *influxql.ShowDatabasesStatement: + // Handle `SHOW DATABASES` + return processShowDatabasesV3Response(b) + + case *influxql.ShowRetentionPoliciesStatement: + // Handle `SHOW RETENTION POLICIES` + return processShowRetentionPoliciesV3Response(b) + + default: + // Handle response form v1 compatibility endpoint + return processV1Response(b) + } +} + +// processShowDatabasesV3Response parses InfluxDB v3 SHOW DATABASES response and returns non-deleted database names aS InfluxQL response +func processShowDatabasesV3Response(responseBody []byte) (chronograf.Response, error) { + var databases []v3Database + if err := json.Unmarshal(responseBody, &databases); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + // Convert response, filtering out deleted databases. + dbNames := make([]string, 0) + for _, db := range databases { + if !db.Deleted { + dbNames = append(dbNames, db.Database) + } + } + return constructShowDatabasesResponse(dbNames), nil +} + +// processShowRetentionPoliciesV3Response parses InfluxDB v3 SHOW RETENTION POLICIES response and returns it as InfluxQL response +func processShowRetentionPoliciesV3Response(responseBody []byte) (chronograf.Response, error) { + var policies []v3RetentionPolicy + if err := json.Unmarshal(responseBody, &policies); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + // Convert to InfluxQL response format with only "name" column + values := make([][]interface{}, len(policies)) + for i, policy := range policies { + values[i] = []interface{}{policy.Name} + } + + return buildSingleSeriesResponse("", []string{"name"}, values) +} + +// processV1Response parses a v1 compatibility API response and returns it as chronograf.Response +func processV1Response(responseBody []byte) (chronograf.Response, error) { + var response responseType + if err := json.Unmarshal(responseBody, &response); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + return &response, nil +} + +// clearMeasurementRP clears the retention policy from a measurement if it exists. +// Returns true if the retention policy was cleared. +func clearMeasurementRP(source influxql.Source) bool { + if mm, ok := source.(*influxql.Measurement); ok && mm.RetentionPolicy != "" { + mm.RetentionPolicy = "" + return true + } + return false +} + +// clearRetentionPolicies removes retention policy references from InfluxQL statements +// since they're not supported in InfluxDB v3. Returns true if the statement was modified. +func clearRetentionPolicies(stmt influxql.Statement) bool { + modified := false + + var sources influxql.Sources + switch s := stmt.(type) { + case *influxql.ShowMeasurementsStatement: + return clearMeasurementRP(s.Source) + case *influxql.ShowTagKeysStatement: + sources = s.Sources + case *influxql.ShowTagValuesStatement: + sources = s.Sources + case *influxql.ShowFieldKeysStatement: + sources = s.Sources + case *influxql.SelectStatement: + sources = s.Sources + default: + return false + } + + if sources != nil { + for _, source := range sources { + if clearMeasurementRP(source) { + modified = true + } + } + } + + return modified +} + +// parseDatabaseNameFromStatement extracts the database name from InfluxQL query statement if present. +// Returns empty string if not found. +func parseDatabaseNameFromStatement(stmt influxql.Statement) string { + switch s := stmt.(type) { + case *influxql.SelectStatement: + if len(s.Sources) > 0 { + for _, source := range s.Sources { + if measurement, ok := source.(*influxql.Measurement); ok { + return measurement.Database + } + } + } + } + + return "" +} + +// buildSeriesResponse creates a standardized InfluxQL response with multiple series +func buildSeriesResponse(seriesResults []series) (chronograf.Response, error) { + response := fakeInfluxResponse{ + { + StatementID: 0, + Series: seriesResults, + }, + } + + data, err := json.Marshal(response) + if err != nil { + return nil, fmt.Errorf("failed to marshal response: %w", err) + } + + return &responseType{ + Results: data, + }, nil +} + +// buildSingleSeriesResponse creates a standardized InfluxQL response with a single series +func buildSingleSeriesResponse(name string, columns []string, values [][]interface{}) (chronograf.Response, error) { + seriesResults := []series{ + { + Name: name, + Columns: columns, + Values: values, + }, + } + return buildSeriesResponse(seriesResults) +} diff --git a/influx/permissions.go b/influx/permissions.go index 90ef01402a..4e007968ad 100644 --- a/influx/permissions.go +++ b/influx/permissions.go @@ -123,6 +123,22 @@ func (r *showResults) RetentionPolicies(logger chronograf.Logger) []chronograf.R // parseRetentionPolicy validates and parses a retention policy row func parseRetentionPolicy(v []interface{}) (chronograf.RetentionPolicy, error) { columns := len(v) + + if columns == 1 { + // 1-column format: [name] -- returned by InfluxDB 3 + if name, ok := v[0].(string); !ok { + return chronograf.RetentionPolicy{}, fmt.Errorf("column 0 (name) is not a string") + } else { + return chronograf.RetentionPolicy{ + Name: name, + Duration: "0s", + ShardDuration: "0s", + Replication: 1, + Default: false, + }, nil + } + } + if columns < 5 { return chronograf.RetentionPolicy{}, fmt.Errorf("insufficient columns: expected at least 5, got %d", columns) } else if name, ok := v[0].(string); !ok { diff --git a/kv/internal/internal.pb.go b/kv/internal/internal.pb.go index 6a1fc5d087..c67f1773e1 100644 --- a/kv/internal/internal.pb.go +++ b/kv/internal/internal.pb.go @@ -43,7 +43,7 @@ type Source struct { ClusterID string `protobuf:"bytes,16,opt,name=ClusterID,proto3" json:"ClusterID,omitempty"` // Cluster ID of an InfluxDB Cloud Dedicated source AccountID string `protobuf:"bytes,17,opt,name=AccountID,proto3" json:"AccountID,omitempty"` // Account ID of an InfluxDB Cloud Dedicated source ManagementToken string `protobuf:"bytes,18,opt,name=ManagementToken,proto3" json:"ManagementToken,omitempty"` // Management token of an InfluxDB Cloud Dedicated source - DatabaseToken string `protobuf:"bytes,19,opt,name=DatabaseToken,proto3" json:"DatabaseToken,omitempty"` // Database token of an InfluxDB Cloud Dedicated source + DatabaseToken string `protobuf:"bytes,19,opt,name=DatabaseToken,proto3" json:"DatabaseToken,omitempty"` // Database token of an InfluxDB Cloud Dedicated or other InfluxDB 3 source TagsCSVPath string `protobuf:"bytes,20,opt,name=TagsCSVPath,proto3" json:"TagsCSVPath,omitempty"` // TagsCSVPATH is the path to a directory containing CSV files (per db) with tags for the source } diff --git a/kv/internal/internal.proto b/kv/internal/internal.proto index 54bd53d1b4..ae0375d098 100644 --- a/kv/internal/internal.proto +++ b/kv/internal/internal.proto @@ -21,7 +21,7 @@ message Source { string ClusterID = 16; // Cluster ID of an InfluxDB Cloud Dedicated source string AccountID = 17; // Account ID of an InfluxDB Cloud Dedicated source string ManagementToken = 18; // Management token of an InfluxDB Cloud Dedicated source - string DatabaseToken = 19; // Database token of an InfluxDB Cloud Dedicated source + string DatabaseToken = 19; // Database token of an InfluxDB Cloud Dedicated or other InfluxDB 3 source string TagsCSVPath = 20; // TagsCSVPath is the path to a directory containing CSV files (per db) with tags for the source } diff --git a/server/builders.go b/server/builders.go index 130ceb884a..71cf629b50 100644 --- a/server/builders.go +++ b/server/builders.go @@ -145,6 +145,11 @@ func (fs *MultiSourceBuilder) Build(db chronograf.SourcesStore) (*multistore.Sou mgmtToken = fs.InfluxDBMgmtToken dbToken = fs.InfluxDBToken tagsCSVPath = fs.TagsCSVPath + } else if fs.InfluxDBToken != "" { + // TODO simon: modify later, once other v3 versions are added; maybe use the source.type? + // InfluxDB 3 Core + influxdbType = chronograf.InfluxDBv3Core + dbToken = fs.InfluxDBToken } else if fs.InfluxDBOrg == "" || fs.InfluxDBToken == "" { // v1 InfluxDB username = fs.InfluxDBUsername diff --git a/server/sources.go b/server/sources.go index f31c7a9261..4007dad6a0 100644 --- a/server/sources.go +++ b/server/sources.go @@ -78,7 +78,7 @@ func hasFlux(ctx context.Context, src chronograf.Source) (bool, error) { if src.Version == "" /* v2 OSS reports no version */ || strings.HasPrefix(src.Version, "2.") { return src.Type == chronograf.InfluxDBv2 && src.Username != "", nil } - if src.Type == chronograf.InfluxDBCloudDedicated { + if chronograf.IsV3SrcType(src.Type) { // InfluxDB 3 doesn't support Flux. return false, nil } @@ -227,7 +227,9 @@ func (s *Service) tsdbVersion(ctx context.Context, src *chronograf.Source) (stri } func (s *Service) tsdbType(ctx context.Context, src *chronograf.Source) (string, error) { - if src.Type == chronograf.InfluxDBv2 || src.Type == chronograf.InfluxDBCloudDedicated { + if src.Type == chronograf.InfluxDBv2 || + src.Type == chronograf.InfluxDBCloudDedicated || + src.Type == chronograf.InfluxDBv3Core { return src.Type, nil // type selected by the user } cli := &influx.Client{ @@ -509,6 +511,7 @@ func ValidSourceRequest(s *chronograf.Source, defaultOrgID string) error { if s.Type != "" { if s.Type != chronograf.InfluxDB && s.Type != chronograf.InfluxDBv2 && + s.Type != chronograf.InfluxDBv3Core && s.Type != chronograf.InfluxDBCloudDedicated && s.Type != chronograf.InfluxEnterprise && s.Type != chronograf.InfluxRelay { @@ -528,6 +531,12 @@ func ValidSourceRequest(s *chronograf.Source, defaultOrgID string) error { return fmt.Errorf("invalid URL; no URL scheme defined") } + if s.Type == chronograf.InfluxDBv3Core { + if len(s.DatabaseToken) == 0 { + return fmt.Errorf("database token required") + } + } + if s.Type == chronograf.InfluxDBCloudDedicated { if len(s.ClusterID) == 0 { return fmt.Errorf("cluster ID required") diff --git a/server/sources_test.go b/server/sources_test.go index 77f2354a3d..d8bc22edba 100644 --- a/server/sources_test.go +++ b/server/sources_test.go @@ -151,7 +151,7 @@ func Test_ValidSourceRequest(t *testing.T) { }, }, { - name: "support InfluxDB Cloud Dedicated type", + name: "InfluxDB Cloud Dedicated - supported", args: args{ source: &chronograf.Source{ ID: 1, @@ -337,6 +337,50 @@ func Test_ValidSourceRequest(t *testing.T) { err: fmt.Errorf("database token required"), }, }, + { + name: "InfluxDB 3 Core - supported", + args: args{ + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBv3Core, + DatabaseToken: "database-token", + URL: "http://www.any.url.com", + InsecureSkipVerify: true, + Default: true, + Telegraf: "telegraf", + }, + }, + wants: wants{ + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBv3Core, + DatabaseToken: "database-token", + URL: "http://www.any.url.com", + InsecureSkipVerify: true, + Default: true, + Telegraf: "telegraf", + }, + }, + }, + { + name: "InfluxDB 3 Core - missing database token", + args: args{ + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBv3Core, + URL: "http://www.any.url.com", + InsecureSkipVerify: true, + Default: true, + Telegraf: "telegraf", + }, + }, + wants: wants{ + err: fmt.Errorf("database token required"), + }, + }, { name: "bad url", args: args{ diff --git a/ui/src/admin/containers/influxdb/AdminInfluxDBScopedPage.tsx b/ui/src/admin/containers/influxdb/AdminInfluxDBScopedPage.tsx index 34630394cf..e4b9429cb1 100644 --- a/ui/src/admin/containers/influxdb/AdminInfluxDBScopedPage.tsx +++ b/ui/src/admin/containers/influxdb/AdminInfluxDBScopedPage.tsx @@ -13,7 +13,8 @@ import {Page} from 'src/reusable_ui' import {ErrorHandling} from 'src/shared/decorators/errors' import {notify as notifyAction} from 'src/shared/actions/notifications' -import {Source, RemoteDataState, SourceAuthenticationMethod} from 'src/types' +import {RemoteDataState, Source} from 'src/types' +import {isConnectedToLDAP, isV3Source} from './AdminInfluxDBTabbedPage' const mapDispatchToProps = { loadUsers: loadUsersAsync, @@ -100,7 +101,8 @@ class AdminInfluxDBScopedPage extends PureComponent { try { errorMessage = 'Failed to load databases.' await loadDBsAndRPs(source.links.databases) - if (source.authentication !== SourceAuthenticationMethod.LDAP) { + if (!isConnectedToLDAP(source) && !isV3Source(source)) { + // Load users and permissions errorMessage = 'Failed to load users.' await loadUsers(source.links.users) errorMessage = 'Failed to load permissions.' diff --git a/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx b/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx index 45ed056ef9..6bd2272c47 100644 --- a/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx +++ b/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx @@ -4,6 +4,10 @@ import SubSections from 'src/shared/components/SubSections' import {Source, SourceAuthenticationMethod} from 'src/types' import {PageSection} from 'src/types/shared' import {WrapToPage} from './AdminInfluxDBScopedPage' +import { + SOURCE_TYPE_INFLUX_CLOUD_DEDICATED, + SOURCE_TYPE_INFLUX_V3_CORE, +} from 'src/shared/constants' interface Props { source: Source @@ -18,6 +22,13 @@ export function isConnectedToLDAP(source: Source) { return source.authentication === SourceAuthenticationMethod.LDAP } +export function isV3Source(source: Source) { + return ( + source.type === SOURCE_TYPE_INFLUX_CLOUD_DEDICATED || + source.type === SOURCE_TYPE_INFLUX_V3_CORE + ) +} + export const AdminTabs = ({ source, activeTab, @@ -27,6 +38,7 @@ export const AdminTabs = ({ const sections = useMemo(() => { const hasRoles = hasRoleManagement(source) const isLDAP = isConnectedToLDAP(source) + const isV3 = isV3Source(source) return [ { url: 'databases', @@ -36,17 +48,17 @@ export const AdminTabs = ({ { url: 'users', name: 'Users', - enabled: !isLDAP, + enabled: !isLDAP && !isV3, }, { url: 'roles', name: 'Roles', - enabled: hasRoles && !isLDAP, + enabled: hasRoles && !isLDAP && !isV3, }, { url: 'queries', name: 'Queries', - enabled: true, + enabled: !isV3, }, ] }, [source]) diff --git a/ui/src/admin/containers/influxdb/DatabaseManagerPage.tsx b/ui/src/admin/containers/influxdb/DatabaseManagerPage.tsx index 0635dafdf1..a0d0d6b469 100644 --- a/ui/src/admin/containers/influxdb/DatabaseManagerPage.tsx +++ b/ui/src/admin/containers/influxdb/DatabaseManagerPage.tsx @@ -17,8 +17,7 @@ import { } from 'src/shared/copy/notifications' import {Source} from 'src/types' import {Database, RetentionPolicy} from 'src/types/influxAdmin' -import AdminInfluxDBTabbedPage from './AdminInfluxDBTabbedPage' -import {SOURCE_TYPE_INFLUX_CLOUD_DEDICATED} from '../../../shared/constants' +import AdminInfluxDBTabbedPage, {isV3Source} from './AdminInfluxDBTabbedPage' interface Props { source: Source @@ -136,7 +135,7 @@ class DatabaseManagerPage extends Component { render() { const {source, databases, actions} = this.props - const isDBReadOnly = source.type === SOURCE_TYPE_INFLUX_CLOUD_DEDICATED + const isDBReadOnly = isV3Source(source) return ( ) => !source.id @@ -109,10 +110,6 @@ class SourceStep extends PureComponent { public render() { const {source} = this.state const {isUsingAuth, onBoarding} = this.props - const sourceIsV2 = this.state.serverType === SOURCE_TYPE_INFLUX_V2 - const sourceIsCD = - this.state.serverType === SOURCE_TYPE_INFLUX_CLOUD_DEDICATED - return ( <> {isUsingAuth && onBoarding && this.authIndicator} @@ -129,6 +126,10 @@ class SourceStep extends PureComponent { value: SOURCE_TYPE_INFLUX_V2, label: 'InfluxDB v2', }, + { + value: SOURCE_TYPE_INFLUX_V3_CORE, + label: 'InfluxDB 3 Core', + }, { value: SOURCE_TYPE_INFLUX_CLOUD_DEDICATED, label: 'InfluxDB Cloud Dedicated', @@ -151,18 +152,27 @@ class SourceStep extends PureComponent { onChange={this.onChangeInput('name')} testId="connection-name--input" /> - {!sourceIsCD && ( + {(this.state.serverType === SOURCE_TYPE_INFLUX_V1 || + this.state.serverType === SOURCE_TYPE_INFLUX_V2) && ( <> { )} + {/* InfluxDB 3 Core fields */} + {this.state.serverType === SOURCE_TYPE_INFLUX_V3_CORE && ( + <> + + + )} + {/* InfluxDB Cloud Dedicated fields */} - {sourceIsCD && ( + {this.state.serverType === SOURCE_TYPE_INFLUX_CLOUD_DEDICATED && ( <> { private getServerTypeFromSource = ( source: Partial ): string | undefined => { - if (source.type === SOURCE_TYPE_INFLUX_V2) { - return SOURCE_TYPE_INFLUX_V2 - } else if (source.type === SOURCE_TYPE_INFLUX_CLOUD_DEDICATED) { - return SOURCE_TYPE_INFLUX_CLOUD_DEDICATED - } else if ( + if ( source.type === SOURCE_TYPE_INFLUX_V1 || + source.type === SOURCE_TYPE_INFLUX_V2 || + source.type === SOURCE_TYPE_INFLUX_V3_CORE || + source.type === SOURCE_TYPE_INFLUX_CLOUD_DEDICATED + ) { + return source.type + } + if ( source.type === SOURCE_TYPE_INFLUX_ENTERPRISE || source.type === SOURCE_TYPE_INFLUX_RELAY ) { - // All 3 source subtypes are displayed as v1 + // Special v1 subtypes are displayed as v1 return SOURCE_TYPE_INFLUX_V1 } return undefined @@ -387,6 +412,9 @@ class SourceStep extends PureComponent { case SOURCE_TYPE_INFLUX_V2: this.changeSourceType(SOURCE_TYPE_INFLUX_V2, '2.x') break + case SOURCE_TYPE_INFLUX_V3_CORE: + this.changeSourceType(SOURCE_TYPE_INFLUX_V3_CORE, '3.x') + break case SOURCE_TYPE_INFLUX_CLOUD_DEDICATED: this.changeSourceType(SOURCE_TYPE_INFLUX_CLOUD_DEDICATED, 'cloud') break From 859b64077620dbb3adb6c1153719601e91e15cd6 Mon Sep 17 00:00:00 2001 From: Jan Simon Date: Wed, 24 Sep 2025 21:47:48 +0200 Subject: [PATCH 22/50] test: fixing tests --- influx/cloud_dedicated_test.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/influx/cloud_dedicated_test.go b/influx/cloud_dedicated_test.go index e1e528261e..4229deed8b 100644 --- a/influx/cloud_dedicated_test.go +++ b/influx/cloud_dedicated_test.go @@ -17,7 +17,6 @@ import ( ) func TestAppendTimeCondition(t *testing.T) { - const timeCondition = `time > now() - 1d` tests := []struct { name string @@ -172,7 +171,7 @@ func TestParseShowTagValuesStatement(t *testing.T) { { name: "SHOW TAG VALUES without WITH", input: `SHOW TAG VALUES`, - expectedError: "failed to parse statement: found EOF, expected WITH", + expectedError: "parsing error: found EOF, expected WITH", }, { name: "query of other type", @@ -182,12 +181,12 @@ func TestParseShowTagValuesStatement(t *testing.T) { { name: "empty string", input: ``, - expectedError: "failed to parse statement: found EOF", + expectedError: "parsing error: found EOF", }, { name: "malformed query", input: `SHOW TAG VALUES FROM machine_data WITH KEY = "tagkey INVALID`, - expectedError: "failed to parse statement", + expectedError: "parsing error", }, } From 0f44757209b47313cea31f88976a4d5767212d32 Mon Sep 17 00:00:00 2001 From: Jan Simon Date: Mon, 29 Sep 2025 17:13:08 +0200 Subject: [PATCH 23/50] feat: add InfluxDB 3 Enterprise support --- chronograf.go | 26 ++++--- influx/authorization.go | 2 +- influx/cloud_dedicated_test.go | 2 +- influx/influx.go | 22 +++--- kv/internal/internal_test.go | 2 +- server/builders.go | 11 +-- server/server.go | 4 +- server/service.go | 2 +- server/sources.go | 20 +++--- server/sources_test.go | 68 +++++++++++++++---- server/swagger.json | 2 +- .../influxdb/AdminInfluxDBTabbedPage.tsx | 8 ++- ui/src/shared/components/TagListItem.tsx | 4 +- ui/src/shared/constants/index.ts | 7 +- ui/src/sources/components/SourceStep.tsx | 38 +++++++---- 15 files changed, 145 insertions(+), 73 deletions(-) diff --git a/chronograf.go b/chronograf.go index 878a96f2ed..ccd4b98e01 100644 --- a/chronograf.go +++ b/chronograf.go @@ -91,22 +91,28 @@ type Assets interface { // Supported time-series databases const ( - // InfluxDB is the open-source time-series database - InfluxDB = "influx" - // InfluxEnteprise is the clustered HA time-series database - InfluxEnterprise = "influx-enterprise" - // InfluxRelay is the basic HA layer over InfluxDB - InfluxRelay = "influx-relay" + // InfluxDBv1 is InfluxDB OSS v1 + InfluxDBv1 = "influx" + // InfluxDBv1Enterprise is InfluxDB v1 Enterprise (the clustered HA time-series database) + InfluxDBv1Enterprise = "influx-enterprise" + // InfluxDBv1Relay is the basic HA layer over InfluxDB v1 + InfluxDBv1Relay = "influx-relay" + // InfluxDBv2 is Influx DB 2.x with Token authentication InfluxDBv2 = "influx-v2" - // InfluxDBv3 is InfluxDB 3 Core + + // InfluxDBv3Core is InfluxDB 3 Core InfluxDBv3Core = "influx-v3-core" - // InfluxDBCloudDedicated is InfluxDB Cloud Dedicated with Account ID, Cluster ID, Management and DB Token - InfluxDBCloudDedicated = "influx-cloud-dedicated" + // InfluxDBv3Enterprise is InfluxDB 3 Enterprise + InfluxDBv3Enterprise = "influx-v3-enterprise" + // InfluxDBv3CloudDedicated is InfluxDB Cloud Dedicated + InfluxDBv3CloudDedicated = "influx-v3-cloud-dedicated" ) func IsV3SrcType(srcType string) bool { - return srcType == InfluxDBv3Core || srcType == InfluxDBCloudDedicated + return srcType == InfluxDBv3Core || + srcType == InfluxDBv3Enterprise || + srcType == InfluxDBv3CloudDedicated } // TSDBStatus represents the current status of a time series database diff --git a/influx/authorization.go b/influx/authorization.go index ebeef8b50b..3cd5fb1c87 100644 --- a/influx/authorization.go +++ b/influx/authorization.go @@ -23,7 +23,7 @@ func (n *NoAuthorization) Set(req *http.Request) error { return nil } // DefaultAuthorization creates either a shared JWT builder, basic auth or Noop or Token authentication func DefaultAuthorization(src *chronograf.Source) Authorizer { - // Use Bearer Token authentication for InfluxDB 3 types + // Use Bearer Token authentication for all InfluxDB 3 types if chronograf.IsV3SrcType(src.Type) { return &BearerToken{ Token: src.DatabaseToken, diff --git a/influx/cloud_dedicated_test.go b/influx/cloud_dedicated_test.go index 4229deed8b..ac4347a65a 100644 --- a/influx/cloud_dedicated_test.go +++ b/influx/cloud_dedicated_test.go @@ -882,7 +882,7 @@ meas1;tag1;v2 client := &Client{ Logger: log.New(log.DebugLevel), - SrcType: chronograf.InfluxDBCloudDedicated, + SrcType: chronograf.InfluxDBv3CloudDedicated, } if tc.csvContent != "" { tmpDir, err := setupCSVTestDirWithContent("db1", tc.csvContent) diff --git a/influx/influx.go b/influx/influx.go index 1861cd9b20..9697c6399c 100644 --- a/influx/influx.go +++ b/influx/influx.go @@ -148,7 +148,7 @@ type result struct { // include both the database and retention policy. In-flight requests can be // cancelled using the provided context. func (c *Client) Query(ctx context.Context, q chronograf.Query) (chronograf.Response, error) { - if c.SrcType == chronograf.InfluxDBCloudDedicated { + if c.SrcType == chronograf.InfluxDBv3CloudDedicated { logs := c.Logger. WithField("component", "proxy"). WithField("command", q.Command) @@ -178,8 +178,8 @@ func (c *Client) Query(ctx context.Context, q chronograf.Query) (chronograf.Resp go func() { var resp chronograf.Response var err error - if c.SrcType == chronograf.InfluxDBv3Core { - // v3 Core + if c.SrcType == chronograf.InfluxDBv3Core || c.SrcType == chronograf.InfluxDBv3Enterprise { + // v3 Core, v3 Enterprise resp, err = c.queryV3(c.URL, q) } else { // v1, v2, v3 Cloud Dedicated @@ -202,7 +202,7 @@ func (c *Client) ValidateAuth(ctx context.Context, src *chronograf.Source) error defer cancel() // v3 Cloud Dedicated: - if src.Type == chronograf.InfluxDBCloudDedicated { + if src.Type == chronograf.InfluxDBv3CloudDedicated { return c.validateCloudDedicatedAuth(ctx) } // v2: use flux query @@ -289,7 +289,7 @@ func (c *Client) Connect(ctx context.Context, src *chronograf.Source) error { c.URL = u // InfluxDB Cloud Dedicated also provides a management API. - if src.Type == chronograf.InfluxDBCloudDedicated { + if src.Type == chronograf.InfluxDBv3CloudDedicated { mgmtUrl := fmt.Sprintf("https://console.influxdata.com/api/v0/accounts/%s/clusters/%s", src.AccountID, src.ClusterID) if u, err = url.Parse(mgmtUrl); err != nil { return err @@ -397,7 +397,7 @@ func (c *Client) ping(u *url.URL) (string, string, error) { return "", "", err } - if c.SrcType == chronograf.InfluxDBv3Core { + if c.SrcType == chronograf.InfluxDBv3Core || c.SrcType == chronograf.InfluxDBv3Enterprise { // Read the version from the body if len(body) == 0 { return "", "", fmt.Errorf("empty ping response body") @@ -418,7 +418,7 @@ func (c *Client) ping(u *url.URL) (string, string, error) { isCloud2 := false for _, build := range builds { if build == "ENT" { - return build, chronograf.InfluxEnterprise, nil + return build, chronograf.InfluxDBv1Enterprise, nil } if build == "cloud2" { isCloud2 = true @@ -429,9 +429,9 @@ func (c *Client) ping(u *url.URL) (string, string, error) { version := resp.Header.Get("X-Influxdb-Version") if version != "" { if strings.Contains(version, "-c") { - return version, chronograf.InfluxEnterprise, nil + return version, chronograf.InfluxDBv1Enterprise, nil } else if strings.Contains(version, "relay") { - return version, chronograf.InfluxRelay, nil + return version, chronograf.InfluxDBv1Relay, nil } } @@ -443,10 +443,10 @@ func (c *Client) ping(u *url.URL) (string, string, error) { if isCloud2 { // TODO: improve this, other influxdb v3 version could also return "cloud2" - return version, chronograf.InfluxDBCloudDedicated, nil + return version, chronograf.InfluxDBv3CloudDedicated, nil } - return version, chronograf.InfluxDB, nil + return version, chronograf.InfluxDBv1, nil } // Write POSTs line protocol to a database and retention policy diff --git a/kv/internal/internal_test.go b/kv/internal/internal_test.go index 5be85f4632..22332ca72a 100644 --- a/kv/internal/internal_test.go +++ b/kv/internal/internal_test.go @@ -47,7 +47,7 @@ func TestMarshalSource(t *testing.T) { src: chronograf.Source{ ID: 12, Name: "Fountain of Truth", - Type: "influx-cloud-dedicated", + Type: "influx-v3-cloud-dedicated", ClusterID: "3F762A1F-B609-4E7A-9657-8F0A39C27A58", AccountID: "27F924B3-FF40-47B1-B587-3AB980B87EF4", ManagementToken: "mgmt-token", diff --git a/server/builders.go b/server/builders.go index 71cf629b50..64810edd37 100644 --- a/server/builders.go +++ b/server/builders.go @@ -112,6 +112,7 @@ type SourcesBuilder interface { // MultiSourceBuilder implements SourcesBuilder type MultiSourceBuilder struct { + InfluxDBType string InfluxDBURL string InfluxDBUsername string InfluxDBPassword string @@ -134,27 +135,28 @@ func (fs *MultiSourceBuilder) Build(db chronograf.SourcesStore) (*multistore.Sou stores := []chronograf.SourcesStore{db, files} + // TODO simon: also process fs.InfluxDBType! if fs.InfluxDBURL != "" { var influxdbType, username, password string var clusterID, accountID, mgmtToken, dbToken, tagsCSVPath string if fs.InfluxDBClusterID != "" && fs.InfluxDBAccountID != "" && fs.InfluxDBToken != "" && fs.InfluxDBMgmtToken != "" { // InfluxDB Cloud Dedicated - influxdbType = chronograf.InfluxDBCloudDedicated + influxdbType = chronograf.InfluxDBv3CloudDedicated clusterID = fs.InfluxDBClusterID accountID = fs.InfluxDBAccountID mgmtToken = fs.InfluxDBMgmtToken dbToken = fs.InfluxDBToken tagsCSVPath = fs.TagsCSVPath } else if fs.InfluxDBToken != "" { - // TODO simon: modify later, once other v3 versions are added; maybe use the source.type? - // InfluxDB 3 Core + // TODO simon: this is not fully correct, it can be either v3 Core or v3 Enterprise + // InfluxDB 3 Core/Enterprise influxdbType = chronograf.InfluxDBv3Core dbToken = fs.InfluxDBToken } else if fs.InfluxDBOrg == "" || fs.InfluxDBToken == "" { // v1 InfluxDB username = fs.InfluxDBUsername password = fs.InfluxDBPassword - influxdbType = chronograf.InfluxDB + influxdbType = chronograf.InfluxDBv1 } else { // v2 InfluxDB username = fs.InfluxDBOrg @@ -163,6 +165,7 @@ func (fs *MultiSourceBuilder) Build(db chronograf.SourcesStore) (*multistore.Sou } influxStore := &memdb.SourcesStore{ + // TODO simon: validate the Source before adding, reuse ValidSourceRequest! Source: &chronograf.Source{ ID: 0, Name: fs.InfluxDBURL, diff --git a/server/server.go b/server/server.go index 1c90d912f2..ce4f0b96b3 100644 --- a/server/server.go +++ b/server/server.go @@ -58,11 +58,12 @@ type Server struct { Cert flags.Filename `long:"cert" description:"Path to PEM encoded public key certificate. " env:"TLS_CERTIFICATE"` Key flags.Filename `long:"key" description:"Path to private key associated with given certificate. " env:"TLS_PRIVATE_KEY"` + InfluxDBType string `long:"influxdb-type" value-name:"choice" choice:"influx" choice:"influx-enterprise" choice:"influx-relay" choice:"influx-v2" choice:"influx-v3-core" choice:"influx-v3-enterprise" choice:"influx-v3-cloud-dedicated" description:"InfluxDB server type instance" env:"INFLUXDB_TYPE"` InfluxDBURL string `long:"influxdb-url" description:"Location of your InfluxDB instance" env:"INFLUXDB_URL"` InfluxDBUsername string `long:"influxdb-username" description:"Username for your InfluxDB instance" env:"INFLUXDB_USERNAME"` InfluxDBPassword string `long:"influxdb-password" description:"Password for your InfluxDB instance" env:"INFLUXDB_PASSWORD"` InfluxDBOrg string `long:"influxdb-org" description:"Organization for your InfluxDB v2 instance" env:"INFLUXDB_ORG"` - InfluxDBToken string `long:"influxdb-token" description:"Token for your InfluxDB v2 or Cloud Dedicated instance" env:"INFLUXDB_TOKEN"` + InfluxDBToken string `long:"influxdb-token" description:"Token for your InfluxDB v2, v3 Core/Enterprise or Cloud Dedicated instance" env:"INFLUXDB_TOKEN"` InfluxDBMgmtToken string `long:"influxdb-mgmt-token" description:"Management token for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_MGMT_TOKEN"` InfluxDBClusterID string `long:"influxdb-cluster-id" description:"Cluster ID for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_CLUSTER_ID"` InfluxDBAccountID string `long:"influxdb-account-id" description:"Account ID for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_ACCOUNT_ID"` @@ -546,6 +547,7 @@ func (s *Server) newBuilders(logger chronograf.Logger) builders { Path: s.ResourcesPath, }, Sources: &MultiSourceBuilder{ + InfluxDBType: s.InfluxDBType, InfluxDBURL: s.InfluxDBURL, InfluxDBUsername: s.InfluxDBUsername, InfluxDBPassword: s.InfluxDBPassword, diff --git a/server/service.go b/server/service.go index 2ea94ebb1a..358cbaae80 100644 --- a/server/service.go +++ b/server/service.go @@ -52,7 +52,7 @@ func (c *InfluxClient) New(src chronograf.Source, logger chronograf.Logger) (chr if err := client.Connect(context.TODO(), &src); err != nil { return nil, err } - if src.Type == chronograf.InfluxEnterprise && src.MetaURL != "" { + if src.Type == chronograf.InfluxDBv1Enterprise && src.MetaURL != "" { tls := strings.Contains(src.MetaURL, "https") insecure := src.InsecureSkipVerify return enterprise.NewClientWithTimeSeries(logger, src.MetaURL, influx.DefaultAuthorization(&src), tls, insecure, client) diff --git a/server/sources.go b/server/sources.go index 4007dad6a0..2c26f01bc1 100644 --- a/server/sources.go +++ b/server/sources.go @@ -139,7 +139,7 @@ func newSourceResponse(ctx context.Context, src chronograf.Source) sourceRespons // MetaURL is currently a string, but eventually, we'd like to change it // to a slice. Checking len(src.MetaURL) is functionally equivalent to // checking if it is equal to the empty string. - if src.Type == chronograf.InfluxEnterprise && len(src.MetaURL) != 0 { + if src.Type == chronograf.InfluxDBv1Enterprise && len(src.MetaURL) != 0 { res.Links.Roles = fmt.Sprintf("%s/%d/roles", httpAPISrcs, src.ID) } return res @@ -228,8 +228,9 @@ func (s *Service) tsdbVersion(ctx context.Context, src *chronograf.Source) (stri func (s *Service) tsdbType(ctx context.Context, src *chronograf.Source) (string, error) { if src.Type == chronograf.InfluxDBv2 || - src.Type == chronograf.InfluxDBCloudDedicated || - src.Type == chronograf.InfluxDBv3Core { + src.Type == chronograf.InfluxDBv3Core || + src.Type == chronograf.InfluxDBv3Enterprise || + src.Type == chronograf.InfluxDBv3CloudDedicated { return src.Type, nil // type selected by the user } cli := &influx.Client{ @@ -509,12 +510,13 @@ func ValidSourceRequest(s *chronograf.Source, defaultOrgID string) error { } // Validate Type if s.Type != "" { - if s.Type != chronograf.InfluxDB && + if s.Type != chronograf.InfluxDBv1 && + s.Type != chronograf.InfluxDBv1Enterprise && + s.Type != chronograf.InfluxDBv1Relay && s.Type != chronograf.InfluxDBv2 && s.Type != chronograf.InfluxDBv3Core && - s.Type != chronograf.InfluxDBCloudDedicated && - s.Type != chronograf.InfluxEnterprise && - s.Type != chronograf.InfluxRelay { + s.Type != chronograf.InfluxDBv3Enterprise && + s.Type != chronograf.InfluxDBv3CloudDedicated { return fmt.Errorf("invalid source type %s", s.Type) } } @@ -531,13 +533,13 @@ func ValidSourceRequest(s *chronograf.Source, defaultOrgID string) error { return fmt.Errorf("invalid URL; no URL scheme defined") } - if s.Type == chronograf.InfluxDBv3Core { + if s.Type == chronograf.InfluxDBv3Core || s.Type == chronograf.InfluxDBv3Enterprise { if len(s.DatabaseToken) == 0 { return fmt.Errorf("database token required") } } - if s.Type == chronograf.InfluxDBCloudDedicated { + if s.Type == chronograf.InfluxDBv3CloudDedicated { if len(s.ClusterID) == 0 { return fmt.Errorf("cluster ID required") } diff --git a/server/sources_test.go b/server/sources_test.go index d8bc22edba..d0aa09e10b 100644 --- a/server/sources_test.go +++ b/server/sources_test.go @@ -45,7 +45,7 @@ func Test_ValidSourceRequest(t *testing.T) { source: &chronograf.Source{ ID: 1, Name: "I'm a really great source", - Type: chronograf.InfluxDB, + Type: chronograf.InfluxDBv1, Username: "fancy", Password: "i'm so", SharedSecret: "supersecret", @@ -89,7 +89,7 @@ func Test_ValidSourceRequest(t *testing.T) { source: &chronograf.Source{ ID: 1, Name: "I'm a really great source", - Type: chronograf.InfluxDB, + Type: chronograf.InfluxDBv1, Username: "fancy", Password: "i'm so", SharedSecret: "supersecret", @@ -104,7 +104,7 @@ func Test_ValidSourceRequest(t *testing.T) { source: &chronograf.Source{ ID: 1, Name: "I'm a really great source", - Type: chronograf.InfluxDB, + Type: chronograf.InfluxDBv1, Username: "fancy", Password: "i'm so", SharedSecret: "supersecret", @@ -156,7 +156,7 @@ func Test_ValidSourceRequest(t *testing.T) { source: &chronograf.Source{ ID: 1, Name: "I'm a really great source", - Type: chronograf.InfluxDBCloudDedicated, + Type: chronograf.InfluxDBv3CloudDedicated, Username: "", Password: "", SharedSecret: "supersecret", @@ -175,7 +175,7 @@ func Test_ValidSourceRequest(t *testing.T) { source: &chronograf.Source{ ID: 1, Name: "I'm a really great source", - Type: chronograf.InfluxDBCloudDedicated, + Type: chronograf.InfluxDBv3CloudDedicated, Username: "", Password: "", SharedSecret: "supersecret", @@ -197,7 +197,7 @@ func Test_ValidSourceRequest(t *testing.T) { source: &chronograf.Source{ ID: 1, Name: "I'm a really great source", - Type: chronograf.InfluxDBCloudDedicated, + Type: chronograf.InfluxDBv3CloudDedicated, Username: "", Password: "", SharedSecret: "supersecret", @@ -221,7 +221,7 @@ func Test_ValidSourceRequest(t *testing.T) { source: &chronograf.Source{ ID: 1, Name: "I'm a really great source", - Type: chronograf.InfluxDBCloudDedicated, + Type: chronograf.InfluxDBv3CloudDedicated, Username: "", Password: "", SharedSecret: "supersecret", @@ -246,7 +246,7 @@ func Test_ValidSourceRequest(t *testing.T) { source: &chronograf.Source{ ID: 1, Name: "I'm a really great source", - Type: chronograf.InfluxDBCloudDedicated, + Type: chronograf.InfluxDBv3CloudDedicated, Username: "", Password: "", SharedSecret: "supersecret", @@ -270,7 +270,7 @@ func Test_ValidSourceRequest(t *testing.T) { source: &chronograf.Source{ ID: 1, Name: "I'm a really great source", - Type: chronograf.InfluxDBCloudDedicated, + Type: chronograf.InfluxDBv3CloudDedicated, Username: "", Password: "", SharedSecret: "supersecret", @@ -295,7 +295,7 @@ func Test_ValidSourceRequest(t *testing.T) { source: &chronograf.Source{ ID: 1, Name: "I'm a really great source", - Type: chronograf.InfluxDBCloudDedicated, + Type: chronograf.InfluxDBv3CloudDedicated, Username: "", Password: "", SharedSecret: "supersecret", @@ -319,7 +319,7 @@ func Test_ValidSourceRequest(t *testing.T) { source: &chronograf.Source{ ID: 1, Name: "I'm a really great source", - Type: chronograf.InfluxDBCloudDedicated, + Type: chronograf.InfluxDBv3CloudDedicated, Username: "", Password: "", SharedSecret: "supersecret", @@ -381,13 +381,57 @@ func Test_ValidSourceRequest(t *testing.T) { err: fmt.Errorf("database token required"), }, }, + { + name: "InfluxDB 3 Enterprise - supported", + args: args{ + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBv3Enterprise, + DatabaseToken: "database-token", + URL: "http://www.any.url.com", + InsecureSkipVerify: true, + Default: true, + Telegraf: "telegraf", + }, + }, + wants: wants{ + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBv3Enterprise, + DatabaseToken: "database-token", + URL: "http://www.any.url.com", + InsecureSkipVerify: true, + Default: true, + Telegraf: "telegraf", + }, + }, + }, + { + name: "InfluxDB 3 Enterprise - missing database token", + args: args{ + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBv3Enterprise, + URL: "http://www.any.url.com", + InsecureSkipVerify: true, + Default: true, + Telegraf: "telegraf", + }, + }, + wants: wants{ + err: fmt.Errorf("database token required"), + }, + }, { name: "bad url", args: args{ source: &chronograf.Source{ ID: 1, Name: "I'm a really great source", - Type: chronograf.InfluxDB, + Type: chronograf.InfluxDBv1, Username: "fancy", Password: "i'm so", SharedSecret: "supersecret", diff --git a/server/swagger.json b/server/swagger.json index ca839950b2..47421653bf 100644 --- a/server/swagger.json +++ b/server/swagger.json @@ -5204,7 +5204,7 @@ "type": "string", "description": "Format of the data source", "readOnly": true, - "enum": ["influx", "influx-enterprise", "influx-relay"] + "enum": ["influx", "influx-enterprise", "influx-relay", "influx-v2", "influx-v3-core", "influx-v3-enterprise", "influx-v3-cloud-dedicated"] }, "username": { "type": "string", diff --git a/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx b/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx index 6bd2272c47..5fe10e1a66 100644 --- a/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx +++ b/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx @@ -5,8 +5,9 @@ import {Source, SourceAuthenticationMethod} from 'src/types' import {PageSection} from 'src/types/shared' import {WrapToPage} from './AdminInfluxDBScopedPage' import { - SOURCE_TYPE_INFLUX_CLOUD_DEDICATED, + SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED, SOURCE_TYPE_INFLUX_V3_CORE, + SOURCE_TYPE_INFLUX_V3_ENTERPRISE, } from 'src/shared/constants' interface Props { @@ -24,8 +25,9 @@ export function isConnectedToLDAP(source: Source) { export function isV3Source(source: Source) { return ( - source.type === SOURCE_TYPE_INFLUX_CLOUD_DEDICATED || - source.type === SOURCE_TYPE_INFLUX_V3_CORE + source.type === SOURCE_TYPE_INFLUX_V3_CORE || + source.type === SOURCE_TYPE_INFLUX_V3_ENTERPRISE || + source.type === SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED ) } diff --git a/ui/src/shared/components/TagListItem.tsx b/ui/src/shared/components/TagListItem.tsx index bc99c9c0b5..a48699c3d4 100644 --- a/ui/src/shared/components/TagListItem.tsx +++ b/ui/src/shared/components/TagListItem.tsx @@ -96,7 +96,9 @@ class TagListItem extends PureComponent { } const filterText = this.state.filterText.toLowerCase() - const filtered = tagValues.filter(v => v.toLowerCase().includes(filterText)) + const filtered = tagValues.filter( + v => !!v && v.toLowerCase().includes(filterText) + ) return (
diff --git a/ui/src/shared/constants/index.ts b/ui/src/shared/constants/index.ts index 02d1f7cec5..4e3de0eec6 100644 --- a/ui/src/shared/constants/index.ts +++ b/ui/src/shared/constants/index.ts @@ -525,11 +525,12 @@ export const MIN_SIZE = 0 export const QUERY_BUILDER_LIST_ITEM_HEIGHT = 28 export const SOURCE_TYPE_INFLUX_V1 = 'influx' -export const SOURCE_TYPE_INFLUX_ENTERPRISE = 'influx-enterprise' -export const SOURCE_TYPE_INFLUX_RELAY = 'influx-relay' +export const SOURCE_TYPE_INFLUX_V1_ENTERPRISE = 'influx-enterprise' +export const SOURCE_TYPE_INFLUX_V1_RELAY = 'influx-relay' export const SOURCE_TYPE_INFLUX_V2 = 'influx-v2' export const SOURCE_TYPE_INFLUX_V3_CORE = 'influx-v3-core' -export const SOURCE_TYPE_INFLUX_CLOUD_DEDICATED = 'influx-cloud-dedicated' +export const SOURCE_TYPE_INFLUX_V3_ENTERPRISE = 'influx-v3-enterprise' +export const SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED = 'influx-v3-cloud-dedicated' export enum DataType { flux = 'flux', diff --git a/ui/src/sources/components/SourceStep.tsx b/ui/src/sources/components/SourceStep.tsx index b58ef844fd..243639c202 100644 --- a/ui/src/sources/components/SourceStep.tsx +++ b/ui/src/sources/components/SourceStep.tsx @@ -32,12 +32,13 @@ import { import {insecureSkipVerifyText} from 'src/shared/copy/tooltipText' import { DEFAULT_SOURCE, - SOURCE_TYPE_INFLUX_CLOUD_DEDICATED, - SOURCE_TYPE_INFLUX_ENTERPRISE, - SOURCE_TYPE_INFLUX_RELAY, SOURCE_TYPE_INFLUX_V1, + SOURCE_TYPE_INFLUX_V1_ENTERPRISE, + SOURCE_TYPE_INFLUX_V1_RELAY, SOURCE_TYPE_INFLUX_V2, + SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED, SOURCE_TYPE_INFLUX_V3_CORE, + SOURCE_TYPE_INFLUX_V3_ENTERPRISE, } from 'src/shared/constants' import {SUPERADMIN_ROLE} from 'src/auth/roles' @@ -131,7 +132,11 @@ class SourceStep extends PureComponent { label: 'InfluxDB 3 Core', }, { - value: SOURCE_TYPE_INFLUX_CLOUD_DEDICATED, + value: SOURCE_TYPE_INFLUX_V3_ENTERPRISE, + label: 'InfluxDB 3 Enterprise', + }, + { + value: SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED, label: 'InfluxDB Cloud Dedicated', }, ]} @@ -182,8 +187,9 @@ class SourceStep extends PureComponent { )} - {/* InfluxDB 3 Core fields */} - {this.state.serverType === SOURCE_TYPE_INFLUX_V3_CORE && ( + {/* InfluxDB 3 Core/Enterprise fields */} + {(this.state.serverType === SOURCE_TYPE_INFLUX_V3_CORE || + this.state.serverType === SOURCE_TYPE_INFLUX_V3_ENTERPRISE) && ( <> { )} {/* InfluxDB Cloud Dedicated fields */} - {this.state.serverType === SOURCE_TYPE_INFLUX_CLOUD_DEDICATED && ( + {this.state.serverType === SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED && ( <> { source.type === SOURCE_TYPE_INFLUX_V1 || source.type === SOURCE_TYPE_INFLUX_V2 || source.type === SOURCE_TYPE_INFLUX_V3_CORE || - source.type === SOURCE_TYPE_INFLUX_CLOUD_DEDICATED + source.type === SOURCE_TYPE_INFLUX_V3_ENTERPRISE || + source.type === SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED ) { return source.type } if ( - source.type === SOURCE_TYPE_INFLUX_ENTERPRISE || - source.type === SOURCE_TYPE_INFLUX_RELAY + source.type === SOURCE_TYPE_INFLUX_V1_ENTERPRISE || + source.type === SOURCE_TYPE_INFLUX_V1_RELAY ) { // Special v1 subtypes are displayed as v1 return SOURCE_TYPE_INFLUX_V1 @@ -410,13 +417,16 @@ class SourceStep extends PureComponent { switch (value) { case SOURCE_TYPE_INFLUX_V2: - this.changeSourceType(SOURCE_TYPE_INFLUX_V2, '2.x') + this.changeSourceType(value, '2.x') break case SOURCE_TYPE_INFLUX_V3_CORE: - this.changeSourceType(SOURCE_TYPE_INFLUX_V3_CORE, '3.x') + this.changeSourceType(value, '3.x') + break + case SOURCE_TYPE_INFLUX_V3_ENTERPRISE: + this.changeSourceType(value, '3.x') break - case SOURCE_TYPE_INFLUX_CLOUD_DEDICATED: - this.changeSourceType(SOURCE_TYPE_INFLUX_CLOUD_DEDICATED, 'cloud') + case SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED: + this.changeSourceType(value, 'cloud') break case SOURCE_TYPE_INFLUX_V1: default: From abe43a8413d682603eae060b675f6140076101b7 Mon Sep 17 00:00:00 2001 From: Jan Simon Date: Tue, 30 Sep 2025 23:13:35 +0200 Subject: [PATCH 24/50] fix: add new source types to MultiSourceBuilder --- server/builders.go | 61 ++++++++++++++++++++++------------------- server/builders_test.go | 2 +- server/server.go | 9 +++++- 3 files changed, 42 insertions(+), 30 deletions(-) diff --git a/server/builders.go b/server/builders.go index 64810edd37..f22e8163b0 100644 --- a/server/builders.go +++ b/server/builders.go @@ -1,6 +1,8 @@ package server import ( + "fmt" + "github.com/influxdata/chronograf" "github.com/influxdata/chronograf/canned" "github.com/influxdata/chronograf/filestore" @@ -107,7 +109,7 @@ func (builder *MultiDashboardBuilder) Build(db chronograf.DashboardsStore) (*mul // SourcesBuilder builds a MultiSourceStore type SourcesBuilder interface { - Build(chronograf.SourcesStore) (*multistore.SourcesStore, error) + Build(chronograf.SourcesStore, string) (*multistore.SourcesStore, error) } // MultiSourceBuilder implements SourcesBuilder @@ -129,29 +131,27 @@ type MultiSourceBuilder struct { } // Build will return a MultiSourceStore -func (fs *MultiSourceBuilder) Build(db chronograf.SourcesStore) (*multistore.SourcesStore, error) { +func (fs *MultiSourceBuilder) Build(db chronograf.SourcesStore, defaultOrgID string) (*multistore.SourcesStore, error) { // These dashboards are those handled from a directory files := filestore.NewSources(fs.Path, fs.ID, fs.Logger) stores := []chronograf.SourcesStore{db, files} - // TODO simon: also process fs.InfluxDBType! if fs.InfluxDBURL != "" { var influxdbType, username, password string var clusterID, accountID, mgmtToken, dbToken, tagsCSVPath string - if fs.InfluxDBClusterID != "" && fs.InfluxDBAccountID != "" && fs.InfluxDBToken != "" && fs.InfluxDBMgmtToken != "" { + if fs.InfluxDBType == chronograf.InfluxDBv3Core || fs.InfluxDBType == chronograf.InfluxDBv3Enterprise { + // InfluxDB 3 Core, InfluxDB 3 Enterprise + influxdbType = fs.InfluxDBType + dbToken = fs.InfluxDBToken + } else if fs.InfluxDBType == chronograf.InfluxDBv3CloudDedicated { // InfluxDB Cloud Dedicated - influxdbType = chronograf.InfluxDBv3CloudDedicated + influxdbType = fs.InfluxDBType clusterID = fs.InfluxDBClusterID accountID = fs.InfluxDBAccountID mgmtToken = fs.InfluxDBMgmtToken dbToken = fs.InfluxDBToken tagsCSVPath = fs.TagsCSVPath - } else if fs.InfluxDBToken != "" { - // TODO simon: this is not fully correct, it can be either v3 Core or v3 Enterprise - // InfluxDB 3 Core/Enterprise - influxdbType = chronograf.InfluxDBv3Core - dbToken = fs.InfluxDBToken } else if fs.InfluxDBOrg == "" || fs.InfluxDBToken == "" { // v1 InfluxDB username = fs.InfluxDBUsername @@ -164,24 +164,29 @@ func (fs *MultiSourceBuilder) Build(db chronograf.SourcesStore) (*multistore.Sou influxdbType = chronograf.InfluxDBv2 } - influxStore := &memdb.SourcesStore{ - // TODO simon: validate the Source before adding, reuse ValidSourceRequest! - Source: &chronograf.Source{ - ID: 0, - Name: fs.InfluxDBURL, - Type: influxdbType, - Username: username, - Password: password, - ClusterID: clusterID, - AccountID: accountID, - ManagementToken: mgmtToken, - DatabaseToken: dbToken, - TagsCSVPath: tagsCSVPath, - URL: fs.InfluxDBURL, - Default: true, - Version: "unknown", // a real version is re-fetched at runtime; use "unknown" version as a fallback, empty version would imply OSS 2.x - }} - stores = append([]chronograf.SourcesStore{influxStore}, stores...) + source := chronograf.Source{ + ID: 0, + Name: fs.InfluxDBURL, + Type: influxdbType, + Username: username, + Password: password, + ClusterID: clusterID, + AccountID: accountID, + ManagementToken: mgmtToken, + DatabaseToken: dbToken, + TagsCSVPath: tagsCSVPath, + URL: fs.InfluxDBURL, + Default: true, + Version: "unknown", // a real version is re-fetched at runtime; use "unknown" version as a fallback, empty version would imply OSS 2.x + } + + if err := ValidSourceRequest(&source, defaultOrgID); err == nil { + influxStore := &memdb.SourcesStore{Source: &source} + stores = append([]chronograf.SourcesStore{influxStore}, stores...) + } else { + // Log the error and ignore + fs.Logger.Error(fmt.Sprintf("Invalid %s source: %s", influxdbType, err)) + } } sources := &multistore.SourcesStore{ Stores: stores, diff --git a/server/builders_test.go b/server/builders_test.go index 39facf7794..f7347fabfc 100644 --- a/server/builders_test.go +++ b/server/builders_test.go @@ -20,7 +20,7 @@ func TestLayoutBuilder(t *testing.T) { func TestSourcesStoresBuilder(t *testing.T) { var b server.SourcesBuilder = &server.MultiSourceBuilder{} - sources, err := b.Build(nil) + sources, err := b.Build(nil, "") if err != nil { t.Fatalf("MultiSourceBuilder can't build a MultiSourcesStore: %v", err) } diff --git a/server/server.go b/server/server.go index ce4f0b96b3..2f574f12ab 100644 --- a/server/server.go +++ b/server/server.go @@ -824,6 +824,13 @@ func openService(ctx context.Context, db kv.Store, builder builders, logger chro Error("Unable to construct a MultiOrganizationStore", err) os.Exit(1) } + defaultOrg, err := organizations.DefaultOrganization(ctx) + if err != nil { + logger. + WithField("component", "OrganizationsStore"). + Error("Unable to get default organization", err) + os.Exit(1) + } kapacitors, err := builder.Kapacitors.Build(svc.ServersStore()) if err != nil { @@ -833,7 +840,7 @@ func openService(ctx context.Context, db kv.Store, builder builders, logger chro os.Exit(1) } - sources, err := builder.Sources.Build(svc.SourcesStore()) + sources, err := builder.Sources.Build(svc.SourcesStore(), defaultOrg.ID) if err != nil { logger. WithField("component", "SourcesStore"). From e7a0551eda91b28bb4dffb71dfcde92058cc750c Mon Sep 17 00:00:00 2001 From: Jan Simon Date: Wed, 8 Oct 2025 09:58:00 +0200 Subject: [PATCH 25/50] feat: add InfluxDB Clustered support --- chronograf.go | 9 ++- influx/cloud_dedicated.go | 55 ++++------------ influx/influx.go | 59 ++++++++++------- server/builders.go | 5 ++ server/server.go | 2 +- server/sources.go | 12 ++++ server/sources_test.go | 65 +++++++++++++++++++ server/swagger.json | 2 +- .../influxdb/AdminInfluxDBTabbedPage.tsx | 2 + ui/src/shared/constants/index.ts | 1 + ui/src/sources/components/SourceStep.tsx | 28 ++++++++ 11 files changed, 168 insertions(+), 72 deletions(-) diff --git a/chronograf.go b/chronograf.go index ccd4b98e01..ea989e4408 100644 --- a/chronograf.go +++ b/chronograf.go @@ -101,17 +101,20 @@ const ( // InfluxDBv2 is Influx DB 2.x with Token authentication InfluxDBv2 = "influx-v2" - // InfluxDBv3Core is InfluxDB 3 Core + // InfluxDBv3Core is InfluxDB 3 Core (self-managed) InfluxDBv3Core = "influx-v3-core" - // InfluxDBv3Enterprise is InfluxDB 3 Enterprise + // InfluxDBv3Enterprise is InfluxDB 3 Enterprise (self-managed) InfluxDBv3Enterprise = "influx-v3-enterprise" - // InfluxDBv3CloudDedicated is InfluxDB Cloud Dedicated + // InfluxDBv3Clustered is InfluxDB Clustered (self-managed) + InfluxDBv3Clustered = "influx-v3-clustered" + // InfluxDBv3CloudDedicated is InfluxDB Cloud Dedicated (fully-managed) InfluxDBv3CloudDedicated = "influx-v3-cloud-dedicated" ) func IsV3SrcType(srcType string) bool { return srcType == InfluxDBv3Core || srcType == InfluxDBv3Enterprise || + srcType == InfluxDBv3Clustered || srcType == InfluxDBv3CloudDedicated } diff --git a/influx/cloud_dedicated.go b/influx/cloud_dedicated.go index 7ebd251437..5a7203963a 100644 --- a/influx/cloud_dedicated.go +++ b/influx/cloud_dedicated.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "net/http" - "net/url" "strings" "github.com/influxdata/chronograf" @@ -49,13 +48,14 @@ func mustParseExpr(expr string) influxql.Expr { return exp } -// validateCloudDedicatedAuth checks both the management endpoint and the database endpoint to validate authentication. -func (c *Client) validateCloudDedicatedAuth(ctx context.Context) error { +// validateClusteredOrCloudDedicatedAuth checks both the management endpoint and the database endpoint to validate authentication. +// Used for InfluxDB Clustered and InfluxDB Cloud Dedicated. +func (c *Client) validateClusteredOrCloudDedicatedAuth(ctx context.Context) error { var req *http.Request var err error // Call list databases on management api. - if req, _, err = c.newListDatabasesRequestForCloudDedicated(ctx); err != nil { + if req, _, err = c.newListDatabasesRequestViaMgmtApi(ctx); err != nil { return fmt.Errorf("management authentication failed: %w", err) } if err = c.executeRequest(err, req); err != nil { @@ -63,20 +63,18 @@ func (c *Client) validateCloudDedicatedAuth(ctx context.Context) error { } // Call dummy query on query api. - if req, err = c.newDummyQueryRequestForCloudDedicated(ctx); err != nil { - return fmt.Errorf("database authentication failed: %w", err) - } - if err = c.executeRequest(err, req); err != nil { + if _, err := c.Query(ctx, chronograf.Query{Command: "SELECT * FROM dummy WHERE time > now()"}); err != nil { return fmt.Errorf("database authentication failed: %w", err) } return nil } -// showDatabasesForCloudDedicated list databases of InfluxDB Cloud Dedicated using the management api and wraps the results into chronograf.Response structure. -func (c *Client) showDatabasesForCloudDedicated(ctx context.Context) (chronograf.Response, error) { +// showDatabasesViaMgmtApi lists databases using the management api and wraps the results into chronograf.Response structure. +// Used for InfluxDB Clustered and InfluxDB Cloud Dedicated. +func (c *Client) showDatabasesViaMgmtApi(ctx context.Context) (chronograf.Response, error) { // Prepare request. - req, logs, err := c.newListDatabasesRequestForCloudDedicated(ctx) + req, logs, err := c.newListDatabasesRequestViaMgmtApi(ctx) if err != nil { return nil, err } @@ -119,8 +117,9 @@ func (c *Client) showDatabasesForCloudDedicated(ctx context.Context) (chronograf return constructShowDatabasesResponse(dbNames), nil } -// newListDatabasesRequestForCloudDedicated constructs a new http.Request for listing databases in InfluxDB Cloud Dedicated. -func (c *Client) newListDatabasesRequestForCloudDedicated(ctx context.Context) (*http.Request, chronograf.Logger, error) { +// newListDatabasesRequestViaMgmtApi constructs a new http.Request for listing databases via Management API. +// Used for InfluxDB Clustered and InfluxDB Cloud Dedicated. +func (c *Client) newListDatabasesRequestViaMgmtApi(ctx context.Context) (*http.Request, chronograf.Logger, error) { req, err := http.NewRequest("GET", util.AppendPath(c.MgmtURL, "/databases").String(), nil) if err != nil { return nil, nil, err @@ -169,36 +168,6 @@ func constructShowDatabasesResponse(dbNames []string) chronograf.Response { } } -// newDummyQueryRequestForCloudDedicated constructs a http.Request to call a dummy query in InfluxDB Cloud Dedicated. -func (c *Client) newDummyQueryRequestForCloudDedicated(ctx context.Context) (*http.Request, error) { - u, err := url.Parse(c.URL.String()) - if err != nil { - return nil, err - } - u = util.AppendPath(u, "/query") - - form := url.Values{} - form.Set("q", "SELECT * FROM dummy") - req, err := http.NewRequest("POST", u.String(), strings.NewReader(form.Encode())) - if err != nil { - return nil, err - } - req = req.WithContext(ctx) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - logs := c.Logger. - WithField("component", "proxy"). - WithField("host", req.Host) - logs.Debug("/query") - - if c.Authorizer != nil { - if err := c.Authorizer.Set(req); err != nil { - logs.Error("Error setting authorization header ", err) - return nil, err - } - } - return req, err -} - func (c *Client) handleShowMeasurements(q chronograf.Query, logs chronograf.Logger) (chronograf.Response, error) { if c.csvTagsStore == nil { return nil, nil diff --git a/influx/influx.go b/influx/influx.go index 9697c6399c..198837e53d 100644 --- a/influx/influx.go +++ b/influx/influx.go @@ -148,7 +148,7 @@ type result struct { // include both the database and retention policy. In-flight requests can be // cancelled using the provided context. func (c *Client) Query(ctx context.Context, q chronograf.Query) (chronograf.Response, error) { - if c.SrcType == chronograf.InfluxDBv3CloudDedicated { + if c.SrcType == chronograf.InfluxDBv3Clustered || c.SrcType == chronograf.InfluxDBv3CloudDedicated { logs := c.Logger. WithField("component", "proxy"). WithField("command", q.Command) @@ -156,7 +156,7 @@ func (c *Client) Query(ctx context.Context, q chronograf.Query) (chronograf.Resp cmdUpper := strings.ToUpper(q.Command) switch { case cmdUpper == "SHOW DATABASES": - return c.showDatabasesForCloudDedicated(ctx) + return c.showDatabasesViaMgmtApi(ctx) case strings.Contains(cmdUpper, "SHOW MEASUREMENTS"): if resp, err := c.handleShowMeasurements(q, logs); resp != nil || err != nil { @@ -182,7 +182,7 @@ func (c *Client) Query(ctx context.Context, q chronograf.Query) (chronograf.Resp // v3 Core, v3 Enterprise resp, err = c.queryV3(c.URL, q) } else { - // v1, v2, v3 Cloud Dedicated + // v1, v2, v3 Clustered, v3 Cloud Dedicated resp, err = c.query(c.URL, q) } resps <- result{resp, err} @@ -201,15 +201,15 @@ func (c *Client) ValidateAuth(ctx context.Context, src *chronograf.Source) error ctx, cancel := context.WithTimeout(ctx, 15*time.Second) defer cancel() - // v3 Cloud Dedicated: - if src.Type == chronograf.InfluxDBv3CloudDedicated { - return c.validateCloudDedicatedAuth(ctx) + // v3 Clustered, v3 Cloud Dedicated: + if src.Type == chronograf.InfluxDBv3Clustered || src.Type == chronograf.InfluxDBv3CloudDedicated { + return c.validateClusteredOrCloudDedicatedAuth(ctx) } // v2: use flux query if src.Type == chronograf.InfluxDBv2 { return c.validateV2Auth(ctx, src) } - // v1, v3 Core: use InfluxQL + // v1, v3 Core, v3 Enterprise: use InfluxQL if _, err := c.Query(ctx, chronograf.Query{Command: "SHOW DATABASES"}); err != nil { return err } @@ -288,8 +288,26 @@ func (c *Client) Connect(ctx context.Context, src *chronograf.Source) error { c.URL = u - // InfluxDB Cloud Dedicated also provides a management API. + if src.Type == chronograf.InfluxDBv3Clustered { + // InfluxDB Clustered also provides a management API. + accountID := "11111111-1111-1111-1111-111111111111" // hardcoded value + clusterID := "11111111-1111-1111-1111-111111111111" // hardcoded value + baseURL := *c.URL + baseURL.Path = "" + baseURL.RawQuery = "" + mgmtUrl := fmt.Sprintf("%s/api/v0/accounts/%s/clusters/%s", baseURL.String(), accountID, clusterID) + if u, err = url.Parse(mgmtUrl); err != nil { + return err + } + + c.MgmtURL = u + c.MgmtAuthorizer = &BearerToken{ + Token: src.ManagementToken, + } + } + if src.Type == chronograf.InfluxDBv3CloudDedicated { + // InfluxDB Cloud Dedicated also provides a management API. mgmtUrl := fmt.Sprintf("https://console.influxdata.com/api/v0/accounts/%s/clusters/%s", src.AccountID, src.ClusterID) if u, err = url.Parse(mgmtUrl); err != nil { return err @@ -413,21 +431,19 @@ func (c *Client) ping(u *url.URL) (string, string, error) { return b.Version, c.SrcType, nil } - // Check the `X-Influxdb-Build` header - builds := resp.Header.Values("X-Influxdb-Build") - isCloud2 := false - for _, build := range builds { - if build == "ENT" { - return build, chronograf.InfluxDBv1Enterprise, nil - } - if build == "cloud2" { - isCloud2 = true + if !c.isV3SrcType() { + // Check the `X-Influxdb-Build` header + builds := resp.Header.Values("X-Influxdb-Build") + for _, build := range builds { + if build == "ENT" { + return build, chronograf.InfluxDBv1Enterprise, nil + } } } // Read the version from the `X-Influxdb-Version` header version := resp.Header.Get("X-Influxdb-Version") - if version != "" { + if !c.isV3SrcType() && version != "" { if strings.Contains(version, "-c") { return version, chronograf.InfluxDBv1Enterprise, nil } else if strings.Contains(version, "relay") { @@ -441,12 +457,7 @@ func (c *Client) ping(u *url.URL) (string, string, error) { version = version[1:] } - if isCloud2 { - // TODO: improve this, other influxdb v3 version could also return "cloud2" - return version, chronograf.InfluxDBv3CloudDedicated, nil - } - - return version, chronograf.InfluxDBv1, nil + return version, c.SrcType, nil } // Write POSTs line protocol to a database and retention policy diff --git a/server/builders.go b/server/builders.go index f22e8163b0..e0d16c4098 100644 --- a/server/builders.go +++ b/server/builders.go @@ -144,6 +144,11 @@ func (fs *MultiSourceBuilder) Build(db chronograf.SourcesStore, defaultOrgID str // InfluxDB 3 Core, InfluxDB 3 Enterprise influxdbType = fs.InfluxDBType dbToken = fs.InfluxDBToken + } else if fs.InfluxDBType == chronograf.InfluxDBv3Clustered { + // InfluxDB Clustered + influxdbType = fs.InfluxDBType + mgmtToken = fs.InfluxDBMgmtToken + dbToken = fs.InfluxDBToken } else if fs.InfluxDBType == chronograf.InfluxDBv3CloudDedicated { // InfluxDB Cloud Dedicated influxdbType = fs.InfluxDBType diff --git a/server/server.go b/server/server.go index 2f574f12ab..920824c54e 100644 --- a/server/server.go +++ b/server/server.go @@ -58,7 +58,7 @@ type Server struct { Cert flags.Filename `long:"cert" description:"Path to PEM encoded public key certificate. " env:"TLS_CERTIFICATE"` Key flags.Filename `long:"key" description:"Path to private key associated with given certificate. " env:"TLS_PRIVATE_KEY"` - InfluxDBType string `long:"influxdb-type" value-name:"choice" choice:"influx" choice:"influx-enterprise" choice:"influx-relay" choice:"influx-v2" choice:"influx-v3-core" choice:"influx-v3-enterprise" choice:"influx-v3-cloud-dedicated" description:"InfluxDB server type instance" env:"INFLUXDB_TYPE"` + InfluxDBType string `long:"influxdb-type" value-name:"choice" choice:"influx" choice:"influx-enterprise" choice:"influx-relay" choice:"influx-v2" choice:"influx-v3-core" choice:"influx-v3-enterprise" choice:"influx-v3-clustered" choice:"influx-v3-cloud-dedicated" description:"InfluxDB server type instance" env:"INFLUXDB_TYPE"` InfluxDBURL string `long:"influxdb-url" description:"Location of your InfluxDB instance" env:"INFLUXDB_URL"` InfluxDBUsername string `long:"influxdb-username" description:"Username for your InfluxDB instance" env:"INFLUXDB_USERNAME"` InfluxDBPassword string `long:"influxdb-password" description:"Password for your InfluxDB instance" env:"INFLUXDB_PASSWORD"` diff --git a/server/sources.go b/server/sources.go index 2c26f01bc1..37d40cc355 100644 --- a/server/sources.go +++ b/server/sources.go @@ -230,6 +230,7 @@ func (s *Service) tsdbType(ctx context.Context, src *chronograf.Source) (string, if src.Type == chronograf.InfluxDBv2 || src.Type == chronograf.InfluxDBv3Core || src.Type == chronograf.InfluxDBv3Enterprise || + src.Type == chronograf.InfluxDBv3Clustered || src.Type == chronograf.InfluxDBv3CloudDedicated { return src.Type, nil // type selected by the user } @@ -516,6 +517,7 @@ func ValidSourceRequest(s *chronograf.Source, defaultOrgID string) error { s.Type != chronograf.InfluxDBv2 && s.Type != chronograf.InfluxDBv3Core && s.Type != chronograf.InfluxDBv3Enterprise && + s.Type != chronograf.InfluxDBv3Clustered && s.Type != chronograf.InfluxDBv3CloudDedicated { return fmt.Errorf("invalid source type %s", s.Type) } @@ -539,6 +541,16 @@ func ValidSourceRequest(s *chronograf.Source, defaultOrgID string) error { } } + if s.Type == chronograf.InfluxDBv3Clustered { + if len(s.ManagementToken) == 0 { + return fmt.Errorf("management token required") + } + if len(s.DatabaseToken) == 0 { + return fmt.Errorf("database token required") + } + // TODO simon: make management token optional, similarly to Cloud Dedicated + } + if s.Type == chronograf.InfluxDBv3CloudDedicated { if len(s.ClusterID) == 0 { return fmt.Errorf("cluster ID required") diff --git a/server/sources_test.go b/server/sources_test.go index d0aa09e10b..124132e6b7 100644 --- a/server/sources_test.go +++ b/server/sources_test.go @@ -425,6 +425,71 @@ func Test_ValidSourceRequest(t *testing.T) { err: fmt.Errorf("database token required"), }, }, + { + name: "InfluxDB 3 Clustered - supported", + args: args{ + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBv3Clustered, + ManagementToken: "mgmt-token", + DatabaseToken: "database-token", + URL: "http://www.any.url.com", + InsecureSkipVerify: true, + Default: true, + Telegraf: "telegraf", + }, + }, + wants: wants{ + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBv3Clustered, + ManagementToken: "mgmt-token", + DatabaseToken: "database-token", + URL: "http://www.any.url.com", + InsecureSkipVerify: true, + Default: true, + Telegraf: "telegraf", + }, + }, + }, + { + name: "InfluxDB 3 Clustered - missing management token", + args: args{ + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBv3Clustered, + DatabaseToken: "database-token", + URL: "http://www.any.url.com", + InsecureSkipVerify: true, + Default: true, + Telegraf: "telegraf", + }, + }, + wants: wants{ + err: fmt.Errorf("management token required"), + }, + }, + { + name: "InfluxDB 3 Clustered - missing database token", + args: args{ + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBv3Clustered, + ManagementToken: "mgmt-token", + URL: "http://www.any.url.com", + InsecureSkipVerify: true, + Default: true, + Telegraf: "telegraf", + }, + }, + wants: wants{ + err: fmt.Errorf("database token required"), + }, + }, { name: "bad url", args: args{ diff --git a/server/swagger.json b/server/swagger.json index 47421653bf..c4b22a3686 100644 --- a/server/swagger.json +++ b/server/swagger.json @@ -5204,7 +5204,7 @@ "type": "string", "description": "Format of the data source", "readOnly": true, - "enum": ["influx", "influx-enterprise", "influx-relay", "influx-v2", "influx-v3-core", "influx-v3-enterprise", "influx-v3-cloud-dedicated"] + "enum": ["influx", "influx-enterprise", "influx-relay", "influx-v2", "influx-v3-core", "influx-v3-enterprise", "influx-v3-clustered", "influx-v3-cloud-dedicated"] }, "username": { "type": "string", diff --git a/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx b/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx index 5fe10e1a66..8512b03937 100644 --- a/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx +++ b/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx @@ -6,6 +6,7 @@ import {PageSection} from 'src/types/shared' import {WrapToPage} from './AdminInfluxDBScopedPage' import { SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED, + SOURCE_TYPE_INFLUX_V3_CLUSTERED, SOURCE_TYPE_INFLUX_V3_CORE, SOURCE_TYPE_INFLUX_V3_ENTERPRISE, } from 'src/shared/constants' @@ -27,6 +28,7 @@ export function isV3Source(source: Source) { return ( source.type === SOURCE_TYPE_INFLUX_V3_CORE || source.type === SOURCE_TYPE_INFLUX_V3_ENTERPRISE || + source.type === SOURCE_TYPE_INFLUX_V3_CLUSTERED || source.type === SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED ) } diff --git a/ui/src/shared/constants/index.ts b/ui/src/shared/constants/index.ts index 4e3de0eec6..d4bdeda2c9 100644 --- a/ui/src/shared/constants/index.ts +++ b/ui/src/shared/constants/index.ts @@ -530,6 +530,7 @@ export const SOURCE_TYPE_INFLUX_V1_RELAY = 'influx-relay' export const SOURCE_TYPE_INFLUX_V2 = 'influx-v2' export const SOURCE_TYPE_INFLUX_V3_CORE = 'influx-v3-core' export const SOURCE_TYPE_INFLUX_V3_ENTERPRISE = 'influx-v3-enterprise' +export const SOURCE_TYPE_INFLUX_V3_CLUSTERED = 'influx-v3-clustered' export const SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED = 'influx-v3-cloud-dedicated' export enum DataType { diff --git a/ui/src/sources/components/SourceStep.tsx b/ui/src/sources/components/SourceStep.tsx index 243639c202..252aab50ec 100644 --- a/ui/src/sources/components/SourceStep.tsx +++ b/ui/src/sources/components/SourceStep.tsx @@ -37,6 +37,7 @@ import { SOURCE_TYPE_INFLUX_V1_RELAY, SOURCE_TYPE_INFLUX_V2, SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED, + SOURCE_TYPE_INFLUX_V3_CLUSTERED, SOURCE_TYPE_INFLUX_V3_CORE, SOURCE_TYPE_INFLUX_V3_ENTERPRISE, } from 'src/shared/constants' @@ -135,6 +136,11 @@ class SourceStep extends PureComponent { value: SOURCE_TYPE_INFLUX_V3_ENTERPRISE, label: 'InfluxDB 3 Enterprise', }, + { + value: SOURCE_TYPE_INFLUX_V3_CLUSTERED, + label: 'InfluxDB Clustered', + }, + // TODO simon: add InfluxDB Cloud Serverless { value: SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED, label: 'InfluxDB Cloud Dedicated', @@ -200,6 +206,24 @@ class SourceStep extends PureComponent { )} + {/* InfluxDB Clustered fields */} + {this.state.serverType === SOURCE_TYPE_INFLUX_V3_CLUSTERED && ( + <> + + + + )} + {/* InfluxDB Cloud Dedicated fields */} {this.state.serverType === SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED && ( <> @@ -398,6 +422,7 @@ class SourceStep extends PureComponent { source.type === SOURCE_TYPE_INFLUX_V2 || source.type === SOURCE_TYPE_INFLUX_V3_CORE || source.type === SOURCE_TYPE_INFLUX_V3_ENTERPRISE || + source.type === SOURCE_TYPE_INFLUX_V3_CLUSTERED || source.type === SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED ) { return source.type @@ -425,6 +450,9 @@ class SourceStep extends PureComponent { case SOURCE_TYPE_INFLUX_V3_ENTERPRISE: this.changeSourceType(value, '3.x') break + case SOURCE_TYPE_INFLUX_V3_CLUSTERED: + this.changeSourceType(value, '3.x') + break case SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED: this.changeSourceType(value, 'cloud') break From 58cf99beb31a8cd5cd69505c37df20d37448a90d Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Thu, 9 Oct 2025 17:06:20 +0200 Subject: [PATCH 26/50] docs: adding v3 todo --- V3TODO.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 V3TODO.md diff --git a/V3TODO.md b/V3TODO.md new file mode 100644 index 0000000000..65e9425904 --- /dev/null +++ b/V3TODO.md @@ -0,0 +1,31 @@ +# InfluxDB v3 support TODOs + +## Features + +- [ ] Support InfluxDB 3 Serverless +- [ ] UI should have old UI look for default +- [ ] Enable new UI look from settings + +## Issues + +- [ ] List databases for Core in Explorer shows fewer dbs than with `show databases` manually +- [ ] Command line help print-out wrongly formated due to new v3 option: +``` +/influxdb-type:choice[influx|influx-enterprise|influx-relay|influx-v2|influx-v3-core|influx-v3-enterprise|influx-v3-cloud-dedicated] +``` + +## Tests + +- [ ] Unit test for Update Source for cloud dedicated fields +- [ ] Unit test for New Source for cloud dedicate fields +- [ ] Unit test for Client cloud dedicated fields +- [ ] Unit test for query specific cloud dedicated fields +- [ ] After finalizing UI, fix Cypress tests + +## Polishing +- [ ] Handle TODOs in code +- [ ] Once all 5 v3 influxdb types are supported reorganize/refactor the code (cloud_dedicated.go, influx.go) to group similar server types + +## Enhancements +- [ ] UI: Better form validation to show errored field(s) +- [ ] UI: distinguish optional fields From 18b56100cb68180f3aa4df795b14288be38d8c24 Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Thu, 9 Oct 2025 17:07:08 +0200 Subject: [PATCH 27/50] test: fixing test on Windows --- kv/etcd/client_test.go | 7 ++++--- server/config/tls_options_test.go | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/kv/etcd/client_test.go b/kv/etcd/client_test.go index 2e1e865096..42354c5885 100644 --- a/kv/etcd/client_test.go +++ b/kv/etcd/client_test.go @@ -2,7 +2,6 @@ package etcd import ( "context" - "fmt" "io/ioutil" "net/url" "os" @@ -12,6 +11,7 @@ import ( "github.com/influxdata/chronograf" "github.com/influxdata/chronograf/kv" "github.com/influxdata/chronograf/mocks" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" clientv3 "go.etcd.io/etcd/client/v3" "go.etcd.io/etcd/server/v3/embed" @@ -119,7 +119,7 @@ func Test_WithURL(t *testing.T) { }, { url: parse("etcd://u:p@127.0.0.1:2379?cert=a&key=b&ca=c"), - err: "no such file or directory", + err: ".*file.*", }, { url: parse("etcd://a:b@1.2.3.4:5555?ca=test.crt&key=test.key&cert=test.crt"), @@ -157,7 +157,8 @@ func Test_WithURL(t *testing.T) { } else { require.NotNil(t, err) // Contains is used, because nested exceptions can evolve with go versions - require.Contains(t, fmt.Sprintf("%v", err), test.err) + //require.Contains(t, fmt.Sprintf("%v", err), test.err) + assert.Regexp(t, test.err, err.Error()) } }) } diff --git a/server/config/tls_options_test.go b/server/config/tls_options_test.go index 87a741baaf..15ab883f0f 100644 --- a/server/config/tls_options_test.go +++ b/server/config/tls_options_test.go @@ -3,10 +3,10 @@ package config_test import ( "crypto/tls" "crypto/x509" - "fmt" "testing" "github.com/influxdata/chronograf/server/config" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -144,7 +144,7 @@ func Test_CreateTLSConfig(t *testing.T) { Key: "tls_options_test.key", CACerts: "tls_options_test2.cert", }, - err: "open tls_options_test2.cert: no such file or directory", + err: "open tls_options_test2.cert:.*file.*", }, { name: "unsupported ca certs", @@ -175,8 +175,8 @@ func Test_CreateTLSConfig(t *testing.T) { } else { require.NotNil(t, err) require.Nil(t, config) - // Contains is used, because nested exceptions can evolve with go versions - require.Contains(t, fmt.Sprintf("%v", err), test.err) + // Regexp is used, because of platform difference message and nested exceptions can evolve with go versions + assert.Regexp(t, test.err, err.Error()) } }) } From 1e906576116efb1eda9bf779f6d22f4ecc2854b8 Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Thu, 9 Oct 2025 17:07:56 +0200 Subject: [PATCH 28/50] fix: fix migration on Windows --- kv/bolt/client.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kv/bolt/client.go b/kv/bolt/client.go index 2acced1f56..fe22c63389 100644 --- a/kv/bolt/client.go +++ b/kv/bolt/client.go @@ -5,7 +5,7 @@ import ( "fmt" "io" "os" - "path" + "path/filepath" "time" "github.com/influxdata/chronograf" @@ -239,7 +239,7 @@ func (c *client) backup(ctx context.Context, lastBuild, build chronograf.BuildIn // copy creates a copy of the database in toFile func (c *client) copy(ctx context.Context, version string) error { - backupDir := path.Join(path.Dir(c.path), "backup") + backupDir := filepath.Join(filepath.Dir(c.path), "backup") if _, err := os.Stat(backupDir); os.IsNotExist(err) { if err = os.Mkdir(backupDir, 0700); err != nil { return err @@ -254,8 +254,8 @@ func (c *client) copy(ctx context.Context, version string) error { } defer fromFile.Close() - toName := fmt.Sprintf("%s.%s", path.Base(c.path), version) - toPath := path.Join(backupDir, toName) + toName := fmt.Sprintf("%s.%s", filepath.Base(c.path), version) + toPath := filepath.Join(backupDir, toName) toFile, err := os.OpenFile(toPath, os.O_RDWR|os.O_CREATE, 0600) if err != nil { return err From 9d4d6f0a946c69ede422ca44c696f1e5d7567641 Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Thu, 9 Oct 2025 17:10:49 +0200 Subject: [PATCH 29/50] feat: Optional ClusterID,AccountID and Management Token --- chronograf.go | 3 + influx/cloud_dedicated.go | 90 ++-- influx/influx.go | 21 +- kv/internal/internal.go | 2 + kv/internal/internal.pb.go | 571 ++++++++++++----------- kv/internal/internal.proto | 1 + server/builders.go | 5 +- server/sources.go | 15 +- server/sources_test.go | 71 ++- ui/src/sources/components/SourceStep.tsx | 7 + ui/src/types/sources.ts | 1 + 11 files changed, 451 insertions(+), 336 deletions(-) diff --git a/chronograf.go b/chronograf.go index ea989e4408..0ecc5c0211 100644 --- a/chronograf.go +++ b/chronograf.go @@ -252,6 +252,8 @@ type Response interface { MarshalJSON() ([]byte, error) } +//TODO use password instead of databaseToken + // Source is connection information to a time-series data store. type Source struct { ID int `json:"id,string"` // ID is the unique ID of the source @@ -273,6 +275,7 @@ type Source struct { Organization string `json:"organization"` // Organization is the organization ID that resource belongs to Role string `json:"role,omitempty"` // Not Currently Used. Role is the name of the minimum role that a user must possess to access the resource. DefaultRP string `json:"defaultRP"` // DefaultRP is the default retention policy used in database queries to this source + DefaultDB string `json:"defaultDB,omitempty"` // DefaultDB is the default database used in queries for InfluxDB Cloud Dedicated when database list is not available Version string `json:"version,omitempty"` // Version of influxdb } diff --git a/influx/cloud_dedicated.go b/influx/cloud_dedicated.go index 5a7203963a..b0bd745281 100644 --- a/influx/cloud_dedicated.go +++ b/influx/cloud_dedicated.go @@ -54,14 +54,15 @@ func (c *Client) validateClusteredOrCloudDedicatedAuth(ctx context.Context) erro var req *http.Request var err error - // Call list databases on management api. - if req, _, err = c.newListDatabasesRequestViaMgmtApi(ctx); err != nil { - return fmt.Errorf("management authentication failed: %w", err) - } - if err = c.executeRequest(err, req); err != nil { - return fmt.Errorf("management authentication failed: %w", err) + if c.MgmtURL != nil { + // Call list databases on management api. + if req, _, err = c.newListDatabasesRequestViaMgmtApi(ctx); err != nil { + return fmt.Errorf("management authentication failed: %w", err) + } + if err = c.executeRequest(err, req); err != nil { + return fmt.Errorf("management authentication failed: %w", err) + } } - // Call dummy query on query api. if _, err := c.Query(ctx, chronograf.Query{Command: "SELECT * FROM dummy WHERE time > now()"}); err != nil { return fmt.Errorf("database authentication failed: %w", err) @@ -73,46 +74,51 @@ func (c *Client) validateClusteredOrCloudDedicatedAuth(ctx context.Context) erro // showDatabasesViaMgmtApi lists databases using the management api and wraps the results into chronograf.Response structure. // Used for InfluxDB Clustered and InfluxDB Cloud Dedicated. func (c *Client) showDatabasesViaMgmtApi(ctx context.Context) (chronograf.Response, error) { - // Prepare request. - req, logs, err := c.newListDatabasesRequestViaMgmtApi(ctx) - if err != nil { - return nil, err - } + var dbNames []string + if c.MgmtURL == nil { + dbNames = []string{c.DefaultDB} + } else { + // Prepare request. + req, logs, err := c.newListDatabasesRequestViaMgmtApi(ctx) + if err != nil { + return nil, err + } - // Do request. - hc := &http.Client{} - hc.Transport = SharedTransport(c.InsecureSkipVerify) - resp, err := hc.Do(req) - if err != nil { - if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { - return nil, chronograf.ErrUpstreamTimeout + // Do request. + hc := &http.Client{} + hc.Transport = SharedTransport(c.InsecureSkipVerify) + resp, err := hc.Do(req) + if err != nil { + if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { + return nil, chronograf.ErrUpstreamTimeout + } + return nil, err + } + defer resp.Body.Close() + + // Handle non-OK status. + if resp.StatusCode != http.StatusOK { + var errorResponse cdListDatabasesError + dec := json.NewDecoder(resp.Body) + _ = dec.Decode(&errorResponse) + return nil, fmt.Errorf("received status code %d from server: err: %s", resp.StatusCode, errorResponse.Message) } - return nil, err - } - defer resp.Body.Close() - // Handle non-OK status. - if resp.StatusCode != http.StatusOK { - var errorResponse cdListDatabasesError + // Decode response. + var databases []cdDatabase dec := json.NewDecoder(resp.Body) - _ = dec.Decode(&errorResponse) - return nil, fmt.Errorf("received status code %d from server: err: %s", resp.StatusCode, errorResponse.Message) - } - - // Decode response. - var databases []cdDatabase - dec := json.NewDecoder(resp.Body) - decErr := dec.Decode(&databases) - if decErr != nil { - logs.WithField("influx_status", resp.StatusCode). - Error("Error parsing results from influxdb: err:", decErr) - return nil, decErr - } + decErr := dec.Decode(&databases) + if decErr != nil { + logs.WithField("influx_status", resp.StatusCode). + Error("Error parsing results from influxdb: err:", decErr) + return nil, decErr + } - // Convert response. - dbNames := make([]string, len(databases)) - for i, db := range databases { - dbNames[i] = db.Name + // Convert response. + dbNames = make([]string, len(databases)) + for i, db := range databases { + dbNames[i] = db.Name + } } return constructShowDatabasesResponse(dbNames), nil } diff --git a/influx/influx.go b/influx/influx.go index 198837e53d..d2305f41dd 100644 --- a/influx/influx.go +++ b/influx/influx.go @@ -47,6 +47,7 @@ type Client struct { InsecureSkipVerify bool SrcType string Logger chronograf.Logger + DefaultDB string csvTagsStore *CSVTagsStore // (optional) Store to load CSV tag files from source.TagsCSVPath directory } @@ -307,17 +308,19 @@ func (c *Client) Connect(ctx context.Context, src *chronograf.Source) error { } if src.Type == chronograf.InfluxDBv3CloudDedicated { - // InfluxDB Cloud Dedicated also provides a management API. - mgmtUrl := fmt.Sprintf("https://console.influxdata.com/api/v0/accounts/%s/clusters/%s", src.AccountID, src.ClusterID) - if u, err = url.Parse(mgmtUrl); err != nil { - return err - } + if len(src.AccountID) > 0 { + // InfluxDB Cloud Dedicated also provides a management API. + mgmtUrl := fmt.Sprintf("https://console.influxdata.com/api/v0/accounts/%s/clusters/%s", src.AccountID, src.ClusterID) + if u, err = url.Parse(mgmtUrl); err != nil { + return err + } - c.MgmtURL = u - c.MgmtAuthorizer = &BearerToken{ - Token: src.ManagementToken, + c.MgmtURL = u + c.MgmtAuthorizer = &BearerToken{ + Token: src.ManagementToken, + } } - + c.DefaultDB = src.DefaultDB if src.TagsCSVPath != "" { if c.csvTagsStore, err = NewCSVTagsStore(src.TagsCSVPath, c.Logger); err != nil { return err diff --git a/kv/internal/internal.go b/kv/internal/internal.go index 13678038a8..59850bf048 100644 --- a/kv/internal/internal.go +++ b/kv/internal/internal.go @@ -53,6 +53,7 @@ func MarshalSource(s chronograf.Source) ([]byte, error) { ManagementToken: s.ManagementToken, DatabaseToken: s.DatabaseToken, TagsCSVPath: s.TagsCSVPath, + DefaultDatabase: s.DefaultDB, }) } @@ -83,6 +84,7 @@ func UnmarshalSource(data []byte, s *chronograf.Source) error { s.ManagementToken = pb.ManagementToken s.DatabaseToken = pb.DatabaseToken s.TagsCSVPath = pb.TagsCSVPath + s.DefaultDB = pb.DefaultDatabase return nil } diff --git a/kv/internal/internal.pb.go b/kv/internal/internal.pb.go index c67f1773e1..4afb75ee5f 100644 --- a/kv/internal/internal.pb.go +++ b/kv/internal/internal.pb.go @@ -44,7 +44,8 @@ type Source struct { AccountID string `protobuf:"bytes,17,opt,name=AccountID,proto3" json:"AccountID,omitempty"` // Account ID of an InfluxDB Cloud Dedicated source ManagementToken string `protobuf:"bytes,18,opt,name=ManagementToken,proto3" json:"ManagementToken,omitempty"` // Management token of an InfluxDB Cloud Dedicated source DatabaseToken string `protobuf:"bytes,19,opt,name=DatabaseToken,proto3" json:"DatabaseToken,omitempty"` // Database token of an InfluxDB Cloud Dedicated or other InfluxDB 3 source - TagsCSVPath string `protobuf:"bytes,20,opt,name=TagsCSVPath,proto3" json:"TagsCSVPath,omitempty"` // TagsCSVPATH is the path to a directory containing CSV files (per db) with tags for the source + TagsCSVPath string `protobuf:"bytes,20,opt,name=TagsCSVPath,proto3" json:"TagsCSVPath,omitempty"` // TagsCSVPath is the path to a directory containing CSV files (per db) with tags for the source + DefaultDatabase string `protobuf:"bytes,21,opt,name=DefaultDatabase,proto3" json:"DefaultDatabase,omitempty"` // DefaultDatabase is the default database used in queries for InfluxDB Cloud Dedicated when database list is not available } func (x *Source) Reset() { @@ -219,6 +220,13 @@ func (x *Source) GetTagsCSVPath() string { return "" } +func (x *Source) GetDefaultDatabase() string { + if x != nil { + return x.DefaultDatabase + } + return "" +} + type Dashboard struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2458,7 +2466,7 @@ var File_internal_proto protoreflect.FileDescriptor var file_internal_proto_rawDesc = []byte{ 0x0a, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x12, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x22, 0xcc, 0x04, 0x0a, 0x06, 0x53, + 0x12, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x22, 0xf6, 0x04, 0x0a, 0x06, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, @@ -2495,286 +2503,289 @@ var file_internal_proto_rawDesc = []byte{ 0x65, 0x6e, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x54, 0x61, 0x67, 0x73, 0x43, 0x53, 0x56, 0x50, 0x61, 0x74, 0x68, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x54, 0x61, - 0x67, 0x73, 0x43, 0x53, 0x56, 0x50, 0x61, 0x74, 0x68, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x61, - 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x05, 0x63, - 0x65, 0x6c, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x43, - 0x65, 0x6c, 0x6c, 0x52, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x30, 0x0a, 0x09, 0x74, 0x65, - 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, - 0x65, 0x52, 0x09, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x12, 0x22, 0x0a, 0x0c, - 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x22, 0x92, 0x05, 0x0a, 0x0d, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x43, 0x65, - 0x6c, 0x6c, 0x12, 0x0c, 0x0a, 0x01, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x78, - 0x12, 0x0c, 0x0a, 0x01, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x79, 0x12, 0x0c, - 0x0a, 0x01, 0x77, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x77, 0x12, 0x0c, 0x0a, 0x01, - 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x68, 0x12, 0x29, 0x0a, 0x07, 0x71, 0x75, - 0x65, 0x72, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x69, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x07, 0x71, 0x75, - 0x65, 0x72, 0x69, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, - 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x0e, 0x0a, - 0x02, 0x49, 0x44, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x35, 0x0a, - 0x04, 0x61, 0x78, 0x65, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x69, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, - 0x43, 0x65, 0x6c, 0x6c, 0x2e, 0x41, 0x78, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, - 0x61, 0x78, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x06, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x73, 0x18, 0x0a, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, - 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x52, 0x06, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x73, 0x12, 0x28, 0x0a, - 0x06, 0x6c, 0x65, 0x67, 0x65, 0x6e, 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x4c, 0x65, 0x67, 0x65, 0x6e, 0x64, 0x52, - 0x06, 0x6c, 0x65, 0x67, 0x65, 0x6e, 0x64, 0x12, 0x3a, 0x0a, 0x0c, 0x74, 0x61, 0x62, 0x6c, 0x65, - 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x4f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x0c, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x4f, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x12, 0x3c, 0x0a, 0x0c, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, - 0x65, 0x6c, 0x64, 0x52, 0x0c, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, - 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, - 0x74, 0x12, 0x3d, 0x0a, 0x0d, 0x64, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x50, 0x6c, 0x61, 0x63, - 0x65, 0x73, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x2e, 0x44, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x50, 0x6c, 0x61, 0x63, 0x65, - 0x73, 0x52, 0x0d, 0x64, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x73, - 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x6f, 0x74, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x6e, 0x6f, 0x74, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x6e, 0x6f, 0x74, 0x65, 0x56, 0x69, 0x73, 0x69, - 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x6f, - 0x74, 0x65, 0x56, 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x1a, 0x47, 0x0a, 0x09, - 0x41, 0x78, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x41, 0x78, 0x69, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x47, 0x0a, 0x0d, 0x44, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, - 0x50, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x73, 0x45, 0x6e, 0x66, 0x6f, - 0x72, 0x63, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, 0x45, 0x6e, - 0x66, 0x6f, 0x72, 0x63, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x67, 0x69, 0x74, 0x73, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x64, 0x69, 0x67, 0x69, 0x74, 0x73, 0x22, 0xbc, - 0x01, 0x0a, 0x0c, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, - 0x2a, 0x0a, 0x10, 0x76, 0x65, 0x72, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x41, - 0x78, 0x69, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x76, 0x65, 0x72, 0x74, 0x69, - 0x63, 0x61, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x41, 0x78, 0x69, 0x73, 0x12, 0x30, 0x0a, 0x06, 0x73, - 0x6f, 0x72, 0x74, 0x42, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x61, 0x62, 0x6c, 0x65, - 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x06, 0x73, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x12, 0x1a, 0x0a, - 0x08, 0x77, 0x72, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x77, 0x72, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x26, 0x0a, 0x0e, 0x66, 0x69, 0x78, - 0x46, 0x69, 0x72, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x08, 0x52, 0x0e, 0x66, 0x69, 0x78, 0x46, 0x69, 0x72, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x75, 0x6d, - 0x6e, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x22, 0x70, 0x0a, - 0x0e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, - 0x22, 0x0a, 0x0c, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x4e, - 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, - 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, - 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x22, - 0x67, 0x0a, 0x05, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x10, 0x0a, 0x03, - 0x48, 0x65, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x48, 0x65, 0x78, 0x12, 0x12, - 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, - 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x3e, 0x0a, 0x06, 0x4c, 0x65, 0x67, 0x65, - 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x4f, 0x72, 0x69, 0x65, 0x6e, 0x74, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x4f, 0x72, 0x69, - 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xb2, 0x01, 0x0a, 0x04, 0x41, 0x78, 0x69, - 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x42, 0x6f, 0x75, 0x6e, 0x64, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x03, 0x52, 0x0c, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x42, - 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x12, 0x14, 0x0a, - 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, - 0x62, 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x73, - 0x75, 0x66, 0x66, 0x69, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x75, 0x66, - 0x66, 0x69, 0x78, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x61, 0x73, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x62, 0x61, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x61, 0x6c, 0x65, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x22, 0xdb, 0x01, - 0x0a, 0x08, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x65, - 0x6d, 0x70, 0x5f, 0x76, 0x61, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x74, 0x65, - 0x6d, 0x70, 0x56, 0x61, 0x72, 0x12, 0x2f, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, - 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, - 0x62, 0x65, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, - 0x12, 0x2d, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x67, 0x73, 0x43, 0x53, 0x56, 0x50, 0x61, 0x74, 0x68, 0x12, 0x28, 0x0a, 0x0f, 0x44, 0x65, 0x66, + 0x61, 0x75, 0x6c, 0x74, 0x44, 0x61, 0x74, 0x61, 0x62, 0x61, 0x73, 0x65, 0x18, 0x15, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0f, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x44, 0x61, 0x74, 0x61, 0x62, + 0x61, 0x73, 0x65, 0x22, 0xb4, 0x01, 0x0a, 0x09, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, + 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x49, + 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x05, 0x63, 0x65, 0x6c, 0x6c, 0x73, 0x18, 0x03, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, + 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x43, 0x65, 0x6c, 0x6c, 0x52, 0x05, 0x63, + 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x30, 0x0a, 0x09, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, + 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x52, 0x09, 0x74, 0x65, 0x6d, + 0x70, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, + 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4f, 0x72, + 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x92, 0x05, 0x0a, 0x0d, 0x44, + 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x43, 0x65, 0x6c, 0x6c, 0x12, 0x0c, 0x0a, 0x01, + 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x78, 0x12, 0x0c, 0x0a, 0x01, 0x79, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x79, 0x12, 0x0c, 0x0a, 0x01, 0x77, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x01, 0x77, 0x12, 0x0c, 0x0a, 0x01, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x01, 0x68, 0x12, 0x29, 0x0a, 0x07, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x18, + 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x07, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x35, 0x0a, 0x04, 0x61, 0x78, 0x65, 0x73, 0x18, + 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2e, 0x44, 0x61, 0x73, 0x68, 0x62, 0x6f, 0x61, 0x72, 0x64, 0x43, 0x65, 0x6c, 0x6c, 0x2e, 0x41, + 0x78, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x61, 0x78, 0x65, 0x73, 0x12, 0x27, + 0x0a, 0x06, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, + 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x52, + 0x06, 0x63, 0x6f, 0x6c, 0x6f, 0x72, 0x73, 0x12, 0x28, 0x0a, 0x06, 0x6c, 0x65, 0x67, 0x65, 0x6e, + 0x64, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2e, 0x4c, 0x65, 0x67, 0x65, 0x6e, 0x64, 0x52, 0x06, 0x6c, 0x65, 0x67, 0x65, 0x6e, + 0x64, 0x12, 0x3a, 0x0a, 0x0c, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2e, 0x54, 0x61, 0x62, 0x6c, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, + 0x0c, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x3c, 0x0a, + 0x0c, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0d, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x52, + 0x65, 0x6e, 0x61, 0x6d, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x0c, 0x66, + 0x69, 0x65, 0x6c, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x74, + 0x69, 0x6d, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x3d, 0x0a, 0x0d, 0x64, + 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x18, 0x0f, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x44, 0x65, + 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x52, 0x0d, 0x64, 0x65, 0x63, + 0x69, 0x6d, 0x61, 0x6c, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x6f, + 0x74, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x6f, 0x74, 0x65, 0x12, 0x26, + 0x0a, 0x0e, 0x6e, 0x6f, 0x74, 0x65, 0x56, 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, + 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x6f, 0x74, 0x65, 0x56, 0x69, 0x73, 0x69, + 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x1a, 0x47, 0x0a, 0x09, 0x41, 0x78, 0x65, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, + 0x41, 0x78, 0x69, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, + 0x47, 0x0a, 0x0d, 0x44, 0x65, 0x63, 0x69, 0x6d, 0x61, 0x6c, 0x50, 0x6c, 0x61, 0x63, 0x65, 0x73, + 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x73, 0x45, 0x6e, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, 0x45, 0x6e, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x64, + 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x67, 0x69, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x06, 0x64, 0x69, 0x67, 0x69, 0x74, 0x73, 0x22, 0xbc, 0x01, 0x0a, 0x0c, 0x54, 0x61, 0x62, + 0x6c, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2a, 0x0a, 0x10, 0x76, 0x65, 0x72, + 0x74, 0x69, 0x63, 0x61, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x41, 0x78, 0x69, 0x73, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x10, 0x76, 0x65, 0x72, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x54, 0x69, 0x6d, + 0x65, 0x41, 0x78, 0x69, 0x73, 0x12, 0x30, 0x0a, 0x06, 0x73, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2e, 0x52, 0x65, 0x6e, 0x61, 0x6d, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, + 0x06, 0x73, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x72, 0x61, 0x70, 0x70, + 0x69, 0x6e, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x72, 0x61, 0x70, 0x70, + 0x69, 0x6e, 0x67, 0x12, 0x26, 0x0a, 0x0e, 0x66, 0x69, 0x78, 0x46, 0x69, 0x72, 0x73, 0x74, 0x43, + 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x66, 0x69, 0x78, + 0x46, 0x69, 0x72, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x4a, 0x04, 0x08, 0x01, 0x10, + 0x02, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x22, 0x70, 0x0a, 0x0e, 0x52, 0x65, 0x6e, 0x61, 0x6d, + 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, + 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x12, + 0x18, 0x0a, 0x07, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x07, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x22, 0x67, 0x0a, 0x05, 0x43, 0x6f, 0x6c, + 0x6f, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, + 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x48, 0x65, 0x78, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x48, 0x65, 0x78, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x22, 0x3e, 0x0a, 0x06, 0x4c, 0x65, 0x67, 0x65, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, + 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x20, 0x0a, 0x0b, 0x4f, 0x72, 0x69, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x4f, 0x72, 0x69, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x22, 0xb2, 0x01, 0x0a, 0x04, 0x41, 0x78, 0x69, 0x73, 0x12, 0x22, 0x0a, 0x0c, 0x6c, + 0x65, 0x67, 0x61, 0x63, 0x79, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x03, 0x52, 0x0c, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x12, + 0x16, 0x0a, 0x06, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x06, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x16, 0x0a, + 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, + 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x75, 0x66, 0x66, 0x69, 0x78, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x75, 0x66, 0x66, 0x69, 0x78, 0x12, 0x12, 0x0a, + 0x04, 0x62, 0x61, 0x73, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x62, 0x61, 0x73, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x22, 0xdb, 0x01, 0x0a, 0x08, 0x54, 0x65, 0x6d, 0x70, + 0x6c, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x02, 0x49, 0x44, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x5f, 0x76, 0x61, 0x72, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x74, 0x65, 0x6d, 0x70, 0x56, 0x61, 0x72, 0x12, + 0x2f, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, - 0x1a, 0x0a, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x44, 0x22, 0x67, 0x0a, 0x0d, 0x54, - 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, - 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, - 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, - 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, - 0x65, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x22, 0xb5, 0x01, 0x0a, 0x0d, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, - 0x65, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x12, 0x0e, 0x0a, 0x02, 0x64, 0x62, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x64, 0x62, - 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x72, 0x70, - 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x61, 0x67, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x67, 0x4b, 0x65, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x66, - 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x6c, 0x75, 0x78, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x6c, 0x75, 0x78, 0x22, 0xb0, 0x02, 0x0a, - 0x06, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x55, - 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x55, - 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x61, 0x73, 0x73, 0x77, - 0x6f, 0x72, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x61, 0x73, 0x73, 0x77, - 0x6f, 0x72, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x72, 0x63, 0x49, 0x44, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x53, 0x72, 0x63, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x41, - 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x41, 0x63, 0x74, - 0x69, 0x76, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, - 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x12, 0x49, 0x6e, 0x73, 0x65, 0x63, - 0x75, 0x72, 0x65, 0x53, 0x6b, 0x69, 0x70, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x18, 0x09, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x12, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x53, 0x6b, 0x69, - 0x70, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, - 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x4d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4a, 0x53, 0x4f, 0x4e, 0x18, 0x0b, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4a, 0x53, 0x4f, 0x4e, 0x22, - 0x9e, 0x01, 0x0a, 0x06, 0x4c, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x20, 0x0a, 0x0b, 0x41, 0x70, - 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, - 0x4d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0b, 0x4d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x24, - 0x0a, 0x05, 0x43, 0x65, 0x6c, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x43, 0x65, 0x6c, 0x6c, 0x52, 0x05, 0x43, - 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x74, 0x6f, 0x66, 0x6c, 0x6f, 0x77, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x41, 0x75, 0x74, 0x6f, 0x66, 0x6c, 0x6f, 0x77, - 0x22, 0xca, 0x02, 0x0a, 0x04, 0x43, 0x65, 0x6c, 0x6c, 0x12, 0x0c, 0x0a, 0x01, 0x78, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x78, 0x12, 0x0c, 0x0a, 0x01, 0x79, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x01, 0x79, 0x12, 0x0c, 0x0a, 0x01, 0x77, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x01, 0x77, 0x12, 0x0c, 0x0a, 0x01, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, - 0x68, 0x12, 0x29, 0x0a, 0x07, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x51, 0x75, - 0x65, 0x72, 0x79, 0x52, 0x07, 0x71, 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x12, 0x0c, 0x0a, 0x01, - 0x69, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x01, 0x69, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, - 0x0a, 0x07, 0x79, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x03, 0x52, - 0x07, 0x79, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x79, 0x6c, 0x61, 0x62, - 0x65, 0x6c, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x79, 0x6c, 0x61, 0x62, 0x65, - 0x6c, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2c, 0x0a, 0x04, 0x61, 0x78, 0x65, 0x73, 0x18, 0x0b, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, - 0x43, 0x65, 0x6c, 0x6c, 0x2e, 0x41, 0x78, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, - 0x61, 0x78, 0x65, 0x73, 0x1a, 0x47, 0x0a, 0x09, 0x41, 0x78, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x41, 0x78, - 0x69, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x8b, 0x02, - 0x0a, 0x05, 0x51, 0x75, 0x65, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, - 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, - 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x44, 0x42, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x44, - 0x42, 0x12, 0x0e, 0x0a, 0x02, 0x52, 0x50, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x52, - 0x50, 0x12, 0x1a, 0x0a, 0x08, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x73, 0x18, 0x04, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x08, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x73, 0x12, 0x16, 0x0a, - 0x06, 0x57, 0x68, 0x65, 0x72, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x57, - 0x68, 0x65, 0x72, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x25, 0x0a, 0x05, 0x52, - 0x61, 0x6e, 0x67, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x05, 0x52, 0x61, 0x6e, - 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x08, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x53, 0x68, - 0x69, 0x66, 0x74, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x68, 0x69, 0x66, 0x74, 0x52, - 0x06, 0x53, 0x68, 0x69, 0x66, 0x74, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, - 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x22, 0x51, 0x0a, 0x09, 0x54, - 0x69, 0x6d, 0x65, 0x53, 0x68, 0x69, 0x66, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x4c, 0x61, 0x62, 0x65, - 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x12, - 0x0a, 0x04, 0x55, 0x6e, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x55, 0x6e, - 0x69, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x51, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x51, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0x33, - 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x55, 0x70, 0x70, 0x65, 0x72, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x55, 0x70, 0x70, 0x65, 0x72, 0x12, 0x14, 0x0a, - 0x05, 0x4c, 0x6f, 0x77, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x4c, 0x6f, - 0x77, 0x65, 0x72, 0x22, 0x5d, 0x0a, 0x09, 0x41, 0x6c, 0x65, 0x72, 0x74, 0x52, 0x75, 0x6c, 0x65, - 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, - 0x12, 0x12, 0x0a, 0x04, 0x4a, 0x53, 0x4f, 0x4e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x4a, 0x53, 0x4f, 0x4e, 0x12, 0x14, 0x0a, 0x05, 0x53, 0x72, 0x63, 0x49, 0x44, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x05, 0x53, 0x72, 0x63, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x4b, 0x61, - 0x70, 0x61, 0x49, 0x44, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4b, 0x61, 0x70, 0x61, - 0x49, 0x44, 0x22, 0xa4, 0x01, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, - 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, - 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x1a, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x53, - 0x63, 0x68, 0x65, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x53, 0x63, 0x68, - 0x65, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x05, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x52, 0x6f, - 0x6c, 0x65, 0x52, 0x05, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x53, 0x75, 0x70, - 0x65, 0x72, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x53, - 0x75, 0x70, 0x65, 0x72, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x22, 0x3e, 0x0a, 0x04, 0x52, 0x6f, 0x6c, - 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0xa5, 0x01, 0x0a, 0x07, 0x4d, 0x61, - 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x12, 0x32, 0x0a, 0x14, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, - 0x02, 0x49, 0x44, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x22, 0x0a, - 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x22, 0x54, 0x0a, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, + 0x61, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, + 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x2d, 0x0a, 0x05, 0x71, 0x75, + 0x65, 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x51, 0x75, 0x65, + 0x72, 0x79, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x49, 0x44, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x49, 0x44, 0x22, 0x67, 0x0a, 0x0d, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, + 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0xb5, + 0x01, 0x0a, 0x0d, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x51, 0x75, 0x65, 0x72, 0x79, + 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x64, 0x62, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x64, 0x62, 0x12, 0x0e, 0x0a, 0x02, 0x72, 0x70, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x72, 0x70, 0x12, 0x20, 0x0a, 0x0b, 0x6d, 0x65, + 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0b, 0x6d, 0x65, 0x61, 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x17, 0x0a, 0x07, + 0x74, 0x61, 0x67, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, + 0x61, 0x67, 0x4b, 0x65, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6b, + 0x65, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x4b, + 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x6c, 0x75, 0x78, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x66, 0x6c, 0x75, 0x78, 0x22, 0xb0, 0x02, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, - 0x52, 0x6f, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x44, 0x65, 0x66, 0x61, - 0x75, 0x6c, 0x74, 0x52, 0x6f, 0x6c, 0x65, 0x22, 0x32, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x12, 0x28, 0x0a, 0x04, 0x41, 0x75, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x14, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x04, 0x41, 0x75, 0x74, 0x68, 0x22, 0x3c, 0x0a, 0x0a, 0x41, - 0x75, 0x74, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2e, 0x0a, 0x12, 0x53, 0x75, 0x70, - 0x65, 0x72, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x4e, 0x65, 0x77, 0x55, 0x73, 0x65, 0x72, 0x73, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x53, 0x75, 0x70, 0x65, 0x72, 0x41, 0x64, 0x6d, 0x69, - 0x6e, 0x4e, 0x65, 0x77, 0x55, 0x73, 0x65, 0x72, 0x73, 0x22, 0x75, 0x0a, 0x12, 0x4f, 0x72, 0x67, - 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x26, 0x0a, 0x0e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, - 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x12, 0x37, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x56, 0x69, - 0x65, 0x77, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, - 0x22, 0x46, 0x0a, 0x0f, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x33, 0x0a, 0x07, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, - 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x52, - 0x07, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x22, 0x79, 0x0a, 0x0f, 0x4c, 0x6f, 0x67, 0x56, - 0x69, 0x65, 0x77, 0x65, 0x72, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x4e, - 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x1a, 0x0a, 0x08, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x05, 0x52, 0x08, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x09, 0x45, - 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, - 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, - 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x09, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, - 0x6e, 0x67, 0x73, 0x22, 0x4e, 0x0a, 0x0e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x45, 0x6e, 0x63, - 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, - 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, - 0x61, 0x6d, 0x65, 0x22, 0x3d, 0x0a, 0x09, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x66, 0x6f, - 0x12, 0x18, 0x0a, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x07, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x43, 0x6f, - 0x6d, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x43, 0x6f, 0x6d, 0x6d, - 0x69, 0x74, 0x42, 0x0c, 0x5a, 0x0a, 0x2e, 0x3b, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x10, 0x0a, + 0x03, 0x55, 0x52, 0x4c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x55, 0x52, 0x4c, 0x12, + 0x14, 0x0a, 0x05, 0x53, 0x72, 0x63, 0x49, 0x44, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, + 0x53, 0x72, 0x63, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x76, 0x65, 0x12, 0x22, 0x0a, + 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x2e, 0x0a, 0x12, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x53, 0x6b, 0x69, + 0x70, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x49, + 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x53, 0x6b, 0x69, 0x70, 0x56, 0x65, 0x72, 0x69, 0x66, + 0x79, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x4a, 0x53, 0x4f, 0x4e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x4a, 0x53, 0x4f, 0x4e, 0x22, 0x9e, 0x01, 0x0a, 0x06, 0x4c, 0x61, + 0x79, 0x6f, 0x75, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x02, 0x49, 0x44, 0x12, 0x20, 0x0a, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x41, 0x70, 0x70, 0x6c, 0x69, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x4d, 0x65, 0x61, 0x73, 0x75, 0x72, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x4d, 0x65, 0x61, + 0x73, 0x75, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x24, 0x0a, 0x05, 0x43, 0x65, 0x6c, 0x6c, + 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2e, 0x43, 0x65, 0x6c, 0x6c, 0x52, 0x05, 0x43, 0x65, 0x6c, 0x6c, 0x73, 0x12, 0x1a, + 0x0a, 0x08, 0x41, 0x75, 0x74, 0x6f, 0x66, 0x6c, 0x6f, 0x77, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x08, 0x41, 0x75, 0x74, 0x6f, 0x66, 0x6c, 0x6f, 0x77, 0x22, 0xca, 0x02, 0x0a, 0x04, 0x43, + 0x65, 0x6c, 0x6c, 0x12, 0x0c, 0x0a, 0x01, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, + 0x78, 0x12, 0x0c, 0x0a, 0x01, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x79, 0x12, + 0x0c, 0x0a, 0x01, 0x77, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x77, 0x12, 0x0c, 0x0a, + 0x01, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x68, 0x12, 0x29, 0x0a, 0x07, 0x71, + 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x07, 0x71, + 0x75, 0x65, 0x72, 0x69, 0x65, 0x73, 0x12, 0x0c, 0x0a, 0x01, 0x69, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x01, 0x69, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x79, 0x72, 0x61, 0x6e, + 0x67, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x03, 0x52, 0x07, 0x79, 0x72, 0x61, 0x6e, 0x67, + 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x79, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x09, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x07, 0x79, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x12, 0x0a, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x12, 0x2c, 0x0a, 0x04, 0x61, 0x78, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, + 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x43, 0x65, 0x6c, 0x6c, 0x2e, 0x41, + 0x78, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x61, 0x78, 0x65, 0x73, 0x1a, 0x47, + 0x0a, 0x09, 0x41, 0x78, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x24, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x41, 0x78, 0x69, 0x73, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x8b, 0x02, 0x0a, 0x05, 0x51, 0x75, 0x65, 0x72, + 0x79, 0x12, 0x18, 0x0a, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x44, + 0x42, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x44, 0x42, 0x12, 0x0e, 0x0a, 0x02, 0x52, + 0x50, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x52, 0x50, 0x12, 0x1a, 0x0a, 0x08, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x47, + 0x72, 0x6f, 0x75, 0x70, 0x42, 0x79, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x57, 0x68, 0x65, 0x72, 0x65, + 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x57, 0x68, 0x65, 0x72, 0x65, 0x73, 0x12, + 0x14, 0x0a, 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x25, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, + 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x05, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, + 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x53, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x12, 0x2b, 0x0a, 0x06, 0x53, 0x68, 0x69, 0x66, 0x74, 0x73, 0x18, 0x09, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x53, 0x68, 0x69, 0x66, 0x74, 0x52, 0x06, 0x53, 0x68, 0x69, 0x66, 0x74, + 0x73, 0x12, 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x54, 0x79, 0x70, 0x65, 0x22, 0x51, 0x0a, 0x09, 0x54, 0x69, 0x6d, 0x65, 0x53, 0x68, 0x69, + 0x66, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x55, 0x6e, 0x69, 0x74, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x55, 0x6e, 0x69, 0x74, 0x12, 0x1a, 0x0a, 0x08, + 0x51, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x51, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0x33, 0x0a, 0x05, 0x52, 0x61, 0x6e, 0x67, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x55, 0x70, 0x70, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x05, 0x55, 0x70, 0x70, 0x65, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x4c, 0x6f, 0x77, 0x65, 0x72, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x4c, 0x6f, 0x77, 0x65, 0x72, 0x22, 0x5d, 0x0a, + 0x09, 0x41, 0x6c, 0x65, 0x72, 0x74, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4a, 0x53, + 0x4f, 0x4e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4a, 0x53, 0x4f, 0x4e, 0x12, 0x14, + 0x0a, 0x05, 0x53, 0x72, 0x63, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x53, + 0x72, 0x63, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x4b, 0x61, 0x70, 0x61, 0x49, 0x44, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x4b, 0x61, 0x70, 0x61, 0x49, 0x44, 0x22, 0xa4, 0x01, 0x0a, + 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x12, 0x24, 0x0a, + 0x05, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x52, 0x6f, 0x6c, 0x65, 0x52, 0x05, 0x52, 0x6f, + 0x6c, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x53, 0x75, 0x70, 0x65, 0x72, 0x41, 0x64, 0x6d, 0x69, + 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x53, 0x75, 0x70, 0x65, 0x72, 0x41, 0x64, + 0x6d, 0x69, 0x6e, 0x22, 0x3e, 0x0a, 0x04, 0x52, 0x6f, 0x6c, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x4f, + 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, + 0x61, 0x6d, 0x65, 0x22, 0xa5, 0x01, 0x0a, 0x07, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, + 0x1a, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x53, + 0x63, 0x68, 0x65, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x53, 0x63, 0x68, + 0x65, 0x6d, 0x65, 0x12, 0x32, 0x0a, 0x14, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4f, + 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x14, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4f, 0x72, 0x67, 0x61, 0x6e, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x4f, 0x72, 0x67, 0x61, 0x6e, + 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x4f, + 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x54, 0x0a, 0x0c, 0x4f, + 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x49, + 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x44, 0x12, 0x12, 0x0a, 0x04, 0x4e, + 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, + 0x20, 0x0a, 0x0b, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x52, 0x6f, 0x6c, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x52, 0x6f, 0x6c, + 0x65, 0x22, 0x32, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x28, 0x0a, 0x04, 0x41, + 0x75, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x04, 0x41, 0x75, 0x74, 0x68, 0x22, 0x3c, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x2e, 0x0a, 0x12, 0x53, 0x75, 0x70, 0x65, 0x72, 0x41, 0x64, 0x6d, 0x69, + 0x6e, 0x4e, 0x65, 0x77, 0x55, 0x73, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x12, 0x53, 0x75, 0x70, 0x65, 0x72, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x4e, 0x65, 0x77, 0x55, 0x73, + 0x65, 0x72, 0x73, 0x22, 0x75, 0x0a, 0x12, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x26, 0x0a, 0x0e, 0x4f, 0x72, 0x67, + 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0e, 0x4f, 0x72, 0x67, 0x61, 0x6e, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, + 0x44, 0x12, 0x37, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, + 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, + 0x09, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x22, 0x46, 0x0a, 0x0f, 0x4c, 0x6f, + 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x33, 0x0a, + 0x07, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, + 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, + 0x77, 0x65, 0x72, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x52, 0x07, 0x43, 0x6f, 0x6c, 0x75, 0x6d, + 0x6e, 0x73, 0x22, 0x79, 0x0a, 0x0f, 0x4c, 0x6f, 0x67, 0x56, 0x69, 0x65, 0x77, 0x65, 0x72, 0x43, + 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x50, 0x6f, 0x73, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x50, 0x6f, 0x73, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x09, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, + 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, + 0x6e, 0x67, 0x52, 0x09, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x4e, 0x0a, + 0x0e, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x12, + 0x12, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x54, + 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x4e, 0x61, 0x6d, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3d, 0x0a, + 0x09, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x18, 0x0a, 0x07, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x43, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x42, 0x0c, 0x5a, 0x0a, + 0x2e, 0x3b, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( diff --git a/kv/internal/internal.proto b/kv/internal/internal.proto index ae0375d098..4e56bbfc46 100644 --- a/kv/internal/internal.proto +++ b/kv/internal/internal.proto @@ -23,6 +23,7 @@ message Source { string ManagementToken = 18; // Management token of an InfluxDB Cloud Dedicated source string DatabaseToken = 19; // Database token of an InfluxDB Cloud Dedicated or other InfluxDB 3 source string TagsCSVPath = 20; // TagsCSVPath is the path to a directory containing CSV files (per db) with tags for the source + string DefaultDatabase = 21; // DefaultDatabase is the default database used in queries for InfluxDB Cloud Dedicated when database list is not available } message Dashboard { diff --git a/server/builders.go b/server/builders.go index e0d16c4098..3a2bbd1f68 100644 --- a/server/builders.go +++ b/server/builders.go @@ -124,6 +124,7 @@ type MultiSourceBuilder struct { InfluxDBClusterID string InfluxDBAccountID string TagsCSVPath string + DefaultDB string Logger chronograf.Logger ID chronograf.ID @@ -139,7 +140,7 @@ func (fs *MultiSourceBuilder) Build(db chronograf.SourcesStore, defaultOrgID str if fs.InfluxDBURL != "" { var influxdbType, username, password string - var clusterID, accountID, mgmtToken, dbToken, tagsCSVPath string + var clusterID, accountID, mgmtToken, dbToken, tagsCSVPath, defaultDB string if fs.InfluxDBType == chronograf.InfluxDBv3Core || fs.InfluxDBType == chronograf.InfluxDBv3Enterprise { // InfluxDB 3 Core, InfluxDB 3 Enterprise influxdbType = fs.InfluxDBType @@ -157,6 +158,7 @@ func (fs *MultiSourceBuilder) Build(db chronograf.SourcesStore, defaultOrgID str mgmtToken = fs.InfluxDBMgmtToken dbToken = fs.InfluxDBToken tagsCSVPath = fs.TagsCSVPath + defaultDB = fs.DefaultDB } else if fs.InfluxDBOrg == "" || fs.InfluxDBToken == "" { // v1 InfluxDB username = fs.InfluxDBUsername @@ -181,6 +183,7 @@ func (fs *MultiSourceBuilder) Build(db chronograf.SourcesStore, defaultOrgID str DatabaseToken: dbToken, TagsCSVPath: tagsCSVPath, URL: fs.InfluxDBURL, + DefaultDB: defaultDB, Default: true, Version: "unknown", // a real version is re-fetched at runtime; use "unknown" version as a fallback, empty version would imply OSS 2.x } diff --git a/server/sources.go b/server/sources.go index 37d40cc355..cc70917dae 100644 --- a/server/sources.go +++ b/server/sources.go @@ -449,6 +449,7 @@ func (s *Service) UpdateSource(w http.ResponseWriter, r *http.Request) { src.Telegraf = req.Telegraf } src.DefaultRP = req.DefaultRP + src.DefaultDB = req.DefaultDB defaultOrg, err := s.Store.Organizations(ctx).DefaultOrganization(ctx) if err != nil { @@ -552,6 +553,16 @@ func ValidSourceRequest(s *chronograf.Source, defaultOrgID string) error { } if s.Type == chronograf.InfluxDBv3CloudDedicated { + if len(s.DatabaseToken) == 0 { + return fmt.Errorf("database token required") + } + // ClusterID, AccountID, and ManagementToken are not required for InfluxDB 3 Cloud Dedicated + if len(s.ClusterID) == 0 && len(s.AccountID) == 0 && len(s.ManagementToken) == 0 { + if len(s.DefaultDB) == 0 { + return fmt.Errorf("default database is required for queries") + } + return nil + } if len(s.ClusterID) == 0 { return fmt.Errorf("cluster ID required") } @@ -567,9 +578,7 @@ func ValidSourceRequest(s *chronograf.Source, defaultOrgID string) error { if len(s.ManagementToken) == 0 { return fmt.Errorf("management token required") } - if len(s.DatabaseToken) == 0 { - return fmt.Errorf("database token required") - } + } return nil diff --git a/server/sources_test.go b/server/sources_test.go index 124132e6b7..39929e7b33 100644 --- a/server/sources_test.go +++ b/server/sources_test.go @@ -191,6 +191,49 @@ func Test_ValidSourceRequest(t *testing.T) { }, }, }, + { + name: "InfluxDB Cloud Dedicated - without cluster/account IDs and management token", + args: args{ + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBv3CloudDedicated, + Username: "", + Password: "", + SharedSecret: "supersecret", + ClusterID: "", + AccountID: "", + ManagementToken: "", + DatabaseToken: "database-token", + URL: "http://www.any.url.com", + MetaURL: "http://www.so.meta.com", + InsecureSkipVerify: true, + Default: true, + Telegraf: "telegraf", + DefaultDB: "defaultDB", + }, + }, + wants: wants{ + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBv3CloudDedicated, + Username: "", + Password: "", + SharedSecret: "supersecret", + ClusterID: "", + AccountID: "", + ManagementToken: "", + DatabaseToken: "database-token", + URL: "http://www.any.url.com", + MetaURL: "http://www.so.meta.com", + InsecureSkipVerify: true, + Default: true, + Telegraf: "telegraf", + DefaultDB: "defaultDB", + }, + }, + }, { name: "InfluxDB Cloud Dedicated - missing cluster ID", args: args{ @@ -337,6 +380,30 @@ func Test_ValidSourceRequest(t *testing.T) { err: fmt.Errorf("database token required"), }, }, + { + name: "InfluxDB Cloud Dedicated - missing default DB", + args: args{ + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBv3CloudDedicated, + Username: "", + Password: "", + SharedSecret: "supersecret", + AccountID: "", + ManagementToken: "", + DatabaseToken: "database-token", + URL: "http://www.any.url.com", + MetaURL: "http://www.so.meta.com", + InsecureSkipVerify: true, + Default: true, + Telegraf: "telegraf", + }, + }, + wants: wants{ + err: fmt.Errorf("default database is required for queries"), + }, + }, { name: "InfluxDB 3 Core - supported", args: args{ @@ -523,7 +590,9 @@ func Test_ValidSourceRequest(t *testing.T) { } return } - if err.Error() != tt.wants.err.Error() { + if err != nil && tt.wants.err == nil { + t.Errorf("%q. ValidSourceRequest() = %q", tt.name, err) + } else if err.Error() != tt.wants.err.Error() { if err != nil && tt.wants.err != nil { if strings.HasPrefix(err.Error(), tt.wants.err.Error()) { // error messages vary between go versions, but they have the same prefixes diff --git a/ui/src/sources/components/SourceStep.tsx b/ui/src/sources/components/SourceStep.tsx index 252aab50ec..0b3102d7a0 100644 --- a/ui/src/sources/components/SourceStep.tsx +++ b/ui/src/sources/components/SourceStep.tsx @@ -267,6 +267,13 @@ class SourceStep extends PureComponent { label="Default Retention Policy" onChange={this.onChangeInput('defaultRP')} /> + {this.state.serverType === SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED && ( + + )} {this.isEnterprise && ( Date: Fri, 24 Oct 2025 13:31:38 +0200 Subject: [PATCH 30/50] feat: Adding config for hardcoded InfluxDB 3 Clustered related values --- influx/influx.go | 7 +- influx/influx_test.go | 364 ++++++++++++++++++++++++++++++++++++++++++ influx/v3config.go | 7 + server/server.go | 30 ++-- server/service.go | 1 + server/sources.go | 9 +- 6 files changed, 402 insertions(+), 16 deletions(-) create mode 100644 influx/v3config.go diff --git a/influx/influx.go b/influx/influx.go index d2305f41dd..d6305f7450 100644 --- a/influx/influx.go +++ b/influx/influx.go @@ -48,6 +48,7 @@ type Client struct { SrcType string Logger chronograf.Logger DefaultDB string + V3Config V3Config csvTagsStore *CSVTagsStore // (optional) Store to load CSV tag files from source.TagsCSVPath directory } @@ -291,8 +292,8 @@ func (c *Client) Connect(ctx context.Context, src *chronograf.Source) error { if src.Type == chronograf.InfluxDBv3Clustered { // InfluxDB Clustered also provides a management API. - accountID := "11111111-1111-1111-1111-111111111111" // hardcoded value - clusterID := "11111111-1111-1111-1111-111111111111" // hardcoded value + accountID := c.V3Config.ClusteredAccountID + clusterID := c.V3Config.ClusteredClusterID baseURL := *c.URL baseURL.Path = "" baseURL.RawQuery = "" @@ -310,7 +311,7 @@ func (c *Client) Connect(ctx context.Context, src *chronograf.Source) error { if src.Type == chronograf.InfluxDBv3CloudDedicated { if len(src.AccountID) > 0 { // InfluxDB Cloud Dedicated also provides a management API. - mgmtUrl := fmt.Sprintf("https://console.influxdata.com/api/v0/accounts/%s/clusters/%s", src.AccountID, src.ClusterID) + mgmtUrl := fmt.Sprintf("%s/api/v0/accounts/%s/clusters/%s", c.V3Config.CloudDedicatedManagementURL, src.AccountID, src.ClusterID) if u, err = url.Parse(mgmtUrl); err != nil { return err } diff --git a/influx/influx_test.go b/influx/influx_test.go index b64a5dd683..106b40130c 100644 --- a/influx/influx_test.go +++ b/influx/influx_test.go @@ -761,3 +761,367 @@ func Test_Query(t *testing.T) { } } } + +func Test_Influx_ValidateAuth_V3Core(t *testing.T) { + t.Parallel() + calledPath := "" + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + calledPath = r.URL.Path + // V3 Core uses /api/v3/query_influxql for SHOW DATABASES + if strings.HasSuffix(r.URL.Path, "/api/v3/query_influxql") { + rw.WriteHeader(http.StatusUnauthorized) + rw.Write([]byte(`{"error":"v3coreauthfailed"}`)) + if auth := r.Header.Get("Authorization"); auth != "Bearer my-db-token" { + t.Errorf("Expected Authorization 'Bearer my-db-token' but was: %v", auth) + } + } + })) + defer ts.Close() + for _, urlContext := range []string{"", "/ctx"} { + calledPath = "" + client, err := NewClient(ts.URL+urlContext, log.New(log.DebugLevel)) + if err != nil { + t.Fatal("Unexpected error initializing client: err:", err) + } + source := &chronograf.Source{ + URL: ts.URL + urlContext, + Type: chronograf.InfluxDBv3Core, + DatabaseToken: "my-db-token", + } + + client.Connect(context.Background(), source) + err = client.ValidateAuth(context.Background(), source) + if err == nil { + t.Fatal("Expected error but nil") + } + if !strings.Contains(err.Error(), "v3coreauthfailed") { + t.Errorf("Expected client error '%v' to contain server-sent error message", err) + } + expectedPath := urlContext + "/api/v3/query_influxql" + if calledPath != expectedPath { + t.Errorf("Path received: %v, want: %v ", calledPath, expectedPath) + } + } +} + +func Test_Influx_ValidateAuth_V3Enterprise(t *testing.T) { + t.Parallel() + calledPath := "" + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + calledPath = r.URL.Path + // V3 Enterprise uses /api/v3/query_influxql for SHOW DATABASES + if strings.HasSuffix(r.URL.Path, "/api/v3/query_influxql") { + rw.WriteHeader(http.StatusUnauthorized) + rw.Write([]byte(`{"error":"v3enterpriseauthfailed"}`)) + if auth := r.Header.Get("Authorization"); auth != "Bearer my-db-token" { + t.Errorf("Expected Authorization 'Bearer my-db-token' but was: %v", auth) + } + } + })) + defer ts.Close() + for _, urlContext := range []string{"", "/ctx"} { + calledPath = "" + client, err := NewClient(ts.URL+urlContext, log.New(log.DebugLevel)) + if err != nil { + t.Fatal("Unexpected error initializing client: err:", err) + } + source := &chronograf.Source{ + URL: ts.URL + urlContext, + Type: chronograf.InfluxDBv3Enterprise, + DatabaseToken: "my-db-token", + } + + client.Connect(context.Background(), source) + err = client.ValidateAuth(context.Background(), source) + if err == nil { + t.Fatal("Expected error but nil") + } + if !strings.Contains(err.Error(), "v3enterpriseauthfailed") { + t.Errorf("Expected client error '%v' to contain server-sent error message", err) + } + expectedPath := urlContext + "/api/v3/query_influxql" + if calledPath != expectedPath { + t.Errorf("Path received: %v, want: %v ", calledPath, expectedPath) + } + } +} + +func Test_Influx_ValidateAuth_V3Clustered(t *testing.T) { + t.Parallel() + mgmtAuthCalled := false + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + // V3 Clustered validates both management and database auth + if strings.Contains(r.URL.Path, "/api/v0/accounts/") { + // Management API call + mgmtAuthCalled = true + auth := r.Header.Get("Authorization") + if auth != "Bearer my-mgmt-token" { + t.Errorf("Expected Authorization 'Bearer my-mgmt-token' but was: %v", auth) + } + rw.WriteHeader(http.StatusUnauthorized) + rw.Write([]byte(`{"error":"mgmt auth failed"}`)) + } else if strings.HasSuffix(r.URL.Path, "/query") { + // Database query endpoint + auth := r.Header.Get("Authorization") + if auth != "Bearer my-db-token" { + t.Errorf("Expected Authorization 'Bearer my-db-token' but was: %v", auth) + } + rw.WriteHeader(http.StatusUnauthorized) + rw.Write([]byte(`{"error":"db auth failed"}`)) + } + })) + defer ts.Close() + + client, err := NewClient(ts.URL, log.New(log.DebugLevel)) + if err != nil { + t.Fatal("Unexpected error initializing client: err:", err) + } + source := &chronograf.Source{ + URL: ts.URL, + Type: chronograf.InfluxDBv3Clustered, + DatabaseToken: "my-db-token", + ManagementToken: "my-mgmt-token", + } + + client.Connect(context.Background(), source) + err = client.ValidateAuth(context.Background(), source) + if err == nil { + t.Fatal("Expected error but nil") + } + if !strings.Contains(err.Error(), "management authentication failed") { + t.Errorf("Expected error to contain 'management authentication failed' but was: %v", err) + } + if !mgmtAuthCalled { + t.Error("Expected management API to be called") + } +} + +func Test_Influx_ValidateAuth_V3CloudDedicated(t *testing.T) { + t.Parallel() + mgmtAuthCalled := false + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + // V3 Cloud Dedicated validates both management and database auth + if strings.Contains(r.URL.Path, "/api/v0/accounts/") { + // Management API call + mgmtAuthCalled = true + auth := r.Header.Get("Authorization") + if auth != "Bearer my-mgmt-token" { + t.Errorf("Expected Authorization 'Bearer my-mgmt-token' but was: %v", auth) + } + rw.WriteHeader(http.StatusUnauthorized) + rw.Write([]byte(`{"error":"mgmt auth failed"}`)) + } else if strings.HasSuffix(r.URL.Path, "/query") { + // Database query endpoint + auth := r.Header.Get("Authorization") + if auth != "Bearer my-db-token" { + t.Errorf("Expected Authorization 'Bearer my-db-token' but was: %v", auth) + } + rw.WriteHeader(http.StatusUnauthorized) + rw.Write([]byte(`{"error":"db auth failed"}`)) + } + })) + defer ts.Close() + + client, err := NewClient(ts.URL, log.New(log.DebugLevel)) + client.V3Config = influx.V3Config{ + CloudDedicatedManagementURL: ts.URL, + ClusteredAccountID: "test-account-id", + ClusteredClusterID: "test-cluster-id", + } + if err != nil { + t.Fatal("Unexpected error initializing client: err:", err) + } + source := &chronograf.Source{ + URL: ts.URL, + Type: chronograf.InfluxDBv3CloudDedicated, + DatabaseToken: "my-db-token", + ManagementToken: "my-mgmt-token", + AccountID: "test-account-id", + ClusterID: "test-cluster-id", + } + + client.Connect(context.Background(), source) + err = client.ValidateAuth(context.Background(), source) + if err == nil { + t.Fatal("Expected error but nil") + } + // Cloud Dedicated validates management auth first + if !strings.Contains(err.Error(), "management authentication failed") { + t.Errorf("Expected error to contain 'management authentication failed' but was: %v", err) + } + if !mgmtAuthCalled { + t.Error("Expected management API to be called") + } +} + +func Test_Influx_Authorization_V3Core(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if auth != "Bearer test-token-core" { + t.Errorf("Expected Authorization 'Bearer test-token-core' but was: %v", auth) + } + rw.WriteHeader(http.StatusOK) + // V3 Core returns different response format + if strings.HasSuffix(r.URL.Path, "/api/v3/query_influxql") { + rw.Write([]byte(`[{"iox::database":"mydb","deleted":false}]`)) + } else { + rw.Write([]byte(`{"results":[{}]}`)) + } + })) + defer ts.Close() + + client, err := NewClient(ts.URL, log.New(log.DebugLevel)) + if err != nil { + t.Fatal("Unexpected error initializing client: err:", err) + } + source := &chronograf.Source{ + URL: ts.URL, + Type: chronograf.InfluxDBv3Core, + DatabaseToken: "test-token-core", + } + + client.Connect(context.Background(), source) + query := chronograf.Query{ + Command: "SHOW DATABASES", + } + _, err = client.Query(context.Background(), query) + if err != nil { + t.Fatal("Expected no error but was", err) + } +} + +func Test_Influx_Authorization_V3Enterprise(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if auth != "Bearer test-token-enterprise" { + t.Errorf("Expected Authorization 'Bearer test-token-enterprise' but was: %v", auth) + } + rw.WriteHeader(http.StatusOK) + // V3 Enterprise returns different response format + if strings.HasSuffix(r.URL.Path, "/api/v3/query_influxql") { + rw.Write([]byte(`[{"iox::database":"mydb","deleted":false}]`)) + } else { + rw.Write([]byte(`{"results":[{}]}`)) + } + })) + defer ts.Close() + + client, err := NewClient(ts.URL, log.New(log.DebugLevel)) + if err != nil { + t.Fatal("Unexpected error initializing client: err:", err) + } + source := &chronograf.Source{ + URL: ts.URL, + Type: chronograf.InfluxDBv3Enterprise, + DatabaseToken: "test-token-enterprise", + } + + client.Connect(context.Background(), source) + query := chronograf.Query{ + Command: "SHOW DATABASES", + } + _, err = client.Query(context.Background(), query) + if err != nil { + t.Fatal("Expected no error but was", err) + } +} + +func Test_Influx_Authorization_V3Clustered(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + // V3 Clustered intercepts SHOW DATABASES and calls management API + if strings.Contains(r.URL.Path, "/api/v0/accounts/") { + // Management API - expects management token + auth := r.Header.Get("Authorization") + if auth != "Bearer test-mgmt-token" { + t.Errorf("Expected Authorization 'Bearer test-mgmt-token' but was: %v", auth) + } + rw.WriteHeader(http.StatusOK) + rw.Write([]byte(`[{"name":"mydb","maxTables":500,"maxColumnsPerTable":200,"retentionPeriod":0}]`)) + } else { + // Database query - expects database token + auth := r.Header.Get("Authorization") + if auth != "Bearer test-token-clustered" { + t.Errorf("Expected Authorization 'Bearer test-token-clustered' but was: %v", auth) + } + rw.WriteHeader(http.StatusOK) + rw.Write([]byte(`{"results":[{}]}`)) + } + })) + defer ts.Close() + + client, err := NewClient(ts.URL, log.New(log.DebugLevel)) + if err != nil { + t.Fatal("Unexpected error initializing client: err:", err) + } + source := &chronograf.Source{ + URL: ts.URL, + Type: chronograf.InfluxDBv3Clustered, + DatabaseToken: "test-token-clustered", + ManagementToken: "test-mgmt-token", + } + client.V3Config = influx.V3Config{ + CloudDedicatedManagementURL: ts.URL, + ClusteredAccountID: "test-account-id", + ClusteredClusterID: "test-cluster-id", + } + client.Connect(context.Background(), source) + query := chronograf.Query{ + Command: "SHOW DATABASES", + } + _, err = client.Query(context.Background(), query) + if err != nil { + t.Fatal("Expected no error but was", err) + } +} + +func Test_Influx_Authorization_V3CloudDedicated(t *testing.T) { + t.Parallel() + ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + // V3 Cloud Dedicated intercepts SHOW DATABASES and calls management API + if strings.Contains(r.URL.Path, "/api/v0/accounts/") { + // Management API - expects management token + auth := r.Header.Get("Authorization") + if auth != "Bearer test-mgmt-token" { + t.Errorf("Expected Authorization 'Bearer test-mgmt-token' but was: %v", auth) + } + rw.WriteHeader(http.StatusOK) + rw.Write([]byte(`[{"name":"mydb","maxTables":500,"maxColumnsPerTable":200,"retentionPeriod":0}]`)) + } else { + // Database query - expects database token + auth := r.Header.Get("Authorization") + if auth != "Bearer test-token-cloud-dedicated" { + t.Errorf("Expected Authorization 'Bearer test-token-cloud-dedicated' but was: %v", auth) + } + rw.WriteHeader(http.StatusOK) + rw.Write([]byte(`{"results":[{}]}`)) + } + })) + defer ts.Close() + + client, err := NewClient(ts.URL, log.New(log.DebugLevel)) + if err != nil { + t.Fatal("Unexpected error initializing client: err:", err) + } + source := &chronograf.Source{ + URL: ts.URL, + Type: chronograf.InfluxDBv3CloudDedicated, + DatabaseToken: "test-token-cloud-dedicated", + ManagementToken: "test-mgmt-token", + AccountID: "test-account-id", + ClusterID: "test-cluster-id", + } + client.V3Config = influx.V3Config{ + CloudDedicatedManagementURL: ts.URL, + } + client.Connect(context.Background(), source) + query := chronograf.Query{ + Command: "SHOW DATABASES", + } + _, err = client.Query(context.Background(), query) + if err != nil { + t.Fatal("Expected no error but was", err) + } +} diff --git a/influx/v3config.go b/influx/v3config.go new file mode 100644 index 0000000000..59911efdab --- /dev/null +++ b/influx/v3config.go @@ -0,0 +1,7 @@ +package influx + +type V3Config struct { + CloudDedicatedManagementURL string + ClusteredAccountID string + ClusteredClusterID string +} diff --git a/server/server.go b/server/server.go index 920824c54e..1656c00b2b 100644 --- a/server/server.go +++ b/server/server.go @@ -58,16 +58,20 @@ type Server struct { Cert flags.Filename `long:"cert" description:"Path to PEM encoded public key certificate. " env:"TLS_CERTIFICATE"` Key flags.Filename `long:"key" description:"Path to private key associated with given certificate. " env:"TLS_PRIVATE_KEY"` - InfluxDBType string `long:"influxdb-type" value-name:"choice" choice:"influx" choice:"influx-enterprise" choice:"influx-relay" choice:"influx-v2" choice:"influx-v3-core" choice:"influx-v3-enterprise" choice:"influx-v3-clustered" choice:"influx-v3-cloud-dedicated" description:"InfluxDB server type instance" env:"INFLUXDB_TYPE"` - InfluxDBURL string `long:"influxdb-url" description:"Location of your InfluxDB instance" env:"INFLUXDB_URL"` - InfluxDBUsername string `long:"influxdb-username" description:"Username for your InfluxDB instance" env:"INFLUXDB_USERNAME"` - InfluxDBPassword string `long:"influxdb-password" description:"Password for your InfluxDB instance" env:"INFLUXDB_PASSWORD"` - InfluxDBOrg string `long:"influxdb-org" description:"Organization for your InfluxDB v2 instance" env:"INFLUXDB_ORG"` - InfluxDBToken string `long:"influxdb-token" description:"Token for your InfluxDB v2, v3 Core/Enterprise or Cloud Dedicated instance" env:"INFLUXDB_TOKEN"` - InfluxDBMgmtToken string `long:"influxdb-mgmt-token" description:"Management token for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_MGMT_TOKEN"` - InfluxDBClusterID string `long:"influxdb-cluster-id" description:"Cluster ID for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_CLUSTER_ID"` - InfluxDBAccountID string `long:"influxdb-account-id" description:"Account ID for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_ACCOUNT_ID"` - TagsCSVPath string `long:"tags-csv-path" description:"Path to a directory containing CSV files (per db) with tags for InfluxDB v3 sources. Used to populate the tags field in Query Editor for your InfluxDB Cloud Dedicated instance." env:"TAGS_CSV_PATH"` + InfluxDBType string `long:"influxdb-type" value-name:"choice" choice:"influx" choice:"influx-enterprise" choice:"influx-relay" choice:"influx-v2" choice:"influx-v3-core" choice:"influx-v3-enterprise" choice:"influx-v3-clustered" choice:"influx-v3-cloud-dedicated" description:"InfluxDB server type instance" env:"INFLUXDB_TYPE"` + InfluxDBURL string `long:"influxdb-url" description:"Location of your InfluxDB instance" env:"INFLUXDB_URL"` + InfluxDBUsername string `long:"influxdb-username" description:"Username for your InfluxDB instance" env:"INFLUXDB_USERNAME"` + InfluxDBPassword string `long:"influxdb-password" description:"Password for your InfluxDB instance" env:"INFLUXDB_PASSWORD"` + InfluxDBOrg string `long:"influxdb-org" description:"Organization for your InfluxDB v2 instance" env:"INFLUXDB_ORG"` + InfluxDBToken string `long:"influxdb-token" description:"Token for your InfluxDB v2, v3 Core/Enterprise or Cloud Dedicated instance" env:"INFLUXDB_TOKEN"` + InfluxDBMgmtToken string `long:"influxdb-mgmt-token" description:"Management token for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_MGMT_TOKEN"` + InfluxDBClusterID string `long:"influxdb-cluster-id" description:"Cluster ID for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_CLUSTER_ID"` + InfluxDBAccountID string `long:"influxdb-account-id" description:"Account ID for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_ACCOUNT_ID"` + TagsCSVPath string `long:"tags-csv-path" description:"Path to a directory containing CSV files (per db) with tags for InfluxDB v3 sources. Used to populate the tags field in Query Editor for your InfluxDB Cloud Dedicated instance." env:"TAGS_CSV_PATH"` + InfluxDBDefaultDB string `long:"influxdb-default-db" description:"Default database for your InfluxDB instance" env:"INFLUXDB_DEFAULT_DB"` + InfluxDBCloudDedicatedMgmtURL string `long:"influxdb-cloud-dedicated-mgmt-url" description:"Management URL for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_CLOUD_DEDICATED_MGMT_URL" default:"https://console.influxdata.com"` + InfluxDBClusteredClusterID string `long:"influxdb-clustered-cluster-id" description:"Cluster ID for your InfluxDB v3 Clustered instance" env:"INFLUXDB_CLUSTERED_CLUSTER_ID" default:"11111111-1111-1111-1111-111111111111"` + InfluxDBClusteredAccountID string `long:"influxdb-clustered-account-id" description:"Account ID for your InfluxDB v3 Clustered instance" env:"INFLUXDB_CLUSTERED_ACCOUNT_ID" default:"11111111-1111-1111-1111-111111111111"` KapacitorURL string `long:"kapacitor-url" description:"Location of your Kapacitor instance" env:"KAPACITOR_URL"` KapacitorUsername string `long:"kapacitor-username" description:"Username of your Kapacitor instance" env:"KAPACITOR_USERNAME"` @@ -557,6 +561,7 @@ func (s *Server) newBuilders(logger chronograf.Logger) builders { InfluxDBClusterID: s.InfluxDBClusterID, InfluxDBAccountID: s.InfluxDBAccountID, TagsCSVPath: s.TagsCSVPath, + DefaultDB: s.InfluxDBDefaultDB, Logger: logger, ID: idgen.NewTime(), @@ -681,6 +686,11 @@ func (s *Server) Serve(ctx context.Context) { HostPageDisabled: s.HostPageDisabled, CustomAutoRefresh: s.CustomAutoRefresh, } + service.V3Config = influx.V3Config{ + CloudDedicatedManagementURL: s.InfluxDBCloudDedicatedMgmtURL, + ClusteredAccountID: s.InfluxDBClusteredAccountID, + ClusteredClusterID: s.InfluxDBClusteredClusterID, + } if !validBasepath(s.Basepath) { err := fmt.Errorf("invalid basepath, must follow format \"/mybasepath\"") diff --git a/server/service.go b/server/service.go index 358cbaae80..ecbaeba084 100644 --- a/server/service.go +++ b/server/service.go @@ -18,6 +18,7 @@ type Service struct { SuperAdminProviderGroups superAdminProviderGroups Env chronograf.Environment Databases chronograf.Databases + V3Config influx.V3Config } type superAdminProviderGroups struct { diff --git a/server/sources.go b/server/sources.go index cc70917dae..8c84ec48cd 100644 --- a/server/sources.go +++ b/server/sources.go @@ -213,7 +213,8 @@ func (s *Service) sourceVersion(ctx context.Context, src *chronograf.Source) str func (s *Service) tsdbVersion(ctx context.Context, src *chronograf.Source) (string, error) { cli := &influx.Client{ - Logger: s.Logger, + Logger: s.Logger, + V3Config: s.V3Config, } if err := cli.Connect(ctx, src); err != nil { @@ -250,7 +251,8 @@ func (s *Service) tsdbType(ctx context.Context, src *chronograf.Source) (string, func (s *Service) validateCredentials(ctx context.Context, src *chronograf.Source) error { cli := &influx.Client{ - Logger: s.Logger, + Logger: s.Logger, + V3Config: s.V3Config, } if err := cli.Connect(ctx, src); err != nil { return err @@ -356,7 +358,8 @@ func (s *Service) SourceHealth(w http.ResponseWriter, r *http.Request) { } cli := &influx.Client{ - Logger: s.Logger, + Logger: s.Logger, + V3Config: s.V3Config, } if err := cli.Connect(ctx, &src); err != nil { From 71904771c567db2f4e67b9d3ee30f5f86cd6a54b Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:33:27 +0100 Subject: [PATCH 31/50] chore: Extended tests for updating source --- server/sources_test.go | 270 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 249 insertions(+), 21 deletions(-) diff --git a/server/sources_test.go b/server/sources_test.go index 39929e7b33..f88ccdf971 100644 --- a/server/sources_test.go +++ b/server/sources_test.go @@ -14,6 +14,7 @@ import ( "github.com/bouk/httprouter" "github.com/google/go-cmp/cmp" "github.com/influxdata/chronograf" + "github.com/influxdata/chronograf/influx" "github.com/influxdata/chronograf/log" "github.com/influxdata/chronograf/mocks" ) @@ -762,13 +763,15 @@ func TestService_UpdateSource(t *testing.T) { r *http.Request } tests := []struct { - name string - args args - fields fields - ID string - wantStatusCode int - wantContentType string - wantBody func(string) string + name string + args args + fields fields + ID string + requestBody func(string) string + mockServerHandler http.HandlerFunc + wantStatusCode int + wantContentType string + wantBody func(string) string }{ { name: "Update source updates fields", @@ -800,33 +803,260 @@ func TestService_UpdateSource(t *testing.T) { }, Logger: log.New(log.DebugLevel), }, - ID: "1", + ID: "1", + requestBody: func(url string) string { + return fmt.Sprintf(`{"name":"marty","password":"the_lake","username":"bob","type":"influx","telegraf":"murlin","defaultRP":"pineapple","url":"%s","metaUrl":"http://murl"}`, url) + }, wantStatusCode: 200, wantContentType: "application/json", wantBody: func(url string) string { return fmt.Sprintf(`{"id":"1","name":"marty","type":"influx","username":"bob","url":"%s","metaUrl":"http://murl","default":false,"telegraf":"murlin","organization":"1337","defaultRP":"pineapple","authentication":"basic","links":{"self":"/chronograf/v1/sources/1","kapacitors":"/chronograf/v1/sources/1/kapacitors","services":"/chronograf/v1/sources/1/services","proxy":"/chronograf/v1/sources/1/proxy","queries":"/chronograf/v1/sources/1/queries","write":"/chronograf/v1/sources/1/write","permissions":"/chronograf/v1/sources/1/permissions","users":"/chronograf/v1/sources/1/users","databases":"/chronograf/v1/sources/1/dbs","annotations":"/chronograf/v1/sources/1/annotations","health":"/chronograf/v1/sources/1/health"}} +`, url) + }, + }, + { + name: "Update InfluxDB v3 Core source", + args: args{ + w: httptest.NewRecorder(), + r: httptest.NewRequest( + "PATCH", + "http://any.url", + nil), + }, + fields: fields{ + SourcesStore: &mocks.SourcesStore{ + GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { + return chronograf.Source{ + ID: 2, + Type: chronograf.InfluxDBv3Core, + DatabaseToken: "old-token", + }, nil + }, + UpdateF: func(ctx context.Context, upd chronograf.Source) error { + return nil + }, + }, + OrganizationsStore: &mocks.OrganizationsStore{ + DefaultOrganizationF: func(context.Context) (*chronograf.Organization, error) { + return &chronograf.Organization{ + ID: "1337", + Name: "pineapple_kingdom", + }, nil + }, + }, + Logger: log.New(log.DebugLevel), + }, + ID: "2", + requestBody: func(url string) string { + return fmt.Sprintf(`{"name":"v3-core","type":"influx-v3-core","url":"%s","databaseToken":"test-token-123","defaultDB":"mydb","telegraf":"telegraf"}`, url) + }, + mockServerHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // v3 Core sources validate by querying /api/v3/query_influxql + if r.URL.Path == "/api/v3/query_influxql" { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`[]`)) + } else { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{}`)) + } + }), + wantStatusCode: 200, + wantContentType: "application/json", + wantBody: func(url string) string { + return fmt.Sprintf(`{"id":"2","name":"v3-core","type":"influx-v3-core","databaseToken":"test-token-123","url":"%s","default":false,"telegraf":"telegraf","organization":"1337","defaultRP":"","defaultDB":"mydb","version":"Unknown","authentication":"unknown","links":{"self":"/chronograf/v1/sources/2","kapacitors":"/chronograf/v1/sources/2/kapacitors","services":"/chronograf/v1/sources/2/services","proxy":"/chronograf/v1/sources/2/proxy","queries":"/chronograf/v1/sources/2/queries","write":"/chronograf/v1/sources/2/write","permissions":"/chronograf/v1/sources/2/permissions","users":"/chronograf/v1/sources/2/users","databases":"/chronograf/v1/sources/2/dbs","annotations":"/chronograf/v1/sources/2/annotations","health":"/chronograf/v1/sources/2/health"}} +`, url) + }, + }, + { + name: "Update InfluxDB v3 Enterprise source", + args: args{ + w: httptest.NewRecorder(), + r: httptest.NewRequest( + "PATCH", + "http://any.url", + nil), + }, + fields: fields{ + SourcesStore: &mocks.SourcesStore{ + GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { + return chronograf.Source{ + ID: 3, + Type: chronograf.InfluxDBv3Enterprise, + DatabaseToken: "old-enterprise-token", + }, nil + }, + UpdateF: func(ctx context.Context, upd chronograf.Source) error { + return nil + }, + }, + OrganizationsStore: &mocks.OrganizationsStore{ + DefaultOrganizationF: func(context.Context) (*chronograf.Organization, error) { + return &chronograf.Organization{ + ID: "1337", + Name: "pineapple_kingdom", + }, nil + }, + }, + Logger: log.New(log.DebugLevel), + }, + ID: "3", + requestBody: func(url string) string { + return fmt.Sprintf(`{"name":"v3-enterprise","type":"influx-v3-enterprise","url":"%s","databaseToken":"enterprise-token-456","defaultDB":"enterprise_db","telegraf":"telegraf"}`, url) + }, + mockServerHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // v3 Enterprise sources validate by querying /api/v3/query_influxql + if r.URL.Path == "/api/v3/query_influxql" { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`[]`)) + } else { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{}`)) + } + }), + wantStatusCode: 200, + wantContentType: "application/json", + wantBody: func(url string) string { + return fmt.Sprintf(`{"id":"3","name":"v3-enterprise","type":"influx-v3-enterprise","databaseToken":"enterprise-token-456","url":"%s","default":false,"telegraf":"telegraf","organization":"1337","defaultRP":"","defaultDB":"enterprise_db","version":"Unknown","authentication":"unknown","links":{"self":"/chronograf/v1/sources/3","kapacitors":"/chronograf/v1/sources/3/kapacitors","services":"/chronograf/v1/sources/3/services","proxy":"/chronograf/v1/sources/3/proxy","queries":"/chronograf/v1/sources/3/queries","write":"/chronograf/v1/sources/3/write","permissions":"/chronograf/v1/sources/3/permissions","users":"/chronograf/v1/sources/3/users","databases":"/chronograf/v1/sources/3/dbs","annotations":"/chronograf/v1/sources/3/annotations","health":"/chronograf/v1/sources/3/health"}} +`, url) + }, + }, + { + name: "Update InfluxDB v3 Clustered source", + args: args{ + w: httptest.NewRecorder(), + r: httptest.NewRequest( + "PATCH", + "http://any.url", + nil), + }, + fields: fields{ + SourcesStore: &mocks.SourcesStore{ + GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { + return chronograf.Source{ + ID: 4, + Type: chronograf.InfluxDBv3Clustered, + DatabaseToken: "old-db-token", + ManagementToken: "old-mgmt-token", + }, nil + }, + UpdateF: func(ctx context.Context, upd chronograf.Source) error { + return nil + }, + }, + OrganizationsStore: &mocks.OrganizationsStore{ + DefaultOrganizationF: func(context.Context) (*chronograf.Organization, error) { + return &chronograf.Organization{ + ID: "1337", + Name: "pineapple_kingdom", + }, nil + }, + }, + Logger: log.New(log.DebugLevel), + }, + ID: "4", + requestBody: func(url string) string { + return fmt.Sprintf(`{"name":"v3-clustered","type":"influx-v3-clustered","url":"%s","databaseToken":"db-token-789","managementToken":"mgmt-token-789","clusterId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","accountId":"f1e2d3c4-b5a6-9870-dcba-fe9876543210","defaultDB":"clustered_db","telegraf":"telegraf"}`, url) + }, + mockServerHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // v3 Clustered sources validate by accessing the management API + if strings.Contains(r.URL.Path, "/api/v0/accounts/") || r.URL.Path == "/api/v3/query_influxql" { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"databases":[]}`)) + } else { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{}`)) + } + }), + wantStatusCode: 200, + wantContentType: "application/json", + wantBody: func(url string) string { + return fmt.Sprintf(`{"id":"4","name":"v3-clustered","type":"influx-v3-clustered","clusterId":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","accountId":"f1e2d3c4-b5a6-9870-dcba-fe9876543210","managementToken":"mgmt-token-789","databaseToken":"db-token-789","url":"%s","default":false,"telegraf":"telegraf","organization":"1337","defaultRP":"","defaultDB":"clustered_db","authentication":"unknown","links":{"self":"/chronograf/v1/sources/4","kapacitors":"/chronograf/v1/sources/4/kapacitors","services":"/chronograf/v1/sources/4/services","proxy":"/chronograf/v1/sources/4/proxy","queries":"/chronograf/v1/sources/4/queries","write":"/chronograf/v1/sources/4/write","permissions":"/chronograf/v1/sources/4/permissions","users":"/chronograf/v1/sources/4/users","databases":"/chronograf/v1/sources/4/dbs","annotations":"/chronograf/v1/sources/4/annotations","health":"/chronograf/v1/sources/4/health"}} +`, url) + }, + }, + { + name: "Update InfluxDB v3 Cloud Dedicated source", + args: args{ + w: httptest.NewRecorder(), + r: httptest.NewRequest( + "PATCH", + "http://any.url", + nil), + }, + fields: fields{ + SourcesStore: &mocks.SourcesStore{ + GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { + return chronograf.Source{ + ID: 5, + Type: chronograf.InfluxDBv3CloudDedicated, + DatabaseToken: "old-cloud-token", + ManagementToken: "old-cloud-mgmt", + ClusterID: "11111111-1111-1111-1111-111111111111", + AccountID: "22222222-2222-2222-2222-222222222222", + }, nil + }, + UpdateF: func(ctx context.Context, upd chronograf.Source) error { + return nil + }, + }, + OrganizationsStore: &mocks.OrganizationsStore{ + DefaultOrganizationF: func(context.Context) (*chronograf.Organization, error) { + return &chronograf.Organization{ + ID: "1337", + Name: "pineapple_kingdom", + }, nil + }, + }, + Logger: log.New(log.DebugLevel), + }, + ID: "5", + requestBody: func(url string) string { + return fmt.Sprintf(`{"name":"v3-cloud-dedicated","type":"influx-v3-cloud-dedicated","url":"%s","databaseToken":"cloud-token-abc","managementToken":"cloud-mgmt-abc","clusterId":"12345678-1234-5678-1234-567812345678","accountId":"87654321-4321-8765-4321-876543218765","defaultDB":"cloud_db","telegraf":"telegraf"}`, url) + }, + mockServerHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // v3 Cloud Dedicated sources validate by accessing the management API + if strings.Contains(r.URL.Path, "/api/v0/accounts/") || r.URL.Path == "/api/v3/query_influxql" { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"databases":[]}`)) + } else { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{}`)) + } + }), + wantStatusCode: 200, + wantContentType: "application/json", + wantBody: func(url string) string { + return fmt.Sprintf(`{"id":"5","name":"v3-cloud-dedicated","type":"influx-v3-cloud-dedicated","clusterId":"12345678-1234-5678-1234-567812345678","accountId":"87654321-4321-8765-4321-876543218765","managementToken":"cloud-mgmt-abc","databaseToken":"cloud-token-abc","url":"%s","default":false,"telegraf":"telegraf","organization":"1337","defaultRP":"","defaultDB":"cloud_db","authentication":"unknown","links":{"self":"/chronograf/v1/sources/5","kapacitors":"/chronograf/v1/sources/5/kapacitors","services":"/chronograf/v1/sources/5/services","proxy":"/chronograf/v1/sources/5/proxy","queries":"/chronograf/v1/sources/5/queries","write":"/chronograf/v1/sources/5/write","permissions":"/chronograf/v1/sources/5/permissions","users":"/chronograf/v1/sources/5/users","databases":"/chronograf/v1/sources/5/dbs","annotations":"/chronograf/v1/sources/5/annotations","health":"/chronograf/v1/sources/5/health"}} `, url) }, }, } for _, tt := range tests { + mockHandler := tt.mockServerHandler + if mockHandler == nil { + mockHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/query" { + w.WriteHeader(http.StatusOK) // credentials + } else { + w.WriteHeader(http.StatusNoContent) + w.Header().Set("X-Influxdb-Build", "ENT") + } + w.Write(([]byte)("{}")) + }) + } + ts := httptest.NewServer(mockHandler) + defer ts.Close() + h := &Service{ Store: &mocks.Store{ SourcesStore: tt.fields.SourcesStore, OrganizationsStore: tt.fields.OrganizationsStore, }, Logger: tt.fields.Logger, + V3Config: influx.V3Config{ + CloudDedicatedManagementURL: ts.URL, + }, } - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/query" { - w.WriteHeader(http.StatusOK) // credentials - } else { - w.WriteHeader(http.StatusNoContent) - w.Header().Set("X-Influxdb-Build", "ENT") - } - w.Write(([]byte)("{}")) - })) - defer ts.Close() tt.args.r = tt.args.r.WithContext(httprouter.WithParams( context.Background(), @@ -837,9 +1067,7 @@ func TestService_UpdateSource(t *testing.T) { }, })) tt.args.r.Body = ioutil.NopCloser( - bytes.NewReader([]byte( - fmt.Sprintf(`{"name":"marty","password":"the_lake","username":"bob","type":"influx","telegraf":"murlin","defaultRP":"pineapple","url":"%s","metaUrl":"http://murl"}`, ts.URL)), - ), + bytes.NewReader([]byte(tt.requestBody(ts.URL))), ) h.UpdateSource(tt.args.w, tt.args.r) From 07d68acc8e5ed83ba5d23423d3b4b8293259aef3 Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Thu, 30 Oct 2025 15:32:55 +0100 Subject: [PATCH 32/50] feat: Added InfluxDB 3 Serverless --- chronograf.go | 5 ++- influx/authorization.go | 8 +++- server/server.go | 4 +- server/sources.go | 14 ++++++- server/sources_test.go | 53 ++++++++++++++++++++++++ ui/src/shared/constants/index.ts | 1 + ui/src/sources/components/SourceStep.tsx | 17 ++++++-- 7 files changed, 93 insertions(+), 9 deletions(-) diff --git a/chronograf.go b/chronograf.go index 0ecc5c0211..f128a08cc5 100644 --- a/chronograf.go +++ b/chronograf.go @@ -109,13 +109,16 @@ const ( InfluxDBv3Clustered = "influx-v3-clustered" // InfluxDBv3CloudDedicated is InfluxDB Cloud Dedicated (fully-managed) InfluxDBv3CloudDedicated = "influx-v3-cloud-dedicated" + // InfluxDBv3Serverless is InfluxDB Cloud Serverless (fully-managed) + InfluxDBv3Serverless = "influx-v3-serverless" ) func IsV3SrcType(srcType string) bool { return srcType == InfluxDBv3Core || srcType == InfluxDBv3Enterprise || srcType == InfluxDBv3Clustered || - srcType == InfluxDBv3CloudDedicated + srcType == InfluxDBv3CloudDedicated || + srcType == InfluxDBv3Serverless } // TSDBStatus represents the current status of a time series database diff --git a/influx/authorization.go b/influx/authorization.go index 3cd5fb1c87..ad852f2981 100644 --- a/influx/authorization.go +++ b/influx/authorization.go @@ -23,7 +23,13 @@ func (n *NoAuthorization) Set(req *http.Request) error { return nil } // DefaultAuthorization creates either a shared JWT builder, basic auth or Noop or Token authentication func DefaultAuthorization(src *chronograf.Source) Authorizer { - // Use Bearer Token authentication for all InfluxDB 3 types + // Use Token authentication for InfluxDB v3 Serverless + if src.Type == chronograf.InfluxDBv3Serverless { + return &TokenAuth{ + Token: src.DatabaseToken, + } + } + // Use Bearer Token authentication for all other InfluxDB 3 types if chronograf.IsV3SrcType(src.Type) { return &BearerToken{ Token: src.DatabaseToken, diff --git a/server/server.go b/server/server.go index 1656c00b2b..b4e842dc42 100644 --- a/server/server.go +++ b/server/server.go @@ -58,12 +58,12 @@ type Server struct { Cert flags.Filename `long:"cert" description:"Path to PEM encoded public key certificate. " env:"TLS_CERTIFICATE"` Key flags.Filename `long:"key" description:"Path to private key associated with given certificate. " env:"TLS_PRIVATE_KEY"` - InfluxDBType string `long:"influxdb-type" value-name:"choice" choice:"influx" choice:"influx-enterprise" choice:"influx-relay" choice:"influx-v2" choice:"influx-v3-core" choice:"influx-v3-enterprise" choice:"influx-v3-clustered" choice:"influx-v3-cloud-dedicated" description:"InfluxDB server type instance" env:"INFLUXDB_TYPE"` + InfluxDBType string `long:"influxdb-type" value-name:"choice" choice:"influx" choice:"influx-enterprise" choice:"influx-relay" choice:"influx-v2" choice:"influx-v3-core" choice:"influx-v3-enterprise" choice:"influx-v3-clustered" choice:"influx-v3-cloud-dedicated" choice:"influx-v3-serverless" description:"InfluxDB server type instance" env:"INFLUXDB_TYPE"` InfluxDBURL string `long:"influxdb-url" description:"Location of your InfluxDB instance" env:"INFLUXDB_URL"` InfluxDBUsername string `long:"influxdb-username" description:"Username for your InfluxDB instance" env:"INFLUXDB_USERNAME"` InfluxDBPassword string `long:"influxdb-password" description:"Password for your InfluxDB instance" env:"INFLUXDB_PASSWORD"` InfluxDBOrg string `long:"influxdb-org" description:"Organization for your InfluxDB v2 instance" env:"INFLUXDB_ORG"` - InfluxDBToken string `long:"influxdb-token" description:"Token for your InfluxDB v2, v3 Core/Enterprise or Cloud Dedicated instance" env:"INFLUXDB_TOKEN"` + InfluxDBToken string `long:"influxdb-token" description:"Token for your InfluxDB v2, v3 Core/Enterprise, Cloud Dedicated or Serverless instance" env:"INFLUXDB_TOKEN"` InfluxDBMgmtToken string `long:"influxdb-mgmt-token" description:"Management token for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_MGMT_TOKEN"` InfluxDBClusterID string `long:"influxdb-cluster-id" description:"Cluster ID for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_CLUSTER_ID"` InfluxDBAccountID string `long:"influxdb-account-id" description:"Account ID for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_ACCOUNT_ID"` diff --git a/server/sources.go b/server/sources.go index 8c84ec48cd..54054e3eee 100644 --- a/server/sources.go +++ b/server/sources.go @@ -46,6 +46,11 @@ type authenticationResponse struct { } func sourceAuthenticationMethod(ctx context.Context, src chronograf.Source) authenticationResponse { + // Check for Token authentication (v2 and v3 Serverless) + if src.Type == chronograf.InfluxDBv2 || src.Type == chronograf.InfluxDBv3Serverless { + return authenticationResponse{ID: src.ID, AuthenticationMethod: "token"} + } + ldapEnabled := false if src.MetaURL != "" { authorizer := influx.DefaultAuthorization(&src) @@ -522,7 +527,8 @@ func ValidSourceRequest(s *chronograf.Source, defaultOrgID string) error { s.Type != chronograf.InfluxDBv3Core && s.Type != chronograf.InfluxDBv3Enterprise && s.Type != chronograf.InfluxDBv3Clustered && - s.Type != chronograf.InfluxDBv3CloudDedicated { + s.Type != chronograf.InfluxDBv3CloudDedicated && + s.Type != chronograf.InfluxDBv3Serverless { return fmt.Errorf("invalid source type %s", s.Type) } } @@ -545,6 +551,12 @@ func ValidSourceRequest(s *chronograf.Source, defaultOrgID string) error { } } + if s.Type == chronograf.InfluxDBv3Serverless { + if len(s.DatabaseToken) == 0 { + return fmt.Errorf("database token required") + } + } + if s.Type == chronograf.InfluxDBv3Clustered { if len(s.ManagementToken) == 0 { return fmt.Errorf("management token required") diff --git a/server/sources_test.go b/server/sources_test.go index f88ccdf971..7ead167dff 100644 --- a/server/sources_test.go +++ b/server/sources_test.go @@ -1027,6 +1027,59 @@ func TestService_UpdateSource(t *testing.T) { wantContentType: "application/json", wantBody: func(url string) string { return fmt.Sprintf(`{"id":"5","name":"v3-cloud-dedicated","type":"influx-v3-cloud-dedicated","clusterId":"12345678-1234-5678-1234-567812345678","accountId":"87654321-4321-8765-4321-876543218765","managementToken":"cloud-mgmt-abc","databaseToken":"cloud-token-abc","url":"%s","default":false,"telegraf":"telegraf","organization":"1337","defaultRP":"","defaultDB":"cloud_db","authentication":"unknown","links":{"self":"/chronograf/v1/sources/5","kapacitors":"/chronograf/v1/sources/5/kapacitors","services":"/chronograf/v1/sources/5/services","proxy":"/chronograf/v1/sources/5/proxy","queries":"/chronograf/v1/sources/5/queries","write":"/chronograf/v1/sources/5/write","permissions":"/chronograf/v1/sources/5/permissions","users":"/chronograf/v1/sources/5/users","databases":"/chronograf/v1/sources/5/dbs","annotations":"/chronograf/v1/sources/5/annotations","health":"/chronograf/v1/sources/5/health"}} +`, url) + }, + }, + { + name: "Update InfluxDB v3 Serverless source", + args: args{ + w: httptest.NewRecorder(), + r: httptest.NewRequest( + "PATCH", + "http://any.url", + nil), + }, + fields: fields{ + SourcesStore: &mocks.SourcesStore{ + GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { + return chronograf.Source{ + ID: 6, + Type: chronograf.InfluxDBv3Serverless, + DatabaseToken: "old-serverless-token", + }, nil + }, + UpdateF: func(ctx context.Context, upd chronograf.Source) error { + return nil + }, + }, + OrganizationsStore: &mocks.OrganizationsStore{ + DefaultOrganizationF: func(context.Context) (*chronograf.Organization, error) { + return &chronograf.Organization{ + ID: "1337", + Name: "pineapple_kingdom", + }, nil + }, + }, + Logger: log.New(log.DebugLevel), + }, + ID: "6", + requestBody: func(url string) string { + return fmt.Sprintf(`{"name":"v3-serverless","type":"influx-v3-serverless","url":"%s","databaseToken":"serverless-token-xyz","defaultDB":"serverless_db","telegraf":"telegraf"}`, url) + }, + mockServerHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // v3 Serverless uses /query endpoint like v1 + if r.URL.Path == "/query" { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"results":[{"statement_id":0,"series":[{"name":"databases","columns":["name"],"values":[["_internal"]]}]}]}`)) + } else { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{}`)) + } + }), + wantStatusCode: 200, + wantContentType: "application/json", + wantBody: func(url string) string { + return fmt.Sprintf(`{"id":"6","name":"v3-serverless","type":"influx-v3-serverless","databaseToken":"serverless-token-xyz","url":"%s","default":false,"telegraf":"telegraf","organization":"1337","defaultRP":"","defaultDB":"serverless_db","authentication":"token","links":{"self":"/chronograf/v1/sources/6","kapacitors":"/chronograf/v1/sources/6/kapacitors","services":"/chronograf/v1/sources/6/services","proxy":"/chronograf/v1/sources/6/proxy","queries":"/chronograf/v1/sources/6/queries","write":"/chronograf/v1/sources/6/write","permissions":"/chronograf/v1/sources/6/permissions","users":"/chronograf/v1/sources/6/users","databases":"/chronograf/v1/sources/6/dbs","annotations":"/chronograf/v1/sources/6/annotations","health":"/chronograf/v1/sources/6/health"}} `, url) }, }, diff --git a/ui/src/shared/constants/index.ts b/ui/src/shared/constants/index.ts index d4bdeda2c9..6fafb38317 100644 --- a/ui/src/shared/constants/index.ts +++ b/ui/src/shared/constants/index.ts @@ -532,6 +532,7 @@ export const SOURCE_TYPE_INFLUX_V3_CORE = 'influx-v3-core' export const SOURCE_TYPE_INFLUX_V3_ENTERPRISE = 'influx-v3-enterprise' export const SOURCE_TYPE_INFLUX_V3_CLUSTERED = 'influx-v3-clustered' export const SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED = 'influx-v3-cloud-dedicated' +export const SOURCE_TYPE_INFLUX_V3_SERVERLESS = 'influx-v3-serverless' export enum DataType { flux = 'flux', diff --git a/ui/src/sources/components/SourceStep.tsx b/ui/src/sources/components/SourceStep.tsx index 0b3102d7a0..399ea3272f 100644 --- a/ui/src/sources/components/SourceStep.tsx +++ b/ui/src/sources/components/SourceStep.tsx @@ -40,6 +40,7 @@ import { SOURCE_TYPE_INFLUX_V3_CLUSTERED, SOURCE_TYPE_INFLUX_V3_CORE, SOURCE_TYPE_INFLUX_V3_ENTERPRISE, + SOURCE_TYPE_INFLUX_V3_SERVERLESS, } from 'src/shared/constants' import {SUPERADMIN_ROLE} from 'src/auth/roles' @@ -140,11 +141,14 @@ class SourceStep extends PureComponent { value: SOURCE_TYPE_INFLUX_V3_CLUSTERED, label: 'InfluxDB Clustered', }, - // TODO simon: add InfluxDB Cloud Serverless { value: SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED, label: 'InfluxDB Cloud Dedicated', }, + { + value: SOURCE_TYPE_INFLUX_V3_SERVERLESS, + label: 'InfluxDB Cloud Serverless', + }, ]} onChange={this.handleServerTypeChange} testId="server-type-selector--dropdown" @@ -193,9 +197,10 @@ class SourceStep extends PureComponent { )} - {/* InfluxDB 3 Core/Enterprise fields */} + {/* InfluxDB 3 Core/Enterprise/Serverless fields */} {(this.state.serverType === SOURCE_TYPE_INFLUX_V3_CORE || - this.state.serverType === SOURCE_TYPE_INFLUX_V3_ENTERPRISE) && ( + this.state.serverType === SOURCE_TYPE_INFLUX_V3_ENTERPRISE || + this.state.serverType === SOURCE_TYPE_INFLUX_V3_SERVERLESS) && ( <> { source.type === SOURCE_TYPE_INFLUX_V3_CORE || source.type === SOURCE_TYPE_INFLUX_V3_ENTERPRISE || source.type === SOURCE_TYPE_INFLUX_V3_CLUSTERED || - source.type === SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED + source.type === SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED || + source.type === SOURCE_TYPE_INFLUX_V3_SERVERLESS ) { return source.type } @@ -463,6 +469,9 @@ class SourceStep extends PureComponent { case SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED: this.changeSourceType(value, 'cloud') break + case SOURCE_TYPE_INFLUX_V3_SERVERLESS: + this.changeSourceType(value, 'cloud') + break case SOURCE_TYPE_INFLUX_V1: default: this.changeSourceType(SOURCE_TYPE_INFLUX_V1, '1.x') From 84e3e3ad2be8abdff6853d392ec8cd6ed413eafd Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Thu, 30 Oct 2025 17:40:25 +0100 Subject: [PATCH 33/50] fix: fixed help format --- server/builders.go | 4 ++-- server/server.go | 29 ++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/server/builders.go b/server/builders.go index 3a2bbd1f68..6a66194a2f 100644 --- a/server/builders.go +++ b/server/builders.go @@ -141,8 +141,8 @@ func (fs *MultiSourceBuilder) Build(db chronograf.SourcesStore, defaultOrgID str if fs.InfluxDBURL != "" { var influxdbType, username, password string var clusterID, accountID, mgmtToken, dbToken, tagsCSVPath, defaultDB string - if fs.InfluxDBType == chronograf.InfluxDBv3Core || fs.InfluxDBType == chronograf.InfluxDBv3Enterprise { - // InfluxDB 3 Core, InfluxDB 3 Enterprise + if fs.InfluxDBType == chronograf.InfluxDBv3Core || fs.InfluxDBType == chronograf.InfluxDBv3Enterprise || fs.InfluxDBType == chronograf.InfluxDBv3Serverless { + // InfluxDB 3 Core, InfluxDB 3 Enterprise, InfluxDB 3 Serverless influxdbType = fs.InfluxDBType dbToken = fs.InfluxDBToken } else if fs.InfluxDBType == chronograf.InfluxDBv3Clustered { diff --git a/server/server.go b/server/server.go index b4e842dc42..6e35bcb798 100644 --- a/server/server.go +++ b/server/server.go @@ -58,7 +58,7 @@ type Server struct { Cert flags.Filename `long:"cert" description:"Path to PEM encoded public key certificate. " env:"TLS_CERTIFICATE"` Key flags.Filename `long:"key" description:"Path to private key associated with given certificate. " env:"TLS_PRIVATE_KEY"` - InfluxDBType string `long:"influxdb-type" value-name:"choice" choice:"influx" choice:"influx-enterprise" choice:"influx-relay" choice:"influx-v2" choice:"influx-v3-core" choice:"influx-v3-enterprise" choice:"influx-v3-clustered" choice:"influx-v3-cloud-dedicated" choice:"influx-v3-serverless" description:"InfluxDB server type instance" env:"INFLUXDB_TYPE"` + InfluxDBType string `long:"influxdb-type" description:"InfluxDB server type instance. Valid values: influx, influx-enterprise, influx-relay, influx-v2, influx-v3-core, influx-v3-enterprise, influx-v3-clustered, influx-v3-cloud-dedicated, influx-v3-serverless" env:"INFLUXDB_TYPE"` InfluxDBURL string `long:"influxdb-url" description:"Location of your InfluxDB instance" env:"INFLUXDB_URL"` InfluxDBUsername string `long:"influxdb-username" description:"Username for your InfluxDB instance" env:"INFLUXDB_USERNAME"` InfluxDBPassword string `long:"influxdb-password" description:"Password for your InfluxDB instance" env:"INFLUXDB_PASSWORD"` @@ -622,6 +622,33 @@ func (s *Server) Serve(ctx context.Context) { go rotateSuperAdminNonce(ctx, s.NonceExpiration) logger := clog.New(clog.ParseLevel(s.LogLevel)) + + // Validate InfluxDBType if provided + if s.InfluxDBType != "" { + validTypes := []string{ + "influx", + "influx-enterprise", + "influx-relay", + "influx-v2", + "influx-v3-core", + "influx-v3-enterprise", + "influx-v3-clustered", + "influx-v3-cloud-dedicated", + "influx-v3-serverless", + } + isValid := false + for _, validType := range validTypes { + if s.InfluxDBType == validType { + isValid = true + break + } + } + if !isValid { + logger.Error("Invalid --influxdb-type value. Valid values: influx, influx-enterprise, influx-relay, influx-v2, influx-v3-core, influx-v3-enterprise, influx-v3-clustered, influx-v3-cloud-dedicated, influx-v3-serverless") + os.Exit(1) + } + } + customLinks, err := NewCustomLinks(s.CustomLinks) if err != nil { logger. From 31300cbb26d8eac073dd2ecbd954a4d85e2c9fa1 Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Fri, 31 Oct 2025 11:15:27 +0100 Subject: [PATCH 34/50] chore: refactored WizardDropdown to use existing DropDown component --- .../components/wizard/WizardDropdown.tsx | 128 ++++-------------- 1 file changed, 23 insertions(+), 105 deletions(-) diff --git a/ui/src/reusable_ui/components/wizard/WizardDropdown.tsx b/ui/src/reusable_ui/components/wizard/WizardDropdown.tsx index 1e1350cd26..ff6e51647e 100644 --- a/ui/src/reusable_ui/components/wizard/WizardDropdown.tsx +++ b/ui/src/reusable_ui/components/wizard/WizardDropdown.tsx @@ -1,4 +1,6 @@ import React, {PureComponent} from 'react' +import Dropdown from 'src/shared/components/Dropdown' +import {DropdownItem} from 'src/types' import {ErrorHandling} from 'src/shared/decorators/errors' interface DropdownOption { @@ -17,126 +19,42 @@ interface Props { testId?: string } -interface State { - isOpen: boolean - menuStyle?: React.CSSProperties -} - -class WizardDropdown extends PureComponent { - private readonly dropdownRef: React.RefObject - - constructor(props: Props) { - super(props) - this.state = { - isOpen: false, - menuStyle: undefined, - } - this.dropdownRef = React.createRef() - } - - componentDidMount() { - document.addEventListener('click', this.handleClickOutside) - document.addEventListener('scroll', this.handleScroll, true) - } - - componentWillUnmount() { - document.removeEventListener('click', this.handleClickOutside) - document.removeEventListener('scroll', this.handleScroll, true) - } - +class WizardDropdown extends PureComponent { public render() { const {label, subtext, halfWidth, testId} = this.props - const {isOpen} = this.state return (
-
- -
- {this.props.options.map(option => ( -
this.selectOption(option.value)} - > - {option.label} -
- ))} -
-
+ {subtext && {subtext}}
) } - private get buttonText(): string { - const {placeholder = 'Select option...'} = this.props - return this.selectedOption ? this.selectedOption.label : placeholder + private get dropdownItems(): DropdownItem[] { + return this.props.options.map(option => ({ + text: option.label, + })) } - private get selectedOption(): DropdownOption | undefined { - return this.props.options.find(opt => opt.value === this.props.value) - } - - private toggleDropdown = (e: React.MouseEvent) => { - e.stopPropagation() - - if (!this.state.isOpen && this.dropdownRef.current) { - // Calculate position for fixed positioning to avoid overflow issues - const rect = this.dropdownRef.current.getBoundingClientRect() - const menuStyle: React.CSSProperties = { - position: 'fixed', - top: `${rect.bottom + 4}px`, // Include the 4px margin - left: `${rect.left}px`, - width: `${rect.width}px`, - zIndex: 10000, - } - // Set position first, then make visible in next tick - this.setState({menuStyle}, () => { - requestAnimationFrame(() => { - this.setState({isOpen: true}) - }) - }) - } else { - this.setState({isOpen: false, menuStyle: undefined}) - } - } - - private selectOption = (value: string) => { - this.props.onChange(value) - this.setState({isOpen: false, menuStyle: undefined}) - } - - private handleClickOutside = (event: MouseEvent) => { - if ( - this.dropdownRef.current && - !this.dropdownRef.current.contains(event.target as Node) - ) { - this.setState({isOpen: false, menuStyle: undefined}) - } + private get selectedText(): string { + const {value, options, placeholder = 'Select option...'} = this.props + const selectedOption = options.find(opt => opt.value === value) + return selectedOption ? selectedOption.label : placeholder } - private handleScroll = () => { - // Close dropdown when scrolling to prevent position mismatch - if (this.state.isOpen) { - this.setState({isOpen: false, menuStyle: undefined}) + private handleChoose = (item: DropdownItem) => { + const {options, onChange} = this.props + const selectedOption = options.find(opt => opt.label === item.text) + if (selectedOption) { + onChange(selectedOption.value) } } } From 8243c94bf9441cd7ecbf43e7f06059dcb2a6e383 Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Mon, 3 Nov 2025 11:17:10 +0100 Subject: [PATCH 35/50] chore: refactored WizardDropdown to functional component --- .../components/wizard/WizardDropdown.tsx | 81 ++++++++++--------- 1 file changed, 44 insertions(+), 37 deletions(-) diff --git a/ui/src/reusable_ui/components/wizard/WizardDropdown.tsx b/ui/src/reusable_ui/components/wizard/WizardDropdown.tsx index ff6e51647e..b994a93dc6 100644 --- a/ui/src/reusable_ui/components/wizard/WizardDropdown.tsx +++ b/ui/src/reusable_ui/components/wizard/WizardDropdown.tsx @@ -1,7 +1,6 @@ -import React, {PureComponent} from 'react' +import React, {useMemo, useCallback} from 'react' import Dropdown from 'src/shared/components/Dropdown' import {DropdownItem} from 'src/types' -import {ErrorHandling} from 'src/shared/decorators/errors' interface DropdownOption { value: string @@ -19,44 +18,52 @@ interface Props { testId?: string } -class WizardDropdown extends PureComponent { - public render() { - const {label, subtext, halfWidth, testId} = this.props +const WizardDropdown: React.FC = ({ + value, + options, + placeholder = 'Select option...', + label, + subtext, + onChange, + halfWidth, + testId, +}) => { + const dropdownItems = useMemo( + () => + options.map(option => ({ + text: option.label, + })), + [options] + ) - return ( -
- - - {subtext && {subtext}} -
- ) - } - - private get dropdownItems(): DropdownItem[] { - return this.props.options.map(option => ({ - text: option.label, - })) - } - - private get selectedText(): string { - const {value, options, placeholder = 'Select option...'} = this.props + const selectedText = useMemo(() => { const selectedOption = options.find(opt => opt.value === value) return selectedOption ? selectedOption.label : placeholder - } + }, [value, options, placeholder]) + + const handleChoose = useCallback( + (item: DropdownItem) => { + const selectedOption = options.find(opt => opt.label === item.text) + if (selectedOption) { + onChange(selectedOption.value) + } + }, + [options, onChange] + ) - private handleChoose = (item: DropdownItem) => { - const {options, onChange} = this.props - const selectedOption = options.find(opt => opt.label === item.text) - if (selectedOption) { - onChange(selectedOption.value) - } - } + return ( +
+ + + {subtext && {subtext}} +
+ ) } -export default ErrorHandling(WizardDropdown) +export default WizardDropdown From d3dda454634a3aa98a60014af3688234f6176999 Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Mon, 3 Nov 2025 11:19:16 +0100 Subject: [PATCH 36/50] chore: updated TODO --- V3TODO.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/V3TODO.md b/V3TODO.md index 65e9425904..452bac451e 100644 --- a/V3TODO.md +++ b/V3TODO.md @@ -2,23 +2,23 @@ ## Features -- [ ] Support InfluxDB 3 Serverless +- [X] Support InfluxDB 3 Serverless - [ ] UI should have old UI look for default -- [ ] Enable new UI look from settings + - [ ] Enable new UI look from settings ## Issues - [ ] List databases for Core in Explorer shows fewer dbs than with `show databases` manually -- [ ] Command line help print-out wrongly formated due to new v3 option: +- [X] Command line help print-out wrongly formated due to new v3 option: ``` /influxdb-type:choice[influx|influx-enterprise|influx-relay|influx-v2|influx-v3-core|influx-v3-enterprise|influx-v3-cloud-dedicated] ``` ## Tests -- [ ] Unit test for Update Source for cloud dedicated fields -- [ ] Unit test for New Source for cloud dedicate fields -- [ ] Unit test for Client cloud dedicated fields +- [X] Unit test for Update Source for cloud dedicated fields +- [X] Unit test for New Source for cloud dedicate fields +- [X] Unit test for Client cloud dedicated fields - [ ] Unit test for query specific cloud dedicated fields - [ ] After finalizing UI, fix Cypress tests From c2b08fe6b9af7e775a00c25400684a38d7cb682a Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Tue, 4 Nov 2025 15:31:17 +0100 Subject: [PATCH 37/50] chore: workaround InfuxDB 3 core bucket name problems --- influx/influx_v3.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/influx/influx_v3.go b/influx/influx_v3.go index ce4fd98210..6e002e1296 100644 --- a/influx/influx_v3.go +++ b/influx/influx_v3.go @@ -89,6 +89,12 @@ func (c *Client) queryV3(u *url.URL, q chronograf.Query) (chronograf.Response, e cmd = stmt.String() logs.WithField("command", cmd).Debug("time condition added to SHOW TAG VALUES query") } + case *influxql.ShowRetentionPoliciesStatement: + // Clear ON modifier from `SHOW RETENTION POLICIES` since not supported in v3 + if clearOnModifier(s) { + cmd = stmt.String() + logs.WithField("command", cmd).Debug("ON modifier cleared from SHOW RETENTION POLICIES query") + } } // Query parameters @@ -190,6 +196,7 @@ func processV1Response(responseBody []byte) (chronograf.Response, error) { func clearMeasurementRP(source influxql.Source) bool { if mm, ok := source.(*influxql.Measurement); ok && mm.RetentionPolicy != "" { mm.RetentionPolicy = "" + mm.Database = "" return true } return false @@ -244,6 +251,16 @@ func parseDatabaseNameFromStatement(stmt influxql.Statement) string { return "" } +// clearOnModifier clears ON modifiers from show retention policies in the source. +// Returns true if the retention policy was cleared. +func clearOnModifier(stmt influxql.Statement) bool { + if mm, ok := stmt.(*influxql.ShowRetentionPoliciesStatement); ok && mm.Database != "" { + mm.Database = "" + return true + } + return false +} + // buildSeriesResponse creates a standardized InfluxQL response with multiple series func buildSeriesResponse(seriesResults []series) (chronograf.Response, error) { response := fakeInfluxResponse{ From 83b47a71230319614eb6001e67dca4d440bde0bf Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Tue, 4 Nov 2025 15:32:32 +0100 Subject: [PATCH 38/50] chore: fixes for cloud dedicated --- mocks/timeseries.go | 3 +- server/server.go | 40 ++++++++++--------- server/service.go | 9 +++-- .../influxdb/AdminInfluxDBTabbedPage.tsx | 4 +- 4 files changed, 31 insertions(+), 25 deletions(-) diff --git a/mocks/timeseries.go b/mocks/timeseries.go index de6319059c..5b54a5a131 100644 --- a/mocks/timeseries.go +++ b/mocks/timeseries.go @@ -4,6 +4,7 @@ import ( "context" "github.com/influxdata/chronograf" + "github.com/influxdata/chronograf/influx" ) var _ chronograf.TimeSeries = &TimeSeries{} @@ -25,7 +26,7 @@ type TimeSeries struct { } // New implements TimeSeriesClient -func (t *TimeSeries) New(chronograf.Source, chronograf.Logger) (chronograf.TimeSeries, error) { +func (t *TimeSeries) New(chronograf.Source, chronograf.Logger, influx.V3Config) (chronograf.TimeSeries, error) { return t, nil } diff --git a/server/server.go b/server/server.go index 6e35bcb798..d102442bb9 100644 --- a/server/server.go +++ b/server/server.go @@ -58,17 +58,18 @@ type Server struct { Cert flags.Filename `long:"cert" description:"Path to PEM encoded public key certificate. " env:"TLS_CERTIFICATE"` Key flags.Filename `long:"key" description:"Path to private key associated with given certificate. " env:"TLS_PRIVATE_KEY"` - InfluxDBType string `long:"influxdb-type" description:"InfluxDB server type instance. Valid values: influx, influx-enterprise, influx-relay, influx-v2, influx-v3-core, influx-v3-enterprise, influx-v3-clustered, influx-v3-cloud-dedicated, influx-v3-serverless" env:"INFLUXDB_TYPE"` - InfluxDBURL string `long:"influxdb-url" description:"Location of your InfluxDB instance" env:"INFLUXDB_URL"` - InfluxDBUsername string `long:"influxdb-username" description:"Username for your InfluxDB instance" env:"INFLUXDB_USERNAME"` - InfluxDBPassword string `long:"influxdb-password" description:"Password for your InfluxDB instance" env:"INFLUXDB_PASSWORD"` - InfluxDBOrg string `long:"influxdb-org" description:"Organization for your InfluxDB v2 instance" env:"INFLUXDB_ORG"` - InfluxDBToken string `long:"influxdb-token" description:"Token for your InfluxDB v2, v3 Core/Enterprise, Cloud Dedicated or Serverless instance" env:"INFLUXDB_TOKEN"` - InfluxDBMgmtToken string `long:"influxdb-mgmt-token" description:"Management token for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_MGMT_TOKEN"` - InfluxDBClusterID string `long:"influxdb-cluster-id" description:"Cluster ID for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_CLUSTER_ID"` - InfluxDBAccountID string `long:"influxdb-account-id" description:"Account ID for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_ACCOUNT_ID"` - TagsCSVPath string `long:"tags-csv-path" description:"Path to a directory containing CSV files (per db) with tags for InfluxDB v3 sources. Used to populate the tags field in Query Editor for your InfluxDB Cloud Dedicated instance." env:"TAGS_CSV_PATH"` - InfluxDBDefaultDB string `long:"influxdb-default-db" description:"Default database for your InfluxDB instance" env:"INFLUXDB_DEFAULT_DB"` + InfluxDBType string `long:"influxdb-type" description:"InfluxDB server type instance. Valid values: influx, influx-enterprise, influx-relay, influx-v2, influx-v3-core, influx-v3-enterprise, influx-v3-clustered, influx-v3-cloud-dedicated, influx-v3-serverless" env:"INFLUXDB_TYPE"` + InfluxDBURL string `long:"influxdb-url" description:"Location of your InfluxDB instance" env:"INFLUXDB_URL"` + InfluxDBUsername string `long:"influxdb-username" description:"Username for your InfluxDB instance" env:"INFLUXDB_USERNAME"` + InfluxDBPassword string `long:"influxdb-password" description:"Password for your InfluxDB instance" env:"INFLUXDB_PASSWORD"` + InfluxDBOrg string `long:"influxdb-org" description:"Organization for your InfluxDB v2 instance" env:"INFLUXDB_ORG"` + InfluxDBToken string `long:"influxdb-token" description:"Token for your InfluxDB v2, v3 Core/Enterprise, Cloud Dedicated or Serverless instance" env:"INFLUXDB_TOKEN"` + InfluxDBMgmtToken string `long:"influxdb-mgmt-token" description:"Management token for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_MGMT_TOKEN"` + InfluxDBClusterID string `long:"influxdb-cluster-id" description:"Cluster ID for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_CLUSTER_ID"` + InfluxDBAccountID string `long:"influxdb-account-id" description:"Account ID for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_ACCOUNT_ID"` + TagsCSVPath string `long:"tags-csv-path" description:"Path to a directory containing CSV files (per db) with tags for InfluxDB v3 sources. Used to populate the tags field in Query Editor for your InfluxDB Cloud Dedicated instance." env:"TAGS_CSV_PATH"` + InfluxDBDefaultDB string `long:"influxdb-default-db" description:"Default database for your InfluxDB instance" env:"INFLUXDB_DEFAULT_DB"` + InfluxDBCloudDedicatedMgmtURL string `long:"influxdb-cloud-dedicated-mgmt-url" description:"Management URL for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_CLOUD_DEDICATED_MGMT_URL" default:"https://console.influxdata.com"` InfluxDBClusteredClusterID string `long:"influxdb-clustered-cluster-id" description:"Cluster ID for your InfluxDB v3 Clustered instance" env:"INFLUXDB_CLUSTERED_CLUSTER_ID" default:"11111111-1111-1111-1111-111111111111"` InfluxDBClusteredAccountID string `long:"influxdb-clustered-account-id" description:"Account ID for your InfluxDB v3 Clustered instance" env:"INFLUXDB_CLUSTERED_ACCOUNT_ID" default:"11111111-1111-1111-1111-111111111111"` @@ -704,7 +705,12 @@ func (s *Server) Serve(ctx context.Context) { } } - service := openService(ctx, db, s.newBuilders(logger), logger, s.useAuth()) + service := openService(ctx, db, s.newBuilders(logger), logger, s.useAuth(), + influx.V3Config{ + CloudDedicatedManagementURL: s.InfluxDBCloudDedicatedMgmtURL, + ClusteredAccountID: s.InfluxDBClusteredAccountID, + ClusteredClusterID: s.InfluxDBClusteredClusterID, + }) service.SuperAdminProviderGroups = superAdminProviderGroups{ auth0: s.Auth0SuperAdminOrg, } @@ -713,11 +719,6 @@ func (s *Server) Serve(ctx context.Context) { HostPageDisabled: s.HostPageDisabled, CustomAutoRefresh: s.CustomAutoRefresh, } - service.V3Config = influx.V3Config{ - CloudDedicatedManagementURL: s.InfluxDBCloudDedicatedMgmtURL, - ClusteredAccountID: s.InfluxDBClusteredAccountID, - ClusteredClusterID: s.InfluxDBClusteredClusterID, - } if !validBasepath(s.Basepath) { err := fmt.Errorf("invalid basepath, must follow format \"/mybasepath\"") @@ -839,7 +840,7 @@ func (s *Server) Serve(ctx context.Context) { Info("Stopped serving chronograf at ", scheme, "://", listener.Addr()) } -func openService(ctx context.Context, db kv.Store, builder builders, logger chronograf.Logger, useAuth bool) Service { +func openService(ctx context.Context, db kv.Store, builder builders, logger chronograf.Logger, useAuth bool, v3Config influx.V3Config) Service { svc, err := kv.NewService(ctx, db, kv.WithLogger(logger)) if err != nil { logger.Error("Unable to create kv service", err) @@ -917,7 +918,8 @@ func openService(ctx context.Context, db kv.Store, builder builders, logger chro }, Logger: logger, UseAuth: useAuth, - Databases: &influx.Client{Logger: logger}, + V3Config: v3Config, + Databases: &influx.Client{Logger: logger, V3Config: v3Config}, } } diff --git a/server/service.go b/server/service.go index ecbaeba084..389ce10a6f 100644 --- a/server/service.go +++ b/server/service.go @@ -28,7 +28,7 @@ type superAdminProviderGroups struct { // TimeSeriesClient returns the correct client for a time series database. // todo(glinton): should this be always reconnecting? type TimeSeriesClient interface { - New(chronograf.Source, chronograf.Logger) (chronograf.TimeSeries, error) + New(chronograf.Source, chronograf.Logger, influx.V3Config) (chronograf.TimeSeries, error) } // ErrorMessage is the error response format for all service errors @@ -39,16 +39,17 @@ type ErrorMessage struct { // TimeSeries returns a new client connected to a time series database func (s *Service) TimeSeries(src chronograf.Source) (chronograf.TimeSeries, error) { - return s.TimeSeriesClient.New(src, s.Logger) + return s.TimeSeriesClient.New(src, s.Logger, s.V3Config) } // InfluxClient returns a new client to connect to OSS or Enterprise type InfluxClient struct{} // New creates a client to connect to OSS or enterprise -func (c *InfluxClient) New(src chronograf.Source, logger chronograf.Logger) (chronograf.TimeSeries, error) { +func (c *InfluxClient) New(src chronograf.Source, logger chronograf.Logger, v3Config influx.V3Config) (chronograf.TimeSeries, error) { client := &influx.Client{ - Logger: logger, + Logger: logger, + V3Config: v3Config, } if err := client.Connect(context.TODO(), &src); err != nil { return nil, err diff --git a/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx b/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx index 8512b03937..ebf710ca97 100644 --- a/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx +++ b/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx @@ -9,6 +9,7 @@ import { SOURCE_TYPE_INFLUX_V3_CLUSTERED, SOURCE_TYPE_INFLUX_V3_CORE, SOURCE_TYPE_INFLUX_V3_ENTERPRISE, + SOURCE_TYPE_INFLUX_V3_SERVERLESS, } from 'src/shared/constants' interface Props { @@ -29,7 +30,8 @@ export function isV3Source(source: Source) { source.type === SOURCE_TYPE_INFLUX_V3_CORE || source.type === SOURCE_TYPE_INFLUX_V3_ENTERPRISE || source.type === SOURCE_TYPE_INFLUX_V3_CLUSTERED || - source.type === SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED + source.type === SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED || + source.type === SOURCE_TYPE_INFLUX_V3_SERVERLESS ) } From 00b981c793ae4f7a5022a2ae721a5070a49cb1c7 Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Fri, 7 Nov 2025 18:09:38 +0100 Subject: [PATCH 39/50] feat: V3 support by cmd/env option --- chronograf.go | 7 + cmd/chronograf/main.go | 1 + influx/influx.go | 2 +- influx/influx_test.go | 6 +- influx/v3config.go | 7 - mocks/timeseries.go | 3 +- server/env.go | 2 + server/server.go | 7 +- server/service.go | 6 +- server/sources_test.go | 3 +- .../influxdb/AdminInfluxDBScopedPage.tsx | 3 +- .../influxdb/AdminInfluxDBTabbedPage.tsx | 18 +-- .../influxdb/DatabaseManagerPage.tsx | 3 +- ui/src/index.tsx | 6 +- ui/src/shared/actions/env.ts | 14 ++ ui/src/shared/apis/env.ts | 1 + ui/src/shared/constants/index.ts | 12 +- ui/src/shared/reducers/env.ts | 9 ++ ui/src/sources/components/SourceStep.tsx | 151 ++++++++++-------- ui/src/types/actions/app.ts | 9 ++ ui/src/types/env.ts | 1 + 21 files changed, 165 insertions(+), 106 deletions(-) delete mode 100644 influx/v3config.go diff --git a/chronograf.go b/chronograf.go index f128a08cc5..67449d3fd0 100644 --- a/chronograf.go +++ b/chronograf.go @@ -121,6 +121,12 @@ func IsV3SrcType(srcType string) bool { srcType == InfluxDBv3Serverless } +type V3Config struct { + CloudDedicatedManagementURL string + ClusteredAccountID string + ClusteredClusterID string +} + // TSDBStatus represents the current status of a time series database type TSDBStatus interface { // Connect will connect to the time series using the information in `Source`. @@ -999,6 +1005,7 @@ type Environment struct { TelegrafSystemInterval time.Duration `json:"telegrafSystemInterval"` HostPageDisabled bool `json:"HostPageDisabled"` CustomAutoRefresh string `json:"customAutoRefresh,omitempty"` + V3SupportEnabled bool `json:"v3SupportEnabled"` } // KVClient defines what each kv store should be capable of. diff --git a/cmd/chronograf/main.go b/cmd/chronograf/main.go index de06dc3a09..e9e940c1ce 100644 --- a/cmd/chronograf/main.go +++ b/cmd/chronograf/main.go @@ -48,6 +48,7 @@ func main() { code = 0 } } + fmt.Printf("Error: %s\n", err) os.Exit(code) } diff --git a/influx/influx.go b/influx/influx.go index d6305f7450..0f5ee60fc3 100644 --- a/influx/influx.go +++ b/influx/influx.go @@ -48,7 +48,7 @@ type Client struct { SrcType string Logger chronograf.Logger DefaultDB string - V3Config V3Config + V3Config chronograf.V3Config csvTagsStore *CSVTagsStore // (optional) Store to load CSV tag files from source.TagsCSVPath directory } diff --git a/influx/influx_test.go b/influx/influx_test.go index 106b40130c..bc69227a33 100644 --- a/influx/influx_test.go +++ b/influx/influx_test.go @@ -923,7 +923,7 @@ func Test_Influx_ValidateAuth_V3CloudDedicated(t *testing.T) { defer ts.Close() client, err := NewClient(ts.URL, log.New(log.DebugLevel)) - client.V3Config = influx.V3Config{ + client.V3Config = chronograf.V3Config{ CloudDedicatedManagementURL: ts.URL, ClusteredAccountID: "test-account-id", ClusteredClusterID: "test-cluster-id", @@ -1062,7 +1062,7 @@ func Test_Influx_Authorization_V3Clustered(t *testing.T) { DatabaseToken: "test-token-clustered", ManagementToken: "test-mgmt-token", } - client.V3Config = influx.V3Config{ + client.V3Config = chronograf.V3Config{ CloudDedicatedManagementURL: ts.URL, ClusteredAccountID: "test-account-id", ClusteredClusterID: "test-cluster-id", @@ -1113,7 +1113,7 @@ func Test_Influx_Authorization_V3CloudDedicated(t *testing.T) { AccountID: "test-account-id", ClusterID: "test-cluster-id", } - client.V3Config = influx.V3Config{ + client.V3Config = chronograf.V3Config{ CloudDedicatedManagementURL: ts.URL, } client.Connect(context.Background(), source) diff --git a/influx/v3config.go b/influx/v3config.go deleted file mode 100644 index 59911efdab..0000000000 --- a/influx/v3config.go +++ /dev/null @@ -1,7 +0,0 @@ -package influx - -type V3Config struct { - CloudDedicatedManagementURL string - ClusteredAccountID string - ClusteredClusterID string -} diff --git a/mocks/timeseries.go b/mocks/timeseries.go index 5b54a5a131..39b5dea60b 100644 --- a/mocks/timeseries.go +++ b/mocks/timeseries.go @@ -4,7 +4,6 @@ import ( "context" "github.com/influxdata/chronograf" - "github.com/influxdata/chronograf/influx" ) var _ chronograf.TimeSeries = &TimeSeries{} @@ -26,7 +25,7 @@ type TimeSeries struct { } // New implements TimeSeriesClient -func (t *TimeSeries) New(chronograf.Source, chronograf.Logger, influx.V3Config) (chronograf.TimeSeries, error) { +func (t *TimeSeries) New(chronograf.Source, chronograf.Logger, chronograf.V3Config) (chronograf.TimeSeries, error) { return t, nil } diff --git a/server/env.go b/server/env.go index bbf43ba2a9..7cf7e20a8a 100644 --- a/server/env.go +++ b/server/env.go @@ -11,6 +11,7 @@ type envResponse struct { TelegrafSystemInterval string `json:"telegrafSystemInterval"` HostPageDisabled bool `json:"hostPageDisabled"` CustomAutoRefresh string `json:"customAutoRefresh,omitempty"` + V3SupportEnabled bool `json:"v3SupportEnabled"` } func newEnvResponse(env chronograf.Environment) *envResponse { @@ -21,6 +22,7 @@ func newEnvResponse(env chronograf.Environment) *envResponse { TelegrafSystemInterval: env.TelegrafSystemInterval.String(), HostPageDisabled: env.HostPageDisabled, CustomAutoRefresh: env.CustomAutoRefresh, + V3SupportEnabled: env.V3SupportEnabled, } } diff --git a/server/server.go b/server/server.go index d102442bb9..3a21287b10 100644 --- a/server/server.go +++ b/server/server.go @@ -73,6 +73,7 @@ type Server struct { InfluxDBCloudDedicatedMgmtURL string `long:"influxdb-cloud-dedicated-mgmt-url" description:"Management URL for your InfluxDB Cloud Dedicated instance" env:"INFLUXDB_CLOUD_DEDICATED_MGMT_URL" default:"https://console.influxdata.com"` InfluxDBClusteredClusterID string `long:"influxdb-clustered-cluster-id" description:"Cluster ID for your InfluxDB v3 Clustered instance" env:"INFLUXDB_CLUSTERED_CLUSTER_ID" default:"11111111-1111-1111-1111-111111111111"` InfluxDBClusteredAccountID string `long:"influxdb-clustered-account-id" description:"Account ID for your InfluxDB v3 Clustered instance" env:"INFLUXDB_CLUSTERED_ACCOUNT_ID" default:"11111111-1111-1111-1111-111111111111"` + InfluxDBV3SupportEnabled bool `long:"influxdb-v3-support-enabled" description:"Enable InfluxDB v3 support" env:"INFLUXDB_V3_SUPPORT_ENABLED"` KapacitorURL string `long:"kapacitor-url" description:"Location of your Kapacitor instance" env:"KAPACITOR_URL"` KapacitorUsername string `long:"kapacitor-username" description:"Username of your Kapacitor instance" env:"KAPACITOR_USERNAME"` @@ -623,6 +624,7 @@ func (s *Server) Serve(ctx context.Context) { go rotateSuperAdminNonce(ctx, s.NonceExpiration) logger := clog.New(clog.ParseLevel(s.LogLevel)) + logger.Info("Starting Chronograf ", s.BuildInfo.Version, s.BuildInfo.Commit) // Validate InfluxDBType if provided if s.InfluxDBType != "" { @@ -706,7 +708,7 @@ func (s *Server) Serve(ctx context.Context) { } service := openService(ctx, db, s.newBuilders(logger), logger, s.useAuth(), - influx.V3Config{ + chronograf.V3Config{ CloudDedicatedManagementURL: s.InfluxDBCloudDedicatedMgmtURL, ClusteredAccountID: s.InfluxDBClusteredAccountID, ClusteredClusterID: s.InfluxDBClusteredClusterID, @@ -718,6 +720,7 @@ func (s *Server) Serve(ctx context.Context) { TelegrafSystemInterval: s.TelegrafSystemInterval, HostPageDisabled: s.HostPageDisabled, CustomAutoRefresh: s.CustomAutoRefresh, + V3SupportEnabled: s.InfluxDBV3SupportEnabled, } if !validBasepath(s.Basepath) { @@ -840,7 +843,7 @@ func (s *Server) Serve(ctx context.Context) { Info("Stopped serving chronograf at ", scheme, "://", listener.Addr()) } -func openService(ctx context.Context, db kv.Store, builder builders, logger chronograf.Logger, useAuth bool, v3Config influx.V3Config) Service { +func openService(ctx context.Context, db kv.Store, builder builders, logger chronograf.Logger, useAuth bool, v3Config chronograf.V3Config) Service { svc, err := kv.NewService(ctx, db, kv.WithLogger(logger)) if err != nil { logger.Error("Unable to create kv service", err) diff --git a/server/service.go b/server/service.go index 389ce10a6f..8abb75131b 100644 --- a/server/service.go +++ b/server/service.go @@ -18,7 +18,7 @@ type Service struct { SuperAdminProviderGroups superAdminProviderGroups Env chronograf.Environment Databases chronograf.Databases - V3Config influx.V3Config + V3Config chronograf.V3Config } type superAdminProviderGroups struct { @@ -28,7 +28,7 @@ type superAdminProviderGroups struct { // TimeSeriesClient returns the correct client for a time series database. // todo(glinton): should this be always reconnecting? type TimeSeriesClient interface { - New(chronograf.Source, chronograf.Logger, influx.V3Config) (chronograf.TimeSeries, error) + New(chronograf.Source, chronograf.Logger, chronograf.V3Config) (chronograf.TimeSeries, error) } // ErrorMessage is the error response format for all service errors @@ -46,7 +46,7 @@ func (s *Service) TimeSeries(src chronograf.Source) (chronograf.TimeSeries, erro type InfluxClient struct{} // New creates a client to connect to OSS or enterprise -func (c *InfluxClient) New(src chronograf.Source, logger chronograf.Logger, v3Config influx.V3Config) (chronograf.TimeSeries, error) { +func (c *InfluxClient) New(src chronograf.Source, logger chronograf.Logger, v3Config chronograf.V3Config) (chronograf.TimeSeries, error) { client := &influx.Client{ Logger: logger, V3Config: v3Config, diff --git a/server/sources_test.go b/server/sources_test.go index 7ead167dff..e3b19edefd 100644 --- a/server/sources_test.go +++ b/server/sources_test.go @@ -14,7 +14,6 @@ import ( "github.com/bouk/httprouter" "github.com/google/go-cmp/cmp" "github.com/influxdata/chronograf" - "github.com/influxdata/chronograf/influx" "github.com/influxdata/chronograf/log" "github.com/influxdata/chronograf/mocks" ) @@ -1106,7 +1105,7 @@ func TestService_UpdateSource(t *testing.T) { OrganizationsStore: tt.fields.OrganizationsStore, }, Logger: tt.fields.Logger, - V3Config: influx.V3Config{ + V3Config: chronograf.V3Config{ CloudDedicatedManagementURL: ts.URL, }, } diff --git a/ui/src/admin/containers/influxdb/AdminInfluxDBScopedPage.tsx b/ui/src/admin/containers/influxdb/AdminInfluxDBScopedPage.tsx index e4b9429cb1..7ddc15748c 100644 --- a/ui/src/admin/containers/influxdb/AdminInfluxDBScopedPage.tsx +++ b/ui/src/admin/containers/influxdb/AdminInfluxDBScopedPage.tsx @@ -14,7 +14,8 @@ import {ErrorHandling} from 'src/shared/decorators/errors' import {notify as notifyAction} from 'src/shared/actions/notifications' import {RemoteDataState, Source} from 'src/types' -import {isConnectedToLDAP, isV3Source} from './AdminInfluxDBTabbedPage' +import {isConnectedToLDAP} from './AdminInfluxDBTabbedPage' +import {isV3Source} from 'src/shared/constants' const mapDispatchToProps = { loadUsers: loadUsersAsync, diff --git a/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx b/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx index ebf710ca97..755e41ec6c 100644 --- a/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx +++ b/ui/src/admin/containers/influxdb/AdminInfluxDBTabbedPage.tsx @@ -4,13 +4,7 @@ import SubSections from 'src/shared/components/SubSections' import {Source, SourceAuthenticationMethod} from 'src/types' import {PageSection} from 'src/types/shared' import {WrapToPage} from './AdminInfluxDBScopedPage' -import { - SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED, - SOURCE_TYPE_INFLUX_V3_CLUSTERED, - SOURCE_TYPE_INFLUX_V3_CORE, - SOURCE_TYPE_INFLUX_V3_ENTERPRISE, - SOURCE_TYPE_INFLUX_V3_SERVERLESS, -} from 'src/shared/constants' +import {isV3Source} from 'src/shared/constants' interface Props { source: Source @@ -25,16 +19,6 @@ export function isConnectedToLDAP(source: Source) { return source.authentication === SourceAuthenticationMethod.LDAP } -export function isV3Source(source: Source) { - return ( - source.type === SOURCE_TYPE_INFLUX_V3_CORE || - source.type === SOURCE_TYPE_INFLUX_V3_ENTERPRISE || - source.type === SOURCE_TYPE_INFLUX_V3_CLUSTERED || - source.type === SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED || - source.type === SOURCE_TYPE_INFLUX_V3_SERVERLESS - ) -} - export const AdminTabs = ({ source, activeTab, diff --git a/ui/src/admin/containers/influxdb/DatabaseManagerPage.tsx b/ui/src/admin/containers/influxdb/DatabaseManagerPage.tsx index a0d0d6b469..e92c830407 100644 --- a/ui/src/admin/containers/influxdb/DatabaseManagerPage.tsx +++ b/ui/src/admin/containers/influxdb/DatabaseManagerPage.tsx @@ -17,7 +17,8 @@ import { } from 'src/shared/copy/notifications' import {Source} from 'src/types' import {Database, RetentionPolicy} from 'src/types/influxAdmin' -import AdminInfluxDBTabbedPage, {isV3Source} from './AdminInfluxDBTabbedPage' +import AdminInfluxDBTabbedPage from './AdminInfluxDBTabbedPage' +import {isV3Source} from 'src/shared/constants' interface Props { source: Source diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 135f13d3bf..369953f243 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -48,7 +48,10 @@ import {getMeAsync} from 'src/shared/actions/auth' import {disablePresentationMode} from 'src/shared/actions/app' import {errorThrown} from 'src/shared/actions/errors' import {notify} from 'src/shared/actions/notifications' -import {setHostPageDisplayStatus} from 'src/shared/actions/env' +import { + setHostPageDisplayStatus, + setV3SupportEnabled, +} from 'src/shared/actions/env' import {TimeMachineContextProvider} from 'src/shared/utils/TimeMachineContext' import {getEnv} from 'src/shared/apis/env' @@ -115,6 +118,7 @@ const populateEnv = async url => { try { const envVars = await getEnv(url) dispatch(setHostPageDisplayStatus(envVars.hostPageDisabled)) + dispatch(setV3SupportEnabled(envVars.v3SupportEnabled)) setCustomAutoRefreshOptions(envVars.customAutoRefresh) } catch (error) { console.error('Error fetching envVars', error) diff --git a/ui/src/shared/actions/env.ts b/ui/src/shared/actions/env.ts index 377e637bbe..09758e7d8b 100644 --- a/ui/src/shared/actions/env.ts +++ b/ui/src/shared/actions/env.ts @@ -2,6 +2,7 @@ import { ActionTypes, SetTelegrafSystemIntervalAction, SetHostPageDisplayStatusAction, + SetV3SupportEnabledAction, } from 'src/types/actions/app' export type SetTelegrafSystemIntervalActionCreator = ( @@ -12,6 +13,10 @@ export type SetHostPageDisplayStatusActionCreator = ( isHostPageDisabled: boolean ) => SetHostPageDisplayStatusAction +export type SetV3SupportEnabledActionCreator = ( + v3SupportEnabled: boolean +) => SetV3SupportEnabledAction + export const setTelegrafSystemInterval: SetTelegrafSystemIntervalActionCreator = ( telegrafSystemInterval ): SetTelegrafSystemIntervalAction => ({ @@ -29,3 +34,12 @@ export const setHostPageDisplayStatus: SetHostPageDisplayStatusActionCreator = ( hostPageDisabled, }, }) + +export const setV3SupportEnabled: SetV3SupportEnabledActionCreator = ( + v3SupportEnabled +): SetV3SupportEnabledAction => ({ + type: ActionTypes.SetV3SupportEnabled, + payload: { + v3SupportEnabled, + }, +}) diff --git a/ui/src/shared/apis/env.ts b/ui/src/shared/apis/env.ts index fc7e39880f..dce06513f3 100644 --- a/ui/src/shared/apis/env.ts +++ b/ui/src/shared/apis/env.ts @@ -4,6 +4,7 @@ const DEFAULT_ENVS = { telegrafSystemInterval: '1m', hostPageDisabled: false, customAutoRefresh: undefined, + v3SupportEnabled: false, } export const getEnv = async url => { diff --git a/ui/src/shared/constants/index.ts b/ui/src/shared/constants/index.ts index 6fafb38317..3f135f42ce 100644 --- a/ui/src/shared/constants/index.ts +++ b/ui/src/shared/constants/index.ts @@ -1,6 +1,6 @@ import _ from 'lodash' -import {TemplateValueType, TemplateType, Template} from 'src/types' +import {TemplateValueType, TemplateType, Template, Source} from 'src/types' import {CellType} from 'src/types/dashboards' export const VERSION = process.env.APP_VERSION @@ -534,6 +534,16 @@ export const SOURCE_TYPE_INFLUX_V3_CLUSTERED = 'influx-v3-clustered' export const SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED = 'influx-v3-cloud-dedicated' export const SOURCE_TYPE_INFLUX_V3_SERVERLESS = 'influx-v3-serverless' +export const isV3Source = (source: Partial): boolean => { + return ( + source.type === SOURCE_TYPE_INFLUX_V3_CORE || + source.type === SOURCE_TYPE_INFLUX_V3_ENTERPRISE || + source.type === SOURCE_TYPE_INFLUX_V3_CLUSTERED || + source.type === SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED || + source.type === SOURCE_TYPE_INFLUX_V3_SERVERLESS + ) +} + export enum DataType { flux = 'flux', influxQL = 'influxQL', diff --git a/ui/src/shared/reducers/env.ts b/ui/src/shared/reducers/env.ts index ab03b1f4fc..d0dc620b7f 100644 --- a/ui/src/shared/reducers/env.ts +++ b/ui/src/shared/reducers/env.ts @@ -4,6 +4,7 @@ import {Env} from 'src/types/' const initialState: Env = { telegrafSystemInterval: '1m', hostPageDisabled: false, + v3SupportEnabled: false, } const envReducer = (state = initialState, action: Action) => { @@ -24,6 +25,14 @@ const envReducer = (state = initialState, action: Action) => { } } + case ActionTypes.SetV3SupportEnabled: { + const {v3SupportEnabled} = action.payload + return { + ...state, + v3SupportEnabled, + } + } + default: return state } diff --git a/ui/src/sources/components/SourceStep.tsx b/ui/src/sources/components/SourceStep.tsx index 399ea3272f..783b10387d 100644 --- a/ui/src/sources/components/SourceStep.tsx +++ b/ui/src/sources/components/SourceStep.tsx @@ -32,6 +32,7 @@ import { import {insecureSkipVerifyText} from 'src/shared/copy/tooltipText' import { DEFAULT_SOURCE, + isV3Source, SOURCE_TYPE_INFLUX_V1, SOURCE_TYPE_INFLUX_V1_ENTERPRISE, SOURCE_TYPE_INFLUX_V1_RELAY, @@ -45,7 +46,7 @@ import { import {SUPERADMIN_ROLE} from 'src/auth/roles' // Types -import {Me, Source} from 'src/types' +import {Me, Source, Env} from 'src/types' import {NextReturn} from 'src/types/wizard' const isNewSource = (source: Partial) => !source.id @@ -59,6 +60,7 @@ interface Props { onBoarding?: boolean me: Me isUsingAuth: boolean + env: Env } interface State { @@ -111,48 +113,51 @@ class SourceStep extends PureComponent { } public render() { - const {source} = this.state - const {isUsingAuth, onBoarding} = this.props + const {source, serverType} = this.state + const {isUsingAuth, onBoarding, env} = this.props + const isV3 = isV3Source(source) return ( <> {isUsingAuth && onBoarding && this.authIndicator} - + {env.v3SupportEnabled && ( + + )} { onChange={this.onChangeInput('name')} testId="connection-name--input" /> - {(this.state.serverType === SOURCE_TYPE_INFLUX_V1 || - this.state.serverType === SOURCE_TYPE_INFLUX_V2) && ( + {(serverType === SOURCE_TYPE_INFLUX_V1 || + serverType === SOURCE_TYPE_INFLUX_V2) && ( <> { { )} {/* InfluxDB 3 Core/Enterprise/Serverless fields */} - {(this.state.serverType === SOURCE_TYPE_INFLUX_V3_CORE || - this.state.serverType === SOURCE_TYPE_INFLUX_V3_ENTERPRISE || - this.state.serverType === SOURCE_TYPE_INFLUX_V3_SERVERLESS) && ( + {(serverType === SOURCE_TYPE_INFLUX_V3_CORE || + serverType === SOURCE_TYPE_INFLUX_V3_ENTERPRISE || + serverType === SOURCE_TYPE_INFLUX_V3_SERVERLESS) && ( <> { )} {/* InfluxDB Clustered fields */} - {this.state.serverType === SOURCE_TYPE_INFLUX_V3_CLUSTERED && ( + {serverType === SOURCE_TYPE_INFLUX_V3_CLUSTERED && ( <> { )} {/* InfluxDB Cloud Dedicated fields */} - {this.state.serverType === SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED && ( + {serverType === SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED && ( <> { label="Telegraf Database Name" onChange={this.onChangeInput('telegraf')} /> - - {this.state.serverType === SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED && ( + {!isV3 && ( + + )} + {serverType === SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED && ( { )} {!onBoarding && ( )} + {!env.v3SupportEnabled && !isV3 && ( + + )} {this.isHTTPS && ( { this.setState({source: {...source, [key]: value}}) setError(false) } + private changeAuth = (v2: boolean) => { + this.changeSourceType( + v2 ? SOURCE_TYPE_INFLUX_V2 : SOURCE_TYPE_INFLUX_V1, + v2 ? '2.x' : '1.x' + ) + } + private changeSourceType = (type: string, version: string) => { const {source} = this.state this.setState({ + serverType: type, source: { ...source, username: '', @@ -426,9 +447,7 @@ class SourceStep extends PureComponent { return _.get(source, 'type', '').includes('enterprise') } - private getServerTypeFromSource = ( - source: Partial - ): string | undefined => { + private getServerTypeFromSource = (source: Partial): string => { if ( source.type === SOURCE_TYPE_INFLUX_V1 || source.type === SOURCE_TYPE_INFLUX_V2 || @@ -447,12 +466,10 @@ class SourceStep extends PureComponent { // Special v1 subtypes are displayed as v1 return SOURCE_TYPE_INFLUX_V1 } - return undefined + return SOURCE_TYPE_INFLUX_V1 } private handleServerTypeChange = (value: string) => { - this.setState({serverType: value}) - switch (value) { case SOURCE_TYPE_INFLUX_V2: this.changeSourceType(value, '2.x') @@ -479,12 +496,16 @@ class SourceStep extends PureComponent { } } +const mstp = ({env}) => ({ + env, +}) + const mdtp = { notify: notifyAction, addSource: addSourceAction, updateSource: updateSourceAction, } -export default connect(null, mdtp, null, {forwardRef: true})( +export default connect(mstp, mdtp, null, {forwardRef: true})( ErrorHandling(SourceStep) ) diff --git a/ui/src/types/actions/app.ts b/ui/src/types/actions/app.ts index 0a1eaa8f3b..5a96089627 100644 --- a/ui/src/types/actions/app.ts +++ b/ui/src/types/actions/app.ts @@ -15,6 +15,7 @@ export enum ActionTypes { SetTimeZone = 'SET_TIME_ZONE', SetTelegrafSystemInterval = 'SET_TELEGRAF_SYSTEM_INTERVAL', SetHostPageDisplayStatus = 'SET_HOST_PAGE_DISPLAY_STATUS', + SetV3SupportEnabled = 'SET_V3_SUPPORT_ENABLED', } export type Action = @@ -26,6 +27,7 @@ export type Action = | SetTimeZoneAction | SetTelegrafSystemIntervalAction | SetHostPageDisplayStatusAction + | SetV3SupportEnabledAction | AddingAnnotationAction | SetAnnotationsDisplaySettingAction @@ -83,3 +85,10 @@ export interface SetHostPageDisplayStatusAction { hostPageDisabled: boolean } } + +export interface SetV3SupportEnabledAction { + type: ActionTypes.SetV3SupportEnabled + payload: { + v3SupportEnabled: boolean + } +} diff --git a/ui/src/types/env.ts b/ui/src/types/env.ts index 519784d923..0a17543100 100644 --- a/ui/src/types/env.ts +++ b/ui/src/types/env.ts @@ -1,4 +1,5 @@ export interface Env { telegrafSystemInterval: string hostPageDisabled: boolean + v3SupportEnabled: boolean } From 5bbf5c59d2100950f016895460fe74808159ee87 Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Fri, 7 Nov 2025 20:19:39 +0100 Subject: [PATCH 40/50] test: env tests fixed and extended --- server/env_test.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/server/env_test.go b/server/env_test.go index 22ea286ab0..6ed9d377a7 100644 --- a/server/env_test.go +++ b/server/env_test.go @@ -35,7 +35,7 @@ func TestEnvironment(t *testing.T) { wants: wants{ statusCode: 200, contentType: "application/json", - body: `{"links":{"self":"/chronograf/v1/env"},"telegrafSystemInterval":"1m0s","hostPageDisabled":false}`, + body: `{"links":{"self":"/chronograf/v1/env"},"telegrafSystemInterval":"1m0s","hostPageDisabled":false,"v3SupportEnabled":false}`, }, }, { @@ -49,7 +49,20 @@ func TestEnvironment(t *testing.T) { wants: wants{ statusCode: 200, contentType: "application/json", - body: `{"links":{"self":"/chronograf/v1/env"},"telegrafSystemInterval":"2m0s","hostPageDisabled":false,"customAutoRefresh": "500ms=500"}`, + body: `{"links":{"self":"/chronograf/v1/env"},"telegrafSystemInterval":"2m0s","hostPageDisabled":false,"customAutoRefresh": "500ms=500","v3SupportEnabled":false}`, + }, + }, + { + name: "Get environment with V3SupportEnabled", + fields: fields{ + Environment: chronograf.Environment{ + V3SupportEnabled: true, + }, + }, + wants: wants{ + statusCode: 200, + contentType: "application/json", + body: `{"links":{"self":"/chronograf/v1/env"},"telegrafSystemInterval":"0s","hostPageDisabled":false,"v3SupportEnabled":true}`, }, }, } From c70f3361765a993da42780fd7c513515c6da1fb9 Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Tue, 11 Nov 2025 17:24:50 +0100 Subject: [PATCH 41/50] chore: udpate V3todo --- V3TODO.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/V3TODO.md b/V3TODO.md index 452bac451e..da60dbd2c7 100644 --- a/V3TODO.md +++ b/V3TODO.md @@ -3,12 +3,12 @@ ## Features - [X] Support InfluxDB 3 Serverless -- [ ] UI should have old UI look for default - - [ ] Enable new UI look from settings +- [X] UI should have old UI look for default + - [X] Enable new UI look from settings ## Issues -- [ ] List databases for Core in Explorer shows fewer dbs than with `show databases` manually +- [X] List databases for Core in Explorer shows fewer dbs than with `show databases` manually - [X] Command line help print-out wrongly formated due to new v3 option: ``` /influxdb-type:choice[influx|influx-enterprise|influx-relay|influx-v2|influx-v3-core|influx-v3-enterprise|influx-v3-cloud-dedicated] From b12f2fa46a8acf1e1d233051a5a49f785398cc8a Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Tue, 11 Nov 2025 17:27:47 +0100 Subject: [PATCH 42/50] fix: allow clearing source properties --- server/sources.go | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/server/sources.go b/server/sources.go index 54054e3eee..d442e3b885 100644 --- a/server/sources.go +++ b/server/sources.go @@ -453,11 +453,14 @@ func (s *Service) UpdateSource(w http.ResponseWriter, r *http.Request) { if req.Type != "" { src.Type = req.Type } - if req.Telegraf != "" { - src.Telegraf = req.Telegraf - } + src.Telegraf = req.Telegraf src.DefaultRP = req.DefaultRP src.DefaultDB = req.DefaultDB + src.ManagementToken = req.ManagementToken + src.DatabaseToken = req.DatabaseToken + src.ClusterID = req.ClusterID + src.AccountID = req.AccountID + src.TagsCSVPath = req.TagsCSVPath defaultOrg, err := s.Store.Organizations(ctx).DefaultOrganization(ctx) if err != nil { @@ -479,20 +482,6 @@ func (s *Service) UpdateSource(w http.ResponseWriter, r *http.Request) { } src.Type = dbType - if req.ManagementToken != "" { - src.ManagementToken = req.ManagementToken - } - if req.DatabaseToken != "" { - src.DatabaseToken = req.DatabaseToken - } - if req.ClusterID != "" { - src.ClusterID = req.ClusterID - } - if req.AccountID != "" { - src.AccountID = req.AccountID - } - src.TagsCSVPath = req.TagsCSVPath - if err := s.validateCredentials(ctx, &src); err != nil { Error(w, http.StatusBadRequest, err.Error(), s.Logger) return From 65cdfc9652f66c268542d590e45115350c15ee9b Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Wed, 12 Nov 2025 20:04:40 +0100 Subject: [PATCH 43/50] test: extending UpdateSource tests --- server/sources_test.go | 164 ++++++++++++++++++++++++++++------------- 1 file changed, 112 insertions(+), 52 deletions(-) diff --git a/server/sources_test.go b/server/sources_test.go index e3b19edefd..39ab96a8a2 100644 --- a/server/sources_test.go +++ b/server/sources_test.go @@ -827,6 +827,7 @@ func TestService_UpdateSource(t *testing.T) { GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { return chronograf.Source{ ID: 2, + URL: "http://old.url", Type: chronograf.InfluxDBv3Core, DatabaseToken: "old-token", }, nil @@ -1026,6 +1027,62 @@ func TestService_UpdateSource(t *testing.T) { wantContentType: "application/json", wantBody: func(url string) string { return fmt.Sprintf(`{"id":"5","name":"v3-cloud-dedicated","type":"influx-v3-cloud-dedicated","clusterId":"12345678-1234-5678-1234-567812345678","accountId":"87654321-4321-8765-4321-876543218765","managementToken":"cloud-mgmt-abc","databaseToken":"cloud-token-abc","url":"%s","default":false,"telegraf":"telegraf","organization":"1337","defaultRP":"","defaultDB":"cloud_db","authentication":"unknown","links":{"self":"/chronograf/v1/sources/5","kapacitors":"/chronograf/v1/sources/5/kapacitors","services":"/chronograf/v1/sources/5/services","proxy":"/chronograf/v1/sources/5/proxy","queries":"/chronograf/v1/sources/5/queries","write":"/chronograf/v1/sources/5/write","permissions":"/chronograf/v1/sources/5/permissions","users":"/chronograf/v1/sources/5/users","databases":"/chronograf/v1/sources/5/dbs","annotations":"/chronograf/v1/sources/5/annotations","health":"/chronograf/v1/sources/5/health"}} +`, url) + }, + }, + { + name: "Update InfluxDB v3 Cloud Dedicated source to default DB", + args: args{ + w: httptest.NewRecorder(), + r: httptest.NewRequest( + "PATCH", + "http://any.url", + nil), + }, + fields: fields{ + SourcesStore: &mocks.SourcesStore{ + GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { + return chronograf.Source{ + ID: 6, + Type: chronograf.InfluxDBv3CloudDedicated, + DatabaseToken: "old-cloud-token", + ManagementToken: "old-cloud-mgmt", + ClusterID: "11111111-1111-1111-1111-111111111111", + AccountID: "22222222-2222-2222-2222-222222222222", + }, nil + }, + UpdateF: func(ctx context.Context, upd chronograf.Source) error { + return nil + }, + }, + OrganizationsStore: &mocks.OrganizationsStore{ + DefaultOrganizationF: func(context.Context) (*chronograf.Organization, error) { + return &chronograf.Organization{ + ID: "1337", + Name: "pineapple_kingdom", + }, nil + }, + }, + Logger: log.New(log.DebugLevel), + }, + ID: "6", + requestBody: func(url string) string { + return fmt.Sprintf(`{"name":"v3-cloud-dedicated","type":"influx-v3-cloud-dedicated","url":"%s","databaseToken":"old-cloud-token","managementToken":"","clusterId":"","accountId":"","defaultDB":"my-db","telegraf":"my-db"}`, url) + }, + mockServerHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // v3 Cloud Dedicated sources validate by accessing the management API + if strings.Contains(r.URL.Path, "/api/v0/accounts/") || r.URL.Path == "/api/v3/query_influxql" { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"databases":[]}`)) + } else { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{}`)) + } + }), + wantStatusCode: 200, + wantContentType: "application/json", + wantBody: func(url string) string { + return fmt.Sprintf(`{"id":"6","name":"v3-cloud-dedicated","type":"influx-v3-cloud-dedicated","databaseToken":"old-cloud-token","url":"%s","default":false,"telegraf":"my-db","organization":"1337","defaultRP":"","defaultDB":"my-db","authentication":"unknown","links":{"self":"/chronograf/v1/sources/6","kapacitors":"/chronograf/v1/sources/6/kapacitors","services":"/chronograf/v1/sources/6/services","proxy":"/chronograf/v1/sources/6/proxy","queries":"/chronograf/v1/sources/6/queries","write":"/chronograf/v1/sources/6/write","permissions":"/chronograf/v1/sources/6/permissions","users":"/chronograf/v1/sources/6/users","databases":"/chronograf/v1/sources/6/dbs","annotations":"/chronograf/v1/sources/6/annotations","health":"/chronograf/v1/sources/6/health"}} `, url) }, }, @@ -1042,7 +1099,7 @@ func TestService_UpdateSource(t *testing.T) { SourcesStore: &mocks.SourcesStore{ GetF: func(ctx context.Context, ID int) (chronograf.Source, error) { return chronograf.Source{ - ID: 6, + ID: 7, Type: chronograf.InfluxDBv3Serverless, DatabaseToken: "old-serverless-token", }, nil @@ -1061,7 +1118,7 @@ func TestService_UpdateSource(t *testing.T) { }, Logger: log.New(log.DebugLevel), }, - ID: "6", + ID: "7", requestBody: func(url string) string { return fmt.Sprintf(`{"name":"v3-serverless","type":"influx-v3-serverless","url":"%s","databaseToken":"serverless-token-xyz","defaultDB":"serverless_db","telegraf":"telegraf"}`, url) }, @@ -1078,66 +1135,69 @@ func TestService_UpdateSource(t *testing.T) { wantStatusCode: 200, wantContentType: "application/json", wantBody: func(url string) string { - return fmt.Sprintf(`{"id":"6","name":"v3-serverless","type":"influx-v3-serverless","databaseToken":"serverless-token-xyz","url":"%s","default":false,"telegraf":"telegraf","organization":"1337","defaultRP":"","defaultDB":"serverless_db","authentication":"token","links":{"self":"/chronograf/v1/sources/6","kapacitors":"/chronograf/v1/sources/6/kapacitors","services":"/chronograf/v1/sources/6/services","proxy":"/chronograf/v1/sources/6/proxy","queries":"/chronograf/v1/sources/6/queries","write":"/chronograf/v1/sources/6/write","permissions":"/chronograf/v1/sources/6/permissions","users":"/chronograf/v1/sources/6/users","databases":"/chronograf/v1/sources/6/dbs","annotations":"/chronograf/v1/sources/6/annotations","health":"/chronograf/v1/sources/6/health"}} + return fmt.Sprintf(`{"id":"7","name":"v3-serverless","type":"influx-v3-serverless","databaseToken":"serverless-token-xyz","url":"%s","default":false,"telegraf":"telegraf","organization":"1337","defaultRP":"","defaultDB":"serverless_db","authentication":"token","links":{"self":"/chronograf/v1/sources/7","kapacitors":"/chronograf/v1/sources/7/kapacitors","services":"/chronograf/v1/sources/7/services","proxy":"/chronograf/v1/sources/7/proxy","queries":"/chronograf/v1/sources/7/queries","write":"/chronograf/v1/sources/7/write","permissions":"/chronograf/v1/sources/7/permissions","users":"/chronograf/v1/sources/7/users","databases":"/chronograf/v1/sources/7/dbs","annotations":"/chronograf/v1/sources/7/annotations","health":"/chronograf/v1/sources/7/health"}} `, url) }, }, } for _, tt := range tests { - mockHandler := tt.mockServerHandler - if mockHandler == nil { - mockHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/query" { - w.WriteHeader(http.StatusOK) // credentials - } else { - w.WriteHeader(http.StatusNoContent) - w.Header().Set("X-Influxdb-Build", "ENT") - } - w.Write(([]byte)("{}")) - }) - } - ts := httptest.NewServer(mockHandler) - defer ts.Close() - - h := &Service{ - Store: &mocks.Store{ - SourcesStore: tt.fields.SourcesStore, - OrganizationsStore: tt.fields.OrganizationsStore, - }, - Logger: tt.fields.Logger, - V3Config: chronograf.V3Config{ - CloudDedicatedManagementURL: ts.URL, - }, - } + t.Run(tt.name, func(t *testing.T) { + mockHandler := tt.mockServerHandler + if mockHandler == nil { + mockHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/query" { + w.WriteHeader(http.StatusOK) // credentials + } else { + w.WriteHeader(http.StatusNoContent) + w.Header().Set("X-Influxdb-Build", "ENT") + } + w.Write(([]byte)("{}")) + }) + } + ts := httptest.NewServer(mockHandler) + defer ts.Close() - tt.args.r = tt.args.r.WithContext(httprouter.WithParams( - context.Background(), - httprouter.Params{ - { - Key: "id", - Value: tt.ID, + h := &Service{ + Store: &mocks.Store{ + SourcesStore: tt.fields.SourcesStore, + OrganizationsStore: tt.fields.OrganizationsStore, }, - })) - tt.args.r.Body = ioutil.NopCloser( - bytes.NewReader([]byte(tt.requestBody(ts.URL))), - ) - h.UpdateSource(tt.args.w, tt.args.r) + Logger: tt.fields.Logger, + V3Config: chronograf.V3Config{ + CloudDedicatedManagementURL: ts.URL, + }, + } - resp := tt.args.w.Result() - contentType := resp.Header.Get("Content-Type") - body, _ := ioutil.ReadAll(resp.Body) + tt.args.r = tt.args.r.WithContext(httprouter.WithParams( + context.Background(), + httprouter.Params{ + { + Key: "id", + Value: tt.ID, + }, + })) + tt.args.r.Body = ioutil.NopCloser( + bytes.NewReader([]byte(tt.requestBody(ts.URL))), + ) + h.UpdateSource(tt.args.w, tt.args.r) - if resp.StatusCode != tt.wantStatusCode { - t.Errorf("%q. UpdateSource() = got %v, want %v", tt.name, resp.StatusCode, tt.wantStatusCode) - } - if contentType != tt.wantContentType { - t.Errorf("%q. UpdateSource() = got %v, want %v", tt.name, contentType, tt.wantContentType) - } - wantBody := tt.wantBody(ts.URL) - if string(body) != wantBody { - t.Errorf("%q. UpdateSource() =\ngot ***%v***\nwant ***%v***\n", tt.name, string(body), wantBody) - } + resp := tt.args.w.Result() + contentType := resp.Header.Get("Content-Type") + body, _ := ioutil.ReadAll(resp.Body) + + if resp.StatusCode != tt.wantStatusCode { + t.Errorf("%q. UpdateSource() = got %v, want %v", tt.name, resp.StatusCode, tt.wantStatusCode) + } + if contentType != tt.wantContentType { + t.Errorf("%q. UpdateSource() = got %v, want %v", tt.name, contentType, tt.wantContentType) + } + wantBody := tt.wantBody(ts.URL) + if string(body) != wantBody { + t.Errorf("%q. UpdateSource() =\ngot ***%v***\nwant ***%v***\n", tt.name, string(body), wantBody) + } + }) } + } func TestService_NewSourceUser(t *testing.T) { From f1d8c6b881e2ffe805362ef39993e6d5c57716a2 Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Tue, 9 Dec 2025 13:34:32 +0100 Subject: [PATCH 44/50] fix: help, meta field --- cmd/chronograf/main.go | 10 +++++----- ui/src/sources/components/SourceStep.tsx | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/chronograf/main.go b/cmd/chronograf/main.go index e9e940c1ce..06a3caebda 100644 --- a/cmd/chronograf/main.go +++ b/cmd/chronograf/main.go @@ -43,12 +43,12 @@ func main() { if _, err := parser.Parse(); err != nil { code := 1 - if fe, ok := err.(*flags.Error); ok { - if fe.Type == flags.ErrHelp { - code = 0 - } + if fe, ok := err.(*flags.Error); ok && fe.Type == flags.ErrHelp { + code = 0 + } + if code != 0 { + fmt.Printf("Error: %s\n", err) } - fmt.Printf("Error: %s\n", err) os.Exit(code) } diff --git a/ui/src/sources/components/SourceStep.tsx b/ui/src/sources/components/SourceStep.tsx index 783b10387d..6844c6d4a9 100644 --- a/ui/src/sources/components/SourceStep.tsx +++ b/ui/src/sources/components/SourceStep.tsx @@ -444,7 +444,7 @@ class SourceStep extends PureComponent { private get isEnterprise(): boolean { const {source} = this.state - return _.get(source, 'type', '').includes('enterprise') + return source.type === SOURCE_TYPE_INFLUX_V1_ENTERPRISE } private getServerTypeFromSource = (source: Partial): string => { From f8950ccb0645d3fdbc8f94d106046ecc1e12d646 Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Fri, 12 Dec 2025 10:57:43 +0100 Subject: [PATCH 45/50] fix: fixing annotations management --- influx/influx_v3.go | 4 ++++ .../components/AnnotationFilterControlInput.tsx | 11 +++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/influx/influx_v3.go b/influx/influx_v3.go index 6e002e1296..1b07d869f4 100644 --- a/influx/influx_v3.go +++ b/influx/influx_v3.go @@ -101,6 +101,10 @@ func (c *Client) queryV3(u *url.URL, q chronograf.Query) (chronograf.Response, e params := req.URL.Query() params.Set("q", cmd) params.Set("db", q.DB) + params.Set("epoch", "ms") + if q.Epoch != "" { + params.Set("epoch", q.Epoch) + } req.URL.RawQuery = params.Encode() // Authorization diff --git a/ui/src/shared/components/AnnotationFilterControlInput.tsx b/ui/src/shared/components/AnnotationFilterControlInput.tsx index 5c1c24f8e1..cde8c22b0b 100644 --- a/ui/src/shared/components/AnnotationFilterControlInput.tsx +++ b/ui/src/shared/components/AnnotationFilterControlInput.tsx @@ -16,7 +16,12 @@ interface State { shouldShowAllSuggestions: boolean } -const lexographicOrder = (a: string, b: string) => a.localeCompare(b) +const lexographicOrder = (a: string, b: string) => { + if (a) { + return a.localeCompare(b) + } + return 0 +} class AnnotationFilterControlInput extends PureComponent { public static getDerivedStateFromProps(props: Props, state: State) { @@ -29,7 +34,9 @@ class AnnotationFilterControlInput extends PureComponent { return {filteredSuggestions} } - filteredSuggestions = filteredSuggestions.filter(v => v.includes(value)) + filteredSuggestions = filteredSuggestions.filter( + v => v && v.includes(value) + ) return {filteredSuggestions} } From 97541af229089343b1243d64e39d01cbe8673edd Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Mon, 15 Dec 2025 11:25:23 +0100 Subject: [PATCH 46/50] fix: use DefaultDB if provided --- chronograf.go | 6 ------ influx/cloud_dedicated.go | 7 +++++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/chronograf.go b/chronograf.go index 67449d3fd0..b54b9a3077 100644 --- a/chronograf.go +++ b/chronograf.go @@ -27,7 +27,6 @@ const ( ErrSourceInvalid = Error("source is invalid") ErrServerInvalid = Error("server is invalid") ErrAlertNotFound = Error("alert not found") - ErrAuthentication = Error("user not authenticated") ErrUninitialized = Error("client uninitialized. Call Open() method") ErrInvalidAxis = Error("Unexpected axis in cell. Valid axes are 'x', 'y', and 'y2'") ErrInvalidColorType = Error("Invalid color type. Valid color types are 'min', 'max', 'threshold', 'text', and 'background'") @@ -42,9 +41,6 @@ const ( ErrCannotDeleteDefaultOrganization = Error("cannot delete default organization") ErrConfigNotFound = Error("cannot find configuration") ErrAnnotationNotFound = Error("annotation not found") - ErrInvalidCellOptionsText = Error("invalid text wrapping option. Valid wrappings are 'truncate', 'wrap', and 'single line'") - ErrInvalidCellOptionsSort = Error("cell options sortby cannot be empty'") - ErrInvalidCellOptionsColumns = Error("cell options columns cannot be empty'") ErrOrganizationConfigNotFound = Error("could not find organization config") ErrInvalidCellQueryType = Error("invalid cell query type: must be 'flux' or 'influxql'") ) @@ -261,8 +257,6 @@ type Response interface { MarshalJSON() ([]byte, error) } -//TODO use password instead of databaseToken - // Source is connection information to a time-series data store. type Source struct { ID int `json:"id,string"` // ID is the unique ID of the source diff --git a/influx/cloud_dedicated.go b/influx/cloud_dedicated.go index b0bd745281..651c138e3a 100644 --- a/influx/cloud_dedicated.go +++ b/influx/cloud_dedicated.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net/http" "strings" @@ -75,7 +76,7 @@ func (c *Client) validateClusteredOrCloudDedicatedAuth(ctx context.Context) erro // Used for InfluxDB Clustered and InfluxDB Cloud Dedicated. func (c *Client) showDatabasesViaMgmtApi(ctx context.Context) (chronograf.Response, error) { var dbNames []string - if c.MgmtURL == nil { + if c.DefaultDB != "" { dbNames = []string{c.DefaultDB} } else { // Prepare request. @@ -94,7 +95,9 @@ func (c *Client) showDatabasesViaMgmtApi(ctx context.Context) (chronograf.Respon } return nil, err } - defer resp.Body.Close() + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) // Handle non-OK status. if resp.StatusCode != http.StatusOK { From 21d1e94121b3185f93799e15a507caa31d5e4e4d Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Tue, 16 Dec 2025 10:09:47 +0100 Subject: [PATCH 47/50] fix: use DefaultDB also in Clustered --- influx/influx.go | 28 ++++----- influx/influx_test.go | 74 +++++++++++++++++++++++- server/sources.go | 7 +-- server/sources_test.go | 33 ++++++++++- ui/src/sources/components/SourceStep.tsx | 3 +- 5 files changed, 123 insertions(+), 22 deletions(-) diff --git a/influx/influx.go b/influx/influx.go index 0f5ee60fc3..9fbc53f88d 100644 --- a/influx/influx.go +++ b/influx/influx.go @@ -292,20 +292,22 @@ func (c *Client) Connect(ctx context.Context, src *chronograf.Source) error { if src.Type == chronograf.InfluxDBv3Clustered { // InfluxDB Clustered also provides a management API. - accountID := c.V3Config.ClusteredAccountID - clusterID := c.V3Config.ClusteredClusterID - baseURL := *c.URL - baseURL.Path = "" - baseURL.RawQuery = "" - mgmtUrl := fmt.Sprintf("%s/api/v0/accounts/%s/clusters/%s", baseURL.String(), accountID, clusterID) - if u, err = url.Parse(mgmtUrl); err != nil { - return err - } - - c.MgmtURL = u - c.MgmtAuthorizer = &BearerToken{ - Token: src.ManagementToken, + if len(src.ManagementToken) > 0 { + accountID := c.V3Config.ClusteredAccountID + clusterID := c.V3Config.ClusteredClusterID + baseURL := *c.URL + baseURL.Path = "" + baseURL.RawQuery = "" + mgmtUrl := fmt.Sprintf("%s/api/v0/accounts/%s/clusters/%s", baseURL.String(), accountID, clusterID) + if u, err = url.Parse(mgmtUrl); err != nil { + return err + } + c.MgmtURL = u + c.MgmtAuthorizer = &BearerToken{ + Token: src.ManagementToken, + } } + c.DefaultDB = src.DefaultDB } if src.Type == chronograf.InfluxDBv3CloudDedicated { diff --git a/influx/influx_test.go b/influx/influx_test.go index bc69227a33..d8b1b13e2f 100644 --- a/influx/influx_test.go +++ b/influx/influx_test.go @@ -883,7 +883,10 @@ func Test_Influx_ValidateAuth_V3Clustered(t *testing.T) { ManagementToken: "my-mgmt-token", } - client.Connect(context.Background(), source) + err = client.Connect(context.Background(), source) + if err != nil { + t.Fatal("Unexpected error connecting client: err:", err) + } err = client.ValidateAuth(context.Background(), source) if err == nil { t.Fatal("Expected error but nil") @@ -894,6 +897,35 @@ func Test_Influx_ValidateAuth_V3Clustered(t *testing.T) { if !mgmtAuthCalled { t.Error("Expected management API to be called") } + + mgmtAuthCalled = false + client, err = NewClient(ts.URL, log.New(log.DebugLevel)) + if err != nil { + t.Fatal("Unexpected error connecting client: err:", err) + } + source = &chronograf.Source{ + URL: ts.URL, + Type: chronograf.InfluxDBv3Clustered, + DatabaseToken: "my-db-token", + DefaultDB: "defaultdb", + } + + err = client.Connect(context.Background(), source) + if err != nil { + t.Fatal("Unexpected error connecting client: err:", err) + } + if client.DefaultDB != "defaultdb" { + t.Errorf("Expected default DB to be 'defaultdb' but was: %v", client.DefaultDB) + } + + err = client.ValidateAuth(context.Background(), source) + if err == nil { + t.Fatal("Expected error but nil") + } + + if mgmtAuthCalled { + t.Error("Expected management API called") + } } func Test_Influx_ValidateAuth_V3CloudDedicated(t *testing.T) { @@ -923,6 +955,9 @@ func Test_Influx_ValidateAuth_V3CloudDedicated(t *testing.T) { defer ts.Close() client, err := NewClient(ts.URL, log.New(log.DebugLevel)) + if err != nil { + t.Fatal("Unexpected error connecting client: err:", err) + } client.V3Config = chronograf.V3Config{ CloudDedicatedManagementURL: ts.URL, ClusteredAccountID: "test-account-id", @@ -940,7 +975,10 @@ func Test_Influx_ValidateAuth_V3CloudDedicated(t *testing.T) { ClusterID: "test-cluster-id", } - client.Connect(context.Background(), source) + err = client.Connect(context.Background(), source) + if err != nil { + t.Fatal("Unexpected error connecting client: err:", err) + } err = client.ValidateAuth(context.Background(), source) if err == nil { t.Fatal("Expected error but nil") @@ -952,6 +990,38 @@ func Test_Influx_ValidateAuth_V3CloudDedicated(t *testing.T) { if !mgmtAuthCalled { t.Error("Expected management API to be called") } + + mgmtAuthCalled = false + client, err = NewClient(ts.URL, log.New(log.DebugLevel)) + if err != nil { + t.Fatal("Unexpected error connecting client: err:", err) + } + client.V3Config = chronograf.V3Config{ + CloudDedicatedManagementURL: ts.URL, + } + source = &chronograf.Source{ + URL: ts.URL, + Type: chronograf.InfluxDBv3CloudDedicated, + DatabaseToken: "my-db-token", + DefaultDB: "defaultdb", + } + + err = client.Connect(context.Background(), source) + if err != nil { + t.Fatal("Unexpected error connecting client: err:", err) + } + if client.DefaultDB != "defaultdb" { + t.Errorf("Expected default DB to be 'defaultdb' but was: %v", client.DefaultDB) + } + + err = client.ValidateAuth(context.Background(), source) + if err == nil { + t.Fatal("Expected error but nil") + } + + if mgmtAuthCalled { + t.Error("Expected management API called") + } } func Test_Influx_Authorization_V3Core(t *testing.T) { diff --git a/server/sources.go b/server/sources.go index d442e3b885..4e8dea64ab 100644 --- a/server/sources.go +++ b/server/sources.go @@ -547,13 +547,12 @@ func ValidSourceRequest(s *chronograf.Source, defaultOrgID string) error { } if s.Type == chronograf.InfluxDBv3Clustered { - if len(s.ManagementToken) == 0 { - return fmt.Errorf("management token required") - } if len(s.DatabaseToken) == 0 { return fmt.Errorf("database token required") } - // TODO simon: make management token optional, similarly to Cloud Dedicated + if len(s.ManagementToken) == 0 && len(s.DefaultDB) == 0 { + return fmt.Errorf("management token or default database is required") + } } if s.Type == chronograf.InfluxDBv3CloudDedicated { diff --git a/server/sources_test.go b/server/sources_test.go index 39ab96a8a2..850f84362f 100644 --- a/server/sources_test.go +++ b/server/sources_test.go @@ -522,7 +522,7 @@ func Test_ValidSourceRequest(t *testing.T) { }, }, { - name: "InfluxDB 3 Clustered - missing management token", + name: "InfluxDB 3 Clustered - default DB without management token", args: args{ source: &chronograf.Source{ ID: 1, @@ -532,11 +532,40 @@ func Test_ValidSourceRequest(t *testing.T) { URL: "http://www.any.url.com", InsecureSkipVerify: true, Default: true, + DefaultDB: "defaultDB", Telegraf: "telegraf", }, }, wants: wants{ - err: fmt.Errorf("management token required"), + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBv3Clustered, + DatabaseToken: "database-token", + URL: "http://www.any.url.com", + InsecureSkipVerify: true, + Default: true, + DefaultDB: "defaultDB", + Telegraf: "telegraf", + }, + }, + }, + { + name: "InfluxDB 3 Clustered - missing management token and default db", + args: args{ + source: &chronograf.Source{ + ID: 1, + Name: "I'm a really great source", + Type: chronograf.InfluxDBv3Clustered, + DatabaseToken: "database-token", + URL: "http://www.any.url.com", + InsecureSkipVerify: true, + Default: true, + Telegraf: "telegraf", + }, + }, + wants: wants{ + err: fmt.Errorf("management token or default database is required"), }, }, { diff --git a/ui/src/sources/components/SourceStep.tsx b/ui/src/sources/components/SourceStep.tsx index 6844c6d4a9..d8f725ae81 100644 --- a/ui/src/sources/components/SourceStep.tsx +++ b/ui/src/sources/components/SourceStep.tsx @@ -277,7 +277,8 @@ class SourceStep extends PureComponent { onChange={this.onChangeInput('defaultRP')} /> )} - {serverType === SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED && ( + {(serverType === SOURCE_TYPE_INFLUX_V3_CLOUD_DEDICATED || + serverType === SOURCE_TYPE_INFLUX_V3_CLUSTERED) && ( Date: Tue, 16 Dec 2025 10:12:22 +0100 Subject: [PATCH 48/50] Feat: Configurable Time condition for show tag values --- chronograf.go | 2 ++ influx/cloud_dedicated.go | 25 ++++++----------- influx/cloud_dedicated_test.go | 49 +++++++++++++++++++--------------- influx/influx_v3.go | 2 +- server/server.go | 21 +++++++++++++++ 5 files changed, 60 insertions(+), 39 deletions(-) diff --git a/chronograf.go b/chronograf.go index b54b9a3077..64ae53446d 100644 --- a/chronograf.go +++ b/chronograf.go @@ -9,6 +9,7 @@ import ( "strconv" "time" + "github.com/influxdata/influxdb/influxql" "github.com/influxdata/kapacitor/client/v1" ) @@ -121,6 +122,7 @@ type V3Config struct { CloudDedicatedManagementURL string ClusteredAccountID string ClusteredClusterID string + TimeConditionExpr influxql.Expr // Parsed time condition for SHOW TAG VALUES queries } // TSDBStatus represents the current status of a time series database diff --git a/influx/cloud_dedicated.go b/influx/cloud_dedicated.go index 651c138e3a..5e63f0b979 100644 --- a/influx/cloud_dedicated.go +++ b/influx/cloud_dedicated.go @@ -35,20 +35,6 @@ type cdListDatabasesError struct { Message string `json:"message,omitempty"` } -// TODO simon: make this expression configurable via environment variable -// const timeCondition = "time > now() - 1d" -const timeCondition = "time > 0" - -var timeExpr = mustParseExpr(timeCondition) - -func mustParseExpr(expr string) influxql.Expr { - exp, err := influxql.ParseExpr(expr) - if err != nil { - panic(fmt.Sprintf("failed to parse expression %q: %v", expr, err)) - } - return exp -} - // validateClusteredOrCloudDedicatedAuth checks both the management endpoint and the database endpoint to validate authentication. // Used for InfluxDB Clustered and InfluxDB Cloud Dedicated. func (c *Client) validateClusteredOrCloudDedicatedAuth(ctx context.Context) error { @@ -217,7 +203,7 @@ func (c *Client) handleShowTagValues(q *chronograf.Query, logs chronograf.Logger } else { // Call InfluxDB logs.Info("Returning tag values from InfluxDB with time condition applied") - appendTimeCondition(stmt) + appendTimeCondition(stmt, c.V3Config.TimeConditionExpr) q.Command = stmt.String() } return nil, nil @@ -249,9 +235,14 @@ func parseShowTagKeysStatement(query string) (*influxql.ShowTagKeysStatement, er return showStmt, nil } -// appendTimeCondition appends a default "WHERE time > now() - 1d" clause to the provided SHOW TAG VALUES statement if no time condition exists. +// appendTimeCondition appends a time condition clause to the provided SHOW TAG VALUES statement if no time condition exists. // Returns true if the statement was modified. -func appendTimeCondition(showStmt *influxql.ShowTagValuesStatement) bool { +func appendTimeCondition(showStmt *influxql.ShowTagValuesStatement, timeExpr influxql.Expr) bool { + // If no time expression provided, do nothing + if timeExpr == nil { + return false + } + // Check if there's already a time condition in the WHERE clause if showStmt.Condition != nil && hasTimeCondition(showStmt.Condition) { // Already has a time condition, do nothing diff --git a/influx/cloud_dedicated_test.go b/influx/cloud_dedicated_test.go index ac4347a65a..37eac484d1 100644 --- a/influx/cloud_dedicated_test.go +++ b/influx/cloud_dedicated_test.go @@ -14,9 +14,16 @@ import ( "github.com/influxdata/chronograf" "github.com/influxdata/chronograf/log" + "github.com/influxdata/influxdb/influxql" ) func TestAppendTimeCondition(t *testing.T) { + // Define test time condition + testTimeCondition := "time > 0" + testTimeExpr, err := influxql.ParseExpr(testTimeCondition) + if err != nil { + t.Fatalf("Failed to parse test time condition: %v", err) + } tests := []struct { name string @@ -26,77 +33,77 @@ func TestAppendTimeCondition(t *testing.T) { { name: "basic query without WHERE", input: `SHOW TAG VALUES FROM machine_data WITH KEY = "tagkey"`, - expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE ` + timeCondition, + expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE ` + testTimeCondition, }, { name: "query without FROM clause", input: `SHOW TAG VALUES WITH KEY = "tagkey"`, - expected: `SHOW TAG VALUES WITH KEY = tagkey WHERE ` + timeCondition, + expected: `SHOW TAG VALUES WITH KEY = tagkey WHERE ` + testTimeCondition, }, { name: "query with tag name with where in it", input: `SHOW TAG VALUES WITH KEY = "tag name with where in it"`, - expected: `SHOW TAG VALUES WITH KEY = "tag name with where in it" WHERE ` + timeCondition, + expected: `SHOW TAG VALUES WITH KEY = "tag name with where in it" WHERE ` + testTimeCondition, }, { name: "query with quoted table name and retention policy", input: `SHOW TAG VALUES FROM "autogen"."machine_data" WITH KEY IN ("t1", "t2")`, - expected: `SHOW TAG VALUES FROM autogen.machine_data WITH KEY IN (t1, t2) WHERE ` + timeCondition, + expected: `SHOW TAG VALUES FROM autogen.machine_data WITH KEY IN (t1, t2) WHERE ` + testTimeCondition, }, { name: "query with trailing semicolon", input: `SHOW TAG VALUES FROM machine_data WITH KEY = "tagkey";`, - expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE ` + timeCondition, + expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE ` + testTimeCondition, }, { name: "query with existing WHERE clause", - input: `SHOW TAG VALUES FROM machine_data WITH KEY = "tagkey" WHERE ` + timeCondition, - expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE ` + timeCondition, + input: `SHOW TAG VALUES FROM machine_data WITH KEY = "tagkey" WHERE ` + testTimeCondition, + expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE ` + testTimeCondition, }, { name: "query with existing WHERE clause (case insensitive)", - input: `SHOW TAG VALUES FROM machine_data WITH KEY = "tagkey" where ` + timeCondition, - expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE ` + timeCondition, + input: `SHOW TAG VALUES FROM machine_data WITH KEY = "tagkey" where ` + testTimeCondition, + expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE ` + testTimeCondition, }, { name: "query with LIMIT clause", input: `SHOW TAG VALUES FROM machine_data WITH KEY = "tagkey" LIMIT 10`, - expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE ` + timeCondition + ` LIMIT 10`, + expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE ` + testTimeCondition + ` LIMIT 10`, }, { name: "query with OFFSET clause", input: `SHOW TAG VALUES FROM machine_data WITH KEY = "tagkey" OFFSET 5`, - expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE ` + timeCondition + ` OFFSET 5`, + expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE ` + testTimeCondition + ` OFFSET 5`, }, { name: "query with LIMIT and OFFSET", input: `SHOW TAG VALUES FROM machine_data WITH KEY = "tagkey" LIMIT 10 OFFSET 5`, - expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE ` + timeCondition + ` LIMIT 10 OFFSET 5`, + expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE ` + testTimeCondition + ` LIMIT 10 OFFSET 5`, }, { name: "query with OFFSET and LIMIT (reverse order)", input: `SHOW TAG VALUES FROM machine_data WITH KEY = "tagkey" OFFSET 5 LIMIT 10`, - expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE ` + timeCondition + ` OFFSET 5`, + expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE ` + testTimeCondition + ` OFFSET 5`, }, { name: "query with LIMIT and semicolon", input: `SHOW TAG VALUES FROM machine_data WITH KEY = "tagkey" LIMIT 10;`, - expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE ` + timeCondition + ` LIMIT 10`, + expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE ` + testTimeCondition + ` LIMIT 10`, }, { name: "query with WHERE and LIMIT", - input: `SHOW TAG VALUES FROM machine_data WITH KEY = "tagkey" WHERE ` + timeCondition + ` LIMIT 10`, - expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE ` + timeCondition + ` LIMIT 10`, + input: `SHOW TAG VALUES FROM machine_data WITH KEY = "tagkey" WHERE ` + testTimeCondition + ` LIMIT 10`, + expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE ` + testTimeCondition + ` LIMIT 10`, }, { name: "query with WHERE and semicolon", - input: `SHOW TAG VALUES FROM machine_data WITH KEY = "tagkey" WHERE ` + timeCondition + `;`, - expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE ` + timeCondition, + input: `SHOW TAG VALUES FROM machine_data WITH KEY = "tagkey" WHERE ` + testTimeCondition + `;`, + expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE ` + testTimeCondition, }, { name: "query with existing WHERE clause but no time condition", input: `SHOW TAG VALUES FROM machine_data WITH KEY = "tagkey" WHERE "host" = 'server1'`, - expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE host = 'server1' AND ` + timeCondition, + expected: `SHOW TAG VALUES FROM machine_data WITH KEY = tagkey WHERE host = 'server1' AND ` + testTimeCondition, }, { name: "query with complex WHERE clause containing time", @@ -121,7 +128,7 @@ func TestAppendTimeCondition(t *testing.T) { { name: "query with tag name containing spaces", input: `SHOW TAG VALUES FROM machine_data WITH KEY = "tag with spaces"`, - expected: `SHOW TAG VALUES FROM machine_data WITH KEY = "tag with spaces" WHERE ` + timeCondition, + expected: `SHOW TAG VALUES FROM machine_data WITH KEY = "tag with spaces" WHERE ` + testTimeCondition, }, } @@ -136,7 +143,7 @@ func TestAppendTimeCondition(t *testing.T) { } // Apply appendTimeCondition to the parsed statement - appendTimeCondition(showStmt) + appendTimeCondition(showStmt, testTimeExpr) // Convert back to string and compare result := showStmt.String() diff --git a/influx/influx_v3.go b/influx/influx_v3.go index 1b07d869f4..9c6d8e09cb 100644 --- a/influx/influx_v3.go +++ b/influx/influx_v3.go @@ -85,7 +85,7 @@ func (c *Client) queryV3(u *url.URL, q chronograf.Query) (chronograf.Response, e switch s := stmt.(type) { case *influxql.ShowTagValuesStatement: // Ensure time condition is added to `SHOW TAG VALUES` queries - if appendTimeCondition(s) { + if appendTimeCondition(s, c.V3Config.TimeConditionExpr) { cmd = stmt.String() logs.WithField("command", cmd).Debug("time condition added to SHOW TAG VALUES query") } diff --git a/server/server.go b/server/server.go index 3a21287b10..aa32a2581b 100644 --- a/server/server.go +++ b/server/server.go @@ -34,6 +34,7 @@ import ( "github.com/influxdata/chronograf/oauth2" "github.com/influxdata/chronograf/server/config" "github.com/influxdata/chronograf/util" + "github.com/influxdata/influxdb/influxql" client "github.com/influxdata/usage-client/v1" flags "github.com/jessevdk/go-flags" ) @@ -74,6 +75,7 @@ type Server struct { InfluxDBClusteredClusterID string `long:"influxdb-clustered-cluster-id" description:"Cluster ID for your InfluxDB v3 Clustered instance" env:"INFLUXDB_CLUSTERED_CLUSTER_ID" default:"11111111-1111-1111-1111-111111111111"` InfluxDBClusteredAccountID string `long:"influxdb-clustered-account-id" description:"Account ID for your InfluxDB v3 Clustered instance" env:"INFLUXDB_CLUSTERED_ACCOUNT_ID" default:"11111111-1111-1111-1111-111111111111"` InfluxDBV3SupportEnabled bool `long:"influxdb-v3-support-enabled" description:"Enable InfluxDB v3 support" env:"INFLUXDB_V3_SUPPORT_ENABLED"` + InfluxDBV3TimeCondition string `long:"influxdb-v3-time-condition" description:"Time condition for SHOW TAG VALUES queries in InfluxDB v3 (e.g., 'time > now() - 1d')" env:"INFLUXDB_V3_TIME_CONDITION" default:"time > now() - 7d"` KapacitorURL string `long:"kapacitor-url" description:"Location of your Kapacitor instance" env:"KAPACITOR_URL"` KapacitorUsername string `long:"kapacitor-username" description:"Username of your Kapacitor instance" env:"KAPACITOR_USERNAME"` @@ -707,11 +709,30 @@ func (s *Server) Serve(ctx context.Context) { } } + // Parse and validate v3 time condition at startup + var v3TimeConditionExpr influxql.Expr + if s.InfluxDBV3TimeCondition != "" { + expr, err := influxql.ParseExpr(s.InfluxDBV3TimeCondition) + if err != nil { + logger. + WithField("component", "server"). + WithField("time_condition", s.InfluxDBV3TimeCondition). + Error(fmt.Errorf("invalid InfluxDB v3 time condition: %w", err)) + os.Exit(1) + } + v3TimeConditionExpr = expr + logger. + WithField("component", "server"). + WithField("time_condition", s.InfluxDBV3TimeCondition). + Info("InfluxDB v3 time condition validated and configured") + } + service := openService(ctx, db, s.newBuilders(logger), logger, s.useAuth(), chronograf.V3Config{ CloudDedicatedManagementURL: s.InfluxDBCloudDedicatedMgmtURL, ClusteredAccountID: s.InfluxDBClusteredAccountID, ClusteredClusterID: s.InfluxDBClusteredClusterID, + TimeConditionExpr: v3TimeConditionExpr, }) service.SuperAdminProviderGroups = superAdminProviderGroups{ auth0: s.Auth0SuperAdminOrg, From ea4cbd61a94a9ee7ce0bd53b71fbd46573c37417 Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Wed, 17 Dec 2025 22:01:06 +0100 Subject: [PATCH 49/50] chore: change default time condition to last day --- server/server.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/server.go b/server/server.go index aa32a2581b..87ca77f55e 100644 --- a/server/server.go +++ b/server/server.go @@ -75,7 +75,7 @@ type Server struct { InfluxDBClusteredClusterID string `long:"influxdb-clustered-cluster-id" description:"Cluster ID for your InfluxDB v3 Clustered instance" env:"INFLUXDB_CLUSTERED_CLUSTER_ID" default:"11111111-1111-1111-1111-111111111111"` InfluxDBClusteredAccountID string `long:"influxdb-clustered-account-id" description:"Account ID for your InfluxDB v3 Clustered instance" env:"INFLUXDB_CLUSTERED_ACCOUNT_ID" default:"11111111-1111-1111-1111-111111111111"` InfluxDBV3SupportEnabled bool `long:"influxdb-v3-support-enabled" description:"Enable InfluxDB v3 support" env:"INFLUXDB_V3_SUPPORT_ENABLED"` - InfluxDBV3TimeCondition string `long:"influxdb-v3-time-condition" description:"Time condition for SHOW TAG VALUES queries in InfluxDB v3 (e.g., 'time > now() - 1d')" env:"INFLUXDB_V3_TIME_CONDITION" default:"time > now() - 7d"` + InfluxDBV3TimeCondition string `long:"influxdb-v3-time-condition" description:"Time condition for SHOW TAG VALUES queries in InfluxDB v3 (e.g., 'time > now() - 1d')" env:"INFLUXDB_V3_TIME_CONDITION" default:"time > now() - 1d"` KapacitorURL string `long:"kapacitor-url" description:"Location of your Kapacitor instance" env:"KAPACITOR_URL"` KapacitorUsername string `long:"kapacitor-username" description:"Username of your Kapacitor instance" env:"KAPACITOR_USERNAME"` @@ -718,7 +718,7 @@ func (s *Server) Serve(ctx context.Context) { WithField("component", "server"). WithField("time_condition", s.InfluxDBV3TimeCondition). Error(fmt.Errorf("invalid InfluxDB v3 time condition: %w", err)) - os.Exit(1) + return } v3TimeConditionExpr = expr logger. From e9f520913c5ed61d2b6708480cd1f5341271a46d Mon Sep 17 00:00:00 2001 From: vlastahajek <29980246+vlastahajek@users.noreply.github.com> Date: Thu, 18 Dec 2025 09:37:58 +0100 Subject: [PATCH 50/50] docs: change log --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6283d57ed..407ec971ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## [unreleased] +### Features + +1. [#6139](https://github.com/influxdata/chronograf/pull/6139): Add support for InfluxDB V3 + ### Other 1. [#6150](https://github.com/influxdata/chronograf/pull/6150): Upgrade golang to 1.25.3