From b65ce75c1f8de521b1b988e409d6131c771ffc33 Mon Sep 17 00:00:00 2001 From: Miles Yucht Date: Wed, 21 Feb 2024 15:16:36 +0100 Subject: [PATCH] Use Go SDK Iterators when listing resources with the CLI (#1202) ## Changes Currently, when the CLI run a list API call (like list jobs), it uses the `List*All` methods from the SDK, which list all resources in the collection. This is very slow for large collections: if you need to list all jobs from a workspace that has 10,000+ jobs, you'll be waiting for at least 100 RPCs to complete before seeing any output. Instead of using List*All() methods, the SDK recently added an iterator data structure that allows traversing the collection without needing to completely list it first. New pages are fetched lazily if the next requested item belongs to the next page. Using the List() methods that return these iterators, the CLI can proactively print out some of the response before the complete collection has been fetched. This involves a pretty major rewrite of the rendering logic in `cmdio`. The idea there is to define custom rendering logic based on the type of the provided resource. There are three renderer interfaces: 1. textRenderer: supports printing something in a textual format (i.e. not JSON, and not templated). 2. jsonRenderer: supports printing something in a pretty-printed JSON format. 3. templateRenderer: supports printing something using a text template. There are also three renderer implementations: 1. readerRenderer: supports printing a reader. This only implements the textRenderer interface. 2. iteratorRenderer: supports printing a `listing.Iterator` from the Go SDK. This implements jsonRenderer and templateRenderer, buffering 20 resources at a time before writing them to the output. 3. defaultRenderer: supports printing arbitrary resources (the previous implementation). Callers will either use `cmdio.Render()` for rendering individual resources or `io.Reader` or `cmdio.RenderIterator()` for rendering an iterator. This separate method is needed to safely be able to match on the type of the iterator, since Go does not allow runtime type matches on generic types with an existential type parameter. One other change that needs to happen is to split the templates used for text representation of list resources into a header template and a row template. The template is now executed multiple times for List API calls, but the header should only be printed once. To support this, I have added `headerTemplate` to `cmdIO`, and I have also changed `RenderWithTemplate` to include a `headerTemplate` parameter everywhere. ## Tests - [x] Unit tests for text rendering logic - [x] Unit test for reflection-based iterator construction. --------- Co-authored-by: Andrew Nester --- .codegen/service.go.tmpl | 12 +- bundle/schema/docs/bundle_descriptions.json | 92 +++--- cmd/account/billable-usage/billable-usage.go | 2 +- cmd/account/budgets/budgets.go | 7 +- .../custom-app-integration.go | 7 +- cmd/account/groups/groups.go | 7 +- .../ip-access-lists/ip-access-lists.go | 7 +- cmd/account/log-delivery/log-delivery.go | 7 +- .../metastore-assignments.go | 7 +- cmd/account/metastores/metastores.go | 7 +- .../network-connectivity.go | 14 +- .../o-auth-published-apps.go | 7 +- .../published-app-integration.go | 7 +- .../service-principal-secrets.go | 7 +- .../service-principals/service-principals.go | 7 +- cmd/account/users/users.go | 7 +- .../workspace-assignment.go | 7 +- cmd/fs/cat.go | 2 +- cmd/fs/cp.go | 4 +- cmd/fs/ls.go | 4 +- cmd/labs/project/proxy.go | 2 +- cmd/root/io.go | 5 +- cmd/workspace/catalogs/catalogs.go | 7 +- cmd/workspace/catalogs/overrides.go | 3 +- cmd/workspace/clean-rooms/clean-rooms.go | 7 +- .../cluster-policies/cluster-policies.go | 7 +- cmd/workspace/clusters/clusters.go | 14 +- cmd/workspace/clusters/overrides.go | 3 +- cmd/workspace/connections/connections.go | 7 +- cmd/workspace/dashboards/dashboards.go | 7 +- cmd/workspace/dashboards/overrides.go | 3 +- cmd/workspace/experiments/experiments.go | 35 +-- .../external-locations/external-locations.go | 7 +- cmd/workspace/external-locations/overrides.go | 3 +- cmd/workspace/functions/functions.go | 7 +- .../git-credentials/git-credentials.go | 7 +- .../global-init-scripts.go | 7 +- cmd/workspace/groups/groups.go | 7 +- .../instance-pools/instance-pools.go | 7 +- .../instance-profiles/instance-profiles.go | 7 +- .../ip-access-lists/ip-access-lists.go | 7 +- cmd/workspace/jobs/jobs.go | 14 +- cmd/workspace/jobs/overrides.go | 3 +- cmd/workspace/libraries/libraries.go | 7 +- cmd/workspace/metastores/metastores.go | 7 +- cmd/workspace/metastores/overrides.go | 3 +- .../model-registry/model-registry.go | 42 +-- .../model-versions/model-versions.go | 7 +- cmd/workspace/pipelines/pipelines.go | 14 +- .../policy-families/policy-families.go | 7 +- cmd/workspace/providers/providers.go | 14 +- cmd/workspace/queries/overrides.go | 3 +- cmd/workspace/queries/queries.go | 7 +- cmd/workspace/query-history/query-history.go | 7 +- cmd/workspace/recipients/recipients.go | 7 +- .../registered-models/registered-models.go | 7 +- cmd/workspace/repos/repos.go | 7 +- cmd/workspace/schemas/overrides.go | 3 +- cmd/workspace/schemas/schemas.go | 7 +- cmd/workspace/secrets/overrides.go | 6 +- cmd/workspace/secrets/secrets.go | 21 +- .../service-principals/service-principals.go | 7 +- .../serving-endpoints/serving-endpoints.go | 7 +- cmd/workspace/shares/shares.go | 7 +- .../storage-credentials/overrides.go | 3 +- .../storage-credentials.go | 7 +- .../system-schemas/system-schemas.go | 7 +- cmd/workspace/tables/overrides.go | 3 +- cmd/workspace/tables/tables.go | 14 +- cmd/workspace/token-management/overrides.go | 3 +- .../token-management/token-management.go | 7 +- cmd/workspace/tokens/overrides.go | 3 +- cmd/workspace/tokens/tokens.go | 7 +- cmd/workspace/users/users.go | 7 +- .../vector-search-endpoints.go | 7 +- .../vector-search-indexes.go | 7 +- cmd/workspace/volumes/volumes.go | 7 +- cmd/workspace/warehouses/overrides.go | 3 +- cmd/workspace/warehouses/warehouses.go | 7 +- cmd/workspace/workspace/export_dir.go | 4 +- cmd/workspace/workspace/import_dir.go | 4 +- cmd/workspace/workspace/overrides.go | 3 +- cmd/workspace/workspace/workspace.go | 7 +- internal/bundle/helpers.go | 2 +- libs/cmdio/io.go | 70 +---- libs/cmdio/render.go | 261 +++++++++++++++++- libs/cmdio/render_test.go | 190 +++++++++++++ .../databrickscfg/cfgpickers/clusters_test.go | 4 +- libs/template/helpers_test.go | 2 +- 89 files changed, 714 insertions(+), 519 deletions(-) create mode 100644 libs/cmdio/render_test.go diff --git a/.codegen/service.go.tmpl b/.codegen/service.go.tmpl index a0cd0219..ad25135a 100644 --- a/.codegen/service.go.tmpl +++ b/.codegen/service.go.tmpl @@ -300,16 +300,22 @@ func init() { // end service {{.Name}}{{end}} {{- define "method-call" -}} - {{if .Response}}response, err :={{else}}err ={{end}} {{if .Service.IsAccounts}}a{{else}}w{{end}}.{{(.Service.TrimPrefix "account").PascalName}}.{{.PascalName}}{{if .Pagination}}All{{end}}(ctx{{if .Request}}, {{.CamelName}}Req{{end}}) + {{if .Response -}} + response{{ if not .Pagination}}, err{{end}} := + {{- else -}} + err = + {{- end}} {{if .Service.IsAccounts}}a{{else}}w{{end}}.{{(.Service.TrimPrefix "account").PascalName}}.{{.PascalName}}(ctx{{if .Request}}, {{.CamelName}}Req{{end}}) + {{- if not (and .Response .Pagination) }} if err != nil { return err } + {{- end}} {{ if .Response -}} {{- if .IsResponseByteStream -}} defer response.{{.ResponseBodyField.PascalName}}.Close() - return cmdio.RenderReader(ctx, response.{{.ResponseBodyField.PascalName}}) + return cmdio.Render{{ if .Pagination}}Iterator{{end}}(ctx, response.{{.ResponseBodyField.PascalName}}) {{- else -}} - return cmdio.Render(ctx, response) + return cmdio.Render{{ if .Pagination}}Iterator{{end}}(ctx, response) {{- end -}} {{ else -}} return nil diff --git a/bundle/schema/docs/bundle_descriptions.json b/bundle/schema/docs/bundle_descriptions.json index 5b63bb6d..982dd4eb 100644 --- a/bundle/schema/docs/bundle_descriptions.json +++ b/bundle/schema/docs/bundle_descriptions.json @@ -1,8 +1,8 @@ { - "description": "Root of the bundle config", + "description": "", "properties": { "artifacts": { - "description": "A description of all code artifacts in this bundle.", + "description": "", "additionalproperties": { "description": "", "properties": { @@ -33,7 +33,7 @@ } }, "bundle": { - "description": "The details for this bundle.", + "description": "", "properties": { "compute_id": { "description": "" @@ -58,7 +58,7 @@ } }, "name": { - "description": "The name of the bundle." + "description": "" } } }, @@ -77,7 +77,7 @@ } }, "include": { - "description": "A list of glob patterns of files to load and merge into the this configuration. Defaults to no files being included.", + "description": "", "items": { "description": "" } @@ -193,7 +193,7 @@ "description": "An optional continuous property for this job. The continuous property will ensure that there is always one run executing. Only one of `schedule` and `continuous` can be used.", "properties": { "pause_status": { - "description": "Whether this trigger is paused or not." + "description": "Indicate whether this schedule is paused or not." } } }, @@ -322,7 +322,7 @@ "description": "A unique name for the job cluster. This field is required and must be unique within the job.\n`JobTaskSettings` may refer to this field to determine which cluster to launch for the task execution." }, "new_cluster": { - "description": "If new_cluster, a description of a cluster that is created for each task.", + "description": "If new_cluster, a description of a cluster that is created for only for this task.", "properties": { "apply_policy_default_values": { "description": "" @@ -725,7 +725,7 @@ "description": "An optional periodic schedule for this job. The default behavior is that the job only runs when triggered by clicking “Run Now” in the Jobs UI or sending an API request to `runNow`.", "properties": { "pause_status": { - "description": "Whether this trigger is paused or not." + "description": "Indicate whether this schedule is paused or not." }, "quartz_cron_expression": { "description": "A Cron expression using Quartz syntax that describes the schedule for a job.\nSee [Cron Trigger](http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html)\nfor details. This field is required.\"\n" @@ -785,7 +785,7 @@ "description": "Optional schema to write to. This parameter is only used when a warehouse_id is also provided. If not provided, the `default` schema is used." }, "source": { - "description": "Optional location type of the SQL file. When set to `WORKSPACE`, the SQL file will be retrieved\nfrom the local \u003cDatabricks\u003e workspace. When set to `GIT`, the SQL file will be retrieved from a Git repository\ndefined in `git_source`. If the value is empty, the task will use `GIT` if `git_source` is defined and `WORKSPACE` otherwise.\n\n* `WORKSPACE`: SQL file is located in \u003cDatabricks\u003e workspace.\n* `GIT`: SQL file is located in cloud Git provider.\n" + "description": "Optional location type of the project directory. When set to `WORKSPACE`, the project will be retrieved\nfrom the local \u003cDatabricks\u003e workspace. When set to `GIT`, the project will be retrieved from a Git repository\ndefined in `git_source`. If the value is empty, the task will use `GIT` if `git_source` is defined and `WORKSPACE` otherwise.\n\n* `WORKSPACE`: Project is located in \u003cDatabricks\u003e workspace.\n* `GIT`: Project is located in cloud Git provider.\n" }, "warehouse_id": { "description": "ID of the SQL warehouse to connect to. If provided, we automatically generate and provide the profile and connection details to dbt. It can be overridden on a per-command basis by using the `--profiles-dir` command line argument." @@ -930,7 +930,7 @@ "description": "An optional minimal interval in milliseconds between the start of the failed run and the subsequent retry run. The default behavior is that unsuccessful runs are immediately retried." }, "new_cluster": { - "description": "If new_cluster, a description of a cluster that is created for each task.", + "description": "If new_cluster, a description of a cluster that is created for only for this task.", "properties": { "apply_policy_default_values": { "description": "" @@ -1269,7 +1269,7 @@ "description": "The path of the notebook to be run in the Databricks workspace or remote repository.\nFor notebooks stored in the Databricks workspace, the path must be absolute and begin with a slash.\nFor notebooks stored in a remote repository, the path must be relative. This field is required.\n" }, "source": { - "description": "Optional location type of the SQL file. When set to `WORKSPACE`, the SQL file will be retrieved\nfrom the local \u003cDatabricks\u003e workspace. When set to `GIT`, the SQL file will be retrieved from a Git repository\ndefined in `git_source`. If the value is empty, the task will use `GIT` if `git_source` is defined and `WORKSPACE` otherwise.\n\n* `WORKSPACE`: SQL file is located in \u003cDatabricks\u003e workspace.\n* `GIT`: SQL file is located in cloud Git provider.\n" + "description": "Optional location type of the project directory. When set to `WORKSPACE`, the project will be retrieved\nfrom the local \u003cDatabricks\u003e workspace. When set to `GIT`, the project will be retrieved from a Git repository\ndefined in `git_source`. If the value is empty, the task will use `GIT` if `git_source` is defined and `WORKSPACE` otherwise.\n\n* `WORKSPACE`: Project is located in \u003cDatabricks\u003e workspace.\n* `GIT`: Project is located in cloud Git provider.\n" } } }, @@ -1371,7 +1371,7 @@ "description": "The Python file to be executed. Cloud file URIs (such as dbfs:/, s3:/, adls:/, gcs:/) and workspace paths are supported. For python files stored in the Databricks workspace, the path must be absolute and begin with `/`. For files stored in a remote repository, the path must be relative. This field is required." }, "source": { - "description": "Optional location type of the SQL file. When set to `WORKSPACE`, the SQL file will be retrieved\nfrom the local \u003cDatabricks\u003e workspace. When set to `GIT`, the SQL file will be retrieved from a Git repository\ndefined in `git_source`. If the value is empty, the task will use `GIT` if `git_source` is defined and `WORKSPACE` otherwise.\n\n* `WORKSPACE`: SQL file is located in \u003cDatabricks\u003e workspace.\n* `GIT`: SQL file is located in cloud Git provider.\n" + "description": "Optional location type of the project directory. When set to `WORKSPACE`, the project will be retrieved\nfrom the local \u003cDatabricks\u003e workspace. When set to `GIT`, the project will be retrieved from a Git repository\ndefined in `git_source`. If the value is empty, the task will use `GIT` if `git_source` is defined and `WORKSPACE` otherwise.\n\n* `WORKSPACE`: Project is located in \u003cDatabricks\u003e workspace.\n* `GIT`: Project is located in cloud Git provider.\n" } } }, @@ -1449,7 +1449,7 @@ "description": "Path of the SQL file. Must be relative if the source is a remote Git repository and absolute for workspace paths." }, "source": { - "description": "Optional location type of the SQL file. When set to `WORKSPACE`, the SQL file will be retrieved\nfrom the local \u003cDatabricks\u003e workspace. When set to `GIT`, the SQL file will be retrieved from a Git repository\ndefined in `git_source`. If the value is empty, the task will use `GIT` if `git_source` is defined and `WORKSPACE` otherwise.\n\n* `WORKSPACE`: SQL file is located in \u003cDatabricks\u003e workspace.\n* `GIT`: SQL file is located in cloud Git provider.\n" + "description": "Optional location type of the project directory. When set to `WORKSPACE`, the project will be retrieved\nfrom the local \u003cDatabricks\u003e workspace. When set to `GIT`, the project will be retrieved from a Git repository\ndefined in `git_source`. If the value is empty, the task will use `GIT` if `git_source` is defined and `WORKSPACE` otherwise.\n\n* `WORKSPACE`: Project is located in \u003cDatabricks\u003e workspace.\n* `GIT`: Project is located in cloud Git provider.\n" } } }, @@ -1551,7 +1551,7 @@ } }, "pause_status": { - "description": "Whether this trigger is paused or not." + "description": "Indicate whether this schedule is paused or not." }, "table": { "description": "Table trigger settings.", @@ -2535,7 +2535,7 @@ "description": "", "properties": { "artifacts": { - "description": "A description of all code artifacts in this bundle.", + "description": "", "additionalproperties": { "description": "", "properties": { @@ -2566,7 +2566,7 @@ } }, "bundle": { - "description": "The details for this bundle.", + "description": "", "properties": { "compute_id": { "description": "" @@ -2591,7 +2591,7 @@ } }, "name": { - "description": "The name of the bundle." + "description": "" } } }, @@ -2726,7 +2726,7 @@ "description": "An optional continuous property for this job. The continuous property will ensure that there is always one run executing. Only one of `schedule` and `continuous` can be used.", "properties": { "pause_status": { - "description": "Whether this trigger is paused or not." + "description": "Indicate whether this schedule is paused or not." } } }, @@ -2855,7 +2855,7 @@ "description": "A unique name for the job cluster. This field is required and must be unique within the job.\n`JobTaskSettings` may refer to this field to determine which cluster to launch for the task execution." }, "new_cluster": { - "description": "If new_cluster, a description of a cluster that is created for each task.", + "description": "If new_cluster, a description of a cluster that is created for only for this task.", "properties": { "apply_policy_default_values": { "description": "" @@ -3258,7 +3258,7 @@ "description": "An optional periodic schedule for this job. The default behavior is that the job only runs when triggered by clicking “Run Now” in the Jobs UI or sending an API request to `runNow`.", "properties": { "pause_status": { - "description": "Whether this trigger is paused or not." + "description": "Indicate whether this schedule is paused or not." }, "quartz_cron_expression": { "description": "A Cron expression using Quartz syntax that describes the schedule for a job.\nSee [Cron Trigger](http://www.quartz-scheduler.org/documentation/quartz-2.3.0/tutorials/crontrigger.html)\nfor details. This field is required.\"\n" @@ -3318,7 +3318,7 @@ "description": "Optional schema to write to. This parameter is only used when a warehouse_id is also provided. If not provided, the `default` schema is used." }, "source": { - "description": "Optional location type of the SQL file. When set to `WORKSPACE`, the SQL file will be retrieved\nfrom the local \u003cDatabricks\u003e workspace. When set to `GIT`, the SQL file will be retrieved from a Git repository\ndefined in `git_source`. If the value is empty, the task will use `GIT` if `git_source` is defined and `WORKSPACE` otherwise.\n\n* `WORKSPACE`: SQL file is located in \u003cDatabricks\u003e workspace.\n* `GIT`: SQL file is located in cloud Git provider.\n" + "description": "Optional location type of the project directory. When set to `WORKSPACE`, the project will be retrieved\nfrom the local \u003cDatabricks\u003e workspace. When set to `GIT`, the project will be retrieved from a Git repository\ndefined in `git_source`. If the value is empty, the task will use `GIT` if `git_source` is defined and `WORKSPACE` otherwise.\n\n* `WORKSPACE`: Project is located in \u003cDatabricks\u003e workspace.\n* `GIT`: Project is located in cloud Git provider.\n" }, "warehouse_id": { "description": "ID of the SQL warehouse to connect to. If provided, we automatically generate and provide the profile and connection details to dbt. It can be overridden on a per-command basis by using the `--profiles-dir` command line argument." @@ -3463,7 +3463,7 @@ "description": "An optional minimal interval in milliseconds between the start of the failed run and the subsequent retry run. The default behavior is that unsuccessful runs are immediately retried." }, "new_cluster": { - "description": "If new_cluster, a description of a cluster that is created for each task.", + "description": "If new_cluster, a description of a cluster that is created for only for this task.", "properties": { "apply_policy_default_values": { "description": "" @@ -3802,7 +3802,7 @@ "description": "The path of the notebook to be run in the Databricks workspace or remote repository.\nFor notebooks stored in the Databricks workspace, the path must be absolute and begin with a slash.\nFor notebooks stored in a remote repository, the path must be relative. This field is required.\n" }, "source": { - "description": "Optional location type of the SQL file. When set to `WORKSPACE`, the SQL file will be retrieved\nfrom the local \u003cDatabricks\u003e workspace. When set to `GIT`, the SQL file will be retrieved from a Git repository\ndefined in `git_source`. If the value is empty, the task will use `GIT` if `git_source` is defined and `WORKSPACE` otherwise.\n\n* `WORKSPACE`: SQL file is located in \u003cDatabricks\u003e workspace.\n* `GIT`: SQL file is located in cloud Git provider.\n" + "description": "Optional location type of the project directory. When set to `WORKSPACE`, the project will be retrieved\nfrom the local \u003cDatabricks\u003e workspace. When set to `GIT`, the project will be retrieved from a Git repository\ndefined in `git_source`. If the value is empty, the task will use `GIT` if `git_source` is defined and `WORKSPACE` otherwise.\n\n* `WORKSPACE`: Project is located in \u003cDatabricks\u003e workspace.\n* `GIT`: Project is located in cloud Git provider.\n" } } }, @@ -3904,7 +3904,7 @@ "description": "The Python file to be executed. Cloud file URIs (such as dbfs:/, s3:/, adls:/, gcs:/) and workspace paths are supported. For python files stored in the Databricks workspace, the path must be absolute and begin with `/`. For files stored in a remote repository, the path must be relative. This field is required." }, "source": { - "description": "Optional location type of the SQL file. When set to `WORKSPACE`, the SQL file will be retrieved\nfrom the local \u003cDatabricks\u003e workspace. When set to `GIT`, the SQL file will be retrieved from a Git repository\ndefined in `git_source`. If the value is empty, the task will use `GIT` if `git_source` is defined and `WORKSPACE` otherwise.\n\n* `WORKSPACE`: SQL file is located in \u003cDatabricks\u003e workspace.\n* `GIT`: SQL file is located in cloud Git provider.\n" + "description": "Optional location type of the project directory. When set to `WORKSPACE`, the project will be retrieved\nfrom the local \u003cDatabricks\u003e workspace. When set to `GIT`, the project will be retrieved from a Git repository\ndefined in `git_source`. If the value is empty, the task will use `GIT` if `git_source` is defined and `WORKSPACE` otherwise.\n\n* `WORKSPACE`: Project is located in \u003cDatabricks\u003e workspace.\n* `GIT`: Project is located in cloud Git provider.\n" } } }, @@ -3982,7 +3982,7 @@ "description": "Path of the SQL file. Must be relative if the source is a remote Git repository and absolute for workspace paths." }, "source": { - "description": "Optional location type of the SQL file. When set to `WORKSPACE`, the SQL file will be retrieved\nfrom the local \u003cDatabricks\u003e workspace. When set to `GIT`, the SQL file will be retrieved from a Git repository\ndefined in `git_source`. If the value is empty, the task will use `GIT` if `git_source` is defined and `WORKSPACE` otherwise.\n\n* `WORKSPACE`: SQL file is located in \u003cDatabricks\u003e workspace.\n* `GIT`: SQL file is located in cloud Git provider.\n" + "description": "Optional location type of the project directory. When set to `WORKSPACE`, the project will be retrieved\nfrom the local \u003cDatabricks\u003e workspace. When set to `GIT`, the project will be retrieved from a Git repository\ndefined in `git_source`. If the value is empty, the task will use `GIT` if `git_source` is defined and `WORKSPACE` otherwise.\n\n* `WORKSPACE`: Project is located in \u003cDatabricks\u003e workspace.\n* `GIT`: Project is located in cloud Git provider.\n" } } }, @@ -4084,7 +4084,7 @@ } }, "pause_status": { - "description": "Whether this trigger is paused or not." + "description": "Indicate whether this schedule is paused or not." }, "table": { "description": "Table trigger settings.", @@ -5115,10 +5115,10 @@ } }, "workspace": { - "description": "Configures which workspace to connect to and locations for files, state, and similar locations within the workspace file tree.", + "description": "", "properties": { "artifact_path": { - "description": "The remote path to synchronize build artifacts to. This defaults to `${workspace.root}/artifacts`" + "description": "" }, "auth_type": { "description": "" @@ -5127,10 +5127,10 @@ "description": "" }, "azure_environment": { - "description": "Azure environment, one of (Public, UsGov, China, Germany)." + "description": "" }, "azure_login_app_id": { - "description": "Azure Login Application ID." + "description": "" }, "azure_tenant_id": { "description": "" @@ -5139,28 +5139,28 @@ "description": "" }, "azure_workspace_resource_id": { - "description": "Azure Resource Manager ID for Azure Databricks workspace." + "description": "" }, "client_id": { "description": "" }, "file_path": { - "description": "The remote path to synchronize local files artifacts to. This defaults to `${workspace.root}/files`" + "description": "" }, "google_service_account": { "description": "" }, "host": { - "description": "Host url of the workspace." + "description": "" }, "profile": { - "description": "Connection profile to use. By default profiles are specified in ~/.databrickscfg." + "description": "" }, "root_path": { - "description": "The base location for synchronizing files, artifacts and state. Defaults to `/Users/jane@doe.com/.bundle/${bundle.name}/${bundle.target}`" + "description": "" }, "state_path": { - "description": "The remote path to synchronize bundle state to. This defaults to `${workspace.root}/state`" + "description": "" } } } @@ -5220,10 +5220,10 @@ } }, "workspace": { - "description": "Configures which workspace to connect to and locations for files, state, and similar locations within the workspace file tree.", + "description": "", "properties": { "artifact_path": { - "description": "The remote path to synchronize build artifacts to. This defaults to `${workspace.root}/artifacts`" + "description": "" }, "auth_type": { "description": "" @@ -5232,10 +5232,10 @@ "description": "" }, "azure_environment": { - "description": "Azure environment, one of (Public, UsGov, China, Germany)." + "description": "" }, "azure_login_app_id": { - "description": "Azure Login Application ID." + "description": "" }, "azure_tenant_id": { "description": "" @@ -5244,28 +5244,28 @@ "description": "" }, "azure_workspace_resource_id": { - "description": "Azure Resource Manager ID for Azure Databricks workspace." + "description": "" }, "client_id": { "description": "" }, "file_path": { - "description": "The remote path to synchronize local files artifacts to. This defaults to `${workspace.root}/files`" + "description": "" }, "google_service_account": { "description": "" }, "host": { - "description": "Host url of the workspace." + "description": "" }, "profile": { - "description": "Connection profile to use. By default profiles are specified in ~/.databrickscfg." + "description": "" }, "root_path": { - "description": "The base location for synchronizing files, artifacts and state. Defaults to `/Users/jane@doe.com/.bundle/${bundle.name}/${bundle.target}`" + "description": "" }, "state_path": { - "description": "The remote path to synchronize bundle state to. This defaults to `${workspace.root}/state`" + "description": "" } } } diff --git a/cmd/account/billable-usage/billable-usage.go b/cmd/account/billable-usage/billable-usage.go index ec9b7a63..bbbc9af2 100755 --- a/cmd/account/billable-usage/billable-usage.go +++ b/cmd/account/billable-usage/billable-usage.go @@ -92,7 +92,7 @@ func newDownload() *cobra.Command { return err } defer response.Contents.Close() - return cmdio.RenderReader(ctx, response.Contents) + return cmdio.Render(ctx, response.Contents) } // Disable completions since they are not applicable. diff --git a/cmd/account/budgets/budgets.go b/cmd/account/budgets/budgets.go index 69237900..dfa2f6bc 100755 --- a/cmd/account/budgets/budgets.go +++ b/cmd/account/budgets/budgets.go @@ -281,11 +281,8 @@ func newList() *cobra.Command { cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { ctx := cmd.Context() a := root.AccountClient(ctx) - response, err := a.Budgets.ListAll(ctx) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := a.Budgets.List(ctx) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/account/custom-app-integration/custom-app-integration.go b/cmd/account/custom-app-integration/custom-app-integration.go index e6d216df..79c0f837 100755 --- a/cmd/account/custom-app-integration/custom-app-integration.go +++ b/cmd/account/custom-app-integration/custom-app-integration.go @@ -262,11 +262,8 @@ func newList() *cobra.Command { cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { ctx := cmd.Context() a := root.AccountClient(ctx) - response, err := a.CustomAppIntegration.ListAll(ctx) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := a.CustomAppIntegration.List(ctx) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/account/groups/groups.go b/cmd/account/groups/groups.go index ed1fa164..a068fba4 100755 --- a/cmd/account/groups/groups.go +++ b/cmd/account/groups/groups.go @@ -314,11 +314,8 @@ func newList() *cobra.Command { ctx := cmd.Context() a := root.AccountClient(ctx) - response, err := a.Groups.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := a.Groups.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/account/ip-access-lists/ip-access-lists.go b/cmd/account/ip-access-lists/ip-access-lists.go index 20511265..dd836c90 100755 --- a/cmd/account/ip-access-lists/ip-access-lists.go +++ b/cmd/account/ip-access-lists/ip-access-lists.go @@ -339,11 +339,8 @@ func newList() *cobra.Command { cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { ctx := cmd.Context() a := root.AccountClient(ctx) - response, err := a.IpAccessLists.ListAll(ctx) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := a.IpAccessLists.List(ctx) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/account/log-delivery/log-delivery.go b/cmd/account/log-delivery/log-delivery.go index 1846e0fd..eed8942b 100755 --- a/cmd/account/log-delivery/log-delivery.go +++ b/cmd/account/log-delivery/log-delivery.go @@ -303,11 +303,8 @@ func newList() *cobra.Command { ctx := cmd.Context() a := root.AccountClient(ctx) - response, err := a.LogDelivery.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := a.LogDelivery.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/account/metastore-assignments/metastore-assignments.go b/cmd/account/metastore-assignments/metastore-assignments.go index 619bde50..b1d0508b 100755 --- a/cmd/account/metastore-assignments/metastore-assignments.go +++ b/cmd/account/metastore-assignments/metastore-assignments.go @@ -294,11 +294,8 @@ func newList() *cobra.Command { listReq.MetastoreId = args[0] - response, err := a.MetastoreAssignments.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := a.MetastoreAssignments.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/account/metastores/metastores.go b/cmd/account/metastores/metastores.go index 797bef5e..e8b7c8f7 100755 --- a/cmd/account/metastores/metastores.go +++ b/cmd/account/metastores/metastores.go @@ -257,11 +257,8 @@ func newList() *cobra.Command { cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { ctx := cmd.Context() a := root.AccountClient(ctx) - response, err := a.Metastores.ListAll(ctx) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := a.Metastores.List(ctx) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/account/network-connectivity/network-connectivity.go b/cmd/account/network-connectivity/network-connectivity.go index 27ab3174..bfe116f2 100755 --- a/cmd/account/network-connectivity/network-connectivity.go +++ b/cmd/account/network-connectivity/network-connectivity.go @@ -546,11 +546,8 @@ func newListNetworkConnectivityConfigurations() *cobra.Command { ctx := cmd.Context() a := root.AccountClient(ctx) - response, err := a.NetworkConnectivity.ListNetworkConnectivityConfigurationsAll(ctx, listNetworkConnectivityConfigurationsReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := a.NetworkConnectivity.ListNetworkConnectivityConfigurations(ctx, listNetworkConnectivityConfigurationsReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. @@ -612,11 +609,8 @@ func newListPrivateEndpointRules() *cobra.Command { listPrivateEndpointRulesReq.NetworkConnectivityConfigId = args[0] - response, err := a.NetworkConnectivity.ListPrivateEndpointRulesAll(ctx, listPrivateEndpointRulesReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := a.NetworkConnectivity.ListPrivateEndpointRules(ctx, listPrivateEndpointRulesReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/account/o-auth-published-apps/o-auth-published-apps.go b/cmd/account/o-auth-published-apps/o-auth-published-apps.go index b611724d..1ce363ac 100755 --- a/cmd/account/o-auth-published-apps/o-auth-published-apps.go +++ b/cmd/account/o-auth-published-apps/o-auth-published-apps.go @@ -72,11 +72,8 @@ func newList() *cobra.Command { ctx := cmd.Context() a := root.AccountClient(ctx) - response, err := a.OAuthPublishedApps.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := a.OAuthPublishedApps.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/account/published-app-integration/published-app-integration.go b/cmd/account/published-app-integration/published-app-integration.go index d3209c67..54cf6337 100755 --- a/cmd/account/published-app-integration/published-app-integration.go +++ b/cmd/account/published-app-integration/published-app-integration.go @@ -262,11 +262,8 @@ func newList() *cobra.Command { cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { ctx := cmd.Context() a := root.AccountClient(ctx) - response, err := a.PublishedAppIntegration.ListAll(ctx) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := a.PublishedAppIntegration.List(ctx) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/account/service-principal-secrets/service-principal-secrets.go b/cmd/account/service-principal-secrets/service-principal-secrets.go index 19d6a491..1a646e25 100755 --- a/cmd/account/service-principal-secrets/service-principal-secrets.go +++ b/cmd/account/service-principal-secrets/service-principal-secrets.go @@ -226,11 +226,8 @@ func newList() *cobra.Command { return fmt.Errorf("invalid SERVICE_PRINCIPAL_ID: %s", args[0]) } - response, err := a.ServicePrincipalSecrets.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := a.ServicePrincipalSecrets.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/account/service-principals/service-principals.go b/cmd/account/service-principals/service-principals.go index 80f1bf46..af18d534 100755 --- a/cmd/account/service-principals/service-principals.go +++ b/cmd/account/service-principals/service-principals.go @@ -313,11 +313,8 @@ func newList() *cobra.Command { ctx := cmd.Context() a := root.AccountClient(ctx) - response, err := a.ServicePrincipals.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := a.ServicePrincipals.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/account/users/users.go b/cmd/account/users/users.go index 551766e8..f5b81f21 100755 --- a/cmd/account/users/users.go +++ b/cmd/account/users/users.go @@ -329,11 +329,8 @@ func newList() *cobra.Command { ctx := cmd.Context() a := root.AccountClient(ctx) - response, err := a.Users.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := a.Users.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/account/workspace-assignment/workspace-assignment.go b/cmd/account/workspace-assignment/workspace-assignment.go index 7780d90f..ab82cd39 100755 --- a/cmd/account/workspace-assignment/workspace-assignment.go +++ b/cmd/account/workspace-assignment/workspace-assignment.go @@ -219,11 +219,8 @@ func newList() *cobra.Command { return fmt.Errorf("invalid WORKSPACE_ID: %s", args[0]) } - response, err := a.WorkspaceAssignment.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := a.WorkspaceAssignment.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/fs/cat.go b/cmd/fs/cat.go index be186653..df94d1d7 100644 --- a/cmd/fs/cat.go +++ b/cmd/fs/cat.go @@ -27,7 +27,7 @@ func newCatCommand() *cobra.Command { if err != nil { return err } - return cmdio.RenderReader(ctx, r) + return cmdio.Render(ctx, r) } return cmd diff --git a/cmd/fs/cp.go b/cmd/fs/cp.go index f0f480fe..1ba0daf0 100644 --- a/cmd/fs/cp.go +++ b/cmd/fs/cp.go @@ -107,7 +107,7 @@ func (c *copy) emitFileSkippedEvent(sourcePath, targetPath string) error { event := newFileSkippedEvent(fullSourcePath, fullTargetPath) template := "{{.SourcePath}} -> {{.TargetPath}} (skipped; already exists)\n" - return cmdio.RenderWithTemplate(c.ctx, event, template) + return cmdio.RenderWithTemplate(c.ctx, event, "", template) } func (c *copy) emitFileCopiedEvent(sourcePath, targetPath string) error { @@ -123,7 +123,7 @@ func (c *copy) emitFileCopiedEvent(sourcePath, targetPath string) error { event := newFileCopiedEvent(fullSourcePath, fullTargetPath) template := "{{.SourcePath}} -> {{.TargetPath}}\n" - return cmdio.RenderWithTemplate(c.ctx, event, template) + return cmdio.RenderWithTemplate(c.ctx, event, "", template) } func newCpCommand() *cobra.Command { diff --git a/cmd/fs/ls.go b/cmd/fs/ls.go index be52b928..1d9ee876 100644 --- a/cmd/fs/ls.go +++ b/cmd/fs/ls.go @@ -78,12 +78,12 @@ func newLsCommand() *cobra.Command { // Use template for long mode if the flag is set if long { - return cmdio.RenderWithTemplate(ctx, jsonDirEntries, cmdio.Heredoc(` + return cmdio.RenderWithTemplate(ctx, jsonDirEntries, "", cmdio.Heredoc(` {{range .}}{{if .IsDir}}DIRECTORY {{else}}FILE {{end}}{{.Size}} {{.ModTime|pretty_date}} {{.Name}} {{end}} `)) } - return cmdio.RenderWithTemplate(ctx, jsonDirEntries, cmdio.Heredoc(` + return cmdio.RenderWithTemplate(ctx, jsonDirEntries, "", cmdio.Heredoc(` {{range .}}{{.Name}} {{end}} `)) diff --git a/cmd/labs/project/proxy.go b/cmd/labs/project/proxy.go index d872560a..ee1b0aa9 100644 --- a/cmd/labs/project/proxy.go +++ b/cmd/labs/project/proxy.go @@ -87,7 +87,7 @@ func (cp *proxy) renderJsonAsTable(cmd *cobra.Command, args []string, envs map[s } // IntelliJ eagerly replaces tabs with spaces, even though we're not asking for it fixedTemplate := strings.ReplaceAll(cp.TableTemplate, "\\t", "\t") - return cmdio.RenderWithTemplate(ctx, anyVal, fixedTemplate) + return cmdio.RenderWithTemplate(ctx, anyVal, "", fixedTemplate) } func (cp *proxy) commandInput(cmd *cobra.Command) ([]string, error) { diff --git a/cmd/root/io.go b/cmd/root/io.go index 23c7d6c6..b224bbb2 100644 --- a/cmd/root/io.go +++ b/cmd/root/io.go @@ -38,13 +38,14 @@ func OutputType(cmd *cobra.Command) flags.Output { } func (f *outputFlag) initializeIO(cmd *cobra.Command) error { - var template string + var headerTemplate, template string if cmd.Annotations != nil { // rely on zeroval being an empty string template = cmd.Annotations["template"] + headerTemplate = cmd.Annotations["headerTemplate"] } - cmdIO := cmdio.NewIO(f.output, cmd.InOrStdin(), cmd.OutOrStdout(), cmd.ErrOrStderr(), template) + cmdIO := cmdio.NewIO(f.output, cmd.InOrStdin(), cmd.OutOrStdout(), cmd.ErrOrStderr(), headerTemplate, template) ctx := cmdio.InContext(cmd.Context(), cmdIO) cmd.SetContext(ctx) return nil diff --git a/cmd/workspace/catalogs/catalogs.go b/cmd/workspace/catalogs/catalogs.go index 6ffe4a39..8e639023 100755 --- a/cmd/workspace/catalogs/catalogs.go +++ b/cmd/workspace/catalogs/catalogs.go @@ -292,11 +292,8 @@ func newList() *cobra.Command { cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.Catalogs.ListAll(ctx) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Catalogs.List(ctx) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/catalogs/overrides.go b/cmd/workspace/catalogs/overrides.go index 6de7a777..9ab1bf05 100644 --- a/cmd/workspace/catalogs/overrides.go +++ b/cmd/workspace/catalogs/overrides.go @@ -6,8 +6,9 @@ import ( ) func listOverride(listCmd *cobra.Command) { + listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` + {{header "Name"}} {{header "Type"}} {{header "Comment"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` - {{header "Name"}} {{header "Type"}} {{header "Comment"}} {{range .}}{{.Name|green}} {{blue "%s" .CatalogType}} {{.Comment}} {{end}}`) } diff --git a/cmd/workspace/clean-rooms/clean-rooms.go b/cmd/workspace/clean-rooms/clean-rooms.go index cac5de34..4cee2ce6 100755 --- a/cmd/workspace/clean-rooms/clean-rooms.go +++ b/cmd/workspace/clean-rooms/clean-rooms.go @@ -282,11 +282,8 @@ func newList() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.CleanRooms.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.CleanRooms.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/cluster-policies/cluster-policies.go b/cmd/workspace/cluster-policies/cluster-policies.go index 65f1af57..f6edee2b 100755 --- a/cmd/workspace/cluster-policies/cluster-policies.go +++ b/cmd/workspace/cluster-policies/cluster-policies.go @@ -603,11 +603,8 @@ func newList() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.ClusterPolicies.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.ClusterPolicies.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/clusters/clusters.go b/cmd/workspace/clusters/clusters.go index b009a1f5..cf35b283 100755 --- a/cmd/workspace/clusters/clusters.go +++ b/cmd/workspace/clusters/clusters.go @@ -653,11 +653,8 @@ func newEvents() *cobra.Command { eventsReq.ClusterId = args[0] } - response, err := w.Clusters.EventsAll(ctx, eventsReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Clusters.Events(ctx, eventsReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. @@ -957,11 +954,8 @@ func newList() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.Clusters.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Clusters.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/clusters/overrides.go b/cmd/workspace/clusters/overrides.go index ab32a4cd..55976d40 100644 --- a/cmd/workspace/clusters/overrides.go +++ b/cmd/workspace/clusters/overrides.go @@ -7,8 +7,9 @@ import ( ) func listOverride(listCmd *cobra.Command, _ *compute.ListClustersRequest) { + listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` + {{header "ID"}} {{header "Name"}} {{header "State"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` - {{header "ID"}} {{header "Name"}} {{header "State"}} {{range .}}{{.ClusterId | green}} {{.ClusterName | cyan}} {{if eq .State "RUNNING"}}{{green "%s" .State}}{{else if eq .State "TERMINATED"}}{{red "%s" .State}}{{else}}{{blue "%s" .State}}{{end}} {{end}}`) } diff --git a/cmd/workspace/connections/connections.go b/cmd/workspace/connections/connections.go index e28004c0..f740c778 100755 --- a/cmd/workspace/connections/connections.go +++ b/cmd/workspace/connections/connections.go @@ -293,11 +293,8 @@ func newList() *cobra.Command { cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.Connections.ListAll(ctx) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Connections.List(ctx) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/dashboards/dashboards.go b/cmd/workspace/dashboards/dashboards.go index 34bbb28b..e07f7392 100755 --- a/cmd/workspace/dashboards/dashboards.go +++ b/cmd/workspace/dashboards/dashboards.go @@ -293,11 +293,8 @@ func newList() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.Dashboards.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Dashboards.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/dashboards/overrides.go b/cmd/workspace/dashboards/overrides.go index 709e657f..6a26ebbf 100644 --- a/cmd/workspace/dashboards/overrides.go +++ b/cmd/workspace/dashboards/overrides.go @@ -7,8 +7,9 @@ import ( ) func listOverride(listCmd *cobra.Command, _ *sql.ListDashboardsRequest) { + listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` + {{header "ID"}} {{header "Name"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` - {{header "ID"}} {{header "Name"}} {{range .}}{{.Id|green}} {{.Name}} {{end}}`) } diff --git a/cmd/workspace/experiments/experiments.go b/cmd/workspace/experiments/experiments.go index 7bd28938..368ec7f9 100755 --- a/cmd/workspace/experiments/experiments.go +++ b/cmd/workspace/experiments/experiments.go @@ -733,11 +733,8 @@ func newGetHistory() *cobra.Command { getHistoryReq.MetricKey = args[0] - response, err := w.Experiments.GetHistoryAll(ctx, getHistoryReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Experiments.GetHistory(ctx, getHistoryReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. @@ -998,11 +995,8 @@ func newListArtifacts() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.Experiments.ListArtifactsAll(ctx, listArtifactsReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Experiments.ListArtifacts(ctx, listArtifactsReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. @@ -1061,11 +1055,8 @@ func newListExperiments() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.Experiments.ListExperimentsAll(ctx, listExperimentsReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Experiments.ListExperiments(ctx, listExperimentsReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. @@ -1842,11 +1833,8 @@ func newSearchExperiments() *cobra.Command { } } - response, err := w.Experiments.SearchExperimentsAll(ctx, searchExperimentsReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Experiments.SearchExperiments(ctx, searchExperimentsReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. @@ -1919,11 +1907,8 @@ func newSearchRuns() *cobra.Command { } } - response, err := w.Experiments.SearchRunsAll(ctx, searchRunsReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Experiments.SearchRuns(ctx, searchRunsReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/external-locations/external-locations.go b/cmd/workspace/external-locations/external-locations.go index b4166086..7ddc0d84 100755 --- a/cmd/workspace/external-locations/external-locations.go +++ b/cmd/workspace/external-locations/external-locations.go @@ -319,11 +319,8 @@ func newList() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.ExternalLocations.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.ExternalLocations.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/external-locations/overrides.go b/cmd/workspace/external-locations/overrides.go index 63a30cfc..00b4921d 100644 --- a/cmd/workspace/external-locations/overrides.go +++ b/cmd/workspace/external-locations/overrides.go @@ -7,8 +7,9 @@ import ( ) func listOverride(listCmd *cobra.Command, listReq *catalog.ListExternalLocationsRequest) { + listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` + {{header "Name"}} {{header "Credential"}} {{header "URL"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` - {{header "Name"}} {{header "Credential"}} {{header "URL"}} {{range .}}{{.Name|green}} {{.CredentialName|cyan}} {{.Url}} {{end}}`) } diff --git a/cmd/workspace/functions/functions.go b/cmd/workspace/functions/functions.go index 35356be0..d1db1ec9 100755 --- a/cmd/workspace/functions/functions.go +++ b/cmd/workspace/functions/functions.go @@ -327,11 +327,8 @@ func newList() *cobra.Command { listReq.CatalogName = args[0] listReq.SchemaName = args[1] - response, err := w.Functions.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Functions.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/git-credentials/git-credentials.go b/cmd/workspace/git-credentials/git-credentials.go index ca256564..8984a953 100755 --- a/cmd/workspace/git-credentials/git-credentials.go +++ b/cmd/workspace/git-credentials/git-credentials.go @@ -311,11 +311,8 @@ func newList() *cobra.Command { cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.GitCredentials.ListAll(ctx) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.GitCredentials.List(ctx) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/global-init-scripts/global-init-scripts.go b/cmd/workspace/global-init-scripts/global-init-scripts.go index c40b6785..de08614f 100755 --- a/cmd/workspace/global-init-scripts/global-init-scripts.go +++ b/cmd/workspace/global-init-scripts/global-init-scripts.go @@ -309,11 +309,8 @@ func newList() *cobra.Command { cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.GlobalInitScripts.ListAll(ctx) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.GlobalInitScripts.List(ctx) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/groups/groups.go b/cmd/workspace/groups/groups.go index 588bce31..aba54b8b 100755 --- a/cmd/workspace/groups/groups.go +++ b/cmd/workspace/groups/groups.go @@ -314,11 +314,8 @@ func newList() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.Groups.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Groups.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/instance-pools/instance-pools.go b/cmd/workspace/instance-pools/instance-pools.go index 968f64bc..c9389fef 100755 --- a/cmd/workspace/instance-pools/instance-pools.go +++ b/cmd/workspace/instance-pools/instance-pools.go @@ -602,11 +602,8 @@ func newList() *cobra.Command { cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.InstancePools.ListAll(ctx) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.InstancePools.List(ctx) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/instance-profiles/instance-profiles.go b/cmd/workspace/instance-profiles/instance-profiles.go index ca78a15f..2077c4bf 100755 --- a/cmd/workspace/instance-profiles/instance-profiles.go +++ b/cmd/workspace/instance-profiles/instance-profiles.go @@ -251,11 +251,8 @@ func newList() *cobra.Command { cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.InstanceProfiles.ListAll(ctx) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.InstanceProfiles.List(ctx) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/ip-access-lists/ip-access-lists.go b/cmd/workspace/ip-access-lists/ip-access-lists.go index 5bba8b51..9eb08cb4 100755 --- a/cmd/workspace/ip-access-lists/ip-access-lists.go +++ b/cmd/workspace/ip-access-lists/ip-access-lists.go @@ -340,11 +340,8 @@ func newList() *cobra.Command { cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.IpAccessLists.ListAll(ctx) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.IpAccessLists.List(ctx) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/jobs/jobs.go b/cmd/workspace/jobs/jobs.go index 634a7f39..957aa609 100755 --- a/cmd/workspace/jobs/jobs.go +++ b/cmd/workspace/jobs/jobs.go @@ -1042,11 +1042,8 @@ func newList() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.Jobs.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Jobs.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. @@ -1112,11 +1109,8 @@ func newListRuns() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.Jobs.ListRunsAll(ctx, listRunsReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Jobs.ListRuns(ctx, listRunsReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/jobs/overrides.go b/cmd/workspace/jobs/overrides.go index fd22dcbd..ee7d2055 100644 --- a/cmd/workspace/jobs/overrides.go +++ b/cmd/workspace/jobs/overrides.go @@ -13,8 +13,9 @@ func listOverride(listCmd *cobra.Command, listReq *jobs.ListJobsRequest) { } func listRunsOverride(listRunsCmd *cobra.Command, listRunsReq *jobs.ListRunsRequest) { + listRunsCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` + {{header "Job ID"}} {{header "Run ID"}} {{header "Result State"}} URL`) listRunsCmd.Annotations["template"] = cmdio.Heredoc(` - {{header "Job ID"}} {{header "Run ID"}} {{header "Result State"}} URL {{range .}}{{green "%d" .JobId}} {{cyan "%d" .RunId}} {{if eq .State.ResultState "SUCCESS"}}{{"SUCCESS"|green}}{{else}}{{red "%s" .State.ResultState}}{{end}} {{.RunPageUrl}} {{end}}`) } diff --git a/cmd/workspace/libraries/libraries.go b/cmd/workspace/libraries/libraries.go index 1e742892..fef81c25 100755 --- a/cmd/workspace/libraries/libraries.go +++ b/cmd/workspace/libraries/libraries.go @@ -157,11 +157,8 @@ func newClusterStatus() *cobra.Command { clusterStatusReq.ClusterId = args[0] - response, err := w.Libraries.ClusterStatusAll(ctx, clusterStatusReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Libraries.ClusterStatus(ctx, clusterStatusReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/metastores/metastores.go b/cmd/workspace/metastores/metastores.go index fdd0d1c0..d63576d4 100755 --- a/cmd/workspace/metastores/metastores.go +++ b/cmd/workspace/metastores/metastores.go @@ -455,11 +455,8 @@ func newList() *cobra.Command { cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.Metastores.ListAll(ctx) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Metastores.List(ctx) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/metastores/overrides.go b/cmd/workspace/metastores/overrides.go index 2c9ca6f7..3ee6a107 100644 --- a/cmd/workspace/metastores/overrides.go +++ b/cmd/workspace/metastores/overrides.go @@ -6,8 +6,9 @@ import ( ) func listOverride(listCmd *cobra.Command) { + listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` + {{header "ID"}} {{header "Name"}} {{"Region"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` - {{header "ID"}} {{header "Name"}} {{"Region"}} {{range .}}{{.MetastoreId|green}} {{.Name|cyan}} {{.Region}} {{end}}`) } diff --git a/cmd/workspace/model-registry/model-registry.go b/cmd/workspace/model-registry/model-registry.go index fade898e..9c6034b5 100755 --- a/cmd/workspace/model-registry/model-registry.go +++ b/cmd/workspace/model-registry/model-registry.go @@ -1128,11 +1128,8 @@ func newGetLatestVersions() *cobra.Command { getLatestVersionsReq.Name = args[0] } - response, err := w.ModelRegistry.GetLatestVersionsAll(ctx, getLatestVersionsReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.ModelRegistry.GetLatestVersions(ctx, getLatestVersionsReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. @@ -1520,11 +1517,8 @@ func newListModels() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.ModelRegistry.ListModelsAll(ctx, listModelsReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.ModelRegistry.ListModels(ctx, listModelsReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. @@ -1586,11 +1580,8 @@ func newListTransitionRequests() *cobra.Command { listTransitionRequestsReq.Name = args[0] listTransitionRequestsReq.Version = args[1] - response, err := w.ModelRegistry.ListTransitionRequestsAll(ctx, listTransitionRequestsReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.ModelRegistry.ListTransitionRequests(ctx, listTransitionRequestsReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. @@ -1651,11 +1642,8 @@ func newListWebhooks() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.ModelRegistry.ListWebhooksAll(ctx, listWebhooksReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.ModelRegistry.ListWebhooks(ctx, listWebhooksReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. @@ -1900,11 +1888,8 @@ func newSearchModelVersions() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.ModelRegistry.SearchModelVersionsAll(ctx, searchModelVersionsReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.ModelRegistry.SearchModelVersions(ctx, searchModelVersionsReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. @@ -1964,11 +1949,8 @@ func newSearchModels() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.ModelRegistry.SearchModelsAll(ctx, searchModelsReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.ModelRegistry.SearchModels(ctx, searchModelsReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/model-versions/model-versions.go b/cmd/workspace/model-versions/model-versions.go index 97438264..b4492cb3 100755 --- a/cmd/workspace/model-versions/model-versions.go +++ b/cmd/workspace/model-versions/model-versions.go @@ -315,11 +315,8 @@ func newList() *cobra.Command { listReq.FullName = args[0] - response, err := w.ModelVersions.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.ModelVersions.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/pipelines/pipelines.go b/cmd/workspace/pipelines/pipelines.go index ad54b6b1..4c2db6aa 100755 --- a/cmd/workspace/pipelines/pipelines.go +++ b/cmd/workspace/pipelines/pipelines.go @@ -536,11 +536,8 @@ func newListPipelineEvents() *cobra.Command { } listPipelineEventsReq.PipelineId = args[0] - response, err := w.Pipelines.ListPipelineEventsAll(ctx, listPipelineEventsReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Pipelines.ListPipelineEvents(ctx, listPipelineEventsReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. @@ -600,11 +597,8 @@ func newListPipelines() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.Pipelines.ListPipelinesAll(ctx, listPipelinesReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Pipelines.ListPipelines(ctx, listPipelinesReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/policy-families/policy-families.go b/cmd/workspace/policy-families/policy-families.go index 75ab862a..c81d2e92 100755 --- a/cmd/workspace/policy-families/policy-families.go +++ b/cmd/workspace/policy-families/policy-families.go @@ -138,11 +138,8 @@ func newList() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.PolicyFamilies.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.PolicyFamilies.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/providers/providers.go b/cmd/workspace/providers/providers.go index 851c668a..25529648 100755 --- a/cmd/workspace/providers/providers.go +++ b/cmd/workspace/providers/providers.go @@ -323,11 +323,8 @@ func newList() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.Providers.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Providers.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. @@ -401,11 +398,8 @@ func newListShares() *cobra.Command { } listSharesReq.Name = args[0] - response, err := w.Providers.ListSharesAll(ctx, listSharesReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Providers.ListShares(ctx, listSharesReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/queries/overrides.go b/cmd/workspace/queries/overrides.go index a06dabde..d7edf93a 100644 --- a/cmd/workspace/queries/overrides.go +++ b/cmd/workspace/queries/overrides.go @@ -8,8 +8,9 @@ import ( func listOverride(listCmd *cobra.Command, listReq *sql.ListQueriesRequest) { // TODO: figure out colored/non-colored headers and colspan shifts + listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` + {{header "ID"}} {{header "Name"}} {{header "Author"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` - {{header "ID"}} {{header "Name"}} {{header "Author"}} {{range .}}{{.Id|green}} {{.Name|cyan}} {{.User.Email|cyan}} {{end}}`) } diff --git a/cmd/workspace/queries/queries.go b/cmd/workspace/queries/queries.go index c4349213..ef2de446 100755 --- a/cmd/workspace/queries/queries.go +++ b/cmd/workspace/queries/queries.go @@ -303,11 +303,8 @@ func newList() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.Queries.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Queries.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/query-history/query-history.go b/cmd/workspace/query-history/query-history.go index 337ab403..84746105 100755 --- a/cmd/workspace/query-history/query-history.go +++ b/cmd/workspace/query-history/query-history.go @@ -73,11 +73,8 @@ func newList() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.QueryHistory.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.QueryHistory.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/recipients/recipients.go b/cmd/workspace/recipients/recipients.go index 463d7985..d7d432b9 100755 --- a/cmd/workspace/recipients/recipients.go +++ b/cmd/workspace/recipients/recipients.go @@ -342,11 +342,8 @@ func newList() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.Recipients.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Recipients.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/registered-models/registered-models.go b/cmd/workspace/registered-models/registered-models.go index b506e180..98aec3bb 100755 --- a/cmd/workspace/registered-models/registered-models.go +++ b/cmd/workspace/registered-models/registered-models.go @@ -450,11 +450,8 @@ func newList() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.RegisteredModels.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.RegisteredModels.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/repos/repos.go b/cmd/workspace/repos/repos.go index 62f63750..0c38183a 100755 --- a/cmd/workspace/repos/repos.go +++ b/cmd/workspace/repos/repos.go @@ -485,11 +485,8 @@ func newList() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.Repos.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Repos.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/schemas/overrides.go b/cmd/workspace/schemas/overrides.go index 180690b6..ba4c65ce 100644 --- a/cmd/workspace/schemas/overrides.go +++ b/cmd/workspace/schemas/overrides.go @@ -7,8 +7,9 @@ import ( ) func listOverride(listCmd *cobra.Command, listReq *catalog.ListSchemasRequest) { + listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` + {{header "Full Name"}} {{header "Owner"}} {{header "Comment"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` - {{header "Full Name"}} {{header "Owner"}} {{header "Comment"}} {{range .}}{{.FullName|green}} {{.Owner|cyan}} {{.Comment}} {{end}}`) } diff --git a/cmd/workspace/schemas/schemas.go b/cmd/workspace/schemas/schemas.go index fc496467..ebdab2ab 100755 --- a/cmd/workspace/schemas/schemas.go +++ b/cmd/workspace/schemas/schemas.go @@ -333,11 +333,8 @@ func newList() *cobra.Command { listReq.CatalogName = args[0] - response, err := w.Schemas.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Schemas.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/secrets/overrides.go b/cmd/workspace/secrets/overrides.go index 6e765bf7..b215f17a 100644 --- a/cmd/workspace/secrets/overrides.go +++ b/cmd/workspace/secrets/overrides.go @@ -11,15 +11,17 @@ func cmdOverride(cmd *cobra.Command) { } func listScopesOverride(listScopesCmd *cobra.Command) { + listScopesCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` + {{header "Scope"}} {{header "Backend Type"}}`) listScopesCmd.Annotations["template"] = cmdio.Heredoc(` - {{header "Scope"}} {{header "Backend Type"}} {{range .}}{{.Name|green}} {{.BackendType}} {{end}}`) } func listSecretsOverride(listSecretsCommand *cobra.Command, _ *workspace.ListSecretsRequest) { + listSecretsCommand.Annotations["headerTemplate"] = cmdio.Heredoc(` + {{header "Key"}} {{header "Last Updated Timestamp"}}`) listSecretsCommand.Annotations["template"] = cmdio.Heredoc(` - {{header "Key"}} {{header "Last Updated Timestamp"}} {{range .}}{{.Key|green}} {{.LastUpdatedTimestamp}} {{end}}`) } diff --git a/cmd/workspace/secrets/secrets.go b/cmd/workspace/secrets/secrets.go index 270538b0..ec6423d0 100755 --- a/cmd/workspace/secrets/secrets.go +++ b/cmd/workspace/secrets/secrets.go @@ -590,11 +590,8 @@ func newListAcls() *cobra.Command { listAclsReq.Scope = args[0] - response, err := w.Secrets.ListAclsAll(ctx, listAclsReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Secrets.ListAcls(ctx, listAclsReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. @@ -641,11 +638,8 @@ func newListScopes() *cobra.Command { cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.Secrets.ListScopesAll(ctx) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Secrets.ListScopes(ctx) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. @@ -712,11 +706,8 @@ func newListSecrets() *cobra.Command { listSecretsReq.Scope = args[0] - response, err := w.Secrets.ListSecretsAll(ctx, listSecretsReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Secrets.ListSecrets(ctx, listSecretsReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/service-principals/service-principals.go b/cmd/workspace/service-principals/service-principals.go index 5e66804d..353c0876 100755 --- a/cmd/workspace/service-principals/service-principals.go +++ b/cmd/workspace/service-principals/service-principals.go @@ -313,11 +313,8 @@ func newList() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.ServicePrincipals.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.ServicePrincipals.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/serving-endpoints/serving-endpoints.go b/cmd/workspace/serving-endpoints/serving-endpoints.go index 8c488d09..9424c5e4 100755 --- a/cmd/workspace/serving-endpoints/serving-endpoints.go +++ b/cmd/workspace/serving-endpoints/serving-endpoints.go @@ -543,11 +543,8 @@ func newList() *cobra.Command { cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.ServingEndpoints.ListAll(ctx) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.ServingEndpoints.List(ctx) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/shares/shares.go b/cmd/workspace/shares/shares.go index 7cb85abf..2c0479a0 100755 --- a/cmd/workspace/shares/shares.go +++ b/cmd/workspace/shares/shares.go @@ -281,11 +281,8 @@ func newList() *cobra.Command { cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.Shares.ListAll(ctx) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Shares.List(ctx) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/storage-credentials/overrides.go b/cmd/workspace/storage-credentials/overrides.go index 534e045d..92dec91e 100644 --- a/cmd/workspace/storage-credentials/overrides.go +++ b/cmd/workspace/storage-credentials/overrides.go @@ -7,8 +7,9 @@ import ( ) func listOverride(listCmd *cobra.Command, listReq *catalog.ListStorageCredentialsRequest) { + listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` + {{header "ID"}} {{header "Name"}} {{header "Credentials"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` - {{header "ID"}} {{header "Name"}} {{header "Credentials"}} {{range .}}{{.Id|green}} {{.Name|cyan}} {{if .AwsIamRole}}{{.AwsIamRole.RoleArn}}{{end}}{{if .AzureServicePrincipal}}{{.AzureServicePrincipal.ApplicationId}}{{end}}{{if .DatabricksGcpServiceAccount}}{{.DatabricksGcpServiceAccount.Email}}{{end}} {{end}}`) } diff --git a/cmd/workspace/storage-credentials/storage-credentials.go b/cmd/workspace/storage-credentials/storage-credentials.go index 910d2b5d..4a0d8f30 100755 --- a/cmd/workspace/storage-credentials/storage-credentials.go +++ b/cmd/workspace/storage-credentials/storage-credentials.go @@ -336,11 +336,8 @@ func newList() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.StorageCredentials.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.StorageCredentials.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/system-schemas/system-schemas.go b/cmd/workspace/system-schemas/system-schemas.go index 6dbad5a3..9b2392a6 100755 --- a/cmd/workspace/system-schemas/system-schemas.go +++ b/cmd/workspace/system-schemas/system-schemas.go @@ -216,11 +216,8 @@ func newList() *cobra.Command { listReq.MetastoreId = args[0] - response, err := w.SystemSchemas.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.SystemSchemas.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/tables/overrides.go b/cmd/workspace/tables/overrides.go index 35fc351a..a0849ada 100644 --- a/cmd/workspace/tables/overrides.go +++ b/cmd/workspace/tables/overrides.go @@ -7,8 +7,9 @@ import ( ) func listOverride(listCmd *cobra.Command, listReq *catalog.ListTablesRequest) { + listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` + {{header "Full Name"}} {{header "Table Type"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` - {{header "Full Name"}} {{header "Table Type"}} {{range .}}{{.FullName|green}} {{blue "%s" .TableType}} {{end}}`) } diff --git a/cmd/workspace/tables/tables.go b/cmd/workspace/tables/tables.go index 0dfae0fe..d4e76587 100755 --- a/cmd/workspace/tables/tables.go +++ b/cmd/workspace/tables/tables.go @@ -342,11 +342,8 @@ func newList() *cobra.Command { listReq.CatalogName = args[0] listReq.SchemaName = args[1] - response, err := w.Tables.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Tables.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. @@ -433,11 +430,8 @@ func newListSummaries() *cobra.Command { } listSummariesReq.CatalogName = args[0] - response, err := w.Tables.ListSummariesAll(ctx, listSummariesReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Tables.ListSummaries(ctx, listSummariesReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/token-management/overrides.go b/cmd/workspace/token-management/overrides.go index 46967d37..8122c1a1 100644 --- a/cmd/workspace/token-management/overrides.go +++ b/cmd/workspace/token-management/overrides.go @@ -7,8 +7,9 @@ import ( ) func listOverride(listCmd *cobra.Command, listReq *settings.ListTokenManagementRequest) { + listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` + {{header "ID"}} {{header "Created By"}} {{header "Comment"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` - {{header "ID"}} {{header "Created By"}} {{header "Comment"}} {{range .}}{{.TokenId|green}} {{.CreatedByUsername|cyan}} {{.Comment|cyan}} {{end}}`) } diff --git a/cmd/workspace/token-management/token-management.go b/cmd/workspace/token-management/token-management.go index 276de6a8..1c2e2c37 100755 --- a/cmd/workspace/token-management/token-management.go +++ b/cmd/workspace/token-management/token-management.go @@ -422,11 +422,8 @@ func newList() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.TokenManagement.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.TokenManagement.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/tokens/overrides.go b/cmd/workspace/tokens/overrides.go index 09c51758..142902da 100644 --- a/cmd/workspace/tokens/overrides.go +++ b/cmd/workspace/tokens/overrides.go @@ -6,8 +6,9 @@ import ( ) func listOverride(listCmd *cobra.Command) { + listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` + {{header "ID"}} {{header "Expiry time"}} {{header "Comment"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` - {{header "ID"}} {{header "Expiry time"}} {{header "Comment"}} {{range .}}{{.TokenId|green}} {{cyan "%d" .ExpiryTime}} {{.Comment|cyan}} {{end}}`) } diff --git a/cmd/workspace/tokens/tokens.go b/cmd/workspace/tokens/tokens.go index cd82ef63..5550acfa 100755 --- a/cmd/workspace/tokens/tokens.go +++ b/cmd/workspace/tokens/tokens.go @@ -232,11 +232,8 @@ func newList() *cobra.Command { cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.Tokens.ListAll(ctx) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Tokens.List(ctx) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/users/users.go b/cmd/workspace/users/users.go index 4cc485e9..078a712e 100755 --- a/cmd/workspace/users/users.go +++ b/cmd/workspace/users/users.go @@ -426,11 +426,8 @@ func newList() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.Users.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Users.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/vector-search-endpoints/vector-search-endpoints.go b/cmd/workspace/vector-search-endpoints/vector-search-endpoints.go index d429267a..d6863b66 100755 --- a/cmd/workspace/vector-search-endpoints/vector-search-endpoints.go +++ b/cmd/workspace/vector-search-endpoints/vector-search-endpoints.go @@ -308,11 +308,8 @@ func newListEndpoints() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.VectorSearchEndpoints.ListEndpointsAll(ctx, listEndpointsReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.VectorSearchEndpoints.ListEndpoints(ctx, listEndpointsReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/vector-search-indexes/vector-search-indexes.go b/cmd/workspace/vector-search-indexes/vector-search-indexes.go index 0d3277f2..6beca7d2 100755 --- a/cmd/workspace/vector-search-indexes/vector-search-indexes.go +++ b/cmd/workspace/vector-search-indexes/vector-search-indexes.go @@ -389,11 +389,8 @@ func newListIndexes() *cobra.Command { listIndexesReq.EndpointName = args[0] - response, err := w.VectorSearchIndexes.ListIndexesAll(ctx, listIndexesReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.VectorSearchIndexes.ListIndexes(ctx, listIndexesReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/volumes/volumes.go b/cmd/workspace/volumes/volumes.go index 1944237c..12cafeaf 100755 --- a/cmd/workspace/volumes/volumes.go +++ b/cmd/workspace/volumes/volumes.go @@ -292,11 +292,8 @@ func newList() *cobra.Command { listReq.CatalogName = args[0] listReq.SchemaName = args[1] - response, err := w.Volumes.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Volumes.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/warehouses/overrides.go b/cmd/workspace/warehouses/overrides.go index 0714937c..9457557d 100644 --- a/cmd/workspace/warehouses/overrides.go +++ b/cmd/workspace/warehouses/overrides.go @@ -7,8 +7,9 @@ import ( ) func listOverride(listCmd *cobra.Command, listReq *sql.ListWarehousesRequest) { + listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` + {{header "ID"}} {{header "Name"}} {{header "Size"}} {{header "State"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` - {{header "ID"}} {{header "Name"}} {{header "Size"}} {{header "State"}} {{range .}}{{.Id|green}} {{.Name|cyan}} {{.ClusterSize|cyan}} {{if eq .State "RUNNING"}}{{"RUNNING"|green}}{{else if eq .State "STOPPED"}}{{"STOPPED"|red}}{{else}}{{blue "%s" .State}}{{end}} {{end}}`) } diff --git a/cmd/workspace/warehouses/warehouses.go b/cmd/workspace/warehouses/warehouses.go index c64788b8..2e9282a8 100755 --- a/cmd/workspace/warehouses/warehouses.go +++ b/cmd/workspace/warehouses/warehouses.go @@ -661,11 +661,8 @@ func newList() *cobra.Command { ctx := cmd.Context() w := root.WorkspaceClient(ctx) - response, err := w.Warehouses.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Warehouses.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/workspace/export_dir.go b/cmd/workspace/workspace/export_dir.go index d2a86d00..79e64e8a 100644 --- a/cmd/workspace/workspace/export_dir.go +++ b/cmd/workspace/workspace/export_dir.go @@ -55,7 +55,7 @@ func (opts exportDirOptions) callback(ctx context.Context, workspaceFiler filer. // If a file exists, and overwrite is not set, we skip exporting the file if _, err := os.Stat(targetPath); err == nil && !overwrite { // Log event that this file/directory has been skipped - return cmdio.RenderWithTemplate(ctx, newFileSkippedEvent(relPath, targetPath), "{{.SourcePath}} -> {{.TargetPath}} (skipped; already exists)\n") + return cmdio.RenderWithTemplate(ctx, newFileSkippedEvent(relPath, targetPath), "", "{{.SourcePath}} -> {{.TargetPath}} (skipped; already exists)\n") } // create the file @@ -74,7 +74,7 @@ func (opts exportDirOptions) callback(ctx context.Context, workspaceFiler filer. if err != nil { return err } - return cmdio.RenderWithTemplate(ctx, newFileExportedEvent(sourcePath, targetPath), "{{.SourcePath}} -> {{.TargetPath}}\n") + return cmdio.RenderWithTemplate(ctx, newFileExportedEvent(sourcePath, targetPath), "", "{{.SourcePath}} -> {{.TargetPath}}\n") } } diff --git a/cmd/workspace/workspace/import_dir.go b/cmd/workspace/workspace/import_dir.go index bc0b8066..6ce5f3c2 100644 --- a/cmd/workspace/workspace/import_dir.go +++ b/cmd/workspace/workspace/import_dir.go @@ -93,14 +93,14 @@ func (opts importDirOptions) callback(ctx context.Context, workspaceFiler filer. // Emit file skipped event with the appropriate template fileSkippedEvent := newFileSkippedEvent(localName, path.Join(targetDir, remoteName)) template := "{{.SourcePath}} -> {{.TargetPath}} (skipped; already exists)\n" - return cmdio.RenderWithTemplate(ctx, fileSkippedEvent, template) + return cmdio.RenderWithTemplate(ctx, fileSkippedEvent, "", template) } if err != nil { return err } } fileImportedEvent := newFileImportedEvent(localName, path.Join(targetDir, remoteName)) - return cmdio.RenderWithTemplate(ctx, fileImportedEvent, "{{.SourcePath}} -> {{.TargetPath}}\n") + return cmdio.RenderWithTemplate(ctx, fileImportedEvent, "", "{{.SourcePath}} -> {{.TargetPath}}\n") } } diff --git a/cmd/workspace/workspace/overrides.go b/cmd/workspace/workspace/overrides.go index 1cac6741..cfed0a6e 100644 --- a/cmd/workspace/workspace/overrides.go +++ b/cmd/workspace/workspace/overrides.go @@ -17,8 +17,9 @@ import ( func listOverride(listCmd *cobra.Command, listReq *workspace.ListWorkspaceRequest) { listReq.Path = "/" + listCmd.Annotations["headerTemplate"] = cmdio.Heredoc(` + {{header "ID"}} {{header "Type"}} {{header "Language"}} {{header "Path"}}`) listCmd.Annotations["template"] = cmdio.Heredoc(` - {{header "ID"}} {{header "Type"}} {{header "Language"}} {{header "Path"}} {{range .}}{{green "%d" .ObjectId}} {{blue "%s" .ObjectType}} {{cyan "%s" .Language}} {{.Path|cyan}} {{end}}`) } diff --git a/cmd/workspace/workspace/workspace.go b/cmd/workspace/workspace/workspace.go index 5777f22f..4fb63f0c 100755 --- a/cmd/workspace/workspace/workspace.go +++ b/cmd/workspace/workspace/workspace.go @@ -577,11 +577,8 @@ func newList() *cobra.Command { listReq.Path = args[0] - response, err := w.Workspace.ListAll(ctx, listReq) - if err != nil { - return err - } - return cmdio.Render(ctx, response) + response := w.Workspace.List(ctx, listReq) + return cmdio.RenderIterator(ctx, response) } // Disable completions since they are not applicable. diff --git a/internal/bundle/helpers.go b/internal/bundle/helpers.go index 2c2b2dac..a8fbd230 100644 --- a/internal/bundle/helpers.go +++ b/internal/bundle/helpers.go @@ -25,7 +25,7 @@ func initTestTemplate(t *testing.T, ctx context.Context, templateName string, co } ctx = root.SetWorkspaceClient(ctx, nil) - cmd := cmdio.NewIO(flags.OutputJSON, strings.NewReader(""), os.Stdout, os.Stderr, "bundles") + cmd := cmdio.NewIO(flags.OutputJSON, strings.NewReader(""), os.Stdout, os.Stderr, "", "bundles") ctx = cmdio.InContext(ctx, cmd) err = template.Materialize(ctx, configFilePath, templateRoot, bundleRoot) diff --git a/libs/cmdio/io.go b/libs/cmdio/io.go index d20991a7..75c0c4b8 100644 --- a/libs/cmdio/io.go +++ b/libs/cmdio/io.go @@ -22,27 +22,29 @@ import ( type cmdIO struct { // states if we are in the interactive mode // e.g. if stdout is a terminal - interactive bool - outputFormat flags.Output - template string - in io.Reader - out io.Writer - err io.Writer + interactive bool + outputFormat flags.Output + headerTemplate string + template string + in io.Reader + out io.Writer + err io.Writer } -func NewIO(outputFormat flags.Output, in io.Reader, out io.Writer, err io.Writer, template string) *cmdIO { +func NewIO(outputFormat flags.Output, in io.Reader, out io.Writer, err io.Writer, headerTemplate, template string) *cmdIO { // The check below is similar to color.NoColor but uses the specified err writer. dumb := os.Getenv("NO_COLOR") != "" || os.Getenv("TERM") == "dumb" if f, ok := err.(*os.File); ok && !dumb { dumb = !isatty.IsTerminal(f.Fd()) && !isatty.IsCygwinTerminal(f.Fd()) } return &cmdIO{ - interactive: !dumb, - outputFormat: outputFormat, - template: template, - in: in, - out: out, - err: err, + interactive: !dumb, + outputFormat: outputFormat, + headerTemplate: headerTemplate, + template: template, + in: in, + out: out, + err: err, } } @@ -113,48 +115,6 @@ func IsGitBash(ctx context.Context) bool { return false } -func Render(ctx context.Context, v any) error { - c := fromContext(ctx) - return RenderWithTemplate(ctx, v, c.template) -} - -func RenderWithTemplate(ctx context.Context, v any, template string) error { - // TODO: add terminal width & white/dark theme detection - c := fromContext(ctx) - switch c.outputFormat { - case flags.OutputJSON: - return renderJson(c.out, v) - case flags.OutputText: - if template != "" { - return renderTemplate(c.out, template, v) - } - return renderJson(c.out, v) - default: - return fmt.Errorf("invalid output format: %s", c.outputFormat) - } -} - -func RenderJson(ctx context.Context, v any) error { - c := fromContext(ctx) - if c.outputFormat == flags.OutputJSON { - return renderJson(c.out, v) - } - return nil -} - -func RenderReader(ctx context.Context, r io.Reader) error { - c := fromContext(ctx) - switch c.outputFormat { - case flags.OutputJSON: - return fmt.Errorf("json output not supported") - case flags.OutputText: - _, err := io.Copy(c.out, r) - return err - default: - return fmt.Errorf("invalid output format: %s", c.outputFormat) - } -} - type Tuple struct{ Name, Id string } func (c *cmdIO) Select(items []Tuple, label string) (id string, err error) { diff --git a/libs/cmdio/render.go b/libs/cmdio/render.go index d641f61d..40cdde35 100644 --- a/libs/cmdio/render.go +++ b/libs/cmdio/render.go @@ -2,14 +2,19 @@ package cmdio import ( "bytes" + "context" "encoding/base64" "encoding/json" + "errors" + "fmt" "io" "strings" "text/tabwriter" "text/template" "time" + "github.com/databricks/cli/libs/flags" + "github.com/databricks/databricks-sdk-go/listing" "github.com/fatih/color" "github.com/nwidger/jsoncolor" ) @@ -46,8 +51,123 @@ func Heredoc(tmpl string) (trimmed string) { return strings.TrimSpace(trimmed) } -func renderJson(w io.Writer, v any) error { - pretty, err := fancyJSON(v) +// writeFlusher represents a buffered writer that can be flushed. This is useful when +// buffering writing a large number of resources (such as during a list API). +type writeFlusher interface { + io.Writer + Flush() error +} + +type jsonRenderer interface { + // Render an object as JSON to the provided writeFlusher. + renderJson(context.Context, writeFlusher) error +} + +type textRenderer interface { + // Render an object as text to the provided writeFlusher. + renderText(context.Context, io.Writer) error +} + +type templateRenderer interface { + // Render an object using the provided template and write to the provided tabwriter.Writer. + renderTemplate(context.Context, *template.Template, *tabwriter.Writer) error +} + +type readerRenderer struct { + reader io.Reader +} + +func (r readerRenderer) renderText(_ context.Context, w io.Writer) error { + _, err := io.Copy(w, r.reader) + return err +} + +type iteratorRenderer[T any] struct { + t listing.Iterator[T] + bufferSize int +} + +func (ir iteratorRenderer[T]) getBufferSize() int { + if ir.bufferSize == 0 { + return 20 + } + return ir.bufferSize +} + +func (ir iteratorRenderer[T]) renderJson(ctx context.Context, w writeFlusher) error { + // Iterators are always rendered as a list of resources in JSON. + _, err := w.Write([]byte("[\n ")) + if err != nil { + return err + } + for i := 0; ir.t.HasNext(ctx); i++ { + if i != 0 { + _, err = w.Write([]byte(",\n ")) + if err != nil { + return err + } + } + n, err := ir.t.Next(ctx) + if err != nil { + return err + } + res, err := json.MarshalIndent(n, " ", " ") + if err != nil { + return err + } + _, err = w.Write(res) + if err != nil { + return err + } + if (i+1)%ir.getBufferSize() == 0 { + err = w.Flush() + if err != nil { + return err + } + } + } + _, err = w.Write([]byte("\n]\n")) + if err != nil { + return err + } + return w.Flush() +} + +func (ir iteratorRenderer[T]) renderTemplate(ctx context.Context, t *template.Template, w *tabwriter.Writer) error { + buf := make([]any, 0, ir.getBufferSize()) + for i := 0; ir.t.HasNext(ctx); i++ { + n, err := ir.t.Next(ctx) + if err != nil { + return err + } + buf = append(buf, n) + if len(buf) == cap(buf) { + err = t.Execute(w, buf) + if err != nil { + return err + } + err = w.Flush() + if err != nil { + return err + } + buf = buf[:0] + } + } + if len(buf) > 0 { + err := t.Execute(w, buf) + if err != nil { + return err + } + } + return w.Flush() +} + +type defaultRenderer struct { + t any +} + +func (d defaultRenderer) renderJson(_ context.Context, w writeFlusher) error { + pretty, err := fancyJSON(d.t) if err != nil { return err } @@ -56,12 +176,126 @@ func renderJson(w io.Writer, v any) error { return err } _, err = w.Write([]byte("\n")) - return err + if err != nil { + return err + } + return w.Flush() } -func renderTemplate(w io.Writer, tmpl string, v any) error { +func (d defaultRenderer) renderTemplate(_ context.Context, t *template.Template, w *tabwriter.Writer) error { + return t.Execute(w, d.t) +} + +// Returns something implementing one of the following interfaces: +// - jsonRenderer +// - textRenderer +// - templateRenderer +func newRenderer(t any) any { + if r, ok := t.(io.Reader); ok { + return readerRenderer{reader: r} + } + return defaultRenderer{t: t} +} + +func newIteratorRenderer[T any](i listing.Iterator[T]) iteratorRenderer[T] { + return iteratorRenderer[T]{t: i} +} + +type bufferedFlusher struct { + w io.Writer + b *bytes.Buffer +} + +func (b bufferedFlusher) Write(bs []byte) (int, error) { + return b.b.Write(bs) +} + +func (b bufferedFlusher) Flush() error { + _, err := b.w.Write(b.b.Bytes()) + if err != nil { + return err + } + b.b.Reset() + return nil +} + +func newBufferedFlusher(w io.Writer) writeFlusher { + return bufferedFlusher{ + w: w, + b: &bytes.Buffer{}, + } +} + +func renderWithTemplate(r any, ctx context.Context, outputFormat flags.Output, w io.Writer, headerTemplate, template string) error { + // TODO: add terminal width & white/dark theme detection + switch outputFormat { + case flags.OutputJSON: + if jr, ok := r.(jsonRenderer); ok { + return jr.renderJson(ctx, newBufferedFlusher(w)) + } + return errors.New("json output not supported") + case flags.OutputText: + if tr, ok := r.(templateRenderer); ok && template != "" { + return renderUsingTemplate(ctx, tr, w, headerTemplate, template) + } + if tr, ok := r.(textRenderer); ok { + return tr.renderText(ctx, w) + } + if jr, ok := r.(jsonRenderer); ok { + return jr.renderJson(ctx, newBufferedFlusher(w)) + } + return errors.New("no renderer defined") + default: + return fmt.Errorf("invalid output format: %s", outputFormat) + } +} + +type listingInterface interface { + HasNext(context.Context) bool +} + +func Render(ctx context.Context, v any) error { + c := fromContext(ctx) + if _, ok := v.(listingInterface); ok { + panic("use RenderIterator instead") + } + return renderWithTemplate(newRenderer(v), ctx, c.outputFormat, c.out, c.headerTemplate, c.template) +} + +func RenderIterator[T any](ctx context.Context, i listing.Iterator[T]) error { + c := fromContext(ctx) + return renderWithTemplate(newIteratorRenderer(i), ctx, c.outputFormat, c.out, c.headerTemplate, c.template) +} + +func RenderWithTemplate(ctx context.Context, v any, headerTemplate, template string) error { + c := fromContext(ctx) + if _, ok := v.(listingInterface); ok { + panic("use RenderIteratorWithTemplate instead") + } + return renderWithTemplate(newRenderer(v), ctx, c.outputFormat, c.out, headerTemplate, template) +} + +func RenderIteratorWithTemplate[T any](ctx context.Context, i listing.Iterator[T], headerTemplate, template string) error { + c := fromContext(ctx) + return renderWithTemplate(newIteratorRenderer(i), ctx, c.outputFormat, c.out, headerTemplate, template) +} + +func RenderJson(ctx context.Context, v any) error { + c := fromContext(ctx) + if _, ok := v.(listingInterface); ok { + panic("use RenderIteratorJson instead") + } + return renderWithTemplate(newRenderer(v), ctx, flags.OutputJSON, c.out, c.headerTemplate, c.template) +} + +func RenderIteratorJson[T any](ctx context.Context, i listing.Iterator[T]) error { + c := fromContext(ctx) + return renderWithTemplate(newIteratorRenderer(i), ctx, c.outputFormat, c.out, c.headerTemplate, c.template) +} + +func renderUsingTemplate(ctx context.Context, r templateRenderer, w io.Writer, headerTmpl, tmpl string) error { tw := tabwriter.NewWriter(w, 0, 4, 2, ' ', 0) - t, err := template.New("command").Funcs(template.FuncMap{ + base := template.New("command").Funcs(template.FuncMap{ // we render colored output if stdout is TTY, otherwise we render text. // in the future we'll check if we can explicitly check for stderr being // a TTY @@ -116,11 +350,24 @@ func renderTemplate(w io.Writer, tmpl string, v any) error { } return string(out), nil }, - }).Parse(tmpl) + }) + if headerTmpl != "" { + headerT, err := base.Parse(headerTmpl) + if err != nil { + return err + } + err = headerT.Execute(tw, nil) + if err != nil { + return err + } + tw.Write([]byte("\n")) + // Do not flush here. Instead, allow the first 100 resources to determine the initial spacing of the header columns. + } + t, err := base.Parse(tmpl) if err != nil { return err } - err = t.Execute(tw, v) + err = r.renderTemplate(ctx, t, tw) if err != nil { return err } diff --git a/libs/cmdio/render_test.go b/libs/cmdio/render_test.go new file mode 100644 index 00000000..6bde446c --- /dev/null +++ b/libs/cmdio/render_test.go @@ -0,0 +1,190 @@ +package cmdio + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "strings" + "testing" + + "github.com/databricks/cli/libs/flags" + "github.com/databricks/databricks-sdk-go/listing" + "github.com/databricks/databricks-sdk-go/service/provisioning" + "github.com/stretchr/testify/assert" +) + +type testCase struct { + name string + v any + outputFormat flags.Output + headerTemplate string + template string + expected string + errMessage string +} + +var dummyWorkspace1 = provisioning.Workspace{ + WorkspaceId: 123, + WorkspaceName: "abc", +} + +var dummyWorkspace2 = provisioning.Workspace{ + WorkspaceId: 456, + WorkspaceName: "def", +} + +type dummyIterator struct { + items []*provisioning.Workspace +} + +func (d *dummyIterator) HasNext(_ context.Context) bool { + return len(d.items) > 0 +} + +func (d *dummyIterator) Next(ctx context.Context) (*provisioning.Workspace, error) { + if !d.HasNext(ctx) { + return nil, errors.New("no more items") + } + item := d.items[0] + d.items = d.items[1:] + return item, nil +} + +func makeWorkspaces(count int) []*provisioning.Workspace { + res := make([]*provisioning.Workspace, 0, count) + next := []*provisioning.Workspace{&dummyWorkspace1, &dummyWorkspace2} + for i := 0; i < count; i++ { + n := next[0] + next = append(next[1:], n) + res = append(res, n) + } + return res +} + +func makeIterator(count int) listing.Iterator[*provisioning.Workspace] { + items := make([]*provisioning.Workspace, 0, count) + items = append(items, makeWorkspaces(count)...) + return &dummyIterator{ + items: items, + } +} + +func makeBigOutput(count int) string { + res := bytes.Buffer{} + for _, ws := range makeWorkspaces(count) { + res.Write([]byte(fmt.Sprintf("%d %s\n", ws.WorkspaceId, ws.WorkspaceName))) + } + return res.String() +} + +func must[T any](a T, e error) T { + if e != nil { + panic(e) + } + return a +} + +var testCases = []testCase{ + { + name: "Workspace with header and template", + v: dummyWorkspace1, + outputFormat: flags.OutputText, + headerTemplate: "id\tname", + template: "{{.WorkspaceId}}\t{{.WorkspaceName}}", + expected: `id name +123 abc`, + }, + { + name: "Workspace with no header and template", + v: dummyWorkspace1, + outputFormat: flags.OutputText, + template: "{{.WorkspaceId}}\t{{.WorkspaceName}}", + expected: `123 abc`, + }, + { + name: "Workspace with no header and no template", + v: dummyWorkspace1, + outputFormat: flags.OutputText, + expected: `{ + "workspace_id":123, + "workspace_name":"abc" +} +`, + }, + { + name: "Workspace Iterator with header and template", + v: makeIterator(2), + outputFormat: flags.OutputText, + headerTemplate: "id\tname", + template: "{{range .}}{{.WorkspaceId}}\t{{.WorkspaceName}}\n{{end}}", + expected: `id name +123 abc +456 def +`, + }, + { + name: "Workspace Iterator with no header and template", + v: makeIterator(2), + outputFormat: flags.OutputText, + template: "{{range .}}{{.WorkspaceId}}\t{{.WorkspaceName}}\n{{end}}", + expected: `123 abc +456 def +`, + }, + { + name: "Workspace Iterator with no header and no template", + v: makeIterator(2), + outputFormat: flags.OutputText, + expected: string(must(json.MarshalIndent(makeWorkspaces(2), "", " "))) + "\n", + }, + { + name: "Big Workspace Iterator with template", + v: makeIterator(234), + outputFormat: flags.OutputText, + headerTemplate: "id\tname", + template: "{{range .}}{{.WorkspaceId}}\t{{.WorkspaceName}}\n{{end}}", + expected: "id name\n" + makeBigOutput(234), + }, + { + name: "Big Workspace Iterator with no template", + v: makeIterator(234), + outputFormat: flags.OutputText, + expected: string(must(json.MarshalIndent(makeWorkspaces(234), "", " "))) + "\n", + }, + { + name: "io.Reader", + v: strings.NewReader("a test"), + outputFormat: flags.OutputText, + expected: "a test", + }, + { + name: "io.Reader", + v: strings.NewReader("a test"), + outputFormat: flags.OutputJSON, + errMessage: "json output not supported", + }, +} + +func TestRender(t *testing.T) { + for _, c := range testCases { + t.Run(c.name, func(t *testing.T) { + output := &bytes.Buffer{} + cmdIO := NewIO(c.outputFormat, nil, output, output, c.headerTemplate, c.template) + ctx := InContext(context.Background(), cmdIO) + var err error + if vv, ok := c.v.(listing.Iterator[*provisioning.Workspace]); ok { + err = RenderIterator(ctx, vv) + } else { + err = Render(ctx, c.v) + } + if c.errMessage != "" { + assert.ErrorContains(t, err, c.errMessage) + } else { + assert.NoError(t, err) + assert.Equal(t, c.expected, output.String()) + } + }) + } +} diff --git a/libs/databrickscfg/cfgpickers/clusters_test.go b/libs/databrickscfg/cfgpickers/clusters_test.go index 8afcd6d0..2e62f93a 100644 --- a/libs/databrickscfg/cfgpickers/clusters_test.go +++ b/libs/databrickscfg/cfgpickers/clusters_test.go @@ -115,7 +115,7 @@ func TestFirstCompatibleCluster(t *testing.T) { w := databricks.Must(databricks.NewWorkspaceClient((*databricks.Config)(cfg))) ctx := context.Background() - ctx = cmdio.InContext(ctx, cmdio.NewIO(flags.OutputText, &bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{}, "...")) + ctx = cmdio.InContext(ctx, cmdio.NewIO(flags.OutputText, &bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{}, "", "...")) clusterID, err := AskForCluster(ctx, w, WithDatabricksConnect("13.1")) require.NoError(t, err) require.Equal(t, "bcd-id", clusterID) @@ -162,7 +162,7 @@ func TestNoCompatibleClusters(t *testing.T) { w := databricks.Must(databricks.NewWorkspaceClient((*databricks.Config)(cfg))) ctx := context.Background() - ctx = cmdio.InContext(ctx, cmdio.NewIO(flags.OutputText, &bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{}, "...")) + ctx = cmdio.InContext(ctx, cmdio.NewIO(flags.OutputText, &bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{}, "", "...")) _, err := AskForCluster(ctx, w, WithDatabricksConnect("13.1")) require.Equal(t, ErrNoCompatibleClusters, err) } diff --git a/libs/template/helpers_test.go b/libs/template/helpers_test.go index d495ae89..a07b26f8 100644 --- a/libs/template/helpers_test.go +++ b/libs/template/helpers_test.go @@ -111,7 +111,7 @@ func TestWorkspaceHost(t *testing.T) { func TestWorkspaceHostNotConfigured(t *testing.T) { ctx := context.Background() - cmd := cmdio.NewIO(flags.OutputJSON, strings.NewReader(""), os.Stdout, os.Stderr, "template") + cmd := cmdio.NewIO(flags.OutputJSON, strings.NewReader(""), os.Stdout, os.Stderr, "", "template") ctx = cmdio.InContext(ctx, cmd) tmpDir := t.TempDir()