feat(pkger): add support for variable resource kind to pkger

pull/15797/head
Johnny Steenbergen 2019-11-06 14:41:06 -08:00 committed by Johnny Steenbergen
parent 591f2870d8
commit d252b20ecc
7 changed files with 805 additions and 443 deletions

View File

@ -13,17 +13,27 @@ const (
kindDashboard kind = "dashboard"
kindLabel kind = "label"
kindPackage kind = "package"
kindVariable kind = "variable"
)
var kinds = map[kind]bool{
kindBucket: true,
kindDashboard: true,
kindLabel: true,
kindPackage: true,
kindVariable: true,
}
type kind string
func (k kind) String() string {
switch k {
case kindBucket, kindLabel, kindDashboard, kindPackage:
if kinds[k] {
return string(k)
default:
}
if k == kindUnknown {
return "unknown"
}
return string(k)
}
// SafeID is an equivalent influxdb.ID that encodes safely with
@ -160,6 +170,7 @@ type Summary struct {
Dashboards []SummaryDashboard `json:"dashboards"`
Labels []SummaryLabel `json:"labels"`
LabelMappings []SummaryLabelMapping `json:"labelMappings"`
Variables []SummaryVariable `json:"variables"`
}
// SummaryBucket provides a summary of a pkg bucket.
@ -226,6 +237,11 @@ type SummaryLabelMapping struct {
influxdb.LabelMapping
}
// SummaryVariable provides a summary of a pkg variable.
type SummaryVariable struct {
influxdb.Variable
}
type bucket struct {
id influxdb.ID
OrgID influxdb.ID
@ -275,17 +291,17 @@ func (b *bucket) shouldApply() bool {
b.RetentionPeriod != b.existing.RetentionPeriod
}
type labelMapKey struct {
type assocMapKey struct {
resType influxdb.ResourceType
name string
}
type labelMapVal struct {
type assocMapVal struct {
exists bool
v interface{}
}
func (l labelMapVal) bucket() (*bucket, bool) {
func (l assocMapVal) bucket() (*bucket, bool) {
if l.v == nil {
return nil, false
}
@ -293,7 +309,7 @@ func (l labelMapVal) bucket() (*bucket, bool) {
return b, ok
}
func (l labelMapVal) dashboard() (*dashboard, bool) {
func (l assocMapVal) dashboard() (*dashboard, bool) {
if l.v == nil {
return nil, false
}
@ -308,7 +324,7 @@ type label struct {
Color string
Description string
mappings map[labelMapKey]labelMapVal
mappings map[assocMapKey]assocMapVal
// exists provides context for a resource that already
// exists in the platform. If a resource already exists(exists=true)
@ -359,7 +375,7 @@ func (l *label) mappingSummary() []SummaryLabelMapping {
return mappings
}
func (l *label) getMappedResourceID(k labelMapKey) influxdb.ID {
func (l *label) getMappedResourceID(k assocMapKey) influxdb.ID {
switch k.resType {
case influxdb.BucketsResourceType:
b, ok := l.mappings[k].bucket()
@ -380,14 +396,14 @@ func (l *label) setBucketMapping(b *bucket, exists bool) {
return
}
if l.mappings == nil {
l.mappings = make(map[labelMapKey]labelMapVal)
l.mappings = make(map[assocMapKey]assocMapVal)
}
key := labelMapKey{
key := assocMapKey{
resType: influxdb.BucketsResourceType,
name: b.Name,
}
l.mappings[key] = labelMapVal{
l.mappings[key] = assocMapVal{
exists: exists,
v: b,
}
@ -398,14 +414,14 @@ func (l *label) setDashboardMapping(d *dashboard) {
return
}
if l.mappings == nil {
l.mappings = make(map[labelMapKey]labelMapVal)
l.mappings = make(map[assocMapKey]assocMapVal)
}
key := labelMapKey{
key := assocMapKey{
resType: d.ResourceType(),
name: d.Name,
}
l.mappings[key] = labelMapVal{v: d}
l.mappings[key] = assocMapVal{v: d}
}
func (l *label) properties() map[string]string {
@ -428,6 +444,82 @@ func toInfluxLabels(labels ...*label) []influxdb.Label {
return iLabels
}
type variable struct {
id influxdb.ID
OrgID influxdb.ID
Name string
Description string
Type string
Query string
Language string
ConstValues []string
MapValues map[string]string
mappings map[assocMapKey]assocMapVal
}
func (v *variable) summarize() SummaryVariable {
args := &influxdb.VariableArguments{
Type: v.Type,
}
switch args.Type {
case "query":
args.Values = influxdb.VariableQueryValues{
Query: v.Query,
Language: v.Language,
}
case "constant":
args.Values = influxdb.VariableConstantValues(v.ConstValues)
case "map":
args.Values = influxdb.VariableMapValues(v.MapValues)
}
return SummaryVariable{
Variable: influxdb.Variable{
ID: v.id,
OrganizationID: v.OrgID,
Name: v.Name,
Description: v.Description,
Arguments: args,
},
}
}
func (v *variable) valid() []failure {
var failures []failure
switch v.Type {
case "map":
if len(v.MapValues) == 0 {
failures = append(failures, failure{
Field: "values",
Msg: "map variable must have at least 1 key/val pair",
})
}
case "constant":
if len(v.ConstValues) == 0 {
failures = append(failures, failure{
Field: "values",
Msg: "constant variable must have a least 1 value provided",
})
}
case "query":
if v.Query == "" {
failures = append(failures, failure{
Field: "query",
Msg: "query variable must provide a query string",
})
}
if v.Language != "influxql" && v.Language != "flux" {
const msgFmt = "query variable language must be either %q or %q; got %q"
failures = append(failures, failure{
Field: "language",
Msg: fmt.Sprintf(msgFmt, "influxql", "flux", v.Language),
})
}
}
return failures
}
type dashboard struct {
id influxdb.ID
OrgID influxdb.ID

View File

@ -94,8 +94,8 @@ func TestPkg(t *testing.T) {
Name: "name2",
Description: "desc2",
Color: "blurple",
mappings: map[labelMapKey]labelMapVal{
labelMapKey{
mappings: map[assocMapKey]assocMapVal{
assocMapKey{
resType: influxdb.BucketsResourceType,
name: bucket1.Name,
}: {

View File

@ -135,6 +135,7 @@ type Pkg struct {
mLabels map[string]*label
mBuckets map[string]*bucket
mDashboards map[string]*dashboard
mVariables map[string]*variable
isVerified bool // dry run has verified pkg resources with existing resources
isParsed bool // indicates the pkg has been parsed and all resources graphed accordingly
@ -146,10 +147,6 @@ type Pkg struct {
func (p *Pkg) Summary() Summary {
var sum Summary
for _, l := range p.labels() {
sum.Labels = append(sum.Labels, l.summarize())
}
for _, b := range p.buckets() {
sum.Buckets = append(sum.Buckets, b.summarize())
}
@ -158,6 +155,10 @@ func (p *Pkg) Summary() Summary {
sum.Dashboards = append(sum.Dashboards, d.summarize())
}
for _, l := range p.labels() {
sum.Labels = append(sum.Labels, l.summarize())
}
for _, m := range p.labelMappings() {
sum.LabelMappings = append(sum.LabelMappings, SummaryLabelMapping{
ResourceName: m.ResourceName,
@ -166,6 +167,10 @@ func (p *Pkg) Summary() Summary {
})
}
for _, v := range p.variables() {
sum.Variables = append(sum.Variables, v.summarize())
}
return sum
}
@ -226,6 +231,19 @@ func (p *Pkg) dashboards() []*dashboard {
return dashes
}
func (p *Pkg) variables() []*variable {
vars := make([]*variable, 0, len(p.mVariables))
for _, v := range p.mVariables {
vars = append(vars, v)
}
sort.Slice(vars, func(i, j int) bool {
return vars[i].Name < vars[j].Name
})
return vars
}
// labelMappings returns the mappings that will be created for
// valid pairs of labels and resources of which all have IDs.
// If a resource does not exist yet, a label mapping will not
@ -293,7 +311,7 @@ func (p *Pkg) validMetadata() error {
}
res := errResource{
Kind: "Package",
Kind: kindPackage.String(),
Idx: -1,
}
for _, f := range failures {
@ -332,6 +350,7 @@ func (p *Pkg) graphResources() error {
graphFns := []func() error{
// labels are first to validate associations with other resources
p.graphLabels,
p.graphVariables,
p.graphBuckets,
p.graphDashboards,
}
@ -467,6 +486,47 @@ func (p *Pkg) graphDashboards() error {
})
}
func (p *Pkg) graphVariables() error {
p.mVariables = make(map[string]*variable)
return p.eachResource(kindVariable, func(r Resource) []failure {
if r.Name() == "" {
return []failure{{
Field: "name",
Msg: "must be provided",
}}
}
if _, ok := p.mVariables[r.Name()]; ok {
return []failure{{
Field: "name",
Msg: "duplicate name: " + r.Name(),
}}
}
newVar := &variable{
Name: r.Name(),
Description: r.stringShort("description"),
Type: strings.ToLower(r.stringShort("type")),
Query: strings.TrimSpace(r.stringShort("query")),
Language: strings.ToLower(strings.TrimSpace(r.stringShort("language"))),
ConstValues: r.slcStr("values"),
MapValues: r.mapStrStr("values"),
}
p.mVariables[r.Name()] = newVar
// here we set the var on the var map and return fails
// reaons for this is we could end up providing bad
// errors to the user if we dont' set it b/c of a bad
// query or something, and its being referenced by a
// dashboard or something. The var exists, its just
// invalid. So the mapping is correct. So we keep this
// to validate that mapping is correct, and return fails
// to indicate fails from the var.
return newVar.valid()
})
}
func (p *Pkg) eachResource(resourceKind kind, fn func(r Resource) []failure) error {
var parseErr ParseErr
for i, r := range p.Spec.Resources {
@ -602,7 +662,7 @@ func parseChart(r Resource) (chart, []failure) {
Geom: r.stringShort("geom"),
}
if leg, ok := ifaceMapToResource(r["legend"]); ok {
if leg, ok := ifaceToResource(r["legend"]); ok {
c.Legend.Type = leg.stringShort("type")
c.Legend.Orientation = leg.stringShort("orientation")
}
@ -669,6 +729,9 @@ func (r Resource) kind() (kind, error) {
if newKind == kindUnknown {
return kindUnknown, errors.New("invalid kind")
}
if !kinds[newKind] {
return newKind, errors.New("unsupported kind provided")
}
return newKind, nil
}
@ -695,7 +758,7 @@ func (r Resource) nestedAssociations() []Resource {
var resources []Resource
for _, iface := range ifaces {
newRes, ok := ifaceMapToResource(iface)
newRes, ok := ifaceToResource(iface)
if !ok {
continue
}
@ -757,15 +820,7 @@ func (r Resource) intShort(key string) int {
}
func (r Resource) string(key string) (string, bool) {
if s, ok := r[key].(string); ok {
return s, true
}
if i, ok := r[key].(int); ok {
return strconv.Itoa(i), true
}
return "", false
return ifaceToStr(r[key])
}
func (r Resource) stringShort(key string) string {
@ -786,7 +841,7 @@ func (r Resource) slcResource(key string) []Resource {
var newResources []Resource
for _, iFace := range iFaceSlc {
r, ok := ifaceMapToResource(iFace)
r, ok := ifaceToResource(iFace)
if !ok {
continue
}
@ -796,7 +851,51 @@ func (r Resource) slcResource(key string) []Resource {
return newResources
}
func ifaceMapToResource(i interface{}) (Resource, bool) {
func (r Resource) slcStr(key string) []string {
v, ok := r[key]
if !ok {
return nil
}
iFaceSlc, ok := v.([]interface{})
if !ok {
return nil
}
var out []string
for _, iface := range iFaceSlc {
s, ok := ifaceToStr(iface)
if !ok {
continue
}
out = append(out, s)
}
return out
}
func (r Resource) mapStrStr(key string) map[string]string {
res, ok := ifaceToResource(r[key])
if !ok {
return nil
}
m := make(map[string]string)
for k, v := range res {
s, ok := ifaceToStr(v)
if !ok {
continue
}
m[k] = s
}
return m
}
func ifaceToResource(i interface{}) (Resource, bool) {
if i == nil {
return nil, false
}
res, ok := i.(Resource)
if ok {
return res, true
@ -822,6 +921,26 @@ func ifaceMapToResource(i interface{}) (Resource, bool) {
return newRes, true
}
func ifaceToStr(v interface{}) (string, bool) {
if v == nil {
return "", false
}
if s, ok := v.(string); ok {
return s, true
}
if i, ok := v.(int); ok {
return strconv.Itoa(i), true
}
if f, ok := v.(float64); ok {
return strconv.FormatFloat(f, 'f', -1, 64), true
}
return "", false
}
// ParseErr is a error from parsing the given package. The ParseErr
// provides a list of resources that failed and all validations
// that failed for that resource. A resource can multiple errors,

File diff suppressed because it is too large Load Diff

45
pkger/testdata/variables.json vendored Normal file
View File

@ -0,0 +1,45 @@
{
"apiVersion": "0.1.0",
"kind": "Package",
"meta": {
"pkgName": "pkg_name",
"pkgVersion": "1",
"description": "pack description"
},
"spec": {
"resources": [
{
"kind": "Variable",
"name": "var_query_1",
"description": "var_query_1 desc",
"type": "query",
"query": "buckets() |> filter(fn: (r) => r.name !~ /^_/) |> rename(columns: {name: \"_value\"}) |> keep(columns: [\"_value\"])",
"language": "flux"
},
{
"kind": "Variable",
"name": "var_query_2",
"description": "var_query_2 desc",
"type": "query",
"query": "an influxql query of sorts",
"language": "influxql"
},
{
"kind": "Variable",
"name": "var_const",
"description": "var_const desc",
"type": "constant",
"values": ["first val"]
},
{
"kind": "Variable",
"name": "var_map",
"description": "var_map desc",
"type": "map",
"values": {
"k1": "v1"
}
}
]
}
}

33
pkger/testdata/variables.yml vendored Normal file
View File

@ -0,0 +1,33 @@
apiVersion: 0.1.0
kind: Package
meta:
pkgName: pkg_name
pkgVersion: 1
description: pack description
spec:
resources:
- kind: Variable
name: var_query_1
description: var_query_1 desc
type: query
language: flux
query: |
buckets() |> filter(fn: (r) => r.name !~ /^_/) |> rename(columns: {name: "_value"}) |> keep(columns: ["_value"])
- kind: Variable
name: var_query_2
description: var_query_2 desc
type: query
query: an influxql query of sorts
language: influxql
- kind: Variable
name: var_const
description: var_const desc
type: constant
values:
- first val
- kind: Variable
name: var_map
description: var_map desc
type: map
values:
k1: v1

View File

@ -123,7 +123,7 @@ func (m *Variable) Valid() error {
"query": true,
}
if _, prs := validTypes[m.Arguments.Type]; !prs {
if !validTypes[m.Arguments.Type] {
return fmt.Errorf("invalid arguments type")
}