feat: port export all command from influxdb (#134)

This commit is contained in:
Daniel Moran 2021-06-21 12:34:15 -04:00 committed by GitHub
parent bd0b90df41
commit a4fdefc392
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 387 additions and 144 deletions

View File

@ -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 = &params.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
}

41
clients/export/out.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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: &params.orgId,
},
&cli.StringFlag{
Name: "org",
Usage: "The name of the organization",
Aliases: []string{"o"},
EnvVars: []string{"INFLUX_ORG"},
Destination: &params.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: &params.out,
},
&cli.StringSliceFlag{
Name: "filter",
Usage: "Filter exported resources by labelName or resourceKind (format: labelName=example)",
Destination: &params.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
}