Beispiel #1
0
func mustParseStatement(s string) influxql.Statement {
	stmt, err := influxql.ParseStatement(s)
	if err != nil {
		panic(err)
	}
	return stmt
}
Beispiel #2
0
func (s *Service) execQuery(q, cluster string) (kapacitor.DBRP, *influxdb.Response, error) {
	// Parse query to determine dbrp
	dbrp := kapacitor.DBRP{}
	stmt, err := influxql.ParseStatement(q)
	if err != nil {
		return dbrp, nil, err
	}
	if slct, ok := stmt.(*influxql.SelectStatement); ok && len(slct.Sources) == 1 {
		if m, ok := slct.Sources[0].(*influxql.Measurement); ok {
			dbrp.Database = m.Database
			dbrp.RetentionPolicy = m.RetentionPolicy
		}
	}
	if dbrp.Database == "" || dbrp.RetentionPolicy == "" {
		return dbrp, nil, errors.New("could not determine database and retention policy. Is the query fully qualified?")
	}
	if s.InfluxDBService == nil {
		return dbrp, nil, errors.New("InfluxDB not configured, cannot record query")
	}
	// Query InfluxDB
	con, err := s.InfluxDBService.NewNamedClient(cluster)
	if err != nil {
		return dbrp, nil, errors.Wrap(err, "failed to get InfluxDB client")
	}
	query := influxdb.Query{
		Command: q,
	}
	resp, err := con.Query(query)
	if err != nil {
		return dbrp, nil, errors.Wrap(err, "InfluxDB query failed")
	}
	return dbrp, resp, nil
}
Beispiel #3
0
// Parse statements that might appear valid but should return an error.
// If allowed to execute, at least some of these statements would result in a panic.
func TestParse_Errors(t *testing.T) {
	for _, tt := range []struct {
		tmpl string
		good string
		bad  string
	}{
		// Second argument to derivative must be duration
		{tmpl: `SELECT derivative(f, %s) FROM m`, good: "1h", bad: "true"},
	} {
		good := fmt.Sprintf(tt.tmpl, tt.good)
		if _, err := influxql.ParseStatement(good); err != nil {
			t.Fatalf("statement %q should have parsed correctly but returned error: %s", good, err)
		}

		bad := fmt.Sprintf(tt.tmpl, tt.bad)
		if _, err := influxql.ParseStatement(bad); err == nil {
			t.Fatalf("statement %q should have resulted in a parse error but did not", bad)
		}
	}
}
Beispiel #4
0
func (s *Service) processExecuteStatementRequest(buf []byte) error {
	// Unmarshal the request.
	var req ExecuteStatementRequest
	if err := req.UnmarshalBinary(buf); err != nil {
		return err
	}

	// Parse the InfluxQL statement.
	stmt, err := influxql.ParseStatement(req.Statement())
	if err != nil {
		return err
	}

	return s.executeStatement(stmt, req.Database())
}
Beispiel #5
0
func influxTag(args []parse.Node) (parse.Tags, error) {
	st, err := influxql.ParseStatement(args[1].(*parse.StringNode).Text)
	if err != nil {
		return nil, err
	}
	s, ok := st.(*influxql.SelectStatement)
	if !ok {
		return nil, fmt.Errorf("influx: expected select statement")
	}

	t := make(parse.Tags, len(s.Dimensions))
	for _, d := range s.Dimensions {
		if _, ok := d.Expr.(*influxql.Call); ok {
			continue
		}
		t[d.String()] = struct{}{}
	}
	return t, nil
}
Beispiel #6
0
// ParseStatements takes a configFile and returns a slice of Statements
func ParseStatements(file string) ([]statement.Statement, error) {
	seq := []statement.Statement{}

	f, err := os.Open(file)
	check(err)

	s := NewScanner(f)

	for {
		t, l := s.Scan()

		if t == EOF {
			break
		}
		_, err := influxql.ParseStatement(l)
		if err == nil {

			seq = append(seq, &statement.InfluxqlStatement{Query: l, StatementID: stressql.RandStr(10)})
		} else if t == BREAK {
			continue
		} else {
			f := strings.NewReader(l)
			p := stressql.NewParser(f)
			s, err := p.Parse()
			if err != nil {
				return nil, err
			}
			seq = append(seq, s)

		}
	}

	f.Close()

	return seq, nil

}
func TestRewriteStatement(t *testing.T) {
	tests := []struct {
		stmt string
		s    string
	}{
		{
			stmt: `SHOW FIELD KEYS`,
			s:    `SELECT fieldKey FROM _fieldKeys`,
		},
		{
			stmt: `SHOW FIELD KEYS FROM cpu`,
			s:    `SELECT fieldKey FROM _fieldKeys WHERE _name = 'cpu'`,
		},
		{
			stmt: `SHOW FIELD KEYS FROM /c.*/`,
			s:    `SELECT fieldKey FROM _fieldKeys WHERE _name =~ /c.*/`,
		},
		{
			stmt: `SHOW FIELD KEYS FROM mydb.myrp2.cpu`,
			s:    `SELECT fieldKey FROM mydb.myrp2._fieldKeys WHERE _name = 'cpu'`,
		},
		{
			stmt: `SHOW FIELD KEYS FROM mydb.myrp2./c.*/`,
			s:    `SELECT fieldKey FROM mydb.myrp2._fieldKeys WHERE _name =~ /c.*/`,
		},
		{
			stmt: `SHOW MEASUREMENTS`,
			s:    `SELECT _name AS "name" FROM _measurements`,
		},
		{
			stmt: `SHOW MEASUREMENTS WITH MEASUREMENT = cpu`,
			s:    `SELECT _name AS "name" FROM _measurements WHERE _name = 'cpu'`,
		},
		{
			stmt: `SHOW MEASUREMENTS WITH MEASUREMENT =~ /c.*/`,
			s:    `SELECT _name AS "name" FROM _measurements WHERE _name =~ /c.*/`,
		},
		{
			stmt: `SHOW MEASUREMENTS WHERE region = 'uswest'`,
			s:    `SELECT _name AS "name" FROM _measurements WHERE region = 'uswest'`,
		},
		{
			stmt: `SHOW MEASUREMENTS WITH MEASUREMENT = cpu WHERE region = 'uswest'`,
			s:    `SELECT _name AS "name" FROM _measurements WHERE (_name = 'cpu') AND (region = 'uswest')`,
		},
		{
			stmt: `SHOW SERIES`,
			s:    `SELECT "key" FROM _series`,
		},
		{
			stmt: `SHOW SERIES FROM cpu`,
			s:    `SELECT "key" FROM _series WHERE _name = 'cpu'`,
		},
		{
			stmt: `SHOW SERIES FROM mydb.myrp1.cpu`,
			s:    `SELECT "key" FROM mydb.myrp1._series WHERE _name = 'cpu'`,
		},
		{
			stmt: `SHOW SERIES FROM mydb.myrp1./c.*/`,
			s:    `SELECT "key" FROM mydb.myrp1._series WHERE _name =~ /c.*/`,
		},
		{
			stmt: `SHOW TAG KEYS`,
			s:    `SELECT tagKey FROM _tagKeys`,
		},
		{
			stmt: `SHOW TAG KEYS FROM cpu`,
			s:    `SELECT tagKey FROM _tagKeys WHERE _name = 'cpu'`,
		},
		{
			stmt: `SHOW TAG KEYS FROM /c.*/`,
			s:    `SELECT tagKey FROM _tagKeys WHERE _name =~ /c.*/`,
		},
		{
			stmt: `SHOW TAG KEYS FROM cpu WHERE region = 'uswest'`,
			s:    `SELECT tagKey FROM _tagKeys WHERE (_name = 'cpu') AND (region = 'uswest')`,
		},
		{
			stmt: `SHOW TAG KEYS FROM mydb.myrp1.cpu`,
			s:    `SELECT tagKey FROM mydb.myrp1._tagKeys WHERE _name = 'cpu'`,
		},
		{
			stmt: `SHOW TAG KEYS FROM mydb.myrp1./c.*/`,
			s:    `SELECT tagKey FROM mydb.myrp1._tagKeys WHERE _name =~ /c.*/`,
		},
		{
			stmt: `SHOW TAG KEYS FROM mydb.myrp1.cpu WHERE region = 'uswest'`,
			s:    `SELECT tagKey FROM mydb.myrp1._tagKeys WHERE (_name = 'cpu') AND (region = 'uswest')`,
		},
		{
			stmt: `SHOW TAG VALUES WITH KEY = region`,
			s:    `SELECT _tagKey AS "key", value FROM _tags WHERE _tagKey = 'region'`,
		},
		{
			stmt: `SHOW TAG VALUES FROM cpu WITH KEY = region`,
			s:    `SELECT _tagKey AS "key", value FROM _tags WHERE (_name = 'cpu') AND (_tagKey = 'region')`,
		},
		{
			stmt: `SHOW TAG VALUES FROM cpu WITH KEY IN (region, host)`,
			s:    `SELECT _tagKey AS "key", value FROM _tags WHERE (_name = 'cpu') AND (_tagKey = 'region' OR _tagKey = 'host')`,
		},
		{
			stmt: `SHOW TAG VALUES FROM mydb.myrp1.cpu WITH KEY IN (region, host)`,
			s:    `SELECT _tagKey AS "key", value FROM mydb.myrp1._tags WHERE (_name = 'cpu') AND (_tagKey = 'region' OR _tagKey = 'host')`,
		},
		{
			stmt: `SELECT value FROM cpu`,
			s:    `SELECT value FROM cpu`,
		},
	}

	for _, test := range tests {
		stmt, err := influxql.ParseStatement(test.stmt)
		if err != nil {
			t.Errorf("error parsing statement: %s", err)
		} else {
			stmt, err = influxql.RewriteStatement(stmt)
			if err != nil {
				t.Errorf("error rewriting statement: %s", err)
			} else if s := stmt.String(); s != test.s {
				t.Errorf("error rendering string. expected %s, actual: %s", test.s, s)
			}
		}
	}
}
func TestRewriteStatement(t *testing.T) {
	tests := []struct {
		stmt string
		s    string
	}{
		{
			stmt: `SHOW FIELD KEYS`,
			s:    `SELECT fieldKey, fieldType FROM _fieldKeys`,
		},
		{
			stmt: `SHOW FIELD KEYS FROM cpu`,
			s:    `SELECT fieldKey, fieldType FROM _fieldKeys WHERE _name = 'cpu'`,
		},
		{
			stmt: `SHOW FIELD KEYS FROM /c.*/`,
			s:    `SELECT fieldKey, fieldType FROM _fieldKeys WHERE _name =~ /c.*/`,
		},
		{
			stmt: `SHOW FIELD KEYS FROM mydb.myrp2.cpu`,
			s:    `SELECT fieldKey, fieldType FROM mydb.myrp2._fieldKeys WHERE _name = 'cpu'`,
		},
		{
			stmt: `SHOW FIELD KEYS FROM mydb.myrp2./c.*/`,
			s:    `SELECT fieldKey, fieldType FROM mydb.myrp2._fieldKeys WHERE _name =~ /c.*/`,
		},
		{
			stmt: `SHOW SERIES`,
			s:    `SELECT "key" FROM _series`,
		},
		{
			stmt: `SHOW SERIES FROM cpu`,
			s:    `SELECT "key" FROM _series WHERE _name = 'cpu'`,
		},
		{
			stmt: `SHOW SERIES FROM mydb.myrp1.cpu`,
			s:    `SELECT "key" FROM mydb.myrp1._series WHERE _name = 'cpu'`,
		},
		{
			stmt: `SHOW SERIES FROM mydb.myrp1./c.*/`,
			s:    `SELECT "key" FROM mydb.myrp1._series WHERE _name =~ /c.*/`,
		},
		{
			stmt: `SHOW TAG KEYS`,
			s:    `SELECT tagKey FROM _tagKeys`,
		},
		{
			stmt: `SHOW TAG KEYS FROM cpu`,
			s:    `SELECT tagKey FROM _tagKeys WHERE _name = 'cpu'`,
		},
		{
			stmt: `SHOW TAG KEYS FROM /c.*/`,
			s:    `SELECT tagKey FROM _tagKeys WHERE _name =~ /c.*/`,
		},
		{
			stmt: `SHOW TAG KEYS FROM cpu WHERE region = 'uswest'`,
			s:    `SELECT tagKey FROM _tagKeys WHERE (_name = 'cpu') AND (region = 'uswest')`,
		},
		{
			stmt: `SHOW TAG KEYS FROM mydb.myrp1.cpu`,
			s:    `SELECT tagKey FROM mydb.myrp1._tagKeys WHERE _name = 'cpu'`,
		},
		{
			stmt: `SHOW TAG KEYS FROM mydb.myrp1./c.*/`,
			s:    `SELECT tagKey FROM mydb.myrp1._tagKeys WHERE _name =~ /c.*/`,
		},
		{
			stmt: `SHOW TAG KEYS FROM mydb.myrp1.cpu WHERE region = 'uswest'`,
			s:    `SELECT tagKey FROM mydb.myrp1._tagKeys WHERE (_name = 'cpu') AND (region = 'uswest')`,
		},
		{
			stmt: `SELECT value FROM cpu`,
			s:    `SELECT value FROM cpu`,
		},
	}

	for _, test := range tests {
		stmt, err := influxql.ParseStatement(test.stmt)
		if err != nil {
			t.Errorf("error parsing statement: %s", err)
		} else {
			stmt, err = influxql.RewriteStatement(stmt)
			if err != nil {
				t.Errorf("error rewriting statement: %s", err)
			} else if s := stmt.String(); s != test.s {
				t.Errorf("error rendering string. expected %s, actual: %s", test.s, s)
			}
		}
	}
}
Beispiel #9
0
// influxQueryDuration adds time WHERE clauses to query for the given start and end durations.
func influxQueryDuration(now time.Time, query, start, end, groupByInterval string) (string, error) {
	sd, err := opentsdb.ParseDuration(start)
	if err != nil {
		return "", err
	}
	ed, err := opentsdb.ParseDuration(end)
	if end == "" {
		ed = 0
	} else if err != nil {
		return "", err
	}
	st, err := influxql.ParseStatement(query)
	if err != nil {
		return "", err
	}
	s, ok := st.(*influxql.SelectStatement)
	if !ok {
		return "", fmt.Errorf("influx: expected select statement")
	}
	isTime := func(n influxql.Node) bool {
		v, ok := n.(*influxql.VarRef)
		if !ok {
			return false
		}
		s := strings.ToLower(v.Val)
		return s == "time"
	}
	influxql.WalkFunc(s.Condition, func(n influxql.Node) {
		b, ok := n.(*influxql.BinaryExpr)
		if !ok {
			return
		}
		if isTime(b.LHS) || isTime(b.RHS) {
			err = fmt.Errorf("influx query must not contain time in WHERE")
		}
	})
	if err != nil {
		return "", err
	}

	//Add New BinaryExpr for time clause
	startExpr := &influxql.BinaryExpr{
		Op:  influxql.GTE,
		LHS: &influxql.VarRef{Val: "time"},
		RHS: &influxql.TimeLiteral{Val: now.Add(time.Duration(-sd))},
	}

	stopExpr := &influxql.BinaryExpr{
		Op:  influxql.LTE,
		LHS: &influxql.VarRef{Val: "time"},
		RHS: &influxql.TimeLiteral{Val: now.Add(time.Duration(-ed))},
	}

	if s.Condition != nil {
		s.Condition = &influxql.BinaryExpr{
			Op:  influxql.AND,
			LHS: s.Condition,
			RHS: &influxql.BinaryExpr{
				Op:  influxql.AND,
				LHS: startExpr,
				RHS: stopExpr,
			},
		}
	} else {
		s.Condition = &influxql.BinaryExpr{
			Op:  influxql.AND,
			LHS: startExpr,
			RHS: stopExpr,
		}
	}

	// parse last argument
	if len(groupByInterval) > 0 {
		gbi, err := time.ParseDuration(groupByInterval)
		if err != nil {
			return "", err
		}
		s.Dimensions = append(s.Dimensions,
			&influxql.Dimension{Expr: &influxql.Call{
				Name: "time",
				Args: []influxql.Expr{&influxql.DurationLiteral{Val: gbi}},
			},
			})
	}

	// emtpy aggregate windows should be purged from the result
	// this default resembles the opentsdb results.
	if s.Fill == influxql.NullFill {
		s.Fill = influxql.NoFill
		s.FillValue = nil
	}

	return s.String(), nil
}
Beispiel #10
0
func TestService_Open_LinkSubscriptions(t *testing.T) {
	type clusterInfo struct {
		name  string
		dbrps map[string][]string
		// Map of db names to list of rp that have subs
		subs map[string][]string
	}
	type subChanged struct {
		// All the ways a sub can change
		NoDests       bool
		InvalidURL    bool
		WrongProtocol bool
		Host          bool
		Port          bool
		ExtraUser     bool
		NoUser        bool
		WrongUser     bool
		NoPassword    bool
		WrongCluster  bool
	}
	type partialConfig struct {
		configSubs   map[string][]string
		configExSubs map[string][]string
	}
	testCases := map[string]struct {
		useTokens bool

		// First round
		clusters      map[string]clusterInfo
		tokens        []string
		createSubs    []string
		dropSubs      []string
		grantedTokens []tokenGrant
		revokedTokens []string
		subChanged    subChanged

		// apply new config between rounds
		partialConfigs map[string]partialConfig

		// Second round
		secondClusters      map[string]clusterInfo
		secondTokens        []string
		secondCreateSubs    []string
		secondDropSubs      []string
		secondGrantedTokens []tokenGrant
		secondRevokedTokens []string
		secondSubChanged    subChanged
	}{
		"NoExisting": {
			useTokens: true,
			clusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC"},
					},
				}},
			secondClusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC"},
					},
				}},
			createSubs: []string{
				`CREATE SUBSCRIPTION "` + testSubName + `" ON db1.rpA DESTINATIONS ANY '` + testDestinationWithTokenForCluster(testClusterName) + `'`,
				`CREATE SUBSCRIPTION "` + testSubName + `" ON db1.rpB DESTINATIONS ANY '` + testDestinationWithTokenForCluster(testClusterName) + `'`,
				`CREATE SUBSCRIPTION "` + testSubName + `" ON db2.rpC DESTINATIONS ANY '` + testDestinationWithTokenForCluster(testClusterName) + `'`,
			},
			grantedTokens: []tokenGrant{
				{
					token: randomToken,
					db:    "db1",
					rp:    "rpA",
				},
				{
					token: randomToken,
					db:    "db1",
					rp:    "rpB",
				},
				{
					token: randomToken,
					db:    "db2",
					rp:    "rpC",
				},
			},
		},
		"NoExisting_NoTokens": {
			useTokens: false,
			clusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC"},
					},
				}},
			secondClusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC"},
					},
				}},
			createSubs: []string{
				`CREATE SUBSCRIPTION "` + testSubName + `" ON db1.rpA DESTINATIONS ANY '` + testDestination + `'`,
				`CREATE SUBSCRIPTION "` + testSubName + `" ON db1.rpB DESTINATIONS ANY '` + testDestination + `'`,
				`CREATE SUBSCRIPTION "` + testSubName + `" ON db2.rpC DESTINATIONS ANY '` + testDestination + `'`,
			},
		},
		"Existing": {
			useTokens: true,
			clusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			secondClusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC"},
					},
				}},
			tokens: []string{
				randomToken,
			},
			createSubs: []string{
				`CREATE SUBSCRIPTION "` + testSubName + `" ON db1.rpB DESTINATIONS ANY '` + testDestinationWithTokenForCluster(testClusterName) + `'`,
				`CREATE SUBSCRIPTION "` + testSubName + `" ON db2.rpC DESTINATIONS ANY '` + testDestinationWithTokenForCluster(testClusterName) + `'`,
			},
			grantedTokens: []tokenGrant{
				{
					token: randomToken,
					db:    "db1",
					rp:    "rpB",
				},
				{
					token: randomToken,
					db:    "db2",
					rp:    "rpC",
				},
			},
		},
		"NoChanges": {
			useTokens: true,
			clusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC"},
					},
				}},
			secondClusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC"},
					},
				}},
			tokens: []string{
				randomToken,
			},
		},
		"ExtraTokens": {
			useTokens: true,
			clusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC"},
					},
				},
				"anothercluster": {
					dbrps: map[string][]string{
						"db10": []string{"rpZ"},
					},
					subs: map[string][]string{
						"db10": []string{"rpZ"},
					},
				}},
			secondClusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC"},
					},
				},
				"anothercluster": {
					dbrps: map[string][]string{
						"db10": []string{"rpZ"},
					},
					subs: map[string][]string{
						"db10": []string{"rpZ"},
					},
				},
			},
			tokens: []string{
				randomToken,
				"invalidtoken",
				tokenForCluster("anothercluster"),
				testClusterName + ";unusedtoken",
			},
			revokedTokens: []string{
				"invalidtoken",
				testClusterName + ";unusedtoken",
			},
		},
		"ExtraNonClusterTokens": {
			useTokens: true,
			clusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC"},
					},
				},
				"anothercluster": {
					dbrps: map[string][]string{
						"db10": []string{"rpZ"},
					},
					subs: map[string][]string{
						"db10": []string{"rpZ"},
					},
				}},
			secondClusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC"},
					},
				},
				"anothercluster": {
					dbrps: map[string][]string{
						"db10": []string{"rpZ"},
					},
					subs: map[string][]string{
						"db10": []string{"rpZ"},
					},
				},
			},
			tokens: []string{
				randomToken,
				"invalidtoken",
				tokenForCluster("anothercluster"),
				testClusterName + ";unusedtoken",
				tokenForCluster("nonexistantcluster"),
			},
			revokedTokens: []string{
				"invalidtoken",
				testClusterName + ";unusedtoken",
				tokenForCluster("nonexistantcluster"),
			},
		},
		"SecondDroppedDB": {
			useTokens: true,
			clusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			secondClusters: map[string]clusterInfo{
				testClusterName: {
				// db1 had been dropped
				}},
			tokens: []string{
				randomToken,
			},
			secondTokens: []string{
				randomToken,
			},
			secondRevokedTokens: []string{
				randomToken,
			},
		},
		"SecondNewDB": {
			useTokens: true,
			clusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			secondClusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
						"db2": []string{"rpB"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			tokens: []string{
				randomToken,
			},
			secondTokens: []string{
				randomToken,
			},
			secondCreateSubs: []string{
				`CREATE SUBSCRIPTION "` + testSubName + `" ON db2.rpB DESTINATIONS ANY '` + testDestinationWithTokenForCluster(testClusterName) + `'`,
			},
			secondGrantedTokens: []tokenGrant{{
				token: randomToken,
				db:    "db2",
				rp:    "rpB",
			}},
		},
		"SC_NoDests": {
			useTokens:  false,
			subChanged: subChanged{NoDests: true},
			clusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			secondClusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			dropSubs: []string{
				`DROP SUBSCRIPTION "` + testSubName + `" ON db1.rpA`,
			},
			createSubs: []string{
				`CREATE SUBSCRIPTION "` + testSubName + `" ON db1.rpA DESTINATIONS ANY '` + testDestination + `'`,
			},
		},
		"SC_InvalidURL": {
			useTokens:  true,
			subChanged: subChanged{InvalidURL: true},
			clusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			secondClusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			dropSubs: []string{
				`DROP SUBSCRIPTION "` + testSubName + `" ON db1.rpA`,
			},
			createSubs: []string{
				`CREATE SUBSCRIPTION "` + testSubName + `" ON db1.rpA DESTINATIONS ANY '` + testDestinationWithTokenForCluster(testClusterName) + `'`,
			},
			tokens: []string{
				randomToken,
			},
			secondTokens: []string{
				randomToken,
			},
			grantedTokens: []tokenGrant{{
				token: randomToken,
				db:    "db1",
				rp:    "rpA",
			}},
		},
		"SC_WrongProtocol": {
			useTokens:  false,
			subChanged: subChanged{WrongProtocol: true},
			clusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			secondClusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			dropSubs: []string{
				`DROP SUBSCRIPTION "` + testSubName + `" ON db1.rpA`,
			},
			createSubs: []string{
				`CREATE SUBSCRIPTION "` + testSubName + `" ON db1.rpA DESTINATIONS ANY '` + testDestination + `'`,
			},
		},
		"SC_Host": {
			useTokens:  false,
			subChanged: subChanged{Host: true},
			clusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			secondClusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			dropSubs: []string{
				`DROP SUBSCRIPTION "` + testSubName + `" ON db1.rpA`,
			},
			createSubs: []string{
				`CREATE SUBSCRIPTION "` + testSubName + `" ON db1.rpA DESTINATIONS ANY '` + testDestination + `'`,
			},
		},
		"SC_Port": {
			useTokens:  false,
			subChanged: subChanged{Port: true},
			clusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			secondClusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			dropSubs: []string{
				`DROP SUBSCRIPTION "` + testSubName + `" ON db1.rpA`,
			},
			createSubs: []string{
				`CREATE SUBSCRIPTION "` + testSubName + `" ON db1.rpA DESTINATIONS ANY '` + testDestination + `'`,
			},
		},
		"SC_ExtraUser": {
			useTokens:  false,
			subChanged: subChanged{ExtraUser: true},
			clusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			secondClusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			dropSubs: []string{
				`DROP SUBSCRIPTION "` + testSubName + `" ON db1.rpA`,
			},
			createSubs: []string{
				`CREATE SUBSCRIPTION "` + testSubName + `" ON db1.rpA DESTINATIONS ANY '` + testDestination + `'`,
			},
		},
		"SC_NoUser": {
			useTokens:  true,
			subChanged: subChanged{NoUser: true},
			clusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			secondClusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			dropSubs: []string{
				`DROP SUBSCRIPTION "` + testSubName + `" ON db1.rpA`,
			},
			createSubs: []string{
				`CREATE SUBSCRIPTION "` + testSubName + `" ON db1.rpA DESTINATIONS ANY '` + testDestinationWithTokenForCluster(testClusterName) + `'`,
			},
			tokens: []string{
				randomToken,
			},
			secondTokens: []string{
				randomToken,
			},
			grantedTokens: []tokenGrant{{
				token: randomToken,
				db:    "db1",
				rp:    "rpA",
			}},
		},
		"SC_WrongUser": {
			useTokens:  true,
			subChanged: subChanged{WrongUser: true},
			clusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			secondClusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			dropSubs: []string{
				`DROP SUBSCRIPTION "` + testSubName + `" ON db1.rpA`,
			},
			createSubs: []string{
				`CREATE SUBSCRIPTION "` + testSubName + `" ON db1.rpA DESTINATIONS ANY '` + testDestinationWithTokenForCluster(testClusterName) + `'`,
			},
			tokens: []string{
				randomToken,
			},
			secondTokens: []string{
				randomToken,
			},
			grantedTokens: []tokenGrant{{
				token: randomToken,
				db:    "db1",
				rp:    "rpA",
			}},
		},
		"SC_NoPassword": {
			useTokens:  true,
			subChanged: subChanged{NoPassword: true},
			clusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			secondClusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			dropSubs: []string{
				`DROP SUBSCRIPTION "` + testSubName + `" ON db1.rpA`,
			},
			createSubs: []string{
				`CREATE SUBSCRIPTION "` + testSubName + `" ON db1.rpA DESTINATIONS ANY '` + testDestinationWithTokenForCluster(testClusterName) + `'`,
			},
			tokens: []string{
				randomToken,
			},
			secondTokens: []string{
				randomToken,
			},
			grantedTokens: []tokenGrant{{
				token: randomToken,
				db:    "db1",
				rp:    "rpA",
			}},
		},
		"SC_WrongCluster": {
			useTokens:  true,
			subChanged: subChanged{WrongCluster: true},
			clusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			secondClusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA"},
					},
				}},
			dropSubs: []string{
				`DROP SUBSCRIPTION "` + testSubName + `" ON db1.rpA`,
			},
			createSubs: []string{
				`CREATE SUBSCRIPTION "` + testSubName + `" ON db1.rpA DESTINATIONS ANY '` + testDestinationWithTokenForCluster(testClusterName) + `'`,
			},
			tokens: []string{
				randomToken,
			},
			secondTokens: []string{
				randomToken,
			},
			grantedTokens: []tokenGrant{{
				token: randomToken,
				db:    "db1",
				rp:    "rpA",
			}},
		},
		"ConfigChange_NewSubs": {
			clusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC", "rpD"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC", "rpD"},
					},
				},
			},
			secondClusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC", "rpD"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC", "rpD"},
					},
				},
			},
			partialConfigs: map[string]partialConfig{
				testClusterName: {
					configSubs: map[string][]string{
						"db1": {"rpA"},
					},
				},
			},
			secondDropSubs: []string{
				`DROP SUBSCRIPTION "` + testSubName + `" ON db1.rpB`,
				`DROP SUBSCRIPTION "` + testSubName + `" ON db2.rpC`,
				`DROP SUBSCRIPTION "` + testSubName + `" ON db2.rpD`,
			},
		},
		"ConfigChange_NewExcludes": {
			clusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC", "rpD"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC", "rpD"},
					},
				}},
			secondClusters: map[string]clusterInfo{
				testClusterName: {
					dbrps: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC", "rpD"},
					},
					subs: map[string][]string{
						"db1": []string{"rpA", "rpB"},
						"db2": []string{"rpC", "rpD"},
					},
				}},
			partialConfigs: map[string]partialConfig{
				testClusterName: {
					configExSubs: map[string][]string{
						"db1": {"rpA"},
					},
				},
			},
			secondDropSubs: []string{
				`DROP SUBSCRIPTION "` + testSubName + `" ON db1.rpA`,
			},
		},
	}
	for testName, tc := range testCases {
		t.Log("starting test:", testName)
		log.Println("starting test:", testName)
		clusterNames := make([]string, 0, len(tc.clusters))
		clusterNameLookup := make(map[string]int, len(tc.clusters))
		i := 0
		for clusterName := range tc.clusters {
			clusterNames = append(clusterNames, clusterName)
			clusterNameLookup[clusterName] = i
			i++
		}
		defaultConfigs := NewDefaultTestConfigs(clusterNames)
		s, as, cs := NewTestService(defaultConfigs, "localhost", tc.useTokens)

		// Define the active vars
		var activeClusters map[string]clusterInfo
		var tokens []string
		var subChanges subChanged
		var createSubs map[string]bool
		var dropSubs map[string]bool
		var grantedTokens map[tokenGrant]bool
		var revokedTokens map[string]bool

		// Setup functions
		cs.QueryFunc = func(clusterName string, q influxcli.Query) (*influxcli.Response, error) {
			log.Println("query:", q.Command)
			switch {
			case q.Command == "SHOW DATABASES":
				dbs := make([][]interface{}, 0, len(activeClusters[clusterName].dbrps))
				for db := range activeClusters[clusterName].dbrps {
					dbs = append(dbs, []interface{}{db})
				}
				return &influxcli.Response{
					Results: []influxcli.Result{{
						Series: []models.Row{
							{
								Values: dbs,
							},
						},
					}},
				}, nil
			case strings.HasPrefix(q.Command, "SHOW RETENTION POLICIES ON"):
				stmt, _ := influxql.ParseStatement(q.Command)
				if show, ok := stmt.(*influxql.ShowRetentionPoliciesStatement); ok {
					var rps [][]interface{}
					for _, rp := range activeClusters[clusterName].dbrps[show.Database] {
						rps = append(rps, []interface{}{rp})
					}
					return &influxcli.Response{
						Results: []influxcli.Result{{
							Series: []models.Row{
								{
									Values: rps,
								},
							},
						}},
					}, nil
				}
				return nil, fmt.Errorf("invalid show rp query: %s", q.Command)
			case q.Command == "SHOW SUBSCRIPTIONS":
				result := influxcli.Result{}
				for db, subs := range activeClusters[clusterName].subs {
					series := models.Row{
						Name: db,
						Columns: []string{
							"name",
							"retention_policy",
							"mode",
							"destinations",
						},
					}
					for _, rp := range subs {
						var destinations []interface{}
						switch {
						case subChanges.NoDests:
							destinations = nil
						case subChanges.InvalidURL:
							destinations = []interface{}{"://broken url"}
						case subChanges.WrongProtocol:
							destinations = []interface{}{"unknown://*****:*****@localhost:9092"}
						case subChanges.WrongUser:
							destinations = []interface{}{"http://*****:*****@localhost:9092"}
						case subChanges.NoPassword:
							destinations = []interface{}{"http://~subscriber@localhost:9092"}
						case subChanges.WrongCluster:
							destinations = testDestinationsWithTokensForCluster("wrong")
						default:
							if tc.useTokens {
								destinations = testDestinationsWithTokensForCluster(clusterName)
							} else {
								destinations = []interface{}{testDestination}
							}
						}
						series.Values = append(series.Values, []interface{}{
							testSubName,
							rp,
							subMode,
							destinations,
						})
					}
					result.Series = append(result.Series, series)
				}
				return &influxcli.Response{Results: []influxcli.Result{result}}, nil
			case strings.HasPrefix(q.Command, "CREATE SUBSCRIPTION"):
				createSubs[q.Command] = true
				return &influxcli.Response{}, nil
			case strings.HasPrefix(q.Command, "DROP SUBSCRIPTION"):
				dropSubs[q.Command] = true
				return &influxcli.Response{}, nil
			default:
				msg := fmt.Sprintf("unexpected query: %s", q.Command)
				t.Error(msg)
				return nil, errors.New(msg)
			}
		}
		as.ListSubscriptionTokensFunc = func() ([]string, error) {
			ts := make([]string, len(tokens))
			for i, token := range tokens {
				ts[i] = base64.RawURLEncoding.EncodeToString([]byte(token))
			}
			return ts, nil
		}
		as.GrantSubscriptionAccessFunc = func(token, db, rp string) error {
			raw, err := base64.RawURLEncoding.DecodeString(token)
			if err != nil {
				return err
			}
			log.Println("granted token:", string(raw), db, rp)
			grantedTokens[tokenGrant{
				token: string(raw),
				db:    db,
				rp:    rp,
			}] = true
			return nil
		}
		as.RevokeSubscriptionAccessFunc = func(token string) error {
			raw, err := base64.RawURLEncoding.DecodeString(token)
			if err != nil {
				return err
			}
			log.Println("revoked token:", string(raw))
			revokedTokens[string(raw)] = true
			return nil
		}

		// Run first round
		activeClusters = tc.clusters
		tokens = tc.tokens
		subChanges = tc.subChanged
		createSubs = make(map[string]bool)
		dropSubs = make(map[string]bool)
		grantedTokens = make(map[tokenGrant]bool)
		revokedTokens = make(map[string]bool)

		log.Println("D! first round")
		if err := s.Open(); err != nil {
			t.Fatal(err)
		}
		defer s.Close()
		validate(
			t,
			testName+"-1",
			tc.createSubs,
			tc.dropSubs,
			tc.grantedTokens,
			tc.revokedTokens,
			createSubs,
			dropSubs,
			grantedTokens,
			revokedTokens,
		)

		// Run second round
		activeClusters = tc.secondClusters
		tokens = tc.secondTokens
		subChanges = tc.secondSubChanged
		createSubs = make(map[string]bool)
		dropSubs = make(map[string]bool)
		grantedTokens = make(map[tokenGrant]bool)
		revokedTokens = make(map[string]bool)

		log.Println("D! second round")
		if len(tc.partialConfigs) > 0 {
			configs := make([]interface{}, 0, len(tc.partialConfigs))
			for name, pc := range tc.partialConfigs {
				c := defaultConfigs[clusterNameLookup[name]]
				c.Subscriptions = pc.configSubs
				c.ExcludedSubscriptions = pc.configExSubs
				configs = append(configs, c)
			}
			if err := s.Update(configs); err != nil {
				t.Fatal(err)
			}
		}
		s.LinkSubscriptions()

		validate(
			t,
			testName+"-2",
			tc.secondCreateSubs,
			tc.secondDropSubs,
			tc.secondGrantedTokens,
			tc.secondRevokedTokens,
			createSubs,
			dropSubs,
			grantedTokens,
			revokedTokens,
		)
	}
}