diff --git a/CHANGELOG.md b/CHANGELOG.md index 736d5dd54a..24a0f904ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ need to update any InfluxDB CLI config profiles with the new port number. 1. [19433](https://github.com/influxdata/influxdb/pull/19433): Add option to dump raw query results in CLI 1. [19506](https://github.com/influxdata/influxdb/pull/19506): Add TSM 1.x storage options as flags 1. [19508](https://github.com/influxdata/influxdb/pull/19508): Add subset of InfluxQL coordinator options as flags +1. [19457](https://github.com/influxdata/influxdb/pull/19457): Add ability to export resources by name via the CLI ### Bug Fixes diff --git a/cmd/influx/template.go b/cmd/influx/template.go index 9031199031..97f0d6d413 100644 --- a/cmd/influx/template.go +++ b/cmd/influx/template.go @@ -77,16 +77,25 @@ type cmdTemplateBuilder struct { } exportOpts struct { - resourceType string - buckets string - checks string - dashboards string - endpoints string - labels string - rules string - tasks string - telegrafs string - variables string + resourceType string + buckets string + checks string + dashboards string + endpoints string + labels string + rules string + tasks string + telegrafs string + variables string + bucketNames string + checkNames string + dashboardNames string + endpointNames string + labelNames string + ruleNames string + taskNames string + telegrafNames string + variableNames string } updateStackOpts struct { @@ -351,6 +360,15 @@ func (b *cmdTemplateBuilder) cmdExport() *cobra.Command { cmd.Flags().StringVar(&b.exportOpts.tasks, "tasks", "", "List of task ids comma separated") cmd.Flags().StringVar(&b.exportOpts.telegrafs, "telegraf-configs", "", "List of telegraf config ids comma separated") cmd.Flags().StringVar(&b.exportOpts.variables, "variables", "", "List of variable ids comma separated") + cmd.Flags().StringVar(&b.exportOpts.bucketNames, "bucket-names", "", "List of bucket names comma separated") + cmd.Flags().StringVar(&b.exportOpts.checkNames, "check-names", "", "List of check names comma separated") + cmd.Flags().StringVar(&b.exportOpts.dashboardNames, "dashboard-names", "", "List of dashboard names comma separated") + cmd.Flags().StringVar(&b.exportOpts.endpointNames, "endpoint-names", "", "List of notification endpoint names comma separated") + cmd.Flags().StringVar(&b.exportOpts.labelNames, "label-names", "", "List of label names comma separated") + cmd.Flags().StringVar(&b.exportOpts.ruleNames, "rule-names", "", "List of notification rule names comma separated") + cmd.Flags().StringVar(&b.exportOpts.taskNames, "task-names", "", "List of task names comma separated") + cmd.Flags().StringVar(&b.exportOpts.telegrafNames, "telegraf-config-names", "", "List of telegraf config names comma separated") + cmd.Flags().StringVar(&b.exportOpts.variableNames, "variable-names", "", "List of variable names comma separated") return cmd } @@ -364,21 +382,22 @@ func (b *cmdTemplateBuilder) exportRunEFn(cmd *cobra.Command, args []string) err resTypes := []struct { kind pkger.Kind idStrs []string + names []string }{ - {kind: pkger.KindBucket, idStrs: strings.Split(b.exportOpts.buckets, ",")}, - {kind: pkger.KindCheck, idStrs: strings.Split(b.exportOpts.checks, ",")}, - {kind: pkger.KindDashboard, idStrs: strings.Split(b.exportOpts.dashboards, ",")}, - {kind: pkger.KindLabel, idStrs: strings.Split(b.exportOpts.labels, ",")}, - {kind: pkger.KindNotificationEndpoint, idStrs: strings.Split(b.exportOpts.endpoints, ",")}, - {kind: pkger.KindNotificationRule, idStrs: strings.Split(b.exportOpts.rules, ",")}, - {kind: pkger.KindTask, idStrs: strings.Split(b.exportOpts.tasks, ",")}, - {kind: pkger.KindTelegraf, idStrs: strings.Split(b.exportOpts.telegrafs, ",")}, - {kind: pkger.KindVariable, idStrs: strings.Split(b.exportOpts.variables, ",")}, + {kind: pkger.KindBucket, idStrs: strings.Split(b.exportOpts.buckets, ","), names: strings.Split(b.exportOpts.bucketNames, ",")}, + {kind: pkger.KindCheck, idStrs: strings.Split(b.exportOpts.checks, ","), names: strings.Split(b.exportOpts.checkNames, ",")}, + {kind: pkger.KindDashboard, idStrs: strings.Split(b.exportOpts.dashboards, ","), names: strings.Split(b.exportOpts.dashboardNames, ",")}, + {kind: pkger.KindLabel, idStrs: strings.Split(b.exportOpts.labels, ","), names: strings.Split(b.exportOpts.labelNames, ",")}, + {kind: pkger.KindNotificationEndpoint, idStrs: strings.Split(b.exportOpts.endpoints, ","), names: strings.Split(b.exportOpts.endpointNames, ",")}, + {kind: pkger.KindNotificationRule, idStrs: strings.Split(b.exportOpts.rules, ","), names: strings.Split(b.exportOpts.ruleNames, ",")}, + {kind: pkger.KindTask, idStrs: strings.Split(b.exportOpts.tasks, ","), names: strings.Split(b.exportOpts.taskNames, ",")}, + {kind: pkger.KindTelegraf, idStrs: strings.Split(b.exportOpts.telegrafs, ","), names: strings.Split(b.exportOpts.telegrafNames, ",")}, + {kind: pkger.KindVariable, idStrs: strings.Split(b.exportOpts.variables, ","), names: strings.Split(b.exportOpts.variableNames, ",")}, } var opts []pkger.ExportOptFn for _, rt := range resTypes { - newOpt, err := newResourcesToClone(rt.kind, rt.idStrs) + newOpt, err := newResourcesToClone(rt.kind, rt.idStrs, rt.names) if err != nil { return ierror.Wrap(err, rt.kind.String()) } @@ -410,7 +429,7 @@ func (b *cmdTemplateBuilder) exportRunEFn(cmd *cobra.Command, args []string) err } } - resTypeOpt, err := newResourcesToClone(resKind, args) + resTypeOpt, err := newResourcesToClone(resKind, args, []string{}) if err != nil { return err } @@ -1133,7 +1152,7 @@ func (b *cmdTemplateBuilder) convertEncoding() pkger.Encoding { } } -func newResourcesToClone(kind pkger.Kind, idStrs []string) (pkger.ExportOptFn, error) { +func newResourcesToClone(kind pkger.Kind, idStrs, names []string) (pkger.ExportOptFn, error) { ids, err := toInfluxIDs(idStrs) if err != nil { return nil, err @@ -1146,6 +1165,15 @@ func newResourcesToClone(kind pkger.Kind, idStrs []string) (pkger.ExportOptFn, e ID: id, }) } + for _, name := range names { + if len(name) == 0 { + continue + } + resources = append(resources, pkger.ResourceToClone{ + Kind: kind, + Name: name, + }) + } return pkger.ExportWithExistingResources(resources...), nil } @@ -1156,7 +1184,7 @@ func toInfluxIDs(args []string) ([]influxdb.ID, error) { ) for _, arg := range args { normedArg := strings.TrimSpace(strings.ToLower(arg)) - if normedArg == "" { + if len(normedArg) == 0 { continue } diff --git a/http/swagger.yml b/http/swagger.yml index 38ae8ca04d..4212ce71e4 100644 --- a/http/swagger.yml +++ b/http/swagger.yml @@ -4602,7 +4602,9 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/TemplateExport" + oneOf: + - $ref: "#/components/schemas/TemplateExportByID" + - $ref: "#/components/schemas/TemplateExportByName" responses: "200": description: InfluxDB template created @@ -7475,7 +7477,7 @@ components: - Task - Telegraf - Variable - TemplateExport: + TemplateExportByID: type: object properties: stackID: @@ -7507,7 +7509,39 @@ components: $ref: "#/components/schemas/TemplateKind" name: type: string + description: "if defined with id, name is used for resource exported by id. if defined independently, resources strictly matching name are exported" required: [id, kind] + TemplateExportByName: + type: object + properties: + stackID: + type: string + orgIDs: + type: array + items: + type: object + properties: + orgID: + type: string + resourceFilters: + type: object + properties: + byLabel: + type: array + items: + type: string + byResourceKind: + type: array + items: + $ref: "#/components/schemas/TemplateKind" + resources: + type: object + properties: + kind: + $ref: "#/components/schemas/TemplateKind" + name: + type: string + required: [name, kind] Template: type: array items: diff --git a/kv/dashboard.go b/kv/dashboard.go index a348169be3..e1b3cf3a92 100644 --- a/kv/dashboard.go +++ b/kv/dashboard.go @@ -135,7 +135,9 @@ func filterDashboardsFn(filter influxdb.DashboardFilter) func(d *influxdb.Dashbo } } - return func(d *influxdb.Dashboard) bool { return true } + return func(d *influxdb.Dashboard) bool { + return ((filter.OrganizationID == nil) || (*filter.OrganizationID == d.OrganizationID)) + } } // FindDashboards retrives all dashboards that match an arbitrary dashboard filter. diff --git a/pkger/clone_resource.go b/pkger/clone_resource.go index 40d9345454..aa38611818 100644 --- a/pkger/clone_resource.go +++ b/pkger/clone_resource.go @@ -27,7 +27,7 @@ type NameGenerator func() string // ResourceToClone is a resource that will be cloned. type ResourceToClone struct { Kind Kind `json:"kind"` - ID influxdb.ID `json:"id"` + ID influxdb.ID `json:"id,omitempty"` Name string `json:"name"` // note(jsteenb2): For time being we'll allow this internally, but not externally. A lot of // issues to account for when exposing this to the outside world. Not something I'm keen @@ -40,8 +40,8 @@ func (r ResourceToClone) OK() error { if err := r.Kind.OK(); err != nil { return err } - if r.ID == influxdb.ID(0) { - return errors.New("must provide an ID") + if r.ID == influxdb.ID(0) && len(r.Name) == 0 { + return errors.New("must provide an ID or name") } return nil } @@ -171,13 +171,10 @@ func (ex *resourceExporter) StackResources() []StackResource { return resources } -func (ex *resourceExporter) uniqByNameResID() influxdb.ID { - // we only need an id when we have resources that are not unique by name via the - // metastore. resoureces that are unique by name will be provided a default stamp - // making looksup unique since each resource will be unique by name. - const uniqByNameResID = 0 - return uniqByNameResID -} +// we only need an id when we have resources that are not unique by name via the +// metastore. resoureces that are unique by name will be provided a default stamp +// making looksup unique since each resource will be unique by name. +const uniqByNameResID = influxdb.ID(0) type cloneAssociationsFn func(context.Context, ResourceToClone) (associations []ObjectAssociation, skipResource bool, err error) @@ -220,77 +217,255 @@ func (ex *resourceExporter) resourceCloneToKind(ctx context.Context, r ResourceT ex.mStackResources[key] = stackResource } - uniqByNameResID := ex.uniqByNameResID() - switch { case r.Kind.is(KindBucket): - bkt, err := ex.bucketSVC.FindBucketByID(ctx, r.ID) + filter := influxdb.BucketFilter{} + if r.ID != influxdb.ID(0) { + filter.ID = &r.ID + } + if len(r.Name) > 0 { + filter.Name = &r.Name + } + + bkts, n, err := ex.bucketSVC.FindBuckets(ctx, filter) if err != nil { return err } - mapResource(bkt.OrgID, uniqByNameResID, KindBucket, BucketToObject(r.Name, *bkt)) - case r.Kind.is(KindCheck), - r.Kind.is(KindCheckDeadman), - r.Kind.is(KindCheckThreshold): - ch, err := ex.checkSVC.FindCheckByID(ctx, r.ID) + if n < 1 { + return errors.New("no buckets found") + } + + for _, bkt := range bkts { + mapResource(bkt.OrgID, bkt.ID, KindBucket, BucketToObject(r.Name, *bkt)) + } + case r.Kind.is(KindCheck), r.Kind.is(KindCheckDeadman), r.Kind.is(KindCheckThreshold): + filter := influxdb.CheckFilter{} + if r.ID != influxdb.ID(0) { + filter.ID = &r.ID + } + if len(r.Name) > 0 { + filter.Name = &r.Name + } + chs, n, err := ex.checkSVC.FindChecks(ctx, filter) if err != nil { return err } - mapResource(ch.GetOrgID(), uniqByNameResID, KindCheck, CheckToObject(r.Name, ch)) + if n < 1 { + return errors.New("no checks found") + } + + for _, ch := range chs { + mapResource(ch.GetOrgID(), ch.GetID(), KindCheck, CheckToObject(r.Name, ch)) + } case r.Kind.is(KindDashboard): - dash, err := findDashboardByIDFull(ctx, ex.dashSVC, r.ID) + var ( + hasID bool + filter = influxdb.DashboardFilter{} + ) + if r.ID != influxdb.ID(0) { + hasID = true + filter.IDs = []*influxdb.ID{&r.ID} + } + + dashes, _, err := ex.dashSVC.FindDashboards(ctx, filter, influxdb.DefaultDashboardFindOptions) if err != nil { return err } - mapResource(dash.OrganizationID, dash.ID, KindDashboard, DashboardToObject(r.Name, *dash)) + + var mapped bool + for _, dash := range dashes { + if (!hasID && len(r.Name) > 0 && dash.Name != r.Name) || (hasID && dash.ID != r.ID) { + continue + } + + for _, cell := range dash.Cells { + v, err := ex.dashSVC.GetDashboardCellView(ctx, dash.ID, cell.ID) + if err != nil { + continue + } + cell.View = v + } + + mapResource(dash.OrganizationID, dash.ID, KindDashboard, DashboardToObject(r.Name, *dash)) + mapped = true + } + + if !mapped { + return errors.New("no dashboards found") + } case r.Kind.is(KindLabel): - l, err := ex.labelSVC.FindLabelByID(ctx, r.ID) - if err != nil { - return err + switch { + case r.ID != influxdb.ID(0): + l, err := ex.labelSVC.FindLabelByID(ctx, r.ID) + if err != nil { + return err + } + + mapResource(l.OrgID, uniqByNameResID, KindLabel, LabelToObject(r.Name, *l)) + case len(r.Name) > 0: + labels, err := ex.labelSVC.FindLabels(ctx, influxdb.LabelFilter{Name: r.Name}) + if err != nil { + return err + } + + for _, l := range labels { + mapResource(l.OrgID, uniqByNameResID, KindLabel, LabelToObject(r.Name, *l)) + } } - mapResource(l.OrgID, uniqByNameResID, KindLabel, LabelToObject(r.Name, *l)) case r.Kind.is(KindNotificationEndpoint), r.Kind.is(KindNotificationEndpointHTTP), r.Kind.is(KindNotificationEndpointPagerDuty), r.Kind.is(KindNotificationEndpointSlack): - e, err := ex.endpointSVC.FindNotificationEndpointByID(ctx, r.ID) - if err != nil { - return err + var endpoints []influxdb.NotificationEndpoint + + switch { + case r.ID != influxdb.ID(0): + notifEndpoint, err := ex.endpointSVC.FindNotificationEndpointByID(ctx, r.ID) + if err != nil { + return err + } + endpoints = append(endpoints, notifEndpoint) + case len(r.Name) != 0: + allEndpoints, _, err := ex.endpointSVC.FindNotificationEndpoints(ctx, influxdb.NotificationEndpointFilter{}) + if err != nil { + return err + } + + for _, notifEndpoint := range allEndpoints { + if notifEndpoint.GetName() != r.Name || notifEndpoint == nil { + continue + } + endpoints = append(endpoints, notifEndpoint) + } + } + + if len(endpoints) == 0 { + return errors.New("no notification endpoints found") + } + + for _, e := range endpoints { + mapResource(e.GetOrgID(), uniqByNameResID, KindNotificationEndpoint, NotificationEndpointToObject(r.Name, e)) } - mapResource(e.GetOrgID(), uniqByNameResID, KindNotificationEndpoint, NotificationEndpointToObject(r.Name, e)) case r.Kind.is(KindNotificationRule): - rule, ruleEndpoint, err := ex.getEndpointRule(ctx, r.ID) - if err != nil { - return err + var rules []influxdb.NotificationRule + + switch { + case r.ID != influxdb.ID(0): + r, err := ex.ruleSVC.FindNotificationRuleByID(ctx, r.ID) + if err != nil { + return err + } + rules = append(rules, r) + case len(r.Name) != 0: + allRules, _, err := ex.ruleSVC.FindNotificationRules(ctx, influxdb.NotificationRuleFilter{}) + if err != nil { + return err + } + + for _, rule := range allRules { + if rule.GetName() != r.Name { + continue + } + rules = append(rules, rule) + } } - endpointKey := newExportKey(ruleEndpoint.GetOrgID(), uniqByNameResID, KindNotificationEndpoint, ruleEndpoint.GetName()) - object, ok := ex.mObjects[endpointKey] - if !ok { - mapResource(ruleEndpoint.GetOrgID(), uniqByNameResID, KindNotificationEndpoint, NotificationEndpointToObject("", ruleEndpoint)) - object = ex.mObjects[endpointKey] + if len(rules) == 0 { + return errors.New("no notification rules found") } - endpointObjectName := object.Name() - mapResource(rule.GetOrgID(), rule.GetID(), KindNotificationRule, NotificationRuleToObject(r.Name, endpointObjectName, rule)) + for _, rule := range rules { + ruleEndpoint, err := ex.endpointSVC.FindNotificationEndpointByID(ctx, rule.GetEndpointID()) + if err != nil { + return err + } + + endpointKey := newExportKey(ruleEndpoint.GetOrgID(), uniqByNameResID, KindNotificationEndpoint, ruleEndpoint.GetName()) + object, ok := ex.mObjects[endpointKey] + if !ok { + mapResource(ruleEndpoint.GetOrgID(), uniqByNameResID, KindNotificationEndpoint, NotificationEndpointToObject("", ruleEndpoint)) + object = ex.mObjects[endpointKey] + } + endpointObjectName := object.Name() + + mapResource(rule.GetOrgID(), rule.GetID(), KindNotificationRule, NotificationRuleToObject(r.Name, endpointObjectName, rule)) + } case r.Kind.is(KindTask): - t, err := ex.taskSVC.FindTaskByID(ctx, r.ID) - if err != nil { - return err + switch { + case r.ID != influxdb.ID(0): + t, err := ex.taskSVC.FindTaskByID(ctx, r.ID) + if err != nil { + return err + } + mapResource(t.OrganizationID, t.ID, KindTask, TaskToObject(r.Name, *t)) + case len(r.Name) > 0: + tasks, n, err := ex.taskSVC.FindTasks(ctx, influxdb.TaskFilter{Name: &r.Name}) + if err != nil { + return err + } + if n < 1 { + return errors.New("no tasks found") + } + + for _, t := range tasks { + mapResource(t.OrganizationID, t.ID, KindTask, TaskToObject(r.Name, *t)) + } } - mapResource(t.OrganizationID, t.ID, KindTask, TaskToObject(r.Name, *t)) case r.Kind.is(KindTelegraf): - t, err := ex.teleSVC.FindTelegrafConfigByID(ctx, r.ID) - if err != nil { - return err + switch { + case r.ID != influxdb.ID(0): + t, err := ex.teleSVC.FindTelegrafConfigByID(ctx, r.ID) + if err != nil { + return err + } + mapResource(t.OrgID, t.ID, KindTelegraf, TelegrafToObject(r.Name, *t)) + case len(r.Name) > 0: + telegrafs, _, err := ex.teleSVC.FindTelegrafConfigs(ctx, influxdb.TelegrafConfigFilter{}) + if err != nil { + return err + } + + var mapped bool + for _, t := range telegrafs { + if t.Name != r.Name { + continue + } + + mapResource(t.OrgID, t.ID, KindTelegraf, TelegrafToObject(r.Name, *t)) + mapped = true + } + if !mapped { + return errors.New("no telegraf configs found") + } + } - mapResource(t.OrgID, t.ID, KindTelegraf, TelegrafToObject(r.Name, *t)) case r.Kind.is(KindVariable): - v, err := ex.varSVC.FindVariableByID(ctx, r.ID) - if err != nil { - return err + switch { + case r.ID != influxdb.ID(0): + v, err := ex.varSVC.FindVariableByID(ctx, r.ID) + if err != nil { + return err + } + mapResource(v.OrganizationID, uniqByNameResID, KindVariable, VariableToObject(r.Name, *v)) + case len(r.Name) > 0: + variables, err := ex.varSVC.FindVariables(ctx, influxdb.VariableFilter{}) + if err != nil { + return err + } + + var mapped bool + for _, v := range variables { + if v.Name != r.Name { + continue + } + + mapResource(v.OrganizationID, uniqByNameResID, KindVariable, VariableToObject(r.Name, *v)) + mapped = true + } + if !mapped { + return errors.New("no variables found") + } } - mapResource(v.OrganizationID, uniqByNameResID, KindVariable, VariableToObject(r.Name, *v)) default: return errors.New("unsupported kind provided: " + string(r.Kind)) } @@ -319,6 +494,10 @@ func (ex *resourceExporter) resourceCloneAssociationsGen(ctx context.Context, la return nil, shouldSkip, nil } + if len(r.Name) > 0 && r.ID == influxdb.ID(0) { + return nil, false, nil + } + labels, err := ex.labelSVC.FindResourceLabels(ctx, influxdb.LabelMappingFilter{ ResourceID: r.ID, ResourceType: r.Kind.ResourceType(), @@ -355,7 +534,7 @@ func (ex *resourceExporter) resourceCloneAssociationsGen(ctx context.Context, la } labelObject.Metadata[fieldName] = metaName - k := newExportKey(l.OrgID, ex.uniqByNameResID(), KindLabel, l.Name) + k := newExportKey(l.OrgID, uniqByNameResID, KindLabel, l.Name) existing, ok := ex.mObjects[k] if ok { associations = append(associations, ObjectAssociation{ @@ -379,20 +558,6 @@ func (ex *resourceExporter) resourceCloneAssociationsGen(ctx context.Context, la return cloneFn, nil } -func (ex *resourceExporter) getEndpointRule(ctx context.Context, id influxdb.ID) (influxdb.NotificationRule, influxdb.NotificationEndpoint, error) { - rule, err := ex.ruleSVC.FindNotificationRuleByID(ctx, id) - if err != nil { - return nil, nil, err - } - - ruleEndpoint, err := ex.endpointSVC.FindNotificationEndpointByID(ctx, rule.GetEndpointID()) - if err != nil { - return nil, nil, err - } - - return rule, ruleEndpoint, nil -} - func (ex *resourceExporter) uniqName() string { return uniqMetaName(ex.nameGen, idGenerator, ex.mPkgNames) } @@ -409,21 +574,6 @@ func uniqMetaName(nameGen NameGenerator, idGen influxdb.IDGenerator, existingNam return name } -func findDashboardByIDFull(ctx context.Context, dashSVC influxdb.DashboardService, id influxdb.ID) (*influxdb.Dashboard, error) { - dash, err := dashSVC.FindDashboardByID(ctx, id) - if err != nil { - return nil, err - } - for _, cell := range dash.Cells { - v, err := dashSVC.GetDashboardCellView(ctx, id, cell.ID) - if err != nil { - return nil, err - } - cell.View = v - } - return dash, nil -} - func uniqResourcesToClone(resources []ResourceToClone) []ResourceToClone { type key struct { kind Kind diff --git a/pkger/parser.go b/pkger/parser.go index 924de07c6f..ce7ff6d634 100644 --- a/pkger/parser.go +++ b/pkger/parser.go @@ -860,7 +860,8 @@ func (p *Template) graphLabels() *parseErr { func (p *Template) graphChecks() *parseErr { p.mChecks = make(map[string]*check) - tracker := p.trackNames(true) + // todo: what is the business goal wrt having unique names? (currently duplicates are allowed) + tracker := p.trackNames(false) checkKinds := []struct { kind Kind diff --git a/pkger/parser_models.go b/pkger/parser_models.go index 6c78363cb6..a43b7c4555 100644 --- a/pkger/parser_models.go +++ b/pkger/parser_models.go @@ -2123,7 +2123,6 @@ func (v *variable) summarize() SummaryVariable { envRefs = append(envRefs, convertRefToRefSummary(field, sel)) } } - return SummaryVariable{ SummaryIdentifier: SummaryIdentifier{ Kind: KindVariable, diff --git a/pkger/parser_test.go b/pkger/parser_test.go index ade8a15e45..1bf1d5166e 100644 --- a/pkger/parser_test.go +++ b/pkger/parser_test.go @@ -904,40 +904,42 @@ spec: `, }, }, - { - kind: KindCheckDeadman, - resErr: testTemplateResourceError{ - name: "duplicate meta name and spec name", - validationErrs: 1, - valFields: []string{fieldSpec, fieldAssociations}, - templateStr: ` -apiVersion: influxdata.com/v2alpha1 -kind: CheckDeadman -metadata: - name: check-1 -spec: - every: 5m - level: cRiT - query: > - from(bucket: "rucket_1") |> yield(name: "mean") - statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }" - timeSince: 90s ---- -apiVersion: influxdata.com/v2alpha1 -kind: CheckDeadman -metadata: - name: valid-name -spec: - name: check-1 - every: 5m - level: cRiT - query: > - from(bucket: "rucket_1") |> yield(name: "mean") - statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }" - timeSince: 90s -`, - }, - }, + /* checks are not name unique + { + kind: KindCheckDeadman, + resErr: testTemplateResourceError{ + name: "duplicate meta name and spec name", + validationErrs: 1, + valFields: []string{fieldSpec, fieldAssociations}, + templateStr: ` + apiVersion: influxdata.com/v2alpha1 + kind: CheckDeadman + metadata: + name: check-1 + spec: + every: 5m + level: cRiT + query: > + from(bucket: "rucket_1") |> yield(name: "mean") + statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }" + timeSince: 90s + --- + apiVersion: influxdata.com/v2alpha1 + kind: CheckDeadman + metadata: + name: valid-name + spec: + name: check-1 + every: 5m + level: cRiT + query: > + from(bucket: "rucket_1") |> yield(name: "mean") + statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }" + timeSince: 90s + `, + }, + }, + */ } for _, tt := range tests { diff --git a/pkger/service.go b/pkger/service.go index 0a21af3c3a..a4b874a70c 100644 --- a/pkger/service.go +++ b/pkger/service.go @@ -72,6 +72,7 @@ type ( StackResource struct { APIVersion string ID influxdb.ID + Name string Kind Kind MetaName string Associations []StackResourceAssociation @@ -631,6 +632,7 @@ func (s *Service) Export(ctx context.Context, setters ...ExportOptFn) (*Template Kind: r.Kind, ID: r.ID, MetaName: r.MetaName, + Name: r.Name, })) } @@ -694,6 +696,7 @@ func (s *Service) cloneOrgBuckets(ctx context.Context, orgID influxdb.ID) ([]Res resources = append(resources, ResourceToClone{ Kind: KindBucket, ID: b.ID, + Name: b.Name, }) } return resources, nil @@ -712,6 +715,7 @@ func (s *Service) cloneOrgChecks(ctx context.Context, orgID influxdb.ID) ([]Reso resources = append(resources, ResourceToClone{ Kind: KindCheck, ID: c.GetID(), + Name: c.GetName(), }) } return resources, nil @@ -736,9 +740,11 @@ func (s *Service) cloneOrgDashboards(ctx context.Context, orgID influxdb.ID) ([] } func (s *Service) cloneOrgLabels(ctx context.Context, orgID influxdb.ID) ([]ResourceToClone, error) { - labels, err := s.labelSVC.FindLabels(ctx, influxdb.LabelFilter{ + filter := influxdb.LabelFilter{ OrgID: &orgID, - }, influxdb.FindOptions{Limit: 10000}) + } + + labels, err := s.labelSVC.FindLabels(ctx, filter, influxdb.FindOptions{Limit: 100}) if err != nil { return nil, ierrors.Wrap(err, "finding labels") } @@ -748,6 +754,7 @@ func (s *Service) cloneOrgLabels(ctx context.Context, orgID influxdb.ID) ([]Reso resources = append(resources, ResourceToClone{ Kind: KindLabel, ID: l.ID, + Name: l.Name, }) } return resources, nil @@ -766,6 +773,7 @@ func (s *Service) cloneOrgNotificationEndpoints(ctx context.Context, orgID influ resources = append(resources, ResourceToClone{ Kind: KindNotificationEndpoint, ID: e.GetID(), + Name: e.GetName(), }) } return resources, nil @@ -784,6 +792,7 @@ func (s *Service) cloneOrgNotificationRules(ctx context.Context, orgID influxdb. resources = append(resources, ResourceToClone{ Kind: KindNotificationRule, ID: r.GetID(), + Name: r.GetName(), }) } return resources, nil @@ -869,12 +878,15 @@ func (s *Service) cloneOrgVariables(ctx context.Context, orgID influxdb.ID) ([]R return resources, nil } -type cloneResFn func(context.Context, influxdb.ID) ([]ResourceToClone, error) +type ( + cloneResFn func(context.Context, influxdb.ID) ([]ResourceToClone, error) + resClone struct { + resType influxdb.ResourceType + cloneFn cloneResFn + } +) -func (s *Service) filterOrgResourceKinds(resourceKindFilters []Kind) []struct { - resType influxdb.ResourceType - cloneFn cloneResFn -} { +func (s *Service) filterOrgResourceKinds(resourceKindFilters []Kind) []resClone { mKinds := map[Kind]cloneResFn{ KindBucket: s.cloneOrgBuckets, KindCheck: s.cloneOrgChecks, @@ -887,23 +899,14 @@ func (s *Service) filterOrgResourceKinds(resourceKindFilters []Kind) []struct { KindVariable: s.cloneOrgVariables, } - newResGen := func(resType influxdb.ResourceType, cloneFn cloneResFn) struct { - resType influxdb.ResourceType - cloneFn cloneResFn - } { - return struct { - resType influxdb.ResourceType - cloneFn cloneResFn - }{ + newResGen := func(resType influxdb.ResourceType, cloneFn cloneResFn) resClone { + return resClone{ resType: resType, cloneFn: cloneFn, } } - var resourceTypeGens []struct { - resType influxdb.ResourceType - cloneFn cloneResFn - } + var resourceTypeGens []resClone if len(resourceKindFilters) == 0 { for k, cloneFn := range mKinds { resourceTypeGens = append(resourceTypeGens, newResGen(k.ResourceType(), cloneFn)) diff --git a/pkger/service_logging.go b/pkger/service_logging.go index 54e24b9a11..b34cd7838c 100644 --- a/pkger/service_logging.go +++ b/pkger/service_logging.go @@ -148,7 +148,7 @@ func (s *loggingMW) Export(ctx context.Context, opts ...ExportOptFn) (template * s.logger.Error("failed to export template", zap.Error(err), dur) return } - s.logger.Info("failed to export template", append(s.summaryLogFields(template.Summary()), dur)...) + s.logger.Info("exported template", append(s.summaryLogFields(template.Summary()), dur)...) }(time.Now()) return s.next.Export(ctx, opts...) } diff --git a/pkger/service_test.go b/pkger/service_test.go index 93181ba297..618f2dd4e4 100644 --- a/pkger/service_test.go +++ b/pkger/service_test.go @@ -1834,6 +1834,16 @@ func TestService(t *testing.T) { } return expected, nil } + bktSVC.FindBucketsFn = func(_ context.Context, filter influxdb.BucketFilter, _ ...influxdb.FindOptions) ([]*influxdb.Bucket, int, error) { + if filter.ID != nil { + if *filter.ID != expected.ID { + return nil, 0, errors.New("uh ohhh, wrong id here: " + filter.ID.String()) + } + } else if filter.Name != nil && *filter.Name != expected.Name { + return nil, 0, errors.New("uh ohhh, wrong name here: " + *filter.Name) + } + return []*influxdb.Bucket{expected}, 1, nil + } svc := newTestService(WithBucketSVC(bktSVC), WithLabelSVC(mock.NewLabelService())) @@ -1863,6 +1873,129 @@ func TestService(t *testing.T) { } }) + // todo: bucket names are unique. + t.Run("bucket by name", func(t *testing.T) { + knownBuckets := []*influxdb.Bucket{ + { + ID: influxdb.ID(1), + Name: "bucket", + Description: "desc", + RetentionPeriod: time.Hour, + }, + { + ID: influxdb.ID(2), + Name: "bucketCopy", + Description: "desc", + RetentionPeriod: time.Hour, + }, + { + ID: influxdb.ID(3), + Name: "bucket3", + Description: "desc", + RetentionPeriod: time.Hour, + }, + } + + tests := []struct { + name string + findName string + findID influxdb.ID + expected []*influxdb.Bucket + }{ + { + name: "find bucket with unique name", + findName: "bucket", + expected: []*influxdb.Bucket{knownBuckets[0]}, + }, + { + name: "find no buckets", + findName: "fakeBucket", + expected: nil, + }, + { + name: "find bucket by id", + findID: influxdb.ID(2), + expected: []*influxdb.Bucket{knownBuckets[1]}, + }, + { + // todo: verify this is intended behavior (it is in swagger) + name: "find by id, set new name", + findID: influxdb.ID(2), + findName: "renamedBucket", + expected: []*influxdb.Bucket{knownBuckets[1]}, + }, + } + + for _, tt := range tests { + fn := func(t *testing.T) { + bktSVC := mock.NewBucketService() + bktSVC.FindBucketsFn = func(_ context.Context, filter influxdb.BucketFilter, _ ...influxdb.FindOptions) ([]*influxdb.Bucket, int, error) { + if filter.ID != nil { + for i := range knownBuckets { + if knownBuckets[i].ID == *filter.ID { + return []*influxdb.Bucket{knownBuckets[i]}, 1, nil + } + } + return nil, 0, errors.New("uh ohhh, wrong id here: " + filter.ID.String()) + } else if filter.Name != nil { + bkts := []*influxdb.Bucket{} + + for i := range knownBuckets { + if knownBuckets[i].Name == *filter.Name { + bkts = append(bkts, knownBuckets[i]) + } + } + + if lBkts := len(bkts); lBkts > 0 { + return bkts, lBkts, nil + } + return nil, 0, errors.New("uh ohhh, wrong name here: " + *filter.Name) + } + + return knownBuckets, len(knownBuckets), nil + } + + resToClone := ResourceToClone{ + Kind: KindBucket, + } + if tt.findName != "" { + resToClone.Name = tt.findName + } + if tt.findID != influxdb.ID(0) { + resToClone.ID = tt.findID + } + + svc := newTestService( + WithBucketSVC(bktSVC), + WithLabelSVC(mock.NewLabelService()), + ) + template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) + if tt.expected == nil { + require.Error(t, err) + } else { + require.NoError(t, err) + + if tt.findName != "" && tt.findID != influxdb.ID(0) { + tt.expected[0].Name = tt.findName + } + + actual := template.Summary().Buckets + require.Len(t, actual, len(tt.expected)) + + for i := range actual { + // can't verify id's match due to the use of SafeID's + assert.Equal(t, tt.expected[i].Name, actual[i].Name) + assert.Equal(t, tt.expected[i].Description, actual[i].Description) + assert.Equal(t, tt.expected[i].RetentionPeriod, actual[i].RetentionPeriod) + } + + assert.True(t, encodeAndDecode(t, template) != nil) + } + } + t.Run(tt.name, fn) + } + }) + t.Run("checks", func(t *testing.T) { tests := []struct { name string @@ -1934,6 +2067,17 @@ func TestService(t *testing.T) { } return tt.expected, nil } + checkSVC.FindChecksFn = func(_ context.Context, filter influxdb.CheckFilter, _ ...influxdb.FindOptions) ([]influxdb.Check, int, error) { + if filter.ID != nil { + if *filter.ID != tt.expected.GetID() { + return nil, 0, errors.New("uh ohhh, wrong id here: " + filter.ID.String()) + } + } else if filter.Name != nil && *filter.Name != tt.expected.GetName() { + return nil, 0, errors.New("uh ohhh, wrong name here: " + *filter.Name) + } + + return []influxdb.Check{tt.expected}, 1, nil + } svc := newTestService(WithCheckSVC(checkSVC)) @@ -1961,6 +2105,179 @@ func TestService(t *testing.T) { } }) + t.Run("checks by name", func(t *testing.T) { + knownChecks := []influxdb.Check{ + &icheck.Threshold{ + Base: newThresholdBase(0), + Thresholds: []icheck.ThresholdConfig{ + icheck.Lesser{ + ThresholdConfigBase: icheck.ThresholdConfigBase{ + AllValues: true, + Level: notification.Critical, + }, + Value: 20, + }, + icheck.Greater{ + ThresholdConfigBase: icheck.ThresholdConfigBase{ + AllValues: true, + Level: notification.Warn, + }, + Value: 30, + }, + icheck.Range{ + ThresholdConfigBase: icheck.ThresholdConfigBase{ + AllValues: true, + Level: notification.Info, + }, + Within: false, // outside_range + Min: 10, + Max: 25, + }, + icheck.Range{ + ThresholdConfigBase: icheck.ThresholdConfigBase{ + AllValues: true, + Level: notification.Ok, + }, + Within: true, // inside_range + Min: 21, + Max: 24, + }, + }, + }, + &icheck.Deadman{ + Base: newThresholdBase(1), + TimeSince: mustDuration(t, time.Hour), + StaleTime: mustDuration(t, 5*time.Hour), + ReportZero: true, + Level: notification.Critical, + }, + &icheck.Deadman{ + Base: icheck.Base{ + ID: influxdb.ID(2), + TaskID: 300, + Name: "check_1", + Description: "desc_2", + Every: mustDuration(t, 2*time.Minute), + Offset: mustDuration(t, 30*time.Second), + Query: influxdb.DashboardQuery{ + Text: `from(bucket: "telegraf") |> range(start: -1m) |> filter(fn: (r) => r._field == "usage_user")`, + }, + StatusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }", + Tags: []influxdb.Tag{ + {Key: "key_1", Value: "val_1"}, + {Key: "key_2", Value: "val_2"}, + }, + }, + TimeSince: mustDuration(t, time.Hour), + StaleTime: mustDuration(t, 5*time.Hour), + ReportZero: true, + Level: notification.Critical, + }, + } + + tests := []struct { + name string + findName string + findID influxdb.ID + expected []influxdb.Check + }{ + { + name: "find check with unique name", + findName: "check_0", + expected: []influxdb.Check{knownChecks[0]}, + }, + { + name: "find multiple checks with same name", + findName: "check_1", + expected: []influxdb.Check{knownChecks[1], knownChecks[2]}, + }, + { + name: "find no checks", + findName: "fakeCheck", + expected: nil, + }, + { + name: "find check by id", + findID: influxdb.ID(1), + expected: []influxdb.Check{knownChecks[1]}, + }, + { + name: "find check by id, set new name", + findID: influxdb.ID(1), + findName: "chex original", + expected: []influxdb.Check{knownChecks[1]}, + }, + } + + for _, tt := range tests { + fn := func(t *testing.T) { + checkSVC := mock.NewCheckService() + checkSVC.FindChecksFn = func(_ context.Context, filter influxdb.CheckFilter, _ ...influxdb.FindOptions) ([]influxdb.Check, int, error) { + if filter.ID != nil { + for i := range knownChecks { + if knownChecks[i].GetID() == *filter.ID { + return []influxdb.Check{knownChecks[i]}, 1, nil + } + } + return nil, 0, errors.New("uh ohhh, wrong id here: " + filter.ID.String()) + } else if filter.Name != nil { + checks := []influxdb.Check{} + + for i := range knownChecks { + if knownChecks[i].GetName() == *filter.Name { + checks = append(checks, knownChecks[i]) + } + } + + if lChecks := len(checks); lChecks > 0 { + return checks, lChecks, nil + } + + return nil, 0, errors.New("uh ohhh, wrong name here: " + *filter.Name) + } + + return knownChecks, len(knownChecks), nil + } + + resToClone := ResourceToClone{ + Kind: KindCheck, + } + if tt.findName != "" { + resToClone.Name = tt.findName + } + if tt.findID != influxdb.ID(0) { + resToClone.ID = tt.findID + } + + svc := newTestService(WithCheckSVC(checkSVC)) + template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) + if tt.expected == nil { + require.Error(t, err) + } else { + require.NoError(t, err) + + if tt.findName != "" && tt.findID != influxdb.ID(0) { + tt.expected[0].SetName(tt.findName) + } + + actual := template.Summary().Checks + require.Len(t, actual, len(tt.expected)) + sort.Slice(actual, func(i, j int) bool { + return actual[i].Check.GetDescription() < actual[j].Check.GetDescription() + }) + + for i := range actual { + assert.Equal(t, tt.expected[i].GetName(), actual[i].Check.GetName()) + assert.Equal(t, tt.expected[i].GetDescription(), actual[i].Check.GetDescription()) + } + + assert.True(t, encodeAndDecode(t, template) != nil) + } + } + t.Run(tt.name, fn) + } + }) + newQuery := func() influxdb.DashboardQuery { return influxdb.DashboardQuery{ Text: "from(v.bucket) |> count()", @@ -2381,6 +2698,15 @@ func TestService(t *testing.T) { } return expected, nil } + dashSVC.FindDashboardsF = func(_ context.Context, filter influxdb.DashboardFilter, _ influxdb.FindOptions) ([]*influxdb.Dashboard, int, error) { + if len(filter.IDs) < 1 { + return nil, 0, errors.New("uh ohhh, no id here") + } + if filter.IDs[0] != nil && *filter.IDs[0] != expected.ID { + return nil, 0, errors.New("uh ohhh, wrong id here: " + filter.IDs[0].String()) + } + return []*influxdb.Dashboard{expected}, 1, nil + } dashSVC.GetDashboardCellViewF = func(_ context.Context, id influxdb.ID, cID influxdb.ID) (*influxdb.View, error) { if id == expected.ID && cID == expectedCell.ID { return &tt.expectedView, nil @@ -2432,6 +2758,16 @@ func TestService(t *testing.T) { Description: "desc", }, nil } + dashSVC.FindDashboardsF = func(_ context.Context, filter influxdb.DashboardFilter, _ influxdb.FindOptions) ([]*influxdb.Dashboard, int, error) { + if len(filter.IDs) < 1 { + return nil, 0, errors.New("uh ohhh, no id here") + } + return []*influxdb.Dashboard{{ + ID: *filter.IDs[0], + Name: "dash name", + Description: "desc", + }}, 1, nil + } svc := newTestService(WithDashboardSVC(dashSVC), WithLabelSVC(mock.NewLabelService())) @@ -2461,6 +2797,196 @@ func TestService(t *testing.T) { }) }) + t.Run("dashboard by name", func(t *testing.T) { + id := 0 + newDash := func(name string, view influxdb.View) *influxdb.Dashboard { + id++ + return &influxdb.Dashboard{ + ID: influxdb.ID(id), + Name: name, + Description: fmt.Sprintf("desc_%d", id), + Cells: []*influxdb.Cell{ + { + ID: 0, + CellProperty: influxdb.CellProperty{X: 1, Y: 2, W: 3, H: 4}, + View: &view, + }, + }, + } + } + knownDashboards := []*influxdb.Dashboard{ + newDash("dasher", influxdb.View{ + ViewContents: influxdb.ViewContents{ + Name: "view name", + }, + Properties: influxdb.GaugeViewProperties{ + Type: influxdb.ViewPropertyTypeGauge, + DecimalPlaces: influxdb.DecimalPlaces{IsEnforced: true, Digits: 1}, + Note: "a note", + Prefix: "pre", + TickPrefix: "true", + Suffix: "suf", + TickSuffix: "false", + Queries: []influxdb.DashboardQuery{newQuery()}, + ShowNoteWhenEmpty: true, + ViewColors: newColors("min", "max", "threshold"), + }, + }), + newDash("prancer", influxdb.View{ + ViewContents: influxdb.ViewContents{ + Name: "view name", + }, + Properties: influxdb.HeatmapViewProperties{ + Type: influxdb.ViewPropertyTypeHeatMap, + Note: "a note", + Queries: []influxdb.DashboardQuery{newQuery()}, + ShowNoteWhenEmpty: true, + ViewColors: []string{"#8F8AF4", "#8F8AF4", "#8F8AF4"}, + XColumn: "x", + YColumn: "y", + XDomain: []float64{0, 10}, + YDomain: []float64{0, 100}, + XAxisLabel: "x_label", + XPrefix: "x_prefix", + XSuffix: "x_suffix", + YAxisLabel: "y_label", + YPrefix: "y_prefix", + YSuffix: "y_suffix", + BinSize: 10, + TimeFormat: "", + }, + }), + newDash("prancer", influxdb.View{ + ViewContents: influxdb.ViewContents{ + Name: "view name", + }, + Properties: influxdb.HistogramViewProperties{ + Type: influxdb.ViewPropertyTypeHistogram, + Note: "a note", + Queries: []influxdb.DashboardQuery{newQuery()}, + ShowNoteWhenEmpty: true, + ViewColors: []influxdb.ViewColor{{Type: "scale", Hex: "#8F8AF4", Value: 0}, {Type: "scale", Hex: "#8F8AF4", Value: 0}, {Type: "scale", Hex: "#8F8AF4", Value: 0}}, + FillColumns: []string{"a", "b"}, + XColumn: "_value", + XDomain: []float64{0, 10}, + XAxisLabel: "x_label", + BinCount: 30, + Position: "stacked", + }, + }), + } + + tests := []struct { + name string + findName string + findID influxdb.ID + expected []*influxdb.Dashboard + }{ + { + name: "find dash with unique name", + findName: "dasher", + expected: []*influxdb.Dashboard{knownDashboards[0]}, + }, + { + name: "find multiple dash with shared name", + findName: "prancer", + expected: []*influxdb.Dashboard{knownDashboards[1], knownDashboards[2]}, + }, + { + name: "find no dash", + findName: "fakeDash", + expected: nil, + }, + { + name: "find dash by id", + findID: 1, + expected: []*influxdb.Dashboard{knownDashboards[0]}, + }, + { + name: "find dash by id, set new name", + findID: 1, + findName: "dancer", + expected: []*influxdb.Dashboard{knownDashboards[0]}, + }, + } + + for _, tt := range tests { + fn := func(t *testing.T) { + dashSVC := mock.NewDashboardService() + dashSVC.FindDashboardsF = func(_ context.Context, filter influxdb.DashboardFilter, _ influxdb.FindOptions) ([]*influxdb.Dashboard, int, error) { + if filter.IDs != nil && filter.IDs[0] != nil { + for i := range knownDashboards { + if knownDashboards[i].ID == *filter.IDs[0] { + return []*influxdb.Dashboard{knownDashboards[i]}, 1, nil + } + } + + return nil, 0, errors.New("uh ohhh, wrong id here: " + filter.IDs[0].String()) + } + + return knownDashboards, len(knownDashboards), nil + } + + dashSVC.GetDashboardCellViewF = func(_ context.Context, id influxdb.ID, cID influxdb.ID) (*influxdb.View, error) { + for i := range knownDashboards { + if knownDashboards[i].ID == id { + return knownDashboards[i].Cells[0].View, nil + } + } + + return nil, errors.New("wrongo ids") + } + + resToClone := ResourceToClone{ + Kind: KindDashboard, + } + if tt.findName != "" { + resToClone.Name = tt.findName + } + if tt.findID != influxdb.ID(0) { + resToClone.ID = tt.findID + } + + svc := newTestService( + WithDashboardSVC(dashSVC), + WithLabelSVC(mock.NewLabelService()), + ) + template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) + if tt.expected == nil { + require.Error(t, err) + } else { + require.NoError(t, err) + + if tt.findName != "" && tt.findID != influxdb.ID(0) { + tt.expected[0].Name = tt.findName + } + + actual := template.Summary().Dashboards + require.Len(t, actual, len(tt.expected)) + sort.Slice(actual, func(i, j int) bool { + return actual[i].Description < actual[j].Description + }) + + for i := range actual { + assert.Equal(t, tt.expected[i].Name, actual[i].Name) + assert.Equal(t, tt.expected[i].Description, actual[i].Description) + + require.Len(t, actual[i].Charts, 1) + ch := actual[i].Charts[0] + assert.Equal(t, int(tt.expected[i].Cells[0].CellProperty.X), ch.XPosition) + assert.Equal(t, int(tt.expected[i].Cells[0].CellProperty.Y), ch.YPosition) + assert.Equal(t, int(tt.expected[i].Cells[0].CellProperty.H), ch.Height) + assert.Equal(t, int(tt.expected[i].Cells[0].CellProperty.W), ch.Width) + assert.Equal(t, tt.expected[i].Cells[0].View.Properties, ch.Properties) + } + + assert.True(t, encodeAndDecode(t, template) != nil) + } + } + t.Run(tt.name, fn) + } + }) + t.Run("label", func(t *testing.T) { tests := []struct { name string @@ -2493,6 +3019,12 @@ func TestService(t *testing.T) { } return expectedLabel, nil } + labelSVC.FindLabelsFn = func(_ context.Context, filter influxdb.LabelFilter) ([]*influxdb.Label, error) { + if filter.Name != expectedLabel.Name { + return nil, errors.New("uh ohhh, wrong name here: " + filter.Name) + } + return []*influxdb.Label{expectedLabel}, nil + } svc := newTestService(WithLabelSVC(labelSVC)) @@ -2522,6 +3054,117 @@ func TestService(t *testing.T) { } }) + t.Run("label by name", func(t *testing.T) { + knownLabels := []*influxdb.Label{ + { + ID: 1, + Name: "label one", + Properties: map[string]string{ + "description": "desc", + "color": "red", + }, + }, + { + ID: 2, + Name: "label two", + Properties: map[string]string{ + "description": "desc2", + "color": "green", + }, + }, + } + tests := []struct { + name string + findName string + findID influxdb.ID + expected []*influxdb.Label + }{ + { + name: "find label by name", + findName: "label one", + expected: []*influxdb.Label{knownLabels[0]}, + }, + { + name: "find no label", + findName: "label none", + expected: nil, + }, + { + name: "find label by id", + findID: influxdb.ID(2), + expected: []*influxdb.Label{knownLabels[1]}, + }, + { + name: "find label by id, set new name", + findName: "label three", + findID: influxdb.ID(2), + expected: []*influxdb.Label{knownLabels[1]}, + }, + } + + for _, tt := range tests { + fn := func(t *testing.T) { + labelSVC := mock.NewLabelService() + labelSVC.FindLabelByIDFn = func(_ context.Context, id influxdb.ID) (*influxdb.Label, error) { + for i := range knownLabels { + if knownLabels[i].ID == id { + return knownLabels[i], nil + } + } + + return nil, errors.New("uh ohhh, wrong id here: " + id.String()) + } + labelSVC.FindLabelsFn = func(_ context.Context, filter influxdb.LabelFilter) ([]*influxdb.Label, error) { + if filter.Name != "" { + for i := range knownLabels { + if knownLabels[i].Name == filter.Name { + return []*influxdb.Label{knownLabels[i]}, nil + } + } + + return nil, errors.New("uh ohhh, wrong name here: " + filter.Name) + } + + return knownLabels, nil + } + + resToClone := ResourceToClone{ + Kind: KindLabel, + } + if tt.findName != "" { + resToClone.Name = tt.findName + } + if tt.findID != influxdb.ID(0) { + resToClone.ID = tt.findID + } + + svc := newTestService(WithLabelSVC(labelSVC)) + template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) + if tt.expected == nil { + require.Error(t, err) + } else { + require.NoError(t, err) + + if tt.findName != "" && tt.findID != influxdb.ID(0) { + tt.expected[0].Name = tt.findName + } + + actual := template.Summary().Labels + require.Len(t, actual, len(tt.expected)) + + for i := range actual { + assert.Equal(t, tt.expected[i].Name, actual[i].Name) + assert.Equal(t, tt.expected[i].Properties["color"], actual[i].Properties.Color) + assert.Equal(t, tt.expected[i].Properties["description"], actual[i].Properties.Description) + } + + assert.True(t, encodeAndDecode(t, template) != nil) + } + } + t.Run(tt.name, fn) + } + }) + t.Run("notification endpoints", func(t *testing.T) { tests := []struct { name string @@ -2621,6 +3264,12 @@ func TestService(t *testing.T) { } return tt.expected, nil } + endpointSVC.FindNotificationEndpointsF = func(ctx context.Context, filter influxdb.NotificationEndpointFilter, _ ...influxdb.FindOptions) ([]influxdb.NotificationEndpoint, int, error) { + if filter.ID != nil && *filter.ID != tt.expected.GetID() { + return nil, 0, errors.New("uh ohhh, wrong id here: " + filter.ID.String()) + } + return []influxdb.NotificationEndpoint{tt.expected}, 1, nil + } svc := newTestService(WithNotificationEndpointSVC(endpointSVC)) @@ -2651,6 +3300,133 @@ func TestService(t *testing.T) { } }) + knownEndpoints := []influxdb.NotificationEndpoint{ + &endpoint.PagerDuty{ + Base: endpoint.Base{ + ID: newTestIDPtr(1), + Name: "pd endpoint", + Description: "desc", + Status: influxdb.TaskStatusActive, + }, + ClientURL: "http://example.com", + RoutingKey: influxdb.SecretField{Key: "-routing-key"}, + }, + &endpoint.PagerDuty{ + Base: endpoint.Base{ + ID: newTestIDPtr(2), + Name: "pd-endpoint", + Description: "desc pd", + Status: influxdb.TaskStatusActive, + }, + ClientURL: "http://example.com", + RoutingKey: influxdb.SecretField{Key: "-routing-key"}, + }, + &endpoint.Slack{ + Base: endpoint.Base{ + ID: newTestIDPtr(3), + Name: "slack endpoint", + Description: "desc slack", + Status: influxdb.TaskStatusInactive, + }, + URL: "http://example.com", + Token: influxdb.SecretField{Key: "tokne"}, + }, + } + + t.Run("notification endpoints by name", func(t *testing.T) { + tests := []struct { + name string + findName string + findID influxdb.ID + expected []influxdb.NotificationEndpoint + }{ + { + name: "find notification endpoint with unique name", + findName: "pd endpoint", + expected: []influxdb.NotificationEndpoint{knownEndpoints[0]}, + }, + { + name: "find no notification endpoints", + findName: "fakeEndpoint", + expected: nil, + }, + { + name: "find notification endpoint by id", + findID: influxdb.ID(2), + expected: []influxdb.NotificationEndpoint{knownEndpoints[1]}, + }, + { + name: "find by id, set new name", + findID: influxdb.ID(3), + findName: "slack-endpoint", + expected: []influxdb.NotificationEndpoint{knownEndpoints[2]}, + }, + } + + for _, tt := range tests { + fn := func(t *testing.T) { + endpointSVC := mock.NewNotificationEndpointService() + endpointSVC.FindNotificationEndpointByIDF = func(ctx context.Context, id influxdb.ID) (influxdb.NotificationEndpoint, error) { + for i := range knownEndpoints { + if knownEndpoints[i].GetID() == id { + return knownEndpoints[i], nil + } + } + + return nil, errors.New("uh ohhh, wrong endpoint id here: " + id.String()) + } + endpointSVC.FindNotificationEndpointsF = func(ctx context.Context, filter influxdb.NotificationEndpointFilter, _ ...influxdb.FindOptions) ([]influxdb.NotificationEndpoint, int, error) { + if filter.ID != nil { + for i := range knownEndpoints { + if knownEndpoints[i].GetID() == *filter.ID { + return []influxdb.NotificationEndpoint{knownEndpoints[i]}, 1, nil + } + } + + return nil, 0, errors.New("uh ohhh, wrong id here: " + filter.ID.String()) + } + + return knownEndpoints, len(knownEndpoints), nil + } + + resToClone := ResourceToClone{ + Kind: KindNotificationEndpoint, + } + if tt.findName != "" { + resToClone.Name = tt.findName + } + if tt.findID != influxdb.ID(0) { + resToClone.ID = tt.findID + } + + svc := newTestService(WithNotificationEndpointSVC(endpointSVC)) + template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) + if tt.expected == nil { + require.Error(t, err) + } else { + require.NoError(t, err) + + if tt.findName != "" && tt.findID != influxdb.ID(0) { + tt.expected[0].SetName(tt.findName) + } + + actual := template.Summary().NotificationEndpoints + require.Len(t, actual, len(tt.expected)) + + for i := range actual { + assert.Equal(t, tt.expected[i].GetName(), actual[i].NotificationEndpoint.GetName()) + assert.Equal(t, tt.expected[i].GetDescription(), actual[i].NotificationEndpoint.GetDescription()) + assert.Equal(t, tt.expected[i].GetStatus(), actual[i].NotificationEndpoint.GetStatus()) + assert.Equal(t, tt.expected[i].SecretFields(), actual[i].NotificationEndpoint.SecretFields()) + } + + assert.True(t, encodeAndDecode(t, template) != nil) + } + } + t.Run(tt.name, fn) + } + }) + t.Run("notification rules", func(t *testing.T) { newRuleBase := func(id int) rule.Base { return rule.Base{ @@ -2745,6 +3521,9 @@ func TestService(t *testing.T) { ruleSVC.FindNotificationRuleByIDF = func(ctx context.Context, id influxdb.ID) (influxdb.NotificationRule, error) { return tt.rule, nil } + ruleSVC.FindNotificationRulesF = func(ctx context.Context, _ influxdb.NotificationRuleFilter, _ ...influxdb.FindOptions) ([]influxdb.NotificationRule, int, error) { + return []influxdb.NotificationRule{tt.rule}, 1, nil + } svc := newTestService( WithNotificationEndpointSVC(endpointSVC), @@ -2881,6 +3660,193 @@ func TestService(t *testing.T) { }) }) + t.Run("notification rules by name", func(t *testing.T) { + newRuleBase := func(id int, name string) rule.Base { + return rule.Base{ + ID: influxdb.ID(id), + Name: name, + Description: fmt.Sprintf("desc %d", id), + EndpointID: influxdb.ID(1), // todo: setting to id as well likely doesn't work due to safeID + Every: mustDuration(t, time.Hour), + Offset: mustDuration(t, time.Minute), + TagRules: []notification.TagRule{ + {Tag: influxdb.Tag{Key: "k1", Value: "v1"}}, + }, + StatusRules: []notification.StatusRule{ + {CurrentLevel: notification.Ok, PreviousLevel: levelPtr(notification.Warn)}, + {CurrentLevel: notification.Critical}, + }, + } + } + + knownRules := []influxdb.NotificationRule{ + &rule.PagerDuty{ + Base: newRuleBase(1, "pd notify"), + MessageTemplate: "Template", + }, + &rule.PagerDuty{ + Base: newRuleBase(2, "pd-notify"), + MessageTemplate: "Template2 ", + }, + &rule.Slack{ + Base: newRuleBase(3, "pd-notify"), + Channel: "abc", + MessageTemplate: "SLACK TEMPlate", + }, + } + + tests := []struct { + name string + findName string + findID influxdb.ID + expected []influxdb.NotificationRule + }{ + { + name: "find rule with unique name", + findName: "pd notify", + expected: []influxdb.NotificationRule{knownRules[0]}, + }, + { + name: "find multiple rules with shared name", + findName: "pd-notify", + expected: []influxdb.NotificationRule{knownRules[1], knownRules[2]}, + }, + { + name: "find no rules", + findName: "fakeRule", + expected: nil, + }, + { + name: "find rule by id", + findID: influxdb.ID(2), + expected: []influxdb.NotificationRule{knownRules[1]}, + }, + { + name: "find by id, set new name", + findID: influxdb.ID(3), + findName: "slack-notify", + expected: []influxdb.NotificationRule{knownRules[2]}, + }, + } + + for _, tt := range tests { + fn := func(t *testing.T) { + endpointSVC := mock.NewNotificationEndpointService() + endpointSVC.FindNotificationEndpointByIDF = func(ctx context.Context, id influxdb.ID) (influxdb.NotificationEndpoint, error) { + for i := range knownEndpoints { + if knownEndpoints[i].GetID() == id { + return knownEndpoints[i], nil + } + } + + return nil, errors.New("uh ohhh, wrong endpoint id here: " + id.String()) + } + + ruleSVC := mock.NewNotificationRuleStore() + ruleSVC.FindNotificationRuleByIDF = func(ctx context.Context, id influxdb.ID) (influxdb.NotificationRule, error) { + for i := range knownRules { + if knownRules[i].GetID() == id { + return knownRules[i], nil + } + } + + return nil, errors.New("uh ohhh, wrong rule id here: " + id.String()) + } + ruleSVC.FindNotificationRulesF = func(ctx context.Context, _ influxdb.NotificationRuleFilter, _ ...influxdb.FindOptions) ([]influxdb.NotificationRule, int, error) { + return knownRules, len(knownRules), nil + } + + resToClone := ResourceToClone{ + Kind: KindNotificationRule, + } + if tt.findName != "" { + resToClone.Name = tt.findName + } + if tt.findID != influxdb.ID(0) { + resToClone.ID = tt.findID + } + + svc := newTestService( + WithNotificationEndpointSVC(endpointSVC), + WithNotificationRuleSVC(ruleSVC), + ) + template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) + if tt.expected == nil { + require.Error(t, err) + } else { + require.NoError(t, err) + + actual := template.Summary() + require.Len(t, actual.NotificationRules, len(tt.expected)) + require.Len(t, actual.NotificationEndpoints, 1) + sort.Slice(actual.NotificationRules, func(i, j int) bool { + return actual.NotificationRules[i].Description < actual.NotificationRules[j].Description + }) + sort.Slice(actual.NotificationEndpoints, func(i, j int) bool { + return actual.NotificationEndpoints[i].NotificationEndpoint.GetDescription() < actual.NotificationEndpoints[j].NotificationEndpoint.GetDescription() + }) + + if tt.findName != "" && tt.findID != influxdb.ID(0) { + tt.expected[0].SetName(tt.findName) + } + + for i := range actual.NotificationRules { + assert.Zero(t, actual.NotificationRules[i].ID) + assert.Zero(t, actual.NotificationRules[i].EndpointID) + assert.NotEmpty(t, actual.NotificationRules[i].EndpointType) + assert.NotEmpty(t, actual.NotificationRules[i].EndpointMetaName) + + baseEqual := func(t *testing.T, base rule.Base) { + assert.Equal(t, base.Name, actual.NotificationRules[i].Name) + assert.Equal(t, base.Description, actual.NotificationRules[i].Description) + assert.Equal(t, base.Every.TimeDuration().String(), actual.NotificationRules[i].Every) + assert.Equal(t, base.Offset.TimeDuration().String(), actual.NotificationRules[i].Offset) + + for _, sRule := range base.StatusRules { + expected := SummaryStatusRule{CurrentLevel: sRule.CurrentLevel.String()} + if sRule.PreviousLevel != nil { + expected.PreviousLevel = sRule.PreviousLevel.String() + } + assert.Contains(t, actual.NotificationRules[i].StatusRules, expected) + } + for _, tRule := range base.TagRules { + expected := SummaryTagRule{ + Key: tRule.Key, + Value: tRule.Value, + Operator: tRule.Operator.String(), + } + assert.Contains(t, actual.NotificationRules[i].TagRules, expected) + } + } + + switch p := tt.expected[i].(type) { + case *rule.HTTP: + baseEqual(t, p.Base) + case *rule.PagerDuty: + baseEqual(t, p.Base) + assert.Equal(t, p.MessageTemplate, actual.NotificationRules[i].MessageTemplate) + case *rule.Slack: + baseEqual(t, p.Base) + assert.Equal(t, p.MessageTemplate, actual.NotificationRules[i].MessageTemplate) + } + + for j := range actual.NotificationEndpoints { + endpoint, err := endpointSVC.FindNotificationEndpointByIDF(context.Background(), tt.expected[i].GetEndpointID()) + require.NoError(t, err) + + assert.Equal(t, endpoint.GetName(), actual.NotificationEndpoints[j].NotificationEndpoint.GetName()) + assert.Equal(t, endpoint.GetDescription(), actual.NotificationEndpoints[j].NotificationEndpoint.GetDescription()) + assert.Equal(t, endpoint.GetStatus(), actual.NotificationEndpoints[j].NotificationEndpoint.GetStatus()) + } + } + + assert.True(t, encodeAndDecode(t, template) != nil) + } + } + t.Run(tt.name, fn) + } + }) + t.Run("tasks", func(t *testing.T) { t.Run("single task exports", func(t *testing.T) { tests := []struct { @@ -2921,6 +3887,9 @@ func TestService(t *testing.T) { } return &tt.task, nil } + taskSVC.FindTasksFn = func(ctx context.Context, filter influxdb.TaskFilter) ([]*influxdb.Task, int, error) { + return []*influxdb.Task{&tt.task}, 1, nil + } svc := newTestService(WithTaskSVC(taskSVC)) @@ -3003,17 +3972,146 @@ func TestService(t *testing.T) { }) }) + t.Run("tasks by name", func(t *testing.T) { + knownTasks := []*influxdb.Task{ + { + ID: 1, + Name: "task", + Description: "task 1", + Every: time.Minute.String(), + Offset: 10 * time.Second, + Type: influxdb.TaskSystemType, + Flux: `option task = { name: "larry" } from(bucket: "rucket") |> yield()`, + }, + { + ID: 2, + Name: "taskCopy", + Description: "task 2", + Cron: "2 * * * *", + Type: influxdb.TaskSystemType, + Flux: `option task = { name: "curly" } from(bucket: "rucket") |> yield()`, + }, + { + ID: 3, + Name: "taskCopy", + Description: "task 3", + Cron: "2 3 4 5 *", + Type: influxdb.TaskSystemType, + Flux: `option task = { name: "moe" } from(bucket: "rucket") |> yield()`, + }, + } + + tests := []struct { + name string + findName string + findID influxdb.ID + expected []*influxdb.Task + }{ + { + name: "find task with unique name", + findName: "task", + expected: []*influxdb.Task{knownTasks[0]}, + }, + { + name: "find multiple tasks with shared name", + findName: "taskCopy", + expected: []*influxdb.Task{knownTasks[1], knownTasks[2]}, + }, + { + name: "find no tasks", + findName: "faketask", + expected: nil, + }, + { + name: "find task by id", + findID: influxdb.ID(2), + expected: []*influxdb.Task{knownTasks[1]}, + }, + { + name: "find by id, set new name", + findID: influxdb.ID(2), + findName: "renamedTask", + expected: []*influxdb.Task{knownTasks[1]}, + }, + } + + for _, tt := range tests { + fn := func(t *testing.T) { + taskSVC := mock.NewTaskService() + taskSVC.FindTaskByIDFn = func(ctx context.Context, id influxdb.ID) (*influxdb.Task, error) { + for i := range knownTasks { + if knownTasks[i].ID == id { + return knownTasks[i], nil + } + } + + return nil, errors.New("wrong id provided: " + id.String()) + } + taskSVC.FindTasksFn = func(ctx context.Context, filter influxdb.TaskFilter) ([]*influxdb.Task, int, error) { + tasks := []*influxdb.Task{} + for i := range knownTasks { + if knownTasks[i].Name == *filter.Name { + tasks = append(tasks, knownTasks[i]) + } + } + return tasks, len(tasks), nil + } + + resToClone := ResourceToClone{ + Kind: KindTask, + } + if tt.findName != "" { + resToClone.Name = tt.findName + } + if tt.findID != influxdb.ID(0) { + resToClone.ID = tt.findID + } + + svc := newTestService(WithTaskSVC(taskSVC)) + template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) + if tt.expected == nil { + require.Error(t, err) + } else { + require.NoError(t, err) + + if tt.findName != "" && tt.findID != influxdb.ID(0) { + tt.expected[0].Name = tt.findName + } + + actual := template.Summary().Tasks + require.Len(t, actual, len(tt.expected)) + sort.Slice(actual, func(i, j int) bool { + return actual[i].Description < actual[j].Description + }) + + for i := range actual { + assert.Equal(t, tt.expected[i].Name, actual[i].Name) + assert.Equal(t, tt.expected[i].Cron, actual[i].Cron) + assert.Equal(t, tt.expected[i].Description, actual[i].Description) + assert.Equal(t, tt.expected[i].Every, actual[i].Every) + assert.Equal(t, durToStr(tt.expected[i].Offset), actual[i].Offset) + + assert.Equal(t, `from(bucket: "rucket") |> yield()`, actual[i].Query) + } + assert.True(t, encodeAndDecode(t, template) != nil) + } + } + t.Run(tt.name, fn) + } + }) + t.Run("telegraf configs", func(t *testing.T) { t.Run("allows for duplicate telegraf names to be exported", func(t *testing.T) { + tConfig := &influxdb.TelegrafConfig{ + OrgID: 9000, + Name: "same name", + Description: "desc", + Config: "some config string", + } teleStore := mock.NewTelegrafConfigStore() teleStore.FindTelegrafConfigByIDF = func(ctx context.Context, id influxdb.ID) (*influxdb.TelegrafConfig, error) { - return &influxdb.TelegrafConfig{ - ID: id, - OrgID: 9000, - Name: "same name", - Description: "desc", - Config: "some config string", - }, nil + tConfig.ID = id + return tConfig, nil } svc := newTestService(WithTelegrafSVC(teleStore)) @@ -3028,6 +4126,17 @@ func TestService(t *testing.T) { ID: 2, }, } + + teleStore.FindTelegrafConfigsF = func(ctx context.Context, filter influxdb.TelegrafConfigFilter, _ ...influxdb.FindOptions) ([]*influxdb.TelegrafConfig, int, error) { + tgrafs := []*influxdb.TelegrafConfig{} + for _, r := range resourcesToClone { + t := tConfig + t.ID = r.ID + tgrafs = append(tgrafs, t) + } + return tgrafs, len(tgrafs), nil + } + template, err := svc.Export(context.TODO(), ExportWithExistingResources(resourcesToClone...)) require.NoError(t, err) @@ -3050,6 +4159,120 @@ func TestService(t *testing.T) { }) }) + t.Run("telegraf configs by name", func(t *testing.T) { + knownConfigs := []*influxdb.TelegrafConfig{ + { + ID: 1, + OrgID: 9000, + Name: "my config", + Description: "desc1", + Config: "a config string", + }, + { + ID: 2, + OrgID: 9000, + Name: "telConfig", + Description: "desc2", + Config: "some config string", + }, + { + ID: 3, + OrgID: 9000, + Name: "telConfig", + Description: "desc3", + Config: "some other config string", + }, + } + + tests := []struct { + name string + findName string + findID influxdb.ID + expected []*influxdb.TelegrafConfig + }{ + { + name: "find telegraf with unique name", + findName: "my config", + expected: []*influxdb.TelegrafConfig{knownConfigs[0]}, + }, + { + name: "find multiple telegrafs with shared name", + findName: "telConfig", + expected: []*influxdb.TelegrafConfig{knownConfigs[1], knownConfigs[2]}, + }, + { + name: "find no telegrafs", + findName: "fakeConfig", + expected: nil, + }, + { + name: "find telegraf by id", + findID: influxdb.ID(2), + expected: []*influxdb.TelegrafConfig{knownConfigs[1]}, + }, + { + name: "find by id, set new name", + findID: influxdb.ID(2), + findName: "newConfig", + expected: []*influxdb.TelegrafConfig{knownConfigs[1]}, + }, + } + + for _, tt := range tests { + fn := func(t *testing.T) { + teleStore := mock.NewTelegrafConfigStore() + teleStore.FindTelegrafConfigByIDF = func(_ context.Context, id influxdb.ID) (*influxdb.TelegrafConfig, error) { + for i := range knownConfigs { + if knownConfigs[i].ID == id { + return knownConfigs[i], nil + } + } + return nil, errors.New("uh ohhh, wrong id here: " + id.String()) + } + teleStore.FindTelegrafConfigsF = func(_ context.Context, filter influxdb.TelegrafConfigFilter, _ ...influxdb.FindOptions) ([]*influxdb.TelegrafConfig, int, error) { + return knownConfigs, len(knownConfigs), nil + } + + resToClone := ResourceToClone{ + Kind: KindTelegraf, + } + if tt.findName != "" { + resToClone.Name = tt.findName + } + if tt.findID != influxdb.ID(0) { + resToClone.ID = tt.findID + } + + svc := newTestService(WithTelegrafSVC(teleStore)) + template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) + if tt.expected == nil { + require.Error(t, err) + } else { + require.NoError(t, err) + + if tt.findName != "" && tt.findID != influxdb.ID(0) { + tt.expected[0].Name = tt.findName + } + + actual := template.Summary().TelegrafConfigs + require.Len(t, actual, len(tt.expected)) + sort.Slice(actual, func(i, j int) bool { + return actual[i].TelegrafConfig.Description < actual[j].TelegrafConfig.Description + }) + + for i := range actual { + assert.Equal(t, tt.expected[i].Name, actual[i].TelegrafConfig.Name) + assert.Equal(t, tt.expected[i].Description, actual[i].TelegrafConfig.Description) + assert.Equal(t, tt.expected[i].Config, actual[i].TelegrafConfig.Config) + } + + assert.True(t, encodeAndDecode(t, template) != nil) + } + } + t.Run(tt.name, fn) + } + }) + t.Run("variable", func(t *testing.T) { tests := []struct { name string @@ -3120,6 +4343,12 @@ func TestService(t *testing.T) { } return &tt.expectedVar, nil } + varSVC.FindVariablesF = func(_ context.Context, filter influxdb.VariableFilter, _ ...influxdb.FindOptions) ([]*influxdb.Variable, error) { + if filter.ID != nil && *filter.ID != tt.expectedVar.ID { + return nil, errors.New("uh ohhh, wrong id here: " + fmt.Sprint(*filter.ID)) + } + return []*influxdb.Variable{&tt.expectedVar}, nil + } svc := newTestService(WithVariableSVC(varSVC), WithLabelSVC(mock.NewLabelService())) @@ -3150,6 +4379,120 @@ func TestService(t *testing.T) { } }) + t.Run("variable by name", func(t *testing.T) { + knownVariables := []*influxdb.Variable{ + { + ID: 1, + Name: "variable", + Description: "desc", + Selected: []string{"val1"}, + Arguments: &influxdb.VariableArguments{ + Type: "constant", + Values: influxdb.VariableConstantValues{"val"}, + }, + }, + { + ID: 2, + Name: "var 2", + Selected: []string{"val2"}, + Arguments: &influxdb.VariableArguments{ + Type: "constant", + Values: influxdb.VariableConstantValues{"val"}, + }, + }, + { + ID: 3, + Name: "var 3", + Selected: []string{"v"}, + Arguments: &influxdb.VariableArguments{ + Type: "map", + Values: influxdb.VariableMapValues{"k": "v"}, + }, + }, + } + + tests := []struct { + name string + findName string + findID influxdb.ID + expected []*influxdb.Variable + }{ + { + name: "find variable with unique name", + findName: "variable", + expected: []*influxdb.Variable{knownVariables[0]}, + }, + { + name: "find no variables", + findName: "fakeVariable", + expected: nil, + }, + { + name: "find variable by id", + findID: influxdb.ID(2), + expected: []*influxdb.Variable{knownVariables[1]}, + }, + { + name: "find by id, set new name", + findID: influxdb.ID(2), + findName: "useful var", + expected: []*influxdb.Variable{knownVariables[1]}, + }, + } + + for _, tt := range tests { + fn := func(t *testing.T) { + varSVC := mock.NewVariableService() + varSVC.FindVariableByIDF = func(_ context.Context, id influxdb.ID) (*influxdb.Variable, error) { + for i := range knownVariables { + if knownVariables[i].ID == id { + return knownVariables[i], nil + } + } + + return nil, errors.New("uh ohhh, wrong id here: " + id.String()) + } + varSVC.FindVariablesF = func(_ context.Context, filter influxdb.VariableFilter, _ ...influxdb.FindOptions) ([]*influxdb.Variable, error) { + return knownVariables, nil + } + + resToClone := ResourceToClone{ + Kind: KindVariable, + } + if tt.findName != "" { + resToClone.Name = tt.findName + } + if tt.findID != influxdb.ID(0) { + resToClone.ID = tt.findID + } + + svc := newTestService(WithVariableSVC(varSVC), WithLabelSVC(mock.NewLabelService())) + template, err := svc.Export(context.TODO(), ExportWithExistingResources(resToClone)) + if tt.expected == nil { + require.Error(t, err) + } else { + require.NoError(t, err) + + if tt.findName != "" && tt.findID != influxdb.ID(0) { + tt.expected[0].Name = tt.findName + } + + actual := template.Summary().Variables + require.Len(t, actual, len(tt.expected)) + + for i := range actual { + assert.Equal(t, tt.expected[i].Name, actual[i].Name) + assert.Equal(t, tt.expected[i].Description, actual[i].Description) + assert.Equal(t, tt.expected[i].Arguments, actual[i].Arguments) + } + + assert.True(t, encodeAndDecode(t, template) != nil) + } + } + t.Run(tt.name, fn) + } + }) + t.Run("includes resource associations", func(t *testing.T) { t.Run("single resource with single association", func(t *testing.T) { expected := &influxdb.Bucket{ @@ -3166,6 +4509,12 @@ func TestService(t *testing.T) { } return expected, nil } + bktSVC.FindBucketsFn = func(_ context.Context, f influxdb.BucketFilter, opts ...influxdb.FindOptions) ([]*influxdb.Bucket, int, error) { + if f.ID != nil && *f.ID != expected.ID { + return nil, 0, errors.New("not suppose to get here") + } + return []*influxdb.Bucket{expected}, 1, nil + } labelSVC := mock.NewLabelService() labelSVC.FindResourceLabelsFn = func(_ context.Context, f influxdb.LabelMappingFilter) ([]*influxdb.Label, error) { @@ -3231,6 +4580,15 @@ func TestService(t *testing.T) { ID: 20, }, } + + bktSVC.FindBucketsFn = func(_ context.Context, f influxdb.BucketFilter, opts ...influxdb.FindOptions) ([]*influxdb.Bucket, int, error) { + bkts := []*influxdb.Bucket{} + for _, r := range resourcesToClone { + bkts = append(bkts, &influxdb.Bucket{ID: r.ID, Name: strconv.Itoa(int(r.ID)), Type: influxdb.BucketTypeUser}) + } + return bkts, len(bkts), nil + } + template, err := svc.Export(context.TODO(), ExportWithExistingResources(resourcesToClone...)) require.NoError(t, err) @@ -3287,19 +4645,20 @@ func TestService(t *testing.T) { t.Run("with org id", func(t *testing.T) { orgID := influxdb.ID(9000) + bkt := &influxdb.Bucket{ID: 1, Name: "bucket"} bktSVC := mock.NewBucketService() bktSVC.FindBucketsFn = func(_ context.Context, f influxdb.BucketFilter, opts ...influxdb.FindOptions) ([]*influxdb.Bucket, int, error) { - if f.OrganizationID == nil || *f.OrganizationID != orgID { + if (f.ID != nil && *f.ID != bkt.ID) && (f.OrganizationID == nil || *f.OrganizationID != orgID) { return nil, 0, errors.New("not suppose to get here") } - return []*influxdb.Bucket{{ID: 1, Name: "bucket"}}, 1, nil + return []*influxdb.Bucket{bkt}, 1, nil } bktSVC.FindBucketByIDFn = func(_ context.Context, id influxdb.ID) (*influxdb.Bucket, error) { if id != 1 { return nil, errors.New("wrong id") } - return &influxdb.Bucket{ID: 1, Name: "bucket"}, nil + return bkt, nil } checkSVC := mock.NewCheckService() @@ -3311,7 +4670,7 @@ func TestService(t *testing.T) { Level: notification.Critical, } checkSVC.FindChecksFn = func(ctx context.Context, f influxdb.CheckFilter, _ ...influxdb.FindOptions) ([]influxdb.Check, int, error) { - if f.OrgID == nil || *f.OrgID != orgID { + if (f.ID != nil && *f.ID != expectedCheck.GetID()) && (f.OrgID == nil || *f.OrgID != orgID) { return nil, 0, errors.New("not suppose to get here") } return []influxdb.Check{expectedCheck}, 1, nil @@ -3320,58 +4679,44 @@ func TestService(t *testing.T) { return expectedCheck, nil } + dash := &influxdb.Dashboard{ + ID: 2, + Name: "dashboard", + Cells: []*influxdb.Cell{}, + } dashSVC := mock.NewDashboardService() dashSVC.FindDashboardsF = func(_ context.Context, f influxdb.DashboardFilter, _ influxdb.FindOptions) ([]*influxdb.Dashboard, int, error) { - if f.OrganizationID == nil || *f.OrganizationID != orgID { + if (f.IDs != nil && len(f.IDs) > 0 && + f.IDs[0] != nil && *f.IDs[0] != dash.ID) && + (f.OrganizationID == nil || *f.OrganizationID != orgID) { return nil, 0, errors.New("not suppose to get here") } - return []*influxdb.Dashboard{{ - ID: 2, - Name: "dashboard", - Cells: []*influxdb.Cell{}, - }}, 1, nil + return []*influxdb.Dashboard{dash}, 1, nil } dashSVC.FindDashboardByIDF = func(_ context.Context, id influxdb.ID) (*influxdb.Dashboard, error) { if id != 2 { return nil, errors.New("wrong id") } - return &influxdb.Dashboard{ - ID: 2, - Name: "dashboard", - Cells: []*influxdb.Cell{}, - }, nil + return dash, nil } + notificationEndpoint := &endpoint.HTTP{ + Base: endpoint.Base{ + ID: newTestIDPtr(2), + Name: "http", + }, + URL: "http://example.com/id", + Username: influxdb.SecretField{Key: "2-username"}, + Password: influxdb.SecretField{Key: "2-password"}, + AuthMethod: "basic", + Method: "POST", + } endpointSVC := mock.NewNotificationEndpointService() endpointSVC.FindNotificationEndpointsF = func(ctx context.Context, f influxdb.NotificationEndpointFilter, _ ...influxdb.FindOptions) ([]influxdb.NotificationEndpoint, int, error) { - id := influxdb.ID(2) - endpoints := []influxdb.NotificationEndpoint{ - &endpoint.HTTP{ - Base: endpoint.Base{ - ID: &id, - Name: "http", - }, - URL: "http://example.com", - Username: influxdb.SecretField{Key: id.String() + "-username"}, - Password: influxdb.SecretField{Key: id.String() + "-password"}, - AuthMethod: "basic", - Method: "POST", - }, - } - return endpoints, len(endpoints), nil + return []influxdb.NotificationEndpoint{notificationEndpoint}, 1, nil } endpointSVC.FindNotificationEndpointByIDF = func(ctx context.Context, id influxdb.ID) (influxdb.NotificationEndpoint, error) { - return &endpoint.HTTP{ - Base: endpoint.Base{ - ID: &id, - Name: "http", - }, - URL: "http://example.com", - Username: influxdb.SecretField{Key: id.String() + "-username"}, - Password: influxdb.SecretField{Key: id.String() + "-password"}, - AuthMethod: "basic", - Method: "POST", - }, nil + return notificationEndpoint, nil } expectedRule := &rule.HTTP{