diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2756d58d..8f9185ed 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,7 @@ // For format details, see https://aka.ms/devcontainer.json. { "name": "Go", - "image": "mcr.microsoft.com/devcontainers/go:1.24-bookworm", + "image": "mcr.microsoft.com/devcontainers/go:1.25-bookworm", "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 3cda2ecb..94b5ce75 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -17,9 +17,9 @@ jobs: steps: - uses: actions/setup-go@v5 with: - go-version: 1.24 + go-version: 1.25 - uses: actions/checkout@v4 - name: golangci-lint uses: golangci/golangci-lint-action@v8 with: - version: v2.1 + version: v2.12 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c7b18525..1698cba5 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -18,11 +18,11 @@ jobs: - run: git fetch --force --tags - uses: actions/setup-go@v5 with: - go-version: 1.24 + go-version: 1.25 - uses: goreleaser/goreleaser-action@v5 with: distribution: goreleaser - version: v1.24.0 + version: v1.25.0 args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 43a2b9ef..d42e72e0 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -14,7 +14,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.24 + go-version: 1.25 - name: Build run: go build -v ./... - name: Test @@ -26,7 +26,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.24 + go-version: 1.25 - name: Generate docs run: go run main.go docs - name: Test for changes diff --git a/.gitignore b/.gitignore index a6c187a9..7b7fb609 100644 --- a/.gitignore +++ b/.gitignore @@ -17,7 +17,7 @@ mass # Dependency directories (remove the comment below to include it) # vendor/ - +.claude .vscode bin mass diff --git a/.golangci.yaml b/.golangci.yaml index bbadb5ad..7027461c 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -68,7 +68,7 @@ linters: # - gochecknoinits # checks that no init functions are present in Go code - gochecksumtype # checks exhaustiveness on Go "sum types" - gocognit # computes and checks the cognitive complexity of functions - - goconst # finds repeated strings that could be replaced by a constant + # - goconst # finds repeated strings that could be replaced by a constant; too noisy for data tables and CLI literals - gocritic # provides diagnostics that check for bugs, performance and style issues - gocyclo # computes and checks the cyclomatic complexity of functions # - godot # checks if comments end in a period @@ -420,16 +420,12 @@ linters: - linters: [revive] text: "var-naming: avoid meaningless package names" path: "internal/api/" - # cmd/ uses "json"/"text" as CLI output-format literals; extracting them as constants adds no value - - linters: [goconst] - path: "^cmd/" - path: "_test\\.go" linters: - revive - bodyclose - dupl - funlen - - goconst - gosec - noctx - wrapcheck diff --git a/cmd/bundle.go b/cmd/bundle.go index da4f3fcc..91a7a6fa 100644 --- a/cmd/bundle.go +++ b/cmd/bundle.go @@ -62,9 +62,10 @@ func NewCmdBundle() *cobra.Command { //nolint:funlen // cobra command builders a var bundleListInput bundleList bundleListCmd := &cobra.Command{ - Use: "list", - Short: "List bundles in your organization", - Long: helpdocs.MustRender("bundle/list"), + Use: "list", + Aliases: []string{"ls"}, + Short: "List bundles in your organization", + Long: helpdocs.MustRender("bundle/list"), RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true return runBundleList(&bundleListInput) @@ -158,11 +159,13 @@ func NewCmdBundle() *cobra.Command { //nolint:funlen // cobra command builders a } bundleTemplateListCmd := &cobra.Command{ - Use: "list", - Short: "List bundle templates", - Long: helpdocs.MustRender("bundle/template-list"), - RunE: runBundleTemplateList, + Use: "list", + Aliases: []string{"ls"}, + Short: "List bundle templates", + Long: helpdocs.MustRender("bundle/template-list"), + RunE: runBundleTemplateList, } + bundleTemplateListCmd.Flags().StringP("output", "o", "text", "Output format (text, json)") bundleCreateCmd := &cobra.Command{ Use: "create ", @@ -207,19 +210,34 @@ func runBundleCreate(cmd *cobra.Command, args []string) error { } func runBundleTemplateList(cmd *cobra.Command, args []string) error { - templateList, err := templates.List() + output, err := cmd.Flags().GetString("output") if err != nil { return err } - if len(templateList) == 0 { - fmt.Println("No templates found.") - return nil + templateList, err := templates.List() + if err != nil { + return err } - fmt.Println("Available templates:") - for _, tmpl := range templateList { - fmt.Printf(" %s\n", tmpl) + switch output { + case "json": + jsonBytes, marshalErr := json.MarshalIndent(templateList, "", " ") + if marshalErr != nil { + return fmt.Errorf("failed to marshal templates to JSON: %w", marshalErr) + } + fmt.Println(string(jsonBytes)) + case "text": + if len(templateList) == 0 { + fmt.Println("No templates found.") + return nil + } + fmt.Println("Available templates:") + for _, tmpl := range templateList { + fmt.Printf(" %s\n", tmpl) + } + default: + return fmt.Errorf("unsupported output format: %s", output) } return nil } @@ -522,24 +540,26 @@ func runBundleList(input *bundleList) error { } } - repos, err := mdClient.OciRepos.List(ctx, listInput) - if err != nil { - return fmt.Errorf("failed to list bundles: %w", err) - } + seq := mdClient.OciRepos.Iter(ctx, listInput) switch input.output { case "json": - jsonBytes, err := json.MarshalIndent(repos, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal bundles to JSON: %w", err) + repos, collectErr := types.Collect(seq) + if collectErr != nil { + return fmt.Errorf("failed to list bundles: %w", collectErr) + } + jsonBytes, marshalErr := json.MarshalIndent(repos, "", " ") + if marshalErr != nil { + return fmt.Errorf("failed to marshal bundles to JSON: %w", marshalErr) } fmt.Println(string(jsonBytes)) case "table": - tbl := cli.NewTable("Name", "Latest", "Created At") - for _, repo := range repos { - tbl.AddRow(repo.Name, repo.LatestTag, repo.CreatedAt.Format("2006-01-02 15:04:05")) - } - tbl.Print() + return cli.Paginate(seq, cli.PagerConfig[ocirepos.OciRepo]{ + Columns: []string{"Name", "Latest", "Created At"}, + Row: func(repo ocirepos.OciRepo) []string { + return []string{repo.Name, repo.LatestTag, repo.CreatedAt.Format("2006-01-02 15:04:05")} + }, + }) default: return fmt.Errorf("unsupported output format: %s", input.output) } diff --git a/cmd/deployment.go b/cmd/deployment.go index 0a87c534..254f20ec 100644 --- a/cmd/deployment.go +++ b/cmd/deployment.go @@ -57,6 +57,11 @@ func NewCmdDeployment() *cobra.Command { RunE: runDeploymentList, } deploymentListCmd.Flags().IntP("limit", "n", 10, "Maximum number of deployments to return (max 100)") + deploymentListCmd.Flags().StringP("output", "o", "table", "Output format (table, json)") + deploymentListCmd.Flags().String("status", "", "Filter by status (pending, approved, running, completed, failed, aborted, rejected, proposed)") + deploymentListCmd.Flags().String("action", "", "Filter by action (provision, decommission, plan)") + deploymentListCmd.Flags().String("repo", "", "Filter by OCI repo name") + deploymentListCmd.Flags().String("bundle", "", "Filter by bundle version (name@version) or release channel (name@latest)") deploymentLogsCmd := &cobra.Command{ Use: "logs ", @@ -129,6 +134,11 @@ func runDeploymentList(cmd *cobra.Command, args []string) error { if err != nil { return err } + status, _ := cmd.Flags().GetString("status") + action, _ := cmd.Flags().GetString("action") + repo, _ := cmd.Flags().GetString("repo") + bundle, _ := cmd.Flags().GetString("bundle") + output, _ := cmd.Flags().GetString("output") cmd.SilenceUsage = true mdClient, err := massdriver.NewClient() @@ -136,29 +146,51 @@ func runDeploymentList(cmd *cobra.Command, args []string) error { return fmt.Errorf("error initializing massdriver client: %w", err) } - // SDK's List auto-paginates without a total cap; we want a total cap, so + // SDK's Iter auto-paginates without a total cap; we want a total cap, so // drive Iter and stop after `limit` items. listInput := deployments.ListInput{ - InstanceID: instanceID, - SortBy: deployments.SortByCreatedAt, - SortOrder: deployments.SortDesc, + InstanceID: instanceID, + OciRepoName: repo, + BundleID: bundle, + SortBy: deployments.SortByCreatedAt, + SortOrder: deployments.SortDesc, + } + if status != "" { + listInput.Status = deployments.Status(strings.ToUpper(status)) + } + if action != "" { + listInput.Action = deployments.Action(strings.ToUpper(action)) } - tbl := cli.NewTable("ID", "Action", "Status", "Version", "Created At", "By", "Message") - count := 0 + deps := make([]deployments.Deployment, 0, limit) for d, iterErr := range mdClient.Deployments.Iter(ctx, listInput) { if iterErr != nil { return iterErr } - if limit > 0 && count >= limit { + if limit > 0 && len(deps) >= limit { break } - tbl.AddRow(d.ID, d.Action, d.Status, d.Version, d.CreatedAt, d.DeployedBy, cli.TruncateString(d.Message, 40)) - count++ + deps = append(deps, d) } - tbl.Print() - return nil + switch output { + case "json": + jsonBytes, marshalErr := json.MarshalIndent(deps, "", " ") + if marshalErr != nil { + return fmt.Errorf("failed to marshal deployments to JSON: %w", marshalErr) + } + fmt.Println(string(jsonBytes)) + return nil + case "table": + tbl := cli.NewTable("ID", "Action", "Status", "Version", "Created At", "By", "Message") + for _, d := range deps { + tbl.AddRow(d.ID, d.Action, d.Status, d.Version, d.CreatedAt, d.DeployedBy, cli.TruncateString(d.Message, 40)) + } + tbl.Print() + return nil + default: + return fmt.Errorf("unsupported output format: %s", output) + } } func runDeploymentLogs(cmd *cobra.Command, args []string) error { diff --git a/cmd/environment.go b/cmd/environment.go index 17dddc8d..01787d8a 100644 --- a/cmd/environment.go +++ b/cmd/environment.go @@ -51,12 +51,15 @@ func NewCmdEnvironment() *cobra.Command { environmentGetCmd.Flags().StringP("output", "o", "text", "Output format (text or json)") environmentListCmd := &cobra.Command{ - Use: "list", - Short: "List environments", - Long: helpdocs.MustRender("environment/list"), - Args: cobra.ExactArgs(1), - RunE: runEnvironmentList, + Use: "list", + Aliases: []string{"ls"}, + Short: "List environments", + Long: helpdocs.MustRender("environment/list"), + Args: cobra.ExactArgs(1), + RunE: runEnvironmentList, } + environmentListCmd.Flags().StringP("output", "o", "table", "Output format (table, json)") + environmentListCmd.Flags().StringSlice("id", nil, "Filter to specific environment IDs (repeatable)") environmentCreateCmd := &cobra.Command{ Use: "create [ID]", @@ -220,6 +223,12 @@ func runEnvironmentList(cmd *cobra.Command, args []string) error { projectID := args[0] + output, err := cmd.Flags().GetString("output") + if err != nil { + return err + } + ids, _ := cmd.Flags().GetStringSlice("id") + cmd.SilenceUsage = true mdClient, err := massdriver.NewClient() @@ -227,14 +236,9 @@ func runEnvironmentList(cmd *cobra.Command, args []string) error { return fmt.Errorf("error initializing massdriver client: %w", err) } - envs, err := mdClient.Environments.List(ctx, environments.ListInput{ProjectID: projectID}) - if err != nil { - return err - } - - tbl := cli.NewTable("ID", "Name", "Description", "Monthly $", "Daily $") + seq := mdClient.Environments.Iter(ctx, environments.ListInput{ProjectID: projectID, IDs: ids}) - for _, env := range envs { + row := func(env environments.Environment) []string { monthly := "" daily := "" if env.Cost.MonthlyAverage.Amount != nil { @@ -243,13 +247,29 @@ func runEnvironmentList(cmd *cobra.Command, args []string) error { if env.Cost.DailyAverage.Amount != nil { daily = fmt.Sprintf("%v", *env.Cost.DailyAverage.Amount) } - description := cli.TruncateString(env.Description, 60) - tbl.AddRow(env.ID, env.Name, description, monthly, daily) + return []string{env.ID, env.Name, cli.TruncateString(env.Description, 60), monthly, daily} } - tbl.Print() - - return nil + switch output { + case "json": + envs, collectErr := types.Collect(seq) + if collectErr != nil { + return collectErr + } + jsonBytes, marshalErr := json.MarshalIndent(envs, "", " ") + if marshalErr != nil { + return fmt.Errorf("failed to marshal environments to JSON: %w", marshalErr) + } + fmt.Println(string(jsonBytes)) + return nil + case "table": + return cli.Paginate(seq, cli.PagerConfig[environments.Environment]{ + Columns: []string{"ID", "Name", "Description", "Monthly $", "Daily $"}, + Row: row, + }) + default: + return fmt.Errorf("unsupported output format: %s", output) + } } func renderEnvironment(ctx context.Context, mdClient *massdriver.Client, env *environments.Environment) error { @@ -265,7 +285,7 @@ func renderEnvironment(ctx context.Context, mdClient *massdriver.Client, env *en // Instances aren't embedded on the environment record returned by // Environments.Get; fetch them separately so the template can render them. - insts, err := mdClient.Instances.List(ctx, instances.ListInput{EnvironmentID: env.ID}) + insts, err := types.Collect(mdClient.Instances.Iter(ctx, instances.ListInput{EnvironmentID: env.ID})) if err != nil { return fmt.Errorf("failed to list instances: %w", err) } diff --git a/cmd/instance.go b/cmd/instance.go index 04dbdd33..cbdc1695 100644 --- a/cmd/instance.go +++ b/cmd/instance.go @@ -105,6 +105,10 @@ func NewCmdInstance() *cobra.Command { Args: cobra.ExactArgs(1), RunE: runInstanceList, } + instanceListCmd.Flags().StringP("output", "o", "table", "Output format (table, json)") + instanceListCmd.Flags().String("status", "", "Filter by lifecycle status (initialized, provisioned, decommissioned, failed)") + instanceListCmd.Flags().String("repo", "", "Filter by OCI repo name (matches all versions of a bundle)") + instanceListCmd.Flags().String("bundle", "", "Filter by bundle version (name@version) or release channel (name@latest)") instanceOrphanCmd := &cobra.Command{ Use: `orphan --`, @@ -475,6 +479,14 @@ func runInstanceList(cmd *cobra.Command, args []string) error { environmentID := args[0] + output, err := cmd.Flags().GetString("output") + if err != nil { + return err + } + status, _ := cmd.Flags().GetString("status") + repo, _ := cmd.Flags().GetString("repo") + bundle, _ := cmd.Flags().GetString("bundle") + cmd.SilenceUsage = true mdClient, err := massdriver.NewClient() @@ -482,26 +494,45 @@ func runInstanceList(cmd *cobra.Command, args []string) error { return fmt.Errorf("error initializing massdriver client: %w", err) } - insts, err := mdClient.Instances.List(ctx, instances.ListInput{EnvironmentID: environmentID}) - if err != nil { - return err + listInput := instances.ListInput{ + EnvironmentID: environmentID, + OciRepoName: repo, + BundleID: bundle, + } + if status != "" { + listInput.Status = instances.Status(strings.ToUpper(status)) } - tbl := cli.NewTable("ID", "Name", "Bundle", "Status") + seq := mdClient.Instances.Iter(ctx, listInput) - for _, inst := range insts { - componentName := "" - if inst.Component != nil { - componentName = inst.Component.Name + switch output { + case "json": + insts, collectErr := types.Collect(seq) + if collectErr != nil { + return collectErr } - bundleName := "" - if inst.Bundle != nil { - bundleName = inst.Bundle.Name + jsonBytes, marshalErr := json.MarshalIndent(insts, "", " ") + if marshalErr != nil { + return fmt.Errorf("failed to marshal instances to JSON: %w", marshalErr) } - tbl.AddRow(inst.ID, componentName, bundleName, inst.Status) + fmt.Println(string(jsonBytes)) + return nil + case "table": + return cli.Paginate(seq, cli.PagerConfig[instances.Instance]{ + Columns: []string{"ID", "Name", "Bundle", "Status"}, + Row: func(inst instances.Instance) []string { + componentName := "" + if inst.Component != nil { + componentName = inst.Component.Name + } + bundleName := "" + if inst.Bundle != nil { + bundleName = inst.Bundle.Name + } + return []string{inst.ID, componentName, bundleName, inst.Status} + }, + }) + default: + return fmt.Errorf("unsupported output format: %s", output) } - - tbl.Print() - - return nil } diff --git a/cmd/project.go b/cmd/project.go index 889c2f78..87cb5b01 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -17,6 +17,7 @@ import ( "github.com/massdriver-cloud/mass/internal/commands/project" "github.com/massdriver-cloud/massdriver-sdk-go/massdriver" "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/platform/projects" + "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/platform/types" "github.com/spf13/cobra" ) @@ -50,11 +51,13 @@ func NewCmdProject() *cobra.Command { projectGetCmd.Flags().StringP("output", "o", "text", "Output format (text or json)") projectListCmd := &cobra.Command{ - Use: "list", - Short: "List projects", - Long: helpdocs.MustRender("project/list"), - RunE: runProjectList, + Use: "list", + Aliases: []string{"ls"}, + Short: "List projects", + Long: helpdocs.MustRender("project/list"), + RunE: runProjectList, } + projectListCmd.Flags().StringP("output", "o", "table", "Output format (table, json)") projectCreateCmd := &cobra.Command{ Use: "create [slug]", @@ -152,34 +155,50 @@ func runProjectExport(cmd *cobra.Command, args []string) error { func runProjectList(cmd *cobra.Command, args []string) error { ctx := context.Background() - mdClient, err := massdriver.NewClient() + output, err := cmd.Flags().GetString("output") if err != nil { - return fmt.Errorf("error initializing massdriver client: %w", err) + return err } - projectList, err := mdClient.Projects.List(ctx, projects.ListInput{}) + mdClient, err := massdriver.NewClient() if err != nil { - return err + return fmt.Errorf("error initializing massdriver client: %w", err) } - tbl := cli.NewTable("ID/Slug", "Name", "Description", "Monthly $", "Daily $") + seq := mdClient.Projects.Iter(ctx, projects.ListInput{}) - for _, p := range projectList { - monthly := "" - daily := "" - if p.Cost.MonthlyAverage.Amount != nil { - monthly = fmt.Sprintf("%v", *p.Cost.MonthlyAverage.Amount) + switch output { + case "json": + projectList, collectErr := types.Collect(seq) + if collectErr != nil { + return collectErr } - if p.Cost.DailyAverage.Amount != nil { - daily = fmt.Sprintf("%v", *p.Cost.DailyAverage.Amount) + jsonBytes, marshalErr := json.MarshalIndent(projectList, "", " ") + if marshalErr != nil { + return fmt.Errorf("failed to marshal projects to JSON: %w", marshalErr) } - description := cli.TruncateString(p.Description, 60) - tbl.AddRow(p.ID, p.Name, description, monthly, daily) + fmt.Println(string(jsonBytes)) + return nil + case "table": + // Stream pages on demand: interactive pager when attached to a terminal, + // plain streamed table otherwise. Avoids buffering every project up front. + return cli.Paginate(seq, cli.PagerConfig[projects.Project]{ + Columns: []string{"ID/Slug", "Name", "Description", "Monthly $", "Daily $"}, + Row: func(p projects.Project) []string { + monthly := "" + daily := "" + if p.Cost.MonthlyAverage.Amount != nil { + monthly = fmt.Sprintf("%v", *p.Cost.MonthlyAverage.Amount) + } + if p.Cost.DailyAverage.Amount != nil { + daily = fmt.Sprintf("%v", *p.Cost.DailyAverage.Amount) + } + return []string{p.ID, p.Name, cli.TruncateString(p.Description, 60), monthly, daily} + }, + }) + default: + return fmt.Errorf("unsupported output format: %s", output) } - - tbl.Print() - - return nil } func renderProject(p *projects.Project) error { diff --git a/cmd/repository.go b/cmd/repository.go index c32a511c..b4661215 100644 --- a/cmd/repository.go +++ b/cmd/repository.go @@ -46,11 +46,12 @@ func NewCmdRepository() *cobra.Command { listInput := repositoryListInput{} repositoryListCmd := &cobra.Command{ - Use: "list", - Short: "List OCI repositories", + Use: "list", + Aliases: []string{"ls"}, + Short: "List OCI repositories", RunE: func(cmd *cobra.Command, args []string) error { cmd.SilenceUsage = true - return runRepositoryList(&listInput) + return runRepositoryList(&listInput, cmd.Flags().Changed("order")) }, } repositoryListCmd.Flags().StringVarP(&listInput.name, "name", "n", "", "Filter by exact repository name") @@ -115,7 +116,7 @@ type repositoryListInput struct { output string } -func runRepositoryList(input *repositoryListInput) error { +func runRepositoryList(input *repositoryListInput, orderChanged bool) error { ctx := context.Background() mdClient, err := massdriver.NewClient() @@ -123,29 +124,35 @@ func runRepositoryList(input *repositoryListInput) error { return fmt.Errorf("error initializing massdriver client: %w", err) } - listInput, buildErr := buildOciReposListInput(input) + listInput, buildErr := buildOciReposListInput(input, orderChanged) if buildErr != nil { return buildErr } - repos, err := mdClient.OciRepos.List(ctx, listInput) - if err != nil { - return fmt.Errorf("failed to list repositories: %w", err) - } + seq := mdClient.OciRepos.Iter(ctx, listInput) switch input.output { case "json": - jsonBytes, err := json.MarshalIndent(repos, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal repositories to JSON: %w", err) + // JSON output is consumed by scripts that expect a single document, so + // buffer the full result set before marshaling. + repos, collectErr := types.Collect(seq) + if collectErr != nil { + return fmt.Errorf("failed to list repositories: %w", collectErr) + } + jsonBytes, marshalErr := json.MarshalIndent(repos, "", " ") + if marshalErr != nil { + return fmt.Errorf("failed to marshal repositories to JSON: %w", marshalErr) } fmt.Println(string(jsonBytes)) case "table": - tbl := cli.NewTable("Name", "Type", "Latest", "Created At") - for _, repo := range repos { - tbl.AddRow(repo.Name, artifactTypeLabel(repo.ArtifactType), repo.LatestTag, repo.CreatedAt.Format("2006-01-02 15:04:05")) - } - tbl.Print() + // Interactive pager on a TTY, streamed table otherwise — pages fetched + // on demand rather than buffering every repository. + return cli.Paginate(seq, cli.PagerConfig[ocirepos.OciRepo]{ + Columns: []string{"Name", "Type", "Latest", "Created At"}, + Row: func(repo ocirepos.OciRepo) []string { + return []string{repo.Name, artifactTypeLabel(repo.ArtifactType), repo.LatestTag, repo.CreatedAt.Format("2006-01-02 15:04:05")} + }, + }) default: return fmt.Errorf("unsupported output format: %s", input.output) } @@ -153,7 +160,7 @@ func runRepositoryList(input *repositoryListInput) error { return nil } -func buildOciReposListInput(input *repositoryListInput) (ocirepos.ListInput, error) { +func buildOciReposListInput(input *repositoryListInput, orderChanged bool) (ocirepos.ListInput, error) { out := ocirepos.ListInput{ Search: input.search, } @@ -175,14 +182,14 @@ func buildOciReposListInput(input *repositoryListInput) (ocirepos.ListInput, err out.NameStartsWith = input.prefix } - if input.sortField != "" { - field := ocirepos.SortByName - if strings.EqualFold(input.sortField, "created_at") { - field = ocirepos.SortByCreatedAt + if input.sortField != "" || orderChanged { + field, fieldErr := parseRepoSortField(input.sortField) + if fieldErr != nil { + return out, fieldErr } - order := ocirepos.SortAsc - if strings.EqualFold(input.sortOrder, "desc") { - order = ocirepos.SortDesc + order, orderErr := parseRepoSortOrder(input.sortOrder) + if orderErr != nil { + return out, orderErr } out.SortBy = field out.SortOrder = order @@ -191,6 +198,28 @@ func buildOciReposListInput(input *repositoryListInput) (ocirepos.ListInput, err return out, nil } +func parseRepoSortField(s string) (ocirepos.SortField, error) { + switch strings.ToLower(s) { + case "", "name": + return ocirepos.SortByName, nil + case "created_at": + return ocirepos.SortByCreatedAt, nil + default: + return "", fmt.Errorf("unknown sort field %q (valid: name, created_at)", s) + } +} + +func parseRepoSortOrder(s string) (ocirepos.SortOrder, error) { + switch strings.ToLower(s) { + case "", "asc": + return ocirepos.SortAsc, nil + case "desc": + return ocirepos.SortDesc, nil + default: + return "", fmt.Errorf("unknown sort order %q (valid: asc, desc)", s) + } +} + func resolveArtifactType(s string) (ocirepos.ArtifactType, error) { if at, ok := artifactTypeAliases[strings.ToLower(s)]; ok { return at, nil diff --git a/cmd/resource.go b/cmd/resource.go index bf96a17a..4dd64de8 100644 --- a/cmd/resource.go +++ b/cmd/resource.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "os" + "strings" "text/template" "github.com/charmbracelet/glamour" @@ -14,6 +15,7 @@ import ( "github.com/massdriver-cloud/mass/internal/cli" "github.com/massdriver-cloud/mass/internal/commands/resource" "github.com/massdriver-cloud/massdriver-sdk-go/massdriver" + "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/platform/resources" "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/platform/types" "github.com/spf13/cobra" ) @@ -29,7 +31,17 @@ func NewCmdResource() *cobra.Command { Long: helpdocs.MustRender("resource"), } - // Create + resourceCmd.AddCommand(newResourceCreateCmd()) + resourceCmd.AddCommand(newResourceGetCmd()) + resourceCmd.AddCommand(newResourceDownloadCmd()) + resourceCmd.AddCommand(newResourceUpdateCmd()) + resourceCmd.AddCommand(newResourceDeleteCmd()) + resourceCmd.AddCommand(newResourceListCmd()) + + return resourceCmd +} + +func newResourceCreateCmd() *cobra.Command { resourceCreateCmd := &cobra.Command{ Use: `create`, Short: "Create a resource", @@ -43,8 +55,10 @@ func NewCmdResource() *cobra.Command { _ = resourceCreateCmd.MarkFlagRequired("name") _ = resourceCreateCmd.MarkFlagRequired("type") _ = resourceCreateCmd.MarkFlagRequired("file") + return resourceCreateCmd +} - // Get +func newResourceGetCmd() *cobra.Command { resourceGetCmd := &cobra.Command{ Use: "get [resource-id]", Short: "Get an resource from Massdriver", @@ -59,8 +73,10 @@ func NewCmdResource() *cobra.Command { mass resource get api-prod-grpcapi-host -o json`, } resourceGetCmd.Flags().StringP("output", "o", "text", "Output format (text or json)") + return resourceGetCmd +} - // Download +func newResourceDownloadCmd() *cobra.Command { resourceDownloadCmd := &cobra.Command{ Use: "download [resource-id]", Short: "Download an resource in the specified format", @@ -75,8 +91,10 @@ func NewCmdResource() *cobra.Command { mass resource download network-useast1-vpc-network -f yaml`, } resourceDownloadCmd.Flags().StringP("format", "f", "json", "Download format (json, yaml, etc.)") + return resourceDownloadCmd +} - // Update +func newResourceUpdateCmd() *cobra.Command { resourceUpdateCmd := &cobra.Command{ Use: "update [resource-id]", Short: "Update an imported resource", @@ -92,7 +110,10 @@ func NewCmdResource() *cobra.Command { resourceUpdateCmd.Flags().StringP("name", "n", "", "New resource name") resourceUpdateCmd.Flags().StringP("file", "f", "", "Resource payload file") _ = resourceUpdateCmd.MarkFlagRequired("file") + return resourceUpdateCmd +} +func newResourceDeleteCmd() *cobra.Command { resourceDeleteCmd := &cobra.Command{ Use: "delete [resource-id]", Short: "Delete a resource", @@ -105,14 +126,126 @@ func NewCmdResource() *cobra.Command { mass resource delete 12345678-1234-1234-1234-123456789012 --force`, } resourceDeleteCmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt") + return resourceDeleteCmd +} - resourceCmd.AddCommand(resourceCreateCmd) - resourceCmd.AddCommand(resourceGetCmd) - resourceCmd.AddCommand(resourceDownloadCmd) - resourceCmd.AddCommand(resourceUpdateCmd) - resourceCmd.AddCommand(resourceDeleteCmd) +func newResourceListCmd() *cobra.Command { + resourceListCmd := &cobra.Command{ + Use: "list", + Short: "List resources", + Aliases: []string{"ls"}, + Long: helpdocs.MustRender("resource/list"), + RunE: runResourceList, + Example: ` # List all resources + mass resource list + + # Search and filter + mass resource list --search database + mass resource list --type aws-iam-role --origin provisioned + mass resource list --environment ecomm-prod -o json`, + } + resourceListCmd.Flags().StringP("output", "o", "table", "Output format (table, json)") + resourceListCmd.Flags().StringP("search", "s", "", "Full-text search across resource name") + resourceListCmd.Flags().StringP("type", "t", "", "Filter by resource type id (e.g. aws-iam-role)") + resourceListCmd.Flags().String("origin", "", "Filter by origin (imported, provisioned). Empty matches both") + resourceListCmd.Flags().StringP("environment", "e", "", "Limit to provisioned resources in an environment") + resourceListCmd.Flags().String("sort", "", "Sort field (name, created_at)") + resourceListCmd.Flags().String("order", "asc", "Sort order (asc, desc)") + return resourceListCmd +} - return resourceCmd +func parseResourceSortField(s string) (resources.SortField, error) { + switch strings.ToLower(s) { + case "", "name": + return resources.SortByName, nil + case "created_at": + return resources.SortByCreatedAt, nil + default: + return "", fmt.Errorf("unknown sort field %q (valid: name, created_at)", s) + } +} + +func parseSortOrder(s string) (resources.SortOrder, error) { + switch strings.ToLower(s) { + case "", "asc": + return resources.SortAsc, nil + case "desc": + return resources.SortDesc, nil + default: + return "", fmt.Errorf("unknown sort order %q (valid: asc, desc)", s) + } +} + +func runResourceList(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + output, err := cmd.Flags().GetString("output") + if err != nil { + return err + } + search, _ := cmd.Flags().GetString("search") + resourceType, _ := cmd.Flags().GetString("type") + origin, _ := cmd.Flags().GetString("origin") + environmentID, _ := cmd.Flags().GetString("environment") + sortField, _ := cmd.Flags().GetString("sort") + sortOrder, _ := cmd.Flags().GetString("order") + + cmd.SilenceUsage = true + + mdClient, err := massdriver.NewClient() + if err != nil { + return fmt.Errorf("error initializing massdriver client: %w", err) + } + + listInput := resources.ListInput{ + Search: search, + ResourceType: resourceType, + EnvironmentID: environmentID, + } + if origin != "" { + listInput.Origin = resources.Origin(strings.ToUpper(origin)) + } + if sortField != "" || cmd.Flags().Changed("order") { + sortBy, sortByErr := parseResourceSortField(sortField) + if sortByErr != nil { + return sortByErr + } + order, orderErr := parseSortOrder(sortOrder) + if orderErr != nil { + return orderErr + } + listInput.SortBy = sortBy + listInput.SortOrder = order + } + + seq := mdClient.Resources.Iter(ctx, listInput) + + switch output { + case "json": + res, collectErr := types.Collect(seq) + if collectErr != nil { + return collectErr + } + jsonBytes, marshalErr := json.MarshalIndent(res, "", " ") + if marshalErr != nil { + return fmt.Errorf("failed to marshal resources to JSON: %w", marshalErr) + } + fmt.Println(string(jsonBytes)) + return nil + case "table": + return cli.Paginate(seq, cli.PagerConfig[resources.Resource]{ + Columns: []string{"ID", "Name", "Type", "Origin", "Created At"}, + Row: func(r resources.Resource) []string { + typeName := "" + if r.ResourceType != nil { + typeName = r.ResourceType.Name + } + return []string{r.ID, r.Name, typeName, strings.ToLower(r.Origin), r.CreatedAt.Format("2006-01-02 15:04:05")} + }, + }) + default: + return fmt.Errorf("unsupported output format: %s", output) + } } func runResourceCreate(cmd *cobra.Command, args []string) error { diff --git a/cmd/resource_type.go b/cmd/resource_type.go index 3f038a2e..b5b47144 100644 --- a/cmd/resource_type.go +++ b/cmd/resource_type.go @@ -42,11 +42,13 @@ func NewCmdType() *cobra.Command { typeGetCmd.Flags().StringP("output", "o", "text", "Output format (text or json)") typeListCmd := &cobra.Command{ - Use: "list", - Short: "List resource types", - Long: helpdocs.MustRender("type/list"), - RunE: runTypeList, + Use: "list", + Aliases: []string{"ls"}, + Short: "List resource types", + Long: helpdocs.MustRender("type/list"), + RunE: runTypeList, } + typeListCmd.Flags().StringP("output", "o", "table", "Output format (table, json)") typePublishCmd := &cobra.Command{ Use: "publish [resource-type file]", @@ -135,6 +137,11 @@ func runTypePublish(cmd *cobra.Command, args []string) error { func runTypeList(cmd *cobra.Command, args []string) error { ctx := context.Background() + + output, err := cmd.Flags().GetString("output") + if err != nil { + return err + } cmd.SilenceUsage = true mdClient, err := massdriver.NewClient() @@ -143,16 +150,28 @@ func runTypeList(cmd *cobra.Command, args []string) error { } resourceTypes, err := resourcetype.List(ctx, mdClient) - - tbl := cli.NewTable("ID", "Name", "Updated At") - - for _, rt := range resourceTypes { - tbl.AddRow(rt.ID, rt.Name, rt.UpdatedAt) + if err != nil { + return err } - tbl.Print() + switch output { + case "json": + jsonBytes, marshalErr := json.MarshalIndent(resourceTypes, "", " ") + if marshalErr != nil { + return fmt.Errorf("failed to marshal resource types to JSON: %w", marshalErr) + } + fmt.Println(string(jsonBytes)) + case "table": + tbl := cli.NewTable("ID", "Name", "Updated At") + for _, rt := range resourceTypes { + tbl.AddRow(rt.ID, rt.Name, rt.UpdatedAt) + } + tbl.Print() + default: + return fmt.Errorf("unsupported output format: %s", output) + } - return err + return nil } func renderType(restype *resourcetype.ResourceType) error { diff --git a/cmd/schema.go b/cmd/schema.go index e82eb43b..56521582 100644 --- a/cmd/schema.go +++ b/cmd/schema.go @@ -2,6 +2,7 @@ package cmd import ( "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -23,13 +24,13 @@ func NewCmdSchema() *cobra.Command { } schemaDereferenceCmd := &cobra.Command{ - Use: "dereference", + Use: "dereference [file]", Short: "Dereferences a JSON Schema", Long: helpdocs.MustRender("schema/dereference"), + Args: cobra.MaximumNArgs(1), RunE: runSchemaDereference, } schemaDereferenceCmd.Flags().StringP("file", "f", "", "Path to JSON document") - _ = schemaDereferenceCmd.MarkFlagRequired("file") schemaValidateCmd := &cobra.Command{ Use: "validate", @@ -52,6 +53,16 @@ func runSchemaDereference(cmd *cobra.Command, args []string) error { schemaPath, _ := cmd.Flags().GetString("file") cmd.SilenceUsage = true + if schemaPath != "" && len(args) > 0 { + return errors.New("a schema file must be provided via either the -f flag or a positional argument, not both") + } + if len(args) > 0 { + schemaPath = args[0] + } + if schemaPath == "" { + return errors.New("a schema file must be provided via the -f flag or as a positional argument") + } + schemaFile, openErr := os.Open(schemaPath) if openErr != nil { return fmt.Errorf("failed to open schema file %s: %w", schemaPath, openErr) diff --git a/docs/generated/mass.md b/docs/generated/mass.md index dcf66d74..ea077e38 100644 --- a/docs/generated/mass.md +++ b/docs/generated/mass.md @@ -44,6 +44,5 @@ Configure and deploying infrastructure and applications. * [mass resource](/cli/commands/mass_resource) - Manage resources * [mass resource-type](/cli/commands/mass_resource-type) - Resource type management * [mass schema](/cli/commands/mass_schema) - Manage JSON Schemas -* [mass server](/cli/commands/mass_server) - Start the bundle development server * [mass version](/cli/commands/mass_version) - Version of Mass CLI * [mass whoami](/cli/commands/mass_whoami) - Show the currently authenticated user or service account diff --git a/docs/generated/mass_bundle_publish.md b/docs/generated/mass_bundle_publish.md index 59771ea8..2d9012ed 100644 --- a/docs/generated/mass_bundle_publish.md +++ b/docs/generated/mass_bundle_publish.md @@ -15,7 +15,6 @@ mass bundle publish [path] [flags] ### Options ``` - --access string (Deprecated) Only here for backwards compatibility. Will be removed in a future release. -b, --bundle-directory string Path to a directory containing a massdriver.yaml file. (default ".") -d, --development Publish the bundle as a development release. -f, --fail-warnings Fail on warnings from the linter diff --git a/docs/generated/mass_bundle_template_list.md b/docs/generated/mass_bundle_template_list.md index ca1cca17..38d054e5 100644 --- a/docs/generated/mass_bundle_template_list.md +++ b/docs/generated/mass_bundle_template_list.md @@ -63,7 +63,8 @@ mass bundle template list [flags] ### Options ``` - -h, --help help for list + -h, --help help for list + -o, --output string Output format (text, json) (default "text") ``` ### SEE ALSO diff --git a/docs/generated/mass_deployment.md b/docs/generated/mass_deployment.md index e4a201e4..2cd2730f 100644 --- a/docs/generated/mass_deployment.md +++ b/docs/generated/mass_deployment.md @@ -19,6 +19,7 @@ Use these commands to inspect deployment history and logs: - `mass deployment list ` — list recent deployments for an instance - `mass deployment get ` — show details for a single deployment - `mass deployment logs ` — print log output from a deployment +- `mass deployment abort ` — abort a pending, approved, or running deployment ### Options @@ -30,6 +31,7 @@ Use these commands to inspect deployment history and logs: ### SEE ALSO * [mass](/cli/commands/mass) - Massdriver Cloud CLI +* [mass deployment abort](/cli/commands/mass_deployment_abort) - Abort a pending, approved, or running deployment * [mass deployment get](/cli/commands/mass_deployment_get) - Get a deployment by ID * [mass deployment list](/cli/commands/mass_deployment_list) - List deployments for an instance (most recent first) * [mass deployment logs](/cli/commands/mass_deployment_logs) - Stream the log output from a deployment diff --git a/docs/generated/mass_deployment_list.md b/docs/generated/mass_deployment_list.md index b28913ad..e36c6f44 100644 --- a/docs/generated/mass_deployment_list.md +++ b/docs/generated/mass_deployment_list.md @@ -44,8 +44,13 @@ mass deployment list ecomm-prod-db --limit 25 ### Options ``` - -h, --help help for list - -n, --limit int Maximum number of deployments to return (max 100) (default 10) + --action string Filter by action (provision, decommission, plan) + --bundle string Filter by bundle version (name@version) or release channel (name@latest) + -h, --help help for list + -n, --limit int Maximum number of deployments to return (max 100) (default 10) + -o, --output string Output format (table, json) (default "table") + --repo string Filter by OCI repo name + --status string Filter by status (pending, approved, running, completed, failed, aborted, rejected, proposed) ``` ### SEE ALSO diff --git a/docs/generated/mass_environment.md b/docs/generated/mass_environment.md index 87b8d81d..d6a98378 100644 --- a/docs/generated/mass_environment.md +++ b/docs/generated/mass_environment.md @@ -27,8 +27,12 @@ Environments can be modeled by application stage (production, staging, developme * [mass](/cli/commands/mass) - Massdriver Cloud CLI * [mass environment create](/cli/commands/mass_environment_create) - Create an environment +* [mass environment decommission](/cli/commands/mass_environment_decommission) - Decommission every instance in an environment, in reverse dependency order * [mass environment default](/cli/commands/mass_environment_default) - Set an environment default connection +* [mass environment deploy](/cli/commands/mass_environment_deploy) - Deploy every instance in an environment, in dependency order * [mass environment export](/cli/commands/mass_environment_export) - Export an environment from Massdriver +* [mass environment fork](/cli/commands/mass_environment_fork) - Fork an existing environment * [mass environment get](/cli/commands/mass_environment_get) - Get an environment from Massdriver * [mass environment list](/cli/commands/mass_environment_list) - List environments +* [mass environment preview](/cli/commands/mass_environment_preview) - Converge a preview environment from a YAML config * [mass environment update](/cli/commands/mass_environment_update) - Update an environment's name, description, or attributes diff --git a/docs/generated/mass_environment_list.md b/docs/generated/mass_environment_list.md index 59c076f9..0f7455d2 100644 --- a/docs/generated/mass_environment_list.md +++ b/docs/generated/mass_environment_list.md @@ -35,7 +35,9 @@ mass environment list [flags] ### Options ``` - -h, --help help for list + -h, --help help for list + --id strings Filter to specific environment IDs (repeatable) + -o, --output string Output format (table, json) (default "table") ``` ### SEE ALSO diff --git a/docs/generated/mass_instance.md b/docs/generated/mass_instance.md index b0b004df..c00e6dc2 100644 --- a/docs/generated/mass_instance.md +++ b/docs/generated/mass_instance.md @@ -30,9 +30,11 @@ Instances are used to: ### SEE ALSO * [mass](/cli/commands/mass) - Massdriver Cloud CLI +* [mass instance copy](/cli/commands/mass_instance_copy) - Copy an instance's configuration to another instance of the same component * [mass instance deploy](/cli/commands/mass_instance_deploy) - Deploy instances * [mass instance destroy](/cli/commands/mass_instance_destroy) - Destroy (decommission) an instance * [mass instance export](/cli/commands/mass_instance_export) - Export instances * [mass instance get](/cli/commands/mass_instance_get) - Get an instance * [mass instance list](/cli/commands/mass_instance_list) - List instances in an environment +* [mass instance orphan](/cli/commands/mass_instance_orphan) - Orphan an instance (reset to INITIALIZED, optionally clearing state locks) * [mass instance version](/cli/commands/mass_instance_version) - Set instance version diff --git a/docs/generated/mass_instance_list.md b/docs/generated/mass_instance_list.md index fdbeb00a..aaacb929 100644 --- a/docs/generated/mass_instance_list.md +++ b/docs/generated/mass_instance_list.md @@ -41,7 +41,11 @@ mass instance list ecomm-prod ### Options ``` - -h, --help help for list + --bundle string Filter by bundle version (name@version) or release channel (name@latest) + -h, --help help for list + -o, --output string Output format (table, json) (default "table") + --repo string Filter by OCI repo name (matches all versions of a bundle) + --status string Filter by lifecycle status (initialized, provisioned, decommissioned, failed) ``` ### SEE ALSO diff --git a/docs/generated/mass_instance_version.md b/docs/generated/mass_instance_version.md index 4fec6667..63cef664 100644 --- a/docs/generated/mass_instance_version.md +++ b/docs/generated/mass_instance_version.md @@ -12,7 +12,7 @@ Set instance version # Set instance version -Set the version or release channel for an instance in Massdriver. +Set the version for an instance in Massdriver. ## Examples @@ -22,19 +22,8 @@ Set the version for an instance using the `slug@version` format: mass instance version api-prod-db@latest ``` -Set the version with a specific release channel: - -```shell -mass instance version api-prod-db@latest --release-channel development -``` - The `slug` can be found in the instance info panel. The instance slug is a combination of the `--`. -## Release Channels - -- `stable` (default): Instance receives only stable releases -- `development`: Instance receives both stable and development releases - ## Version Format The version can be: @@ -50,14 +39,13 @@ mass instance version @ [flags] ### Examples ``` -mass instance version api-prod-db@latest --release-channel development +mass instance version api-prod-db@latest ``` ### Options ``` - -h, --help help for version - --release-channel string Release strategy (stable or development) (default "stable") + -h, --help help for version ``` ### SEE ALSO diff --git a/docs/generated/mass_project_list.md b/docs/generated/mass_project_list.md index 5fbbe64e..3ee19799 100644 --- a/docs/generated/mass_project_list.md +++ b/docs/generated/mass_project_list.md @@ -35,7 +35,8 @@ mass project list [flags] ### Options ``` - -h, --help help for list + -h, --help help for list + -o, --output string Output format (table, json) (default "table") ``` ### SEE ALSO diff --git a/docs/generated/mass_resource-type_list.md b/docs/generated/mass_resource-type_list.md index 414e717e..02cad451 100644 --- a/docs/generated/mass_resource-type_list.md +++ b/docs/generated/mass_resource-type_list.md @@ -35,7 +35,8 @@ mass resource-type list [flags] ### Options ``` - -h, --help help for list + -h, --help help for list + -o, --output string Output format (table, json) (default "table") ``` ### SEE ALSO diff --git a/docs/generated/mass_resource.md b/docs/generated/mass_resource.md index e3f4a1d3..df56c0b6 100644 --- a/docs/generated/mass_resource.md +++ b/docs/generated/mass_resource.md @@ -25,6 +25,8 @@ Resources represent infrastructure outputs and connections in Massdriver. They c * [mass](/cli/commands/mass) - Massdriver Cloud CLI * [mass resource create](/cli/commands/mass_resource_create) - Create a resource +* [mass resource delete](/cli/commands/mass_resource_delete) - Delete a resource * [mass resource download](/cli/commands/mass_resource_download) - Download an resource in the specified format * [mass resource get](/cli/commands/mass_resource_get) - Get an resource from Massdriver +* [mass resource list](/cli/commands/mass_resource_list) - List resources * [mass resource update](/cli/commands/mass_resource_update) - Update an imported resource diff --git a/docs/generated/mass_resource_list.md b/docs/generated/mass_resource_list.md new file mode 100644 index 00000000..25e8a661 --- /dev/null +++ b/docs/generated/mass_resource_list.md @@ -0,0 +1,85 @@ +--- +id: mass_resource_list.md +slug: /cli/commands/mass_resource_list +title: Mass Resource List +sidebar_label: Mass Resource List +--- +## mass resource list + +List resources + +### Synopsis + +# List Resources + +Lists resources in your Massdriver organization. + +Resources are either **imported** (registered manually, identified by UUID) or +**provisioned** (created by a deployment, identified by a friendly slug). Use the +filters below to narrow large result sets, and `--output json` for scripting. + +## Usage + +```bash +mass resource list [flags] +``` + +## Flags + +- `-s, --search` — full-text search across the resource name +- `-t, --type` — filter by resource type id (e.g. `aws-iam-role`) +- `--origin` — filter by origin: `imported` or `provisioned` (empty matches both) +- `-e, --environment` — limit to provisioned resources in an environment +- `--sort` — sort field: `name` or `created_at` +- `--order` — sort order: `asc` or `desc` +- `-o, --output` — output format: `table` or `json` + +## Examples + +```bash +# List all resources +mass resource list + +# Full-text search +mass resource list --search database + +# Provisioned IAM roles, newest first +mass resource list --type aws-iam-role --origin provisioned --sort created_at --order desc + +# Resources in a specific environment, as JSON +mass resource list --environment ecomm-prod -o json +``` + + +``` +mass resource list [flags] +``` + +### Examples + +``` + # List all resources + mass resource list + + # Search and filter + mass resource list --search database + mass resource list --type aws-iam-role --origin provisioned + mass resource list --environment ecomm-prod -o json +``` + +### Options + +``` + -e, --environment string Limit to provisioned resources in an environment + -h, --help help for list + --order string Sort order (asc, desc) (default "asc") + --origin string Filter by origin (imported, provisioned). Empty matches both + -o, --output string Output format (table, json) (default "table") + -s, --search string Full-text search across resource name + --sort string Sort field (name, created_at) + -t, --type string Filter by resource type id (e.g. aws-iam-role) +``` + +### SEE ALSO + +* [mass resource](/cli/commands/mass_resource) - Manage resources diff --git a/docs/generated/mass_schema_dereference.md b/docs/generated/mass_schema_dereference.md index 0b5ac2f6..034aec65 100644 --- a/docs/generated/mass_schema_dereference.md +++ b/docs/generated/mass_schema_dereference.md @@ -30,7 +30,7 @@ cat resource-type.json | mass schema dereference -f - ``` -mass schema dereference [flags] +mass schema dereference [file] [flags] ``` ### Options diff --git a/docs/helpdocs/resource/list.md b/docs/helpdocs/resource/list.md new file mode 100644 index 00000000..050c2334 --- /dev/null +++ b/docs/helpdocs/resource/list.md @@ -0,0 +1,39 @@ +# List Resources + +Lists resources in your Massdriver organization. + +Resources are either **imported** (registered manually, identified by UUID) or +**provisioned** (created by a deployment, identified by a friendly slug). Use the +filters below to narrow large result sets, and `--output json` for scripting. + +## Usage + +```bash +mass resource list [flags] +``` + +## Flags + +- `-s, --search` — full-text search across the resource name +- `-t, --type` — filter by resource type id (e.g. `aws-iam-role`) +- `--origin` — filter by origin: `imported` or `provisioned` (empty matches both) +- `-e, --environment` — limit to provisioned resources in an environment +- `--sort` — sort field: `name` or `created_at` +- `--order` — sort order: `asc` or `desc` +- `-o, --output` — output format: `table` or `json` + +## Examples + +```bash +# List all resources +mass resource list + +# Full-text search +mass resource list --search database + +# Provisioned IAM roles, newest first +mass resource list --type aws-iam-role --origin provisioned --sort created_at --order desc + +# Resources in a specific environment, as JSON +mass resource list --environment ecomm-prod -o json +``` diff --git a/go.mod b/go.mod index a62e962b..1aeeb2b4 100644 --- a/go.mod +++ b/go.mod @@ -1,43 +1,44 @@ module github.com/massdriver-cloud/mass -go 1.24.2 - -toolchain go1.24.12 +go 1.25.0 require ( github.com/AlecAivazis/survey/v2 v2.3.7 - github.com/BurntSushi/toml v1.4.0 + github.com/BurntSushi/toml v1.5.0 github.com/Khan/genqlient v0.8.1 - github.com/charmbracelet/glamour v0.8.0 - github.com/charmbracelet/lipgloss v1.0.0 - github.com/fatih/color v1.17.0 - github.com/hashicorp/hcl/v2 v2.23.0 + github.com/charmbracelet/bubbles v0.20.0 + github.com/charmbracelet/bubbletea v1.2.3 + github.com/charmbracelet/glamour v1.0.0 + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 + github.com/fatih/color v1.18.0 + github.com/hashicorp/hcl/v2 v2.24.0 github.com/itchyny/gojq v0.12.16 github.com/manifoldco/promptui v0.9.0 - github.com/massdriver-cloud/airlock v0.0.9 - github.com/massdriver-cloud/massdriver-sdk-go v0.2.4 - github.com/mattn/go-runewidth v0.0.16 + github.com/massdriver-cloud/airlock v0.0.10 + github.com/massdriver-cloud/massdriver-sdk-go v0.2.6 + github.com/mattn/go-runewidth v0.0.24 github.com/opencontainers/image-spec v1.1.1 github.com/osteele/liquid v1.7.0 github.com/rodaine/table v1.3.0 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 - github.com/spf13/cobra v1.8.1 - github.com/stretchr/testify v1.10.0 + github.com/spf13/cobra v1.10.2 + github.com/stretchr/testify v1.11.1 github.com/wk8/go-ordered-map/v2 v2.1.8 - github.com/zclconf/go-cty v1.15.1 - golang.org/x/mod v0.25.0 - golang.org/x/text v0.26.0 + github.com/zclconf/go-cty v1.18.1 + golang.org/x/mod v0.37.0 + golang.org/x/term v0.44.0 + golang.org/x/text v0.38.0 gopkg.in/yaml.v3 v3.0.1 oras.land/oras-go/v2 v2.6.0 - sigs.k8s.io/yaml v1.4.0 + sigs.k8s.io/yaml v1.6.0 ) require ( - github.com/Checkmarx/kics/v2 v2.1.3 // indirect + github.com/Checkmarx/kics/v2 v2.1.20 // indirect github.com/agext/levenshtein v1.2.3 // indirect - github.com/agnivade/levenshtein v1.1.1 // indirect - github.com/alecthomas/chroma/v2 v2.14.0 // indirect + github.com/agnivade/levenshtein v1.2.1 // indirect + github.com/alecthomas/chroma/v2 v2.26.1 // indirect github.com/alexflint/go-arg v1.5.1 // indirect github.com/alexflint/go-scalar v1.2.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect @@ -46,52 +47,65 @@ require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect - github.com/buger/jsonparser v1.1.1 // indirect - github.com/charmbracelet/x/ansi v0.5.2 // indirect - github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b // indirect + github.com/buger/jsonparser v1.2.0 // indirect + github.com/charmbracelet/colorprofile v0.4.3 // indirect + github.com/charmbracelet/x/ansi v0.11.7 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/exp/slice v0.0.0-20260608090822-c3ad58c6c9e5 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect github.com/chzyer/readline v1.5.1 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/creack/pty v1.1.18 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dlclark/regexp2 v1.11.4 // indirect + github.com/dlclark/regexp2 v1.12.0 // indirect + github.com/dlclark/regexp2/v2 v2.2.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/go-resty/resty/v2 v2.16.5 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/css v1.0.1 // indirect - github.com/gorilla/websocket v1.5.3 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/itchyny/timefmt-go v0.1.6 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kelseyhightower/envconfig v1.4.0 // indirect github.com/kr/text v0.2.0 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/massdriver-cloud/terraform-config-inspect v0.0.1 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/lucasb-eyer/go-colorful v1.4.0 // indirect + github.com/mailru/easyjson v0.9.2 // indirect + github.com/massdriver-cloud/terraform-config-inspect v0.0.2 // indirect + github.com/mattn/go-colorable v0.1.15 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect + github.com/muesli/termenv v0.16.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/osteele/tuesday v1.0.3 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rs/zerolog v1.33.0 // indirect + github.com/rs/zerolog v1.35.1 // indirect github.com/sosedoff/ansible-vault-go v0.2.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/vektah/gqlparser/v2 v2.5.19 // indirect - github.com/yuin/goldmark v1.7.8 // indirect - github.com/yuin/goldmark-emoji v1.0.4 // indirect - golang.org/x/crypto v0.39.0 // indirect - golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect - golang.org/x/tools v0.33.0 // indirect + github.com/vektah/gqlparser/v2 v2.5.31 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yuin/goldmark v1.8.2 // indirect + github.com/yuin/goldmark-emoji v1.0.6 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/crypto v0.53.0 // indirect + golang.org/x/exp v0.0.0-20260603202125-055de637280b // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/tools v0.45.0 // indirect + golang.org/x/tools/godoc v0.1.0-deprecated // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 7594b09e..aac789d0 100644 --- a/go.sum +++ b/go.sum @@ -1,23 +1,23 @@ github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= -github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= -github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/Checkmarx/kics/v2 v2.1.3 h1:X+53sjAq9suwzCmZwdGqpBkIgcL1vp5KYqNEbA0woAs= -github.com/Checkmarx/kics/v2 v2.1.3/go.mod h1:66f8m0G8klx4G15sQhuOserSxcni6QpjGiMmXLaIFos= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/Checkmarx/kics/v2 v2.1.20 h1:KuYtGJpMfQeNpjqvssAxsoSXqpNOpXHiog5Eei6ojNw= +github.com/Checkmarx/kics/v2 v2.1.20/go.mod h1:j7HxuuxmBdUDjnSr/TKUAfoa/cyrQieX2HZN0r7jD1g= github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs= github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= -github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= -github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE= -github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= -github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= -github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= -github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= +github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/chroma/v2 v2.26.1 h1:2X21EdxGZNv5GF9mG5u+uzc02GCFyGxbcBm3Grd9A78= +github.com/alecthomas/chroma/v2 v2.26.1/go.mod h1:lxhRRa9H4hPmRLOOdYga4zkQIQjq3dtrrdwQeCfu78Y= +github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= +github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y= github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8= github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= @@ -42,16 +42,28 @@ github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwN github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bradleyjkemp/cupaloy/v2 v2.6.0 h1:knToPYa2xtfg42U3I6punFEjaGFKWQRXJwj0JTv4mTs= github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= -github.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs= -github.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw= -github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg= -github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo= -github.com/charmbracelet/x/ansi v0.5.2 h1:dEa1x2qdOZXD/6439s+wF7xjV+kZLu/iN00GuXXrU9E= -github.com/charmbracelet/x/ansi v0.5.2/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q= +github.com/buger/jsonparser v1.2.0 h1:4EFcvK1kD4jyj6YqNK6skK6w+y7FHHBR+XBCtxwu/6g= +github.com/buger/jsonparser v1.2.0/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.2.3 h1:d9MdMsANIYZB5pE1KkRqaUV6GfsiWm+/9z4fTuGVm9I= +github.com/charmbracelet/bubbletea v1.2.3/go.mod h1:Qr6fVQw+wX7JkWWkVyXYk/ZUQ92a6XNekLXa3rR18MM= +github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= +github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= +github.com/charmbracelet/glamour v1.0.0 h1:AWMLOVFHTsysl4WV8T8QgkQ0s/ZNZo7CiE4WKhk8l08= +github.com/charmbracelet/glamour v1.0.0/go.mod h1:DSdohgOBkMr2ZQNhw4LZxSGpx3SvpeujNoXrQyH2hxo= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= +github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= +github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= +github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= +github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= +github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/slice v0.0.0-20260608090822-c3ad58c6c9e5 h1:K+bEhQCdvynpwszgcTF3MqvP/ouj7ocrZi+24viw0zc= +github.com/charmbracelet/x/exp/slice v0.0.0-20260608090822-c3ad58c6c9e5/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= @@ -61,8 +73,11 @@ github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObk github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= @@ -71,20 +86,22 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= -github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= -github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= -github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= +github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0= +github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -92,12 +109,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos= -github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= +github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= +github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= @@ -108,7 +125,6 @@ github.com/itchyny/gojq v0.12.16 h1:yLfgLxhIr/6sJNVmYfQjTIv0jGctu6/DgDoivmxTr7g= github.com/itchyny/gojq v0.12.16/go.mod h1:6abHbdC2uB9ogMS38XsErnfqJ94UlngIJGlRAIj4jTM= github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q= github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= @@ -117,31 +133,30 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= +github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mailru/easyjson v0.9.2 h1:dX8U45hQsZpxd80nLvDGihsQ/OxlvTkVUXH2r/8cb2M= +github.com/mailru/easyjson v0.9.2/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= -github.com/massdriver-cloud/airlock v0.0.9 h1:t+jTY6nZEiPZNTKx0wEgQTPztIxL4u0RFvVWXn2/RMc= -github.com/massdriver-cloud/airlock v0.0.9/go.mod h1:igJm33JvINiUtbyEspUeKUWyWewG+jYyxO1UDHqLp9Q= -github.com/massdriver-cloud/massdriver-sdk-go v0.2.3 h1:gKRiSbJPI1uWVBRmoexPNbi9IJEYZ2upciMTjxRHb5I= -github.com/massdriver-cloud/massdriver-sdk-go v0.2.3/go.mod h1:6NrSP+wfGQvUOAggsz10/Wkln8CKmk3VBnD+OJzZgFY= -github.com/massdriver-cloud/massdriver-sdk-go v0.2.4 h1:5UqNA0vBkzvqNa3H94SoVlDmxiJiRPltklO/j9/H+F0= -github.com/massdriver-cloud/massdriver-sdk-go v0.2.4/go.mod h1:6NrSP+wfGQvUOAggsz10/Wkln8CKmk3VBnD+OJzZgFY= -github.com/massdriver-cloud/terraform-config-inspect v0.0.1 h1:eLtKFRaklHIxcPvUtZmNacl28n4QIHr29pJzw/u/FKU= -github.com/massdriver-cloud/terraform-config-inspect v0.0.1/go.mod h1:3AbDpWxIRMdMAg7FDmTJuVBhCGNwdm49cBIOmUHjqRg= +github.com/massdriver-cloud/airlock v0.0.10 h1:05wz7kovH09X1VMfHcjLWylYanoLFcxuD0oZi13WG9U= +github.com/massdriver-cloud/airlock v0.0.10/go.mod h1:igJm33JvINiUtbyEspUeKUWyWewG+jYyxO1UDHqLp9Q= +github.com/massdriver-cloud/massdriver-sdk-go v0.2.6 h1:si8MxuvRBTK3cz6IsAaXvtjE8jsxUnL0Q7O4vixAkxw= +github.com/massdriver-cloud/massdriver-sdk-go v0.2.6/go.mod h1:6NrSP+wfGQvUOAggsz10/Wkln8CKmk3VBnD+OJzZgFY= +github.com/massdriver-cloud/terraform-config-inspect v0.0.2 h1:Jc7BrhFHLbK7Epig6ShiEVMzQPPHVIOx0/BatvtEwtY= +github.com/massdriver-cloud/terraform-config-inspect v0.0.2/go.mod h1:3AbDpWxIRMdMAg7FDmTJuVBhCGNwdm49cBIOmUHjqRg= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= +github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= +github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= @@ -149,10 +164,14 @@ github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwX github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg= -github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -161,6 +180,7 @@ github.com/osteele/liquid v1.7.0 h1:VsbPSchE5D5S5scylAIvERET4dnCxsO6IDri2oSJ5Dk= github.com/osteele/liquid v1.7.0/go.mod h1:xU0Z2dn2hOQIEFEWNmeltOmCtfhtoW/2fCyiNQeNG+U= github.com/osteele/tuesday v1.0.3 h1:SrCmo6sWwSgnvs1bivmXLvD7Ko9+aJvvkmDjB5G4FTU= github.com/osteele/tuesday v1.0.3/go.mod h1:pREKpE+L03UFuR+hiznj3q7j3qB1rUZ4XfKejwWFF2M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= @@ -173,9 +193,8 @@ github.com/rodaine/table v1.3.0 h1:4/3S3SVkHnVZX91EHFvAMV7K42AnJ0XuymRR2C5HlGE= github.com/rodaine/table v1.3.0/go.mod h1:47zRsHar4zw0jgxGxL9YtFfs7EGN6B/TaS+/Dmk4WxU= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= -github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= +github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= @@ -185,10 +204,11 @@ github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sosedoff/ansible-vault-go v0.2.0 h1:XqkBdqbXgTuFQ++NdrZvSdUTNozeb6S3V5x7FVs17vg= github.com/sosedoff/ansible-vault-go v0.2.0/go.mod h1:wMU54HNJfY0n0KIgbpA9m15NBfaUDlJrAsaZp0FwzkI= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -200,70 +220,75 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/vektah/gqlparser/v2 v2.5.19 h1:bhCPCX1D4WWzCDvkPl4+TP1N8/kLrWnp43egplt7iSg= -github.com/vektah/gqlparser/v2 v2.5.19/go.mod h1:y7kvl5bBlDeuWIvLtA9849ncyvx6/lj06RsMrEjVy3U= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkWBTJ7k= +github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= -github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -github.com/yuin/goldmark-emoji v1.0.4 h1:vCwMkPZSNefSUnOW2ZKRUjBSD5Ok3W78IXhGxxAEF90= -github.com/yuin/goldmark-emoji v1.0.4/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= -github.com/zclconf/go-cty v1.15.1 h1:RgQYm4j2EvoBRXOPxhUvxPzRrGDo1eCOhHXuGfrj5S0= -github.com/zclconf/go-cty v1.15.1/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= +github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= +github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= +github.com/zclconf/go-cty v1.18.1 h1:yEGE8M4iIZlyKQURZNb2SnEyZlZHUcBCnx6KF81KuwM= +github.com/zclconf/go-cty v1.18.1/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= -golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/exp v0.0.0-20260603202125-055de637280b h1:v1uXiEBHo8QA0LiGCo7UgHMzHT4Kdfpl2zmtH5vaP1Q= +golang.org/x/exp v0.0.0-20260603202125-055de637280b/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk= +golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -275,5 +300,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/api/resource_type.go b/internal/api/resource_type.go index c18d27ba..52b34098 100644 --- a/internal/api/resource_type.go +++ b/internal/api/resource_type.go @@ -50,8 +50,13 @@ const getResourceTypeQuery = `query getResourceType($organizationId: ID!, $id: I } }` -const listResourceTypesQuery = `query listResourceTypes($organizationId: ID!) { - resourceTypes(organizationId: $organizationId) { +// resourceTypesPageSize is the per-request page size for the ListResourceTypes +// page-walk. 100 is the server's documented max, minimizing round-trips; the +// value also keeps the cursor arg non-null (see ListResourceTypes). +const resourceTypesPageSize = 100 + +const listResourceTypesQuery = `query listResourceTypes($organizationId: ID!, $cursor: Cursor) { + resourceTypes(organizationId: $organizationId, cursor: $cursor) { items { id name @@ -60,6 +65,10 @@ const listResourceTypesQuery = `query listResourceTypes($organizationId: ID!) { createdAt updatedAt } + cursor { + next + previous + } } }` @@ -121,27 +130,59 @@ func GetResourceType(ctx context.Context, mdClient *massdriver.Client, name stri return resp.ResourceType, nil } -// ListResourceTypes fetches all resource types in the configured organization. +// ListResourceTypes fetches every resource type in the configured organization. // The legacy CLI supported a filter argument; the few callsites that survive // the v2 migration only need the unfiltered list. +// +// Resource types aren't in the SDK yet, so the cursor page-walk the SDK does for +// its own list endpoints is implemented here by hand: the server returns one +// page at a time, so we follow cursor.next until it's empty and accumulate every +// page. (The prior version requested only `items` with no cursor, silently +// truncating the result to the server's default first page.) func ListResourceTypes(ctx context.Context, mdClient *massdriver.Client) ([]ResourceType, error) { cfg := mdClient.Config() - var resp struct { - ResourceTypes struct { - Items []ResourceType `json:"items"` - } `json:"resourceTypes"` - } - req := &graphql.Request{ - OpName: "listResourceTypes", - Query: listResourceTypesQuery, - Variables: map[string]any{ - "organizationId": cfg.OrganizationID, - }, - } - if err := gqlClient(mdClient).MakeRequest(ctx, req, &graphql.Response{Data: &resp}); err != nil { - return nil, fmt.Errorf("list resource types: %w", err) - } - return resp.ResourceTypes.Items, nil + client := gqlClient(mdClient) + + var all []ResourceType + after := "" + for { + var resp struct { + ResourceTypes struct { + Items []ResourceType `json:"items"` + Cursor struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"cursor"` + } `json:"resourceTypes"` + } + req := &graphql.Request{ + OpName: "listResourceTypes", + Query: listResourceTypesQuery, + Variables: map[string]any{ + "organizationId": cfg.OrganizationID, + // Always send an explicit page size: the server returns 500 on a + // `cursor: null` arg, which is what NewCursor(0, "") would + // produce on the first request. A positive limit makes NewCursor + // emit `{limit, next}` instead. `after` is the prior page's next + // cursor ("" on the first request). + "cursor": scalars.NewCursor(resourceTypesPageSize, after), + }, + } + if err := client.MakeRequest(ctx, req, &graphql.Response{Data: &resp}); err != nil { + return nil, fmt.Errorf("list resource types: %w", err) + } + all = append(all, resp.ResourceTypes.Items...) + + // Stop at the last page. The `next == after` guard is a belt-and-braces + // defense against a server that echoes the same cursor, which would + // otherwise loop forever. + next := resp.ResourceTypes.Cursor.Next + if next == "" || next == after { + break + } + after = next + } + return all, nil } // PublishResourceType registers a resource-type schema. diff --git a/internal/bundle/lint.go b/internal/bundle/lint.go index d642f751..e71b615f 100644 --- a/internal/bundle/lint.go +++ b/internal/bundle/lint.go @@ -272,13 +272,13 @@ func (b *Bundle) LintInputsMatchProvisioner() LintResult { if len(missingMassdriverInputs) > 0 || len(missingProvisionerInputs) > 0 { var sb strings.Builder - sb.WriteString(fmt.Sprintf("missing inputs detected in step %s:\n", step.Path)) + fmt.Fprintf(&sb, "missing inputs detected in step %s:\n", step.Path) for _, p := range missingMassdriverInputs { - sb.WriteString(fmt.Sprintf("\t- input \"%s\" declared in IaC but missing massdriver.yaml declaration\n", p)) + fmt.Fprintf(&sb, "\t- input \"%s\" declared in IaC but missing massdriver.yaml declaration\n", p) } for _, v := range missingProvisionerInputs { - sb.WriteString(fmt.Sprintf("\t- input \"%s\" declared in massdriver.yaml but missing IaC declaration\n", v)) + fmt.Fprintf(&sb, "\t- input \"%s\" declared in massdriver.yaml but missing IaC declaration\n", v) } result.AddWarning("param-mismatch", sb.String()) diff --git a/internal/cli/export_test.go b/internal/cli/export_test.go new file mode 100644 index 00000000..099035e7 --- /dev/null +++ b/internal/cli/export_test.go @@ -0,0 +1,4 @@ +package cli + +// TruncateRows exposes the unexported truncateRows for external tests. +var TruncateRows = truncateRows diff --git a/internal/cli/pager.go b/internal/cli/pager.go new file mode 100644 index 00000000..797405f3 --- /dev/null +++ b/internal/cli/pager.go @@ -0,0 +1,313 @@ +package cli + +import ( + "fmt" + "iter" + "os" + + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/mattn/go-runewidth" + "golang.org/x/term" +) + +// PagerConfig describes how a paginated result set is rendered by [Paginate]. +type PagerConfig[T any] struct { + // Columns are the table header titles. + Columns []string + // Row maps one item to its cells. The returned slice must align with Columns. + Row func(T) []string + // Out is the destination; defaults to os.Stdout when nil. Used both to + // decide whether output is interactive and as the render target. + Out *os.File + // PageSize is how many rows are pulled from the iterator per batch in + // interactive mode. Zero uses a sensible default. + PageSize int +} + +// Paginate renders a paginated iterator. +// +// When Out is an interactive terminal it launches a scrollable table that lazily +// pulls more rows as the cursor approaches the end, so a multi-thousand-row list +// never fetches more than the user actually scrolls through. The user quits with +// q / esc / ctrl-c. +// +// When Out is not a terminal (a pipe, a file, CI), it drains the iterator and +// writes a plain table — keeping scripts and output redirection working. The +// caller is responsible for routing structured (--output json) requests +// elsewhere before reaching here. +func Paginate[T any](seq iter.Seq2[T, error], cfg PagerConfig[T]) error { + out := cfg.Out + if out == nil { + out = os.Stdout + } + if IsInteractive(out) { + return runInteractivePager(seq, cfg, out) + } + return streamAll(seq, cfg, out) +} + +// IsInteractive reports whether f is attached to a terminal. It is the single +// gate used to decide between the interactive pager and plain streaming. +func IsInteractive(f *os.File) bool { + if f == nil { + return false + } + return term.IsTerminal(int(f.Fd())) +} + +// streamAll drains the iterator into a plain table written to out. Used for +// non-interactive destinations where paging UX makes no sense. +func streamAll[T any](seq iter.Seq2[T, error], cfg PagerConfig[T], out *os.File) error { + headers := make([]any, len(cfg.Columns)) + for i, c := range cfg.Columns { + headers[i] = c + } + tbl := NewTable(headers...).WithWriter(out) + for v, err := range seq { + if err != nil { + return err + } + cells := cfg.Row(v) + row := make([]any, len(cells)) + for i, c := range cells { + row[i] = c + } + tbl.AddRow(row...) + } + tbl.Print() + return nil +} + +const defaultPageSize = 25 + +// rowsMsg carries a freshly pulled batch from the iterator into the model's +// Update loop. rows are already item-agnostic ([]string), so the message itself +// need not be generic. +type rowsMsg struct { + rows []table.Row + done bool + err error +} + +// pagerModel is the bubbletea model backing the interactive table. It owns a +// pull function (from iter.Pull2) and appends rows to the embedded table as the +// user scrolls toward the end of what's loaded. +type pagerModel[T any] struct { + table table.Model + next func() (T, error, bool) + rowFn func(T) []string + columns []string + cols []table.Column // applied column widths; used to truncate row cells + pageSize int + + loaded int + done bool // iterator exhausted + loading bool // a load batch is in flight + colsApplied bool // column widths sized from the first batch + quitting bool + err error +} + +func newPagerModel[T any](next func() (T, error, bool), cfg PagerConfig[T]) pagerModel[T] { + pageSize := cfg.PageSize + if pageSize <= 0 { + pageSize = defaultPageSize + } + cols := make([]table.Column, len(cfg.Columns)) + for i, c := range cfg.Columns { + cols[i] = table.Column{Title: c, Width: runewidth.StringWidth(c) + 2} + } + t := table.New( + table.WithColumns(cols), + table.WithFocused(true), + table.WithHeight(20), + ) + styles := table.DefaultStyles() + styles.Header = styles.Header.Bold(true).Foreground(lipgloss.Color("12")).BorderBottom(true) + styles.Selected = styles.Selected.Bold(true).Foreground(lipgloss.Color("15")).Background(lipgloss.Color("12")) + t.SetStyles(styles) + + return pagerModel[T]{ + table: t, + next: next, + rowFn: cfg.Row, + columns: cfg.Columns, + pageSize: pageSize, + loading: true, // Init kicks off the first load + } +} + +func (m pagerModel[T]) Init() tea.Cmd { return m.loadCmd() } + +// loadCmd pulls up to pageSize items from the iterator. It runs in a tea.Cmd +// goroutine; the model's `loading` guard ensures only one is ever in flight, so +// the underlying pull function is never called concurrently. +func (m pagerModel[T]) loadCmd() tea.Cmd { + next, rowFn, n := m.next, m.rowFn, m.pageSize + return func() tea.Msg { + rows := make([]table.Row, 0, n) + for range n { + v, err, ok := next() + if err != nil { + return rowsMsg{err: err} + } + if !ok { + return rowsMsg{rows: rows, done: true} + } + rows = append(rows, table.Row(rowFn(v))) + } + return rowsMsg{rows: rows} + } +} + +func (m pagerModel[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + // Reserve two lines for the footer; keep a sane floor. + h := msg.Height - 2 + if h < 3 { + h = 3 + } + m.table.SetHeight(h) + return m, nil + + case rowsMsg: + m.loading = false + if msg.err != nil { + m.err = msg.err + m.quitting = true + return m, tea.Quit + } + if !m.colsApplied && len(msg.rows) > 0 { + m.cols = sizeColumns(m.columns, msg.rows) + m.table.SetColumns(m.cols) + m.colsApplied = true + } + if len(msg.rows) > 0 { + m.table.SetRows(append(m.table.Rows(), truncateRows(msg.rows, m.cols)...)) + m.loaded = len(m.table.Rows()) + } + if msg.done { + m.done = true + } + // Keep loading until the visible window is filled, so the first screen + // isn't half-empty when pageSize is smaller than the terminal height. + if !m.done && !m.loading && m.loaded < m.table.Height()+m.pageSize { + m.loading = true + return m, m.loadCmd() + } + return m, nil + + case tea.KeyMsg: + switch msg.String() { + case "q", "esc", "ctrl+c": + m.quitting = true + return m, tea.Quit + } + } + + var cmd tea.Cmd + m.table, cmd = m.table.Update(msg) + cmds := []tea.Cmd{cmd} + // Prefetch the next batch once the cursor gets within a page of the end. + if !m.done && !m.loading && m.table.Cursor() >= m.loaded-m.pageSize { + m.loading = true + cmds = append(cmds, m.loadCmd()) + } + return m, tea.Batch(cmds...) +} + +var footerStyle = lipgloss.NewStyle().Faint(true) + +func (m pagerModel[T]) View() string { + if m.quitting { + return "" + } + status := fmt.Sprintf("%d loaded", m.loaded) + switch { + case m.loading: + status += " · loading…" + case m.done: + status += " · end" + } + if m.loaded == 0 && m.done { + return footerStyle.Render("No results.") + "\n" + } + footer := footerStyle.Render(fmt.Sprintf("↑/↓ scroll · pgup/pgdn page · %s · q quit", status)) + return m.table.View() + "\n" + footer +} + +// sizeColumns derives column widths from the headers and a sample batch, so the +// table is laid out from real data on the first page rather than guessed widths. +func sizeColumns(headers []string, sample []table.Row) []table.Column { + const maxWidth = 50 + widths := make([]int, len(headers)) + for i, h := range headers { + widths[i] = runewidth.StringWidth(h) + } + for _, r := range sample { + for i := range headers { + if i < len(r) { + if w := runewidth.StringWidth(r[i]); w > widths[i] { + widths[i] = w + } + } + } + } + cols := make([]table.Column, len(headers)) + for i, h := range headers { + w := widths[i] + if w > maxWidth { + w = maxWidth + } + cols[i] = table.Column{Title: h, Width: w + 2} + } + return cols +} + +// truncateRows clips each cell to its column's data width, appending "…" when +// the value was longer. Without this, rows from later batches (or any cell wider +// than the sampled max) would overflow the column and push the table past the +// terminal edge. +func truncateRows(rows []table.Row, cols []table.Column) []table.Row { + if len(cols) == 0 { + return rows + } + out := make([]table.Row, len(rows)) + for i, r := range rows { + nr := make(table.Row, len(r)) + for j, cell := range r { + if j < len(cols) { + w := cols[j].Width - 2 // strip the +2 padding sizeColumns added + if w < 1 { + w = 1 + } + nr[j] = runewidth.Truncate(cell, w, "…") + } else { + nr[j] = cell + } + } + out[i] = nr + } + return out +} + +// runInteractivePager drives the bubbletea program. It owns the iter.Pull2 +// lifetime so the underlying iterator goroutine is always released on exit. +func runInteractivePager[T any](seq iter.Seq2[T, error], cfg PagerConfig[T], out *os.File) error { + next, stop := iter.Pull2(seq) + defer stop() + + m := newPagerModel(next, cfg) + p := tea.NewProgram(m, tea.WithOutput(out), tea.WithAltScreen()) + final, err := p.Run() + if err != nil { + return err + } + if fm, ok := final.(pagerModel[T]); ok && fm.err != nil { + return fm.err + } + return nil +} diff --git a/internal/cli/pager_test.go b/internal/cli/pager_test.go new file mode 100644 index 00000000..40c93a45 --- /dev/null +++ b/internal/cli/pager_test.go @@ -0,0 +1,145 @@ +package cli_test + +import ( + "errors" + "iter" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/charmbracelet/bubbles/table" + "github.com/massdriver-cloud/mass/internal/cli" +) + +type pagerRow struct { + id string + name string +} + +// seqOf builds an iter.Seq2 over items. If errAt >= 0, it yields an error in +// place of the item at that index and stops, mimicking a failed page fetch. +func seqOf(items []pagerRow, errAt int) iter.Seq2[pagerRow, error] { + return func(yield func(pagerRow, error) bool) { + for i, it := range items { + if i == errAt { + yield(pagerRow{}, errors.New("page fetch failed")) + return + } + if !yield(it, nil) { + return + } + } + } +} + +func pagerConfig(out *os.File) cli.PagerConfig[pagerRow] { + return cli.PagerConfig[pagerRow]{ + Columns: []string{"ID", "Name"}, + Row: func(r pagerRow) []string { return []string{r.id, r.name} }, + Out: out, + } +} + +// tempFile gives a non-terminal *os.File so Paginate takes the streamAll path. +func tempFile(t *testing.T) *os.File { + t.Helper() + f, err := os.Create(filepath.Join(t.TempDir(), "out.txt")) + if err != nil { + t.Fatalf("create temp file: %v", err) + } + t.Cleanup(func() { f.Close() }) + return f +} + +func TestPaginateNonInteractiveStreamsAllRows(t *testing.T) { + out := tempFile(t) + items := []pagerRow{ + {"proj-1", "Alpha"}, + {"proj-2", "Bravo"}, + {"proj-3", "Charlie"}, + } + + if err := cli.Paginate(seqOf(items, -1), pagerConfig(out)); err != nil { + t.Fatalf("Paginate returned error: %v", err) + } + + got, err := os.ReadFile(out.Name()) + if err != nil { + t.Fatalf("read output: %v", err) + } + rendered := string(got) + for _, want := range []string{"ID", "Name", "proj-1", "Alpha", "proj-3", "Charlie"} { + if !strings.Contains(rendered, want) { + t.Errorf("output missing %q\n--- output ---\n%s", want, rendered) + } + } +} + +func TestPaginateNonInteractivePropagatesError(t *testing.T) { + out := tempFile(t) + items := []pagerRow{{"proj-1", "Alpha"}, {"proj-2", "Bravo"}} + + err := cli.Paginate(seqOf(items, 1), pagerConfig(out)) + if err == nil { + t.Fatal("expected error from failed page fetch, got nil") + } + if !strings.Contains(err.Error(), "page fetch failed") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestTruncateRowsClipsWideCellsAndPreservesNarrow(t *testing.T) { + // Column widths include the +2 padding sizeColumns adds, so usable data + // widths are 4 and 6 here. + cols := []table.Column{ + {Title: "ID", Width: 6}, + {Title: "Name", Width: 8}, + } + rows := []table.Row{ + {"short", "ok"}, // ID overflows by 1, Name fits + {"abc", "this name is way too long"}, // Name overflows + {"fits", "fine"}, // both fit + } + + got := cli.TruncateRows(rows, cols) + if len(got) != len(rows) { + t.Fatalf("expected %d rows, got %d", len(rows), len(got)) + } + + checkWidth := func(t *testing.T, cell string, maxWidth int) { + t.Helper() + // Each rune of "…" is one cell wide; runewidth.Truncate guarantees + // output width <= maxWidth. + if w := len([]rune(cell)); w > maxWidth { + t.Errorf("cell %q has rune-width %d, exceeds limit %d", cell, w, maxWidth) + } + } + + checkWidth(t, got[0][0], 4) + checkWidth(t, got[1][1], 6) + + if !strings.HasSuffix(got[1][1], "…") { + t.Errorf("expected overflowed cell to end with ellipsis, got %q", got[1][1]) + } + if got[2][0] != "fits" || got[2][1] != "fine" { + t.Errorf("non-overflowing row mutated: %v", got[2]) + } +} + +func TestTruncateRowsHandlesNoColumns(t *testing.T) { + rows := []table.Row{{"a", "b"}} + got := cli.TruncateRows(rows, nil) + if len(got) != 1 || got[0][0] != "a" || got[0][1] != "b" { + t.Errorf("expected rows passthrough when cols is nil, got %v", got) + } +} + +func TestIsInteractiveFalseForFile(t *testing.T) { + if cli.IsInteractive(tempFile(t)) { + t.Error("a regular file should not be reported as interactive") + } + if cli.IsInteractive(nil) { + t.Error("nil file should not be reported as interactive") + } +} diff --git a/internal/commands/bundle/new.go b/internal/commands/bundle/new.go index 8f823ea9..3d37bccc 100644 --- a/internal/commands/bundle/new.go +++ b/internal/commands/bundle/new.go @@ -82,11 +82,11 @@ params: } else { sb.WriteString(" required:\n") for _, conn := range data.Connections { - sb.WriteString(fmt.Sprintf(" - %s\n", conn.Name)) + fmt.Fprintf(&sb, " - %s\n", conn.Name) } sb.WriteString(" properties:\n") for _, conn := range data.Connections { - sb.WriteString(fmt.Sprintf(" %s:\n $ref: %s\n", conn.Name, conn.ResourceType)) + fmt.Fprintf(&sb, " %s:\n $ref: %s\n", conn.Name, conn.ResourceType) } } yaml += sb.String() diff --git a/internal/commands/environment/export.go b/internal/commands/environment/export.go index 44497789..789258d7 100644 --- a/internal/commands/environment/export.go +++ b/internal/commands/environment/export.go @@ -31,7 +31,7 @@ func ExportEnvironment(ctx context.Context, mdClient *massdriver.Client, env *ty // Instances aren't embedded on the environment record any more; pull them // for this env via the instances service. - slim, err := mdClient.Instances.List(ctx, instances.ListInput{EnvironmentID: env.ID}) + slim, err := types.Collect(mdClient.Instances.Iter(ctx, instances.ListInput{EnvironmentID: env.ID})) if err != nil { return fmt.Errorf("failed to list instances for environment %s: %w", env.ID, err) } diff --git a/internal/commands/environment/follow.go b/internal/commands/environment/follow.go index 1d996e8b..762c230f 100644 --- a/internal/commands/environment/follow.go +++ b/internal/commands/environment/follow.go @@ -42,7 +42,7 @@ func NewFollowAPI(c *massdriver.Client) FollowAPI { return sdkFollowAPI{c: c} } type sdkFollowAPI struct{ c *massdriver.Client } func (s sdkFollowAPI) ListInstances(ctx context.Context, environmentID string) ([]types.Instance, error) { - return s.c.Instances.List(ctx, instances.ListInput{EnvironmentID: environmentID}) + return types.Collect(s.c.Instances.Iter(ctx, instances.ListInput{EnvironmentID: environmentID})) } func (s sdkFollowAPI) StreamEnvironmentEvents(ctx context.Context, environmentID string) (<-chan types.Event, error) { diff --git a/internal/resourcetype/dereference.go b/internal/resourcetype/dereference.go index 23210b79..5604d640 100644 --- a/internal/resourcetype/dereference.go +++ b/internal/resourcetype/dereference.go @@ -204,7 +204,7 @@ func dereferenceFilePathRef(hydratedSchema map[string]any, schema map[string]any func getValue(anyVal any) reflect.Value { val := reflect.ValueOf(anyVal) - if val.Kind() == reflect.Ptr { + if val.Kind() == reflect.Pointer { val = val.Elem() } diff --git a/internal/resourcetype/get_test.go b/internal/resourcetype/get_test.go index 3e1a599d..104f7cc4 100644 --- a/internal/resourcetype/get_test.go +++ b/internal/resourcetype/get_test.go @@ -68,3 +68,72 @@ func TestGet(t *testing.T) { }) } } + +// TestListWalksPages verifies that List follows the API's cursor pagination and +// accumulates every page, rather than returning only the server's first page. +func TestListWalksPages(t *testing.T) { + page := func(items []map[string]any, next string) map[string]any { + return map[string]any{ + "resourceTypes": map[string]any{ + "items": items, + "cursor": map[string]any{ + "next": next, + "previous": "", + }, + }, + } + } + + mock := gqltest.NewClient( + gqltest.RespondWithData(page([]map[string]any{ + {"id": "rt-1", "name": "aws/vpc"}, + {"id": "rt-2", "name": "aws/s3"}, + }, "cursor-2")), + gqltest.RespondWithData(page([]map[string]any{ + {"id": "rt-3", "name": "gcp/bucket"}, + }, "")), + ) + t.Cleanup(api.SetTransportForTest(mock)) + mdClient, err := massdriver.NewClient( + massdriver.WithGQLClient(mock), + massdriver.WithOrganizationID("test-org"), + ) + if err != nil { + t.Fatal(err) + } + + got, err := resourcetype.List(t.Context(), mdClient) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // All three resource types, across both pages, must be accumulated. + wantIDs := []string{"rt-1", "rt-2", "rt-3"} + if len(got) != len(wantIDs) { + t.Fatalf("got %d resource types, want %d (page walk should accumulate all pages): %+v", len(got), len(wantIDs), got) + } + for i, id := range wantIDs { + if got[i].ID != id { + t.Errorf("resource type %d: got id %q, want %q", i, got[i].ID, id) + } + } + + // Two requests, each carrying an explicit page-size limit (a null cursor + // 500s the server). The first has no `next`; the second carries the prior + // page's next cursor. + reqs := mock.Requests() + if len(reqs) != 2 { + t.Fatalf("got %d requests, want 2 (should follow cursor.next)", len(reqs)) + } + cursor1, ok := reqs[0].Variables["cursor"].(map[string]any) + if !ok || cursor1["limit"] == nil || cursor1["next"] != nil { + t.Errorf("first request should send a limit and no next, got %v", reqs[0].Variables["cursor"]) + } + cursor2, ok := reqs[1].Variables["cursor"].(map[string]any) + if !ok || cursor2["next"] != "cursor-2" { + t.Errorf("second request should carry next=cursor-2, got %v", reqs[1].Variables["cursor"]) + } + if pending := mock.Pending(); pending != 0 { + t.Errorf("expected all queued responses consumed, %d pending", pending) + } +} diff --git a/internal/templates/file_manager.go b/internal/templates/file_manager.go index b4a8589c..57336074 100644 --- a/internal/templates/file_manager.go +++ b/internal/templates/file_manager.go @@ -102,6 +102,7 @@ func (f *fileManager) renderFile(template []byte) ([]byte, error) { } func (f *fileManager) writeToFile(outputPath string, outBytes []byte) error { + // #nosec G703 -- outputPath is intentionally derived from the user-supplied write directory; writing rendered template files there is this function's purpose return os.WriteFile(outputPath, outBytes, 0600) }