diff --git a/clients/export/export.go b/clients/export/export.go index 258518c..210fd2e 100644 --- a/clients/export/export.go +++ b/clients/export/export.go @@ -2,30 +2,20 @@ package export import ( "context" - "encoding/json" "fmt" - "io" "github.com/influxdata/influx-cli/v2/api" "github.com/influxdata/influx-cli/v2/clients" - "gopkg.in/yaml.v3" -) - -type OutEncoding int - -const ( - YamlEncoding OutEncoding = iota - JsonEncoding ) type Client struct { clients.CLI api.TemplatesApi + api.OrganizationsApi } type Params struct { - Out io.Writer - OutEncoding + OutParams StackId string BucketIds []string @@ -128,27 +118,86 @@ func (c Client) Export(ctx context.Context, params *Params) error { if err != nil { return fmt.Errorf("failed to export template: %w", err) } - if err := writeTemplate(params.Out, params.OutEncoding, tmpl); err != nil { + if err := params.OutParams.writeTemplate(tmpl); err != nil { return fmt.Errorf("failed to write exported template: %w", err) } return nil } -func writeTemplate(out io.Writer, encoding OutEncoding, template []api.TemplateEntry) error { - switch encoding { - case JsonEncoding: - enc := json.NewEncoder(out) - enc.SetIndent("", "\t") - return enc.Encode(template) - case YamlEncoding: - enc := yaml.NewEncoder(out) - for _, entry := range template { - if err := enc.Encode(entry); err != nil { - return err - } +type AllParams struct { + OutParams + + OrgId string + OrgName string + + LabelFilters []string + KindFilters []ResourceType +} + +func (c Client) ExportAll(ctx context.Context, params *AllParams) error { + if params.OrgId == "" && params.OrgName == "" && c.ActiveConfig.Org == "" { + return clients.ErrMustSpecifyOrg + } + + orgId := params.OrgId + if orgId == "" { + orgName := params.OrgName + if orgName == "" { + orgName = c.ActiveConfig.Org } - default: - return fmt.Errorf("encoding %q is not recognized", encoding) + res, err := c.GetOrgs(ctx).Org(orgName).Execute() + if err != nil { + return fmt.Errorf("failed to look up ID for org %q: %w", orgName, err) + } + if len(res.GetOrgs()) == 0 { + return fmt.Errorf("no org found with name %q", orgName) + } + orgId = res.GetOrgs()[0].GetId() + } + + orgExport := api.TemplateExportOrgIDs{OrgID: &orgId} + if len(params.LabelFilters) > 0 || len(params.KindFilters) > 0 { + orgExport.ResourceFilters = &api.TemplateExportResourceFilters{} + if len(params.LabelFilters) > 0 { + orgExport.ResourceFilters.ByLabel = ¶ms.LabelFilters + } + if len(params.KindFilters) > 0 { + kinds := make([]api.TemplateKind, len(params.KindFilters)) + for i, kf := range params.KindFilters { + switch kf { + case TypeBucket: + kinds[i] = api.TEMPLATEKIND_BUCKET + case TypeCheck: + kinds[i] = api.TEMPLATEKIND_CHECK + case TypeDashboard: + kinds[i] = api.TEMPLATEKIND_DASHBOARD + case TypeLabel: + kinds[i] = api.TEMPLATEKIND_LABEL + case TypeNotificationEndpoint: + kinds[i] = api.TEMPLATEKIND_NOTIFICATION_ENDPOINT + case TypeNotificationRule: + kinds[i] = api.TEMPLATEKIND_NOTIFICATION_RULE + case TypeTask: + kinds[i] = api.TEMPLATEKIND_TASK + case TypeTelegraf: + kinds[i] = api.TEMPLATEKIND_TELEGRAF + case TypeVariable: + kinds[i] = api.TEMPLATEKIND_VARIABLE + default: + return fmt.Errorf("unsupported resourceKind filter %q", kf) + } + } + orgExport.ResourceFilters.ByResourceKind = &kinds + } + } + + exportReq := api.TemplateExport{OrgIDs: &[]api.TemplateExportOrgIDs{orgExport}} + tmpl, err := c.ExportTemplate(ctx).TemplateExport(exportReq).Execute() + if err != nil { + return fmt.Errorf("failed to export template: %w", err) + } + if err := params.OutParams.writeTemplate(tmpl); err != nil { + return fmt.Errorf("failed to write exported template: %w", err) } return nil } diff --git a/clients/export/out.go b/clients/export/out.go new file mode 100644 index 0000000..9680484 --- /dev/null +++ b/clients/export/out.go @@ -0,0 +1,41 @@ +package export + +import ( + "encoding/json" + "fmt" + "io" + + "github.com/influxdata/influx-cli/v2/api" + "gopkg.in/yaml.v3" +) + +type OutEncoding int + +const ( + YamlEncoding OutEncoding = iota + JsonEncoding +) + +type OutParams struct { + Out io.Writer + Encoding OutEncoding +} + +func (o OutParams) writeTemplate(template []api.TemplateEntry) error { + switch o.Encoding { + case JsonEncoding: + enc := json.NewEncoder(o.Out) + enc.SetIndent("", "\t") + return enc.Encode(template) + case YamlEncoding: + enc := yaml.NewEncoder(o.Out) + for _, entry := range template { + if err := enc.Encode(entry); err != nil { + return err + } + } + default: + return fmt.Errorf("encoding %q is not recognized", o.Encoding) + } + return nil +} diff --git a/clients/export/resource.go b/clients/export/resource.go new file mode 100644 index 0000000..c0c9559 --- /dev/null +++ b/clients/export/resource.go @@ -0,0 +1,99 @@ +package export + +import ( + "fmt" + "strings" +) + +type ResourceType int + +const ( + TypeUnset ResourceType = iota + TypeBucket + TypeCheck + TypeCheckDeadman + TypeCheckThreshold + TypeDashboard + TypeLabel + TypeNotificationEndpoint + TypeNotificationEndpointHTTP + TypeNotificationEndpointPagerDuty + TypeNotificationEndpointSlack + TypeNotificationRule + TypeTask + TypeTelegraf + TypeVariable +) + +func (r ResourceType) String() string { + switch r { + case TypeBucket: + return "bucket" + case TypeCheck: + return "check" + case TypeCheckDeadman: + return "checkDeadman" + case TypeCheckThreshold: + return "checkThreshold" + case TypeDashboard: + return "dashboard" + case TypeLabel: + return "label" + case TypeNotificationEndpoint: + return "notificationEndpoint" + case TypeNotificationEndpointHTTP: + return "notificationEndpointHTTP" + case TypeNotificationEndpointPagerDuty: + return "notificationEndpointPagerDuty" + case TypeNotificationEndpointSlack: + return "notificationEndpointSlack" + case TypeNotificationRule: + return "notificationRule" + case TypeTask: + return "task" + case TypeTelegraf: + return "telegraf" + case TypeVariable: + return "variable" + case TypeUnset: + fallthrough + default: + return "unset" + } +} + +func (r *ResourceType) Set(v string) error { + switch strings.ToLower(v) { + case "bucket": + *r = TypeBucket + case "check": + *r = TypeCheck + case "checkdeadman": + *r = TypeCheckDeadman + case "checkthreshold": + *r = TypeCheckThreshold + case "dashboard": + *r = TypeDashboard + case "label": + *r = TypeLabel + case "notificationendpoint": + *r = TypeNotificationEndpoint + case "notificationendpointhttp": + *r = TypeNotificationEndpointHTTP + case "notificationendpointpagerduty": + *r = TypeNotificationEndpointPagerDuty + case "notificationendpointslack": + *r = TypeNotificationEndpointSlack + case "notificationrule": + *r = TypeNotificationRule + case "task": + *r = TypeTask + case "telegraf": + *r = TypeTelegraf + case "variable": + *r = TypeVariable + default: + return fmt.Errorf("unknown resource type: %s", v) + } + return nil +} diff --git a/cmd/influx/export.go b/cmd/influx/export.go index 002843f..85ba91c 100644 --- a/cmd/influx/export.go +++ b/cmd/influx/export.go @@ -17,7 +17,7 @@ func newExportCmd() *cli.Command { var params struct { out string stackId string - resourceType ResourceType + resourceType export.ResourceType bucketIds string bucketNames string checkIds string @@ -64,6 +64,9 @@ resource flag and then provide the IDs. For information about exporting InfluxDB templates, see https://docs.influxdata.com/influxdb/latest/reference/cli/influx/export/`, + Subcommands: []*cli.Command{ + newExportAllCmd(), + }, Flags: append( commonFlagsNoPrint(), &cli.StringFlag{ @@ -198,24 +201,16 @@ https://docs.influxdata.com/influxdb/latest/reference/cli/influx/export/`, VariableNames: splitNonEmpty(params.variableNames), } - if params.out == "" { - parsedParams.Out = os.Stdout - } else { - f, err := os.OpenFile(params.out, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) - if err != nil { - return fmt.Errorf("failed to open output path %q: %w", params.out, err) - } - defer f.Close() - parsedParams.Out = f + outParams, closer, err := parseOutParams(params.out) + if closer != nil { + defer closer() } - switch filepath.Ext(params.out) { - case ".json": - parsedParams.OutEncoding = export.JsonEncoding - default: // Also covers path == "" for stdout. - parsedParams.OutEncoding = export.YamlEncoding + if err != nil { + return err } + parsedParams.OutParams = outParams - if params.resourceType != TypeUnset { + if params.resourceType != export.TypeUnset { ids := ctx.Args().Slice() // Read any IDs from stdin. @@ -229,23 +224,23 @@ https://docs.influxdata.com/influxdb/latest/reference/cli/influx/export/`, } switch params.resourceType { - case TypeBucket: + case export.TypeBucket: parsedParams.BucketIds = append(parsedParams.BucketIds, ids...) - case TypeCheck: + case export.TypeCheck: parsedParams.CheckIds = append(parsedParams.CheckIds, ids...) - case TypeDashboard: + case export.TypeDashboard: parsedParams.DashboardIds = append(parsedParams.DashboardIds, ids...) - case TypeLabel: + case export.TypeLabel: parsedParams.LabelIds = append(parsedParams.LabelIds, ids...) - case TypeNotificationEndpoint: + case export.TypeNotificationEndpoint: parsedParams.EndpointIds = append(parsedParams.EndpointIds, ids...) - case TypeNotificationRule: + case export.TypeNotificationRule: parsedParams.RuleIds = append(parsedParams.RuleIds, ids...) - case TypeTask: + case export.TypeTask: parsedParams.TaskIds = append(parsedParams.TaskIds, ids...) - case TypeTelegraf: + case export.TypeTelegraf: parsedParams.TelegrafIds = append(parsedParams.TelegrafIds, ids...) - case TypeVariable: + case export.TypeVariable: parsedParams.VariableIds = append(parsedParams.VariableIds, ids...) // NOTE: The API doesn't support filtering by these resource subtypes, @@ -258,10 +253,10 @@ https://docs.influxdata.com/influxdb/latest/reference/cli/influx/export/`, // Instead of allowing the type-filter to be silently converted by the server, // we catch the previously-allowed subtypes here and return a (hopefully) useful // error suggesting the correct flag to use. - case TypeCheckDeadman, TypeCheckThreshold: - return fmt.Errorf("filtering on resource-type %q is not supported by the API. Use resource-type %q instead", params.resourceType, TypeCheck) - case TypeNotificationEndpointHTTP, TypeNotificationEndpointPagerDuty, TypeNotificationEndpointSlack: - return fmt.Errorf("filtering on resource-type %q is not supported by the API. Use resource-type %q instead", params.resourceType, TypeNotificationEndpoint) + case export.TypeCheckDeadman, export.TypeCheckThreshold: + return fmt.Errorf("filtering on resource-type %q is not supported by the API. Use resource-type %q instead", params.resourceType, export.TypeCheck) + case export.TypeNotificationEndpointHTTP, export.TypeNotificationEndpointPagerDuty, export.TypeNotificationEndpointSlack: + return fmt.Errorf("filtering on resource-type %q is not supported by the API. Use resource-type %q instead", params.resourceType, export.TypeNotificationEndpoint) default: } @@ -278,6 +273,138 @@ https://docs.influxdata.com/influxdb/latest/reference/cli/influx/export/`, } } +func newExportAllCmd() *cli.Command { + var params struct { + out string + orgId string + orgName string + filters cli.StringSlice + } + return &cli.Command{ + Name: "all", + Usage: "Export all existing resources for an organization as a template", + Description: `The export all command will export all resources for an organization. The +command also provides a mechanism to filter by label name or resource kind. + +Examples: + # Export all resources for an organization + influx export all --org $ORG_NAME + + # Export all bucket resources + influx export all --org $ORG_NAME --filter=kind=Bucket + + # Export all resources associated with label Foo + influx export all --org $ORG_NAME --filter=labelName=Foo + + # Export all bucket resources and filter by label Foo + influx export all --org $ORG_NAME \ + --filter kind=Bucket \ + --filter labelName=Foo + + # Export all bucket or dashboard resources and filter by label Foo. + # note: like filters are unioned and filter types are intersections. + # This example will export a resource if it is a dashboard or + # bucket and has an associated label of Foo. + influx export all --org $ORG_NAME \ + --filter kind=Bucket \ + --filter kind=Dashboard \ + --filter labelName=Foo + +For information about exporting InfluxDB templates, see +https://docs.influxdata.com/influxdb/latest/reference/cli/influx/export/ +and +https://docs.influxdata.com/influxdb/latest/reference/cli/influx/export/all/ +`, + Flags: append( + commonFlagsNoPrint(), + &cli.StringFlag{ + Name: "org-id", + Usage: "The ID of the organization", + EnvVars: []string{"INFLUX_ORG_ID"}, + Destination: ¶ms.orgId, + }, + &cli.StringFlag{ + Name: "org", + Usage: "The name of the organization", + Aliases: []string{"o"}, + EnvVars: []string{"INFLUX_ORG"}, + Destination: ¶ms.orgName, + }, + &cli.StringFlag{ + Name: "file", + Usage: "Output file for created template; defaults to std out if no file provided; the extension of provided file (.yml/.json) will dictate encoding", + Aliases: []string{"f"}, + Destination: ¶ms.out, + }, + &cli.StringSliceFlag{ + Name: "filter", + Usage: "Filter exported resources by labelName or resourceKind (format: labelName=example)", + Destination: ¶ms.filters, + }, + ), + Before: middleware.WithBeforeFns(withCli(), withApi(true)), + Action: func(ctx *cli.Context) error { + parsedParams := export.AllParams{ + OrgId: params.orgId, + OrgName: params.orgName, + } + + for _, filter := range params.filters.Value() { + components := strings.Split(filter, "=") + if len(components) != 2 { + return fmt.Errorf("invalid filter %q, must have format `type=example`", filter) + } + switch key, val := components[0], components[1]; key { + case "labelName": + parsedParams.LabelFilters = append(parsedParams.LabelFilters, val) + case "kind", "resourceKind": + var resType export.ResourceType + if err := resType.Set(val); err != nil { + return err + } + switch resType { + // NOTE: The API doesn't support filtering by these resource subtypes, + // and instead converts them to the parent type. For example, + // `--resource-type notificationEndpointHTTP` gets translated to a filter + // on all notification endpoints on the server-side. I think this was + // intentional since the 2.0.x CLI didn't expose flags to filter on subtypes, + // but a bug/oversight in its parsing still allowed the subtypes through + // when passing IDs over stdin. + // Instead of allowing the type-filter to be silently converted by the server, + // we catch the previously-allowed subtypes here and return a (hopefully) useful + // error suggesting the correct flag to use. + case export.TypeCheckDeadman, export.TypeCheckThreshold: + return fmt.Errorf("filtering on resourceKind=%s is not supported by the API. Use resourceKind=%s instead", resType, export.TypeCheck) + case export.TypeNotificationEndpointSlack, export.TypeNotificationEndpointPagerDuty, export.TypeNotificationEndpointHTTP: + return fmt.Errorf("filtering on resourceKind=%s is not supported by the API. Use resourceKind=%s instead", resType, export.TypeNotificationEndpoint) + default: + } + parsedParams.KindFilters = append(parsedParams.KindFilters, resType) + default: + return fmt.Errorf("invalid filter provided %q; filter must be 1 in [labelName, resourceKind]", filter) + } + } + + outParams, closer, err := parseOutParams(params.out) + if closer != nil { + defer closer() + } + if err != nil { + return err + } + parsedParams.OutParams = outParams + + apiClient := getAPI(ctx) + client := export.Client{ + CLI: getCLI(ctx), + TemplatesApi: apiClient.TemplatesApi, + OrganizationsApi: apiClient.OrganizationsApi, + } + return client.ExportAll(ctx.Context, &parsedParams) + }, + } +} + func splitNonEmpty(s string) []string { if s == "" { return nil @@ -285,95 +412,22 @@ func splitNonEmpty(s string) []string { return strings.Split(s, ",") } -type ResourceType int - -const ( - TypeUnset ResourceType = iota - TypeBucket - TypeCheck - TypeCheckDeadman - TypeCheckThreshold - TypeDashboard - TypeLabel - TypeNotificationEndpoint - TypeNotificationEndpointHTTP - TypeNotificationEndpointPagerDuty - TypeNotificationEndpointSlack - TypeNotificationRule - TypeTask - TypeTelegraf - TypeVariable -) - -func (r ResourceType) String() string { - switch r { - case TypeBucket: - return "bucket" - case TypeCheck: - return "check" - case TypeCheckDeadman: - return "checkDeadman" - case TypeCheckThreshold: - return "checkThreshold" - case TypeDashboard: - return "dashboard" - case TypeLabel: - return "label" - case TypeNotificationEndpoint: - return "notificationEndpoint" - case TypeNotificationEndpointHTTP: - return "notificationEndpointHTTP" - case TypeNotificationEndpointPagerDuty: - return "notificationEndpointPagerDuty" - case TypeNotificationEndpointSlack: - return "notificationEndpointSlack" - case TypeNotificationRule: - return "notificationRule" - case TypeTask: - return "task" - case TypeTelegraf: - return "telegraf" - case TypeVariable: - return "variable" - case TypeUnset: - fallthrough - default: - return "unset" +func parseOutParams(outPath string) (export.OutParams, func(), error) { + if outPath == "" { + return export.OutParams{Out: os.Stdout, Encoding: export.YamlEncoding}, nil, nil } -} -func (r *ResourceType) Set(v string) error { - switch strings.ToLower(v) { - case "bucket": - *r = TypeBucket - case "check": - *r = TypeCheck - case "checkdeadman": - *r = TypeCheckDeadman - case "checkthreshold": - *r = TypeCheckThreshold - case "dashboard": - *r = TypeDashboard - case "label": - *r = TypeLabel - case "notificationendpoint": - *r = TypeNotificationEndpoint - case "notificationendpointhttp": - *r = TypeNotificationEndpointHTTP - case "notificationendpointpagerduty": - *r = TypeNotificationEndpointPagerDuty - case "notificationendpointslack": - *r = TypeNotificationEndpointSlack - case "notificationrule": - *r = TypeNotificationRule - case "task": - *r = TypeTask - case "telegraf": - *r = TypeTelegraf - case "variable": - *r = TypeVariable - default: - return fmt.Errorf("unknown resource type: %s", v) + f, err := os.OpenFile(outPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return export.OutParams{}, nil, fmt.Errorf("failed to open output path %q: %w", outPath, err) } - return nil + params := export.OutParams{Out: f} + switch filepath.Ext(outPath) { + case ".json": + params.Encoding = export.JsonEncoding + default: + params.Encoding = export.YamlEncoding + } + + return params, func() { _ = f.Close() }, nil }