chore: delete the rest of chronograf (#21998)
* chore: deleted the rest of chronograf * chore: tidypull/22023/head
parent
3c1d841df6
commit
3f7b996a10
|
@ -1,827 +0,0 @@
|
||||||
package chronograf
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// General errors.
|
|
||||||
const (
|
|
||||||
ErrUpstreamTimeout = Error("request to backend timed out")
|
|
||||||
ErrSourceNotFound = Error("source not found")
|
|
||||||
ErrServerNotFound = Error("server not found")
|
|
||||||
ErrLayoutNotFound = Error("layout not found")
|
|
||||||
ErrDashboardNotFound = Error("dashboard not found")
|
|
||||||
ErrUserNotFound = Error("user not found")
|
|
||||||
ErrLayoutInvalid = Error("layout is invalid")
|
|
||||||
ErrDashboardInvalid = Error("dashboard is invalid")
|
|
||||||
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'")
|
|
||||||
ErrInvalidColor = Error("Invalid color. Accepted color format is #RRGGBB")
|
|
||||||
ErrUserAlreadyExists = Error("user already exists")
|
|
||||||
ErrOrganizationNotFound = Error("organization not found")
|
|
||||||
ErrMappingNotFound = Error("mapping not found")
|
|
||||||
ErrOrganizationAlreadyExists = Error("organization already exists")
|
|
||||||
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")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Error is a domain error encountered while processing chronograf requests
|
|
||||||
type Error string
|
|
||||||
|
|
||||||
func (e Error) Error() string {
|
|
||||||
return string(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logger represents an abstracted structured logging implementation. It
|
|
||||||
// provides methods to trigger log messages at various alert levels and a
|
|
||||||
// WithField method to set keys for a structured log message.
|
|
||||||
type Logger interface {
|
|
||||||
Debug(...interface{})
|
|
||||||
Info(...interface{})
|
|
||||||
Error(...interface{})
|
|
||||||
|
|
||||||
WithField(string, interface{}) Logger
|
|
||||||
|
|
||||||
// Logger can be transformed into an io.Writer.
|
|
||||||
// That writer is the end of an io.Pipe and it is your responsibility to close it.
|
|
||||||
Writer() *io.PipeWriter
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoopLogger is a chronograf logger that does nothing.
|
|
||||||
type NoopLogger struct{}
|
|
||||||
|
|
||||||
func (l *NoopLogger) Debug(...interface{}) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *NoopLogger) Info(...interface{}) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *NoopLogger) Error(...interface{}) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *NoopLogger) WithField(string, interface{}) Logger {
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *NoopLogger) Writer() *io.PipeWriter {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Router is an abstracted Router based on the API provided by the
|
|
||||||
// julienschmidt/httprouter package.
|
|
||||||
type Router interface {
|
|
||||||
http.Handler
|
|
||||||
GET(string, http.HandlerFunc)
|
|
||||||
PATCH(string, http.HandlerFunc)
|
|
||||||
POST(string, http.HandlerFunc)
|
|
||||||
DELETE(string, http.HandlerFunc)
|
|
||||||
PUT(string, http.HandlerFunc)
|
|
||||||
|
|
||||||
Handler(string, string, http.Handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assets returns a handler to serve the website.
|
|
||||||
type Assets interface {
|
|
||||||
Handler() http.Handler
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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`.
|
|
||||||
Connect(ctx context.Context, src *Source) error
|
|
||||||
// Ping returns version and TSDB type of time series database if reachable.
|
|
||||||
Ping(context.Context) error
|
|
||||||
// Version returns the version of the TSDB database
|
|
||||||
Version(context.Context) (string, error)
|
|
||||||
// Type returns the type of the TSDB database
|
|
||||||
Type(context.Context) (string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Point is a field set in a series
|
|
||||||
type Point struct {
|
|
||||||
Database string
|
|
||||||
RetentionPolicy string
|
|
||||||
Measurement string
|
|
||||||
Time int64
|
|
||||||
Tags map[string]string
|
|
||||||
Fields map[string]interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TimeSeries represents a queryable time series database.
|
|
||||||
type TimeSeries interface {
|
|
||||||
// Connect will connect to the time series using the information in `Source`.
|
|
||||||
Connect(context.Context, *Source) error
|
|
||||||
// Query retrieves time series data from the database.
|
|
||||||
Query(context.Context, Query) (Response, error)
|
|
||||||
// Write records points into a series
|
|
||||||
Write(context.Context, []Point) error
|
|
||||||
// UsersStore represents the user accounts within the TimeSeries database
|
|
||||||
Users(context.Context) UsersStore
|
|
||||||
// Permissions returns all valid names permissions in this database
|
|
||||||
Permissions(context.Context) Permissions
|
|
||||||
// Roles represents the roles associated with this TimesSeriesDatabase
|
|
||||||
Roles(context.Context) (RolesStore, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Role is a restricted set of permissions assigned to a set of users.
|
|
||||||
type Role struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Permissions Permissions `json:"permissions,omitempty"`
|
|
||||||
Users []User `json:"users,omitempty"`
|
|
||||||
Organization string `json:"organization,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RolesStore is the Storage and retrieval of authentication information
|
|
||||||
type RolesStore interface {
|
|
||||||
// All lists all roles from the RolesStore
|
|
||||||
All(context.Context) ([]Role, error)
|
|
||||||
// Create a new Role in the RolesStore
|
|
||||||
Add(context.Context, *Role) (*Role, error)
|
|
||||||
// Delete the Role from the RolesStore
|
|
||||||
Delete(context.Context, *Role) error
|
|
||||||
// Get retrieves a role if name exists.
|
|
||||||
Get(ctx context.Context, name string) (*Role, error)
|
|
||||||
// Update the roles' users or permissions
|
|
||||||
Update(context.Context, *Role) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Range represents an upper and lower bound for data
|
|
||||||
type Range struct {
|
|
||||||
Upper int64 `json:"upper"` // Upper is the upper bound
|
|
||||||
Lower int64 `json:"lower"` // Lower is the lower bound
|
|
||||||
}
|
|
||||||
|
|
||||||
// TemplateValue is a value use to replace a template in an InfluxQL query
|
|
||||||
type TemplateValue struct {
|
|
||||||
Value string `json:"value"` // Value is the specific value used to replace a template in an InfluxQL query
|
|
||||||
Type string `json:"type"` // Type can be tagKey, tagValue, fieldKey, csv, map, measurement, database, constant, influxql
|
|
||||||
Selected bool `json:"selected"` // Selected states that this variable has been picked to use for replacement
|
|
||||||
Key string `json:"key,omitempty"` // Key is the key for the Value if the Template Type is 'map'
|
|
||||||
}
|
|
||||||
|
|
||||||
// TemplateVar is a named variable within an InfluxQL query to be replaced with Values
|
|
||||||
type TemplateVar struct {
|
|
||||||
Var string `json:"tempVar"` // Var is the string to replace within InfluxQL
|
|
||||||
Values []TemplateValue `json:"values"` // Values are the replacement values within InfluxQL
|
|
||||||
}
|
|
||||||
|
|
||||||
// TemplateID is the unique ID used to identify a template
|
|
||||||
type TemplateID string
|
|
||||||
|
|
||||||
// Template represents a series of choices to replace TemplateVars within InfluxQL
|
|
||||||
type Template struct {
|
|
||||||
TemplateVar
|
|
||||||
ID TemplateID `json:"id"` // ID is the unique ID associated with this template
|
|
||||||
Type string `json:"type"` // Type can be fieldKeys, tagKeys, tagValues, csv, constant, measurements, databases, map, influxql, text
|
|
||||||
Label string `json:"label"` // Label is a user-facing description of the Template
|
|
||||||
Query *TemplateQuery `json:"query,omitempty"` // Query is used to generate the choices for a template
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query retrieves a Response from a TimeSeries.
|
|
||||||
type Query struct {
|
|
||||||
Command string `json:"query"` // Command is the query itself
|
|
||||||
DB string `json:"db,omitempty"` // DB is optional and if empty will not be used.
|
|
||||||
RP string `json:"rp,omitempty"` // RP is a retention policy and optional; if empty will not be used.
|
|
||||||
Epoch string `json:"epoch,omitempty"` // Epoch is the time format for the return results
|
|
||||||
Wheres []string `json:"wheres,omitempty"` // Wheres restricts the query to certain attributes
|
|
||||||
GroupBys []string `json:"groupbys,omitempty"` // GroupBys collate the query by these tags
|
|
||||||
Label string `json:"label,omitempty"` // Label is the Y-Axis label for the data
|
|
||||||
Range *Range `json:"range,omitempty"` // Range is the default Y-Axis range for the data
|
|
||||||
}
|
|
||||||
|
|
||||||
// DashboardQuery includes state for the query builder. This is a transition
|
|
||||||
// struct while we move to the full InfluxQL AST
|
|
||||||
type DashboardQuery struct {
|
|
||||||
Command string `json:"query"` // Command is the query itself
|
|
||||||
Label string `json:"label,omitempty"` // Label is the Y-Axis label for the data
|
|
||||||
Range *Range `json:"range,omitempty"` // Range is the default Y-Axis range for the data
|
|
||||||
QueryConfig QueryConfig `json:"queryConfig,omitempty"` // QueryConfig represents the query state that is understood by the data explorer
|
|
||||||
Source string `json:"source"` // Source is the optional URI to the data source for this queryConfig
|
|
||||||
Shifts []TimeShift `json:"-"` // Shifts represents shifts to apply to an influxql query's time range. Clients expect the shift to be in the generated QueryConfig
|
|
||||||
// This was added after this code was brought over to influxdb.
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TemplateQuery is used to retrieve choices for template replacement
|
|
||||||
type TemplateQuery struct {
|
|
||||||
Command string `json:"influxql"` // Command is the query itself
|
|
||||||
DB string `json:"db,omitempty"` // DB is optional and if empty will not be used.
|
|
||||||
RP string `json:"rp,omitempty"` // RP is a retention policy and optional; if empty will not be used.
|
|
||||||
Measurement string `json:"measurement"` // Measurement is the optionally selected measurement for the query
|
|
||||||
TagKey string `json:"tagKey"` // TagKey is the optionally selected tag key for the query
|
|
||||||
FieldKey string `json:"fieldKey"` // FieldKey is the optionally selected field key for the query
|
|
||||||
}
|
|
||||||
|
|
||||||
// Response is the result of a query against a TimeSeries
|
|
||||||
type Response interface {
|
|
||||||
MarshalJSON() ([]byte, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
Name string `json:"name"` // Name is the user-defined name for the source
|
|
||||||
Type string `json:"type,omitempty"` // Type specifies which kinds of source (enterprise vs oss)
|
|
||||||
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
|
|
||||||
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.
|
|
||||||
Default bool `json:"default"` // Default specifies the default source for the application
|
|
||||||
Telegraf string `json:"telegraf"` // Telegraf is the db telegraf is written to. By default it is "telegraf"
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// SourcesStore stores connection information for a `TimeSeries`
|
|
||||||
type SourcesStore interface {
|
|
||||||
// All returns all sources in the store
|
|
||||||
All(context.Context) ([]Source, error)
|
|
||||||
// Add creates a new source in the SourcesStore and returns Source with ID
|
|
||||||
Add(context.Context, Source) (Source, error)
|
|
||||||
// Delete the Source from the store
|
|
||||||
Delete(context.Context, Source) error
|
|
||||||
// Get retrieves Source if `ID` exists
|
|
||||||
Get(ctx context.Context, ID int) (Source, error)
|
|
||||||
// Update the Source in the store.
|
|
||||||
Update(context.Context, Source) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// DBRP represents a database and retention policy for a time series source
|
|
||||||
type DBRP struct {
|
|
||||||
DB string `json:"db"`
|
|
||||||
RP string `json:"rp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AlertRule represents rules for building a tickscript alerting task
|
|
||||||
type AlertRule struct {
|
|
||||||
ID string `json:"id,omitempty"` // ID is the unique ID of the alert
|
|
||||||
TICKScript TICKScript `json:"tickscript"` // TICKScript is the raw tickscript associated with this Alert
|
|
||||||
Query *QueryConfig `json:"query"` // Query is the filter of data for the alert.
|
|
||||||
Every string `json:"every"` // Every how often to check for the alerting criteria
|
|
||||||
AlertNodes AlertNodes `json:"alertNodes"` // AlertNodes defines the destinations for the alert
|
|
||||||
Message string `json:"message"` // Message included with alert
|
|
||||||
Details string `json:"details"` // Details is generally used for the Email alert. If empty will not be added.
|
|
||||||
Trigger string `json:"trigger"` // Trigger is a type that defines when to trigger the alert
|
|
||||||
TriggerValues TriggerValues `json:"values"` // Defines the values that cause the alert to trigger
|
|
||||||
Name string `json:"name"` // Name is the user-defined name for the alert
|
|
||||||
Type string `json:"type"` // Represents the task type where stream is data streamed to kapacitor and batch is queried by kapacitor
|
|
||||||
DBRPs []DBRP `json:"dbrps"` // List of database retention policy pairs the task is allowed to access
|
|
||||||
Status string `json:"status"` // Represents if this rule is enabled or disabled in kapacitor
|
|
||||||
Executing bool `json:"executing"` // Whether the task is currently executing
|
|
||||||
Error string `json:"error"` // Any error encountered when kapacitor executes the task
|
|
||||||
Created time.Time `json:"created"` // Date the task was first created
|
|
||||||
Modified time.Time `json:"modified"` // Date the task was last modified
|
|
||||||
LastEnabled time.Time `json:"last-enabled,omitempty"` // Date the task was last set to status enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
// TICKScript task to be used by kapacitor
|
|
||||||
type TICKScript string
|
|
||||||
|
|
||||||
// Ticker generates tickscript tasks for kapacitor
|
|
||||||
type Ticker interface {
|
|
||||||
// Generate will create the tickscript to be used as a kapacitor task
|
|
||||||
Generate(AlertRule) (TICKScript, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TriggerValues specifies the alerting logic for a specific trigger type
|
|
||||||
type TriggerValues struct {
|
|
||||||
Change string `json:"change,omitempty"` // Change specifies if the change is a percent or absolute
|
|
||||||
Period string `json:"period,omitempty"` // Period length of time before deadman is alerted
|
|
||||||
Shift string `json:"shift,omitempty"` // Shift is the amount of time to look into the past for the alert to compare to the present
|
|
||||||
Operator string `json:"operator,omitempty"` // Operator for alert comparison
|
|
||||||
Value string `json:"value,omitempty"` // Value is the boundary value when alert goes critical
|
|
||||||
RangeValue string `json:"rangeValue"` // RangeValue is an optional value for range comparisons
|
|
||||||
}
|
|
||||||
|
|
||||||
// Field represent influxql fields and functions from the UI
|
|
||||||
type Field struct {
|
|
||||||
Value interface{} `json:"value"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Alias string `json:"alias"`
|
|
||||||
Args []Field `json:"args,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GroupBy represents influxql group by tags from the UI
|
|
||||||
type GroupBy struct {
|
|
||||||
Time string `json:"time"`
|
|
||||||
Tags []string `json:"tags"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DurationRange represents the lower and upper durations of the query config
|
|
||||||
type DurationRange struct {
|
|
||||||
Upper string `json:"upper"`
|
|
||||||
Lower string `json:"lower"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TimeShift represents a shift to apply to an influxql query's time range
|
|
||||||
type TimeShift struct {
|
|
||||||
Label string `json:"label"` // Label user facing description
|
|
||||||
Unit string `json:"unit"` // Unit influxql time unit representation i.e. ms, s, m, h, d
|
|
||||||
Quantity string `json:"quantity"` // Quantity number of units
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryConfig represents UI query from the data explorer
|
|
||||||
type QueryConfig struct {
|
|
||||||
ID string `json:"id,omitempty"`
|
|
||||||
Database string `json:"database"`
|
|
||||||
Measurement string `json:"measurement"`
|
|
||||||
RetentionPolicy string `json:"retentionPolicy"`
|
|
||||||
Fields []Field `json:"fields"`
|
|
||||||
Tags map[string][]string `json:"tags"`
|
|
||||||
GroupBy GroupBy `json:"groupBy"`
|
|
||||||
AreTagsAccepted bool `json:"areTagsAccepted"`
|
|
||||||
Fill string `json:"fill,omitempty"`
|
|
||||||
RawText *string `json:"rawText"`
|
|
||||||
Range *DurationRange `json:"range"`
|
|
||||||
Shifts []TimeShift `json:"shifts"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// KapacitorNode adds arguments and properties to an alert
|
|
||||||
type KapacitorNode struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Args []string `json:"args"`
|
|
||||||
Properties []KapacitorProperty `json:"properties"`
|
|
||||||
// In the future we could add chaining methods here.
|
|
||||||
}
|
|
||||||
|
|
||||||
// KapacitorProperty modifies the node they are called on
|
|
||||||
type KapacitorProperty struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Args []string `json:"args"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server represents a proxy connection to an HTTP server
|
|
||||||
type Server struct {
|
|
||||||
ID int `json:"id,string"` // ID is the unique ID of the server
|
|
||||||
SrcID int `json:"srcId,string"` // SrcID of the data source
|
|
||||||
Name string `json:"name"` // Name is the user-defined name for the server
|
|
||||||
Username string `json:"username"` // Username is the username to connect to the server
|
|
||||||
Password string `json:"password"` // Password is in CLEARTEXT
|
|
||||||
URL string `json:"url"` // URL are the connections to the server
|
|
||||||
InsecureSkipVerify bool `json:"insecureSkipVerify"` // InsecureSkipVerify as true means any certificate presented by the server is accepted.
|
|
||||||
Active bool `json:"active"` // Is this the active server for the source?
|
|
||||||
Organization string `json:"organization"` // Organization is the organization ID that resource belongs to
|
|
||||||
Type string `json:"type"` // Type is the kind of service (e.g. kapacitor or flux)
|
|
||||||
Metadata map[string]interface{} `json:"metadata"` // Metadata is any other data that the frontend wants to store about this service
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServersStore stores connection information for a `Server`
|
|
||||||
type ServersStore interface {
|
|
||||||
// All returns all servers in the store
|
|
||||||
All(context.Context) ([]Server, error)
|
|
||||||
// Add creates a new source in the ServersStore and returns Server with ID
|
|
||||||
Add(context.Context, Server) (Server, error)
|
|
||||||
// Delete the Server from the store
|
|
||||||
Delete(context.Context, Server) error
|
|
||||||
// Get retrieves Server if `ID` exists
|
|
||||||
Get(ctx context.Context, ID int) (Server, error)
|
|
||||||
// Update the Server in the store.
|
|
||||||
Update(context.Context, Server) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ID creates uniq ID string
|
|
||||||
type ID interface {
|
|
||||||
// Generate creates a unique ID string
|
|
||||||
Generate() (string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// AllScope grants permission for all databases.
|
|
||||||
AllScope Scope = "all"
|
|
||||||
// DBScope grants permissions for a specific database
|
|
||||||
DBScope Scope = "database"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Permission is a specific allowance for User or Role bound to a
|
|
||||||
// scope of the data source
|
|
||||||
type Permission struct {
|
|
||||||
Scope Scope `json:"scope"`
|
|
||||||
Name string `json:"name,omitempty"`
|
|
||||||
Allowed Allowances `json:"allowed"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Permissions represent the entire set of permissions a User or Role may have
|
|
||||||
type Permissions []Permission
|
|
||||||
|
|
||||||
// Allowances defines what actions a user can have on a scoped permission
|
|
||||||
type Allowances []string
|
|
||||||
|
|
||||||
// Scope defines the location of access of a permission
|
|
||||||
type Scope string
|
|
||||||
|
|
||||||
// User represents an authenticated user.
|
|
||||||
type User struct {
|
|
||||||
ID uint64 `json:"id,string,omitempty"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Passwd string `json:"password,omitempty"`
|
|
||||||
Permissions Permissions `json:"permissions,omitempty"`
|
|
||||||
Roles []Role `json:"roles"`
|
|
||||||
Provider string `json:"provider,omitempty"`
|
|
||||||
Scheme string `json:"scheme,omitempty"`
|
|
||||||
SuperAdmin bool `json:"superAdmin,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserQuery represents the attributes that a user may be retrieved by.
|
|
||||||
// It is predominantly used in the UsersStore.Get method.
|
|
||||||
//
|
|
||||||
// It is expected that only one of ID or Name, Provider, and Scheme will be
|
|
||||||
// specified, but all are provided UserStores should prefer ID.
|
|
||||||
type UserQuery struct {
|
|
||||||
ID *uint64
|
|
||||||
Name *string
|
|
||||||
Provider *string
|
|
||||||
Scheme *string
|
|
||||||
}
|
|
||||||
|
|
||||||
// UsersStore is the Storage and retrieval of authentication information
|
|
||||||
//
|
|
||||||
// While not necessary for the app to function correctly, it is
|
|
||||||
// expected that Implementors of the UsersStore will take
|
|
||||||
// care to guarantee that the combinartion of a users Name, Provider,
|
|
||||||
// and Scheme are unique.
|
|
||||||
type UsersStore interface {
|
|
||||||
// All lists all users from the UsersStore
|
|
||||||
All(context.Context) ([]User, error)
|
|
||||||
// Create a new User in the UsersStore
|
|
||||||
Add(context.Context, *User) (*User, error)
|
|
||||||
// Delete the User from the UsersStore
|
|
||||||
Delete(context.Context, *User) error
|
|
||||||
// Get retrieves a user if name exists.
|
|
||||||
Get(ctx context.Context, q UserQuery) (*User, error)
|
|
||||||
// Update the user's permissions or roles
|
|
||||||
Update(context.Context, *User) error
|
|
||||||
// Num returns the number of users in the UsersStore
|
|
||||||
Num(context.Context) (int, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Database represents a database in a time series source
|
|
||||||
type Database struct {
|
|
||||||
Name string `json:"name"` // a unique string identifier for the database
|
|
||||||
Duration string `json:"duration,omitempty"` // the duration (when creating a default retention policy)
|
|
||||||
Replication int32 `json:"replication,omitempty"` // the replication factor (when creating a default retention policy)
|
|
||||||
ShardDuration string `json:"shardDuration,omitempty"` // the shard duration (when creating a default retention policy)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RetentionPolicy represents a retention policy in a time series source
|
|
||||||
type RetentionPolicy struct {
|
|
||||||
Name string `json:"name"` // a unique string identifier for the retention policy
|
|
||||||
Duration string `json:"duration,omitempty"` // the duration
|
|
||||||
Replication int32 `json:"replication,omitempty"` // the replication factor
|
|
||||||
ShardDuration string `json:"shardDuration,omitempty"` // the shard duration
|
|
||||||
Default bool `json:"isDefault,omitempty"` // whether the RP should be the default
|
|
||||||
}
|
|
||||||
|
|
||||||
// Measurement represents a measurement in a time series source
|
|
||||||
type Measurement struct {
|
|
||||||
Name string `json:"name"` // a unique string identifier for the measurement
|
|
||||||
}
|
|
||||||
|
|
||||||
// Databases represents a databases in a time series source
|
|
||||||
type Databases interface {
|
|
||||||
// AllDB lists all databases in the current data source
|
|
||||||
AllDB(context.Context) ([]Database, error)
|
|
||||||
// Connect connects to a database in the current data source
|
|
||||||
Connect(context.Context, *Source) error
|
|
||||||
// CreateDB creates a database in the current data source
|
|
||||||
CreateDB(context.Context, *Database) (*Database, error)
|
|
||||||
// DropDB drops a database in the current data source
|
|
||||||
DropDB(context.Context, string) error
|
|
||||||
|
|
||||||
// AllRP lists all retention policies in the current data source
|
|
||||||
AllRP(context.Context, string) ([]RetentionPolicy, error)
|
|
||||||
// CreateRP creates a retention policy in the current data source
|
|
||||||
CreateRP(context.Context, string, *RetentionPolicy) (*RetentionPolicy, error)
|
|
||||||
// UpdateRP updates a retention policy in the current data source
|
|
||||||
UpdateRP(context.Context, string, string, *RetentionPolicy) (*RetentionPolicy, error)
|
|
||||||
// DropRP drops a retention policy in the current data source
|
|
||||||
DropRP(context.Context, string, string) error
|
|
||||||
|
|
||||||
// GetMeasurements lists measurements in the current data source
|
|
||||||
GetMeasurements(ctx context.Context, db string, limit, offset int) ([]Measurement, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Annotation represents a time-based metadata associated with a source
|
|
||||||
type Annotation struct {
|
|
||||||
ID string // ID is the unique annotation identifier
|
|
||||||
StartTime time.Time // StartTime starts the annotation
|
|
||||||
EndTime time.Time // EndTime ends the annotation
|
|
||||||
Text string // Text is the associated user-facing text describing the annotation
|
|
||||||
Type string // Type describes the kind of annotation
|
|
||||||
}
|
|
||||||
|
|
||||||
// AnnotationStore represents storage and retrieval of annotations
|
|
||||||
type AnnotationStore interface {
|
|
||||||
All(ctx context.Context, start, stop time.Time) ([]Annotation, error) // All lists all Annotations between start and stop
|
|
||||||
Add(context.Context, *Annotation) (*Annotation, error) // Add creates a new annotation in the store
|
|
||||||
Delete(ctx context.Context, id string) error // Delete removes the annotation from the store
|
|
||||||
Get(ctx context.Context, id string) (*Annotation, error) // Get retrieves an annotation
|
|
||||||
Update(context.Context, *Annotation) error // Update replaces annotation
|
|
||||||
}
|
|
||||||
|
|
||||||
// DashboardID is the dashboard ID
|
|
||||||
type DashboardID int
|
|
||||||
|
|
||||||
// Dashboard represents all visual and query data for a dashboard
|
|
||||||
type Dashboard struct {
|
|
||||||
ID DashboardID `json:"id"`
|
|
||||||
Cells []DashboardCell `json:"cells"`
|
|
||||||
Templates []Template `json:"templates"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Organization string `json:"organization"` // Organization is the organization ID that resource belongs to
|
|
||||||
}
|
|
||||||
|
|
||||||
// Axis represents the visible extents of a visualization
|
|
||||||
type Axis struct {
|
|
||||||
Bounds []string `json:"bounds"` // bounds are an arbitrary list of client-defined strings that specify the viewport for a cell
|
|
||||||
LegacyBounds [2]int64 `json:"-"` // legacy bounds are for testing a migration from an earlier version of axis
|
|
||||||
Label string `json:"label"` // label is a description of this Axis
|
|
||||||
Prefix string `json:"prefix"` // Prefix represents a label prefix for formatting axis values
|
|
||||||
Suffix string `json:"suffix"` // Suffix represents a label suffix for formatting axis values
|
|
||||||
Base string `json:"base"` // Base represents the radix for formatting axis values
|
|
||||||
Scale string `json:"scale"` // Scale is the axis formatting scale. Supported: "log", "linear"
|
|
||||||
}
|
|
||||||
|
|
||||||
// CellColor represents the encoding of data into visualizations
|
|
||||||
type CellColor struct {
|
|
||||||
ID string `json:"id"` // ID is the unique id of the cell color
|
|
||||||
Type string `json:"type"` // Type is how the color is used. Accepted (min,max,threshold)
|
|
||||||
Hex string `json:"hex"` // Hex is the hex number of the color
|
|
||||||
Name string `json:"name"` // Name is the user-facing name of the hex color
|
|
||||||
Value string `json:"value"` // Value is the data value mapped to this color
|
|
||||||
}
|
|
||||||
|
|
||||||
// DashboardCell holds visual and query information for a cell
|
|
||||||
type DashboardCell struct {
|
|
||||||
ID string `json:"i"`
|
|
||||||
X int32 `json:"x"`
|
|
||||||
Y int32 `json:"y"`
|
|
||||||
W int32 `json:"w"`
|
|
||||||
H int32 `json:"h"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Queries []DashboardQuery `json:"queries"`
|
|
||||||
Axes map[string]Axis `json:"axes"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
CellColors []CellColor `json:"colors"`
|
|
||||||
TableOptions TableOptions `json:"tableOptions,omitempty"`
|
|
||||||
FieldOptions []RenamableField `json:"fieldOptions"`
|
|
||||||
TimeFormat string `json:"timeFormat"`
|
|
||||||
DecimalPlaces DecimalPlaces `json:"decimalPlaces"`
|
|
||||||
// These were added after this code was brought over to influxdb.
|
|
||||||
Note string `json:"note,omitempty"`
|
|
||||||
NoteVisibility string `json:"noteVisibility,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenamableField is a column/row field in a DashboardCell of type Table
|
|
||||||
type RenamableField struct {
|
|
||||||
InternalName string `json:"internalName"`
|
|
||||||
DisplayName string `json:"displayName"`
|
|
||||||
Visible bool `json:"visible"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TableOptions is a type of options for a DashboardCell with type Table
|
|
||||||
type TableOptions struct {
|
|
||||||
VerticalTimeAxis bool `json:"verticalTimeAxis"`
|
|
||||||
SortBy RenamableField `json:"sortBy"`
|
|
||||||
Wrapping string `json:"wrapping"`
|
|
||||||
FixFirstColumn bool `json:"fixFirstColumn"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecimalPlaces indicates whether decimal places should be enforced, and how many digits it should show.
|
|
||||||
type DecimalPlaces struct {
|
|
||||||
IsEnforced bool `json:"isEnforced"`
|
|
||||||
Digits int32 `json:"digits"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DashboardsStore is the storage and retrieval of dashboards
|
|
||||||
type DashboardsStore interface {
|
|
||||||
// All lists all dashboards from the DashboardStore
|
|
||||||
All(context.Context) ([]Dashboard, error)
|
|
||||||
// Create a new Dashboard in the DashboardStore
|
|
||||||
Add(context.Context, Dashboard) (Dashboard, error)
|
|
||||||
// Delete the Dashboard from the DashboardStore if `ID` exists.
|
|
||||||
Delete(context.Context, Dashboard) error
|
|
||||||
// Get retrieves a dashboard if `ID` exists.
|
|
||||||
Get(ctx context.Context, id DashboardID) (Dashboard, error)
|
|
||||||
// Update replaces the dashboard information
|
|
||||||
Update(context.Context, Dashboard) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cell is a rectangle and multiple time series queries to visualize.
|
|
||||||
type Cell struct {
|
|
||||||
X int32 `json:"x"`
|
|
||||||
Y int32 `json:"y"`
|
|
||||||
W int32 `json:"w"`
|
|
||||||
H int32 `json:"h"`
|
|
||||||
I string `json:"i"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Queries []Query `json:"queries"`
|
|
||||||
Axes map[string]Axis `json:"axes"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
CellColors []CellColor `json:"colors"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Layout is a collection of Cells for visualization
|
|
||||||
type Layout struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Application string `json:"app"`
|
|
||||||
Measurement string `json:"measurement"`
|
|
||||||
Autoflow bool `json:"autoflow"`
|
|
||||||
Cells []Cell `json:"cells"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// LayoutsStore stores dashboards and associated Cells
|
|
||||||
type LayoutsStore interface {
|
|
||||||
// All returns all dashboards in the store
|
|
||||||
All(context.Context) ([]Layout, error)
|
|
||||||
// Add creates a new dashboard in the LayoutsStore
|
|
||||||
Add(context.Context, Layout) (Layout, error)
|
|
||||||
// Delete the dashboard from the store
|
|
||||||
Delete(context.Context, Layout) error
|
|
||||||
// Get retrieves Layout if `ID` exists
|
|
||||||
Get(ctx context.Context, ID string) (Layout, error)
|
|
||||||
// Update the dashboard in the store.
|
|
||||||
Update(context.Context, Layout) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// MappingWildcard is the wildcard value for mappings
|
|
||||||
const MappingWildcard string = "*"
|
|
||||||
|
|
||||||
// A Mapping is the structure that is used to determine a users
|
|
||||||
// role within an organization. The high level idea is to grant
|
|
||||||
// certain roles to certain users without them having to be given
|
|
||||||
// explicit role within the organization.
|
|
||||||
//
|
|
||||||
// One can think of a mapping like so:
|
|
||||||
// Provider:Scheme:Group -> Organization
|
|
||||||
// github:oauth2:influxdata -> Happy
|
|
||||||
// beyondcorp:ldap:influxdata -> TheBillHilliettas
|
|
||||||
//
|
|
||||||
// Any of Provider, Scheme, or Group may be provided as a wildcard *
|
|
||||||
// github:oauth2:* -> MyOrg
|
|
||||||
// *:*:* -> AllOrg
|
|
||||||
type Mapping struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Organization string `json:"organizationId"`
|
|
||||||
Provider string `json:"provider"`
|
|
||||||
Scheme string `json:"scheme"`
|
|
||||||
ProviderOrganization string `json:"providerOrganization"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// MappingsStore is the storage and retrieval of Mappings
|
|
||||||
type MappingsStore interface {
|
|
||||||
// Add creates a new Mapping.
|
|
||||||
// The Created mapping is returned back to the user with the
|
|
||||||
// ID field populated.
|
|
||||||
Add(context.Context, *Mapping) (*Mapping, error)
|
|
||||||
// All lists all Mapping in the MappingsStore
|
|
||||||
All(context.Context) ([]Mapping, error)
|
|
||||||
// Delete removes an Mapping from the MappingsStore
|
|
||||||
Delete(context.Context, *Mapping) error
|
|
||||||
// Get retrieves an Mapping from the MappingsStore
|
|
||||||
Get(context.Context, string) (*Mapping, error)
|
|
||||||
// Update updates an Mapping in the MappingsStore
|
|
||||||
Update(context.Context, *Mapping) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Organization is a group of resources under a common name
|
|
||||||
type Organization struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
// DefaultRole is the name of the role that is the default for any users added to the organization
|
|
||||||
DefaultRole string `json:"defaultRole,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OrganizationQuery represents the attributes that a organization may be retrieved by.
|
|
||||||
// It is predominantly used in the OrganizationsStore.Get method.
|
|
||||||
// It is expected that only one of ID or Name will be specified, but will prefer ID over Name if both are specified.
|
|
||||||
type OrganizationQuery struct {
|
|
||||||
// If an ID is provided in the query, the lookup time for an organization will be O(1).
|
|
||||||
ID *string
|
|
||||||
// If Name is provided, the lookup time will be O(n).
|
|
||||||
Name *string
|
|
||||||
}
|
|
||||||
|
|
||||||
// OrganizationsStore is the storage and retrieval of Organizations
|
|
||||||
//
|
|
||||||
// While not necessary for the app to function correctly, it is
|
|
||||||
// expected that Implementors of the OrganizationsStore will take
|
|
||||||
// care to guarantee that the Organization.Name is unique. Allowing
|
|
||||||
// for duplicate names creates a confusing UX experience for the User.
|
|
||||||
type OrganizationsStore interface {
|
|
||||||
// Add creates a new Organization.
|
|
||||||
// The Created organization is returned back to the user with the
|
|
||||||
// ID field populated.
|
|
||||||
Add(context.Context, *Organization) (*Organization, error)
|
|
||||||
// All lists all Organizations in the OrganizationsStore
|
|
||||||
All(context.Context) ([]Organization, error)
|
|
||||||
// Delete removes an Organization from the OrganizationsStore
|
|
||||||
Delete(context.Context, *Organization) error
|
|
||||||
// Get retrieves an Organization from the OrganizationsStore
|
|
||||||
Get(context.Context, OrganizationQuery) (*Organization, error)
|
|
||||||
// Update updates an Organization in the OrganizationsStore
|
|
||||||
Update(context.Context, *Organization) error
|
|
||||||
// CreateDefault creates the default organization
|
|
||||||
CreateDefault(ctx context.Context) error
|
|
||||||
// DefaultOrganization returns the DefaultOrganization
|
|
||||||
DefaultOrganization(ctx context.Context) (*Organization, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config is the global application Config for parameters that can be set via
|
|
||||||
// API, with different sections, such as Auth
|
|
||||||
type Config struct {
|
|
||||||
Auth AuthConfig `json:"auth"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthConfig is the global application config section for auth parameters
|
|
||||||
type AuthConfig struct {
|
|
||||||
// SuperAdminNewUsers configuration option that specifies which users will auto become super admin
|
|
||||||
SuperAdminNewUsers bool `json:"superAdminNewUsers"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConfigStore is the storage and retrieval of global application Config
|
|
||||||
type ConfigStore interface {
|
|
||||||
// Initialize creates the initial configuration
|
|
||||||
Initialize(context.Context) error
|
|
||||||
// Get retrieves the whole Config from the ConfigStore
|
|
||||||
Get(context.Context) (*Config, error)
|
|
||||||
// Update updates the whole Config in the ConfigStore
|
|
||||||
Update(context.Context, *Config) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// OrganizationConfig is the organization config for parameters that can
|
|
||||||
// be set via API, with different sections, such as LogViewer
|
|
||||||
type OrganizationConfig struct {
|
|
||||||
OrganizationID string `json:"organization"`
|
|
||||||
LogViewer LogViewerConfig `json:"logViewer"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogViewerConfig is the configuration settings for the Log Viewer UI
|
|
||||||
type LogViewerConfig struct {
|
|
||||||
Columns []LogViewerColumn `json:"columns"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// LogViewerColumn is a specific column of the Log Viewer UI
|
|
||||||
type LogViewerColumn struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Position int32 `json:"position"`
|
|
||||||
Encodings []ColumnEncoding `json:"encodings"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ColumnEncoding is the settings for a specific column of the Log Viewer UI
|
|
||||||
type ColumnEncoding struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
Name string `json:"name,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OrganizationConfigStore is the storage and retrieval of organization Configs
|
|
||||||
type OrganizationConfigStore interface {
|
|
||||||
// FindOrCreate gets an existing OrganizationConfig and creates one if none exists
|
|
||||||
FindOrCreate(ctx context.Context, orgID string) (*OrganizationConfig, error)
|
|
||||||
// Put replaces the whole organization config in the OrganizationConfigStore
|
|
||||||
Put(context.Context, *OrganizationConfig) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildInfo is sent to the usage client to track versions and commits
|
|
||||||
type BuildInfo struct {
|
|
||||||
Version string
|
|
||||||
Commit string
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildStore is the storage and retrieval of Chronograf build information
|
|
||||||
type BuildStore interface {
|
|
||||||
Get(context.Context) (BuildInfo, error)
|
|
||||||
Update(context.Context, BuildInfo) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Environment is the set of front-end exposed environment variables
|
|
||||||
// that were set on the server
|
|
||||||
type Environment struct {
|
|
||||||
TelegrafSystemInterval time.Duration `json:"telegrafSystemInterval"`
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
package id
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf"
|
|
||||||
)
|
|
||||||
|
|
||||||
// tm generates an id based on current time
|
|
||||||
type tm struct {
|
|
||||||
Now func() time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTime builds a chronograf.ID generator based on current time
|
|
||||||
func NewTime() chronograf.ID {
|
|
||||||
return &tm{
|
|
||||||
Now: time.Now,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate creates a string based on the current time as an integer
|
|
||||||
func (i *tm) Generate() (string, error) {
|
|
||||||
return strconv.Itoa(int(i.Now().Unix())), nil
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
package id
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf"
|
|
||||||
uuid "github.com/satori/go.uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ chronograf.ID = &UUID{}
|
|
||||||
|
|
||||||
// UUID generates a V4 uuid
|
|
||||||
type UUID struct{}
|
|
||||||
|
|
||||||
// Generate creates a UUID v4 string
|
|
||||||
func (i *UUID) Generate() (string, error) {
|
|
||||||
uuid, err := uuid.NewV4()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return uuid.String(), nil
|
|
||||||
}
|
|
|
@ -1,270 +0,0 @@
|
||||||
package influx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf"
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf/id"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// AllAnnotations returns all annotations from the chronograf database
|
|
||||||
AllAnnotations = `SELECT "start_time", "modified_time_ns", "text", "type", "id" FROM "annotations" WHERE "deleted"=false AND time >= %dns and "start_time" <= %d ORDER BY time DESC`
|
|
||||||
// GetAnnotationID returns all annotations from the chronograf database where id is %s
|
|
||||||
GetAnnotationID = `SELECT "start_time", "modified_time_ns", "text", "type", "id" FROM "annotations" WHERE "id"='%s' AND "deleted"=false ORDER BY time DESC`
|
|
||||||
// AnnotationsDB is chronograf. Perhaps later we allow this to be changed
|
|
||||||
AnnotationsDB = "chronograf"
|
|
||||||
// DefaultRP is autogen. Perhaps later we allow this to be changed
|
|
||||||
DefaultRP = "autogen"
|
|
||||||
// DefaultMeasurement is annotations.
|
|
||||||
DefaultMeasurement = "annotations"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ chronograf.AnnotationStore = &AnnotationStore{}
|
|
||||||
|
|
||||||
// AnnotationStore stores annotations within InfluxDB
|
|
||||||
type AnnotationStore struct {
|
|
||||||
client chronograf.TimeSeries
|
|
||||||
id chronograf.ID
|
|
||||||
now Now
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAnnotationStore constructs an annoation store with a client
|
|
||||||
func NewAnnotationStore(client chronograf.TimeSeries) *AnnotationStore {
|
|
||||||
return &AnnotationStore{
|
|
||||||
client: client,
|
|
||||||
id: &id.UUID{},
|
|
||||||
now: time.Now,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// All lists all Annotations
|
|
||||||
func (a *AnnotationStore) All(ctx context.Context, start, stop time.Time) ([]chronograf.Annotation, error) {
|
|
||||||
return a.queryAnnotations(ctx, fmt.Sprintf(AllAnnotations, start.UnixNano(), stop.UnixNano()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get retrieves an annotation
|
|
||||||
func (a *AnnotationStore) Get(ctx context.Context, id string) (*chronograf.Annotation, error) {
|
|
||||||
annos, err := a.queryAnnotations(ctx, fmt.Sprintf(GetAnnotationID, id))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(annos) == 0 {
|
|
||||||
return nil, chronograf.ErrAnnotationNotFound
|
|
||||||
}
|
|
||||||
return &annos[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add creates a new annotation in the store
|
|
||||||
func (a *AnnotationStore) Add(ctx context.Context, anno *chronograf.Annotation) (*chronograf.Annotation, error) {
|
|
||||||
var err error
|
|
||||||
anno.ID, err = a.id.Generate()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return anno, a.client.Write(ctx, []chronograf.Point{
|
|
||||||
toPoint(anno, a.now()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete removes the annotation from the store
|
|
||||||
func (a *AnnotationStore) Delete(ctx context.Context, id string) error {
|
|
||||||
cur, err := a.Get(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return a.client.Write(ctx, []chronograf.Point{
|
|
||||||
toDeletedPoint(cur, a.now()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update replaces annotation; if the annotation's time is different, it
|
|
||||||
// also removes the previous annotation
|
|
||||||
func (a *AnnotationStore) Update(ctx context.Context, anno *chronograf.Annotation) error {
|
|
||||||
cur, err := a.Get(ctx, anno.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := a.client.Write(ctx, []chronograf.Point{toPoint(anno, a.now())}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the updated annotation has a different time, then, we must
|
|
||||||
// delete the previous annotation
|
|
||||||
if !cur.EndTime.Equal(anno.EndTime) {
|
|
||||||
return a.client.Write(ctx, []chronograf.Point{
|
|
||||||
toDeletedPoint(cur, a.now()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// queryAnnotations queries the chronograf db and produces all annotations
|
|
||||||
func (a *AnnotationStore) queryAnnotations(ctx context.Context, query string) ([]chronograf.Annotation, error) {
|
|
||||||
res, err := a.client.Query(ctx, chronograf.Query{
|
|
||||||
Command: query,
|
|
||||||
DB: AnnotationsDB,
|
|
||||||
Epoch: "ns",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
octets, err := res.MarshalJSON()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
results := influxResults{}
|
|
||||||
d := json.NewDecoder(bytes.NewReader(octets))
|
|
||||||
d.UseNumber()
|
|
||||||
if err := d.Decode(&results); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return results.Annotations()
|
|
||||||
}
|
|
||||||
|
|
||||||
func toPoint(anno *chronograf.Annotation, now time.Time) chronograf.Point {
|
|
||||||
return chronograf.Point{
|
|
||||||
Database: AnnotationsDB,
|
|
||||||
RetentionPolicy: DefaultRP,
|
|
||||||
Measurement: DefaultMeasurement,
|
|
||||||
Time: anno.EndTime.UnixNano(),
|
|
||||||
Tags: map[string]string{
|
|
||||||
"id": anno.ID,
|
|
||||||
},
|
|
||||||
Fields: map[string]interface{}{
|
|
||||||
"deleted": false,
|
|
||||||
"start_time": anno.StartTime.UnixNano(),
|
|
||||||
"modified_time_ns": int64(now.UnixNano()),
|
|
||||||
"text": anno.Text,
|
|
||||||
"type": anno.Type,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func toDeletedPoint(anno *chronograf.Annotation, now time.Time) chronograf.Point {
|
|
||||||
return chronograf.Point{
|
|
||||||
Database: AnnotationsDB,
|
|
||||||
RetentionPolicy: DefaultRP,
|
|
||||||
Measurement: DefaultMeasurement,
|
|
||||||
Time: anno.EndTime.UnixNano(),
|
|
||||||
Tags: map[string]string{
|
|
||||||
"id": anno.ID,
|
|
||||||
},
|
|
||||||
Fields: map[string]interface{}{
|
|
||||||
"deleted": true,
|
|
||||||
"start_time": int64(0),
|
|
||||||
"modified_time_ns": int64(now.UnixNano()),
|
|
||||||
"text": "",
|
|
||||||
"type": "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type value []interface{}
|
|
||||||
|
|
||||||
func (v value) Int64(idx int) (int64, error) {
|
|
||||||
if idx >= len(v) {
|
|
||||||
return 0, fmt.Errorf("index %d does not exist in values", idx)
|
|
||||||
}
|
|
||||||
n, ok := v[idx].(json.Number)
|
|
||||||
if !ok {
|
|
||||||
return 0, fmt.Errorf("value at index %d is not int64, but, %T", idx, v[idx])
|
|
||||||
}
|
|
||||||
return n.Int64()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v value) Time(idx int) (time.Time, error) {
|
|
||||||
tm, err := v.Int64(idx)
|
|
||||||
if err != nil {
|
|
||||||
return time.Time{}, err
|
|
||||||
}
|
|
||||||
return time.Unix(0, tm), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v value) String(idx int) (string, error) {
|
|
||||||
if idx >= len(v) {
|
|
||||||
return "", fmt.Errorf("index %d does not exist in values", idx)
|
|
||||||
}
|
|
||||||
str, ok := v[idx].(string)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("value at index %d is not string, but, %T", idx, v[idx])
|
|
||||||
}
|
|
||||||
return str, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type influxResults []struct {
|
|
||||||
Series []struct {
|
|
||||||
Values []value `json:"values"`
|
|
||||||
} `json:"series"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// annotationResult is an intermediate struct to track the latest modified
|
|
||||||
// time of an annotation
|
|
||||||
type annotationResult struct {
|
|
||||||
chronograf.Annotation
|
|
||||||
// modTime is bookkeeping to handle the case when an update fails; the latest
|
|
||||||
// modTime will be the record returned
|
|
||||||
modTime int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// Annotations converts AllAnnotations query to annotations
|
|
||||||
func (r *influxResults) Annotations() (res []chronograf.Annotation, err error) {
|
|
||||||
annos := map[string]annotationResult{}
|
|
||||||
for _, u := range *r {
|
|
||||||
for _, s := range u.Series {
|
|
||||||
for _, v := range s.Values {
|
|
||||||
anno := annotationResult{}
|
|
||||||
|
|
||||||
if anno.EndTime, err = v.Time(0); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if anno.StartTime, err = v.Time(1); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if anno.modTime, err = v.Int64(2); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if anno.Text, err = v.String(3); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if anno.Type, err = v.String(4); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if anno.ID, err = v.String(5); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are two annotations with the same id, take
|
|
||||||
// the annotation with the latest modification time
|
|
||||||
// This is to prevent issues when an update or delete fails.
|
|
||||||
// Updates and deletes are multiple step queries.
|
|
||||||
prev, ok := annos[anno.ID]
|
|
||||||
if !ok || anno.modTime > prev.modTime {
|
|
||||||
annos[anno.ID] = anno
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res = []chronograf.Annotation{}
|
|
||||||
for _, a := range annos {
|
|
||||||
res = append(res, a.Annotation)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Slice(res, func(i int, j int) bool {
|
|
||||||
return res[i].StartTime.Before(res[j].StartTime) || res[i].ID < res[j].ID
|
|
||||||
})
|
|
||||||
|
|
||||||
return res, err
|
|
||||||
}
|
|
|
@ -1,665 +0,0 @@
|
||||||
package influx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf"
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf/mocks"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_toPoint(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
anno *chronograf.Annotation
|
|
||||||
now time.Time
|
|
||||||
want chronograf.Point
|
|
||||||
}{
|
|
||||||
0: {
|
|
||||||
name: "convert annotation to point w/o start and end times",
|
|
||||||
anno: &chronograf.Annotation{
|
|
||||||
ID: "1",
|
|
||||||
Text: "mytext",
|
|
||||||
Type: "mytype",
|
|
||||||
},
|
|
||||||
now: time.Unix(0, 0),
|
|
||||||
want: chronograf.Point{
|
|
||||||
Database: AnnotationsDB,
|
|
||||||
RetentionPolicy: DefaultRP,
|
|
||||||
Measurement: DefaultMeasurement,
|
|
||||||
Time: time.Time{}.UnixNano(),
|
|
||||||
Tags: map[string]string{
|
|
||||||
"id": "1",
|
|
||||||
},
|
|
||||||
Fields: map[string]interface{}{
|
|
||||||
"deleted": false,
|
|
||||||
"start_time": time.Time{}.UnixNano(),
|
|
||||||
"modified_time_ns": int64(time.Unix(0, 0).UnixNano()),
|
|
||||||
"text": "mytext",
|
|
||||||
"type": "mytype",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
1: {
|
|
||||||
name: "convert annotation to point with start/end time",
|
|
||||||
anno: &chronograf.Annotation{
|
|
||||||
ID: "1",
|
|
||||||
Text: "mytext",
|
|
||||||
Type: "mytype",
|
|
||||||
StartTime: time.Unix(100, 0),
|
|
||||||
EndTime: time.Unix(200, 0),
|
|
||||||
},
|
|
||||||
now: time.Unix(0, 0),
|
|
||||||
want: chronograf.Point{
|
|
||||||
Database: AnnotationsDB,
|
|
||||||
RetentionPolicy: DefaultRP,
|
|
||||||
Measurement: DefaultMeasurement,
|
|
||||||
Time: time.Unix(200, 0).UnixNano(),
|
|
||||||
Tags: map[string]string{
|
|
||||||
"id": "1",
|
|
||||||
},
|
|
||||||
Fields: map[string]interface{}{
|
|
||||||
"deleted": false,
|
|
||||||
"start_time": time.Unix(100, 0).UnixNano(),
|
|
||||||
"modified_time_ns": int64(time.Unix(0, 0).UnixNano()),
|
|
||||||
"text": "mytext",
|
|
||||||
"type": "mytype",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := toPoint(tt.anno, tt.now); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("toPoint() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_toDeletedPoint(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
anno *chronograf.Annotation
|
|
||||||
now time.Time
|
|
||||||
want chronograf.Point
|
|
||||||
}{
|
|
||||||
0: {
|
|
||||||
name: "convert annotation to point w/o start and end times",
|
|
||||||
anno: &chronograf.Annotation{
|
|
||||||
ID: "1",
|
|
||||||
EndTime: time.Unix(0, 0),
|
|
||||||
},
|
|
||||||
now: time.Unix(0, 0),
|
|
||||||
want: chronograf.Point{
|
|
||||||
Database: AnnotationsDB,
|
|
||||||
RetentionPolicy: DefaultRP,
|
|
||||||
Measurement: DefaultMeasurement,
|
|
||||||
Time: 0,
|
|
||||||
Tags: map[string]string{
|
|
||||||
"id": "1",
|
|
||||||
},
|
|
||||||
Fields: map[string]interface{}{
|
|
||||||
"deleted": true,
|
|
||||||
"start_time": int64(0),
|
|
||||||
"modified_time_ns": int64(0),
|
|
||||||
"text": "",
|
|
||||||
"type": "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := toDeletedPoint(tt.anno, tt.now); !cmp.Equal(got, tt.want) {
|
|
||||||
t.Errorf("toDeletedPoint() = %s", cmp.Diff(got, tt.want))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_value_Int64(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
v value
|
|
||||||
idx int
|
|
||||||
want int64
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "index out of range returns error",
|
|
||||||
idx: 1,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "converts a string to int64",
|
|
||||||
v: value{
|
|
||||||
json.Number("1"),
|
|
||||||
},
|
|
||||||
idx: 0,
|
|
||||||
want: int64(1),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "when not a json.Number, return error",
|
|
||||||
v: value{
|
|
||||||
"howdy",
|
|
||||||
},
|
|
||||||
idx: 0,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := tt.v.Int64(tt.idx)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("value.Int64() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if got != tt.want {
|
|
||||||
t.Errorf("value.Int64() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_value_Time(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
v value
|
|
||||||
idx int
|
|
||||||
want time.Time
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "index out of range returns error",
|
|
||||||
idx: 1,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "converts a string to int64",
|
|
||||||
v: value{
|
|
||||||
json.Number("1"),
|
|
||||||
},
|
|
||||||
idx: 0,
|
|
||||||
want: time.Unix(0, 1),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "when not a json.Number, return error",
|
|
||||||
v: value{
|
|
||||||
"howdy",
|
|
||||||
},
|
|
||||||
idx: 0,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := tt.v.Time(tt.idx)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("value.Time() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("value.Time() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_value_String(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
v value
|
|
||||||
idx int
|
|
||||||
want string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "index out of range returns error",
|
|
||||||
idx: 1,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "converts a string",
|
|
||||||
v: value{
|
|
||||||
"howdy",
|
|
||||||
},
|
|
||||||
idx: 0,
|
|
||||||
want: "howdy",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "when not a string, return error",
|
|
||||||
v: value{
|
|
||||||
0,
|
|
||||||
},
|
|
||||||
idx: 0,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := tt.v.String(tt.idx)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("value.String() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if got != tt.want {
|
|
||||||
t.Errorf("value.String() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAnnotationStore_queryAnnotations(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
ctx context.Context
|
|
||||||
query string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
client chronograf.TimeSeries
|
|
||||||
args args
|
|
||||||
want []chronograf.Annotation
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "query error returns an error",
|
|
||||||
client: &mocks.TimeSeries{
|
|
||||||
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
|
|
||||||
return nil, fmt.Errorf("error")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "response marshal error returns an error",
|
|
||||||
client: &mocks.TimeSeries{
|
|
||||||
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
|
|
||||||
return mocks.NewResponse("", fmt.Errorf("")), nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Bad JSON returns an error",
|
|
||||||
client: &mocks.TimeSeries{
|
|
||||||
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
|
|
||||||
return mocks.NewResponse(`{}`, nil), nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "Incorrect fields returns error",
|
|
||||||
client: &mocks.TimeSeries{
|
|
||||||
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
|
|
||||||
return mocks.NewResponse(`[{
|
|
||||||
"series": [
|
|
||||||
{
|
|
||||||
"name": "annotations",
|
|
||||||
"columns": [
|
|
||||||
"time",
|
|
||||||
"deleted",
|
|
||||||
"id",
|
|
||||||
"modified_time_ns",
|
|
||||||
"start_time",
|
|
||||||
"text",
|
|
||||||
"type"
|
|
||||||
],
|
|
||||||
"values": [
|
|
||||||
[
|
|
||||||
1516920117000000000,
|
|
||||||
true,
|
|
||||||
"4ba9f836-20e8-4b8e-af51-e1363edd7b6d",
|
|
||||||
1517425994487495051,
|
|
||||||
0,
|
|
||||||
"",
|
|
||||||
""
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]}]`, nil), nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "two annotation response",
|
|
||||||
client: &mocks.TimeSeries{
|
|
||||||
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
|
|
||||||
return mocks.NewResponse(`[
|
|
||||||
{
|
|
||||||
"series": [
|
|
||||||
{
|
|
||||||
"name": "annotations",
|
|
||||||
"columns": [
|
|
||||||
"time",
|
|
||||||
"start_time",
|
|
||||||
"modified_time_ns",
|
|
||||||
"text",
|
|
||||||
"type",
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"values": [
|
|
||||||
[
|
|
||||||
1516920177345000000,
|
|
||||||
0,
|
|
||||||
1516989242129417403,
|
|
||||||
"mytext",
|
|
||||||
"mytype",
|
|
||||||
"ecf3a75d-f1c0-40e8-9790-902701467e92"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1516920177345000000,
|
|
||||||
0,
|
|
||||||
1517425914433539296,
|
|
||||||
"mytext2",
|
|
||||||
"mytype2",
|
|
||||||
"ea0aa94b-969a-4cd5-912a-5db61d502268"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]`, nil), nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: []chronograf.Annotation{
|
|
||||||
{
|
|
||||||
EndTime: time.Unix(0, 1516920177345000000),
|
|
||||||
StartTime: time.Unix(0, 0),
|
|
||||||
Text: "mytext2",
|
|
||||||
Type: "mytype2",
|
|
||||||
ID: "ea0aa94b-969a-4cd5-912a-5db61d502268",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
EndTime: time.Unix(0, 1516920177345000000),
|
|
||||||
StartTime: time.Unix(0, 0),
|
|
||||||
Text: "mytext",
|
|
||||||
Type: "mytype",
|
|
||||||
ID: "ecf3a75d-f1c0-40e8-9790-902701467e92",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "same id returns one",
|
|
||||||
client: &mocks.TimeSeries{
|
|
||||||
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
|
|
||||||
return mocks.NewResponse(`[
|
|
||||||
{
|
|
||||||
"series": [
|
|
||||||
{
|
|
||||||
"name": "annotations",
|
|
||||||
"columns": [
|
|
||||||
"time",
|
|
||||||
"start_time",
|
|
||||||
"modified_time_ns",
|
|
||||||
"text",
|
|
||||||
"type",
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"values": [
|
|
||||||
[
|
|
||||||
1516920177345000000,
|
|
||||||
0,
|
|
||||||
1516989242129417403,
|
|
||||||
"mytext",
|
|
||||||
"mytype",
|
|
||||||
"ea0aa94b-969a-4cd5-912a-5db61d502268"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1516920177345000000,
|
|
||||||
0,
|
|
||||||
1517425914433539296,
|
|
||||||
"mytext2",
|
|
||||||
"mytype2",
|
|
||||||
"ea0aa94b-969a-4cd5-912a-5db61d502268"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]`, nil), nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: []chronograf.Annotation{
|
|
||||||
{
|
|
||||||
EndTime: time.Unix(0, 1516920177345000000),
|
|
||||||
StartTime: time.Unix(0, 0),
|
|
||||||
Text: "mytext2",
|
|
||||||
Type: "mytype2",
|
|
||||||
ID: "ea0aa94b-969a-4cd5-912a-5db61d502268",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no responses returns empty array",
|
|
||||||
client: &mocks.TimeSeries{
|
|
||||||
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
|
|
||||||
return mocks.NewResponse(`[ { } ]`, nil), nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: []chronograf.Annotation{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
a := &AnnotationStore{
|
|
||||||
client: tt.client,
|
|
||||||
}
|
|
||||||
got, err := a.queryAnnotations(tt.args.ctx, tt.args.query)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("AnnotationStore.queryAnnotations() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("AnnotationStore.queryAnnotations() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAnnotationStore_Update(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
client chronograf.TimeSeries
|
|
||||||
now Now
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
ctx context.Context
|
|
||||||
anno *chronograf.Annotation
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "no responses returns error",
|
|
||||||
fields: fields{
|
|
||||||
client: &mocks.TimeSeries{
|
|
||||||
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
|
|
||||||
return mocks.NewResponse(`[ { } ]`, nil), nil
|
|
||||||
},
|
|
||||||
WriteF: func(context.Context, []chronograf.Point) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
ctx: context.Background(),
|
|
||||||
anno: &chronograf.Annotation{
|
|
||||||
ID: "1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "error writing returns error",
|
|
||||||
fields: fields{
|
|
||||||
now: func() time.Time { return time.Time{} },
|
|
||||||
client: &mocks.TimeSeries{
|
|
||||||
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
|
|
||||||
return mocks.NewResponse(`[
|
|
||||||
{
|
|
||||||
"series": [
|
|
||||||
{
|
|
||||||
"name": "annotations",
|
|
||||||
"columns": [
|
|
||||||
"time",
|
|
||||||
"start_time",
|
|
||||||
"modified_time_ns",
|
|
||||||
"text",
|
|
||||||
"type",
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"values": [
|
|
||||||
[
|
|
||||||
1516920177345000000,
|
|
||||||
0,
|
|
||||||
1516989242129417403,
|
|
||||||
"mytext",
|
|
||||||
"mytype",
|
|
||||||
"ecf3a75d-f1c0-40e8-9790-902701467e92"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1516920177345000000,
|
|
||||||
0,
|
|
||||||
1517425914433539296,
|
|
||||||
"mytext2",
|
|
||||||
"mytype2",
|
|
||||||
"ea0aa94b-969a-4cd5-912a-5db61d502268"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]`, nil), nil
|
|
||||||
},
|
|
||||||
WriteF: func(context.Context, []chronograf.Point) error {
|
|
||||||
return fmt.Errorf("error")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
ctx: context.Background(),
|
|
||||||
anno: &chronograf.Annotation{
|
|
||||||
ID: "1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Update with delete",
|
|
||||||
fields: fields{
|
|
||||||
now: func() time.Time { return time.Time{} },
|
|
||||||
client: &mocks.TimeSeries{
|
|
||||||
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
|
|
||||||
return mocks.NewResponse(`[
|
|
||||||
{
|
|
||||||
"series": [
|
|
||||||
{
|
|
||||||
"name": "annotations",
|
|
||||||
"columns": [
|
|
||||||
"time",
|
|
||||||
"start_time",
|
|
||||||
"modified_time_ns",
|
|
||||||
"text",
|
|
||||||
"type",
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"values": [
|
|
||||||
[
|
|
||||||
1516920177345000000,
|
|
||||||
0,
|
|
||||||
1516989242129417403,
|
|
||||||
"mytext",
|
|
||||||
"mytype",
|
|
||||||
"ecf3a75d-f1c0-40e8-9790-902701467e92"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]`, nil), nil
|
|
||||||
},
|
|
||||||
WriteF: func(context.Context, []chronograf.Point) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
ctx: context.Background(),
|
|
||||||
anno: &chronograf.Annotation{
|
|
||||||
ID: "1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Update with delete no delete",
|
|
||||||
fields: fields{
|
|
||||||
now: func() time.Time { return time.Time{} },
|
|
||||||
client: &mocks.TimeSeries{
|
|
||||||
QueryF: func(context.Context, chronograf.Query) (chronograf.Response, error) {
|
|
||||||
return mocks.NewResponse(`[
|
|
||||||
{
|
|
||||||
"series": [
|
|
||||||
{
|
|
||||||
"name": "annotations",
|
|
||||||
"columns": [
|
|
||||||
"time",
|
|
||||||
"start_time",
|
|
||||||
"modified_time_ns",
|
|
||||||
"text",
|
|
||||||
"type",
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"values": [
|
|
||||||
[
|
|
||||||
1516920177345000000,
|
|
||||||
0,
|
|
||||||
1516989242129417403,
|
|
||||||
"mytext",
|
|
||||||
"mytype",
|
|
||||||
"ecf3a75d-f1c0-40e8-9790-902701467e92"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]`, nil), nil
|
|
||||||
},
|
|
||||||
WriteF: func(context.Context, []chronograf.Point) error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
ctx: context.Background(),
|
|
||||||
anno: &chronograf.Annotation{
|
|
||||||
ID: "ecf3a75d-f1c0-40e8-9790-902701467e92",
|
|
||||||
EndTime: time.Unix(0, 1516920177345000000),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
a := &AnnotationStore{
|
|
||||||
client: tt.fields.client,
|
|
||||||
now: tt.fields.now,
|
|
||||||
}
|
|
||||||
if err := a.Update(tt.args.ctx, tt.args.anno); (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("AnnotationStore.Update() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,94 +0,0 @@
|
||||||
package influx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
jwt "github.com/golang-jwt/jwt"
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Authorizer adds optional authorization header to request
|
|
||||||
type Authorizer interface {
|
|
||||||
// Set may manipulate the request by adding the Authorization header
|
|
||||||
Set(req *http.Request) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// NoAuthorization does not add any authorization headers
|
|
||||||
type NoAuthorization struct{}
|
|
||||||
|
|
||||||
// Set does not add authorization
|
|
||||||
func (n *NoAuthorization) Set(req *http.Request) error { return nil }
|
|
||||||
|
|
||||||
// DefaultAuthorization creates either a shared JWT builder, basic auth or Noop
|
|
||||||
func DefaultAuthorization(src *chronograf.Source) Authorizer {
|
|
||||||
// Optionally, add the shared secret JWT token creation
|
|
||||||
if src.Username != "" && src.SharedSecret != "" {
|
|
||||||
return &BearerJWT{
|
|
||||||
Username: src.Username,
|
|
||||||
SharedSecret: src.SharedSecret,
|
|
||||||
}
|
|
||||||
} else if src.Username != "" && src.Password != "" {
|
|
||||||
return &BasicAuth{
|
|
||||||
Username: src.Username,
|
|
||||||
Password: src.Password,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &NoAuthorization{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BasicAuth adds Authorization: Basic to the request header
|
|
||||||
type BasicAuth struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set adds the basic auth headers to the request
|
|
||||||
func (b *BasicAuth) Set(r *http.Request) error {
|
|
||||||
r.SetBasicAuth(b.Username, b.Password)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BearerJWT is the default Bearer for InfluxDB
|
|
||||||
type BearerJWT struct {
|
|
||||||
Username string
|
|
||||||
SharedSecret string
|
|
||||||
Now Now
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set adds an Authorization Bearer to the request if has a shared secret
|
|
||||||
func (b *BearerJWT) Set(r *http.Request) error {
|
|
||||||
if b.SharedSecret != "" && b.Username != "" {
|
|
||||||
token, err := b.Token(b.Username)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to create token")
|
|
||||||
}
|
|
||||||
r.Header.Set("Authorization", "Bearer "+token)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Token returns the expected InfluxDB JWT signed with the sharedSecret
|
|
||||||
func (b *BearerJWT) Token(username string) (string, error) {
|
|
||||||
if b.Now == nil {
|
|
||||||
b.Now = time.Now
|
|
||||||
}
|
|
||||||
return JWT(username, b.SharedSecret, b.Now)
|
|
||||||
}
|
|
||||||
|
|
||||||
// JWT returns a token string accepted by InfluxDB using the sharedSecret as an Authorization: Bearer header
|
|
||||||
func JWT(username, sharedSecret string, now Now) (string, error) {
|
|
||||||
token := &jwt.Token{
|
|
||||||
Header: map[string]interface{}{
|
|
||||||
"typ": "JWT",
|
|
||||||
"alg": jwt.SigningMethodHS512.Alg(),
|
|
||||||
},
|
|
||||||
Claims: jwt.MapClaims{
|
|
||||||
"username": username,
|
|
||||||
"exp": now().Add(time.Minute).Unix(),
|
|
||||||
},
|
|
||||||
Method: jwt.SigningMethodHS512,
|
|
||||||
}
|
|
||||||
return token.SignedString([]byte(sharedSecret))
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
package influx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestJWT(t *testing.T) {
|
|
||||||
type args struct {
|
|
||||||
username string
|
|
||||||
sharedSecret string
|
|
||||||
now Now
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "",
|
|
||||||
args: args{
|
|
||||||
username: "AzureDiamond",
|
|
||||||
sharedSecret: "hunter2",
|
|
||||||
now: func() time.Time {
|
|
||||||
return time.Unix(0, 0)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHAiOjYwLCJ1c2VybmFtZSI6IkF6dXJlRGlhbW9uZCJ9.kUWGwcpCPwV7MEk7luO1rt8036LyvG4bRL_CfseQGmz4b0S34gATx30g4xvqVAV6bwwYE0YU3P8FjG8ij4kc5g",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := JWT(tt.args.username, tt.args.sharedSecret, tt.args.now)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("JWT() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if got != tt.want {
|
|
||||||
t.Errorf("JWT() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,269 +0,0 @@
|
||||||
package influx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf"
|
|
||||||
"github.com/influxdata/influxdb/v2/kit/tracing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AllDB returns all databases from within Influx
|
|
||||||
func (c *Client) AllDB(ctx context.Context) ([]chronograf.Database, error) {
|
|
||||||
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
return c.showDatabases(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateDB creates a database within Influx
|
|
||||||
func (c *Client) CreateDB(ctx context.Context, db *chronograf.Database) (*chronograf.Database, error) {
|
|
||||||
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
_, err := c.Query(ctx, chronograf.Query{
|
|
||||||
Command: fmt.Sprintf(`CREATE DATABASE "%s"`, db.Name),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res := &chronograf.Database{Name: db.Name}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DropDB drops a database within Influx
|
|
||||||
func (c *Client) DropDB(ctx context.Context, db string) error {
|
|
||||||
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
_, err := c.Query(ctx, chronograf.Query{
|
|
||||||
Command: fmt.Sprintf(`DROP DATABASE "%s"`, db),
|
|
||||||
DB: db,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AllRP returns all the retention policies for a specific database
|
|
||||||
func (c *Client) AllRP(ctx context.Context, db string) ([]chronograf.RetentionPolicy, error) {
|
|
||||||
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
return c.showRetentionPolicies(ctx, db)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) getRP(ctx context.Context, db, rp string) (chronograf.RetentionPolicy, error) {
|
|
||||||
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
rs, err := c.AllRP(ctx, db)
|
|
||||||
if err != nil {
|
|
||||||
return chronograf.RetentionPolicy{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range rs {
|
|
||||||
if r.Name == rp {
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return chronograf.RetentionPolicy{}, fmt.Errorf("unknown retention policy")
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateRP creates a retention policy for a specific database
|
|
||||||
func (c *Client) CreateRP(ctx context.Context, db string, rp *chronograf.RetentionPolicy) (*chronograf.RetentionPolicy, error) {
|
|
||||||
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if rp.Default {
|
|
||||||
query = fmt.Sprintf(`%s DEFAULT`, query)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := c.Query(ctx, chronograf.Query{
|
|
||||||
Command: query,
|
|
||||||
DB: db,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := c.getRP(ctx, db, rp.Name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
buffer.WriteString(fmt.Sprintf(`ALTER RETENTION POLICY "%s" ON "%s"`, rp, db))
|
|
||||||
if len(upd.Duration) > 0 {
|
|
||||||
buffer.WriteString(" DURATION " + upd.Duration)
|
|
||||||
}
|
|
||||||
if upd.Replication > 0 {
|
|
||||||
buffer.WriteString(" REPLICATION " + fmt.Sprint(upd.Replication))
|
|
||||||
}
|
|
||||||
if len(upd.ShardDuration) > 0 {
|
|
||||||
buffer.WriteString(" SHARD DURATION " + upd.ShardDuration)
|
|
||||||
}
|
|
||||||
if upd.Default {
|
|
||||||
buffer.WriteString(" DEFAULT")
|
|
||||||
}
|
|
||||||
queryRes, err := c.Query(ctx, chronograf.Query{
|
|
||||||
Command: buffer.String(),
|
|
||||||
DB: db,
|
|
||||||
RP: rp,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// The ALTER RETENTION POLICIES statements puts the error within the results itself
|
|
||||||
// So, we have to crack open the results to see what happens
|
|
||||||
octets, err := queryRes.MarshalJSON()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
results := make([]struct{ Error string }, 0)
|
|
||||||
if err := json.Unmarshal(octets, &results); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// At last, we can check if there are any error strings
|
|
||||||
for _, r := range results {
|
|
||||||
if r.Error != "" {
|
|
||||||
return nil, fmt.Errorf(r.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := c.getRP(ctx, db, upd.Name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DropRP removes a specific retention policy for a specific database
|
|
||||||
func (c *Client) DropRP(ctx context.Context, db string, rp string) error {
|
|
||||||
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
_, err := c.Query(ctx, chronograf.Query{
|
|
||||||
Command: fmt.Sprintf(`DROP RETENTION POLICY "%s" ON "%s"`, rp, db),
|
|
||||||
DB: db,
|
|
||||||
RP: rp,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMeasurements returns measurements in a specified database, paginated by
|
|
||||||
// optional limit and offset. If no limit or offset is provided, it defaults to
|
|
||||||
// a limit of 100 measurements with no offset.
|
|
||||||
func (c *Client) GetMeasurements(ctx context.Context, db string, limit, offset int) ([]chronograf.Measurement, error) {
|
|
||||||
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
return c.showMeasurements(ctx, db, limit, offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) showDatabases(ctx context.Context) ([]chronograf.Database, error) {
|
|
||||||
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
res, err := c.Query(ctx, chronograf.Query{
|
|
||||||
Command: `SHOW DATABASES`,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
octets, err := res.MarshalJSON()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
results := showResults{}
|
|
||||||
if err := json.Unmarshal(octets, &results); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return results.Databases(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) showRetentionPolicies(ctx context.Context, db string) ([]chronograf.RetentionPolicy, error) {
|
|
||||||
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
retentionPolicies, err := c.Query(ctx, chronograf.Query{
|
|
||||||
Command: fmt.Sprintf(`SHOW RETENTION POLICIES ON "%s"`, db),
|
|
||||||
DB: db,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
octets, err := retentionPolicies.MarshalJSON()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
results := showResults{}
|
|
||||||
if err := json.Unmarshal(octets, &results); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return results.RetentionPolicies(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) showMeasurements(ctx context.Context, db string, limit, offset int) ([]chronograf.Measurement, error) {
|
|
||||||
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
show := fmt.Sprintf(`SHOW MEASUREMENTS ON "%s"`, db)
|
|
||||||
if limit > 0 {
|
|
||||||
show += fmt.Sprintf(" LIMIT %d", limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
if offset > 0 {
|
|
||||||
show += fmt.Sprintf(" OFFSET %d", offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
measurements, err := c.Query(ctx, chronograf.Query{
|
|
||||||
Command: show,
|
|
||||||
DB: db,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
octets, err := measurements.MarshalJSON()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
results := showResults{}
|
|
||||||
if err := json.Unmarshal(octets, &results); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return results.Measurements(), nil
|
|
||||||
}
|
|
|
@ -1,388 +0,0 @@
|
||||||
package influx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf"
|
|
||||||
"github.com/influxdata/influxdb/v2/kit/tracing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ chronograf.TimeSeries = &Client{}
|
|
||||||
var _ chronograf.TSDBStatus = &Client{}
|
|
||||||
var _ chronograf.Databases = &Client{}
|
|
||||||
|
|
||||||
// Shared transports for all clients to prevent leaking connections
|
|
||||||
var (
|
|
||||||
skipVerifyTransport = &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
||||||
}
|
|
||||||
defaultTransport = &http.Transport{}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Client is a device for retrieving time series data from an InfluxDB instance
|
|
||||||
type Client struct {
|
|
||||||
URL *url.URL
|
|
||||||
Authorizer Authorizer
|
|
||||||
InsecureSkipVerify bool
|
|
||||||
Logger chronograf.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// Response is a partial JSON decoded InfluxQL response used
|
|
||||||
// to check for some errors
|
|
||||||
type Response struct {
|
|
||||||
Results json.RawMessage
|
|
||||||
Err string `json:"error,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON returns the raw results bytes from the response
|
|
||||||
func (r Response) MarshalJSON() ([]byte, error) {
|
|
||||||
return r.Results, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) query(ctx context.Context, u *url.URL, q chronograf.Query) (chronograf.Response, error) {
|
|
||||||
span, _ := tracing.StartSpanFromContext(ctx)
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
u.Path = "query"
|
|
||||||
req, err := http.NewRequest("POST", u.String(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
command := q.Command
|
|
||||||
logs := c.Logger.
|
|
||||||
WithField("component", "proxy").
|
|
||||||
WithField("host", req.Host).
|
|
||||||
WithField("command", command).
|
|
||||||
WithField("db", q.DB).
|
|
||||||
WithField("rp", q.RP)
|
|
||||||
logs.Debug("query")
|
|
||||||
|
|
||||||
params := req.URL.Query()
|
|
||||||
params.Set("q", command)
|
|
||||||
params.Set("db", q.DB)
|
|
||||||
params.Set("rp", q.RP)
|
|
||||||
params.Set("epoch", "ms")
|
|
||||||
if q.Epoch != "" {
|
|
||||||
params.Set("epoch", q.Epoch)
|
|
||||||
}
|
|
||||||
req.URL.RawQuery = params.Encode()
|
|
||||||
tracing.InjectToHTTPRequest(span, req)
|
|
||||||
|
|
||||||
if c.Authorizer != nil {
|
|
||||||
if err := c.Authorizer.Set(req); err != nil {
|
|
||||||
logs.Error("Error setting authorization header ", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hc := &http.Client{}
|
|
||||||
if c.InsecureSkipVerify {
|
|
||||||
hc.Transport = skipVerifyTransport
|
|
||||||
} else {
|
|
||||||
hc.Transport = defaultTransport
|
|
||||||
}
|
|
||||||
resp, err := hc.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
var response Response
|
|
||||||
dec := json.NewDecoder(resp.Body)
|
|
||||||
decErr := dec.Decode(&response)
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("received status code %d from server: err: %s", resp.StatusCode, response.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore this error if we got an invalid status code
|
|
||||||
if decErr != nil && decErr.Error() == "EOF" && resp.StatusCode != http.StatusOK {
|
|
||||||
decErr = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we got a valid decode error, send that back
|
|
||||||
if decErr != nil {
|
|
||||||
logs.WithField("influx_status", resp.StatusCode).
|
|
||||||
Error("Error parsing results from influxdb: err:", decErr)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
type result struct {
|
|
||||||
Response chronograf.Response
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query issues a request to a configured InfluxDB instance for time series
|
|
||||||
// information specified by query. Queries must be "fully-qualified," and
|
|
||||||
// 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) {
|
|
||||||
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
resps := make(chan (result))
|
|
||||||
go func() {
|
|
||||||
resp, err := c.query(ctx, c.URL, q)
|
|
||||||
resps <- result{resp, err}
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case resp := <-resps:
|
|
||||||
return resp.Response, resp.Err
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, chronograf.ErrUpstreamTimeout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect caches the URL and optional Bearer Authorization for the data source
|
|
||||||
func (c *Client) Connect(ctx context.Context, src *chronograf.Source) error {
|
|
||||||
span, _ := tracing.StartSpanFromContext(ctx)
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
u, err := url.Parse(src.URL)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.Authorizer = DefaultAuthorization(src)
|
|
||||||
// Only allow acceptance of all certs if the scheme is https AND the user opted into to the setting.
|
|
||||||
if u.Scheme == "https" && src.InsecureSkipVerify {
|
|
||||||
c.InsecureSkipVerify = src.InsecureSkipVerify
|
|
||||||
}
|
|
||||||
|
|
||||||
c.URL = u
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Users transforms InfluxDB into a user store
|
|
||||||
func (c *Client) Users(ctx context.Context) chronograf.UsersStore {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Roles aren't support in OSS
|
|
||||||
func (c *Client) Roles(ctx context.Context) (chronograf.RolesStore, error) {
|
|
||||||
return nil, fmt.Errorf("roles not support in open-source InfluxDB. Roles are support in Influx Enterprise")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ping hits the influxdb ping endpoint and returns the type of influx
|
|
||||||
func (c *Client) Ping(ctx context.Context) error {
|
|
||||||
_, _, err := c.pingTimeout(ctx)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Version hits the influxdb ping endpoint and returns the version of influx
|
|
||||||
func (c *Client) Version(ctx context.Context) (string, error) {
|
|
||||||
version, _, err := c.pingTimeout(ctx)
|
|
||||||
return version, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type hits the influxdb ping endpoint and returns the type of influx running
|
|
||||||
func (c *Client) Type(ctx context.Context) (string, error) {
|
|
||||||
_, tsdbType, err := c.pingTimeout(ctx)
|
|
||||||
return tsdbType, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) pingTimeout(ctx context.Context) (string, string, error) {
|
|
||||||
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
resps := make(chan (pingResult))
|
|
||||||
go func() {
|
|
||||||
version, tsdbType, err := c.ping(ctx, c.URL)
|
|
||||||
resps <- pingResult{version, tsdbType, err}
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case resp := <-resps:
|
|
||||||
return resp.Version, resp.Type, resp.Err
|
|
||||||
case <-ctx.Done():
|
|
||||||
return "", "", chronograf.ErrUpstreamTimeout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type pingResult struct {
|
|
||||||
Version string
|
|
||||||
Type string
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) ping(ctx context.Context, u *url.URL) (string, string, error) {
|
|
||||||
span, _ := tracing.StartSpanFromContext(ctx)
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
u.Path = "ping"
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", u.String(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
tracing.InjectToHTTPRequest(span, req)
|
|
||||||
|
|
||||||
hc := &http.Client{}
|
|
||||||
if c.InsecureSkipVerify {
|
|
||||||
hc.Transport = skipVerifyTransport
|
|
||||||
} else {
|
|
||||||
hc.Transport = defaultTransport
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := hc.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusNoContent {
|
|
||||||
var err = fmt.Errorf(string(body))
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
version := resp.Header.Get("X-Influxdb-Build")
|
|
||||||
if version == "ENT" {
|
|
||||||
return version, chronograf.InfluxEnterprise, nil
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
return version, chronograf.InfluxDB, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write POSTs line protocol to a database and retention policy
|
|
||||||
func (c *Client) Write(ctx context.Context, points []chronograf.Point) error {
|
|
||||||
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
for _, point := range points {
|
|
||||||
if err := c.writePoint(ctx, &point); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) writePoint(ctx context.Context, point *chronograf.Point) error {
|
|
||||||
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
lp, err := toLineProtocol(point)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.write(ctx, c.URL, point.Database, point.RetentionPolicy, lp)
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some influxdb errors should not be treated as errors
|
|
||||||
if strings.Contains(err.Error(), "hinted handoff queue not empty") {
|
|
||||||
// This is an informational message
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the database was not found, try to recreate it:
|
|
||||||
if strings.Contains(err.Error(), "database not found") {
|
|
||||||
_, err = c.CreateDB(ctx, &chronograf.Database{
|
|
||||||
Name: point.Database,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// retry the write
|
|
||||||
return c.write(ctx, c.URL, point.Database, point.RetentionPolicy, lp)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) write(ctx context.Context, u *url.URL, db, rp, lp string) error {
|
|
||||||
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
u.Path = "write"
|
|
||||||
req, err := http.NewRequest("POST", u.String(), strings.NewReader(lp))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "text/plain; charset=utf-8")
|
|
||||||
if c.Authorizer != nil {
|
|
||||||
if err := c.Authorizer.Set(req); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
params := req.URL.Query()
|
|
||||||
params.Set("db", db)
|
|
||||||
params.Set("rp", rp)
|
|
||||||
req.URL.RawQuery = params.Encode()
|
|
||||||
tracing.InjectToHTTPRequest(span, req)
|
|
||||||
|
|
||||||
hc := &http.Client{}
|
|
||||||
if c.InsecureSkipVerify {
|
|
||||||
hc.Transport = skipVerifyTransport
|
|
||||||
} else {
|
|
||||||
hc.Transport = defaultTransport
|
|
||||||
}
|
|
||||||
|
|
||||||
errChan := make(chan (error))
|
|
||||||
go func() {
|
|
||||||
resp, err := hc.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
errChan <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode == http.StatusNoContent {
|
|
||||||
errChan <- nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var response Response
|
|
||||||
dec := json.NewDecoder(resp.Body)
|
|
||||||
err = dec.Decode(&response)
|
|
||||||
if err != nil && err.Error() != "EOF" {
|
|
||||||
errChan <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
errChan <- errors.New(response.Err)
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case err := <-errChan:
|
|
||||||
return err
|
|
||||||
case <-ctx.Done():
|
|
||||||
return chronograf.ErrUpstreamTimeout
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,538 +0,0 @@
|
||||||
package influx_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
gojwt "github.com/golang-jwt/jwt"
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf"
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf/influx"
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf/mocks"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewClient initializes an HTTP Client for InfluxDB.
|
|
||||||
func NewClient(host string, lg chronograf.Logger) (*influx.Client, error) {
|
|
||||||
l := lg.WithField("host", host)
|
|
||||||
u, err := url.Parse(host)
|
|
||||||
if err != nil {
|
|
||||||
l.Error("Error initialize influx client: err:", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &influx.Client{
|
|
||||||
URL: u,
|
|
||||||
Logger: l,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_Influx_MakesRequestsToQueryEndpoint(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
called := false
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
rw.WriteHeader(http.StatusOK)
|
|
||||||
rw.Write([]byte(`{}`))
|
|
||||||
called = true
|
|
||||||
if path := r.URL.Path; path != "/query" {
|
|
||||||
t.Error("Expected the path to contain `/query` but was", path)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
var series chronograf.TimeSeries
|
|
||||||
series, err := NewClient(ts.URL, &chronograf.NoopLogger{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unexpected error initializing client: err:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
query := chronograf.Query{
|
|
||||||
Command: "show databases",
|
|
||||||
}
|
|
||||||
_, err = series.Query(context.Background(), query)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Expected no error but was", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !called {
|
|
||||||
t.Error("Expected http request to Influx but there was none")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type MockAuthorization struct {
|
|
||||||
Bearer string
|
|
||||||
Error error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockAuthorization) Set(req *http.Request) error {
|
|
||||||
return m.Error
|
|
||||||
}
|
|
||||||
func Test_Influx_AuthorizationBearer(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
rw.WriteHeader(http.StatusOK)
|
|
||||||
rw.Write([]byte(`{}`))
|
|
||||||
auth := r.Header.Get("Authorization")
|
|
||||||
tokenString := strings.Split(auth, " ")[1]
|
|
||||||
token, err := gojwt.Parse(tokenString, func(token *gojwt.Token) (interface{}, error) {
|
|
||||||
if _, ok := token.Method.(*gojwt.SigningMethodHMAC); !ok {
|
|
||||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
|
||||||
}
|
|
||||||
return []byte("42"), nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Invalid token %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if claims, ok := token.Claims.(gojwt.MapClaims); ok && token.Valid {
|
|
||||||
got := claims["username"]
|
|
||||||
want := "AzureDiamond"
|
|
||||||
if got != want {
|
|
||||||
t.Errorf("Test_Influx_AuthorizationBearer got %s want %s", got, want)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Errorf("Invalid token %v", token)
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
src := &chronograf.Source{
|
|
||||||
Username: "AzureDiamond",
|
|
||||||
URL: ts.URL,
|
|
||||||
SharedSecret: "42",
|
|
||||||
}
|
|
||||||
series := &influx.Client{
|
|
||||||
Logger: &chronograf.NoopLogger{},
|
|
||||||
}
|
|
||||||
series.Connect(context.Background(), src)
|
|
||||||
|
|
||||||
query := chronograf.Query{
|
|
||||||
Command: "show databases",
|
|
||||||
}
|
|
||||||
_, err := series.Query(context.Background(), query)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Expected no error but was", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_Influx_AuthorizationBearerCtx(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
rw.WriteHeader(http.StatusOK)
|
|
||||||
rw.Write([]byte(`{}`))
|
|
||||||
got := r.Header.Get("Authorization")
|
|
||||||
if got == "" {
|
|
||||||
t.Error("Test_Influx_AuthorizationBearerCtx got empty string")
|
|
||||||
}
|
|
||||||
incomingToken := strings.Split(got, " ")[1]
|
|
||||||
|
|
||||||
alg := func(token *gojwt.Token) (interface{}, error) {
|
|
||||||
if _, ok := token.Method.(*gojwt.SigningMethodHMAC); !ok {
|
|
||||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
|
||||||
}
|
|
||||||
return []byte("hunter2"), nil
|
|
||||||
}
|
|
||||||
claims := &gojwt.MapClaims{}
|
|
||||||
token, err := gojwt.ParseWithClaims(string(incomingToken), claims, alg)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Test_Influx_AuthorizationBearerCtx unexpected claims error %v", err)
|
|
||||||
}
|
|
||||||
if !token.Valid {
|
|
||||||
t.Error("Test_Influx_AuthorizationBearerCtx unexpected valid claim")
|
|
||||||
}
|
|
||||||
if err := claims.Valid(); err != nil {
|
|
||||||
t.Errorf("Test_Influx_AuthorizationBearerCtx not expires already %v", err)
|
|
||||||
}
|
|
||||||
user := (*claims)["username"].(string)
|
|
||||||
if user != "AzureDiamond" {
|
|
||||||
t.Errorf("Test_Influx_AuthorizationBearerCtx expected username AzureDiamond but got %s", user)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
series := &influx.Client{
|
|
||||||
Logger: &chronograf.NoopLogger{},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := series.Connect(context.Background(), &chronograf.Source{
|
|
||||||
Username: "AzureDiamond",
|
|
||||||
SharedSecret: "hunter2",
|
|
||||||
URL: ts.URL,
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
query := chronograf.Query{
|
|
||||||
Command: "show databases",
|
|
||||||
}
|
|
||||||
_, err = series.Query(context.Background(), query)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Expected no error but was", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_Influx_AuthorizationBearerFailure(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
bearer := &MockAuthorization{
|
|
||||||
Error: fmt.Errorf("cracked1337"),
|
|
||||||
}
|
|
||||||
|
|
||||||
u, _ := url.Parse("http://haxored.net")
|
|
||||||
u.User = url.UserPassword("AzureDiamond", "hunter2")
|
|
||||||
series := &influx.Client{
|
|
||||||
URL: u,
|
|
||||||
Authorizer: bearer,
|
|
||||||
Logger: &chronograf.NoopLogger{},
|
|
||||||
}
|
|
||||||
|
|
||||||
query := chronograf.Query{
|
|
||||||
Command: "show databases",
|
|
||||||
}
|
|
||||||
_, err := series.Query(context.Background(), query)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Test_Influx_AuthorizationBearerFailure Expected error but received nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_Influx_HTTPS_Failure(t *testing.T) {
|
|
||||||
called := false
|
|
||||||
ts := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
called = true
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
var series chronograf.TimeSeries
|
|
||||||
series, err := NewClient(ts.URL, &chronograf.NoopLogger{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unexpected error initializing client: err:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
src := chronograf.Source{
|
|
||||||
URL: ts.URL,
|
|
||||||
}
|
|
||||||
if err := series.Connect(ctx, &src); err != nil {
|
|
||||||
t.Fatal("Unexpected error connecting to client: err:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
query := chronograf.Query{
|
|
||||||
Command: "show databases",
|
|
||||||
}
|
|
||||||
_, err = series.Query(ctx, query)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error but was successful")
|
|
||||||
}
|
|
||||||
|
|
||||||
if called {
|
|
||||||
t.Error("Expected http request to fail, but, succeeded")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_Influx_HTTPS_InsecureSkipVerify(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
called := false
|
|
||||||
q := ""
|
|
||||||
ts := httptest.NewTLSServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
rw.WriteHeader(http.StatusOK)
|
|
||||||
rw.Write([]byte(`{}`))
|
|
||||||
called = true
|
|
||||||
if path := r.URL.Path; path != "/query" {
|
|
||||||
t.Error("Expected the path to contain `/query` but was", path)
|
|
||||||
}
|
|
||||||
values := r.URL.Query()
|
|
||||||
q = values.Get("q")
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
var series chronograf.TimeSeries
|
|
||||||
series, err := NewClient(ts.URL, &chronograf.NoopLogger{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unexpected error initializing client: err:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
src := chronograf.Source{
|
|
||||||
URL: ts.URL,
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
}
|
|
||||||
if err := series.Connect(ctx, &src); err != nil {
|
|
||||||
t.Fatal("Unexpected error connecting to client: err:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
query := chronograf.Query{
|
|
||||||
Command: "show databases",
|
|
||||||
}
|
|
||||||
_, err = series.Query(ctx, query)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Expected no error but was", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !called {
|
|
||||||
t.Error("Expected http request to Influx but there was none")
|
|
||||||
}
|
|
||||||
called = false
|
|
||||||
q = ""
|
|
||||||
query = chronograf.Query{
|
|
||||||
Command: `select "usage_user" from cpu`,
|
|
||||||
}
|
|
||||||
_, err = series.Query(ctx, query)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Expected no error but was", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !called {
|
|
||||||
t.Error("Expected http request to Influx but there was none")
|
|
||||||
}
|
|
||||||
|
|
||||||
if q != `select "usage_user" from cpu` {
|
|
||||||
t.Errorf("Unexpected query: %s", q)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_Influx_CancelsInFlightRequests(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
started := make(chan bool, 1)
|
|
||||||
finished := make(chan bool, 1)
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
started <- true
|
|
||||||
time.Sleep(20 * time.Millisecond)
|
|
||||||
finished <- true
|
|
||||||
}))
|
|
||||||
defer func() {
|
|
||||||
ts.CloseClientConnections()
|
|
||||||
ts.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
series, _ := NewClient(ts.URL, &chronograf.NoopLogger{})
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
errs := make(chan (error))
|
|
||||||
go func() {
|
|
||||||
query := chronograf.Query{
|
|
||||||
Command: "show databases",
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := series.Query(ctx, query)
|
|
||||||
errs <- err
|
|
||||||
}()
|
|
||||||
|
|
||||||
timer := time.NewTimer(10 * time.Second)
|
|
||||||
defer timer.Stop()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case s := <-started:
|
|
||||||
if !s {
|
|
||||||
t.Errorf("Expected cancellation during request processing. Started: %t", s)
|
|
||||||
}
|
|
||||||
case <-timer.C:
|
|
||||||
t.Fatalf("Expected server to finish")
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case f := <-finished:
|
|
||||||
if !f {
|
|
||||||
t.Errorf("Expected cancellation during request processing. Finished: %t", f)
|
|
||||||
}
|
|
||||||
case <-timer.C:
|
|
||||||
t.Fatalf("Expected server to finish")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := <-errs
|
|
||||||
if err != chronograf.ErrUpstreamTimeout {
|
|
||||||
t.Error("Expected timeout error but wasn't. err was", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_Influx_RejectsInvalidHosts(t *testing.T) {
|
|
||||||
_, err := NewClient(":", &chronograf.NoopLogger{})
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Expected err but was nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_Influx_ReportsInfluxErrs(t *testing.T) {
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
rw.WriteHeader(http.StatusOK)
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
cl, err := NewClient(ts.URL, &chronograf.NoopLogger{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Encountered unexpected error while initializing influx client: err:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = cl.Query(context.Background(), chronograf.Query{
|
|
||||||
Command: "show shards",
|
|
||||||
DB: "_internal",
|
|
||||||
RP: "autogen",
|
|
||||||
})
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Expected an error but received none")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClient_Roles(t *testing.T) {
|
|
||||||
c := &influx.Client{}
|
|
||||||
_, err := c.Roles(context.Background())
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Client.Roles() want error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestClient_write(t *testing.T) {
|
|
||||||
type fields struct {
|
|
||||||
Authorizer influx.Authorizer
|
|
||||||
InsecureSkipVerify bool
|
|
||||||
Logger chronograf.Logger
|
|
||||||
}
|
|
||||||
type args struct {
|
|
||||||
ctx context.Context
|
|
||||||
point chronograf.Point
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
fields fields
|
|
||||||
args args
|
|
||||||
body string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "write point to influxdb",
|
|
||||||
fields: fields{
|
|
||||||
Logger: mocks.NewLogger(),
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
ctx: context.Background(),
|
|
||||||
point: chronograf.Point{
|
|
||||||
Database: "mydb",
|
|
||||||
RetentionPolicy: "myrp",
|
|
||||||
Measurement: "mymeas",
|
|
||||||
Time: 10,
|
|
||||||
Tags: map[string]string{
|
|
||||||
"tag1": "value1",
|
|
||||||
"tag2": "value2",
|
|
||||||
},
|
|
||||||
Fields: map[string]interface{}{
|
|
||||||
"field1": "value1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "point without fields",
|
|
||||||
args: args{
|
|
||||||
ctx: context.Background(),
|
|
||||||
point: chronograf.Point{},
|
|
||||||
},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "hinted handoff errors are not errors really.",
|
|
||||||
fields: fields{
|
|
||||||
Logger: mocks.NewLogger(),
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
ctx: context.Background(),
|
|
||||||
point: chronograf.Point{
|
|
||||||
Database: "mydb",
|
|
||||||
RetentionPolicy: "myrp",
|
|
||||||
Measurement: "mymeas",
|
|
||||||
Time: 10,
|
|
||||||
Tags: map[string]string{
|
|
||||||
"tag1": "value1",
|
|
||||||
"tag2": "value2",
|
|
||||||
},
|
|
||||||
Fields: map[string]interface{}{
|
|
||||||
"field1": "value1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
body: `{"error":"hinted handoff queue not empty"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "database not found creates a new db",
|
|
||||||
fields: fields{
|
|
||||||
Logger: mocks.NewLogger(),
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
ctx: context.Background(),
|
|
||||||
point: chronograf.Point{
|
|
||||||
Database: "mydb",
|
|
||||||
RetentionPolicy: "myrp",
|
|
||||||
Measurement: "mymeas",
|
|
||||||
Time: 10,
|
|
||||||
Tags: map[string]string{
|
|
||||||
"tag1": "value1",
|
|
||||||
"tag2": "value2",
|
|
||||||
},
|
|
||||||
Fields: map[string]interface{}{
|
|
||||||
"field1": "value1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
body: `{"error":"database not found"}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "error from database reported",
|
|
||||||
fields: fields{
|
|
||||||
Logger: mocks.NewLogger(),
|
|
||||||
},
|
|
||||||
args: args{
|
|
||||||
ctx: context.Background(),
|
|
||||||
point: chronograf.Point{
|
|
||||||
Database: "mydb",
|
|
||||||
RetentionPolicy: "myrp",
|
|
||||||
Measurement: "mymeas",
|
|
||||||
Time: 10,
|
|
||||||
Tags: map[string]string{
|
|
||||||
"tag1": "value1",
|
|
||||||
"tag2": "value2",
|
|
||||||
},
|
|
||||||
Fields: map[string]interface{}{
|
|
||||||
"field1": "value1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
body: `{"error":"oh no!"}`,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
retry := 0 // if the retry is > 0 then we don't error
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
if strings.HasPrefix(r.RequestURI, "/write") {
|
|
||||||
if tt.body == "" || retry > 0 {
|
|
||||||
rw.WriteHeader(http.StatusNoContent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
retry++
|
|
||||||
rw.WriteHeader(http.StatusBadRequest)
|
|
||||||
rw.Write([]byte(tt.body))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rw.WriteHeader(http.StatusOK)
|
|
||||||
rw.Write([]byte(`{"results":[{}]}`))
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
u, _ := url.Parse(ts.URL)
|
|
||||||
c := &influx.Client{
|
|
||||||
URL: u,
|
|
||||||
Authorizer: tt.fields.Authorizer,
|
|
||||||
InsecureSkipVerify: tt.fields.InsecureSkipVerify,
|
|
||||||
Logger: tt.fields.Logger,
|
|
||||||
}
|
|
||||||
if err := c.Write(tt.args.ctx, []chronograf.Point{tt.args.point}); (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("Client.write() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,84 +0,0 @@
|
||||||
package influx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
escapeMeasurement = strings.NewReplacer(
|
|
||||||
`,` /* to */, `\,`,
|
|
||||||
` ` /* to */, `\ `,
|
|
||||||
)
|
|
||||||
escapeKeys = strings.NewReplacer(
|
|
||||||
`,` /* to */, `\,`,
|
|
||||||
`"` /* to */, `\"`,
|
|
||||||
` ` /* to */, `\ `,
|
|
||||||
`=` /* to */, `\=`,
|
|
||||||
)
|
|
||||||
escapeTagValues = strings.NewReplacer(
|
|
||||||
`,` /* to */, `\,`,
|
|
||||||
`"` /* to */, `\"`,
|
|
||||||
` ` /* to */, `\ `,
|
|
||||||
`=` /* to */, `\=`,
|
|
||||||
)
|
|
||||||
escapeFieldStrings = strings.NewReplacer(
|
|
||||||
`"` /* to */, `\"`,
|
|
||||||
`\` /* to */, `\\`,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
func toLineProtocol(point *chronograf.Point) (string, error) {
|
|
||||||
measurement := escapeMeasurement.Replace(point.Measurement)
|
|
||||||
if len(measurement) == 0 {
|
|
||||||
return "", fmt.Errorf("measurement required to write point")
|
|
||||||
}
|
|
||||||
if len(point.Fields) == 0 {
|
|
||||||
return "", fmt.Errorf("at least one field required to write point")
|
|
||||||
}
|
|
||||||
|
|
||||||
tags := []string{}
|
|
||||||
for tag, value := range point.Tags {
|
|
||||||
if value != "" {
|
|
||||||
t := fmt.Sprintf("%s=%s", escapeKeys.Replace(tag), escapeTagValues.Replace(value))
|
|
||||||
tags = append(tags, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// it is faster to insert data into influx db if the tags are sorted
|
|
||||||
sort.Strings(tags)
|
|
||||||
|
|
||||||
fields := []string{}
|
|
||||||
for field, value := range point.Fields {
|
|
||||||
var format string
|
|
||||||
switch v := value.(type) {
|
|
||||||
case int64, int32, int16, int8, int:
|
|
||||||
format = fmt.Sprintf("%s=%di", escapeKeys.Replace(field), v)
|
|
||||||
case uint64, uint32, uint16, uint8, uint:
|
|
||||||
format = fmt.Sprintf("%s=%du", escapeKeys.Replace(field), v)
|
|
||||||
case float64, float32:
|
|
||||||
format = fmt.Sprintf("%s=%f", escapeKeys.Replace(field), v)
|
|
||||||
case string:
|
|
||||||
format = fmt.Sprintf(`%s="%s"`, escapeKeys.Replace(field), escapeFieldStrings.Replace(v))
|
|
||||||
case bool:
|
|
||||||
format = fmt.Sprintf("%s=%t", escapeKeys.Replace(field), v)
|
|
||||||
}
|
|
||||||
if format != "" {
|
|
||||||
fields = append(fields, format)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Strings(fields)
|
|
||||||
|
|
||||||
lp := measurement
|
|
||||||
if len(tags) > 0 {
|
|
||||||
lp += fmt.Sprintf(",%s", strings.Join(tags, ","))
|
|
||||||
}
|
|
||||||
|
|
||||||
lp += fmt.Sprintf(" %s", strings.Join(fields, ","))
|
|
||||||
if point.Time != 0 {
|
|
||||||
lp += fmt.Sprintf(" %d", point.Time)
|
|
||||||
}
|
|
||||||
return lp, nil
|
|
||||||
}
|
|
|
@ -1,129 +0,0 @@
|
||||||
package influx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_toLineProtocol(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
point *chronograf.Point
|
|
||||||
want string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
0: {
|
|
||||||
name: "requires a measurement",
|
|
||||||
point: &chronograf.Point{},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
1: {
|
|
||||||
name: "requires at least one field",
|
|
||||||
point: &chronograf.Point{
|
|
||||||
Measurement: "telegraf",
|
|
||||||
},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
name: "no tags produces line protocol",
|
|
||||||
point: &chronograf.Point{
|
|
||||||
Measurement: "telegraf",
|
|
||||||
Fields: map[string]interface{}{
|
|
||||||
"myfield": 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: "telegraf myfield=1i",
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
name: "test all influx data types",
|
|
||||||
point: &chronograf.Point{
|
|
||||||
Measurement: "telegraf",
|
|
||||||
Fields: map[string]interface{}{
|
|
||||||
"int": 19,
|
|
||||||
"uint": uint(85),
|
|
||||||
"float": 88.0,
|
|
||||||
"string": "mph",
|
|
||||||
"time_machine": true,
|
|
||||||
"invalidField": time.Time{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: `telegraf float=88.000000,int=19i,string="mph",time_machine=true,uint=85u`,
|
|
||||||
},
|
|
||||||
4: {
|
|
||||||
name: "test all influx data types",
|
|
||||||
point: &chronograf.Point{
|
|
||||||
Measurement: "telegraf",
|
|
||||||
Tags: map[string]string{
|
|
||||||
"marty": "mcfly",
|
|
||||||
"doc": "brown",
|
|
||||||
},
|
|
||||||
Fields: map[string]interface{}{
|
|
||||||
"int": 19,
|
|
||||||
"uint": uint(85),
|
|
||||||
"float": 88.0,
|
|
||||||
"string": "mph",
|
|
||||||
"time_machine": true,
|
|
||||||
"invalidField": time.Time{},
|
|
||||||
},
|
|
||||||
Time: 497115501000000000,
|
|
||||||
},
|
|
||||||
want: `telegraf,doc=brown,marty=mcfly float=88.000000,int=19i,string="mph",time_machine=true,uint=85u 497115501000000000`,
|
|
||||||
},
|
|
||||||
5: {
|
|
||||||
name: "measurements with comma or spaces are escaped",
|
|
||||||
point: &chronograf.Point{
|
|
||||||
Measurement: "O Romeo, Romeo, wherefore art thou Romeo",
|
|
||||||
Tags: map[string]string{
|
|
||||||
"part": "JULIET",
|
|
||||||
},
|
|
||||||
Fields: map[string]interface{}{
|
|
||||||
"act": 2,
|
|
||||||
"scene": 2,
|
|
||||||
"page": 2,
|
|
||||||
"line": 33,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: `O\ Romeo\,\ Romeo\,\ wherefore\ art\ thou\ Romeo,part=JULIET act=2i,line=33i,page=2i,scene=2i`,
|
|
||||||
},
|
|
||||||
6: {
|
|
||||||
name: "tags with comma, quota, space, equal are escaped",
|
|
||||||
point: &chronograf.Point{
|
|
||||||
Measurement: "quotes",
|
|
||||||
Tags: map[string]string{
|
|
||||||
"comma,": "comma,",
|
|
||||||
`quote"`: `quote"`,
|
|
||||||
"space ": `space "`,
|
|
||||||
"equal=": "equal=",
|
|
||||||
},
|
|
||||||
Fields: map[string]interface{}{
|
|
||||||
"myfield": 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: `quotes,comma\,=comma\,,equal\==equal\=,quote\"=quote\",space\ =space\ \" myfield=1i`,
|
|
||||||
},
|
|
||||||
7: {
|
|
||||||
name: "fields with quotes or backslashes are escaped",
|
|
||||||
point: &chronograf.Point{
|
|
||||||
Measurement: "quotes",
|
|
||||||
Fields: map[string]interface{}{
|
|
||||||
`quote"\`: `quote"\`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: `quotes quote\"\="quote\"\\"`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := toLineProtocol(tt.point)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("toLineProtocol() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if got != tt.want {
|
|
||||||
t.Errorf("toLineProtocol() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
package influx
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// Now returns the current time
|
|
||||||
type Now func() time.Time
|
|
|
@ -1,278 +0,0 @@
|
||||||
package influx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// AllowAllDB means a user gets both read and write permissions for a db
|
|
||||||
AllowAllDB = chronograf.Allowances{"WRITE", "READ"}
|
|
||||||
// AllowAllAdmin means a user gets both read and write permissions for an admin
|
|
||||||
AllowAllAdmin = chronograf.Allowances{"ALL"}
|
|
||||||
// AllowRead means a user is only able to read the database.
|
|
||||||
AllowRead = chronograf.Allowances{"READ"}
|
|
||||||
// AllowWrite means a user is able to only write to the database
|
|
||||||
AllowWrite = chronograf.Allowances{"WRITE"}
|
|
||||||
// NoPrivileges occasionally shows up as a response for a users grants.
|
|
||||||
NoPrivileges = "NO PRIVILEGES"
|
|
||||||
// AllPrivileges means that a user has both read and write perms
|
|
||||||
AllPrivileges = "ALL PRIVILEGES"
|
|
||||||
// All means a user has both read and write perms. Alternative to AllPrivileges
|
|
||||||
All = "ALL"
|
|
||||||
// Read means a user can read a database
|
|
||||||
Read = "READ"
|
|
||||||
// Write means a user can write to a database
|
|
||||||
Write = "WRITE"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Permissions return just READ and WRITE for OSS Influx
|
|
||||||
func (c *Client) Permissions(context.Context) chronograf.Permissions {
|
|
||||||
return chronograf.Permissions{
|
|
||||||
{
|
|
||||||
Scope: chronograf.AllScope,
|
|
||||||
Allowed: AllowAllAdmin,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Scope: chronograf.DBScope,
|
|
||||||
Allowed: AllowAllDB,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// showResults is used to deserialize InfluxQL SHOW commands
|
|
||||||
type showResults []struct {
|
|
||||||
Series []struct {
|
|
||||||
Values [][]interface{} `json:"values"`
|
|
||||||
} `json:"series"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Users converts SHOW USERS to chronograf Users
|
|
||||||
func (r *showResults) Users() []chronograf.User {
|
|
||||||
res := []chronograf.User{}
|
|
||||||
for _, u := range *r {
|
|
||||||
for _, s := range u.Series {
|
|
||||||
for _, v := range s.Values {
|
|
||||||
if name, ok := v[0].(string); !ok {
|
|
||||||
continue
|
|
||||||
} else if admin, ok := v[1].(bool); !ok {
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
c := chronograf.User{
|
|
||||||
Name: name,
|
|
||||||
Permissions: chronograf.Permissions{},
|
|
||||||
}
|
|
||||||
if admin {
|
|
||||||
c.Permissions = adminPerms()
|
|
||||||
}
|
|
||||||
res = append(res, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// Databases converts SHOW DATABASES to chronograf Databases
|
|
||||||
func (r *showResults) Databases() []chronograf.Database {
|
|
||||||
res := []chronograf.Database{}
|
|
||||||
for _, u := range *r {
|
|
||||||
for _, s := range u.Series {
|
|
||||||
for _, v := range s.Values {
|
|
||||||
if name, ok := v[0].(string); !ok {
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
d := chronograf.Database{Name: name}
|
|
||||||
res = append(res, d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *showResults) RetentionPolicies() []chronograf.RetentionPolicy {
|
|
||||||
res := []chronograf.RetentionPolicy{}
|
|
||||||
for _, u := range *r {
|
|
||||||
for _, s := range u.Series {
|
|
||||||
for _, v := range s.Values {
|
|
||||||
if name, ok := v[0].(string); !ok {
|
|
||||||
continue
|
|
||||||
} else if duration, ok := v[1].(string); !ok {
|
|
||||||
continue
|
|
||||||
} else if sduration, ok := v[2].(string); !ok {
|
|
||||||
continue
|
|
||||||
} else if replication, ok := v[3].(float64); !ok {
|
|
||||||
continue
|
|
||||||
} else if def, ok := v[4].(bool); !ok {
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
d := chronograf.RetentionPolicy{
|
|
||||||
Name: name,
|
|
||||||
Duration: duration,
|
|
||||||
ShardDuration: sduration,
|
|
||||||
Replication: int32(replication),
|
|
||||||
Default: def,
|
|
||||||
}
|
|
||||||
res = append(res, d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// Measurements converts SHOW MEASUREMENTS to chronograf Measurement
|
|
||||||
func (r *showResults) Measurements() []chronograf.Measurement {
|
|
||||||
res := []chronograf.Measurement{}
|
|
||||||
for _, u := range *r {
|
|
||||||
for _, s := range u.Series {
|
|
||||||
for _, v := range s.Values {
|
|
||||||
if name, ok := v[0].(string); !ok {
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
d := chronograf.Measurement{Name: name}
|
|
||||||
res = append(res, d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// Permissions converts SHOW GRANTS to chronograf.Permissions
|
|
||||||
func (r *showResults) Permissions() chronograf.Permissions {
|
|
||||||
res := []chronograf.Permission{}
|
|
||||||
for _, u := range *r {
|
|
||||||
for _, s := range u.Series {
|
|
||||||
for _, v := range s.Values {
|
|
||||||
if db, ok := v[0].(string); !ok {
|
|
||||||
continue
|
|
||||||
} else if priv, ok := v[1].(string); !ok {
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
c := chronograf.Permission{
|
|
||||||
Name: db,
|
|
||||||
Scope: chronograf.DBScope,
|
|
||||||
}
|
|
||||||
switch priv {
|
|
||||||
case AllPrivileges, All:
|
|
||||||
c.Allowed = AllowAllDB
|
|
||||||
case Read:
|
|
||||||
c.Allowed = AllowRead
|
|
||||||
case Write:
|
|
||||||
c.Allowed = AllowWrite
|
|
||||||
default:
|
|
||||||
// sometimes influx reports back NO PRIVILEGES
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
res = append(res, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func adminPerms() chronograf.Permissions {
|
|
||||||
return []chronograf.Permission{
|
|
||||||
{
|
|
||||||
Scope: chronograf.AllScope,
|
|
||||||
Allowed: AllowAllAdmin,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToInfluxQL converts the permission into InfluxQL
|
|
||||||
func ToInfluxQL(action, preposition, username string, perm chronograf.Permission) string {
|
|
||||||
if perm.Scope == chronograf.AllScope {
|
|
||||||
return fmt.Sprintf(`%s ALL PRIVILEGES %s "%s"`, action, preposition, username)
|
|
||||||
} else if len(perm.Allowed) == 0 {
|
|
||||||
// All privileges are to be removed for this user on this database
|
|
||||||
return fmt.Sprintf(`%s ALL PRIVILEGES ON "%s" %s "%s"`, action, perm.Name, preposition, username)
|
|
||||||
}
|
|
||||||
priv := ToPriv(perm.Allowed)
|
|
||||||
if priv == NoPrivileges {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return fmt.Sprintf(`%s %s ON "%s" %s "%s"`, action, priv, perm.Name, preposition, username)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToRevoke converts the permission into InfluxQL revokes
|
|
||||||
func ToRevoke(username string, perm chronograf.Permission) string {
|
|
||||||
return ToInfluxQL("REVOKE", "FROM", username, perm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToGrant converts the permission into InfluxQL grants
|
|
||||||
func ToGrant(username string, perm chronograf.Permission) string {
|
|
||||||
if len(perm.Allowed) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return ToInfluxQL("GRANT", "TO", username, perm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToPriv converts chronograf allowances to InfluxQL
|
|
||||||
func ToPriv(a chronograf.Allowances) string {
|
|
||||||
if len(a) == 0 {
|
|
||||||
return NoPrivileges
|
|
||||||
}
|
|
||||||
hasWrite := false
|
|
||||||
hasRead := false
|
|
||||||
for _, aa := range a {
|
|
||||||
if aa == Read {
|
|
||||||
hasRead = true
|
|
||||||
} else if aa == Write {
|
|
||||||
hasWrite = true
|
|
||||||
} else if aa == All {
|
|
||||||
hasRead, hasWrite = true, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasWrite && hasRead {
|
|
||||||
return All
|
|
||||||
} else if hasWrite {
|
|
||||||
return Write
|
|
||||||
} else if hasRead {
|
|
||||||
return Read
|
|
||||||
}
|
|
||||||
return NoPrivileges
|
|
||||||
}
|
|
||||||
|
|
||||||
// Difference compares two permission sets and returns a set to be revoked and a set to be added
|
|
||||||
func Difference(wants chronograf.Permissions, haves chronograf.Permissions) (revoke chronograf.Permissions, add chronograf.Permissions) {
|
|
||||||
for _, want := range wants {
|
|
||||||
found := false
|
|
||||||
for _, got := range haves {
|
|
||||||
if want.Scope != got.Scope || want.Name != got.Name {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
found = true
|
|
||||||
if len(want.Allowed) == 0 {
|
|
||||||
revoke = append(revoke, want)
|
|
||||||
} else {
|
|
||||||
add = append(add, want)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
add = append(add, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, got := range haves {
|
|
||||||
found := false
|
|
||||||
for _, want := range wants {
|
|
||||||
if want.Scope != got.Scope || want.Name != got.Name {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
revoke = append(revoke, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,422 +0,0 @@
|
||||||
package influx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDifference(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
type args struct {
|
|
||||||
wants chronograf.Permissions
|
|
||||||
haves chronograf.Permissions
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
wantRevoke chronograf.Permissions
|
|
||||||
wantAdd chronograf.Permissions
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "add write to permissions",
|
|
||||||
args: args{
|
|
||||||
wants: chronograf.Permissions{
|
|
||||||
chronograf.Permission{
|
|
||||||
Scope: "database",
|
|
||||||
Name: "tensorflowdb",
|
|
||||||
Allowed: []string{"READ", "WRITE"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
haves: chronograf.Permissions{
|
|
||||||
chronograf.Permission{
|
|
||||||
Scope: "database",
|
|
||||||
Name: "tensorflowdb",
|
|
||||||
Allowed: []string{"READ"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantRevoke: nil,
|
|
||||||
wantAdd: chronograf.Permissions{
|
|
||||||
chronograf.Permission{
|
|
||||||
Scope: "database",
|
|
||||||
Name: "tensorflowdb",
|
|
||||||
Allowed: []string{"READ", "WRITE"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "revoke write to permissions",
|
|
||||||
args: args{
|
|
||||||
wants: chronograf.Permissions{
|
|
||||||
chronograf.Permission{
|
|
||||||
Scope: "database",
|
|
||||||
Name: "tensorflowdb",
|
|
||||||
Allowed: []string{"READ"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
haves: chronograf.Permissions{
|
|
||||||
chronograf.Permission{
|
|
||||||
Scope: "database",
|
|
||||||
Name: "tensorflowdb",
|
|
||||||
Allowed: []string{"READ", "WRITE"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantRevoke: nil,
|
|
||||||
wantAdd: chronograf.Permissions{
|
|
||||||
chronograf.Permission{
|
|
||||||
Scope: "database",
|
|
||||||
Name: "tensorflowdb",
|
|
||||||
Allowed: []string{"READ"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "revoke all permissions",
|
|
||||||
args: args{
|
|
||||||
wants: chronograf.Permissions{
|
|
||||||
chronograf.Permission{
|
|
||||||
Scope: "database",
|
|
||||||
Name: "tensorflowdb",
|
|
||||||
Allowed: []string{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
haves: chronograf.Permissions{
|
|
||||||
chronograf.Permission{
|
|
||||||
Scope: "database",
|
|
||||||
Name: "tensorflowdb",
|
|
||||||
Allowed: []string{"READ", "WRITE"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantRevoke: chronograf.Permissions{
|
|
||||||
chronograf.Permission{
|
|
||||||
Scope: "database",
|
|
||||||
Name: "tensorflowdb",
|
|
||||||
Allowed: []string{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantAdd: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "add permissions different db",
|
|
||||||
args: args{
|
|
||||||
wants: chronograf.Permissions{
|
|
||||||
chronograf.Permission{
|
|
||||||
Scope: "database",
|
|
||||||
Name: "new",
|
|
||||||
Allowed: []string{"READ"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
haves: chronograf.Permissions{
|
|
||||||
chronograf.Permission{
|
|
||||||
Scope: "database",
|
|
||||||
Name: "old",
|
|
||||||
Allowed: []string{"READ", "WRITE"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantRevoke: chronograf.Permissions{
|
|
||||||
chronograf.Permission{
|
|
||||||
Scope: "database",
|
|
||||||
Name: "old",
|
|
||||||
Allowed: []string{"READ", "WRITE"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
wantAdd: chronograf.Permissions{
|
|
||||||
chronograf.Permission{
|
|
||||||
Scope: "database",
|
|
||||||
Name: "new",
|
|
||||||
Allowed: []string{"READ"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
gotRevoke, gotAdd := Difference(tt.args.wants, tt.args.haves)
|
|
||||||
if !reflect.DeepEqual(gotRevoke, tt.wantRevoke) {
|
|
||||||
t.Errorf("%q. Difference() gotRevoke = %v, want %v", tt.name, gotRevoke, tt.wantRevoke)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(gotAdd, tt.wantAdd) {
|
|
||||||
t.Errorf("%q. Difference() gotAdd = %v, want %v", tt.name, gotAdd, tt.wantAdd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestToPriv(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
type args struct {
|
|
||||||
a chronograf.Allowances
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "no privs",
|
|
||||||
args: args{
|
|
||||||
a: chronograf.Allowances{},
|
|
||||||
},
|
|
||||||
want: NoPrivileges,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "read and write privs",
|
|
||||||
args: args{
|
|
||||||
a: chronograf.Allowances{"READ", "WRITE"},
|
|
||||||
},
|
|
||||||
want: All,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "write privs",
|
|
||||||
args: args{
|
|
||||||
a: chronograf.Allowances{"WRITE"},
|
|
||||||
},
|
|
||||||
want: Write,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "read privs",
|
|
||||||
args: args{
|
|
||||||
a: chronograf.Allowances{"READ"},
|
|
||||||
},
|
|
||||||
want: Read,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "all privs",
|
|
||||||
args: args{
|
|
||||||
a: chronograf.Allowances{"ALL"},
|
|
||||||
},
|
|
||||||
want: All,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bad privs",
|
|
||||||
args: args{
|
|
||||||
a: chronograf.Allowances{"BAD"},
|
|
||||||
},
|
|
||||||
want: NoPrivileges,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
if got := ToPriv(tt.args.a); got != tt.want {
|
|
||||||
t.Errorf("%q. ToPriv() = %v, want %v", tt.name, got, tt.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestToGrant(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
type args struct {
|
|
||||||
username string
|
|
||||||
perm chronograf.Permission
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "grant all for all dbs",
|
|
||||||
args: args{
|
|
||||||
username: "biff",
|
|
||||||
perm: chronograf.Permission{
|
|
||||||
Scope: chronograf.AllScope,
|
|
||||||
Allowed: chronograf.Allowances{"ALL"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: `GRANT ALL PRIVILEGES TO "biff"`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "grant all for one db",
|
|
||||||
args: args{
|
|
||||||
username: "biff",
|
|
||||||
perm: chronograf.Permission{
|
|
||||||
Scope: chronograf.DBScope,
|
|
||||||
Name: "gray_sports_almanac",
|
|
||||||
Allowed: chronograf.Allowances{"ALL"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: `GRANT ALL ON "gray_sports_almanac" TO "biff"`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bad allowance",
|
|
||||||
args: args{
|
|
||||||
username: "biff",
|
|
||||||
perm: chronograf.Permission{
|
|
||||||
Scope: chronograf.DBScope,
|
|
||||||
Name: "gray_sports_almanac",
|
|
||||||
Allowed: chronograf.Allowances{"bad"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
if got := ToGrant(tt.args.username, tt.args.perm); got != tt.want {
|
|
||||||
t.Errorf("%q. ToGrant() = %v, want %v", tt.name, got, tt.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestToRevoke(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
type args struct {
|
|
||||||
username string
|
|
||||||
perm chronograf.Permission
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "revoke all for all dbs",
|
|
||||||
args: args{
|
|
||||||
username: "biff",
|
|
||||||
perm: chronograf.Permission{
|
|
||||||
Scope: chronograf.AllScope,
|
|
||||||
Allowed: chronograf.Allowances{"ALL"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: `REVOKE ALL PRIVILEGES FROM "biff"`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "revoke all for one db",
|
|
||||||
args: args{
|
|
||||||
username: "biff",
|
|
||||||
perm: chronograf.Permission{
|
|
||||||
Scope: chronograf.DBScope,
|
|
||||||
Name: "pleasure_paradice",
|
|
||||||
Allowed: chronograf.Allowances{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: `REVOKE ALL PRIVILEGES ON "pleasure_paradice" FROM "biff"`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
if got := ToRevoke(tt.args.username, tt.args.perm); got != tt.want {
|
|
||||||
t.Errorf("%q. ToRevoke() = %v, want %v", tt.name, got, tt.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_showResults_Users(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
octets []byte
|
|
||||||
want []chronograf.User
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "admin and non-admin",
|
|
||||||
octets: []byte(`[{"series":[{"columns":["user","admin"],"values":[["admin",true],["reader",false]]}]}]`),
|
|
||||||
want: []chronograf.User{
|
|
||||||
{
|
|
||||||
Name: "admin",
|
|
||||||
Permissions: chronograf.Permissions{
|
|
||||||
{
|
|
||||||
Scope: chronograf.AllScope,
|
|
||||||
Allowed: chronograf.Allowances{"ALL"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "reader",
|
|
||||||
Permissions: chronograf.Permissions{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bad JSON",
|
|
||||||
octets: []byte(`[{"series":[{"columns":["user","admin"],"values":[[1,true],["reader","false"]]}]}]`),
|
|
||||||
want: []chronograf.User{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
r := &showResults{}
|
|
||||||
json.Unmarshal(tt.octets, r)
|
|
||||||
if got := r.Users(); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("%q. showResults.Users() = %v, want %v", tt.name, got, tt.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_showResults_Permissions(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
octets []byte
|
|
||||||
want chronograf.Permissions
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "write for one db",
|
|
||||||
octets: []byte(`[{"series":[{"columns":["database","privilege"],"values":[["tensorflowdb","WRITE"]]}]}]`),
|
|
||||||
want: chronograf.Permissions{
|
|
||||||
chronograf.Permission{
|
|
||||||
Scope: "database",
|
|
||||||
Name: "tensorflowdb",
|
|
||||||
Allowed: []string{"WRITE"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "all for one db",
|
|
||||||
octets: []byte(`[{"series":[{"columns":["database","privilege"],"values":[["tensorflowdb","ALL PRIVILEGES"]]}]}]`),
|
|
||||||
want: chronograf.Permissions{
|
|
||||||
chronograf.Permission{
|
|
||||||
Scope: "database",
|
|
||||||
Name: "tensorflowdb",
|
|
||||||
Allowed: []string{"WRITE", "READ"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "read for one db",
|
|
||||||
octets: []byte(`[{"series":[{"columns":["database","privilege"],"values":[["tensorflowdb","READ"]]}]}]`),
|
|
||||||
want: chronograf.Permissions{
|
|
||||||
chronograf.Permission{
|
|
||||||
Scope: "database",
|
|
||||||
Name: "tensorflowdb",
|
|
||||||
Allowed: []string{"READ"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "other all for one db",
|
|
||||||
octets: []byte(`[{"series":[{"columns":["database","privilege"],"values":[["tensorflowdb","ALL"]]}]}]`),
|
|
||||||
want: chronograf.Permissions{
|
|
||||||
chronograf.Permission{
|
|
||||||
Scope: "database",
|
|
||||||
Name: "tensorflowdb",
|
|
||||||
Allowed: []string{"WRITE", "READ"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "other all for one db",
|
|
||||||
octets: []byte(`[{"series":[{"columns":["database","privilege"],"values":[["tensorflowdb","NO PRIVILEGES"]]}]}]`),
|
|
||||||
want: chronograf.Permissions{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bad JSON",
|
|
||||||
octets: []byte(`[{"series":[{"columns":["database","privilege"],"values":[[1,"WRITE"]]}]}]`),
|
|
||||||
want: chronograf.Permissions{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bad JSON",
|
|
||||||
octets: []byte(`[{"series":[{"columns":["database","privilege"],"values":[["tensorflowdb",1]]}]}]`),
|
|
||||||
want: chronograf.Permissions{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
r := &showResults{}
|
|
||||||
json.Unmarshal(tt.octets, r)
|
|
||||||
if got := r.Permissions(); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("%q. showResults.Users() = %v, want %v", tt.name, got, tt.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,491 +0,0 @@
|
||||||
package queries
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"reflect"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/influxdata/influxql"
|
|
||||||
)
|
|
||||||
|
|
||||||
type literalJSON struct {
|
|
||||||
Expr string `json:"expr"`
|
|
||||||
Val string `json:"val"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseSelect(q string) (*SelectStatement, error) {
|
|
||||||
stmt, err := influxql.NewParser(strings.NewReader(q)).ParseStatement()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s, ok := stmt.(*influxql.SelectStatement)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("error parsing query: not a SELECT statement")
|
|
||||||
}
|
|
||||||
return &SelectStatement{s}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type BinaryExpr struct {
|
|
||||||
*influxql.BinaryExpr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *BinaryExpr) MarshalJSON() ([]byte, error) {
|
|
||||||
octets, err := MarshalJSON(b.BinaryExpr.LHS)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
lhs := json.RawMessage(octets)
|
|
||||||
|
|
||||||
octets, err = MarshalJSON(b.BinaryExpr.RHS)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rhs := json.RawMessage(octets)
|
|
||||||
|
|
||||||
return json.Marshal(struct {
|
|
||||||
Expr string `json:"expr"`
|
|
||||||
Op string `json:"op"`
|
|
||||||
LHS *json.RawMessage `json:"lhs"`
|
|
||||||
RHS *json.RawMessage `json:"rhs"`
|
|
||||||
}{"binary", b.Op.String(), &lhs, &rhs})
|
|
||||||
}
|
|
||||||
|
|
||||||
type Call struct {
|
|
||||||
*influxql.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Call) MarshalJSON() ([]byte, error) {
|
|
||||||
args := make([]json.RawMessage, len(c.Args))
|
|
||||||
for i, arg := range c.Args {
|
|
||||||
b, err := MarshalJSON(arg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
args[i] = b
|
|
||||||
}
|
|
||||||
return json.Marshal(struct {
|
|
||||||
Expr string `json:"expr"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Args []json.RawMessage `json:"args,omitempty"`
|
|
||||||
}{"call", c.Name, args})
|
|
||||||
}
|
|
||||||
|
|
||||||
type Distinct struct {
|
|
||||||
*influxql.Distinct
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Distinct) MarshalJSON() ([]byte, error) {
|
|
||||||
return []byte(fmt.Sprintf(`{"expr": "distinct", "val": "%s"}`, d.Val)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Fill struct {
|
|
||||||
Option influxql.FillOption
|
|
||||||
Value interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fill) MarshalJSON() ([]byte, error) {
|
|
||||||
var fill string
|
|
||||||
switch f.Option {
|
|
||||||
case influxql.NullFill:
|
|
||||||
fill = "null"
|
|
||||||
case influxql.NoFill:
|
|
||||||
fill = "none"
|
|
||||||
case influxql.PreviousFill:
|
|
||||||
fill = "previous"
|
|
||||||
case influxql.LinearFill:
|
|
||||||
fill = "linear"
|
|
||||||
case influxql.NumberFill:
|
|
||||||
fill = fmt.Sprintf("%v", f.Value)
|
|
||||||
}
|
|
||||||
return json.Marshal(fill)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ParenExpr struct {
|
|
||||||
*influxql.ParenExpr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ParenExpr) MarshalJSON() ([]byte, error) {
|
|
||||||
expr, err := MarshalJSON(p.Expr)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return []byte(fmt.Sprintf(`{"expr": "paren", "val": %s}`, expr)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func LiteralJSON(lit string, litType string) ([]byte, error) {
|
|
||||||
result := literalJSON{
|
|
||||||
Expr: "literal",
|
|
||||||
Val: lit,
|
|
||||||
Type: litType,
|
|
||||||
}
|
|
||||||
return json.Marshal(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
type BooleanLiteral struct {
|
|
||||||
*influxql.BooleanLiteral
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *BooleanLiteral) MarshalJSON() ([]byte, error) {
|
|
||||||
return LiteralJSON(b.String(), "boolean")
|
|
||||||
}
|
|
||||||
|
|
||||||
type DurationLiteral struct {
|
|
||||||
*influxql.DurationLiteral
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DurationLiteral) MarshalJSON() ([]byte, error) {
|
|
||||||
return LiteralJSON(d.String(), "duration")
|
|
||||||
}
|
|
||||||
|
|
||||||
type IntegerLiteral struct {
|
|
||||||
*influxql.IntegerLiteral
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *IntegerLiteral) MarshalJSON() ([]byte, error) {
|
|
||||||
return LiteralJSON(i.String(), "integer")
|
|
||||||
}
|
|
||||||
|
|
||||||
type NumberLiteral struct {
|
|
||||||
*influxql.NumberLiteral
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *NumberLiteral) MarshalJSON() ([]byte, error) {
|
|
||||||
return LiteralJSON(n.String(), "number")
|
|
||||||
}
|
|
||||||
|
|
||||||
type RegexLiteral struct {
|
|
||||||
*influxql.RegexLiteral
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RegexLiteral) MarshalJSON() ([]byte, error) {
|
|
||||||
return LiteralJSON(r.String(), "regex")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: I don't think list is right
|
|
||||||
type ListLiteral struct {
|
|
||||||
*influxql.ListLiteral
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *ListLiteral) MarshalJSON() ([]byte, error) {
|
|
||||||
vals := make([]string, len(l.Vals))
|
|
||||||
for i, v := range l.Vals {
|
|
||||||
vals[i] = fmt.Sprintf(`"%s"`, v)
|
|
||||||
}
|
|
||||||
list := "[" + strings.Join(vals, ",") + "]"
|
|
||||||
return []byte(list), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type StringLiteral struct {
|
|
||||||
*influxql.StringLiteral
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StringLiteral) MarshalJSON() ([]byte, error) {
|
|
||||||
return LiteralJSON(s.Val, "string")
|
|
||||||
}
|
|
||||||
|
|
||||||
type TimeLiteral struct {
|
|
||||||
*influxql.TimeLiteral
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TimeLiteral) MarshalJSON() ([]byte, error) {
|
|
||||||
return LiteralJSON(t.Val.UTC().Format(time.RFC3339Nano), "time")
|
|
||||||
}
|
|
||||||
|
|
||||||
type VarRef struct {
|
|
||||||
*influxql.VarRef
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *VarRef) MarshalJSON() ([]byte, error) {
|
|
||||||
if v.Type != influxql.Unknown {
|
|
||||||
return []byte(fmt.Sprintf(`{"expr": "reference", "val": "%s", "type": "%s"}`, v.Val, v.Type.String())), nil
|
|
||||||
} else {
|
|
||||||
return []byte(fmt.Sprintf(`{"expr": "reference", "val": "%s"}`, v.Val)), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Wildcard struct {
|
|
||||||
*influxql.Wildcard
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Wildcard) MarshalJSON() ([]byte, error) {
|
|
||||||
return []byte(fmt.Sprintf(`{"expr": "wildcard", "val": "%s"}`, w.String())), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func MarshalJSON(v interface{}) ([]byte, error) {
|
|
||||||
switch v := v.(type) {
|
|
||||||
case *influxql.BinaryExpr:
|
|
||||||
return json.Marshal(&BinaryExpr{v})
|
|
||||||
case *influxql.BooleanLiteral:
|
|
||||||
return json.Marshal(&BooleanLiteral{v})
|
|
||||||
case *influxql.Call:
|
|
||||||
return json.Marshal(&Call{v})
|
|
||||||
case *influxql.Distinct:
|
|
||||||
return json.Marshal(&Distinct{v})
|
|
||||||
case *influxql.DurationLiteral:
|
|
||||||
return json.Marshal(&DurationLiteral{v})
|
|
||||||
case *influxql.IntegerLiteral:
|
|
||||||
return json.Marshal(&IntegerLiteral{v})
|
|
||||||
case *influxql.NumberLiteral:
|
|
||||||
return json.Marshal(&NumberLiteral{v})
|
|
||||||
case *influxql.ParenExpr:
|
|
||||||
return json.Marshal(&ParenExpr{v})
|
|
||||||
case *influxql.RegexLiteral:
|
|
||||||
return json.Marshal(&RegexLiteral{v})
|
|
||||||
case *influxql.ListLiteral:
|
|
||||||
return json.Marshal(&ListLiteral{v})
|
|
||||||
case *influxql.StringLiteral:
|
|
||||||
return json.Marshal(&StringLiteral{v})
|
|
||||||
case *influxql.TimeLiteral:
|
|
||||||
return json.Marshal(&TimeLiteral{v})
|
|
||||||
case *influxql.VarRef:
|
|
||||||
return json.Marshal(&VarRef{v})
|
|
||||||
case *influxql.Wildcard:
|
|
||||||
return json.Marshal(&Wildcard{v})
|
|
||||||
default:
|
|
||||||
t := reflect.TypeOf(v)
|
|
||||||
return nil, fmt.Errorf("error marshaling query: unknown type %s", t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Measurement struct {
|
|
||||||
Database string `json:"database"`
|
|
||||||
RetentionPolicy string `json:"retentionPolicy"`
|
|
||||||
Name string `json:"name,omitempty"`
|
|
||||||
Regex *regexp.Regexp `json:"regex,omitempty"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Source struct {
|
|
||||||
influxql.Source
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Source) MarshalJSON() ([]byte, error) {
|
|
||||||
switch src := s.Source.(type) {
|
|
||||||
case *influxql.Measurement:
|
|
||||||
m := Measurement{
|
|
||||||
Database: src.Database,
|
|
||||||
RetentionPolicy: src.RetentionPolicy,
|
|
||||||
Name: src.Name,
|
|
||||||
Type: "measurement",
|
|
||||||
}
|
|
||||||
if src.Regex != nil {
|
|
||||||
m.Regex = src.Regex.Val
|
|
||||||
}
|
|
||||||
return json.Marshal(m)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("error marshaling source. Subqueries not supported yet")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Sources struct {
|
|
||||||
influxql.Sources
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Handle subqueries
|
|
||||||
func (s *Sources) MarshalJSON() ([]byte, error) {
|
|
||||||
srcs := make([]Source, len(s.Sources))
|
|
||||||
for i, src := range s.Sources {
|
|
||||||
srcs[i] = Source{src}
|
|
||||||
}
|
|
||||||
return json.Marshal(srcs)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Field struct {
|
|
||||||
*influxql.Field
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Field) MarshalJSON() ([]byte, error) {
|
|
||||||
b, err := MarshalJSON(f.Expr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
column := json.RawMessage(b)
|
|
||||||
return json.Marshal(struct {
|
|
||||||
Alias string `json:"alias,omitempty"`
|
|
||||||
Column *json.RawMessage `json:"column"`
|
|
||||||
}{f.Alias, &column})
|
|
||||||
}
|
|
||||||
|
|
||||||
type Fields struct {
|
|
||||||
influxql.Fields
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Fields) MarshalJSON() ([]byte, error) {
|
|
||||||
fields := make([]Field, len(f.Fields))
|
|
||||||
for i, field := range f.Fields {
|
|
||||||
fields[i] = Field{field}
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal(fields)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Condition struct {
|
|
||||||
influxql.Expr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Condition) MarshalJSON() ([]byte, error) {
|
|
||||||
return MarshalJSON(c.Expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
type SortField struct {
|
|
||||||
*influxql.SortField
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SortField) MarshalJSON() ([]byte, error) {
|
|
||||||
var order string
|
|
||||||
if s.Ascending {
|
|
||||||
order = "ascending"
|
|
||||||
} else {
|
|
||||||
order = "descending"
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal(struct {
|
|
||||||
Name string `json:"name,omitempty"`
|
|
||||||
Order string `json:"order,omitempty"`
|
|
||||||
}{s.Name, order})
|
|
||||||
}
|
|
||||||
|
|
||||||
type SortFields struct {
|
|
||||||
influxql.SortFields
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *SortFields) MarshalJSON() ([]byte, error) {
|
|
||||||
fields := make([]SortField, len(f.SortFields))
|
|
||||||
for i, field := range f.SortFields {
|
|
||||||
fields[i] = SortField{field}
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal(fields)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Limits struct {
|
|
||||||
Limit int `json:"limit,omitempty"`
|
|
||||||
Offset int `json:"offset,omitempty"`
|
|
||||||
SLimit int `json:"slimit,omitempty"`
|
|
||||||
SOffset int `json:"soffset,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SelectStatement struct {
|
|
||||||
*influxql.SelectStatement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SelectStatement) MarshalJSON() ([]byte, error) {
|
|
||||||
stmt := map[string]interface{}{
|
|
||||||
"fields": &Fields{s.Fields},
|
|
||||||
"sources": &Sources{s.Sources},
|
|
||||||
}
|
|
||||||
if len(s.Dimensions) > 0 {
|
|
||||||
stmt["groupBy"] = &Dimensions{s.Dimensions, s.Fill, s.FillValue}
|
|
||||||
}
|
|
||||||
if s.Condition != nil {
|
|
||||||
stmt["condition"] = &Condition{s.Condition}
|
|
||||||
}
|
|
||||||
if s.Limit != 0 || s.Offset != 0 || s.SLimit != 0 || s.SOffset != 0 {
|
|
||||||
stmt["limits"] = &Limits{s.Limit, s.Offset, s.SLimit, s.SOffset}
|
|
||||||
}
|
|
||||||
if len(s.SortFields) > 0 {
|
|
||||||
stmt["orderbys"] = &SortFields{s.SortFields}
|
|
||||||
}
|
|
||||||
return json.Marshal(stmt)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Dimension struct {
|
|
||||||
*influxql.Dimension
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Dimension) MarshalJSON() ([]byte, error) {
|
|
||||||
switch v := d.Expr.(type) {
|
|
||||||
case *influxql.Call:
|
|
||||||
if v.Name != "time" {
|
|
||||||
return nil, errors.New("time dimension offset function must be now()")
|
|
||||||
}
|
|
||||||
// Make sure there is exactly one argument.
|
|
||||||
if got := len(v.Args); got < 1 || got > 2 {
|
|
||||||
return nil, errors.New("time dimension expected 1 or 2 arguments")
|
|
||||||
}
|
|
||||||
// Ensure the argument is a duration.
|
|
||||||
lit, ok := v.Args[0].(*influxql.DurationLiteral)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("time dimension must have duration argument")
|
|
||||||
}
|
|
||||||
var offset string
|
|
||||||
if len(v.Args) == 2 {
|
|
||||||
switch o := v.Args[1].(type) {
|
|
||||||
case *influxql.DurationLiteral:
|
|
||||||
offset = o.String()
|
|
||||||
case *influxql.Call:
|
|
||||||
if o.Name != "now" {
|
|
||||||
return nil, errors.New("time dimension offset function must be now()")
|
|
||||||
} else if len(o.Args) != 0 {
|
|
||||||
return nil, errors.New("time dimension offset now() function requires no arguments")
|
|
||||||
}
|
|
||||||
offset = "now()"
|
|
||||||
default:
|
|
||||||
return nil, errors.New("time dimension offset must be duration or now()")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return json.Marshal(struct {
|
|
||||||
Interval string `json:"interval"`
|
|
||||||
Offset string `json:"offset,omitempty"`
|
|
||||||
}{lit.String(), offset})
|
|
||||||
case *influxql.VarRef:
|
|
||||||
return json.Marshal(v.Val)
|
|
||||||
case *influxql.Wildcard:
|
|
||||||
return json.Marshal(v.String())
|
|
||||||
case *influxql.RegexLiteral:
|
|
||||||
return json.Marshal(v.String())
|
|
||||||
}
|
|
||||||
return MarshalJSON(d.Expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Dimensions struct {
|
|
||||||
influxql.Dimensions
|
|
||||||
FillOption influxql.FillOption
|
|
||||||
FillValue interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Dimensions) MarshalJSON() ([]byte, error) {
|
|
||||||
groupBys := struct {
|
|
||||||
Time *json.RawMessage `json:"time,omitempty"`
|
|
||||||
Tags []*json.RawMessage `json:"tags,omitempty"`
|
|
||||||
Fill *json.RawMessage `json:"fill,omitempty"`
|
|
||||||
}{}
|
|
||||||
|
|
||||||
for _, dim := range d.Dimensions {
|
|
||||||
switch dim.Expr.(type) {
|
|
||||||
case *influxql.Call:
|
|
||||||
octets, err := json.Marshal(&Dimension{dim})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
time := json.RawMessage(octets)
|
|
||||||
groupBys.Time = &time
|
|
||||||
default:
|
|
||||||
octets, err := json.Marshal(&Dimension{dim})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tag := json.RawMessage(octets)
|
|
||||||
groupBys.Tags = append(groupBys.Tags, &tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if d.FillOption != influxql.NullFill {
|
|
||||||
octets, err := json.Marshal(&Fill{d.FillOption, d.FillValue})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
fill := json.RawMessage(octets)
|
|
||||||
groupBys.Fill = &fill
|
|
||||||
}
|
|
||||||
return json.Marshal(groupBys)
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
package queries
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSelect(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
q string
|
|
||||||
}{
|
|
||||||
{q: fmt.Sprintf(`SELECT mean(field1), sum(field2) ,count(field3::field) AS field_x FROM myseries WHERE host = 'hosta.influxdb.org' and time > '%s' GROUP BY time(10h) ORDER BY DESC LIMIT 20 OFFSET 10;`, time.Now().UTC().Format(time.RFC3339Nano))},
|
|
||||||
{q: fmt.Sprintf(`SELECT difference(max(field1)) FROM myseries WHERE time > '%s' GROUP BY time(1m)`, time.Now().UTC().Format(time.RFC3339Nano))},
|
|
||||||
{q: `SELECT derivative(field1, 1h) / derivative(field2, 1h) FROM myseries`},
|
|
||||||
{q: `SELECT mean("load1") FROM "system" WHERE "cluster_id" =~ /^$ClusterID$/ AND time > now() - 1h GROUP BY time(10m), "host" fill(null)`},
|
|
||||||
{q: "SELECT max(\"n_cpus\") AS \"max_cpus\", non_negative_derivative(median(\"n_users\"), 5m) FROM \"system\" WHERE \"cluster_id\" =~ /^23/ AND \"host\" = 'prod-2ccccc04-us-east-1-data-3' AND time > now() - 15m GROUP BY time(15m, 10s),host,tag_x fill(10)"},
|
|
||||||
{q: "SELECT mean(\"usage_user\") AS \"mean_usage_user\" FROM \"telegraf\".\"default\".\"cpu\" WHERE host =~ /\\./ AND time > now() - 1h"},
|
|
||||||
{q: `SELECT 1 + "A" FROM howdy`},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, tt := range tests {
|
|
||||||
stmt, err := ParseSelect(tt.q)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Test %d query %s invalid statement: %v", i, tt.q, err)
|
|
||||||
}
|
|
||||||
_, err = json.MarshalIndent(stmt, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Test %d query %s Unable to marshal statement: %v", i, tt.q, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,537 +0,0 @@
|
||||||
package influx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf"
|
|
||||||
"github.com/influxdata/influxql"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TimeRangeAsEpochNano extracts the min and max epoch times from the expression
|
|
||||||
func TimeRangeAsEpochNano(expr influxql.Expr, now time.Time) (min, max int64, err error) {
|
|
||||||
// TODO(desa): is this OK?
|
|
||||||
_, trange, err := influxql.ConditionExpr(expr, nil)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, err
|
|
||||||
}
|
|
||||||
tmin, tmax := trange.Min, trange.Max
|
|
||||||
if tmin.IsZero() {
|
|
||||||
min = time.Unix(0, influxql.MinTime).UnixNano()
|
|
||||||
} else {
|
|
||||||
min = tmin.UnixNano()
|
|
||||||
}
|
|
||||||
if tmax.IsZero() {
|
|
||||||
max = now.UnixNano()
|
|
||||||
} else {
|
|
||||||
max = tmax.UnixNano()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// WhereToken is used to parse the time expression from an influxql query
|
|
||||||
const WhereToken = "WHERE"
|
|
||||||
|
|
||||||
// ParseTime extracts the duration of the time range of the query
|
|
||||||
func ParseTime(influxQL string, now time.Time) (time.Duration, error) {
|
|
||||||
start := strings.Index(strings.ToUpper(influxQL), WhereToken)
|
|
||||||
if start == -1 {
|
|
||||||
return 0, fmt.Errorf("not a relative duration")
|
|
||||||
}
|
|
||||||
start += len(WhereToken)
|
|
||||||
where := influxQL[start:]
|
|
||||||
cond, err := influxql.ParseExpr(where)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
nowVal := &influxql.NowValuer{
|
|
||||||
Now: now,
|
|
||||||
}
|
|
||||||
cond = influxql.Reduce(cond, nowVal)
|
|
||||||
min, max, err := TimeRangeAsEpochNano(cond, now)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
dur := time.Duration(max - min)
|
|
||||||
if dur < 0 {
|
|
||||||
dur = 0
|
|
||||||
}
|
|
||||||
return dur, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert changes an InfluxQL query to a QueryConfig
|
|
||||||
func Convert(influxQL string) (chronograf.QueryConfig, error) {
|
|
||||||
itsDashboardTime := false
|
|
||||||
intervalTime := false
|
|
||||||
|
|
||||||
if strings.Contains(influxQL, ":interval:") {
|
|
||||||
influxQL = strings.Replace(influxQL, ":interval:", "8675309ns", -1)
|
|
||||||
intervalTime = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(influxQL, ":dashboardTime:") {
|
|
||||||
influxQL = strings.Replace(influxQL, ":dashboardTime:", "now() - 15m", 1)
|
|
||||||
itsDashboardTime = true
|
|
||||||
}
|
|
||||||
|
|
||||||
query, err := influxql.ParseQuery(influxQL)
|
|
||||||
if err != nil {
|
|
||||||
return chronograf.QueryConfig{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if itsDashboardTime {
|
|
||||||
influxQL = strings.Replace(influxQL, "now() - 15m", ":dashboardTime:", 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if intervalTime {
|
|
||||||
influxQL = strings.Replace(influxQL, "8675309ns", ":interval:", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
raw := chronograf.QueryConfig{
|
|
||||||
RawText: &influxQL,
|
|
||||||
Fields: []chronograf.Field{},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
Tags: make(map[string][]string),
|
|
||||||
}
|
|
||||||
qc := chronograf.QueryConfig{
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
Tags: make(map[string][]string),
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(query.Statements) != 1 {
|
|
||||||
return raw, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
stmt, ok := query.Statements[0].(*influxql.SelectStatement)
|
|
||||||
if !ok {
|
|
||||||
return raw, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query config doesn't support limits
|
|
||||||
if stmt.Limit != 0 || stmt.Offset != 0 || stmt.SLimit != 0 || stmt.SOffset != 0 {
|
|
||||||
return raw, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query config doesn't support sorting
|
|
||||||
if len(stmt.SortFields) > 0 {
|
|
||||||
return raw, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query config doesn't allow SELECT INTO
|
|
||||||
if stmt.Target != nil {
|
|
||||||
return raw, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query config only allows selecting from one source at a time.
|
|
||||||
if len(stmt.Sources) != 1 {
|
|
||||||
return raw, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
src := stmt.Sources[0]
|
|
||||||
measurement, ok := src.(*influxql.Measurement)
|
|
||||||
if !ok {
|
|
||||||
return raw, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if measurement.Regex != nil {
|
|
||||||
return raw, nil
|
|
||||||
}
|
|
||||||
qc.Database = measurement.Database
|
|
||||||
qc.RetentionPolicy = measurement.RetentionPolicy
|
|
||||||
qc.Measurement = measurement.Name
|
|
||||||
|
|
||||||
for _, dim := range stmt.Dimensions {
|
|
||||||
switch v := dim.Expr.(type) {
|
|
||||||
default:
|
|
||||||
return raw, nil
|
|
||||||
case *influxql.Call:
|
|
||||||
if v.Name != "time" {
|
|
||||||
return raw, nil
|
|
||||||
}
|
|
||||||
// Make sure there is exactly one argument.
|
|
||||||
if len(v.Args) != 1 {
|
|
||||||
return raw, nil
|
|
||||||
}
|
|
||||||
// Ensure the argument is a duration.
|
|
||||||
lit, ok := v.Args[0].(*influxql.DurationLiteral)
|
|
||||||
if !ok {
|
|
||||||
return raw, nil
|
|
||||||
}
|
|
||||||
if intervalTime {
|
|
||||||
qc.GroupBy.Time = "auto"
|
|
||||||
} else {
|
|
||||||
qc.GroupBy.Time = lit.String()
|
|
||||||
}
|
|
||||||
// Add fill to queryConfig only if there's a `GROUP BY time`
|
|
||||||
switch stmt.Fill {
|
|
||||||
case influxql.NullFill:
|
|
||||||
qc.Fill = "null"
|
|
||||||
case influxql.NoFill:
|
|
||||||
qc.Fill = "none"
|
|
||||||
case influxql.NumberFill:
|
|
||||||
qc.Fill = fmt.Sprint(stmt.FillValue)
|
|
||||||
case influxql.PreviousFill:
|
|
||||||
qc.Fill = "previous"
|
|
||||||
case influxql.LinearFill:
|
|
||||||
qc.Fill = "linear"
|
|
||||||
default:
|
|
||||||
return raw, nil
|
|
||||||
}
|
|
||||||
case *influxql.VarRef:
|
|
||||||
qc.GroupBy.Tags = append(qc.GroupBy.Tags, v.Val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
qc.Fields = []chronograf.Field{}
|
|
||||||
for _, fld := range stmt.Fields {
|
|
||||||
switch f := fld.Expr.(type) {
|
|
||||||
default:
|
|
||||||
return raw, nil
|
|
||||||
case *influxql.Call:
|
|
||||||
// only support certain query config functions
|
|
||||||
if _, ok = supportedFuncs[f.Name]; !ok {
|
|
||||||
return raw, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
fldArgs := []chronograf.Field{}
|
|
||||||
for _, arg := range f.Args {
|
|
||||||
switch ref := arg.(type) {
|
|
||||||
case *influxql.VarRef:
|
|
||||||
fldArgs = append(fldArgs, chronograf.Field{
|
|
||||||
Value: ref.Val,
|
|
||||||
Type: "field",
|
|
||||||
})
|
|
||||||
case *influxql.IntegerLiteral:
|
|
||||||
fldArgs = append(fldArgs, chronograf.Field{
|
|
||||||
Value: strconv.FormatInt(ref.Val, 10),
|
|
||||||
Type: "integer",
|
|
||||||
})
|
|
||||||
case *influxql.NumberLiteral:
|
|
||||||
fldArgs = append(fldArgs, chronograf.Field{
|
|
||||||
Value: strconv.FormatFloat(ref.Val, 'f', -1, 64),
|
|
||||||
Type: "number",
|
|
||||||
})
|
|
||||||
case *influxql.RegexLiteral:
|
|
||||||
fldArgs = append(fldArgs, chronograf.Field{
|
|
||||||
Value: ref.Val.String(),
|
|
||||||
Type: "regex",
|
|
||||||
})
|
|
||||||
case *influxql.Wildcard:
|
|
||||||
fldArgs = append(fldArgs, chronograf.Field{
|
|
||||||
Value: "*",
|
|
||||||
Type: "wildcard",
|
|
||||||
})
|
|
||||||
default:
|
|
||||||
return raw, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
qc.Fields = append(qc.Fields, chronograf.Field{
|
|
||||||
Value: f.Name,
|
|
||||||
Type: "func",
|
|
||||||
Alias: fld.Alias,
|
|
||||||
Args: fldArgs,
|
|
||||||
})
|
|
||||||
case *influxql.VarRef:
|
|
||||||
if f.Type != influxql.Unknown {
|
|
||||||
return raw, nil
|
|
||||||
}
|
|
||||||
qc.Fields = append(qc.Fields, chronograf.Field{
|
|
||||||
Value: f.Val,
|
|
||||||
Type: "field",
|
|
||||||
Alias: fld.Alias,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if stmt.Condition == nil {
|
|
||||||
return qc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
reduced := influxql.Reduce(stmt.Condition, nil)
|
|
||||||
logic, ok := isTagLogic(reduced)
|
|
||||||
if !ok {
|
|
||||||
return raw, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ops := map[string]bool{}
|
|
||||||
for _, l := range logic {
|
|
||||||
values, ok := qc.Tags[l.Tag]
|
|
||||||
if !ok {
|
|
||||||
values = []string{}
|
|
||||||
}
|
|
||||||
ops[l.Op] = true
|
|
||||||
values = append(values, l.Value)
|
|
||||||
qc.Tags[l.Tag] = values
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(logic) > 0 {
|
|
||||||
if len(ops) != 1 {
|
|
||||||
return raw, nil
|
|
||||||
}
|
|
||||||
if _, ok := ops["=="]; ok {
|
|
||||||
qc.AreTagsAccepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the condition has a time range we report back its duration
|
|
||||||
if dur, ok := hasTimeRange(stmt.Condition); ok {
|
|
||||||
if !itsDashboardTime {
|
|
||||||
qc.Range = &chronograf.DurationRange{
|
|
||||||
Lower: "now() - " + shortDur(dur),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return qc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// tagFilter represents a single tag that is filtered by some condition
|
|
||||||
type tagFilter struct {
|
|
||||||
Op string
|
|
||||||
Tag string
|
|
||||||
Value string
|
|
||||||
}
|
|
||||||
|
|
||||||
func isTime(exp influxql.Expr) bool {
|
|
||||||
if p, ok := exp.(*influxql.ParenExpr); ok {
|
|
||||||
return isTime(p.Expr)
|
|
||||||
} else if ref, ok := exp.(*influxql.VarRef); ok && strings.ToLower(ref.Val) == "time" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isNow(exp influxql.Expr) bool {
|
|
||||||
if p, ok := exp.(*influxql.ParenExpr); ok {
|
|
||||||
return isNow(p.Expr)
|
|
||||||
} else if call, ok := exp.(*influxql.Call); ok && strings.ToLower(call.Name) == "now" && len(call.Args) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isDuration(exp influxql.Expr) (time.Duration, bool) {
|
|
||||||
switch e := exp.(type) {
|
|
||||||
case *influxql.ParenExpr:
|
|
||||||
return isDuration(e.Expr)
|
|
||||||
case *influxql.DurationLiteral:
|
|
||||||
return e.Val, true
|
|
||||||
case *influxql.NumberLiteral, *influxql.IntegerLiteral, *influxql.TimeLiteral:
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isPreviousTime(exp influxql.Expr) (time.Duration, bool) {
|
|
||||||
if p, ok := exp.(*influxql.ParenExpr); ok {
|
|
||||||
return isPreviousTime(p.Expr)
|
|
||||||
} else if bin, ok := exp.(*influxql.BinaryExpr); ok {
|
|
||||||
now := isNow(bin.LHS) || isNow(bin.RHS) // either side can be now
|
|
||||||
op := bin.Op == influxql.SUB
|
|
||||||
dur, hasDur := isDuration(bin.LHS)
|
|
||||||
if !hasDur {
|
|
||||||
dur, hasDur = isDuration(bin.RHS)
|
|
||||||
}
|
|
||||||
return dur, now && op && hasDur
|
|
||||||
} else if isNow(exp) { // just comparing to now
|
|
||||||
return 0, true
|
|
||||||
}
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isTimeRange(exp influxql.Expr) (time.Duration, bool) {
|
|
||||||
if p, ok := exp.(*influxql.ParenExpr); ok {
|
|
||||||
return isTimeRange(p.Expr)
|
|
||||||
} else if bin, ok := exp.(*influxql.BinaryExpr); ok {
|
|
||||||
tm := isTime(bin.LHS) || isTime(bin.RHS) // Either side could be time
|
|
||||||
op := false
|
|
||||||
switch bin.Op {
|
|
||||||
case influxql.LT, influxql.LTE, influxql.GT, influxql.GTE:
|
|
||||||
op = true
|
|
||||||
}
|
|
||||||
dur, prev := isPreviousTime(bin.LHS)
|
|
||||||
if !prev {
|
|
||||||
dur, prev = isPreviousTime(bin.RHS)
|
|
||||||
}
|
|
||||||
return dur, tm && op && prev
|
|
||||||
}
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasTimeRange(exp influxql.Expr) (time.Duration, bool) {
|
|
||||||
v := &timeRangeVisitor{}
|
|
||||||
influxql.Walk(v, exp)
|
|
||||||
return v.Duration, v.Ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// timeRangeVisitor implements influxql.Visitor to search for time ranges
|
|
||||||
type timeRangeVisitor struct {
|
|
||||||
Duration time.Duration
|
|
||||||
Ok bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *timeRangeVisitor) Visit(n influxql.Node) influxql.Visitor {
|
|
||||||
if exp, ok := n.(influxql.Expr); !ok {
|
|
||||||
return nil
|
|
||||||
} else if dur, ok := isTimeRange(exp); ok {
|
|
||||||
v.Duration = dur
|
|
||||||
v.Ok = ok
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func isTagLogic(exp influxql.Expr) ([]tagFilter, bool) {
|
|
||||||
if p, ok := exp.(*influxql.ParenExpr); ok {
|
|
||||||
return isTagLogic(p.Expr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := isTimeRange(exp); ok {
|
|
||||||
return nil, true
|
|
||||||
} else if tf, ok := isTagFilter(exp); ok {
|
|
||||||
return []tagFilter{tf}, true
|
|
||||||
}
|
|
||||||
|
|
||||||
bin, ok := exp.(*influxql.BinaryExpr)
|
|
||||||
if !ok {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
lhs, lhsOK := isTagFilter(bin.LHS)
|
|
||||||
rhs, rhsOK := isTagFilter(bin.RHS)
|
|
||||||
|
|
||||||
if lhsOK && rhsOK && lhs.Tag == rhs.Tag && lhs.Op == rhs.Op && bin.Op == influxql.OR {
|
|
||||||
return []tagFilter{lhs, rhs}, true
|
|
||||||
}
|
|
||||||
|
|
||||||
if bin.Op != influxql.AND && bin.Op != influxql.OR {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
_, tm := isTimeRange(bin.LHS)
|
|
||||||
if !tm {
|
|
||||||
_, tm = isTimeRange(bin.RHS)
|
|
||||||
}
|
|
||||||
tf := lhsOK || rhsOK
|
|
||||||
if tm && tf {
|
|
||||||
if lhsOK {
|
|
||||||
return []tagFilter{lhs}, true
|
|
||||||
}
|
|
||||||
return []tagFilter{rhs}, true
|
|
||||||
}
|
|
||||||
|
|
||||||
tlLHS, lhsOK := isTagLogic(bin.LHS)
|
|
||||||
tlRHS, rhsOK := isTagLogic(bin.RHS)
|
|
||||||
if lhsOK && rhsOK {
|
|
||||||
ops := map[string]bool{} // there must only be one kind of ops
|
|
||||||
for _, tf := range tlLHS {
|
|
||||||
ops[tf.Op] = true
|
|
||||||
}
|
|
||||||
for _, tf := range tlRHS {
|
|
||||||
ops[tf.Op] = true
|
|
||||||
}
|
|
||||||
if len(ops) > 1 {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return append(tlLHS, tlRHS...), true
|
|
||||||
}
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isVarRef(exp influxql.Expr) bool {
|
|
||||||
if p, ok := exp.(*influxql.ParenExpr); ok {
|
|
||||||
return isVarRef(p.Expr)
|
|
||||||
} else if _, ok := exp.(*influxql.VarRef); ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isString(exp influxql.Expr) bool {
|
|
||||||
if p, ok := exp.(*influxql.ParenExpr); ok {
|
|
||||||
return isString(p.Expr)
|
|
||||||
} else if _, ok := exp.(*influxql.StringLiteral); ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isTagFilter(exp influxql.Expr) (tagFilter, bool) {
|
|
||||||
switch expr := exp.(type) {
|
|
||||||
default:
|
|
||||||
return tagFilter{}, false
|
|
||||||
case *influxql.ParenExpr:
|
|
||||||
return isTagFilter(expr.Expr)
|
|
||||||
case *influxql.BinaryExpr:
|
|
||||||
var Op string
|
|
||||||
if expr.Op == influxql.EQ {
|
|
||||||
Op = "=="
|
|
||||||
} else if expr.Op == influxql.NEQ {
|
|
||||||
Op = "!="
|
|
||||||
} else {
|
|
||||||
return tagFilter{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
hasValue := isString(expr.LHS) || isString(expr.RHS)
|
|
||||||
hasTag := isVarRef(expr.LHS) || isVarRef(expr.RHS)
|
|
||||||
if !(hasValue && hasTag) {
|
|
||||||
return tagFilter{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
value := ""
|
|
||||||
tag := ""
|
|
||||||
// Either tag op value or value op tag
|
|
||||||
if isVarRef(expr.LHS) {
|
|
||||||
t, _ := expr.LHS.(*influxql.VarRef)
|
|
||||||
tag = t.Val
|
|
||||||
v, _ := expr.RHS.(*influxql.StringLiteral)
|
|
||||||
value = v.Val
|
|
||||||
} else {
|
|
||||||
t, _ := expr.RHS.(*influxql.VarRef)
|
|
||||||
tag = t.Val
|
|
||||||
v, _ := expr.LHS.(*influxql.StringLiteral)
|
|
||||||
value = v.Val
|
|
||||||
}
|
|
||||||
|
|
||||||
return tagFilter{
|
|
||||||
Op: Op,
|
|
||||||
Tag: tag,
|
|
||||||
Value: value,
|
|
||||||
}, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var supportedFuncs = map[string]bool{
|
|
||||||
"mean": true,
|
|
||||||
"median": true,
|
|
||||||
"count": true,
|
|
||||||
"min": true,
|
|
||||||
"max": true,
|
|
||||||
"sum": true,
|
|
||||||
"first": true,
|
|
||||||
"last": true,
|
|
||||||
"spread": true,
|
|
||||||
"stddev": true,
|
|
||||||
"percentile": true,
|
|
||||||
"top": true,
|
|
||||||
"bottom": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// shortDur converts duration into the queryConfig duration format
|
|
||||||
func shortDur(d time.Duration) string {
|
|
||||||
s := d.String()
|
|
||||||
if strings.HasSuffix(s, "m0s") {
|
|
||||||
s = s[:len(s)-2]
|
|
||||||
}
|
|
||||||
if strings.HasSuffix(s, "h0m") {
|
|
||||||
s = s[:len(s)-2]
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
|
@ -1,810 +0,0 @@
|
||||||
package influx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestConvert(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
influxQL string
|
|
||||||
RawText string
|
|
||||||
want chronograf.QueryConfig
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Test field order",
|
|
||||||
influxQL: `SELECT "usage_idle", "usage_guest_nice", "usage_system", "usage_guest" FROM "telegraf"."autogen"."cpu" WHERE time > :dashboardTime:`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Database: "telegraf",
|
|
||||||
Measurement: "cpu",
|
|
||||||
RetentionPolicy: "autogen",
|
|
||||||
Fields: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "usage_idle",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: "usage_guest_nice",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: "usage_system",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: "usage_guest",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Tags: map[string][]string{},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test field function order",
|
|
||||||
influxQL: `SELECT mean("usage_idle"), median("usage_idle"), count("usage_guest_nice"), mean("usage_guest_nice") FROM "telegraf"."autogen"."cpu" WHERE time > :dashboardTime:`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Database: "telegraf",
|
|
||||||
Measurement: "cpu",
|
|
||||||
RetentionPolicy: "autogen",
|
|
||||||
Fields: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "mean",
|
|
||||||
Type: "func",
|
|
||||||
Args: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "usage_idle",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: "median",
|
|
||||||
Type: "func",
|
|
||||||
Args: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "usage_idle",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: "count",
|
|
||||||
Type: "func",
|
|
||||||
Args: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "usage_guest_nice",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: "mean",
|
|
||||||
Type: "func",
|
|
||||||
Args: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "usage_guest_nice",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Tags: map[string][]string{},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test named count field",
|
|
||||||
influxQL: `SELECT moving_average(mean("count"),14) FROM "usage_computed"."autogen".unique_clusters_by_day WHERE time > now() - 90d AND product = 'influxdb' group by time(1d)`,
|
|
||||||
RawText: `SELECT moving_average(mean("count"),14) FROM "usage_computed"."autogen".unique_clusters_by_day WHERE time > now() - 90d AND product = 'influxdb' group by time(1d)`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Fields: []chronograf.Field{},
|
|
||||||
Tags: map[string][]string{},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test math",
|
|
||||||
influxQL: `SELECT count("event_id")/3 as "event_count_id" from discource.autogen.discourse_events where time > now() - 7d group by time(1d), "event_type"`,
|
|
||||||
RawText: `SELECT count("event_id")/3 as "event_count_id" from discource.autogen.discourse_events where time > now() - 7d group by time(1d), "event_type"`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Fields: []chronograf.Field{},
|
|
||||||
Tags: map[string][]string{},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test range",
|
|
||||||
influxQL: `SELECT usage_user from telegraf.autogen.cpu where "host" != 'myhost' and time > now() - 15m`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Database: "telegraf",
|
|
||||||
Measurement: "cpu",
|
|
||||||
RetentionPolicy: "autogen",
|
|
||||||
Fields: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "usage_user",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Tags: map[string][]string{"host": {"myhost"}},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Time: "",
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
AreTagsAccepted: false,
|
|
||||||
Range: &chronograf.DurationRange{
|
|
||||||
Lower: "now() - 15m",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test invalid range",
|
|
||||||
influxQL: `SELECT usage_user from telegraf.autogen.cpu where "host" != 'myhost' and time > now() - 15`,
|
|
||||||
RawText: `SELECT usage_user from telegraf.autogen.cpu where "host" != 'myhost' and time > now() - 15`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Fields: []chronograf.Field{},
|
|
||||||
Tags: map[string][]string{},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test range with no duration",
|
|
||||||
influxQL: `SELECT usage_user from telegraf.autogen.cpu where "host" != 'myhost' and time > now()`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Database: "telegraf",
|
|
||||||
Measurement: "cpu",
|
|
||||||
RetentionPolicy: "autogen",
|
|
||||||
Fields: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "usage_user",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Tags: map[string][]string{"host": {"myhost"}},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Time: "",
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
AreTagsAccepted: false,
|
|
||||||
Range: &chronograf.DurationRange{
|
|
||||||
Lower: "now() - 0s",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test range with no tags",
|
|
||||||
influxQL: `SELECT usage_user from telegraf.autogen.cpu where time > now() - 15m`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Database: "telegraf",
|
|
||||||
Measurement: "cpu",
|
|
||||||
RetentionPolicy: "autogen",
|
|
||||||
Tags: map[string][]string{},
|
|
||||||
Fields: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "usage_user",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Time: "",
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
AreTagsAccepted: false,
|
|
||||||
Range: &chronograf.DurationRange{
|
|
||||||
Lower: "now() - 15m",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test range with no tags nor duration",
|
|
||||||
influxQL: `SELECT usage_user from telegraf.autogen.cpu where time`,
|
|
||||||
RawText: `SELECT usage_user from telegraf.autogen.cpu where time`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Fields: []chronograf.Field{},
|
|
||||||
Tags: map[string][]string{},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test with no time range",
|
|
||||||
influxQL: `SELECT usage_user from telegraf.autogen.cpu where "host" != 'myhost' and time`,
|
|
||||||
RawText: `SELECT usage_user from telegraf.autogen.cpu where "host" != 'myhost' and time`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Fields: []chronograf.Field{},
|
|
||||||
Tags: map[string][]string{},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test with no where clauses",
|
|
||||||
influxQL: `SELECT usage_user from telegraf.autogen.cpu`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Database: "telegraf",
|
|
||||||
Measurement: "cpu",
|
|
||||||
RetentionPolicy: "autogen",
|
|
||||||
Fields: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "usage_user",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Tags: map[string][]string{},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Time: "",
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test tags accepted",
|
|
||||||
influxQL: `SELECT usage_user from telegraf.autogen.cpu where "host" = 'myhost' and time > now() - 15m`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Database: "telegraf",
|
|
||||||
Measurement: "cpu",
|
|
||||||
RetentionPolicy: "autogen",
|
|
||||||
Fields: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "usage_user",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Tags: map[string][]string{"host": {"myhost"}},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Time: "",
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
AreTagsAccepted: true,
|
|
||||||
Range: &chronograf.DurationRange{
|
|
||||||
Lower: "now() - 15m",
|
|
||||||
Upper: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test multible tags not accepted",
|
|
||||||
influxQL: `SELECT usage_user from telegraf.autogen.cpu where time > now() - 15m and "host" != 'myhost' and "cpu" != 'cpu-total'`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Database: "telegraf",
|
|
||||||
Measurement: "cpu",
|
|
||||||
RetentionPolicy: "autogen",
|
|
||||||
Fields: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "usage_user",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Tags: map[string][]string{
|
|
||||||
"host": {
|
|
||||||
"myhost",
|
|
||||||
},
|
|
||||||
"cpu": {
|
|
||||||
"cpu-total",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Time: "",
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
AreTagsAccepted: false,
|
|
||||||
Range: &chronograf.DurationRange{
|
|
||||||
Lower: "now() - 15m",
|
|
||||||
Upper: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test mixed tag logic",
|
|
||||||
influxQL: `SELECT usage_user from telegraf.autogen.cpu where ("host" = 'myhost' or "this" = 'those') and ("howdy" != 'doody') and time > now() - 15m`,
|
|
||||||
RawText: `SELECT usage_user from telegraf.autogen.cpu where ("host" = 'myhost' or "this" = 'those') and ("howdy" != 'doody') and time > now() - 15m`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Fields: []chronograf.Field{},
|
|
||||||
Tags: map[string][]string{},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test tags accepted",
|
|
||||||
influxQL: `SELECT usage_user from telegraf.autogen.cpu where ("host" = 'myhost' OR "host" = 'yourhost') and ("these" = 'those') and time > now() - 15m`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Database: "telegraf",
|
|
||||||
Measurement: "cpu",
|
|
||||||
RetentionPolicy: "autogen",
|
|
||||||
Fields: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "usage_user",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Tags: map[string][]string{
|
|
||||||
"host": {"myhost", "yourhost"},
|
|
||||||
"these": {"those"},
|
|
||||||
},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Time: "",
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
AreTagsAccepted: true,
|
|
||||||
Range: &chronograf.DurationRange{
|
|
||||||
Lower: "now() - 15m",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Complex Logic with tags not accepted",
|
|
||||||
influxQL: `SELECT "usage_idle", "usage_guest_nice", "usage_system", "usage_guest" FROM "telegraf"."autogen"."cpu" WHERE time > now() - 15m AND ("cpu"!='cpu-total' OR "cpu"!='cpu0') AND ("host"!='dev-052978d6-us-east-2-meta-0' OR "host"!='dev-052978d6-us-east-2-data-5' OR "host"!='dev-052978d6-us-east-2-data-4' OR "host"!='dev-052978d6-us-east-2-data-3')`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Database: "telegraf",
|
|
||||||
Measurement: "cpu",
|
|
||||||
RetentionPolicy: "autogen",
|
|
||||||
Fields: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "usage_idle",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: "usage_guest_nice",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: "usage_system",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: "usage_guest",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Tags: map[string][]string{
|
|
||||||
"host": {
|
|
||||||
"dev-052978d6-us-east-2-meta-0",
|
|
||||||
"dev-052978d6-us-east-2-data-5",
|
|
||||||
"dev-052978d6-us-east-2-data-4",
|
|
||||||
"dev-052978d6-us-east-2-data-3",
|
|
||||||
},
|
|
||||||
"cpu": {
|
|
||||||
"cpu-total",
|
|
||||||
"cpu0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Time: "",
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
AreTagsAccepted: false,
|
|
||||||
Range: &chronograf.DurationRange{
|
|
||||||
Lower: "now() - 15m",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Complex Logic with tags accepted",
|
|
||||||
influxQL: `SELECT "usage_idle", "usage_guest_nice", "usage_system", "usage_guest" FROM "telegraf"."autogen"."cpu" WHERE time > now() - 15m AND ("cpu" = 'cpu-total' OR "cpu" = 'cpu0') AND ("host" = 'dev-052978d6-us-east-2-meta-0' OR "host" = 'dev-052978d6-us-east-2-data-5' OR "host" = 'dev-052978d6-us-east-2-data-4' OR "host" = 'dev-052978d6-us-east-2-data-3')`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Database: "telegraf",
|
|
||||||
Measurement: "cpu",
|
|
||||||
RetentionPolicy: "autogen",
|
|
||||||
Fields: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "usage_idle",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: "usage_guest_nice",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: "usage_system",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: "usage_guest",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Tags: map[string][]string{
|
|
||||||
"host": {
|
|
||||||
"dev-052978d6-us-east-2-meta-0",
|
|
||||||
"dev-052978d6-us-east-2-data-5",
|
|
||||||
"dev-052978d6-us-east-2-data-4",
|
|
||||||
"dev-052978d6-us-east-2-data-3",
|
|
||||||
},
|
|
||||||
"cpu": {
|
|
||||||
"cpu-total",
|
|
||||||
"cpu0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Time: "",
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
AreTagsAccepted: true,
|
|
||||||
Range: &chronograf.DurationRange{
|
|
||||||
Lower: "now() - 15m",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test explicit non-null fill accepted",
|
|
||||||
influxQL: `SELECT mean("usage_idle") FROM "telegraf"."autogen"."cpu" WHERE time > now() - 15m GROUP BY time(1m) FILL(linear)`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Database: "telegraf",
|
|
||||||
Measurement: "cpu",
|
|
||||||
RetentionPolicy: "autogen",
|
|
||||||
Fields: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "mean",
|
|
||||||
Type: "func",
|
|
||||||
Args: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "usage_idle",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Time: "1m",
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
Tags: map[string][]string{},
|
|
||||||
AreTagsAccepted: false,
|
|
||||||
Fill: "linear",
|
|
||||||
Range: &chronograf.DurationRange{
|
|
||||||
Lower: "now() - 15m",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test explicit null fill accepted",
|
|
||||||
influxQL: `SELECT mean("usage_idle") FROM "telegraf"."autogen"."cpu" WHERE time > now() - 15m GROUP BY time(1m) FILL(null)`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Database: "telegraf",
|
|
||||||
Measurement: "cpu",
|
|
||||||
RetentionPolicy: "autogen",
|
|
||||||
Fields: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "mean",
|
|
||||||
Type: "func",
|
|
||||||
Args: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "usage_idle",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Time: "1m",
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
Tags: map[string][]string{},
|
|
||||||
AreTagsAccepted: false,
|
|
||||||
Fill: "null",
|
|
||||||
Range: &chronograf.DurationRange{
|
|
||||||
Lower: "now() - 15m",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test implicit null fill accepted and made explicit",
|
|
||||||
influxQL: `SELECT mean("usage_idle") as "mean_usage_idle" FROM "telegraf"."autogen"."cpu" WHERE time > now() - 15m GROUP BY time(1m)`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Database: "telegraf",
|
|
||||||
Measurement: "cpu",
|
|
||||||
RetentionPolicy: "autogen",
|
|
||||||
Fields: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "mean",
|
|
||||||
Type: "func",
|
|
||||||
Alias: "mean_usage_idle",
|
|
||||||
Args: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "usage_idle",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Time: "1m",
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
Tags: map[string][]string{},
|
|
||||||
AreTagsAccepted: false,
|
|
||||||
Fill: "null",
|
|
||||||
Range: &chronograf.DurationRange{
|
|
||||||
Lower: "now() - 15m",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test percentile with a number parameter",
|
|
||||||
influxQL: `SELECT percentile("usage_idle", 3.14) as "mean_usage_idle" FROM "telegraf"."autogen"."cpu" WHERE time > now() - 15m GROUP BY time(1m)`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Database: "telegraf",
|
|
||||||
Measurement: "cpu",
|
|
||||||
RetentionPolicy: "autogen",
|
|
||||||
Fields: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "percentile",
|
|
||||||
Type: "func",
|
|
||||||
Alias: "mean_usage_idle",
|
|
||||||
Args: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "usage_idle",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: "3.14",
|
|
||||||
Type: "number",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Time: "1m",
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
Tags: map[string][]string{},
|
|
||||||
AreTagsAccepted: false,
|
|
||||||
Fill: "null",
|
|
||||||
Range: &chronograf.DurationRange{
|
|
||||||
Lower: "now() - 15m",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test top with 2 arguments",
|
|
||||||
influxQL: `SELECT TOP("water_level","location",2) FROM "h2o_feet"`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Measurement: "h2o_feet",
|
|
||||||
Fields: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "top",
|
|
||||||
Type: "func",
|
|
||||||
Args: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "water_level",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: "location",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Value: "2",
|
|
||||||
Type: "integer",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
Tags: map[string][]string{},
|
|
||||||
AreTagsAccepted: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "count of a regex",
|
|
||||||
influxQL: ` SELECT COUNT(/water/) FROM "h2o_feet"`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Measurement: "h2o_feet",
|
|
||||||
Fields: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "count",
|
|
||||||
Type: "func",
|
|
||||||
Args: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "water",
|
|
||||||
Type: "regex",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
Tags: map[string][]string{},
|
|
||||||
AreTagsAccepted: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "count with aggregate",
|
|
||||||
influxQL: `SELECT COUNT(water) as "count_water" FROM "h2o_feet"`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Measurement: "h2o_feet",
|
|
||||||
Fields: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "count",
|
|
||||||
Type: "func",
|
|
||||||
Alias: "count_water",
|
|
||||||
Args: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "water",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
Tags: map[string][]string{},
|
|
||||||
AreTagsAccepted: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "count of a wildcard",
|
|
||||||
influxQL: ` SELECT COUNT(*) FROM "h2o_feet"`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Measurement: "h2o_feet",
|
|
||||||
Fields: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "count",
|
|
||||||
Type: "func",
|
|
||||||
Args: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "*",
|
|
||||||
Type: "wildcard",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
Tags: map[string][]string{},
|
|
||||||
AreTagsAccepted: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test fill number (int) accepted",
|
|
||||||
influxQL: `SELECT mean("usage_idle") FROM "telegraf"."autogen"."cpu" WHERE time > now() - 15m GROUP BY time(1m) FILL(1337)`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Database: "telegraf",
|
|
||||||
Measurement: "cpu",
|
|
||||||
RetentionPolicy: "autogen",
|
|
||||||
Fields: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "mean",
|
|
||||||
Type: "func",
|
|
||||||
Args: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "usage_idle",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Time: "1m",
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
Tags: map[string][]string{},
|
|
||||||
AreTagsAccepted: false,
|
|
||||||
Fill: "1337",
|
|
||||||
Range: &chronograf.DurationRange{
|
|
||||||
Lower: "now() - 15m",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test fill number (float) accepted",
|
|
||||||
influxQL: `SELECT mean("usage_idle") FROM "telegraf"."autogen"."cpu" WHERE time > now() - 15m GROUP BY time(1m) FILL(1.337)`,
|
|
||||||
want: chronograf.QueryConfig{
|
|
||||||
Database: "telegraf",
|
|
||||||
Measurement: "cpu",
|
|
||||||
RetentionPolicy: "autogen",
|
|
||||||
Fields: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "mean",
|
|
||||||
Type: "func",
|
|
||||||
Args: []chronograf.Field{
|
|
||||||
{
|
|
||||||
Value: "usage_idle",
|
|
||||||
Type: "field",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
GroupBy: chronograf.GroupBy{
|
|
||||||
Time: "1m",
|
|
||||||
Tags: []string{},
|
|
||||||
},
|
|
||||||
Tags: map[string][]string{},
|
|
||||||
AreTagsAccepted: false,
|
|
||||||
Fill: "1.337",
|
|
||||||
Range: &chronograf.DurationRange{
|
|
||||||
Lower: "now() - 15m",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test invalid fill rejected",
|
|
||||||
influxQL: `SELECT mean("usage_idle") FROM "telegraf"."autogen"."cpu" WHERE time > now() - 15m GROUP BY time(1m) FILL(LINEAR)`,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := Convert(tt.influxQL)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("Convert() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if tt.RawText != "" {
|
|
||||||
tt.want.RawText = &tt.RawText
|
|
||||||
if got.RawText == nil {
|
|
||||||
t.Errorf("Convert() = nil, want %s", tt.RawText)
|
|
||||||
} else if *got.RawText != tt.RawText {
|
|
||||||
t.Errorf("Convert() = %s, want %s", *got.RawText, tt.RawText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !cmp.Equal(got, tt.want) {
|
|
||||||
t.Errorf("Convert() = %s", cmp.Diff(got, tt.want))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseTime(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
influxQL string
|
|
||||||
now string
|
|
||||||
want time.Duration
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "time equal",
|
|
||||||
now: "2000-01-01T00:00:00Z",
|
|
||||||
influxQL: `SELECT mean("numSeries") AS "mean_numSeries" FROM "_internal"."monitor"."database" WHERE time > now() - 1h and time < now() - 1h GROUP BY :interval: FILL(null);`,
|
|
||||||
want: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "time shifted by one hour",
|
|
||||||
now: "2000-01-01T00:00:00Z",
|
|
||||||
influxQL: `SELECT mean("numSeries") AS "mean_numSeries" FROM "_internal"."monitor"."database" WHERE time > now() - 1h - 1h and time < now() - 1h GROUP BY :interval: FILL(null);`,
|
|
||||||
want: 3599999999998,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
now, err := time.Parse(time.RFC3339, tt.now)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%v", err)
|
|
||||||
}
|
|
||||||
got, err := ParseTime(tt.influxQL, now)
|
|
||||||
if (err != nil) != tt.wantErr {
|
|
||||||
t.Errorf("ParseTime() error = %v, wantErr %v", err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if got != tt.want {
|
|
||||||
t.Logf("%d", got)
|
|
||||||
t.Errorf("ParseTime() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,230 +0,0 @@
|
||||||
package influx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Add a new User in InfluxDB
|
|
||||||
func (c *Client) Add(ctx context.Context, u *chronograf.User) (*chronograf.User, error) {
|
|
||||||
_, err := c.Query(ctx, chronograf.Query{
|
|
||||||
Command: fmt.Sprintf(`CREATE USER "%s" WITH PASSWORD '%s'`, u.Name, u.Passwd),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, p := range u.Permissions {
|
|
||||||
if err := c.grantPermission(ctx, u.Name, p); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c.Get(ctx, chronograf.UserQuery{Name: &u.Name})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the User from InfluxDB
|
|
||||||
func (c *Client) Delete(ctx context.Context, u *chronograf.User) error {
|
|
||||||
res, err := c.Query(ctx, chronograf.Query{
|
|
||||||
Command: fmt.Sprintf(`DROP USER "%s"`, u.Name),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// The DROP USER statement puts the error within the results itself
|
|
||||||
// So, we have to crack open the results to see what happens
|
|
||||||
octets, err := res.MarshalJSON()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
results := make([]struct{ Error string }, 0)
|
|
||||||
if err := json.Unmarshal(octets, &results); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// At last, we can check if there are any error strings
|
|
||||||
for _, r := range results {
|
|
||||||
if r.Error != "" {
|
|
||||||
return fmt.Errorf(r.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get retrieves a user if name exists.
|
|
||||||
func (c *Client) Get(ctx context.Context, q chronograf.UserQuery) (*chronograf.User, error) {
|
|
||||||
if q.Name == nil {
|
|
||||||
return nil, fmt.Errorf("query must specify name")
|
|
||||||
}
|
|
||||||
|
|
||||||
users, err := c.showUsers(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, user := range users {
|
|
||||||
if user.Name == *q.Name {
|
|
||||||
perms, err := c.userPermissions(ctx, user.Name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
user.Permissions = append(user.Permissions, perms...)
|
|
||||||
return &user, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("user not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the user's permissions or roles
|
|
||||||
func (c *Client) Update(ctx context.Context, u *chronograf.User) error {
|
|
||||||
// Only allow one type of change at a time. If it is a password
|
|
||||||
// change then do it and return without any changes to permissions
|
|
||||||
if u.Passwd != "" {
|
|
||||||
return c.updatePassword(ctx, u.Name, u.Passwd)
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := c.Get(ctx, chronograf.UserQuery{Name: &u.Name})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
revoke, add := Difference(u.Permissions, user.Permissions)
|
|
||||||
for _, a := range add {
|
|
||||||
if err := c.grantPermission(ctx, u.Name, a); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range revoke {
|
|
||||||
if err := c.revokePermission(ctx, u.Name, r); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// All users in influx
|
|
||||||
func (c *Client) All(ctx context.Context) ([]chronograf.User, error) {
|
|
||||||
users, err := c.showUsers(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// For all users we need to look up permissions to add to the user.
|
|
||||||
for i, user := range users {
|
|
||||||
perms, err := c.userPermissions(ctx, user.Name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
user.Permissions = append(user.Permissions, perms...)
|
|
||||||
users[i] = user
|
|
||||||
}
|
|
||||||
return users, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Num is the number of users in DB
|
|
||||||
func (c *Client) Num(ctx context.Context) (int, error) {
|
|
||||||
all, err := c.All(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(all), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// showUsers runs SHOW USERS InfluxQL command and returns chronograf users.
|
|
||||||
func (c *Client) showUsers(ctx context.Context) ([]chronograf.User, error) {
|
|
||||||
res, err := c.Query(ctx, chronograf.Query{
|
|
||||||
Command: `SHOW USERS`,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
octets, err := res.MarshalJSON()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
results := showResults{}
|
|
||||||
if err := json.Unmarshal(octets, &results); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return results.Users(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) grantPermission(ctx context.Context, username string, perm chronograf.Permission) error {
|
|
||||||
query := ToGrant(username, perm)
|
|
||||||
if query == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := c.Query(ctx, chronograf.Query{
|
|
||||||
Command: query,
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) revokePermission(ctx context.Context, username string, perm chronograf.Permission) error {
|
|
||||||
query := ToRevoke(username, perm)
|
|
||||||
if query == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := c.Query(ctx, chronograf.Query{
|
|
||||||
Command: query,
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) userPermissions(ctx context.Context, name string) (chronograf.Permissions, error) {
|
|
||||||
res, err := c.Query(ctx, chronograf.Query{
|
|
||||||
Command: fmt.Sprintf(`SHOW GRANTS FOR "%s"`, name),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
octets, err := res.MarshalJSON()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
results := showResults{}
|
|
||||||
if err := json.Unmarshal(octets, &results); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return results.Permissions(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) updatePassword(ctx context.Context, name, passwd string) error {
|
|
||||||
res, err := c.Query(ctx, chronograf.Query{
|
|
||||||
Command: fmt.Sprintf(`SET PASSWORD for "%s" = '%s'`, name, passwd),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// The SET PASSWORD statements puts the error within the results itself
|
|
||||||
// So, we have to crack open the results to see what happens
|
|
||||||
octets, err := res.MarshalJSON()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
results := make([]struct{ Error string }, 0)
|
|
||||||
if err := json.Unmarshal(octets, &results); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// At last, we can check if there are any error strings
|
|
||||||
for _, r := range results {
|
|
||||||
if r.Error != "" {
|
|
||||||
return fmt.Errorf(r.Error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,159 +0,0 @@
|
||||||
package chronograf
|
|
||||||
|
|
||||||
import "encoding/json"
|
|
||||||
|
|
||||||
// AlertNodes defines all possible kapacitor interactions with an alert.
|
|
||||||
type AlertNodes struct {
|
|
||||||
IsStateChangesOnly bool `json:"stateChangesOnly"` // IsStateChangesOnly will only send alerts on state changes.
|
|
||||||
UseFlapping bool `json:"useFlapping"` // UseFlapping enables flapping detection. Flapping occurs when a service or host changes state too frequently, resulting in a storm of problem and recovery notification
|
|
||||||
Posts []*Post `json:"post"` // HTTPPost will post the JSON alert data to the specified URLs.
|
|
||||||
TCPs []*TCP `json:"tcp"` // TCP will send the JSON alert data to the specified endpoint via TCP.
|
|
||||||
Email []*Email `json:"email"` // Email will send alert data to the specified emails.
|
|
||||||
Exec []*Exec `json:"exec"` // Exec will run shell commands when an alert triggers
|
|
||||||
Log []*Log `json:"log"` // Log will log JSON alert data to files in JSON lines format.
|
|
||||||
VictorOps []*VictorOps `json:"victorOps"` // VictorOps will send alert to all VictorOps
|
|
||||||
PagerDuty []*PagerDuty `json:"pagerDuty"` // PagerDuty will send alert to all PagerDuty
|
|
||||||
PagerDuty2 []*PagerDuty `json:"pagerDuty2"` // PagerDuty2 will send alert to PagerDuty v2
|
|
||||||
Pushover []*Pushover `json:"pushover"` // Pushover will send alert to all Pushover
|
|
||||||
Sensu []*Sensu `json:"sensu"` // Sensu will send alert to all Sensu
|
|
||||||
Slack []*Slack `json:"slack"` // Slack will send alert to Slack
|
|
||||||
Telegram []*Telegram `json:"telegram"` // Telegram will send alert to all Telegram
|
|
||||||
HipChat []*HipChat `json:"hipChat"` // HipChat will send alert to all HipChat
|
|
||||||
Alerta []*Alerta `json:"alerta"` // Alerta will send alert to all Alerta
|
|
||||||
OpsGenie []*OpsGenie `json:"opsGenie"` // OpsGenie will send alert to all OpsGenie
|
|
||||||
OpsGenie2 []*OpsGenie `json:"opsGenie2"` // OpsGenie2 will send alert to all OpsGenie v2
|
|
||||||
Talk []*Talk `json:"talk"` // Talk will send alert to all Talk
|
|
||||||
Kafka []*Kafka `json:"kafka"` // Kafka will send alert to all Kafka
|
|
||||||
}
|
|
||||||
|
|
||||||
// Post will POST alerts to a destination URL
|
|
||||||
type Post struct {
|
|
||||||
URL string `json:"url"` // URL is the destination of the POST.
|
|
||||||
Headers map[string]string `json:"headers"` // Headers are added to the output POST
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log sends the output of the alert to a file
|
|
||||||
type Log struct {
|
|
||||||
FilePath string `json:"filePath"` // Absolute path the the log file; it will be created if it does not exist.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alerta sends the output of the alert to an alerta service
|
|
||||||
type Alerta struct {
|
|
||||||
Token string `json:"token"` // Token is the authentication token that overrides the global configuration.
|
|
||||||
Resource string `json:"resource"` // Resource under alarm, deliberately not host-centric
|
|
||||||
Event string `json:"event"` // Event is the event name eg. NodeDown, QUEUE:LENGTH:EXCEEDED
|
|
||||||
Environment string `json:"environment"` // Environment is the effected environment; used to namespace the resource
|
|
||||||
Group string `json:"group"` // Group is an event group used to group events of similar type
|
|
||||||
Value string `json:"value"` // Value is the event value eg. 100%, Down, PingFail, 55ms, ORA-1664
|
|
||||||
Origin string `json:"origin"` // Origin is the name of monitoring component that generated the alert
|
|
||||||
Service []string `json:"service"` // Service is the list of affected services
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exec executes a shell command on an alert
|
|
||||||
type Exec struct {
|
|
||||||
Command []string `json:"command"` // Command is the space separated command and args to execute.
|
|
||||||
}
|
|
||||||
|
|
||||||
// TCP sends the alert to the address
|
|
||||||
type TCP struct {
|
|
||||||
Address string `json:"address"` // Endpoint is the Address and port to send the alert
|
|
||||||
}
|
|
||||||
|
|
||||||
// Email sends the alert to a list of email addresses
|
|
||||||
type Email struct {
|
|
||||||
To []string `json:"to"` // ToList is the list of email recipients.
|
|
||||||
}
|
|
||||||
|
|
||||||
// VictorOps sends alerts to the victorops.com service
|
|
||||||
type VictorOps struct {
|
|
||||||
RoutingKey string `json:"routingKey"` // RoutingKey is what is used to map the alert to a team
|
|
||||||
}
|
|
||||||
|
|
||||||
// PagerDuty sends alerts to the pagerduty.com service
|
|
||||||
type PagerDuty struct {
|
|
||||||
ServiceKey string `json:"serviceKey"` // ServiceKey is the GUID of one of the "Generic API" integrations
|
|
||||||
}
|
|
||||||
|
|
||||||
// HipChat sends alerts to stride.com
|
|
||||||
type HipChat struct {
|
|
||||||
Room string `json:"room"` // Room is the HipChat room to post messages.
|
|
||||||
Token string `json:"token"` // Token is the HipChat authentication token.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sensu sends alerts to sensu or sensuapp.org
|
|
||||||
type Sensu struct {
|
|
||||||
Source string `json:"source"` // Source is the check source, used to create a proxy client for an external resource
|
|
||||||
Handlers []string `json:"handlers"` // Handlers are Sensu event handlers are for taking action on events
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pushover sends alerts to pushover.net
|
|
||||||
type Pushover struct {
|
|
||||||
// UserKey is the User/Group key of your user (or you), viewable when logged
|
|
||||||
// into the Pushover dashboard. Often referred to as USER_KEY
|
|
||||||
// in the Pushover documentation.
|
|
||||||
UserKey string `json:"userKey"`
|
|
||||||
|
|
||||||
// Device is the users device name to send message directly to that device,
|
|
||||||
// rather than all of a user's devices (multiple device names may
|
|
||||||
// be separated by a comma)
|
|
||||||
Device string `json:"device"`
|
|
||||||
|
|
||||||
// Title is your message's title, otherwise your apps name is used
|
|
||||||
Title string `json:"title"`
|
|
||||||
|
|
||||||
// URL is a supplementary URL to show with your message
|
|
||||||
URL string `json:"url"`
|
|
||||||
|
|
||||||
// URLTitle is a title for your supplementary URL, otherwise just URL is shown
|
|
||||||
URLTitle string `json:"urlTitle"`
|
|
||||||
|
|
||||||
// Sound is the name of one of the sounds supported by the device clients to override
|
|
||||||
// the user's default sound choice
|
|
||||||
Sound string `json:"sound"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slack sends alerts to a slack.com channel
|
|
||||||
type Slack struct {
|
|
||||||
Channel string `json:"channel"` // Slack channel in which to post messages.
|
|
||||||
Username string `json:"username"` // Username of the Slack bot.
|
|
||||||
IconEmoji string `json:"iconEmoji"` // IconEmoji is an emoji name surrounded in ':' characters; The emoji image will replace the normal user icon for the slack bot.
|
|
||||||
Workspace string `json:"workspace"` // Workspace is the slack workspace for the alert handler
|
|
||||||
}
|
|
||||||
|
|
||||||
// Telegram sends alerts to telegram.org
|
|
||||||
type Telegram struct {
|
|
||||||
ChatID string `json:"chatId"` // ChatID is the Telegram user/group ID to post messages to.
|
|
||||||
ParseMode string `json:"parseMode"` // ParseMode tells telegram how to render the message (Markdown or HTML)
|
|
||||||
DisableWebPagePreview bool `json:"disableWebPagePreview"` // IsDisableWebPagePreview will disables link previews in alert messages.
|
|
||||||
DisableNotification bool `json:"disableNotification"` // IsDisableNotification will disables notifications on iOS devices and disables sounds on Android devices. Android users continue to receive notifications.
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpsGenie sends alerts to opsgenie.com
|
|
||||||
type OpsGenie struct {
|
|
||||||
Teams []string `json:"teams"` // Teams that the alert will be routed to send notifications
|
|
||||||
Recipients []string `json:"recipients"` // Recipients can be a single user, group, escalation, or schedule (https://docs.opsgenie.com/docs/alert-recipients-and-teams)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Talk sends alerts to Jane Talk (https://jianliao.com/site)
|
|
||||||
type Talk struct{}
|
|
||||||
|
|
||||||
// Kafka sends alerts to any Kafka brokers specified in the handler config
|
|
||||||
type Kafka struct {
|
|
||||||
Cluster string `json:"cluster"`
|
|
||||||
Topic string `json:"kafka-topic"`
|
|
||||||
Template string `json:"template"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON converts AlertNodes to JSON
|
|
||||||
func (n *AlertNodes) MarshalJSON() ([]byte, error) {
|
|
||||||
type Alias AlertNodes
|
|
||||||
var raw = &struct {
|
|
||||||
Type string `json:"typeOf"`
|
|
||||||
*Alias
|
|
||||||
}{
|
|
||||||
Type: "alert",
|
|
||||||
Alias: (*Alias)(n),
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.Marshal(raw)
|
|
||||||
}
|
|
|
@ -1,88 +0,0 @@
|
||||||
package mocks
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewLogger returns a mock logger that implements chronograf.Logger
|
|
||||||
func NewLogger() chronograf.Logger {
|
|
||||||
return &TestLogger{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type LogMessage struct {
|
|
||||||
Level string
|
|
||||||
Body string
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLogger is a chronograf.Logger which allows assertions to be made on the
|
|
||||||
// contents of its messages.
|
|
||||||
type TestLogger struct {
|
|
||||||
Messages []LogMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tl *TestLogger) Debug(args ...interface{}) {
|
|
||||||
tl.Messages = append(tl.Messages, LogMessage{"debug", tl.stringify(args...)})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tl *TestLogger) Info(args ...interface{}) {
|
|
||||||
tl.Messages = append(tl.Messages, LogMessage{"info", tl.stringify(args...)})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tl *TestLogger) Error(args ...interface{}) {
|
|
||||||
tl.Messages = append(tl.Messages, LogMessage{"error", tl.stringify(args...)})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tl *TestLogger) WithField(key string, value interface{}) chronograf.Logger {
|
|
||||||
return tl
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tl *TestLogger) Writer() *io.PipeWriter {
|
|
||||||
_, write := io.Pipe()
|
|
||||||
return write
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasMessage will return true if the TestLogger has been called with an exact
|
|
||||||
// match of a particular log message at a particular log level
|
|
||||||
func (tl *TestLogger) HasMessage(level string, body string) bool {
|
|
||||||
for _, msg := range tl.Messages {
|
|
||||||
if msg.Level == level && msg.Body == body {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tl *TestLogger) stringify(args ...interface{}) string {
|
|
||||||
out := []byte{}
|
|
||||||
for _, arg := range args[:len(args)-1] {
|
|
||||||
out = append(out, tl.stringifyArg(arg)...)
|
|
||||||
out = append(out, []byte(" ")...)
|
|
||||||
}
|
|
||||||
out = append(out, tl.stringifyArg(args[len(args)-1])...)
|
|
||||||
return string(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tl *TestLogger) stringifyArg(arg interface{}) []byte {
|
|
||||||
switch a := arg.(type) {
|
|
||||||
case fmt.Stringer:
|
|
||||||
return []byte(a.String())
|
|
||||||
case error:
|
|
||||||
return []byte(a.Error())
|
|
||||||
case string:
|
|
||||||
return []byte(a)
|
|
||||||
default:
|
|
||||||
return []byte("UNKNOWN")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dump dumps out logs into a given testing.T's logs
|
|
||||||
func (tl *TestLogger) Dump(t *testing.T) {
|
|
||||||
t.Log("== Dumping Test Logs ==")
|
|
||||||
for _, msg := range tl.Messages {
|
|
||||||
t.Logf("lvl: %s, msg: %s", msg.Level, msg.Body)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
package mocks
|
|
||||||
|
|
||||||
// NewResponse returns a mocked chronograf.Response
|
|
||||||
func NewResponse(res string, err error) *Response {
|
|
||||||
return &Response{
|
|
||||||
res: res,
|
|
||||||
err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Response is a mocked chronograf.Response
|
|
||||||
type Response struct {
|
|
||||||
res string
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalJSON returns the res and err as the fake response.
|
|
||||||
func (r *Response) MarshalJSON() ([]byte, error) {
|
|
||||||
return []byte(r.res), r.err
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
package mocks
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ chronograf.TimeSeries = &TimeSeries{}
|
|
||||||
|
|
||||||
// TimeSeries is a mockable chronograf time series by overriding the functions.
|
|
||||||
type TimeSeries struct {
|
|
||||||
// Connect will connect to the time series using the information in `Source`.
|
|
||||||
ConnectF func(context.Context, *chronograf.Source) error
|
|
||||||
// Query retrieves time series data from the database.
|
|
||||||
QueryF func(context.Context, chronograf.Query) (chronograf.Response, error)
|
|
||||||
// Write records points into the TimeSeries
|
|
||||||
WriteF func(context.Context, []chronograf.Point) error
|
|
||||||
// UsersStore represents the user accounts within the TimeSeries database
|
|
||||||
UsersF func(context.Context) chronograf.UsersStore
|
|
||||||
// Permissions returns all valid names permissions in this database
|
|
||||||
PermissionsF func(context.Context) chronograf.Permissions
|
|
||||||
// RolesF represents the roles. Roles group permissions and Users
|
|
||||||
RolesF func(context.Context) (chronograf.RolesStore, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// New implements TimeSeriesClient
|
|
||||||
func (t *TimeSeries) New(chronograf.Source, chronograf.Logger) (chronograf.TimeSeries, error) {
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect will connect to the time series using the information in `Source`.
|
|
||||||
func (t *TimeSeries) Connect(ctx context.Context, src *chronograf.Source) error {
|
|
||||||
return t.ConnectF(ctx, src)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query retrieves time series data from the database.
|
|
||||||
func (t *TimeSeries) Query(ctx context.Context, query chronograf.Query) (chronograf.Response, error) {
|
|
||||||
return t.QueryF(ctx, query)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write records a point into the time series
|
|
||||||
func (t *TimeSeries) Write(ctx context.Context, points []chronograf.Point) error {
|
|
||||||
return t.WriteF(ctx, points)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Users represents the user accounts within the TimeSeries database
|
|
||||||
func (t *TimeSeries) Users(ctx context.Context) chronograf.UsersStore {
|
|
||||||
return t.UsersF(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Roles represents the roles. Roles group permissions and Users
|
|
||||||
func (t *TimeSeries) Roles(ctx context.Context) (chronograf.RolesStore, error) {
|
|
||||||
return t.RolesF(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Permissions returns all valid names permissions in this database
|
|
||||||
func (t *TimeSeries) Permissions(ctx context.Context) chronograf.Permissions {
|
|
||||||
return t.PermissionsF(ctx)
|
|
||||||
}
|
|
|
@ -763,7 +763,6 @@ func (m *Launcher) run(ctx context.Context, opts *InfluxdOpts) (err error) {
|
||||||
HTTPErrorHandler: kithttp.ErrorHandler(0),
|
HTTPErrorHandler: kithttp.ErrorHandler(0),
|
||||||
Logger: m.log,
|
Logger: m.log,
|
||||||
SessionRenewDisabled: opts.SessionRenewDisabled,
|
SessionRenewDisabled: opts.SessionRenewDisabled,
|
||||||
NewBucketService: source.NewBucketService,
|
|
||||||
NewQueryService: source.NewQueryService,
|
NewQueryService: source.NewQueryService,
|
||||||
PointsWriter: &storage.LoggingPointsWriter{
|
PointsWriter: &storage.LoggingPointsWriter{
|
||||||
Underlying: pointsWriter,
|
Underlying: pointsWriter,
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -70,7 +70,7 @@ require (
|
||||||
github.com/prometheus/client_model v0.2.0
|
github.com/prometheus/client_model v0.2.0
|
||||||
github.com/prometheus/common v0.9.1
|
github.com/prometheus/common v0.9.1
|
||||||
github.com/retailnext/hllpp v1.0.0
|
github.com/retailnext/hllpp v1.0.0
|
||||||
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b
|
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect
|
||||||
github.com/spf13/cast v1.3.0
|
github.com/spf13/cast v1.3.0
|
||||||
github.com/spf13/cobra v1.0.0
|
github.com/spf13/cobra v1.0.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
|
|
|
@ -54,8 +54,7 @@ type APIBackend struct {
|
||||||
// write request. A value of zero specifies there is no limit.
|
// write request. A value of zero specifies there is no limit.
|
||||||
WriteParserMaxValues int
|
WriteParserMaxValues int
|
||||||
|
|
||||||
NewBucketService func(*influxdb.Source) (influxdb.BucketService, error)
|
NewQueryService func(*influxdb.Source) (query.ProxyQueryService, error)
|
||||||
NewQueryService func(*influxdb.Source) (query.ProxyQueryService, error)
|
|
||||||
|
|
||||||
WriteEventRecorder metric.EventRecorder
|
WriteEventRecorder metric.EventRecorder
|
||||||
QueryEventRecorder metric.EventRecorder
|
QueryEventRecorder metric.EventRecorder
|
||||||
|
|
|
@ -1,80 +0,0 @@
|
||||||
package influxdb
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
platform2 "github.com/influxdata/influxdb/v2/kit/platform"
|
|
||||||
|
|
||||||
platform "github.com/influxdata/influxdb/v2"
|
|
||||||
"github.com/influxdata/influxdb/v2/kit/tracing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BucketService connects to Influx via HTTP using tokens to manage buckets
|
|
||||||
type BucketService struct {
|
|
||||||
Source *platform.Source
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BucketService) FindBucketByName(ctx context.Context, orgID platform2.ID, n string) (*platform.Bucket, error) {
|
|
||||||
return nil, fmt.Errorf("not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BucketService) FindBucketByID(ctx context.Context, id platform2.ID) (*platform.Bucket, error) {
|
|
||||||
return nil, fmt.Errorf("not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BucketService) FindBucket(ctx context.Context, filter platform.BucketFilter) (*platform.Bucket, error) {
|
|
||||||
return nil, fmt.Errorf("not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BucketService) FindBuckets(ctx context.Context, filter platform.BucketFilter, opt ...platform.FindOptions) ([]*platform.Bucket, int, error) {
|
|
||||||
span, ctx := tracing.StartSpanFromContext(ctx)
|
|
||||||
defer span.Finish()
|
|
||||||
|
|
||||||
c, err := newClient(s.Source)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
dbs, err := c.AllDB(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
bs := []*platform.Bucket{}
|
|
||||||
for _, db := range dbs {
|
|
||||||
rps, err := c.AllRP(ctx, db.Name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
for _, rp := range rps {
|
|
||||||
d, err := time.ParseDuration(rp.Duration)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
b := &platform.Bucket{
|
|
||||||
// TODO(desa): what to do about IDs?
|
|
||||||
RetentionPeriod: d,
|
|
||||||
Name: db.Name,
|
|
||||||
RetentionPolicyName: rp.Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
bs = append(bs, b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bs, len(bs), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BucketService) CreateBucket(ctx context.Context, b *platform.Bucket) error {
|
|
||||||
return fmt.Errorf("not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BucketService) UpdateBucket(ctx context.Context, id platform2.ID, upd platform.BucketUpdate) (*platform.Bucket, error) {
|
|
||||||
return nil, fmt.Errorf("not supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *BucketService) DeleteBucket(ctx context.Context, id platform2.ID) error {
|
|
||||||
return fmt.Errorf("not supported")
|
|
||||||
}
|
|
|
@ -5,9 +5,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
platform "github.com/influxdata/influxdb/v2"
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf"
|
|
||||||
"github.com/influxdata/influxdb/v2/chronograf/influx"
|
|
||||||
"github.com/influxdata/influxdb/v2/kit/tracing"
|
"github.com/influxdata/influxdb/v2/kit/tracing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,37 +16,6 @@ var (
|
||||||
defaultTransport = &http.Transport{}
|
defaultTransport = &http.Transport{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func newClient(s *platform.Source) (*influx.Client, error) {
|
|
||||||
c := &influx.Client{}
|
|
||||||
url, err := url.Parse(s.URL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c.URL = url
|
|
||||||
c.Authorizer = DefaultAuthorization(s)
|
|
||||||
c.InsecureSkipVerify = s.InsecureSkipVerify
|
|
||||||
c.Logger = &chronograf.NoopLogger{}
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultAuthorization creates either a shared JWT builder, basic auth or Noop
|
|
||||||
// This is copy of the method from chronograf/influx adapted for platform sources.
|
|
||||||
func DefaultAuthorization(src *platform.Source) influx.Authorizer {
|
|
||||||
// Optionally, add the shared secret JWT token creation
|
|
||||||
if src.Username != "" && src.SharedSecret != "" {
|
|
||||||
return &influx.BearerJWT{
|
|
||||||
Username: src.Username,
|
|
||||||
SharedSecret: src.SharedSecret,
|
|
||||||
}
|
|
||||||
} else if src.Username != "" && src.Password != "" {
|
|
||||||
return &influx.BasicAuth{
|
|
||||||
Username: src.Username,
|
|
||||||
Password: src.Password,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &influx.NoAuthorization{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newURL(addr, path string) (*url.URL, error) {
|
func newURL(addr, path string) (*url.URL, error) {
|
||||||
u, err := url.Parse(addr)
|
u, err := url.Parse(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
package source
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
platform "github.com/influxdata/influxdb/v2"
|
|
||||||
"github.com/influxdata/influxdb/v2/http"
|
|
||||||
"github.com/influxdata/influxdb/v2/http/influxdb"
|
|
||||||
"github.com/influxdata/influxdb/v2/tenant"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewBucketService creates a bucket service from a source.
|
|
||||||
func NewBucketService(s *platform.Source) (platform.BucketService, error) {
|
|
||||||
switch s.Type {
|
|
||||||
case platform.SelfSourceType:
|
|
||||||
// TODO(fntlnz): this is supposed to call a bucket service directly locally,
|
|
||||||
// we are letting it err for now since we have some refactoring to do on
|
|
||||||
// how services are instantiated
|
|
||||||
return nil, fmt.Errorf("self source type not implemented")
|
|
||||||
case platform.V2SourceType:
|
|
||||||
httpClient, err := http.NewHTTPClient(s.URL, s.Token, s.InsecureSkipVerify)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &tenant.BucketClientService{Client: httpClient}, nil
|
|
||||||
case platform.V1SourceType:
|
|
||||||
return &influxdb.BucketService{Source: s}, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("unsupported source type %s", s.Type)
|
|
||||||
}
|
|
Loading…
Reference in New Issue