From 84fc1ed13162fafd54b6cebc3df991202acc431e Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Tue, 1 Oct 2024 12:44:47 +0200 Subject: [PATCH 01/24] Upgrade to Go SDK 0.47.0 (#1799) ## Changes Upgrade to Go SDK 0.47.0 ## Tests --- .codegen/_openapi_sha | 2 +- .gitattributes | 3 + bundle/schema/jsonschema.json | 152 +++++++++++- .../disable-legacy-features.go | 215 +++++++++++++++++ cmd/account/settings/settings.go | 2 + cmd/workspace/apps/apps.go | 83 ++++--- cmd/workspace/clusters/clusters.go | 10 +- cmd/workspace/cmd.go | 2 + .../disable-legacy-access.go | 217 ++++++++++++++++++ cmd/workspace/pipelines/pipelines.go | 1 + .../serving-endpoints/serving-endpoints.go | 80 ++++++- cmd/workspace/settings/settings.go | 2 + cmd/workspace/tables/tables.go | 3 + .../temporary-table-credentials.go | 122 ++++++++++ go.mod | 2 +- go.sum | 4 +- 16 files changed, 851 insertions(+), 49 deletions(-) create mode 100755 cmd/account/disable-legacy-features/disable-legacy-features.go create mode 100755 cmd/workspace/disable-legacy-access/disable-legacy-access.go create mode 100755 cmd/workspace/temporary-table-credentials/temporary-table-credentials.go diff --git a/.codegen/_openapi_sha b/.codegen/_openapi_sha index 4ceeab3d..ffd6f58d 100644 --- a/.codegen/_openapi_sha +++ b/.codegen/_openapi_sha @@ -1 +1 @@ -d05898328669a3f8ab0c2ecee37db2673d3ea3f7 \ No newline at end of file +6f6b1371e640f2dfeba72d365ac566368656f6b6 \ No newline at end of file diff --git a/.gitattributes b/.gitattributes index f35c4f81..2470eb33 100755 --- a/.gitattributes +++ b/.gitattributes @@ -6,6 +6,7 @@ cmd/account/cmd.go linguist-generated=true cmd/account/credentials/credentials.go linguist-generated=true cmd/account/csp-enablement-account/csp-enablement-account.go linguist-generated=true cmd/account/custom-app-integration/custom-app-integration.go linguist-generated=true +cmd/account/disable-legacy-features/disable-legacy-features.go linguist-generated=true cmd/account/encryption-keys/encryption-keys.go linguist-generated=true cmd/account/esm-enablement-account/esm-enablement-account.go linguist-generated=true cmd/account/groups/groups.go linguist-generated=true @@ -52,6 +53,7 @@ cmd/workspace/dashboard-widgets/dashboard-widgets.go linguist-generated=true cmd/workspace/dashboards/dashboards.go linguist-generated=true cmd/workspace/data-sources/data-sources.go linguist-generated=true cmd/workspace/default-namespace/default-namespace.go linguist-generated=true +cmd/workspace/disable-legacy-access/disable-legacy-access.go linguist-generated=true cmd/workspace/enhanced-security-monitoring/enhanced-security-monitoring.go linguist-generated=true cmd/workspace/experiments/experiments.go linguist-generated=true cmd/workspace/external-locations/external-locations.go linguist-generated=true @@ -108,6 +110,7 @@ cmd/workspace/storage-credentials/storage-credentials.go linguist-generated=true cmd/workspace/system-schemas/system-schemas.go linguist-generated=true cmd/workspace/table-constraints/table-constraints.go linguist-generated=true cmd/workspace/tables/tables.go linguist-generated=true +cmd/workspace/temporary-table-credentials/temporary-table-credentials.go linguist-generated=true cmd/workspace/token-management/token-management.go linguist-generated=true cmd/workspace/tokens/tokens.go linguist-generated=true cmd/workspace/users/users.go linguist-generated=true diff --git a/bundle/schema/jsonschema.json b/bundle/schema/jsonschema.json index 2db1a5ab..afdf9fb9 100644 --- a/bundle/schema/jsonschema.json +++ b/bundle/schema/jsonschema.json @@ -59,6 +59,127 @@ "cli": { "bundle": { "config": { + "resources.Cluster": { + "anyOf": [ + { + "type": "object", + "properties": { + "apply_policy_default_values": { + "description": "When set to true, fixed and default values from the policy will be used for fields that are omitted. When set to false, only fixed values from the policy will be applied.", + "$ref": "#/$defs/bool" + }, + "autoscale": { + "description": "Parameters needed in order to automatically scale clusters up and down based on load.\nNote: autoscaling works best with DB runtime versions 3.0 or later.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/compute.AutoScale" + }, + "autotermination_minutes": { + "description": "Automatically terminates the cluster after it is inactive for this time in minutes. If not set,\nthis cluster will not be automatically terminated. If specified, the threshold must be between\n10 and 10000 minutes.\nUsers can also set this value to 0 to explicitly disable automatic termination.", + "$ref": "#/$defs/int" + }, + "aws_attributes": { + "description": "Attributes related to clusters running on Amazon Web Services.\nIf not specified at cluster creation, a set of default values will be used.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/compute.AwsAttributes" + }, + "azure_attributes": { + "description": "Attributes related to clusters running on Microsoft Azure.\nIf not specified at cluster creation, a set of default values will be used.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/compute.AzureAttributes" + }, + "cluster_log_conf": { + "description": "The configuration for delivering spark logs to a long-term storage destination.\nTwo kinds of destinations (dbfs and s3) are supported. Only one destination can be specified\nfor one cluster. If the conf is given, the logs will be delivered to the destination every\n`5 mins`. The destination of driver logs is `$destination/$clusterId/driver`, while\nthe destination of executor logs is `$destination/$clusterId/executor`.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/compute.ClusterLogConf" + }, + "cluster_name": { + "description": "Cluster name requested by the user. This doesn't have to be unique.\nIf not specified at creation, the cluster name will be an empty string.\n", + "$ref": "#/$defs/string" + }, + "custom_tags": { + "description": "Additional tags for cluster resources. Databricks will tag all cluster resources (e.g., AWS\ninstances and EBS volumes) with these tags in addition to `default_tags`. Notes:\n\n- Currently, Databricks allows at most 45 custom tags\n\n- Clusters can only reuse cloud resources if the resources' tags are a subset of the cluster tags", + "$ref": "#/$defs/map/string" + }, + "data_security_mode": { + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/compute.DataSecurityMode" + }, + "docker_image": { + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/compute.DockerImage" + }, + "driver_instance_pool_id": { + "description": "The optional ID of the instance pool for the driver of the cluster belongs.\nThe pool cluster uses the instance pool with id (instance_pool_id) if the driver pool is not\nassigned.", + "$ref": "#/$defs/string" + }, + "driver_node_type_id": { + "description": "The node type of the Spark driver. Note that this field is optional;\nif unset, the driver node type will be set as the same value\nas `node_type_id` defined above.\n", + "$ref": "#/$defs/string" + }, + "enable_elastic_disk": { + "description": "Autoscaling Local Storage: when enabled, this cluster will dynamically acquire additional disk\nspace when its Spark workers are running low on disk space. This feature requires specific AWS\npermissions to function correctly - refer to the User Guide for more details.", + "$ref": "#/$defs/bool" + }, + "enable_local_disk_encryption": { + "description": "Whether to enable LUKS on cluster VMs' local disks", + "$ref": "#/$defs/bool" + }, + "gcp_attributes": { + "description": "Attributes related to clusters running on Google Cloud Platform.\nIf not specified at cluster creation, a set of default values will be used.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/compute.GcpAttributes" + }, + "init_scripts": { + "description": "The configuration for storing init scripts. Any number of destinations can be specified. The scripts are executed sequentially in the order provided. If `cluster_log_conf` is specified, init script logs are sent to `\u003cdestination\u003e/\u003ccluster-ID\u003e/init_scripts`.", + "$ref": "#/$defs/slice/github.com/databricks/databricks-sdk-go/service/compute.InitScriptInfo" + }, + "instance_pool_id": { + "description": "The optional ID of the instance pool to which the cluster belongs.", + "$ref": "#/$defs/string" + }, + "node_type_id": { + "description": "This field encodes, through a single value, the resources available to each of\nthe Spark nodes in this cluster. For example, the Spark nodes can be provisioned\nand optimized for memory or compute intensive workloads. A list of available node\ntypes can be retrieved by using the :method:clusters/listNodeTypes API call.\n", + "$ref": "#/$defs/string" + }, + "num_workers": { + "description": "Number of worker nodes that this cluster should have. A cluster has one Spark Driver\nand `num_workers` Executors for a total of `num_workers` + 1 Spark nodes.\n\nNote: When reading the properties of a cluster, this field reflects the desired number\nof workers rather than the actual current number of workers. For instance, if a cluster\nis resized from 5 to 10 workers, this field will immediately be updated to reflect\nthe target size of 10 workers, whereas the workers listed in `spark_info` will gradually\nincrease from 5 to 10 as the new nodes are provisioned.", + "$ref": "#/$defs/int" + }, + "permissions": { + "$ref": "#/$defs/slice/github.com/databricks/cli/bundle/config/resources.Permission" + }, + "policy_id": { + "description": "The ID of the cluster policy used to create the cluster if applicable.", + "$ref": "#/$defs/string" + }, + "runtime_engine": { + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/compute.RuntimeEngine" + }, + "single_user_name": { + "description": "Single user name if data_security_mode is `SINGLE_USER`", + "$ref": "#/$defs/string" + }, + "spark_conf": { + "description": "An object containing a set of optional, user-specified Spark configuration key-value pairs.\nUsers can also pass in a string of extra JVM options to the driver and the executors via\n`spark.driver.extraJavaOptions` and `spark.executor.extraJavaOptions` respectively.\n", + "$ref": "#/$defs/map/string" + }, + "spark_env_vars": { + "description": "An object containing a set of optional, user-specified environment variable key-value pairs.\nPlease note that key-value pair of the form (X,Y) will be exported as is (i.e.,\n`export X='Y'`) while launching the driver and workers.\n\nIn order to specify an additional set of `SPARK_DAEMON_JAVA_OPTS`, we recommend appending\nthem to `$SPARK_DAEMON_JAVA_OPTS` as shown in the example below. This ensures that all\ndefault databricks managed environmental variables are included as well.\n\nExample Spark environment variables:\n`{\"SPARK_WORKER_MEMORY\": \"28000m\", \"SPARK_LOCAL_DIRS\": \"/local_disk0\"}` or\n`{\"SPARK_DAEMON_JAVA_OPTS\": \"$SPARK_DAEMON_JAVA_OPTS -Dspark.shuffle.service.enabled=true\"}`", + "$ref": "#/$defs/map/string" + }, + "spark_version": { + "description": "The Spark version of the cluster, e.g. `3.3.x-scala2.11`.\nA list of available Spark versions can be retrieved by using\nthe :method:clusters/sparkVersions API call.\n", + "$ref": "#/$defs/string" + }, + "ssh_public_keys": { + "description": "SSH public key contents that will be added to each Spark node in this cluster. The\ncorresponding private keys can be used to login with the user name `ubuntu` on port `2200`.\nUp to 10 keys can be specified.", + "$ref": "#/$defs/slice/string" + }, + "workload_type": { + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/compute.WorkloadType" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "resources.Grant": { "anyOf": [ { @@ -109,7 +230,7 @@ "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/jobs.JobEmailNotifications" }, "environments": { - "description": "A list of task execution environment specifications that can be referenced by tasks of this job.", + "description": "A list of task execution environment specifications that can be referenced by serverless tasks of this job.\nAn environment is required to be present for serverless tasks.\nFor serverless notebook tasks, the environment is accessible in the notebook environment panel.\nFor other serverless tasks, the task environment is required to be specified using environment_key in the task settings.", "$ref": "#/$defs/slice/github.com/databricks/databricks-sdk-go/service/jobs.JobEnvironment" }, "format": { @@ -293,7 +414,7 @@ "$ref": "#/$defs/slice/github.com/databricks/cli/bundle/config/resources.Permission" }, "rate_limits": { - "description": "Rate limits to be applied to the serving endpoint. NOTE: only external and foundation model endpoints are supported as of now.", + "description": "Rate limits to be applied to the serving endpoint. NOTE: this field is deprecated, please use AI Gateway to manage rate limits.", "$ref": "#/$defs/slice/github.com/databricks/databricks-sdk-go/service/serving.RateLimit" }, "route_optimized": { @@ -747,6 +868,9 @@ { "type": "object", "properties": { + "cluster_id": { + "$ref": "#/$defs/string" + }, "compute_id": { "$ref": "#/$defs/string" }, @@ -923,6 +1047,9 @@ { "type": "object", "properties": { + "clusters": { + "$ref": "#/$defs/map/github.com/databricks/cli/bundle/config/resources.Cluster" + }, "experiments": { "$ref": "#/$defs/map/github.com/databricks/cli/bundle/config/resources.MlflowExperiment" }, @@ -990,6 +1117,9 @@ "bundle": { "$ref": "#/$defs/github.com/databricks/cli/bundle/config.Bundle" }, + "cluster_id": { + "$ref": "#/$defs/string" + }, "compute_id": { "$ref": "#/$defs/string" }, @@ -2028,7 +2158,7 @@ }, "compute.RuntimeEngine": { "type": "string", - "description": "Decides which runtime engine to be use, e.g. Standard vs. Photon. If unspecified, the runtime\nengine is inferred from spark_version.", + "description": "Determines the cluster's runtime engine, either standard or Photon.\n\nThis field is not compatible with legacy `spark_version` values that contain `-photon-`.\nRemove `-photon-` from the `spark_version` and set `runtime_engine` to `PHOTON`.\n\nIf left unspecified, the runtime engine defaults to standard unless the spark_version\ncontains -photon-, in which case Photon will be used.\n", "enum": [ "NULL", "STANDARD", @@ -2610,7 +2740,7 @@ "anyOf": [ { "type": "object", - "description": "Write-only setting, available only in Create/Update/Reset and Submit calls. Specifies the user or service principal that the job runs as. If not specified, the job runs as the user who created the job.\n\nOnly `user_name` or `service_principal_name` can be specified. If both are specified, an error is thrown.", + "description": "Write-only setting. Specifies the user, service principal or group that the job/pipeline runs as. If not specified, the job/pipeline runs as the user who created the job/pipeline.\n\nExactly one of `user_name`, `service_principal_name`, `group_name` should be specified. If not, an error is thrown.", "properties": { "service_principal_name": { "description": "Application ID of an active service principal. Setting this field requires the `servicePrincipal/user` role.", @@ -4904,6 +5034,20 @@ "cli": { "bundle": { "config": { + "resources.Cluster": { + "anyOf": [ + { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/github.com/databricks/cli/bundle/config/resources.Cluster" + } + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "resources.Job": { "anyOf": [ { diff --git a/cmd/account/disable-legacy-features/disable-legacy-features.go b/cmd/account/disable-legacy-features/disable-legacy-features.go new file mode 100755 index 00000000..6d25b943 --- /dev/null +++ b/cmd/account/disable-legacy-features/disable-legacy-features.go @@ -0,0 +1,215 @@ +// Code generated from OpenAPI specs by Databricks SDK Generator. DO NOT EDIT. + +package disable_legacy_features + +import ( + "fmt" + + "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/flags" + "github.com/databricks/databricks-sdk-go/service/settings" + "github.com/spf13/cobra" +) + +// Slice with functions to override default command behavior. +// Functions can be added from the `init()` function in manually curated files in this directory. +var cmdOverrides []func(*cobra.Command) + +func New() *cobra.Command { + cmd := &cobra.Command{ + Use: "disable-legacy-features", + Short: `Disable legacy features for new Databricks workspaces.`, + Long: `Disable legacy features for new Databricks workspaces. + + For newly created workspaces: 1. Disables the use of DBFS root and mounts. 2. + Hive Metastore will not be provisioned. 3. Disables the use of ‘No-isolation + clusters’. 4. Disables Databricks Runtime versions prior to 13.3LTS.`, + + // This service is being previewed; hide from help output. + Hidden: true, + } + + // Add methods + cmd.AddCommand(newDelete()) + cmd.AddCommand(newGet()) + cmd.AddCommand(newUpdate()) + + // Apply optional overrides to this command. + for _, fn := range cmdOverrides { + fn(cmd) + } + + return cmd +} + +// start delete command + +// Slice with functions to override default command behavior. +// Functions can be added from the `init()` function in manually curated files in this directory. +var deleteOverrides []func( + *cobra.Command, + *settings.DeleteDisableLegacyFeaturesRequest, +) + +func newDelete() *cobra.Command { + cmd := &cobra.Command{} + + var deleteReq settings.DeleteDisableLegacyFeaturesRequest + + // TODO: short flags + + cmd.Flags().StringVar(&deleteReq.Etag, "etag", deleteReq.Etag, `etag used for versioning.`) + + cmd.Use = "delete" + cmd.Short = `Delete the disable legacy features setting.` + cmd.Long = `Delete the disable legacy features setting. + + Deletes the disable legacy features setting.` + + cmd.Annotations = make(map[string]string) + + cmd.Args = func(cmd *cobra.Command, args []string) error { + check := root.ExactArgs(0) + return check(cmd, args) + } + + cmd.PreRunE = root.MustAccountClient + cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { + ctx := cmd.Context() + a := root.AccountClient(ctx) + + response, err := a.Settings.DisableLegacyFeatures().Delete(ctx, deleteReq) + if err != nil { + return err + } + return cmdio.Render(ctx, response) + } + + // Disable completions since they are not applicable. + // Can be overridden by manual implementation in `override.go`. + cmd.ValidArgsFunction = cobra.NoFileCompletions + + // Apply optional overrides to this command. + for _, fn := range deleteOverrides { + fn(cmd, &deleteReq) + } + + return cmd +} + +// start get command + +// Slice with functions to override default command behavior. +// Functions can be added from the `init()` function in manually curated files in this directory. +var getOverrides []func( + *cobra.Command, + *settings.GetDisableLegacyFeaturesRequest, +) + +func newGet() *cobra.Command { + cmd := &cobra.Command{} + + var getReq settings.GetDisableLegacyFeaturesRequest + + // TODO: short flags + + cmd.Flags().StringVar(&getReq.Etag, "etag", getReq.Etag, `etag used for versioning.`) + + cmd.Use = "get" + cmd.Short = `Get the disable legacy features setting.` + cmd.Long = `Get the disable legacy features setting. + + Gets the value of the disable legacy features setting.` + + cmd.Annotations = make(map[string]string) + + cmd.Args = func(cmd *cobra.Command, args []string) error { + check := root.ExactArgs(0) + return check(cmd, args) + } + + cmd.PreRunE = root.MustAccountClient + cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { + ctx := cmd.Context() + a := root.AccountClient(ctx) + + response, err := a.Settings.DisableLegacyFeatures().Get(ctx, getReq) + if err != nil { + return err + } + return cmdio.Render(ctx, response) + } + + // Disable completions since they are not applicable. + // Can be overridden by manual implementation in `override.go`. + cmd.ValidArgsFunction = cobra.NoFileCompletions + + // Apply optional overrides to this command. + for _, fn := range getOverrides { + fn(cmd, &getReq) + } + + return cmd +} + +// start update command + +// Slice with functions to override default command behavior. +// Functions can be added from the `init()` function in manually curated files in this directory. +var updateOverrides []func( + *cobra.Command, + *settings.UpdateDisableLegacyFeaturesRequest, +) + +func newUpdate() *cobra.Command { + cmd := &cobra.Command{} + + var updateReq settings.UpdateDisableLegacyFeaturesRequest + var updateJson flags.JsonFlag + + // TODO: short flags + cmd.Flags().Var(&updateJson, "json", `either inline JSON string or @path/to/file.json with request body`) + + cmd.Use = "update" + cmd.Short = `Update the disable legacy features setting.` + cmd.Long = `Update the disable legacy features setting. + + Updates the value of the disable legacy features setting.` + + cmd.Annotations = make(map[string]string) + + cmd.PreRunE = root.MustAccountClient + cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { + ctx := cmd.Context() + a := root.AccountClient(ctx) + + if cmd.Flags().Changed("json") { + err = updateJson.Unmarshal(&updateReq) + if err != nil { + return err + } + } else { + return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") + } + + response, err := a.Settings.DisableLegacyFeatures().Update(ctx, updateReq) + if err != nil { + return err + } + return cmdio.Render(ctx, response) + } + + // Disable completions since they are not applicable. + // Can be overridden by manual implementation in `override.go`. + cmd.ValidArgsFunction = cobra.NoFileCompletions + + // Apply optional overrides to this command. + for _, fn := range updateOverrides { + fn(cmd, &updateReq) + } + + return cmd +} + +// end service DisableLegacyFeatures diff --git a/cmd/account/settings/settings.go b/cmd/account/settings/settings.go index a750e81e..9a9cd44b 100755 --- a/cmd/account/settings/settings.go +++ b/cmd/account/settings/settings.go @@ -6,6 +6,7 @@ import ( "github.com/spf13/cobra" csp_enablement_account "github.com/databricks/cli/cmd/account/csp-enablement-account" + disable_legacy_features "github.com/databricks/cli/cmd/account/disable-legacy-features" esm_enablement_account "github.com/databricks/cli/cmd/account/esm-enablement-account" personal_compute "github.com/databricks/cli/cmd/account/personal-compute" ) @@ -27,6 +28,7 @@ func New() *cobra.Command { // Add subservices cmd.AddCommand(csp_enablement_account.New()) + cmd.AddCommand(disable_legacy_features.New()) cmd.AddCommand(esm_enablement_account.New()) cmd.AddCommand(personal_compute.New()) diff --git a/cmd/workspace/apps/apps.go b/cmd/workspace/apps/apps.go index bc3fbe92..baec6d03 100755 --- a/cmd/workspace/apps/apps.go +++ b/cmd/workspace/apps/apps.go @@ -75,8 +75,8 @@ func newCreate() *cobra.Command { var createSkipWait bool var createTimeout time.Duration - cmd.Flags().BoolVar(&createSkipWait, "no-wait", createSkipWait, `do not wait to reach IDLE state`) - cmd.Flags().DurationVar(&createTimeout, "timeout", 20*time.Minute, `maximum amount of time to reach IDLE state`) + cmd.Flags().BoolVar(&createSkipWait, "no-wait", createSkipWait, `do not wait to reach ACTIVE state`) + cmd.Flags().DurationVar(&createTimeout, "timeout", 20*time.Minute, `maximum amount of time to reach ACTIVE state`) // TODO: short flags cmd.Flags().Var(&createJson, "json", `either inline JSON string or @path/to/file.json with request body`) @@ -130,13 +130,13 @@ func newCreate() *cobra.Command { } spinner := cmdio.Spinner(ctx) info, err := wait.OnProgress(func(i *apps.App) { - if i.Status == nil { + if i.ComputeStatus == nil { return } - status := i.Status.State + status := i.ComputeStatus.State statusMessage := fmt.Sprintf("current status: %s", status) - if i.Status != nil { - statusMessage = i.Status.Message + if i.ComputeStatus != nil { + statusMessage = i.ComputeStatus.Message } spinner <- statusMessage }).GetWithTimeout(createTimeout) @@ -198,11 +198,11 @@ func newDelete() *cobra.Command { deleteReq.Name = args[0] - err = w.Apps.Delete(ctx, deleteReq) + response, err := w.Apps.Delete(ctx, deleteReq) if err != nil { return err } - return nil + return cmdio.Render(ctx, response) } // Disable completions since they are not applicable. @@ -240,35 +240,23 @@ func newDeploy() *cobra.Command { // TODO: short flags cmd.Flags().Var(&deployJson, "json", `either inline JSON string or @path/to/file.json with request body`) + cmd.Flags().StringVar(&deployReq.DeploymentId, "deployment-id", deployReq.DeploymentId, `The unique id of the deployment.`) cmd.Flags().Var(&deployReq.Mode, "mode", `The mode of which the deployment will manage the source code. Supported values: [AUTO_SYNC, SNAPSHOT]`) + cmd.Flags().StringVar(&deployReq.SourceCodePath, "source-code-path", deployReq.SourceCodePath, `The workspace file system path of the source code used to create the app deployment.`) - cmd.Use = "deploy APP_NAME SOURCE_CODE_PATH" + cmd.Use = "deploy APP_NAME" cmd.Short = `Create an app deployment.` cmd.Long = `Create an app deployment. Creates an app deployment for the app with the supplied name. Arguments: - APP_NAME: The name of the app. - SOURCE_CODE_PATH: The workspace file system path of the source code used to create the app - deployment. This is different from - deployment_artifacts.source_code_path, which is the path used by the - deployed app. The former refers to the original source code location of - the app in the workspace during deployment creation, whereas the latter - provides a system generated stable snapshotted source code path used by - the deployment.` + APP_NAME: The name of the app.` cmd.Annotations = make(map[string]string) cmd.Args = func(cmd *cobra.Command, args []string) error { - if cmd.Flags().Changed("json") { - err := root.ExactArgs(1)(cmd, args) - if err != nil { - return fmt.Errorf("when --json flag is specified, provide only APP_NAME as positional arguments. Provide 'source_code_path' in your JSON input") - } - return nil - } - check := root.ExactArgs(2) + check := root.ExactArgs(1) return check(cmd, args) } @@ -284,9 +272,6 @@ func newDeploy() *cobra.Command { } } deployReq.AppName = args[0] - if !cmd.Flags().Changed("json") { - deployReq.SourceCodePath = args[1] - } wait, err := w.Apps.Deploy(ctx, deployReq) if err != nil { @@ -759,8 +744,8 @@ func newStart() *cobra.Command { var startSkipWait bool var startTimeout time.Duration - cmd.Flags().BoolVar(&startSkipWait, "no-wait", startSkipWait, `do not wait to reach SUCCEEDED state`) - cmd.Flags().DurationVar(&startTimeout, "timeout", 20*time.Minute, `maximum amount of time to reach SUCCEEDED state`) + cmd.Flags().BoolVar(&startSkipWait, "no-wait", startSkipWait, `do not wait to reach ACTIVE state`) + cmd.Flags().DurationVar(&startTimeout, "timeout", 20*time.Minute, `maximum amount of time to reach ACTIVE state`) // TODO: short flags cmd.Use = "start NAME" @@ -794,14 +779,14 @@ func newStart() *cobra.Command { return cmdio.Render(ctx, wait.Response) } spinner := cmdio.Spinner(ctx) - info, err := wait.OnProgress(func(i *apps.AppDeployment) { - if i.Status == nil { + info, err := wait.OnProgress(func(i *apps.App) { + if i.ComputeStatus == nil { return } - status := i.Status.State + status := i.ComputeStatus.State statusMessage := fmt.Sprintf("current status: %s", status) - if i.Status != nil { - statusMessage = i.Status.Message + if i.ComputeStatus != nil { + statusMessage = i.ComputeStatus.Message } spinner <- statusMessage }).GetWithTimeout(startTimeout) @@ -838,6 +823,11 @@ func newStop() *cobra.Command { var stopReq apps.StopAppRequest + var stopSkipWait bool + var stopTimeout time.Duration + + cmd.Flags().BoolVar(&stopSkipWait, "no-wait", stopSkipWait, `do not wait to reach STOPPED state`) + cmd.Flags().DurationVar(&stopTimeout, "timeout", 20*time.Minute, `maximum amount of time to reach STOPPED state`) // TODO: short flags cmd.Use = "stop NAME" @@ -863,11 +853,30 @@ func newStop() *cobra.Command { stopReq.Name = args[0] - err = w.Apps.Stop(ctx, stopReq) + wait, err := w.Apps.Stop(ctx, stopReq) if err != nil { return err } - return nil + if stopSkipWait { + return cmdio.Render(ctx, wait.Response) + } + spinner := cmdio.Spinner(ctx) + info, err := wait.OnProgress(func(i *apps.App) { + if i.ComputeStatus == nil { + return + } + status := i.ComputeStatus.State + statusMessage := fmt.Sprintf("current status: %s", status) + if i.ComputeStatus != nil { + statusMessage = i.ComputeStatus.Message + } + spinner <- statusMessage + }).GetWithTimeout(stopTimeout) + close(spinner) + if err != nil { + return err + } + return cmdio.Render(ctx, info) } // Disable completions since they are not applicable. diff --git a/cmd/workspace/clusters/clusters.go b/cmd/workspace/clusters/clusters.go index a64a6ab7..b36102d9 100755 --- a/cmd/workspace/clusters/clusters.go +++ b/cmd/workspace/clusters/clusters.go @@ -217,7 +217,7 @@ func newCreate() *cobra.Command { cmd.Flags().StringVar(&createReq.NodeTypeId, "node-type-id", createReq.NodeTypeId, `This field encodes, through a single value, the resources available to each of the Spark nodes in this cluster.`) cmd.Flags().IntVar(&createReq.NumWorkers, "num-workers", createReq.NumWorkers, `Number of worker nodes that this cluster should have.`) cmd.Flags().StringVar(&createReq.PolicyId, "policy-id", createReq.PolicyId, `The ID of the cluster policy used to create the cluster if applicable.`) - cmd.Flags().Var(&createReq.RuntimeEngine, "runtime-engine", `Decides which runtime engine to be use, e.g. Supported values: [NULL, PHOTON, STANDARD]`) + cmd.Flags().Var(&createReq.RuntimeEngine, "runtime-engine", `Determines the cluster's runtime engine, either standard or Photon. Supported values: [NULL, PHOTON, STANDARD]`) cmd.Flags().StringVar(&createReq.SingleUserName, "single-user-name", createReq.SingleUserName, `Single user name if data_security_mode is SINGLE_USER.`) // TODO: map via StringToStringVar: spark_conf // TODO: map via StringToStringVar: spark_env_vars @@ -236,6 +236,12 @@ func newCreate() *cobra.Command { If Databricks acquires at least 85% of the requested on-demand nodes, cluster creation will succeed. Otherwise the cluster will terminate with an informative error message. + + Rather than authoring the cluster's JSON definition from scratch, Databricks + recommends filling out the [create compute UI] and then copying the generated + JSON definition from the UI. + + [create compute UI]: https://docs.databricks.com/compute/configure.html Arguments: SPARK_VERSION: The Spark version of the cluster, e.g. 3.3.x-scala2.11. A list of @@ -463,7 +469,7 @@ func newEdit() *cobra.Command { cmd.Flags().StringVar(&editReq.NodeTypeId, "node-type-id", editReq.NodeTypeId, `This field encodes, through a single value, the resources available to each of the Spark nodes in this cluster.`) cmd.Flags().IntVar(&editReq.NumWorkers, "num-workers", editReq.NumWorkers, `Number of worker nodes that this cluster should have.`) cmd.Flags().StringVar(&editReq.PolicyId, "policy-id", editReq.PolicyId, `The ID of the cluster policy used to create the cluster if applicable.`) - cmd.Flags().Var(&editReq.RuntimeEngine, "runtime-engine", `Decides which runtime engine to be use, e.g. Supported values: [NULL, PHOTON, STANDARD]`) + cmd.Flags().Var(&editReq.RuntimeEngine, "runtime-engine", `Determines the cluster's runtime engine, either standard or Photon. Supported values: [NULL, PHOTON, STANDARD]`) cmd.Flags().StringVar(&editReq.SingleUserName, "single-user-name", editReq.SingleUserName, `Single user name if data_security_mode is SINGLE_USER.`) // TODO: map via StringToStringVar: spark_conf // TODO: map via StringToStringVar: spark_env_vars diff --git a/cmd/workspace/cmd.go b/cmd/workspace/cmd.go index 11be8077..3fe5b268 100755 --- a/cmd/workspace/cmd.go +++ b/cmd/workspace/cmd.go @@ -76,6 +76,7 @@ import ( system_schemas "github.com/databricks/cli/cmd/workspace/system-schemas" table_constraints "github.com/databricks/cli/cmd/workspace/table-constraints" tables "github.com/databricks/cli/cmd/workspace/tables" + temporary_table_credentials "github.com/databricks/cli/cmd/workspace/temporary-table-credentials" token_management "github.com/databricks/cli/cmd/workspace/token-management" tokens "github.com/databricks/cli/cmd/workspace/tokens" users "github.com/databricks/cli/cmd/workspace/users" @@ -165,6 +166,7 @@ func All() []*cobra.Command { out = append(out, system_schemas.New()) out = append(out, table_constraints.New()) out = append(out, tables.New()) + out = append(out, temporary_table_credentials.New()) out = append(out, token_management.New()) out = append(out, tokens.New()) out = append(out, users.New()) diff --git a/cmd/workspace/disable-legacy-access/disable-legacy-access.go b/cmd/workspace/disable-legacy-access/disable-legacy-access.go new file mode 100755 index 00000000..fea2b3c4 --- /dev/null +++ b/cmd/workspace/disable-legacy-access/disable-legacy-access.go @@ -0,0 +1,217 @@ +// Code generated from OpenAPI specs by Databricks SDK Generator. DO NOT EDIT. + +package disable_legacy_access + +import ( + "fmt" + + "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/flags" + "github.com/databricks/databricks-sdk-go/service/settings" + "github.com/spf13/cobra" +) + +// Slice with functions to override default command behavior. +// Functions can be added from the `init()` function in manually curated files in this directory. +var cmdOverrides []func(*cobra.Command) + +func New() *cobra.Command { + cmd := &cobra.Command{ + Use: "disable-legacy-access", + Short: `'Disabling legacy access' has the following impacts: 1.`, + Long: `'Disabling legacy access' has the following impacts: + + 1. Disables direct access to the Hive Metastore. However, you can still access + Hive Metastore through HMS Federation. 2. Disables Fallback Mode (docs link) + on any External Location access from the workspace. 3. Alters DBFS path access + to use External Location permissions in place of legacy credentials. 4. + Enforces Unity Catalog access on all path based access.`, + + // This service is being previewed; hide from help output. + Hidden: true, + } + + // Add methods + cmd.AddCommand(newDelete()) + cmd.AddCommand(newGet()) + cmd.AddCommand(newUpdate()) + + // Apply optional overrides to this command. + for _, fn := range cmdOverrides { + fn(cmd) + } + + return cmd +} + +// start delete command + +// Slice with functions to override default command behavior. +// Functions can be added from the `init()` function in manually curated files in this directory. +var deleteOverrides []func( + *cobra.Command, + *settings.DeleteDisableLegacyAccessRequest, +) + +func newDelete() *cobra.Command { + cmd := &cobra.Command{} + + var deleteReq settings.DeleteDisableLegacyAccessRequest + + // TODO: short flags + + cmd.Flags().StringVar(&deleteReq.Etag, "etag", deleteReq.Etag, `etag used for versioning.`) + + cmd.Use = "delete" + cmd.Short = `Delete Legacy Access Disablement Status.` + cmd.Long = `Delete Legacy Access Disablement Status. + + Deletes legacy access disablement status.` + + cmd.Annotations = make(map[string]string) + + cmd.Args = func(cmd *cobra.Command, args []string) error { + check := root.ExactArgs(0) + return check(cmd, args) + } + + cmd.PreRunE = root.MustWorkspaceClient + cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { + ctx := cmd.Context() + w := root.WorkspaceClient(ctx) + + response, err := w.Settings.DisableLegacyAccess().Delete(ctx, deleteReq) + if err != nil { + return err + } + return cmdio.Render(ctx, response) + } + + // Disable completions since they are not applicable. + // Can be overridden by manual implementation in `override.go`. + cmd.ValidArgsFunction = cobra.NoFileCompletions + + // Apply optional overrides to this command. + for _, fn := range deleteOverrides { + fn(cmd, &deleteReq) + } + + return cmd +} + +// start get command + +// Slice with functions to override default command behavior. +// Functions can be added from the `init()` function in manually curated files in this directory. +var getOverrides []func( + *cobra.Command, + *settings.GetDisableLegacyAccessRequest, +) + +func newGet() *cobra.Command { + cmd := &cobra.Command{} + + var getReq settings.GetDisableLegacyAccessRequest + + // TODO: short flags + + cmd.Flags().StringVar(&getReq.Etag, "etag", getReq.Etag, `etag used for versioning.`) + + cmd.Use = "get" + cmd.Short = `Retrieve Legacy Access Disablement Status.` + cmd.Long = `Retrieve Legacy Access Disablement Status. + + Retrieves legacy access disablement Status.` + + cmd.Annotations = make(map[string]string) + + cmd.Args = func(cmd *cobra.Command, args []string) error { + check := root.ExactArgs(0) + return check(cmd, args) + } + + cmd.PreRunE = root.MustWorkspaceClient + cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { + ctx := cmd.Context() + w := root.WorkspaceClient(ctx) + + response, err := w.Settings.DisableLegacyAccess().Get(ctx, getReq) + if err != nil { + return err + } + return cmdio.Render(ctx, response) + } + + // Disable completions since they are not applicable. + // Can be overridden by manual implementation in `override.go`. + cmd.ValidArgsFunction = cobra.NoFileCompletions + + // Apply optional overrides to this command. + for _, fn := range getOverrides { + fn(cmd, &getReq) + } + + return cmd +} + +// start update command + +// Slice with functions to override default command behavior. +// Functions can be added from the `init()` function in manually curated files in this directory. +var updateOverrides []func( + *cobra.Command, + *settings.UpdateDisableLegacyAccessRequest, +) + +func newUpdate() *cobra.Command { + cmd := &cobra.Command{} + + var updateReq settings.UpdateDisableLegacyAccessRequest + var updateJson flags.JsonFlag + + // TODO: short flags + cmd.Flags().Var(&updateJson, "json", `either inline JSON string or @path/to/file.json with request body`) + + cmd.Use = "update" + cmd.Short = `Update Legacy Access Disablement Status.` + cmd.Long = `Update Legacy Access Disablement Status. + + Updates legacy access disablement status.` + + cmd.Annotations = make(map[string]string) + + cmd.PreRunE = root.MustWorkspaceClient + cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { + ctx := cmd.Context() + w := root.WorkspaceClient(ctx) + + if cmd.Flags().Changed("json") { + err = updateJson.Unmarshal(&updateReq) + if err != nil { + return err + } + } else { + return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") + } + + response, err := w.Settings.DisableLegacyAccess().Update(ctx, updateReq) + if err != nil { + return err + } + return cmdio.Render(ctx, response) + } + + // Disable completions since they are not applicable. + // Can be overridden by manual implementation in `override.go`. + cmd.ValidArgsFunction = cobra.NoFileCompletions + + // Apply optional overrides to this command. + for _, fn := range updateOverrides { + fn(cmd, &updateReq) + } + + return cmd +} + +// end service DisableLegacyAccess diff --git a/cmd/workspace/pipelines/pipelines.go b/cmd/workspace/pipelines/pipelines.go index f1cc4e3f..5b4d9645 100755 --- a/cmd/workspace/pipelines/pipelines.go +++ b/cmd/workspace/pipelines/pipelines.go @@ -935,6 +935,7 @@ func newUpdate() *cobra.Command { cmd.Flags().Var(&updateJson, "json", `either inline JSON string or @path/to/file.json with request body`) cmd.Flags().BoolVar(&updateReq.AllowDuplicateNames, "allow-duplicate-names", updateReq.AllowDuplicateNames, `If false, deployment will fail if name has changed and conflicts the name of another pipeline.`) + cmd.Flags().StringVar(&updateReq.BudgetPolicyId, "budget-policy-id", updateReq.BudgetPolicyId, `Budget policy of this pipeline.`) cmd.Flags().StringVar(&updateReq.Catalog, "catalog", updateReq.Catalog, `A catalog in Unity Catalog to publish data from this pipeline to.`) cmd.Flags().StringVar(&updateReq.Channel, "channel", updateReq.Channel, `DLT Release Channel that specifies which version to use.`) // TODO: array: clusters diff --git a/cmd/workspace/serving-endpoints/serving-endpoints.go b/cmd/workspace/serving-endpoints/serving-endpoints.go index b92f824d..0837652d 100755 --- a/cmd/workspace/serving-endpoints/serving-endpoints.go +++ b/cmd/workspace/serving-endpoints/serving-endpoints.go @@ -53,6 +53,7 @@ func New() *cobra.Command { cmd.AddCommand(newLogs()) cmd.AddCommand(newPatch()) cmd.AddCommand(newPut()) + cmd.AddCommand(newPutAiGateway()) cmd.AddCommand(newQuery()) cmd.AddCommand(newSetPermissions()) cmd.AddCommand(newUpdateConfig()) @@ -151,6 +152,7 @@ func newCreate() *cobra.Command { // TODO: short flags cmd.Flags().Var(&createJson, "json", `either inline JSON string or @path/to/file.json with request body`) + // TODO: complex arg: ai_gateway // TODO: array: rate_limits cmd.Flags().BoolVar(&createReq.RouteOptimized, "route-optimized", createReq.RouteOptimized, `Enable route optimization for the serving endpoint.`) // TODO: array: tags @@ -754,8 +756,9 @@ func newPut() *cobra.Command { cmd.Short = `Update rate limits of a serving endpoint.` cmd.Long = `Update rate limits of a serving endpoint. - Used to update the rate limits of a serving endpoint. NOTE: only external and - foundation model endpoints are supported as of now. + Used to update the rate limits of a serving endpoint. NOTE: Only foundation + model endpoints are currently supported. For external models, use AI Gateway + to manage rate limits. Arguments: NAME: The name of the serving endpoint whose rate limits are being updated. This @@ -800,6 +803,79 @@ func newPut() *cobra.Command { return cmd } +// start put-ai-gateway command + +// Slice with functions to override default command behavior. +// Functions can be added from the `init()` function in manually curated files in this directory. +var putAiGatewayOverrides []func( + *cobra.Command, + *serving.PutAiGatewayRequest, +) + +func newPutAiGateway() *cobra.Command { + cmd := &cobra.Command{} + + var putAiGatewayReq serving.PutAiGatewayRequest + var putAiGatewayJson flags.JsonFlag + + // TODO: short flags + cmd.Flags().Var(&putAiGatewayJson, "json", `either inline JSON string or @path/to/file.json with request body`) + + // TODO: complex arg: guardrails + // TODO: complex arg: inference_table_config + // TODO: array: rate_limits + // TODO: complex arg: usage_tracking_config + + cmd.Use = "put-ai-gateway NAME" + cmd.Short = `Update AI Gateway of a serving endpoint.` + cmd.Long = `Update AI Gateway of a serving endpoint. + + Used to update the AI Gateway of a serving endpoint. NOTE: Only external model + endpoints are currently supported. + + Arguments: + NAME: The name of the serving endpoint whose AI Gateway is being updated. This + field is required.` + + cmd.Annotations = make(map[string]string) + + cmd.Args = func(cmd *cobra.Command, args []string) error { + check := root.ExactArgs(1) + return check(cmd, args) + } + + cmd.PreRunE = root.MustWorkspaceClient + cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { + ctx := cmd.Context() + w := root.WorkspaceClient(ctx) + + if cmd.Flags().Changed("json") { + err = putAiGatewayJson.Unmarshal(&putAiGatewayReq) + if err != nil { + return err + } + } + putAiGatewayReq.Name = args[0] + + response, err := w.ServingEndpoints.PutAiGateway(ctx, putAiGatewayReq) + if err != nil { + return err + } + return cmdio.Render(ctx, response) + } + + // Disable completions since they are not applicable. + // Can be overridden by manual implementation in `override.go`. + cmd.ValidArgsFunction = cobra.NoFileCompletions + + // Apply optional overrides to this command. + for _, fn := range putAiGatewayOverrides { + fn(cmd, &putAiGatewayReq) + } + + return cmd +} + // start query command // Slice with functions to override default command behavior. diff --git a/cmd/workspace/settings/settings.go b/cmd/workspace/settings/settings.go index 214986c7..aaeecf41 100755 --- a/cmd/workspace/settings/settings.go +++ b/cmd/workspace/settings/settings.go @@ -8,6 +8,7 @@ import ( automatic_cluster_update "github.com/databricks/cli/cmd/workspace/automatic-cluster-update" compliance_security_profile "github.com/databricks/cli/cmd/workspace/compliance-security-profile" default_namespace "github.com/databricks/cli/cmd/workspace/default-namespace" + disable_legacy_access "github.com/databricks/cli/cmd/workspace/disable-legacy-access" enhanced_security_monitoring "github.com/databricks/cli/cmd/workspace/enhanced-security-monitoring" restrict_workspace_admins "github.com/databricks/cli/cmd/workspace/restrict-workspace-admins" ) @@ -31,6 +32,7 @@ func New() *cobra.Command { cmd.AddCommand(automatic_cluster_update.New()) cmd.AddCommand(compliance_security_profile.New()) cmd.AddCommand(default_namespace.New()) + cmd.AddCommand(disable_legacy_access.New()) cmd.AddCommand(enhanced_security_monitoring.New()) cmd.AddCommand(restrict_workspace_admins.New()) diff --git a/cmd/workspace/tables/tables.go b/cmd/workspace/tables/tables.go index 4564b4fe..ec297f29 100755 --- a/cmd/workspace/tables/tables.go +++ b/cmd/workspace/tables/tables.go @@ -220,6 +220,7 @@ func newGet() *cobra.Command { cmd.Flags().BoolVar(&getReq.IncludeBrowse, "include-browse", getReq.IncludeBrowse, `Whether to include tables in the response for which the principal can only access selective metadata for.`) cmd.Flags().BoolVar(&getReq.IncludeDeltaMetadata, "include-delta-metadata", getReq.IncludeDeltaMetadata, `Whether delta metadata should be included in the response.`) + cmd.Flags().BoolVar(&getReq.IncludeManifestCapabilities, "include-manifest-capabilities", getReq.IncludeManifestCapabilities, `Whether to include a manifest containing capabilities the table has.`) cmd.Use = "get FULL_NAME" cmd.Short = `Get a table.` @@ -299,6 +300,7 @@ func newList() *cobra.Command { cmd.Flags().BoolVar(&listReq.IncludeBrowse, "include-browse", listReq.IncludeBrowse, `Whether to include tables in the response for which the principal can only access selective metadata for.`) cmd.Flags().BoolVar(&listReq.IncludeDeltaMetadata, "include-delta-metadata", listReq.IncludeDeltaMetadata, `Whether delta metadata should be included in the response.`) + cmd.Flags().BoolVar(&listReq.IncludeManifestCapabilities, "include-manifest-capabilities", listReq.IncludeManifestCapabilities, `Whether to include a manifest containing capabilities the table has.`) cmd.Flags().IntVar(&listReq.MaxResults, "max-results", listReq.MaxResults, `Maximum number of tables to return.`) cmd.Flags().BoolVar(&listReq.OmitColumns, "omit-columns", listReq.OmitColumns, `Whether to omit the columns of the table from the response or not.`) cmd.Flags().BoolVar(&listReq.OmitProperties, "omit-properties", listReq.OmitProperties, `Whether to omit the properties of the table from the response or not.`) @@ -366,6 +368,7 @@ func newListSummaries() *cobra.Command { // TODO: short flags + cmd.Flags().BoolVar(&listSummariesReq.IncludeManifestCapabilities, "include-manifest-capabilities", listSummariesReq.IncludeManifestCapabilities, `Whether to include a manifest containing capabilities the table has.`) cmd.Flags().IntVar(&listSummariesReq.MaxResults, "max-results", listSummariesReq.MaxResults, `Maximum number of summaries for tables to return.`) cmd.Flags().StringVar(&listSummariesReq.PageToken, "page-token", listSummariesReq.PageToken, `Opaque pagination token to go to next page based on previous query.`) cmd.Flags().StringVar(&listSummariesReq.SchemaNamePattern, "schema-name-pattern", listSummariesReq.SchemaNamePattern, `A sql LIKE pattern (% and _) for schema names.`) diff --git a/cmd/workspace/temporary-table-credentials/temporary-table-credentials.go b/cmd/workspace/temporary-table-credentials/temporary-table-credentials.go new file mode 100755 index 00000000..8718f7ba --- /dev/null +++ b/cmd/workspace/temporary-table-credentials/temporary-table-credentials.go @@ -0,0 +1,122 @@ +// Code generated from OpenAPI specs by Databricks SDK Generator. DO NOT EDIT. + +package temporary_table_credentials + +import ( + "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/flags" + "github.com/databricks/databricks-sdk-go/service/catalog" + "github.com/spf13/cobra" +) + +// Slice with functions to override default command behavior. +// Functions can be added from the `init()` function in manually curated files in this directory. +var cmdOverrides []func(*cobra.Command) + +func New() *cobra.Command { + cmd := &cobra.Command{ + Use: "temporary-table-credentials", + Short: `Temporary Table Credentials refer to short-lived, downscoped credentials used to access cloud storage locationswhere table data is stored in Databricks.`, + Long: `Temporary Table Credentials refer to short-lived, downscoped credentials used + to access cloud storage locationswhere table data is stored in Databricks. + These credentials are employed to provide secure and time-limitedaccess to + data in cloud environments such as AWS, Azure, and Google Cloud. Each cloud + provider has its own typeof credentials: AWS uses temporary session tokens via + AWS Security Token Service (STS), Azure utilizesShared Access Signatures (SAS) + for its data storage services, and Google Cloud supports temporary + credentialsthrough OAuth 2.0.Temporary table credentials ensure that data + access is limited in scope and duration, reducing the risk ofunauthorized + access or misuse. To use the temporary table credentials API, a metastore + admin needs to enable the external_access_enabled flag (off by default) at the + metastore level, and user needs to be granted the EXTERNAL USE SCHEMA + permission at the schema level by catalog admin. Note that EXTERNAL USE SCHEMA + is a schema level permission that can only be granted by catalog admin + explicitly and is not included in schema ownership or ALL PRIVILEGES on the + schema for security reason.`, + GroupID: "catalog", + Annotations: map[string]string{ + "package": "catalog", + }, + } + + // Add methods + cmd.AddCommand(newGenerateTemporaryTableCredentials()) + + // Apply optional overrides to this command. + for _, fn := range cmdOverrides { + fn(cmd) + } + + return cmd +} + +// start generate-temporary-table-credentials command + +// Slice with functions to override default command behavior. +// Functions can be added from the `init()` function in manually curated files in this directory. +var generateTemporaryTableCredentialsOverrides []func( + *cobra.Command, + *catalog.GenerateTemporaryTableCredentialRequest, +) + +func newGenerateTemporaryTableCredentials() *cobra.Command { + cmd := &cobra.Command{} + + var generateTemporaryTableCredentialsReq catalog.GenerateTemporaryTableCredentialRequest + var generateTemporaryTableCredentialsJson flags.JsonFlag + + // TODO: short flags + cmd.Flags().Var(&generateTemporaryTableCredentialsJson, "json", `either inline JSON string or @path/to/file.json with request body`) + + cmd.Flags().Var(&generateTemporaryTableCredentialsReq.Operation, "operation", `The operation performed against the table data, either READ or READ_WRITE. Supported values: [READ, READ_WRITE]`) + cmd.Flags().StringVar(&generateTemporaryTableCredentialsReq.TableId, "table-id", generateTemporaryTableCredentialsReq.TableId, `UUID of the table to read or write.`) + + cmd.Use = "generate-temporary-table-credentials" + cmd.Short = `Generate a temporary table credential.` + cmd.Long = `Generate a temporary table credential. + + Get a short-lived credential for directly accessing the table data on cloud + storage. The metastore must have external_access_enabled flag set to true + (default false). The caller must have EXTERNAL_USE_SCHEMA privilege on the + parent schema and this privilege can only be granted by catalog owners.` + + cmd.Annotations = make(map[string]string) + + cmd.Args = func(cmd *cobra.Command, args []string) error { + check := root.ExactArgs(0) + return check(cmd, args) + } + + cmd.PreRunE = root.MustWorkspaceClient + cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { + ctx := cmd.Context() + w := root.WorkspaceClient(ctx) + + if cmd.Flags().Changed("json") { + err = generateTemporaryTableCredentialsJson.Unmarshal(&generateTemporaryTableCredentialsReq) + if err != nil { + return err + } + } + + response, err := w.TemporaryTableCredentials.GenerateTemporaryTableCredentials(ctx, generateTemporaryTableCredentialsReq) + if err != nil { + return err + } + return cmdio.Render(ctx, response) + } + + // Disable completions since they are not applicable. + // Can be overridden by manual implementation in `override.go`. + cmd.ValidArgsFunction = cobra.NoFileCompletions + + // Apply optional overrides to this command. + for _, fn := range generateTemporaryTableCredentialsOverrides { + fn(cmd, &generateTemporaryTableCredentialsReq) + } + + return cmd +} + +// end service TemporaryTableCredentials diff --git a/go.mod b/go.mod index 0cf3ef8a..9141274c 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.22.7 require ( github.com/Masterminds/semver/v3 v3.3.0 // MIT github.com/briandowns/spinner v1.23.1 // Apache 2.0 - github.com/databricks/databricks-sdk-go v0.46.0 // Apache 2.0 + github.com/databricks/databricks-sdk-go v0.47.0 // Apache 2.0 github.com/fatih/color v1.17.0 // MIT github.com/ghodss/yaml v1.0.0 // MIT + NOTICE github.com/google/uuid v1.6.0 // BSD-3-Clause diff --git a/go.sum b/go.sum index d8866775..177707a5 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,8 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= -github.com/databricks/databricks-sdk-go v0.46.0 h1:D0TxmtSVAOsdnfzH4OGtAmcq+8TyA7Z6fA6JEYhupeY= -github.com/databricks/databricks-sdk-go v0.46.0/go.mod h1:ds+zbv5mlQG7nFEU5ojLtgN/u0/9YzZmKQES/CfedzU= +github.com/databricks/databricks-sdk-go v0.47.0 h1:eE7dN9axviL8+s10jnQAayOYDaR+Mfu7E9COGjO4lrQ= +github.com/databricks/databricks-sdk-go v0.47.0/go.mod h1:ds+zbv5mlQG7nFEU5ojLtgN/u0/9YzZmKQES/CfedzU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= From c28d64f2dc3885becbddc3b44b1bdc5893ba6b7c Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Tue, 1 Oct 2024 15:20:17 +0200 Subject: [PATCH 02/24] [Release] Release v0.229.0 (#1801) Bundles: * Added support for creating all-purpose clusters ([#1698](https://github.com/databricks/cli/pull/1698)). * Reduce time until the prompt is shown for bundle run ([#1727](https://github.com/databricks/cli/pull/1727)). * Use Unity Catalog for pipelines in the default-python template ([#1766](https://github.com/databricks/cli/pull/1766)). * Add verbose flag to the "bundle deploy" command ([#1774](https://github.com/databricks/cli/pull/1774)). * Fixed full variable override detection ([#1787](https://github.com/databricks/cli/pull/1787)). * Add sub-extension to resource files in built-in templates ([#1777](https://github.com/databricks/cli/pull/1777)). * Fix panic in `apply_presets.go` ([#1796](https://github.com/databricks/cli/pull/1796)). Internal: * Assert tokens are redacted in origin URL when username is not specified ([#1785](https://github.com/databricks/cli/pull/1785)). * Refactor jobs path translation ([#1782](https://github.com/databricks/cli/pull/1782)). * Add JobTaskClusterSpec validate mutator ([#1784](https://github.com/databricks/cli/pull/1784)). * Pin Go toolchain to 1.22.7 ([#1790](https://github.com/databricks/cli/pull/1790)). * Modify SetLocation test utility to take full locations as argument ([#1788](https://github.com/databricks/cli/pull/1788)). * Simplified isFullVariableOverrideDef implementation ([#1791](https://github.com/databricks/cli/pull/1791)). * Sort tasks by `task_key` before generating the Terraform configuration ([#1776](https://github.com/databricks/cli/pull/1776)). * Trim trailing whitespace ([#1794](https://github.com/databricks/cli/pull/1794)). * Move trampoline code into trampoline package ([#1793](https://github.com/databricks/cli/pull/1793)). * Rename `RootPath` -> `BundleRootPath` ([#1792](https://github.com/databricks/cli/pull/1792)). API Changes: * Changed `databricks apps delete` command to return . * Changed `databricks apps deploy` command with new required argument order. * Changed `databricks apps start` command to return . * Changed `databricks apps stop` command to return . * Added `databricks temporary-table-credentials` command group. * Added `databricks serving-endpoints put-ai-gateway` command. * Added `databricks disable-legacy-access` command group. * Added `databricks account disable-legacy-features` command group. OpenAPI commit 6f6b1371e640f2dfeba72d365ac566368656f6b6 (2024-09-19) Dependency updates: * Upgrade to Go SDK 0.47.0 ([#1799](https://github.com/databricks/cli/pull/1799)). * Upgrade to TF provider 1.52 ([#1781](https://github.com/databricks/cli/pull/1781)). * Bump golang.org/x/mod from 0.20.0 to 0.21.0 ([#1758](https://github.com/databricks/cli/pull/1758)). * Bump github.com/hashicorp/hc-install from 0.7.0 to 0.9.0 ([#1772](https://github.com/databricks/cli/pull/1772)). --- CHANGELOG.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32a7e5cf..4f5f68ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,45 @@ # Version changelog +## [Release] Release v0.229.0 + +Bundles: + * Added support for creating all-purpose clusters ([#1698](https://github.com/databricks/cli/pull/1698)). + * Reduce time until the prompt is shown for bundle run ([#1727](https://github.com/databricks/cli/pull/1727)). + * Use Unity Catalog for pipelines in the default-python template ([#1766](https://github.com/databricks/cli/pull/1766)). + * Add verbose flag to the "bundle deploy" command ([#1774](https://github.com/databricks/cli/pull/1774)). + * Fixed full variable override detection ([#1787](https://github.com/databricks/cli/pull/1787)). + * Add sub-extension to resource files in built-in templates ([#1777](https://github.com/databricks/cli/pull/1777)). + * Fix panic in `apply_presets.go` ([#1796](https://github.com/databricks/cli/pull/1796)). + +Internal: + * Assert tokens are redacted in origin URL when username is not specified ([#1785](https://github.com/databricks/cli/pull/1785)). + * Refactor jobs path translation ([#1782](https://github.com/databricks/cli/pull/1782)). + * Add JobTaskClusterSpec validate mutator ([#1784](https://github.com/databricks/cli/pull/1784)). + * Pin Go toolchain to 1.22.7 ([#1790](https://github.com/databricks/cli/pull/1790)). + * Modify SetLocation test utility to take full locations as argument ([#1788](https://github.com/databricks/cli/pull/1788)). + * Simplified isFullVariableOverrideDef implementation ([#1791](https://github.com/databricks/cli/pull/1791)). + * Sort tasks by `task_key` before generating the Terraform configuration ([#1776](https://github.com/databricks/cli/pull/1776)). + * Trim trailing whitespace ([#1794](https://github.com/databricks/cli/pull/1794)). + * Move trampoline code into trampoline package ([#1793](https://github.com/databricks/cli/pull/1793)). + * Rename `RootPath` -> `BundleRootPath` ([#1792](https://github.com/databricks/cli/pull/1792)). + +API Changes: + * Changed `databricks apps delete` command to return . + * Changed `databricks apps deploy` command with new required argument order. + * Changed `databricks apps start` command to return . + * Changed `databricks apps stop` command to return . + * Added `databricks temporary-table-credentials` command group. + * Added `databricks serving-endpoints put-ai-gateway` command. + * Added `databricks disable-legacy-access` command group. + * Added `databricks account disable-legacy-features` command group. + +OpenAPI commit 6f6b1371e640f2dfeba72d365ac566368656f6b6 (2024-09-19) +Dependency updates: + * Upgrade to Go SDK 0.47.0 ([#1799](https://github.com/databricks/cli/pull/1799)). + * Upgrade to TF provider 1.52 ([#1781](https://github.com/databricks/cli/pull/1781)). + * Bump golang.org/x/mod from 0.20.0 to 0.21.0 ([#1758](https://github.com/databricks/cli/pull/1758)). + * Bump github.com/hashicorp/hc-install from 0.7.0 to 0.9.0 ([#1772](https://github.com/databricks/cli/pull/1772)). + ## [Release] Release v0.228.1 Bundles: From 044a00c7f9fc2f2d35794062b63a54d01081ffa4 Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Wed, 2 Oct 2024 15:53:24 +0200 Subject: [PATCH 03/24] Add an error if state files grow bigger than the export limit (#1795) ## Changes Currently API limits on exporting files from workspaces are set at 10 MBs while uploading to is 500 MBs. We want to prevent users running into deadlock when they won't be able to pull state file anymore so we prevent from uploading large state files (over 10 MBs) to Databricks workspace. --- bundle/deploy/state_push.go | 13 +++++++++++ bundle/deploy/terraform/state_push.go | 11 +++++++++ bundle/deploy/terraform/state_push_test.go | 27 ++++++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/bundle/deploy/state_push.go b/bundle/deploy/state_push.go index 176a907c..6912414c 100644 --- a/bundle/deploy/state_push.go +++ b/bundle/deploy/state_push.go @@ -10,6 +10,8 @@ import ( "github.com/databricks/cli/libs/log" ) +const MaxStateFileSize = 10 * 1024 * 1024 // 10MB + type statePush struct { filerFactory FilerFactory } @@ -35,6 +37,17 @@ func (s *statePush) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostic } defer local.Close() + if !b.Config.Bundle.Force { + state, err := local.Stat() + if err != nil { + return diag.FromErr(err) + } + + if state.Size() > MaxStateFileSize { + return diag.Errorf("Deployment state file size exceeds the maximum allowed size of %d bytes. Please reduce the number of resources in your bundle, split your bundle into multiple or re-run the command with --force flag.", MaxStateFileSize) + } + } + log.Infof(ctx, "Writing local deployment state file to remote state directory") err = f.Write(ctx, DeploymentStateFileName, local, filer.CreateParentDirectories, filer.OverwriteIfExists) if err != nil { diff --git a/bundle/deploy/terraform/state_push.go b/bundle/deploy/terraform/state_push.go index 6cdde137..84d8e767 100644 --- a/bundle/deploy/terraform/state_push.go +++ b/bundle/deploy/terraform/state_push.go @@ -47,6 +47,17 @@ func (l *statePush) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostic } defer local.Close() + if !b.Config.Bundle.Force { + state, err := local.Stat() + if err != nil { + return diag.FromErr(err) + } + + if state.Size() > deploy.MaxStateFileSize { + return diag.Errorf("Terraform state file size exceeds the maximum allowed size of %d bytes. Please reduce the number of resources in your bundle, split your bundle into multiple or re-run the command with --force flag", deploy.MaxStateFileSize) + } + } + // Upload state file from local cache directory to filer. cmdio.LogString(ctx, "Updating deployment state...") log.Infof(ctx, "Writing local state file to remote state directory") diff --git a/bundle/deploy/terraform/state_push_test.go b/bundle/deploy/terraform/state_push_test.go index e022dee1..4cc52b7a 100644 --- a/bundle/deploy/terraform/state_push_test.go +++ b/bundle/deploy/terraform/state_push_test.go @@ -3,6 +3,7 @@ package terraform import ( "context" "encoding/json" + "fmt" "io" "testing" @@ -59,3 +60,29 @@ func TestStatePush(t *testing.T) { diags := bundle.Apply(ctx, b, m) assert.NoError(t, diags.Error()) } + +func TestStatePushLargeState(t *testing.T) { + mock := mockfiler.NewMockFiler(t) + m := &statePush{ + identityFiler(mock), + } + + ctx := context.Background() + b := statePushTestBundle(t) + + largeState := map[string]any{} + for i := 0; i < 1000000; i++ { + largeState[fmt.Sprintf("field_%d", i)] = i + } + + // Write a stale local state file. + writeLocalState(t, ctx, b, largeState) + diags := bundle.Apply(ctx, b, m) + assert.ErrorContains(t, diags.Error(), "Terraform state file size exceeds the maximum allowed size of 10485760 bytes. Please reduce the number of resources in your bundle, split your bundle into multiple or re-run the command with --force flag") + + // Force the write. + b = statePushTestBundle(t) + b.Config.Bundle.Force = true + diags = bundle.Apply(ctx, b, m) + assert.NoError(t, diags.Error()) +} From 80d55f454066a9c1666cb53387f9e735494fe72a Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Wed, 2 Oct 2024 15:55:40 +0200 Subject: [PATCH 04/24] Add resource path field to bundle workspace configuration (#1800) ## Changes Default workspace path for resources with a presence in the workspace tree. Note: this path is **not** created automatically (yet). We need this only for dashboards (so far), so can take care of creation if one or more dashboards are part of a deployment. This saves an API call for deployments where this is not necessary. ## Tests Expanded existing tests. --- bundle/config/mutator/default_workspace_paths.go | 4 ++++ bundle/config/mutator/default_workspace_paths_test.go | 3 +++ bundle/config/mutator/process_target_mode.go | 9 ++++++--- bundle/config/workspace.go | 5 +++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/bundle/config/mutator/default_workspace_paths.go b/bundle/config/mutator/default_workspace_paths.go index 71e562b5..02a1ddb3 100644 --- a/bundle/config/mutator/default_workspace_paths.go +++ b/bundle/config/mutator/default_workspace_paths.go @@ -29,6 +29,10 @@ func (m *defineDefaultWorkspacePaths) Apply(ctx context.Context, b *bundle.Bundl b.Config.Workspace.FilePath = path.Join(root, "files") } + if b.Config.Workspace.ResourcePath == "" { + b.Config.Workspace.ResourcePath = path.Join(root, "resources") + } + if b.Config.Workspace.ArtifactPath == "" { b.Config.Workspace.ArtifactPath = path.Join(root, "artifacts") } diff --git a/bundle/config/mutator/default_workspace_paths_test.go b/bundle/config/mutator/default_workspace_paths_test.go index 0ba20ea2..6779c373 100644 --- a/bundle/config/mutator/default_workspace_paths_test.go +++ b/bundle/config/mutator/default_workspace_paths_test.go @@ -22,6 +22,7 @@ func TestDefineDefaultWorkspacePaths(t *testing.T) { diags := bundle.Apply(context.Background(), b, mutator.DefineDefaultWorkspacePaths()) require.NoError(t, diags.Error()) assert.Equal(t, "/files", b.Config.Workspace.FilePath) + assert.Equal(t, "/resources", b.Config.Workspace.ResourcePath) assert.Equal(t, "/artifacts", b.Config.Workspace.ArtifactPath) assert.Equal(t, "/state", b.Config.Workspace.StatePath) } @@ -32,6 +33,7 @@ func TestDefineDefaultWorkspacePathsAlreadySet(t *testing.T) { Workspace: config.Workspace{ RootPath: "/", FilePath: "/foo/bar", + ResourcePath: "/foo/bar", ArtifactPath: "/foo/bar", StatePath: "/foo/bar", }, @@ -40,6 +42,7 @@ func TestDefineDefaultWorkspacePathsAlreadySet(t *testing.T) { diags := bundle.Apply(context.Background(), b, mutator.DefineDefaultWorkspacePaths()) require.NoError(t, diags.Error()) assert.Equal(t, "/foo/bar", b.Config.Workspace.FilePath) + assert.Equal(t, "/foo/bar", b.Config.Workspace.ResourcePath) assert.Equal(t, "/foo/bar", b.Config.Workspace.ArtifactPath) assert.Equal(t, "/foo/bar", b.Config.Workspace.StatePath) } diff --git a/bundle/config/mutator/process_target_mode.go b/bundle/config/mutator/process_target_mode.go index 70382f05..9944f6ff 100644 --- a/bundle/config/mutator/process_target_mode.go +++ b/bundle/config/mutator/process_target_mode.go @@ -118,15 +118,18 @@ func findNonUserPath(b *bundle.Bundle) string { if b.Config.Workspace.RootPath != "" && !containsName(b.Config.Workspace.RootPath) { return "root_path" } - if b.Config.Workspace.StatePath != "" && !containsName(b.Config.Workspace.StatePath) { - return "state_path" - } if b.Config.Workspace.FilePath != "" && !containsName(b.Config.Workspace.FilePath) { return "file_path" } + if b.Config.Workspace.ResourcePath != "" && !containsName(b.Config.Workspace.ResourcePath) { + return "resource_path" + } if b.Config.Workspace.ArtifactPath != "" && !containsName(b.Config.Workspace.ArtifactPath) { return "artifact_path" } + if b.Config.Workspace.StatePath != "" && !containsName(b.Config.Workspace.StatePath) { + return "state_path" + } return "" } diff --git a/bundle/config/workspace.go b/bundle/config/workspace.go index efc5caa6..878d0783 100644 --- a/bundle/config/workspace.go +++ b/bundle/config/workspace.go @@ -54,6 +54,11 @@ type Workspace struct { // This defaults to "${workspace.root}/files". FilePath string `json:"file_path,omitempty"` + // Remote workspace path for resources with a presence in the workspace. + // These are kept outside [FilePath] to avoid potential naming collisions. + // This defaults to "${workspace.root}/resources". + ResourcePath string `json:"resource_path,omitempty"` + // Remote workspace path for build artifacts. // This defaults to "${workspace.root}/artifacts". ArtifactPath string `json:"artifact_path,omitempty"` From a8cff48c0b4d747cb769d09724fecdf3c5a3f25b Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Wed, 2 Oct 2024 17:34:00 +0200 Subject: [PATCH 05/24] Always prepend bundle remote paths with /Workspace (#1724) ## Changes Due to platform changes, all libraries, notebooks and etc. paths used in Databricks must be started with either /Workspace or /Volumes prefix. This PR makes sure that all bundle paths are correctly prefixed. Note: this change is a breaking change if user previously configured and used `/Workspace/Workspace` folder in their workspace file system or having `/Workspace/${workspace.root_path}...` pattern configured anywhere in their bundle config Fixes: #1751 AI: - [x] Scan DABs config and error out on `/Workspace/${workspace.root_path}...` pattern usage ## Tests Added unit tests --------- Co-authored-by: Pieter Noordhuis --- .../config/mutator/expand_workspace_root.go | 2 +- .../mutator/expand_workspace_root_test.go | 2 +- .../mutator/prepend_workspace_prefix.go | 67 +++++++++++++++ .../mutator/prepend_workspace_prefix_test.go | 79 +++++++++++++++++ .../mutator/rewrite_workspace_prefix.go | 72 ++++++++++++++++ .../mutator/rewrite_workspace_prefix_test.go | 85 +++++++++++++++++++ bundle/config/workspace.go | 2 +- bundle/phases/initialize.go | 7 ++ bundle/tests/pipeline_glob_paths_test.go | 2 +- .../tests/relative_path_translation_test.go | 8 +- internal/bundle/deploy_test.go | 2 +- internal/bundle/helpers.go | 2 +- .../{{.project_name}}/databricks.yml.tmpl | 4 +- .../{{.project_name}}/databricks.yml.tmpl | 4 +- .../{{.project_name}}.pipeline.yml.tmpl | 2 +- .../{{.project_name}}/databricks.yml.tmpl | 4 +- 16 files changed, 327 insertions(+), 17 deletions(-) create mode 100644 bundle/config/mutator/prepend_workspace_prefix.go create mode 100644 bundle/config/mutator/prepend_workspace_prefix_test.go create mode 100644 bundle/config/mutator/rewrite_workspace_prefix.go create mode 100644 bundle/config/mutator/rewrite_workspace_prefix_test.go diff --git a/bundle/config/mutator/expand_workspace_root.go b/bundle/config/mutator/expand_workspace_root.go index 8954abd4..3f0547de 100644 --- a/bundle/config/mutator/expand_workspace_root.go +++ b/bundle/config/mutator/expand_workspace_root.go @@ -33,7 +33,7 @@ func (m *expandWorkspaceRoot) Apply(ctx context.Context, b *bundle.Bundle) diag. } if strings.HasPrefix(root, "~/") { - home := fmt.Sprintf("/Users/%s", currentUser.UserName) + home := fmt.Sprintf("/Workspace/Users/%s", currentUser.UserName) b.Config.Workspace.RootPath = path.Join(home, root[2:]) } diff --git a/bundle/config/mutator/expand_workspace_root_test.go b/bundle/config/mutator/expand_workspace_root_test.go index e6260dbd..40bf35ca 100644 --- a/bundle/config/mutator/expand_workspace_root_test.go +++ b/bundle/config/mutator/expand_workspace_root_test.go @@ -27,7 +27,7 @@ func TestExpandWorkspaceRoot(t *testing.T) { } diags := bundle.Apply(context.Background(), b, mutator.ExpandWorkspaceRoot()) require.NoError(t, diags.Error()) - assert.Equal(t, "/Users/jane@doe.com/foo", b.Config.Workspace.RootPath) + assert.Equal(t, "/Workspace/Users/jane@doe.com/foo", b.Config.Workspace.RootPath) } func TestExpandWorkspaceRootDoesNothing(t *testing.T) { diff --git a/bundle/config/mutator/prepend_workspace_prefix.go b/bundle/config/mutator/prepend_workspace_prefix.go new file mode 100644 index 00000000..dd467344 --- /dev/null +++ b/bundle/config/mutator/prepend_workspace_prefix.go @@ -0,0 +1,67 @@ +package mutator + +import ( + "context" + "fmt" + "strings" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/dyn" +) + +type prependWorkspacePrefix struct{} + +// PrependWorkspacePrefix prepends the workspace root path to all paths in the bundle. +func PrependWorkspacePrefix() bundle.Mutator { + return &prependWorkspacePrefix{} +} + +func (m *prependWorkspacePrefix) Name() string { + return "PrependWorkspacePrefix" +} + +var skipPrefixes = []string{ + "/Workspace/", + "/Volumes/", +} + +func (m *prependWorkspacePrefix) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { + patterns := []dyn.Pattern{ + dyn.NewPattern(dyn.Key("workspace"), dyn.Key("root_path")), + dyn.NewPattern(dyn.Key("workspace"), dyn.Key("file_path")), + dyn.NewPattern(dyn.Key("workspace"), dyn.Key("artifact_path")), + dyn.NewPattern(dyn.Key("workspace"), dyn.Key("state_path")), + } + + err := b.Config.Mutate(func(v dyn.Value) (dyn.Value, error) { + var err error + for _, pattern := range patterns { + v, err = dyn.MapByPattern(v, pattern, func(p dyn.Path, pv dyn.Value) (dyn.Value, error) { + path, ok := pv.AsString() + if !ok { + return dyn.InvalidValue, fmt.Errorf("expected string, got %s", v.Kind()) + } + + for _, prefix := range skipPrefixes { + if strings.HasPrefix(path, prefix) { + return pv, nil + } + } + + return dyn.NewValue(fmt.Sprintf("/Workspace%s", path), v.Locations()), nil + }) + + if err != nil { + return dyn.InvalidValue, err + } + } + return v, nil + }) + + if err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/bundle/config/mutator/prepend_workspace_prefix_test.go b/bundle/config/mutator/prepend_workspace_prefix_test.go new file mode 100644 index 00000000..287c694d --- /dev/null +++ b/bundle/config/mutator/prepend_workspace_prefix_test.go @@ -0,0 +1,79 @@ +package mutator + +import ( + "context" + "testing" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" + "github.com/databricks/databricks-sdk-go/service/iam" + "github.com/stretchr/testify/require" +) + +func TestPrependWorkspacePrefix(t *testing.T) { + testCases := []struct { + path string + expected string + }{ + { + path: "/Users/test", + expected: "/Workspace/Users/test", + }, + { + path: "/Shared/test", + expected: "/Workspace/Shared/test", + }, + { + path: "/Workspace/Users/test", + expected: "/Workspace/Users/test", + }, + { + path: "/Volumes/Users/test", + expected: "/Volumes/Users/test", + }, + } + + for _, tc := range testCases { + b := &bundle.Bundle{ + Config: config.Root{ + Workspace: config.Workspace{ + RootPath: tc.path, + ArtifactPath: tc.path, + FilePath: tc.path, + StatePath: tc.path, + }, + }, + } + + diags := bundle.Apply(context.Background(), b, PrependWorkspacePrefix()) + require.Empty(t, diags) + require.Equal(t, tc.expected, b.Config.Workspace.RootPath) + require.Equal(t, tc.expected, b.Config.Workspace.ArtifactPath) + require.Equal(t, tc.expected, b.Config.Workspace.FilePath) + require.Equal(t, tc.expected, b.Config.Workspace.StatePath) + } +} + +func TestPrependWorkspaceForDefaultConfig(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Bundle: config.Bundle{ + Name: "test", + Target: "dev", + }, + Workspace: config.Workspace{ + CurrentUser: &config.User{ + User: &iam.User{ + UserName: "jane@doe.com", + }, + }, + }, + }, + } + diags := bundle.Apply(context.Background(), b, bundle.Seq(DefineDefaultWorkspaceRoot(), ExpandWorkspaceRoot(), DefineDefaultWorkspacePaths(), PrependWorkspacePrefix())) + require.Empty(t, diags) + require.Equal(t, "/Workspace/Users/jane@doe.com/.bundle/test/dev", b.Config.Workspace.RootPath) + require.Equal(t, "/Workspace/Users/jane@doe.com/.bundle/test/dev/artifacts", b.Config.Workspace.ArtifactPath) + require.Equal(t, "/Workspace/Users/jane@doe.com/.bundle/test/dev/files", b.Config.Workspace.FilePath) + require.Equal(t, "/Workspace/Users/jane@doe.com/.bundle/test/dev/state", b.Config.Workspace.StatePath) +} diff --git a/bundle/config/mutator/rewrite_workspace_prefix.go b/bundle/config/mutator/rewrite_workspace_prefix.go new file mode 100644 index 00000000..8a39ee8a --- /dev/null +++ b/bundle/config/mutator/rewrite_workspace_prefix.go @@ -0,0 +1,72 @@ +package mutator + +import ( + "context" + "fmt" + "strings" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/dyn" +) + +type rewriteWorkspacePrefix struct{} + +// RewriteWorkspacePrefix finds any strings in bundle configration that have +// workspace prefix plus workspace path variable used and removes workspace prefix from it. +func RewriteWorkspacePrefix() bundle.Mutator { + return &rewriteWorkspacePrefix{} +} + +func (m *rewriteWorkspacePrefix) Name() string { + return "RewriteWorkspacePrefix" +} + +func (m *rewriteWorkspacePrefix) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { + diags := diag.Diagnostics{} + paths := map[string]string{ + "/Workspace/${workspace.root_path}": "${workspace.root_path}", + "/Workspace${workspace.root_path}": "${workspace.root_path}", + "/Workspace/${workspace.file_path}": "${workspace.file_path}", + "/Workspace${workspace.file_path}": "${workspace.file_path}", + "/Workspace/${workspace.artifact_path}": "${workspace.artifact_path}", + "/Workspace${workspace.artifact_path}": "${workspace.artifact_path}", + "/Workspace/${workspace.state_path}": "${workspace.state_path}", + "/Workspace${workspace.state_path}": "${workspace.state_path}", + } + + err := b.Config.Mutate(func(root dyn.Value) (dyn.Value, error) { + // Walk through the bundle configuration, check all the string leafs and + // see if any of the prefixes are used in the remote path. + return dyn.Walk(root, func(p dyn.Path, v dyn.Value) (dyn.Value, error) { + vv, ok := v.AsString() + if !ok { + return v, nil + } + + for path, replacePath := range paths { + if strings.Contains(vv, path) { + newPath := strings.Replace(vv, path, replacePath, 1) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: fmt.Sprintf("substring %q found in %q. Please update this to %q.", path, vv, newPath), + Detail: "For more information, please refer to: https://docs.databricks.com/en/release-notes/dev-tools/bundles.html#workspace-paths", + Locations: v.Locations(), + Paths: []dyn.Path{p}, + }) + + // Remove the workspace prefix from the string. + return dyn.NewValue(newPath, v.Locations()), nil + } + } + + return v, nil + }) + }) + + if err != nil { + return diag.FromErr(err) + } + + return diags +} diff --git a/bundle/config/mutator/rewrite_workspace_prefix_test.go b/bundle/config/mutator/rewrite_workspace_prefix_test.go new file mode 100644 index 00000000..d75ec89d --- /dev/null +++ b/bundle/config/mutator/rewrite_workspace_prefix_test.go @@ -0,0 +1,85 @@ +package mutator + +import ( + "context" + "testing" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/databricks-sdk-go/service/compute" + "github.com/databricks/databricks-sdk-go/service/jobs" + "github.com/stretchr/testify/require" +) + +func TestNoWorkspacePrefixUsed(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Workspace: config.Workspace{ + RootPath: "/Workspace/Users/test", + ArtifactPath: "/Workspace/Users/test/artifacts", + FilePath: "/Workspace/Users/test/files", + StatePath: "/Workspace/Users/test/state", + }, + + Resources: config.Resources{ + Jobs: map[string]*resources.Job{ + "test_job": { + JobSettings: &jobs.JobSettings{ + Tasks: []jobs.Task{ + { + SparkPythonTask: &jobs.SparkPythonTask{ + PythonFile: "/Workspace/${workspace.root_path}/file1.py", + }, + }, + { + NotebookTask: &jobs.NotebookTask{ + NotebookPath: "/Workspace${workspace.file_path}/notebook1", + }, + Libraries: []compute.Library{ + { + Jar: "/Workspace/${workspace.artifact_path}/jar1.jar", + }, + }, + }, + { + NotebookTask: &jobs.NotebookTask{ + NotebookPath: "${workspace.file_path}/notebook2", + }, + Libraries: []compute.Library{ + { + Jar: "${workspace.artifact_path}/jar2.jar", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + diags := bundle.Apply(context.Background(), b, RewriteWorkspacePrefix()) + require.Len(t, diags, 3) + + expectedErrors := map[string]bool{ + `substring "/Workspace/${workspace.root_path}" found in "/Workspace/${workspace.root_path}/file1.py". Please update this to "${workspace.root_path}/file1.py".`: true, + `substring "/Workspace${workspace.file_path}" found in "/Workspace${workspace.file_path}/notebook1". Please update this to "${workspace.file_path}/notebook1".`: true, + `substring "/Workspace/${workspace.artifact_path}" found in "/Workspace/${workspace.artifact_path}/jar1.jar". Please update this to "${workspace.artifact_path}/jar1.jar".`: true, + } + + for _, d := range diags { + require.Equal(t, d.Severity, diag.Warning) + require.Contains(t, expectedErrors, d.Summary) + delete(expectedErrors, d.Summary) + } + + require.Equal(t, "${workspace.root_path}/file1.py", b.Config.Resources.Jobs["test_job"].JobSettings.Tasks[0].SparkPythonTask.PythonFile) + require.Equal(t, "${workspace.file_path}/notebook1", b.Config.Resources.Jobs["test_job"].JobSettings.Tasks[1].NotebookTask.NotebookPath) + require.Equal(t, "${workspace.artifact_path}/jar1.jar", b.Config.Resources.Jobs["test_job"].JobSettings.Tasks[1].Libraries[0].Jar) + require.Equal(t, "${workspace.file_path}/notebook2", b.Config.Resources.Jobs["test_job"].JobSettings.Tasks[2].NotebookTask.NotebookPath) + require.Equal(t, "${workspace.artifact_path}/jar2.jar", b.Config.Resources.Jobs["test_job"].JobSettings.Tasks[2].Libraries[0].Jar) + +} diff --git a/bundle/config/workspace.go b/bundle/config/workspace.go index 878d0783..e64808ab 100644 --- a/bundle/config/workspace.go +++ b/bundle/config/workspace.go @@ -47,7 +47,7 @@ type Workspace struct { // Remote workspace base path for deployment state, for artifacts, as synchronization target. // This defaults to "~/.bundle/${bundle.name}/${bundle.target}" where "~" expands to - // the current user's home directory in the workspace (e.g. `/Users/jane@doe.com`). + // the current user's home directory in the workspace (e.g. `/Workspace/Users/jane@doe.com`). RootPath string `json:"root_path,omitempty"` // Remote workspace path to synchronize local files to. diff --git a/bundle/phases/initialize.go b/bundle/phases/initialize.go index 93ce61b2..a41819c7 100644 --- a/bundle/phases/initialize.go +++ b/bundle/phases/initialize.go @@ -39,9 +39,16 @@ func Initialize() bundle.Mutator { mutator.MergePipelineClusters(), mutator.InitializeWorkspaceClient(), mutator.PopulateCurrentUser(), + mutator.DefineDefaultWorkspaceRoot(), mutator.ExpandWorkspaceRoot(), mutator.DefineDefaultWorkspacePaths(), + mutator.PrependWorkspacePrefix(), + + // This mutator needs to be run before variable interpolation because it + // searches for strings with variable references in them. + mutator.RewriteWorkspacePrefix(), + mutator.SetVariables(), // Intentionally placed before ResolveVariableReferencesInLookup, ResolveResourceReferences, // ResolveVariableReferencesInComplexVariables and ResolveVariableReferences. diff --git a/bundle/tests/pipeline_glob_paths_test.go b/bundle/tests/pipeline_glob_paths_test.go index c1c62cfb..d17f0404 100644 --- a/bundle/tests/pipeline_glob_paths_test.go +++ b/bundle/tests/pipeline_glob_paths_test.go @@ -11,7 +11,7 @@ func TestExpandPipelineGlobPaths(t *testing.T) { require.NoError(t, diags.Error()) require.Equal( t, - "/Users/user@domain.com/.bundle/pipeline_glob_paths/default/files/dlt/nyc_taxi_loader", + "/Workspace/Users/user@domain.com/.bundle/pipeline_glob_paths/default/files/dlt/nyc_taxi_loader", b.Config.Resources.Pipelines["nyc_taxi_pipeline"].Libraries[0].Notebook.Path, ) } diff --git a/bundle/tests/relative_path_translation_test.go b/bundle/tests/relative_path_translation_test.go index 199871d2..0f553ac3 100644 --- a/bundle/tests/relative_path_translation_test.go +++ b/bundle/tests/relative_path_translation_test.go @@ -12,9 +12,9 @@ func TestRelativePathTranslationDefault(t *testing.T) { require.NoError(t, diags.Error()) t0 := b.Config.Resources.Jobs["job"].Tasks[0] - assert.Equal(t, "/remote/src/file1.py", t0.SparkPythonTask.PythonFile) + assert.Equal(t, "/Workspace/remote/src/file1.py", t0.SparkPythonTask.PythonFile) t1 := b.Config.Resources.Jobs["job"].Tasks[1] - assert.Equal(t, "/remote/src/file1.py", t1.SparkPythonTask.PythonFile) + assert.Equal(t, "/Workspace/remote/src/file1.py", t1.SparkPythonTask.PythonFile) } func TestRelativePathTranslationOverride(t *testing.T) { @@ -22,7 +22,7 @@ func TestRelativePathTranslationOverride(t *testing.T) { require.NoError(t, diags.Error()) t0 := b.Config.Resources.Jobs["job"].Tasks[0] - assert.Equal(t, "/remote/src/file2.py", t0.SparkPythonTask.PythonFile) + assert.Equal(t, "/Workspace/remote/src/file2.py", t0.SparkPythonTask.PythonFile) t1 := b.Config.Resources.Jobs["job"].Tasks[1] - assert.Equal(t, "/remote/src/file2.py", t1.SparkPythonTask.PythonFile) + assert.Equal(t, "/Workspace/remote/src/file2.py", t1.SparkPythonTask.PythonFile) } diff --git a/internal/bundle/deploy_test.go b/internal/bundle/deploy_test.go index 736c880d..88543585 100644 --- a/internal/bundle/deploy_test.go +++ b/internal/bundle/deploy_test.go @@ -236,7 +236,7 @@ func TestAccDeployBasicBundleLogs(t *testing.T) { stdout, stderr := blackBoxRun(t, root, "bundle", "deploy") assert.Equal(t, strings.Join([]string{ - fmt.Sprintf("Uploading bundle files to /Users/%s/.bundle/%s/files...", currentUser.UserName, uniqueId), + fmt.Sprintf("Uploading bundle files to /Workspace/Users/%s/.bundle/%s/files...", currentUser.UserName, uniqueId), "Deploying resources...", "Updating deployment state...", "Deployment complete!\n", diff --git a/internal/bundle/helpers.go b/internal/bundle/helpers.go index 3547c175..b8c81a8d 100644 --- a/internal/bundle/helpers.go +++ b/internal/bundle/helpers.go @@ -114,7 +114,7 @@ func getBundleRemoteRootPath(w *databricks.WorkspaceClient, t *testing.T, unique // Compute root path for the bundle deployment me, err := w.CurrentUser.Me(context.Background()) require.NoError(t, err) - root := fmt.Sprintf("/Users/%s/.bundle/%s", me.UserName, uniqueId) + root := fmt.Sprintf("/Workspace/Users/%s/.bundle/%s", me.UserName, uniqueId) return root } diff --git a/libs/template/templates/dbt-sql/template/{{.project_name}}/databricks.yml.tmpl b/libs/template/templates/dbt-sql/template/{{.project_name}}/databricks.yml.tmpl index f96ce4fe..5594749a 100644 --- a/libs/template/templates/dbt-sql/template/{{.project_name}}/databricks.yml.tmpl +++ b/libs/template/templates/dbt-sql/template/{{.project_name}}/databricks.yml.tmpl @@ -24,8 +24,8 @@ targets: mode: production workspace: host: {{workspace_host}} - # We explicitly specify /Users/{{user_name}} to make sure we only have a single copy. - root_path: /Users/{{user_name}}/.bundle/${bundle.name}/${bundle.target} + # We explicitly specify /Workspace/Users/{{user_name}} to make sure we only have a single copy. + root_path: /Workspace/Users/{{user_name}}/.bundle/${bundle.name}/${bundle.target} permissions: - {{if is_service_principal}}service_principal{{else}}user{{end}}_name: {{user_name}} level: CAN_MANAGE diff --git a/libs/template/templates/default-python/template/{{.project_name}}/databricks.yml.tmpl b/libs/template/templates/default-python/template/{{.project_name}}/databricks.yml.tmpl index 8544dc83..c42b822a 100644 --- a/libs/template/templates/default-python/template/{{.project_name}}/databricks.yml.tmpl +++ b/libs/template/templates/default-python/template/{{.project_name}}/databricks.yml.tmpl @@ -21,8 +21,8 @@ targets: mode: production workspace: host: {{workspace_host}} - # We explicitly specify /Users/{{user_name}} to make sure we only have a single copy. - root_path: /Users/{{user_name}}/.bundle/${bundle.name}/${bundle.target} + # We explicitly specify /Workspace/Users/{{user_name}} to make sure we only have a single copy. + root_path: /Workspace/Users/{{user_name}}/.bundle/${bundle.name}/${bundle.target} permissions: - {{if is_service_principal}}service_principal{{else}}user{{end}}_name: {{user_name}} level: CAN_MANAGE diff --git a/libs/template/templates/default-python/template/{{.project_name}}/resources/{{.project_name}}.pipeline.yml.tmpl b/libs/template/templates/default-python/template/{{.project_name}}/resources/{{.project_name}}.pipeline.yml.tmpl index bf469046..50e5ad97 100644 --- a/libs/template/templates/default-python/template/{{.project_name}}/resources/{{.project_name}}.pipeline.yml.tmpl +++ b/libs/template/templates/default-python/template/{{.project_name}}/resources/{{.project_name}}.pipeline.yml.tmpl @@ -15,4 +15,4 @@ resources: path: ../src/dlt_pipeline.ipynb configuration: - bundle.sourcePath: /Workspace/${workspace.file_path}/src + bundle.sourcePath: ${workspace.file_path}/src diff --git a/libs/template/templates/default-sql/template/{{.project_name}}/databricks.yml.tmpl b/libs/template/templates/default-sql/template/{{.project_name}}/databricks.yml.tmpl index 55c1aae4..51d03e99 100644 --- a/libs/template/templates/default-sql/template/{{.project_name}}/databricks.yml.tmpl +++ b/libs/template/templates/default-sql/template/{{.project_name}}/databricks.yml.tmpl @@ -41,8 +41,8 @@ targets: mode: production workspace: host: {{workspace_host}} - # We explicitly specify /Users/{{user_name}} to make sure we only have a single copy. - root_path: /Users/{{user_name}}/.bundle/${bundle.name}/${bundle.target} + # We explicitly specify /Workspace/Users/{{user_name}} to make sure we only have a single copy. + root_path: /Workspace/Users/{{user_name}}/.bundle/${bundle.name}/${bundle.target} variables: warehouse_id: {{index ((regexp "[^/]+$").FindStringSubmatch .http_path) 0}} catalog: {{.default_catalog}} From bca9c2eda4cf66d1baaaf78a11abf67d5e587503 Mon Sep 17 00:00:00 2001 From: shreyas-goenka <88374338+shreyas-goenka@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:46:20 +0530 Subject: [PATCH 06/24] Add validation for files with a `.(resource-name).yml` extension (#1780) ## Changes We want to encourage a pattern of specifying only a single resource in a YAML file when the `.(resource-type).yml` extension is used (for example, `.job.yml`). This convention could allow us to bijectively map a resource YAML file to its corresponding resource in the Databricks workspace. This PR: 1. Emits a recommendation diagnostic when we detect this convention is being violated. We can promote this to a warning when we want to encourage this pattern more strongly. 2. Visualises the recommendation diagnostics in the `bundle validate` command. **NOTE:** While this PR also shows the recommendation for `.yaml` files, we do not encourage users to use this extension. We only support it here since it's part of the YAML standard and some existing users might already be using `.yaml`. ## Tests Unit tests and manually. Here's what an example output looks like: ``` Recommendation: define a single job in a file with the .job.yml extension. at resources.jobs.bar resources.jobs.foo in foo.job.yml:13:7 foo.job.yml:5:7 The following resources are defined or configured in this file: - bar (job) - foo (job) ``` --------- Co-authored-by: Lennart Kats (databricks) --- bundle/config/loader/entry_point_test.go | 2 +- bundle/config/loader/process_include.go | 130 ++++++++++++ bundle/config/loader/process_include_test.go | 185 +++++++++++++++++- .../testdata/{ => basic}/databricks.yml | 0 .../loader/testdata/{ => basic}/host.yml | 0 .../format_match/job_and_pipeline.yml | 11 ++ .../format_match/multiple_resources.yml | 43 ++++ .../testdata/format_match/one_job.job.yml | 11 ++ .../format_match/one_pipeline.pipeline.yaml | 4 + .../loader/testdata/format_match/two_job.yml | 7 + .../job_and_pipeline.experiment.yml | 11 ++ .../format_not_match/job_and_pipeline.job.yml | 11 ++ ...tiple_resources.model_serving_endpoint.yml | 43 ++++ .../second_job_in_target.job.yml | 11 ++ .../format_not_match/single_job.pipeline.yaml | 11 ++ .../format_not_match/two_jobs.job.yml | 7 + .../two_jobs_in_target.job.yml | 8 + bundle/config/resources.go | 19 ++ bundle/config/resources_test.go | 16 ++ bundle/render/render_text_output.go | 34 +++- bundle/render/render_text_output_test.go | 164 +++++++++++++++- libs/diag/severity.go | 1 + 22 files changed, 721 insertions(+), 8 deletions(-) rename bundle/config/loader/testdata/{ => basic}/databricks.yml (100%) rename bundle/config/loader/testdata/{ => basic}/host.yml (100%) create mode 100644 bundle/config/loader/testdata/format_match/job_and_pipeline.yml create mode 100644 bundle/config/loader/testdata/format_match/multiple_resources.yml create mode 100644 bundle/config/loader/testdata/format_match/one_job.job.yml create mode 100644 bundle/config/loader/testdata/format_match/one_pipeline.pipeline.yaml create mode 100644 bundle/config/loader/testdata/format_match/two_job.yml create mode 100644 bundle/config/loader/testdata/format_not_match/job_and_pipeline.experiment.yml create mode 100644 bundle/config/loader/testdata/format_not_match/job_and_pipeline.job.yml create mode 100644 bundle/config/loader/testdata/format_not_match/multiple_resources.model_serving_endpoint.yml create mode 100644 bundle/config/loader/testdata/format_not_match/second_job_in_target.job.yml create mode 100644 bundle/config/loader/testdata/format_not_match/single_job.pipeline.yaml create mode 100644 bundle/config/loader/testdata/format_not_match/two_jobs.job.yml create mode 100644 bundle/config/loader/testdata/format_not_match/two_jobs_in_target.job.yml diff --git a/bundle/config/loader/entry_point_test.go b/bundle/config/loader/entry_point_test.go index 406b9b67..0723c056 100644 --- a/bundle/config/loader/entry_point_test.go +++ b/bundle/config/loader/entry_point_test.go @@ -18,7 +18,7 @@ func TestEntryPointNoRootPath(t *testing.T) { func TestEntryPoint(t *testing.T) { b := &bundle.Bundle{ - BundleRootPath: "testdata", + BundleRootPath: "testdata/basic", } diags := bundle.Apply(context.Background(), b, loader.EntryPoint()) require.NoError(t, diags.Error()) diff --git a/bundle/config/loader/process_include.go b/bundle/config/loader/process_include.go index 7cf9a17d..f82f5db1 100644 --- a/bundle/config/loader/process_include.go +++ b/bundle/config/loader/process_include.go @@ -3,12 +3,135 @@ package loader import ( "context" "fmt" + "slices" + "sort" + "strings" "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config" "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/dyn" ) +func validateFileFormat(configRoot dyn.Value, filePath string) diag.Diagnostics { + for _, resourceDescription := range config.SupportedResources() { + singularName := resourceDescription.SingularName + + for _, yamlExt := range []string{"yml", "yaml"} { + ext := fmt.Sprintf(".%s.%s", singularName, yamlExt) + if strings.HasSuffix(filePath, ext) { + return validateSingleResourceDefined(configRoot, ext, singularName) + } + } + } + + return nil +} + +func validateSingleResourceDefined(configRoot dyn.Value, ext, typ string) diag.Diagnostics { + type resource struct { + path dyn.Path + value dyn.Value + typ string + key string + } + + resources := []resource{} + supportedResources := config.SupportedResources() + + // Gather all resources defined in the resources block. + _, err := dyn.MapByPattern( + configRoot, + dyn.NewPattern(dyn.Key("resources"), dyn.AnyKey(), dyn.AnyKey()), + func(p dyn.Path, v dyn.Value) (dyn.Value, error) { + // The key for the resource, e.g. "my_job" for jobs.my_job. + k := p[2].Key() + // The type of the resource, e.g. "job" for jobs.my_job. + typ := supportedResources[p[1].Key()].SingularName + + resources = append(resources, resource{path: p, value: v, typ: typ, key: k}) + return v, nil + }) + if err != nil { + return diag.FromErr(err) + } + + // Gather all resources defined in a target block. + _, err = dyn.MapByPattern( + configRoot, + dyn.NewPattern(dyn.Key("targets"), dyn.AnyKey(), dyn.Key("resources"), dyn.AnyKey(), dyn.AnyKey()), + func(p dyn.Path, v dyn.Value) (dyn.Value, error) { + // The key for the resource, e.g. "my_job" for jobs.my_job. + k := p[4].Key() + // The type of the resource, e.g. "job" for jobs.my_job. + typ := supportedResources[p[3].Key()].SingularName + + resources = append(resources, resource{path: p, value: v, typ: typ, key: k}) + return v, nil + }) + if err != nil { + return diag.FromErr(err) + } + + typeMatch := true + seenKeys := map[string]struct{}{} + for _, rr := range resources { + // case: The resource is not of the correct type. + if rr.typ != typ { + typeMatch = false + break + } + + seenKeys[rr.key] = struct{}{} + } + + // Format matches. There's at most one resource defined in the file. + // The resource is also of the correct type. + if typeMatch && len(seenKeys) <= 1 { + return nil + } + + detail := strings.Builder{} + detail.WriteString("The following resources are defined or configured in this file:\n") + lines := []string{} + for _, r := range resources { + lines = append(lines, fmt.Sprintf(" - %s (%s)\n", r.key, r.typ)) + } + // Sort the lines to print to make the output deterministic. + sort.Strings(lines) + // Compact the lines before writing them to the message to remove any duplicate lines. + // This is needed because we do not dedup earlier when gathering the resources + // and it's valid to define the same resource in both the resources and targets block. + lines = slices.Compact(lines) + for _, l := range lines { + detail.WriteString(l) + } + + locations := []dyn.Location{} + paths := []dyn.Path{} + for _, rr := range resources { + locations = append(locations, rr.value.Locations()...) + paths = append(paths, rr.path) + } + // Sort the locations and paths to make the output deterministic. + sort.Slice(locations, func(i, j int) bool { + return locations[i].String() < locations[j].String() + }) + sort.Slice(paths, func(i, j int) bool { + return paths[i].String() < paths[j].String() + }) + + return diag.Diagnostics{ + { + Severity: diag.Recommendation, + Summary: fmt.Sprintf("define a single %s in a file with the %s extension.", strings.ReplaceAll(typ, "_", " "), ext), + Detail: detail.String(), + Locations: locations, + Paths: paths, + }, + } +} + type processInclude struct { fullPath string relPath string @@ -31,6 +154,13 @@ func (m *processInclude) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnos if diags.HasError() { return diags } + + // Add any diagnostics associated with the file format. + diags = append(diags, validateFileFormat(this.Value(), m.relPath)...) + if diags.HasError() { + return diags + } + err := b.Config.Merge(this) if err != nil { diags = diags.Extend(diag.FromErr(err)) diff --git a/bundle/config/loader/process_include_test.go b/bundle/config/loader/process_include_test.go index 2ccd84b3..66c695e1 100644 --- a/bundle/config/loader/process_include_test.go +++ b/bundle/config/loader/process_include_test.go @@ -8,13 +8,15 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config" "github.com/databricks/cli/bundle/config/loader" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/dyn" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestProcessInclude(t *testing.T) { b := &bundle.Bundle{ - BundleRootPath: "testdata", + BundleRootPath: "testdata/basic", Config: config.Root{ Workspace: config.Workspace{ Host: "foo", @@ -33,3 +35,184 @@ func TestProcessInclude(t *testing.T) { require.NoError(t, diags.Error()) assert.Equal(t, "bar", b.Config.Workspace.Host) } + +func TestProcessIncludeFormatMatch(t *testing.T) { + for _, fileName := range []string{ + "one_job.job.yml", + "one_pipeline.pipeline.yaml", + "two_job.yml", + "job_and_pipeline.yml", + "multiple_resources.yml", + } { + t.Run(fileName, func(t *testing.T) { + b := &bundle.Bundle{ + BundleRootPath: "testdata/format_match", + Config: config.Root{ + Bundle: config.Bundle{ + Name: "format_test", + }, + }, + } + + m := loader.ProcessInclude(filepath.Join(b.BundleRootPath, fileName), fileName) + diags := bundle.Apply(context.Background(), b, m) + assert.Empty(t, diags) + }) + } +} + +func TestProcessIncludeFormatNotMatch(t *testing.T) { + for fileName, expectedDiags := range map[string]diag.Diagnostics{ + "single_job.pipeline.yaml": { + { + Severity: diag.Recommendation, + Summary: "define a single pipeline in a file with the .pipeline.yaml extension.", + Detail: "The following resources are defined or configured in this file:\n - job1 (job)\n", + Locations: []dyn.Location{ + {File: filepath.FromSlash("testdata/format_not_match/single_job.pipeline.yaml"), Line: 11, Column: 11}, + {File: filepath.FromSlash("testdata/format_not_match/single_job.pipeline.yaml"), Line: 4, Column: 7}, + }, + Paths: []dyn.Path{ + dyn.MustPathFromString("resources.jobs.job1"), + dyn.MustPathFromString("targets.target1.resources.jobs.job1"), + }, + }, + }, + "job_and_pipeline.job.yml": { + { + Severity: diag.Recommendation, + Summary: "define a single job in a file with the .job.yml extension.", + Detail: "The following resources are defined or configured in this file:\n - job1 (job)\n - pipeline1 (pipeline)\n", + Locations: []dyn.Location{ + {File: filepath.FromSlash("testdata/format_not_match/job_and_pipeline.job.yml"), Line: 11, Column: 11}, + {File: filepath.FromSlash("testdata/format_not_match/job_and_pipeline.job.yml"), Line: 4, Column: 7}, + }, + Paths: []dyn.Path{ + dyn.MustPathFromString("resources.pipelines.pipeline1"), + dyn.MustPathFromString("targets.target1.resources.jobs.job1"), + }, + }, + }, + "job_and_pipeline.experiment.yml": { + { + Severity: diag.Recommendation, + Summary: "define a single experiment in a file with the .experiment.yml extension.", + Detail: "The following resources are defined or configured in this file:\n - job1 (job)\n - pipeline1 (pipeline)\n", + Locations: []dyn.Location{ + {File: filepath.FromSlash("testdata/format_not_match/job_and_pipeline.experiment.yml"), Line: 11, Column: 11}, + {File: filepath.FromSlash("testdata/format_not_match/job_and_pipeline.experiment.yml"), Line: 4, Column: 7}, + }, + Paths: []dyn.Path{ + dyn.MustPathFromString("resources.pipelines.pipeline1"), + dyn.MustPathFromString("targets.target1.resources.jobs.job1"), + }, + }, + }, + "two_jobs.job.yml": { + { + Severity: diag.Recommendation, + Summary: "define a single job in a file with the .job.yml extension.", + Detail: "The following resources are defined or configured in this file:\n - job1 (job)\n - job2 (job)\n", + Locations: []dyn.Location{ + {File: filepath.FromSlash("testdata/format_not_match/two_jobs.job.yml"), Line: 4, Column: 7}, + {File: filepath.FromSlash("testdata/format_not_match/two_jobs.job.yml"), Line: 7, Column: 7}, + }, + Paths: []dyn.Path{ + dyn.MustPathFromString("resources.jobs.job1"), + dyn.MustPathFromString("resources.jobs.job2"), + }, + }, + }, + "second_job_in_target.job.yml": { + { + Severity: diag.Recommendation, + Summary: "define a single job in a file with the .job.yml extension.", + Detail: "The following resources are defined or configured in this file:\n - job1 (job)\n - job2 (job)\n", + Locations: []dyn.Location{ + {File: filepath.FromSlash("testdata/format_not_match/second_job_in_target.job.yml"), Line: 11, Column: 11}, + {File: filepath.FromSlash("testdata/format_not_match/second_job_in_target.job.yml"), Line: 4, Column: 7}, + }, + Paths: []dyn.Path{ + dyn.MustPathFromString("resources.jobs.job1"), + dyn.MustPathFromString("targets.target1.resources.jobs.job2"), + }, + }, + }, + "two_jobs_in_target.job.yml": { + { + Severity: diag.Recommendation, + Summary: "define a single job in a file with the .job.yml extension.", + Detail: "The following resources are defined or configured in this file:\n - job1 (job)\n - job2 (job)\n", + Locations: []dyn.Location{ + {File: filepath.FromSlash("testdata/format_not_match/two_jobs_in_target.job.yml"), Line: 6, Column: 11}, + {File: filepath.FromSlash("testdata/format_not_match/two_jobs_in_target.job.yml"), Line: 8, Column: 11}, + }, + Paths: []dyn.Path{ + dyn.MustPathFromString("targets.target1.resources.jobs.job1"), + dyn.MustPathFromString("targets.target1.resources.jobs.job2"), + }, + }, + }, + "multiple_resources.model_serving_endpoint.yml": { + { + Severity: diag.Recommendation, + Summary: "define a single model serving endpoint in a file with the .model_serving_endpoint.yml extension.", + Detail: `The following resources are defined or configured in this file: + - experiment1 (experiment) + - job1 (job) + - job2 (job) + - job3 (job) + - model1 (model) + - model_serving_endpoint1 (model_serving_endpoint) + - pipeline1 (pipeline) + - pipeline2 (pipeline) + - quality_monitor1 (quality_monitor) + - registered_model1 (registered_model) + - schema1 (schema) +`, + Locations: []dyn.Location{ + {File: filepath.FromSlash("testdata/format_not_match/multiple_resources.model_serving_endpoint.yml"), Line: 12, Column: 7}, + {File: filepath.FromSlash("testdata/format_not_match/multiple_resources.model_serving_endpoint.yml"), Line: 14, Column: 7}, + {File: filepath.FromSlash("testdata/format_not_match/multiple_resources.model_serving_endpoint.yml"), Line: 18, Column: 7}, + {File: filepath.FromSlash("testdata/format_not_match/multiple_resources.model_serving_endpoint.yml"), Line: 22, Column: 7}, + {File: filepath.FromSlash("testdata/format_not_match/multiple_resources.model_serving_endpoint.yml"), Line: 24, Column: 7}, + {File: filepath.FromSlash("testdata/format_not_match/multiple_resources.model_serving_endpoint.yml"), Line: 28, Column: 7}, + {File: filepath.FromSlash("testdata/format_not_match/multiple_resources.model_serving_endpoint.yml"), Line: 35, Column: 11}, + {File: filepath.FromSlash("testdata/format_not_match/multiple_resources.model_serving_endpoint.yml"), Line: 39, Column: 11}, + {File: filepath.FromSlash("testdata/format_not_match/multiple_resources.model_serving_endpoint.yml"), Line: 43, Column: 11}, + {File: filepath.FromSlash("testdata/format_not_match/multiple_resources.model_serving_endpoint.yml"), Line: 4, Column: 7}, + {File: filepath.FromSlash("testdata/format_not_match/multiple_resources.model_serving_endpoint.yml"), Line: 8, Column: 7}, + }, + Paths: []dyn.Path{ + dyn.MustPathFromString("resources.experiments.experiment1"), + dyn.MustPathFromString("resources.jobs.job1"), + dyn.MustPathFromString("resources.jobs.job2"), + dyn.MustPathFromString("resources.model_serving_endpoints.model_serving_endpoint1"), + dyn.MustPathFromString("resources.models.model1"), + dyn.MustPathFromString("resources.pipelines.pipeline1"), + dyn.MustPathFromString("resources.pipelines.pipeline2"), + dyn.MustPathFromString("resources.schemas.schema1"), + dyn.MustPathFromString("targets.target1.resources.jobs.job3"), + dyn.MustPathFromString("targets.target1.resources.quality_monitors.quality_monitor1"), + dyn.MustPathFromString("targets.target1.resources.registered_models.registered_model1"), + }, + }, + }, + } { + t.Run(fileName, func(t *testing.T) { + b := &bundle.Bundle{ + BundleRootPath: "testdata/format_not_match", + Config: config.Root{ + Bundle: config.Bundle{ + Name: "format_test", + }, + }, + } + + m := loader.ProcessInclude(filepath.Join(b.BundleRootPath, fileName), fileName) + diags := bundle.Apply(context.Background(), b, m) + require.Len(t, diags, 1) + assert.Equal(t, expectedDiags, diags) + }) + } +} diff --git a/bundle/config/loader/testdata/databricks.yml b/bundle/config/loader/testdata/basic/databricks.yml similarity index 100% rename from bundle/config/loader/testdata/databricks.yml rename to bundle/config/loader/testdata/basic/databricks.yml diff --git a/bundle/config/loader/testdata/host.yml b/bundle/config/loader/testdata/basic/host.yml similarity index 100% rename from bundle/config/loader/testdata/host.yml rename to bundle/config/loader/testdata/basic/host.yml diff --git a/bundle/config/loader/testdata/format_match/job_and_pipeline.yml b/bundle/config/loader/testdata/format_match/job_and_pipeline.yml new file mode 100644 index 00000000..0867fcae --- /dev/null +++ b/bundle/config/loader/testdata/format_match/job_and_pipeline.yml @@ -0,0 +1,11 @@ +resources: + pipelines: + pipeline1: + name: pipeline1 + +targets: + target1: + resources: + jobs: + job1: + name: job1 diff --git a/bundle/config/loader/testdata/format_match/multiple_resources.yml b/bundle/config/loader/testdata/format_match/multiple_resources.yml new file mode 100644 index 00000000..dc8e837c --- /dev/null +++ b/bundle/config/loader/testdata/format_match/multiple_resources.yml @@ -0,0 +1,43 @@ +resources: + experiments: + experiment1: + name: experiment1 + + model_serving_endpoints: + model_serving_endpoint1: + name: model_serving_endpoint1 + + jobs: + job1: + name: job1 + job2: + name: job2 + + models: + model1: + name: model1 + + pipelines: + pipeline1: + name: pipeline1 + pipeline2: + name: pipeline2 + + schemas: + schema1: + name: schema1 + +targets: + target1: + resources: + quality_monitors: + quality_monitor1: + baseline_table_name: quality_monitor1 + + jobs: + job3: + name: job3 + + registered_models: + registered_model1: + name: registered_model1 diff --git a/bundle/config/loader/testdata/format_match/one_job.job.yml b/bundle/config/loader/testdata/format_match/one_job.job.yml new file mode 100644 index 00000000..91af87cd --- /dev/null +++ b/bundle/config/loader/testdata/format_match/one_job.job.yml @@ -0,0 +1,11 @@ +resources: + jobs: + job1: + name: job1 + +targets: + target1: + resources: + jobs: + job1: + description: job1 diff --git a/bundle/config/loader/testdata/format_match/one_pipeline.pipeline.yaml b/bundle/config/loader/testdata/format_match/one_pipeline.pipeline.yaml new file mode 100644 index 00000000..85cb0d7f --- /dev/null +++ b/bundle/config/loader/testdata/format_match/one_pipeline.pipeline.yaml @@ -0,0 +1,4 @@ +resources: + pipelines: + pipeline1: + name: pipeline1 diff --git a/bundle/config/loader/testdata/format_match/two_job.yml b/bundle/config/loader/testdata/format_match/two_job.yml new file mode 100644 index 00000000..81ff90a7 --- /dev/null +++ b/bundle/config/loader/testdata/format_match/two_job.yml @@ -0,0 +1,7 @@ +resources: + jobs: + job1: + name: job1 + + job2: + name: job2 diff --git a/bundle/config/loader/testdata/format_not_match/job_and_pipeline.experiment.yml b/bundle/config/loader/testdata/format_not_match/job_and_pipeline.experiment.yml new file mode 100644 index 00000000..0867fcae --- /dev/null +++ b/bundle/config/loader/testdata/format_not_match/job_and_pipeline.experiment.yml @@ -0,0 +1,11 @@ +resources: + pipelines: + pipeline1: + name: pipeline1 + +targets: + target1: + resources: + jobs: + job1: + name: job1 diff --git a/bundle/config/loader/testdata/format_not_match/job_and_pipeline.job.yml b/bundle/config/loader/testdata/format_not_match/job_and_pipeline.job.yml new file mode 100644 index 00000000..0867fcae --- /dev/null +++ b/bundle/config/loader/testdata/format_not_match/job_and_pipeline.job.yml @@ -0,0 +1,11 @@ +resources: + pipelines: + pipeline1: + name: pipeline1 + +targets: + target1: + resources: + jobs: + job1: + name: job1 diff --git a/bundle/config/loader/testdata/format_not_match/multiple_resources.model_serving_endpoint.yml b/bundle/config/loader/testdata/format_not_match/multiple_resources.model_serving_endpoint.yml new file mode 100644 index 00000000..dc8e837c --- /dev/null +++ b/bundle/config/loader/testdata/format_not_match/multiple_resources.model_serving_endpoint.yml @@ -0,0 +1,43 @@ +resources: + experiments: + experiment1: + name: experiment1 + + model_serving_endpoints: + model_serving_endpoint1: + name: model_serving_endpoint1 + + jobs: + job1: + name: job1 + job2: + name: job2 + + models: + model1: + name: model1 + + pipelines: + pipeline1: + name: pipeline1 + pipeline2: + name: pipeline2 + + schemas: + schema1: + name: schema1 + +targets: + target1: + resources: + quality_monitors: + quality_monitor1: + baseline_table_name: quality_monitor1 + + jobs: + job3: + name: job3 + + registered_models: + registered_model1: + name: registered_model1 diff --git a/bundle/config/loader/testdata/format_not_match/second_job_in_target.job.yml b/bundle/config/loader/testdata/format_not_match/second_job_in_target.job.yml new file mode 100644 index 00000000..628b9879 --- /dev/null +++ b/bundle/config/loader/testdata/format_not_match/second_job_in_target.job.yml @@ -0,0 +1,11 @@ +resources: + jobs: + job1: + name: job1 + +targets: + target1: + resources: + jobs: + job2: + name: job2 diff --git a/bundle/config/loader/testdata/format_not_match/single_job.pipeline.yaml b/bundle/config/loader/testdata/format_not_match/single_job.pipeline.yaml new file mode 100644 index 00000000..91af87cd --- /dev/null +++ b/bundle/config/loader/testdata/format_not_match/single_job.pipeline.yaml @@ -0,0 +1,11 @@ +resources: + jobs: + job1: + name: job1 + +targets: + target1: + resources: + jobs: + job1: + description: job1 diff --git a/bundle/config/loader/testdata/format_not_match/two_jobs.job.yml b/bundle/config/loader/testdata/format_not_match/two_jobs.job.yml new file mode 100644 index 00000000..81ff90a7 --- /dev/null +++ b/bundle/config/loader/testdata/format_not_match/two_jobs.job.yml @@ -0,0 +1,7 @@ +resources: + jobs: + job1: + name: job1 + + job2: + name: job2 diff --git a/bundle/config/loader/testdata/format_not_match/two_jobs_in_target.job.yml b/bundle/config/loader/testdata/format_not_match/two_jobs_in_target.job.yml new file mode 100644 index 00000000..3b489c1f --- /dev/null +++ b/bundle/config/loader/testdata/format_not_match/two_jobs_in_target.job.yml @@ -0,0 +1,8 @@ +targets: + target1: + resources: + jobs: + job1: + description: job1 + job2: + description: job2 diff --git a/bundle/config/resources.go b/bundle/config/resources.go index a3afb7fc..dc51a7ca 100644 --- a/bundle/config/resources.go +++ b/bundle/config/resources.go @@ -59,3 +59,22 @@ func (r *Resources) FindResourceByConfigKey(key string) (ConfigResource, error) return found[0], nil } + +type ResourceDescription struct { + SingularName string +} + +// The keys of the map corresponds to the resource key in the bundle configuration. +func SupportedResources() map[string]ResourceDescription { + return map[string]ResourceDescription{ + "jobs": {SingularName: "job"}, + "pipelines": {SingularName: "pipeline"}, + "models": {SingularName: "model"}, + "experiments": {SingularName: "experiment"}, + "model_serving_endpoints": {SingularName: "model_serving_endpoint"}, + "registered_models": {SingularName: "registered_model"}, + "quality_monitors": {SingularName: "quality_monitor"}, + "schemas": {SingularName: "schema"}, + "clusters": {SingularName: "cluster"}, + } +} diff --git a/bundle/config/resources_test.go b/bundle/config/resources_test.go index 6860d73d..c1b76118 100644 --- a/bundle/config/resources_test.go +++ b/bundle/config/resources_test.go @@ -3,6 +3,7 @@ package config import ( "encoding/json" "reflect" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -61,3 +62,18 @@ func TestCustomMarshallerIsImplemented(t *testing.T) { }, "Resource %s does not have a custom unmarshaller", field.Name) } } + +func TestSupportedResources(t *testing.T) { + expected := map[string]ResourceDescription{} + typ := reflect.TypeOf(Resources{}) + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + jsonTags := strings.Split(field.Tag.Get("json"), ",") + singularName := strings.TrimSuffix(jsonTags[0], "s") + expected[jsonTags[0]] = ResourceDescription{SingularName: singularName} + } + + // Please add your resource to the SupportedResources() function in resources.go + // if you are adding a new resource. + assert.Equal(t, expected, SupportedResources()) +} diff --git a/bundle/render/render_text_output.go b/bundle/render/render_text_output.go index e1fad98a..56387c38 100644 --- a/bundle/render/render_text_output.go +++ b/bundle/render/render_text_output.go @@ -56,6 +56,20 @@ const warningTemplate = `{{ "Warning" | yellow }}: {{ .Summary }} ` +const recommendationTemplate = `{{ "Recommendation" | blue }}: {{ .Summary }} +{{- range $index, $element := .Paths }} + {{ if eq $index 0 }}at {{else}} {{ end}}{{ $element.String | green }} +{{- end }} +{{- range $index, $element := .Locations }} + {{ if eq $index 0 }}in {{else}} {{ end}}{{ $element.String | cyan }} +{{- end }} +{{- if .Detail }} + +{{ .Detail }} +{{- end }} + +` + const summaryTemplate = `{{- if .Name -}} Name: {{ .Name | bold }} {{- if .Target }} @@ -94,9 +108,20 @@ func buildTrailer(diags diag.Diagnostics) string { if warnings := len(diags.Filter(diag.Warning)); warnings > 0 { parts = append(parts, color.YellowString(pluralize(warnings, "warning", "warnings"))) } - if len(parts) > 0 { - return fmt.Sprintf("Found %s", strings.Join(parts, " and ")) - } else { + if recommendations := len(diags.Filter(diag.Recommendation)); recommendations > 0 { + parts = append(parts, color.BlueString(pluralize(recommendations, "recommendation", "recommendations"))) + } + switch { + case len(parts) >= 3: + first := strings.Join(parts[:len(parts)-1], ", ") + last := parts[len(parts)-1] + return fmt.Sprintf("Found %s, and %s", first, last) + case len(parts) == 2: + return fmt.Sprintf("Found %s and %s", parts[0], parts[1]) + case len(parts) == 1: + return fmt.Sprintf("Found %s", parts[0]) + default: + // No diagnostics to print. return color.GreenString("Validation OK!") } } @@ -130,6 +155,7 @@ func renderSummaryTemplate(out io.Writer, b *bundle.Bundle, diags diag.Diagnosti func renderDiagnostics(out io.Writer, b *bundle.Bundle, diags diag.Diagnostics) error { errorT := template.Must(template.New("error").Funcs(renderFuncMap).Parse(errorTemplate)) warningT := template.Must(template.New("warning").Funcs(renderFuncMap).Parse(warningTemplate)) + recommendationT := template.Must(template.New("recommendation").Funcs(renderFuncMap).Parse(recommendationTemplate)) // Print errors and warnings. for _, d := range diags { @@ -139,6 +165,8 @@ func renderDiagnostics(out io.Writer, b *bundle.Bundle, diags diag.Diagnostics) t = errorT case diag.Warning: t = warningT + case diag.Recommendation: + t = recommendationT } for i := range d.Locations { diff --git a/bundle/render/render_text_output_test.go b/bundle/render/render_text_output_test.go index 976f86e7..1a41fa01 100644 --- a/bundle/render/render_text_output_test.go +++ b/bundle/render/render_text_output_test.go @@ -45,6 +45,19 @@ func TestRenderTextOutput(t *testing.T) { "\n" + "Found 1 error\n", }, + { + name: "nil bundle and 1 recommendation", + diags: diag.Diagnostics{ + { + Severity: diag.Recommendation, + Summary: "recommendation", + }, + }, + opts: RenderOptions{RenderSummaryTable: true}, + expected: "Recommendation: recommendation\n" + + "\n" + + "Found 1 recommendation\n", + }, { name: "bundle during 'load' and 1 error", bundle: loadingBundle, @@ -84,7 +97,7 @@ func TestRenderTextOutput(t *testing.T) { "Found 2 warnings\n", }, { - name: "bundle during 'load' and 2 errors, 1 warning with details", + name: "bundle during 'load' and 2 errors, 1 warning and 1 recommendation with details", bundle: loadingBundle, diags: diag.Diagnostics{ diag.Diagnostic{ @@ -105,6 +118,12 @@ func TestRenderTextOutput(t *testing.T) { Detail: "detail (3)", Locations: []dyn.Location{{File: "foo.py", Line: 3, Column: 1}}, }, + diag.Diagnostic{ + Severity: diag.Recommendation, + Summary: "recommendation (4)", + Detail: "detail (4)", + Locations: []dyn.Location{{File: "foo.py", Line: 4, Column: 1}}, + }, }, opts: RenderOptions{RenderSummaryTable: true}, expected: "Error: error (1)\n" + @@ -122,10 +141,114 @@ func TestRenderTextOutput(t *testing.T) { "\n" + "detail (3)\n" + "\n" + + "Recommendation: recommendation (4)\n" + + " in foo.py:4:1\n" + + "\n" + + "detail (4)\n" + + "\n" + "Name: test-bundle\n" + "Target: test-target\n" + "\n" + - "Found 2 errors and 1 warning\n", + "Found 2 errors, 1 warning, and 1 recommendation\n", + }, + { + name: "bundle during 'load' and 1 error and 1 warning", + bundle: loadingBundle, + diags: diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "error (1)", + Detail: "detail (1)", + Locations: []dyn.Location{{File: "foo.py", Line: 1, Column: 1}}, + }, + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "warning (2)", + Detail: "detail (2)", + Locations: []dyn.Location{{File: "foo.py", Line: 2, Column: 1}}, + }, + }, + opts: RenderOptions{RenderSummaryTable: true}, + expected: "Error: error (1)\n" + + " in foo.py:1:1\n" + + "\n" + + "detail (1)\n" + + "\n" + + "Warning: warning (2)\n" + + " in foo.py:2:1\n" + + "\n" + + "detail (2)\n" + + "\n" + + "Name: test-bundle\n" + + "Target: test-target\n" + + "\n" + + "Found 1 error and 1 warning\n", + }, + { + name: "bundle during 'load' and 1 errors, 2 warning and 2 recommendations with details", + bundle: loadingBundle, + diags: diag.Diagnostics{ + diag.Diagnostic{ + Severity: diag.Error, + Summary: "error (1)", + Detail: "detail (1)", + Locations: []dyn.Location{{File: "foo.py", Line: 1, Column: 1}}, + }, + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "warning (2)", + Detail: "detail (2)", + Locations: []dyn.Location{{File: "foo.py", Line: 2, Column: 1}}, + }, + diag.Diagnostic{ + Severity: diag.Warning, + Summary: "warning (3)", + Detail: "detail (3)", + Locations: []dyn.Location{{File: "foo.py", Line: 3, Column: 1}}, + }, + diag.Diagnostic{ + Severity: diag.Recommendation, + Summary: "recommendation (4)", + Detail: "detail (4)", + Locations: []dyn.Location{{File: "foo.py", Line: 4, Column: 1}}, + }, + diag.Diagnostic{ + Severity: diag.Recommendation, + Summary: "recommendation (5)", + Detail: "detail (5)", + Locations: []dyn.Location{{File: "foo.py", Line: 5, Column: 1}}, + }, + }, + opts: RenderOptions{RenderSummaryTable: true}, + expected: "Error: error (1)\n" + + " in foo.py:1:1\n" + + "\n" + + "detail (1)\n" + + "\n" + + "Warning: warning (2)\n" + + " in foo.py:2:1\n" + + "\n" + + "detail (2)\n" + + "\n" + + "Warning: warning (3)\n" + + " in foo.py:3:1\n" + + "\n" + + "detail (3)\n" + + "\n" + + "Recommendation: recommendation (4)\n" + + " in foo.py:4:1\n" + + "\n" + + "detail (4)\n" + + "\n" + + "Recommendation: recommendation (5)\n" + + " in foo.py:5:1\n" + + "\n" + + "detail (5)\n" + + "\n" + + "Name: test-bundle\n" + + "Target: test-target\n" + + "\n" + + "Found 1 error, 2 warnings, and 2 recommendations\n", }, { name: "bundle during 'init'", @@ -158,7 +281,7 @@ func TestRenderTextOutput(t *testing.T) { "Validation OK!\n", }, { - name: "nil bundle without summary with 1 error and 1 warning", + name: "nil bundle without summary with 1 error, 1 warning and 1 recommendation", bundle: nil, diags: diag.Diagnostics{ diag.Diagnostic{ @@ -173,6 +296,12 @@ func TestRenderTextOutput(t *testing.T) { Detail: "detail (2)", Locations: []dyn.Location{{File: "foo.py", Line: 3, Column: 1}}, }, + diag.Diagnostic{ + Severity: diag.Recommendation, + Summary: "recommendation (3)", + Detail: "detail (3)", + Locations: []dyn.Location{{File: "foo.py", Line: 5, Column: 1}}, + }, }, opts: RenderOptions{RenderSummaryTable: false}, expected: "Error: error (1)\n" + @@ -184,6 +313,11 @@ func TestRenderTextOutput(t *testing.T) { " in foo.py:3:1\n" + "\n" + "detail (2)\n" + + "\n" + + "Recommendation: recommendation (3)\n" + + " in foo.py:5:1\n" + + "\n" + + "detail (3)\n" + "\n", }, } @@ -304,6 +438,30 @@ func TestRenderDiagnostics(t *testing.T) { "\n" + "'name' is required\n\n", }, + { + name: "recommendation with multiple paths and locations", + diags: diag.Diagnostics{ + { + Severity: diag.Recommendation, + Summary: "summary", + Detail: "detail", + Paths: []dyn.Path{ + dyn.MustPathFromString("resources.jobs.xxx"), + dyn.MustPathFromString("resources.jobs.yyy"), + }, + Locations: []dyn.Location{ + {File: "foo.yaml", Line: 1, Column: 2}, + {File: "bar.yaml", Line: 3, Column: 4}, + }, + }, + }, + expected: "Recommendation: summary\n" + + " at resources.jobs.xxx\n" + + " resources.jobs.yyy\n" + + " in foo.yaml:1:2\n" + + " bar.yaml:3:4\n\n" + + "detail\n\n", + }, } for _, tc := range testCases { diff --git a/libs/diag/severity.go b/libs/diag/severity.go index d25c1280..0e88085f 100644 --- a/libs/diag/severity.go +++ b/libs/diag/severity.go @@ -6,4 +6,5 @@ const ( Error Severity = iota Warning Info + Recommendation ) From 589e5ff1a44b3632d69fa5d2b992530a54ab7247 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 14:58:25 +0200 Subject: [PATCH 07/24] Bump golang.org/x/term from 0.24.0 to 0.25.0 (#1811) Bumps [golang.org/x/term](https://github.com/golang/term) from 0.24.0 to 0.25.0.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/term&package-manager=go_modules&previous-version=0.24.0&new-version=0.25.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 9141274c..ae56b513 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( golang.org/x/mod v0.21.0 golang.org/x/oauth2 v0.23.0 golang.org/x/sync v0.8.0 - golang.org/x/term v0.24.0 + golang.org/x/term v0.25.0 golang.org/x/text v0.18.0 gopkg.in/ini.v1 v1.67.0 // Apache 2.0 gopkg.in/yaml.v3 v3.0.1 @@ -64,7 +64,7 @@ require ( go.opentelemetry.io/otel/trace v1.24.0 // indirect golang.org/x/crypto v0.24.0 // indirect golang.org/x/net v0.26.0 // indirect - golang.org/x/sys v0.25.0 // indirect + golang.org/x/sys v0.26.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/api v0.182.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e // indirect diff --git a/go.sum b/go.sum index 177707a5..e797ac54 100644 --- a/go.sum +++ b/go.sum @@ -212,10 +212,10 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= From 02bfd397eff71380b3b9e466f6a821eee6794fb5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 13:14:02 +0000 Subject: [PATCH 08/24] Bump golang.org/x/text from 0.18.0 to 0.19.0 (#1812) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.18.0 to 0.19.0.
Commits
  • 3043346 x/text: Correct examples in number/doc
  • 38a95c2 all: fix some comments
  • 20097e4 all: fix printf(var) mistakes detected by latest printf checker
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/text&package-manager=go_modules&previous-version=0.18.0&new-version=0.19.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ae56b513..c029e5da 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( golang.org/x/oauth2 v0.23.0 golang.org/x/sync v0.8.0 golang.org/x/term v0.25.0 - golang.org/x/text v0.18.0 + golang.org/x/text v0.19.0 gopkg.in/ini.v1 v1.67.0 // Apache 2.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index e797ac54..9f05b145 100644 --- a/go.sum +++ b/go.sum @@ -218,8 +218,8 @@ golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From 7a2141fc5ba1af695ba55aeac524bf80644d7606 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:21:05 +0200 Subject: [PATCH 09/24] Bump github.com/databricks/databricks-sdk-go from 0.47.0 to 0.48.0 (#1810) Bumps [github.com/databricks/databricks-sdk-go](https://github.com/databricks/databricks-sdk-go) from 0.47.0 to 0.48.0.
Release notes

Sourced from github.com/databricks/databricks-sdk-go's releases.

v0.48.0

Internal Changes

  • Update SDK to latest OpenAPI spec (#1057).

Note: This release contains breaking changes, please see the API changes below for more details.

API Changes:

OpenAPI SHA: 0c86ea6dbd9a730c24ff0d4e509603e476955ac5, Date: 2024-10-02

Changelog

Sourced from github.com/databricks/databricks-sdk-go's changelog.

[Release] Release v0.48.0

Internal Changes

  • Update SDK to latest OpenAPI spec (#1057).

Note: This release contains breaking changes, please see the API changes below for more details.

API Changes:

OpenAPI SHA: 0c86ea6dbd9a730c24ff0d4e509603e476955ac5, Date: 2024-10-02

Commits

Most Recent Ignore Conditions Applied to This Pull Request | Dependency Name | Ignore Conditions | | --- | --- | | github.com/databricks/databricks-sdk-go | [>= 0.28.a, < 0.29] |
[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/databricks/databricks-sdk-go&package-manager=go_modules&previous-version=0.47.0&new-version=0.48.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Andrew Nester --- .codegen/_openapi_sha | 2 +- bundle/schema/jsonschema.json | 235 +++++++++++++++++- cmd/workspace/apps/apps.go | 2 + .../git-credentials/git-credentials.go | 62 ++--- cmd/workspace/pipelines/pipelines.go | 1 + cmd/workspace/repos/overrides.go | 4 +- cmd/workspace/repos/repos.go | 35 +-- go.mod | 2 +- go.sum | 4 +- internal/helpers.go | 2 +- internal/locker_test.go | 2 +- internal/repos_test.go | 2 +- internal/sync_test.go | 2 +- 13 files changed, 296 insertions(+), 59 deletions(-) diff --git a/.codegen/_openapi_sha b/.codegen/_openapi_sha index ffd6f58d..303c7855 100644 --- a/.codegen/_openapi_sha +++ b/.codegen/_openapi_sha @@ -1 +1 @@ -6f6b1371e640f2dfeba72d365ac566368656f6b6 \ No newline at end of file +0c86ea6dbd9a730c24ff0d4e509603e476955ac5 \ No newline at end of file diff --git a/bundle/schema/jsonschema.json b/bundle/schema/jsonschema.json index afdf9fb9..ae209c01 100644 --- a/bundle/schema/jsonschema.json +++ b/bundle/schema/jsonschema.json @@ -402,6 +402,10 @@ { "type": "object", "properties": { + "ai_gateway": { + "description": "The AI Gateway configuration for the serving endpoint. NOTE: only external model endpoints are supported as of now.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/serving.AiGatewayConfig" + }, "config": { "description": "The core config of the serving endpoint.", "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/serving.EndpointCoreConfigInput" @@ -472,6 +476,10 @@ { "type": "object", "properties": { + "budget_policy_id": { + "description": "Budget policy of this pipeline.", + "$ref": "#/$defs/string" + }, "catalog": { "description": "A catalog in Unity Catalog to publish data from this pipeline to. If `target` is specified, tables in this pipeline are published to a `target` schema inside `catalog` (for example, `catalog`.`target`.`table`). If `target` is not specified, no data is published to Unity Catalog.", "$ref": "#/$defs/string" @@ -539,6 +547,10 @@ "description": "Whether Photon is enabled for this pipeline.", "$ref": "#/$defs/bool" }, + "schema": { + "description": "The default schema (database) where tables are read from or published to. The presence of this field implies that the pipeline is in direct publishing mode.", + "$ref": "#/$defs/string" + }, "serverless": { "description": "Whether serverless compute is enabled for this pipeline.", "$ref": "#/$defs/bool" @@ -1206,6 +1218,9 @@ "profile": { "$ref": "#/$defs/string" }, + "resource_path": { + "$ref": "#/$defs/string" + }, "root_path": { "$ref": "#/$defs/string" }, @@ -2632,7 +2647,7 @@ "type": "object", "properties": { "no_alert_for_skipped_runs": { - "description": "If true, do not send email to recipients specified in `on_failure` if the run is skipped.", + "description": "If true, do not send email to recipients specified in `on_failure` if the run is skipped.\nThis field is `deprecated`. Please use the `notification_settings.no_alert_for_skipped_runs` field.", "$ref": "#/$defs/bool" }, "on_duration_warning_threshold_exceeded": { @@ -3073,6 +3088,7 @@ "$ref": "#/$defs/map/string" }, "pipeline_params": { + "description": "Controls whether the pipeline should perform a full refresh", "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/jobs.PipelineParams" }, "python_named_params": { @@ -3547,7 +3563,7 @@ "type": "object", "properties": { "no_alert_for_skipped_runs": { - "description": "If true, do not send email to recipients specified in `on_failure` if the run is skipped.", + "description": "If true, do not send email to recipients specified in `on_failure` if the run is skipped.\nThis field is `deprecated`. Please use the `notification_settings.no_alert_for_skipped_runs` field.", "$ref": "#/$defs/bool" }, "on_duration_warning_threshold_exceeded": { @@ -4365,6 +4381,207 @@ } ] }, + "serving.AiGatewayConfig": { + "anyOf": [ + { + "type": "object", + "properties": { + "guardrails": { + "description": "Configuration for AI Guardrails to prevent unwanted data and unsafe data in requests and responses.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/serving.AiGatewayGuardrails" + }, + "inference_table_config": { + "description": "Configuration for payload logging using inference tables. Use these tables to monitor and audit data being sent to and received from model APIs and to improve model quality.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/serving.AiGatewayInferenceTableConfig" + }, + "rate_limits": { + "description": "Configuration for rate limits which can be set to limit endpoint traffic.", + "$ref": "#/$defs/slice/github.com/databricks/databricks-sdk-go/service/serving.AiGatewayRateLimit" + }, + "usage_tracking_config": { + "description": "Configuration to enable usage tracking using system tables. These tables allow you to monitor operational usage on endpoints and their associated costs.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/serving.AiGatewayUsageTrackingConfig" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "serving.AiGatewayGuardrailParameters": { + "anyOf": [ + { + "type": "object", + "properties": { + "invalid_keywords": { + "description": "List of invalid keywords. AI guardrail uses keyword or string matching to decide if the keyword exists in the request or response content.", + "$ref": "#/$defs/slice/string" + }, + "pii": { + "description": "Configuration for guardrail PII filter.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/serving.AiGatewayGuardrailPiiBehavior" + }, + "safety": { + "description": "Indicates whether the safety filter is enabled.", + "$ref": "#/$defs/bool" + }, + "valid_topics": { + "description": "The list of allowed topics. Given a chat request, this guardrail flags the request if its topic is not in the allowed topics.", + "$ref": "#/$defs/slice/string" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "serving.AiGatewayGuardrailPiiBehavior": { + "anyOf": [ + { + "type": "object", + "properties": { + "behavior": { + "description": "Behavior for PII filter. Currently only 'BLOCK' is supported. If 'BLOCK' is set for the input guardrail and the request contains PII, the request is not sent to the model server and 400 status code is returned; if 'BLOCK' is set for the output guardrail and the model response contains PII, the PII info in the response is redacted and 400 status code is returned.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/serving.AiGatewayGuardrailPiiBehaviorBehavior", + "enum": [ + "NONE", + "BLOCK" + ] + } + }, + "additionalProperties": false, + "required": [ + "behavior" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "serving.AiGatewayGuardrailPiiBehaviorBehavior": { + "type": "string" + }, + "serving.AiGatewayGuardrails": { + "anyOf": [ + { + "type": "object", + "properties": { + "input": { + "description": "Configuration for input guardrail filters.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/serving.AiGatewayGuardrailParameters" + }, + "output": { + "description": "Configuration for output guardrail filters.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/serving.AiGatewayGuardrailParameters" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "serving.AiGatewayInferenceTableConfig": { + "anyOf": [ + { + "type": "object", + "properties": { + "catalog_name": { + "description": "The name of the catalog in Unity Catalog. Required when enabling inference tables. NOTE: On update, you have to disable inference table first in order to change the catalog name.", + "$ref": "#/$defs/string" + }, + "enabled": { + "description": "Indicates whether the inference table is enabled.", + "$ref": "#/$defs/bool" + }, + "schema_name": { + "description": "The name of the schema in Unity Catalog. Required when enabling inference tables. NOTE: On update, you have to disable inference table first in order to change the schema name.", + "$ref": "#/$defs/string" + }, + "table_name_prefix": { + "description": "The prefix of the table in Unity Catalog. NOTE: On update, you have to disable inference table first in order to change the prefix name.", + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "serving.AiGatewayRateLimit": { + "anyOf": [ + { + "type": "object", + "properties": { + "calls": { + "description": "Used to specify how many calls are allowed for a key within the renewal_period.", + "$ref": "#/$defs/int" + }, + "key": { + "description": "Key field for a rate limit. Currently, only 'user' and 'endpoint' are supported, with 'endpoint' being the default if not specified.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/serving.AiGatewayRateLimitKey", + "enum": [ + "user", + "endpoint" + ] + }, + "renewal_period": { + "description": "Renewal period field for a rate limit. Currently, only 'minute' is supported.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/serving.AiGatewayRateLimitRenewalPeriod", + "enum": [ + "minute" + ] + } + }, + "additionalProperties": false, + "required": [ + "calls", + "renewal_period" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "serving.AiGatewayRateLimitKey": { + "type": "string" + }, + "serving.AiGatewayRateLimitRenewalPeriod": { + "type": "string" + }, + "serving.AiGatewayUsageTrackingConfig": { + "anyOf": [ + { + "type": "object", + "properties": { + "enabled": { + "description": "Whether to enable usage tracking.", + "$ref": "#/$defs/bool" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "serving.AmazonBedrockConfig": { "anyOf": [ { @@ -5569,6 +5786,20 @@ } ] }, + "serving.AiGatewayRateLimit": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/serving.AiGatewayRateLimit" + } + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.[a-zA-Z]+([-_]?[a-zA-Z0-9]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "serving.EndpointTag": { "anyOf": [ { diff --git a/cmd/workspace/apps/apps.go b/cmd/workspace/apps/apps.go index baec6d03..780f5594 100755 --- a/cmd/workspace/apps/apps.go +++ b/cmd/workspace/apps/apps.go @@ -81,6 +81,7 @@ func newCreate() *cobra.Command { cmd.Flags().Var(&createJson, "json", `either inline JSON string or @path/to/file.json with request body`) cmd.Flags().StringVar(&createReq.Description, "description", createReq.Description, `The description of the app.`) + // TODO: array: resources cmd.Use = "create NAME" cmd.Short = `Create an app.` @@ -910,6 +911,7 @@ func newUpdate() *cobra.Command { cmd.Flags().Var(&updateJson, "json", `either inline JSON string or @path/to/file.json with request body`) cmd.Flags().StringVar(&updateReq.Description, "description", updateReq.Description, `The description of the app.`) + // TODO: array: resources cmd.Use = "update NAME" cmd.Short = `Update an app.` diff --git a/cmd/workspace/git-credentials/git-credentials.go b/cmd/workspace/git-credentials/git-credentials.go index 2e8cc2cd..b5082d31 100755 --- a/cmd/workspace/git-credentials/git-credentials.go +++ b/cmd/workspace/git-credentials/git-credentials.go @@ -53,13 +53,13 @@ func New() *cobra.Command { // Functions can be added from the `init()` function in manually curated files in this directory. var createOverrides []func( *cobra.Command, - *workspace.CreateCredentials, + *workspace.CreateCredentialsRequest, ) func newCreate() *cobra.Command { cmd := &cobra.Command{} - var createReq workspace.CreateCredentials + var createReq workspace.CreateCredentialsRequest var createJson flags.JsonFlag // TODO: short flags @@ -79,8 +79,9 @@ func newCreate() *cobra.Command { Arguments: GIT_PROVIDER: Git provider. This field is case-insensitive. The available Git providers - are gitHub, bitbucketCloud, gitLab, azureDevOpsServices, gitHubEnterprise, - bitbucketServer, gitLabEnterpriseEdition and awsCodeCommit.` + are gitHub, bitbucketCloud, gitLab, azureDevOpsServices, + gitHubEnterprise, bitbucketServer, gitLabEnterpriseEdition and + awsCodeCommit.` cmd.Annotations = make(map[string]string) @@ -136,13 +137,13 @@ func newCreate() *cobra.Command { // Functions can be added from the `init()` function in manually curated files in this directory. var deleteOverrides []func( *cobra.Command, - *workspace.DeleteGitCredentialRequest, + *workspace.DeleteCredentialsRequest, ) func newDelete() *cobra.Command { cmd := &cobra.Command{} - var deleteReq workspace.DeleteGitCredentialRequest + var deleteReq workspace.DeleteCredentialsRequest // TODO: short flags @@ -209,13 +210,13 @@ func newDelete() *cobra.Command { // Functions can be added from the `init()` function in manually curated files in this directory. var getOverrides []func( *cobra.Command, - *workspace.GetGitCredentialRequest, + *workspace.GetCredentialsRequest, ) func newGet() *cobra.Command { cmd := &cobra.Command{} - var getReq workspace.GetGitCredentialRequest + var getReq workspace.GetCredentialsRequest // TODO: short flags @@ -322,33 +323,48 @@ func newList() *cobra.Command { // Functions can be added from the `init()` function in manually curated files in this directory. var updateOverrides []func( *cobra.Command, - *workspace.UpdateCredentials, + *workspace.UpdateCredentialsRequest, ) func newUpdate() *cobra.Command { cmd := &cobra.Command{} - var updateReq workspace.UpdateCredentials + var updateReq workspace.UpdateCredentialsRequest var updateJson flags.JsonFlag // TODO: short flags cmd.Flags().Var(&updateJson, "json", `either inline JSON string or @path/to/file.json with request body`) - cmd.Flags().StringVar(&updateReq.GitProvider, "git-provider", updateReq.GitProvider, `Git provider.`) cmd.Flags().StringVar(&updateReq.GitUsername, "git-username", updateReq.GitUsername, `The username or email provided with your Git provider account, depending on which provider you are using.`) cmd.Flags().StringVar(&updateReq.PersonalAccessToken, "personal-access-token", updateReq.PersonalAccessToken, `The personal access token used to authenticate to the corresponding Git provider.`) - cmd.Use = "update CREDENTIAL_ID" + cmd.Use = "update CREDENTIAL_ID GIT_PROVIDER" cmd.Short = `Update a credential.` cmd.Long = `Update a credential. Updates the specified Git credential. Arguments: - CREDENTIAL_ID: The ID for the corresponding credential to access.` + CREDENTIAL_ID: The ID for the corresponding credential to access. + GIT_PROVIDER: Git provider. This field is case-insensitive. The available Git providers + are gitHub, bitbucketCloud, gitLab, azureDevOpsServices, + gitHubEnterprise, bitbucketServer, gitLabEnterpriseEdition and + awsCodeCommit.` cmd.Annotations = make(map[string]string) + cmd.Args = func(cmd *cobra.Command, args []string) error { + if cmd.Flags().Changed("json") { + err := root.ExactArgs(1)(cmd, args) + if err != nil { + return fmt.Errorf("when --json flag is specified, provide only CREDENTIAL_ID as positional arguments. Provide 'git_provider' in your JSON input") + } + return nil + } + check := root.ExactArgs(2) + return check(cmd, args) + } + cmd.PreRunE = root.MustWorkspaceClient cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { ctx := cmd.Context() @@ -360,27 +376,13 @@ func newUpdate() *cobra.Command { return err } } - if len(args) == 0 { - promptSpinner := cmdio.Spinner(ctx) - promptSpinner <- "No CREDENTIAL_ID argument specified. Loading names for Git Credentials drop-down." - names, err := w.GitCredentials.CredentialInfoGitProviderToCredentialIdMap(ctx) - close(promptSpinner) - if err != nil { - return fmt.Errorf("failed to load names for Git Credentials drop-down. Please manually specify required arguments. Original error: %w", err) - } - id, err := cmdio.Select(ctx, names, "The ID for the corresponding credential to access") - if err != nil { - return err - } - args = append(args, id) - } - if len(args) != 1 { - return fmt.Errorf("expected to have the id for the corresponding credential to access") - } _, err = fmt.Sscan(args[0], &updateReq.CredentialId) if err != nil { return fmt.Errorf("invalid CREDENTIAL_ID: %s", args[0]) } + if !cmd.Flags().Changed("json") { + updateReq.GitProvider = args[1] + } err = w.GitCredentials.Update(ctx, updateReq) if err != nil { diff --git a/cmd/workspace/pipelines/pipelines.go b/cmd/workspace/pipelines/pipelines.go index 5b4d9645..ac361e31 100755 --- a/cmd/workspace/pipelines/pipelines.go +++ b/cmd/workspace/pipelines/pipelines.go @@ -954,6 +954,7 @@ func newUpdate() *cobra.Command { // TODO: array: notifications cmd.Flags().BoolVar(&updateReq.Photon, "photon", updateReq.Photon, `Whether Photon is enabled for this pipeline.`) cmd.Flags().StringVar(&updateReq.PipelineId, "pipeline-id", updateReq.PipelineId, `Unique identifier for this pipeline.`) + cmd.Flags().StringVar(&updateReq.Schema, "schema", updateReq.Schema, `The default schema (database) where tables are read from or published to.`) cmd.Flags().BoolVar(&updateReq.Serverless, "serverless", updateReq.Serverless, `Whether serverless compute is enabled for this pipeline.`) cmd.Flags().StringVar(&updateReq.Storage, "storage", updateReq.Storage, `DBFS root directory for storing checkpoints and tables.`) cmd.Flags().StringVar(&updateReq.Target, "target", updateReq.Target, `Target schema (database) to add tables in this pipeline to.`) diff --git a/cmd/workspace/repos/overrides.go b/cmd/workspace/repos/overrides.go index 96d645ef..9546d1c1 100644 --- a/cmd/workspace/repos/overrides.go +++ b/cmd/workspace/repos/overrides.go @@ -19,7 +19,7 @@ func listOverride(listCmd *cobra.Command, listReq *workspace.ListReposRequest) { {{end}}`) } -func createOverride(createCmd *cobra.Command, createReq *workspace.CreateRepo) { +func createOverride(createCmd *cobra.Command, createReq *workspace.CreateRepoRequest) { createCmd.Use = "create URL [PROVIDER]" createCmd.Args = func(cmd *cobra.Command, args []string) error { // If the provider argument is not specified, we try to detect it from the URL. @@ -95,7 +95,7 @@ func getOverride(getCmd *cobra.Command, getReq *workspace.GetRepoRequest) { } } -func updateOverride(updateCmd *cobra.Command, updateReq *workspace.UpdateRepo) { +func updateOverride(updateCmd *cobra.Command, updateReq *workspace.UpdateRepoRequest) { updateCmd.Use = "update REPO_ID_OR_PATH" updateJson := updateCmd.Flag("json").Value.(*flags.JsonFlag) diff --git a/cmd/workspace/repos/repos.go b/cmd/workspace/repos/repos.go index fb3d51b0..f11dd3ac 100755 --- a/cmd/workspace/repos/repos.go +++ b/cmd/workspace/repos/repos.go @@ -61,13 +61,13 @@ func New() *cobra.Command { // Functions can be added from the `init()` function in manually curated files in this directory. var createOverrides []func( *cobra.Command, - *workspace.CreateRepo, + *workspace.CreateRepoRequest, ) func newCreate() *cobra.Command { cmd := &cobra.Command{} - var createReq workspace.CreateRepo + var createReq workspace.CreateRepoRequest var createJson flags.JsonFlag // TODO: short flags @@ -87,8 +87,9 @@ func newCreate() *cobra.Command { Arguments: URL: URL of the Git repository to be linked. PROVIDER: Git provider. This field is case-insensitive. The available Git providers - are gitHub, bitbucketCloud, gitLab, azureDevOpsServices, gitHubEnterprise, - bitbucketServer, gitLabEnterpriseEdition and awsCodeCommit.` + are gitHub, bitbucketCloud, gitLab, azureDevOpsServices, + gitHubEnterprise, bitbucketServer, gitLabEnterpriseEdition and + awsCodeCommit.` cmd.Annotations = make(map[string]string) @@ -164,7 +165,7 @@ func newDelete() *cobra.Command { Deletes the specified repo. Arguments: - REPO_ID: The ID for the corresponding repo to access.` + REPO_ID: ID of the Git folder (repo) object in the workspace.` cmd.Annotations = make(map[string]string) @@ -181,14 +182,14 @@ func newDelete() *cobra.Command { if err != nil { return fmt.Errorf("failed to load names for Repos drop-down. Please manually specify required arguments. Original error: %w", err) } - id, err := cmdio.Select(ctx, names, "The ID for the corresponding repo to access") + id, err := cmdio.Select(ctx, names, "ID of the Git folder (repo) object in the workspace") if err != nil { return err } args = append(args, id) } if len(args) != 1 { - return fmt.Errorf("expected to have the id for the corresponding repo to access") + return fmt.Errorf("expected to have id of the git folder (repo) object in the workspace") } _, err = fmt.Sscan(args[0], &deleteReq.RepoId) if err != nil { @@ -237,7 +238,7 @@ func newGet() *cobra.Command { Returns the repo with the given repo ID. Arguments: - REPO_ID: The ID for the corresponding repo to access.` + REPO_ID: ID of the Git folder (repo) object in the workspace.` cmd.Annotations = make(map[string]string) @@ -254,14 +255,14 @@ func newGet() *cobra.Command { if err != nil { return fmt.Errorf("failed to load names for Repos drop-down. Please manually specify required arguments. Original error: %w", err) } - id, err := cmdio.Select(ctx, names, "The ID for the corresponding repo to access") + id, err := cmdio.Select(ctx, names, "ID of the Git folder (repo) object in the workspace") if err != nil { return err } args = append(args, id) } if len(args) != 1 { - return fmt.Errorf("expected to have the id for the corresponding repo to access") + return fmt.Errorf("expected to have id of the git folder (repo) object in the workspace") } _, err = fmt.Sscan(args[0], &getReq.RepoId) if err != nil { @@ -451,8 +452,8 @@ func newList() *cobra.Command { cmd.Short = `Get repos.` cmd.Long = `Get repos. - Returns repos that the calling user has Manage permissions on. Results are - paginated with each page containing twenty repos.` + Returns repos that the calling user has Manage permissions on. Use + next_page_token to iterate through additional pages.` cmd.Annotations = make(map[string]string) @@ -569,13 +570,13 @@ func newSetPermissions() *cobra.Command { // Functions can be added from the `init()` function in manually curated files in this directory. var updateOverrides []func( *cobra.Command, - *workspace.UpdateRepo, + *workspace.UpdateRepoRequest, ) func newUpdate() *cobra.Command { cmd := &cobra.Command{} - var updateReq workspace.UpdateRepo + var updateReq workspace.UpdateRepoRequest var updateJson flags.JsonFlag // TODO: short flags @@ -593,7 +594,7 @@ func newUpdate() *cobra.Command { latest commit on the same branch. Arguments: - REPO_ID: The ID for the corresponding repo to access.` + REPO_ID: ID of the Git folder (repo) object in the workspace.` cmd.Annotations = make(map[string]string) @@ -616,14 +617,14 @@ func newUpdate() *cobra.Command { if err != nil { return fmt.Errorf("failed to load names for Repos drop-down. Please manually specify required arguments. Original error: %w", err) } - id, err := cmdio.Select(ctx, names, "The ID for the corresponding repo to access") + id, err := cmdio.Select(ctx, names, "ID of the Git folder (repo) object in the workspace") if err != nil { return err } args = append(args, id) } if len(args) != 1 { - return fmt.Errorf("expected to have the id for the corresponding repo to access") + return fmt.Errorf("expected to have id of the git folder (repo) object in the workspace") } _, err = fmt.Sscan(args[0], &updateReq.RepoId) if err != nil { diff --git a/go.mod b/go.mod index c029e5da..697205f3 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.22.7 require ( github.com/Masterminds/semver/v3 v3.3.0 // MIT github.com/briandowns/spinner v1.23.1 // Apache 2.0 - github.com/databricks/databricks-sdk-go v0.47.0 // Apache 2.0 + github.com/databricks/databricks-sdk-go v0.48.0 // Apache 2.0 github.com/fatih/color v1.17.0 // MIT github.com/ghodss/yaml v1.0.0 // MIT + NOTICE github.com/google/uuid v1.6.0 // BSD-3-Clause diff --git a/go.sum b/go.sum index 9f05b145..03698b20 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,8 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= -github.com/databricks/databricks-sdk-go v0.47.0 h1:eE7dN9axviL8+s10jnQAayOYDaR+Mfu7E9COGjO4lrQ= -github.com/databricks/databricks-sdk-go v0.47.0/go.mod h1:ds+zbv5mlQG7nFEU5ojLtgN/u0/9YzZmKQES/CfedzU= +github.com/databricks/databricks-sdk-go v0.48.0 h1:46KtsnRo+FGhC3izUXbpL0PXBNomvsdignYDhJZlm9s= +github.com/databricks/databricks-sdk-go v0.48.0/go.mod h1:ds+zbv5mlQG7nFEU5ojLtgN/u0/9YzZmKQES/CfedzU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/helpers.go b/internal/helpers.go index 419fa419..9387706b 100644 --- a/internal/helpers.go +++ b/internal/helpers.go @@ -519,7 +519,7 @@ func TemporaryRepo(t *testing.T, w *databricks.WorkspaceClient) string { repoPath := fmt.Sprintf("/Repos/%s/%s", me.UserName, RandomName("integration-test-repo-")) t.Logf("Creating repo:%s", repoPath) - repoInfo, err := w.Repos.Create(ctx, workspace.CreateRepo{ + repoInfo, err := w.Repos.Create(ctx, workspace.CreateRepoRequest{ Url: "https://github.com/databricks/cli", Provider: "github", Path: repoPath, diff --git a/internal/locker_test.go b/internal/locker_test.go index 21e08f73..3ae783d1 100644 --- a/internal/locker_test.go +++ b/internal/locker_test.go @@ -29,7 +29,7 @@ func createRemoteTestProject(t *testing.T, projectNamePrefix string, wsc *databr assert.NoError(t, err) remoteProjectRoot := fmt.Sprintf("/Repos/%s/%s", me.UserName, RandomName(projectNamePrefix)) - repoInfo, err := wsc.Repos.Create(ctx, workspace.CreateRepo{ + repoInfo, err := wsc.Repos.Create(ctx, workspace.CreateRepoRequest{ Path: remoteProjectRoot, Url: EmptyRepoUrl, Provider: "gitHub", diff --git a/internal/repos_test.go b/internal/repos_test.go index de0d926a..1ad0e877 100644 --- a/internal/repos_test.go +++ b/internal/repos_test.go @@ -34,7 +34,7 @@ func synthesizeTemporaryRepoPath(t *testing.T, w *databricks.WorkspaceClient, ct func createTemporaryRepo(t *testing.T, w *databricks.WorkspaceClient, ctx context.Context) (int64, string) { repoPath := synthesizeTemporaryRepoPath(t, w, ctx) - repoInfo, err := w.Repos.Create(ctx, workspace.CreateRepo{ + repoInfo, err := w.Repos.Create(ctx, workspace.CreateRepoRequest{ Path: repoPath, Url: repoUrl, Provider: "gitHub", diff --git a/internal/sync_test.go b/internal/sync_test.go index 4021e649..6f8b1827 100644 --- a/internal/sync_test.go +++ b/internal/sync_test.go @@ -38,7 +38,7 @@ func setupRepo(t *testing.T, wsc *databricks.WorkspaceClient, ctx context.Contex require.NoError(t, err) repoPath := fmt.Sprintf("/Repos/%s/%s", me.UserName, RandomName("empty-repo-sync-integration-")) - repoInfo, err := wsc.Repos.Create(ctx, workspace.CreateRepo{ + repoInfo, err := wsc.Repos.Create(ctx, workspace.CreateRepoRequest{ Path: repoPath, Url: repoUrl, Provider: "gitHub", From 88318d384acd29ed58143e0bfbbb0c382fa785c8 Mon Sep 17 00:00:00 2001 From: shreyas-goenka <88374338+shreyas-goenka@users.noreply.github.com> Date: Mon, 7 Oct 2024 20:43:42 +0530 Subject: [PATCH 10/24] Remove deprecated or readonly fields from the bundle schema (#1809) ## Changes We do not need the user to specify these fields in their bundle configuration, so we remove them from the JSON schema. ## Tests Manually and end to end tests. The JSON schema has also been regenerated after these changes. --- bundle/internal/schema/main.go | 28 +++++++++++++++++++ .../fail/deprecated_job_field_format.yml | 4 +++ .../fail/hidden_job_field_deployment.yml | 6 ++++ .../fail/hidden_job_field_edit_mode.yml | 6 ++++ .../fail/readonly_job_field_git_snapshot.yml | 8 ++++++ .../fail/readonly_job_field_job_source.yml | 9 ++++++ bundle/internal/schema/testdata/pass/job.yml | 5 ++-- bundle/schema/embed_test.go | 4 +-- bundle/schema/jsonschema.json | 19 ------------- 9 files changed, 66 insertions(+), 23 deletions(-) create mode 100644 bundle/internal/schema/testdata/fail/deprecated_job_field_format.yml create mode 100644 bundle/internal/schema/testdata/fail/hidden_job_field_deployment.yml create mode 100644 bundle/internal/schema/testdata/fail/hidden_job_field_edit_mode.yml create mode 100644 bundle/internal/schema/testdata/fail/readonly_job_field_git_snapshot.yml create mode 100644 bundle/internal/schema/testdata/fail/readonly_job_field_job_source.yml diff --git a/bundle/internal/schema/main.go b/bundle/internal/schema/main.go index 4a237147..ddeffe2f 100644 --- a/bundle/internal/schema/main.go +++ b/bundle/internal/schema/main.go @@ -8,8 +8,10 @@ import ( "reflect" "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/config/resources" "github.com/databricks/cli/bundle/config/variable" "github.com/databricks/cli/libs/jsonschema" + "github.com/databricks/databricks-sdk-go/service/jobs" ) func interpolationPattern(s string) string { @@ -66,6 +68,31 @@ func addInterpolationPatterns(typ reflect.Type, s jsonschema.Schema) jsonschema. } } +func removeJobsFields(typ reflect.Type, s jsonschema.Schema) jsonschema.Schema { + switch typ { + case reflect.TypeOf(resources.Job{}): + // This field has been deprecated in jobs API v2.1 and is always set to + // "MULTI_TASK" in the backend. We should not expose it to the user. + delete(s.Properties, "format") + + // These fields are only meant to be set by the DABs client (ie the CLI) + // and thus should not be exposed to the user. These are used to annotate + // jobs that were created by DABs. + delete(s.Properties, "deployment") + delete(s.Properties, "edit_mode") + + case reflect.TypeOf(jobs.GitSource{}): + // These fields are readonly and are not meant to be set by the user. + delete(s.Properties, "job_source") + delete(s.Properties, "git_snapshot") + + default: + // Do nothing + } + + return s +} + func main() { if len(os.Args) != 2 { fmt.Println("Usage: go run main.go ") @@ -90,6 +117,7 @@ func main() { s, err := jsonschema.FromType(reflect.TypeOf(config.Root{}), []func(reflect.Type, jsonschema.Schema) jsonschema.Schema{ p.addDescriptions, p.addEnums, + removeJobsFields, addInterpolationPatterns, }) if err != nil { diff --git a/bundle/internal/schema/testdata/fail/deprecated_job_field_format.yml b/bundle/internal/schema/testdata/fail/deprecated_job_field_format.yml new file mode 100644 index 00000000..62e490b0 --- /dev/null +++ b/bundle/internal/schema/testdata/fail/deprecated_job_field_format.yml @@ -0,0 +1,4 @@ +resources: + jobs: + foo: + format: SINGLE_TASK diff --git a/bundle/internal/schema/testdata/fail/hidden_job_field_deployment.yml b/bundle/internal/schema/testdata/fail/hidden_job_field_deployment.yml new file mode 100644 index 00000000..705ce951 --- /dev/null +++ b/bundle/internal/schema/testdata/fail/hidden_job_field_deployment.yml @@ -0,0 +1,6 @@ +resources: + jobs: + foo: + deployment: + kind: BUNDLE + metadata_file_path: /a/b/c diff --git a/bundle/internal/schema/testdata/fail/hidden_job_field_edit_mode.yml b/bundle/internal/schema/testdata/fail/hidden_job_field_edit_mode.yml new file mode 100644 index 00000000..9cbe95f0 --- /dev/null +++ b/bundle/internal/schema/testdata/fail/hidden_job_field_edit_mode.yml @@ -0,0 +1,6 @@ +targets: + foo: + resources: + jobs: + bar: + edit_mode: whatever diff --git a/bundle/internal/schema/testdata/fail/readonly_job_field_git_snapshot.yml b/bundle/internal/schema/testdata/fail/readonly_job_field_git_snapshot.yml new file mode 100644 index 00000000..c57a560a --- /dev/null +++ b/bundle/internal/schema/testdata/fail/readonly_job_field_git_snapshot.yml @@ -0,0 +1,8 @@ +resources: + jobs: + foo: + git_source: + git_provider: GITHUB + git_url: www.whatever.com + git_snapshot: + used_commit: abcdef diff --git a/bundle/internal/schema/testdata/fail/readonly_job_field_job_source.yml b/bundle/internal/schema/testdata/fail/readonly_job_field_job_source.yml new file mode 100644 index 00000000..9973e3bd --- /dev/null +++ b/bundle/internal/schema/testdata/fail/readonly_job_field_job_source.yml @@ -0,0 +1,9 @@ +resources: + jobs: + foo: + git_source: + git_provider: GITHUB + git_url: www.whatever.com + job_source: + import_from_git_branch: master + job_config_path: def diff --git a/bundle/internal/schema/testdata/pass/job.yml b/bundle/internal/schema/testdata/pass/job.yml index d9b0e832..e13a52c0 100644 --- a/bundle/internal/schema/testdata/pass/job.yml +++ b/bundle/internal/schema/testdata/pass/job.yml @@ -32,7 +32,6 @@ resources: name: myjob continuous: pause_status: PAUSED - edit_mode: EDITABLE max_concurrent_runs: 10 description: "my job description" email_notifications: @@ -43,10 +42,12 @@ resources: dependencies: - python=3.7 client: "myclient" - format: MULTI_TASK tags: foo: bar bar: baz + git_source: + git_provider: gitHub + git_url: www.github.com/a/b tasks: - task_key: mytask notebook_task: diff --git a/bundle/schema/embed_test.go b/bundle/schema/embed_test.go index ee0b5a61..dcb381b8 100644 --- a/bundle/schema/embed_test.go +++ b/bundle/schema/embed_test.go @@ -39,7 +39,7 @@ func TestJsonSchema(t *testing.T) { // Assert job fields have their descriptions loaded. resourceJob := walk(s.Definitions, "github.com", "databricks", "cli", "bundle", "config", "resources.Job") - fields := []string{"name", "continuous", "deployment", "tasks", "trigger"} + fields := []string{"name", "continuous", "tasks", "trigger"} for _, field := range fields { assert.NotEmpty(t, resourceJob.AnyOf[0].Properties[field].Description) } @@ -53,7 +53,7 @@ func TestJsonSchema(t *testing.T) { // Assert descriptions are loaded for pipelines pipeline := walk(s.Definitions, "github.com", "databricks", "cli", "bundle", "config", "resources.Pipeline") - fields = []string{"name", "catalog", "clusters", "channel", "continuous", "deployment", "development"} + fields = []string{"name", "catalog", "clusters", "channel", "continuous", "development"} for _, field := range fields { assert.NotEmpty(t, pipeline.AnyOf[0].Properties[field].Description) } diff --git a/bundle/schema/jsonschema.json b/bundle/schema/jsonschema.json index ae209c01..06b9cc15 100644 --- a/bundle/schema/jsonschema.json +++ b/bundle/schema/jsonschema.json @@ -213,18 +213,10 @@ "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.", "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/jobs.Continuous" }, - "deployment": { - "description": "Deployment information for jobs managed by external sources.", - "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/jobs.JobDeployment" - }, "description": { "description": "An optional description for the job. The maximum length is 27700 characters in UTF-8 encoding.", "$ref": "#/$defs/string" }, - "edit_mode": { - "description": "Edit mode of the job.\n\n* `UI_LOCKED`: The job is in a locked UI state and cannot be modified.\n* `EDITABLE`: The job is in an editable state and can be modified.", - "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/jobs.JobEditMode" - }, "email_notifications": { "description": "An optional set of email addresses that is notified when runs of this job begin or complete as well as when this job is deleted.", "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/jobs.JobEmailNotifications" @@ -233,10 +225,6 @@ "description": "A list of task execution environment specifications that can be referenced by serverless tasks of this job.\nAn environment is required to be present for serverless tasks.\nFor serverless notebook tasks, the environment is accessible in the notebook environment panel.\nFor other serverless tasks, the task environment is required to be specified using environment_key in the task settings.", "$ref": "#/$defs/slice/github.com/databricks/databricks-sdk-go/service/jobs.JobEnvironment" }, - "format": { - "description": "Used to tell what is the format of the job. This field is ignored in Create/Update/Reset calls. When using the Jobs API 2.1 this value is always set to `\"MULTI_TASK\"`.", - "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/jobs.Format" - }, "git_source": { "description": "An optional specification for a remote Git repository containing the source code used by tasks. Version-controlled source code is supported by notebook, dbt, Python script, and SQL File tasks.\n\nIf `git_source` is set, these tasks retrieve the file from the remote repository by default. However, this behavior can be overridden by setting `source` to `WORKSPACE` on the task.\n\nNote: dbt and SQL File tasks support only version-controlled sources. If dbt or SQL File tasks are used, `git_source` must be defined on the job.", "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/jobs.GitSource" @@ -2547,9 +2535,6 @@ "description": "Unique identifier of the service used to host the Git repository. The value is case insensitive.", "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/jobs.GitProvider" }, - "git_snapshot": { - "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/jobs.GitSnapshot" - }, "git_tag": { "description": "Name of the tag to be checked out and used by this job. This field cannot be specified in conjunction with git_branch or git_commit.", "$ref": "#/$defs/string" @@ -2557,10 +2542,6 @@ "git_url": { "description": "URL of the repository to be cloned by this job.", "$ref": "#/$defs/string" - }, - "job_source": { - "description": "The source of the job specification in the remote repository when the job is source controlled.", - "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/jobs.JobSource" } }, "additionalProperties": false, From ae568743c5ccbfb8ae9d3298c7aeee96d0dcf1b4 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Tue, 8 Oct 2024 13:41:32 +0200 Subject: [PATCH 11/24] Upgrade TF provider to 1.53.0 (#1815) ## Changes See https://github.com/databricks/terraform-provider-databricks/releases/tag/v1.53.0 ## Tests Integration tests pass. --- bundle/internal/tf/codegen/schema/version.go | 2 +- .../schema/data_source_current_metastore.go | 1 + .../tf/schema/data_source_metastore.go | 1 + .../tf/schema/data_source_mlflow_models.go | 8 +++ bundle/internal/tf/schema/data_sources.go | 2 + bundle/internal/tf/schema/resource_budget.go | 49 +++++++++++++++++ .../tf/schema/resource_model_serving.go | 52 +++++++++++++++++++ .../tf/schema/resource_permissions.go | 2 +- .../internal/tf/schema/resource_pipeline.go | 2 + .../internal/tf/schema/resource_sql_table.go | 2 + bundle/internal/tf/schema/resources.go | 2 + bundle/internal/tf/schema/root.go | 2 +- 12 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 bundle/internal/tf/schema/data_source_mlflow_models.go create mode 100644 bundle/internal/tf/schema/resource_budget.go diff --git a/bundle/internal/tf/codegen/schema/version.go b/bundle/internal/tf/codegen/schema/version.go index b71ea7d1..49e48a6e 100644 --- a/bundle/internal/tf/codegen/schema/version.go +++ b/bundle/internal/tf/codegen/schema/version.go @@ -1,3 +1,3 @@ package schema -const ProviderVersion = "1.52.0" +const ProviderVersion = "1.53.0" diff --git a/bundle/internal/tf/schema/data_source_current_metastore.go b/bundle/internal/tf/schema/data_source_current_metastore.go index 11e647fd..4f8c135a 100644 --- a/bundle/internal/tf/schema/data_source_current_metastore.go +++ b/bundle/internal/tf/schema/data_source_current_metastore.go @@ -10,6 +10,7 @@ type DataSourceCurrentMetastoreMetastoreInfo struct { DeltaSharingOrganizationName string `json:"delta_sharing_organization_name,omitempty"` DeltaSharingRecipientTokenLifetimeInSeconds int `json:"delta_sharing_recipient_token_lifetime_in_seconds,omitempty"` DeltaSharingScope string `json:"delta_sharing_scope,omitempty"` + ExternalAccessEnabled bool `json:"external_access_enabled,omitempty"` GlobalMetastoreId string `json:"global_metastore_id,omitempty"` MetastoreId string `json:"metastore_id,omitempty"` Name string `json:"name,omitempty"` diff --git a/bundle/internal/tf/schema/data_source_metastore.go b/bundle/internal/tf/schema/data_source_metastore.go index ce206479..4244febc 100644 --- a/bundle/internal/tf/schema/data_source_metastore.go +++ b/bundle/internal/tf/schema/data_source_metastore.go @@ -10,6 +10,7 @@ type DataSourceMetastoreMetastoreInfo struct { DeltaSharingOrganizationName string `json:"delta_sharing_organization_name,omitempty"` DeltaSharingRecipientTokenLifetimeInSeconds int `json:"delta_sharing_recipient_token_lifetime_in_seconds,omitempty"` DeltaSharingScope string `json:"delta_sharing_scope,omitempty"` + ExternalAccessEnabled bool `json:"external_access_enabled,omitempty"` GlobalMetastoreId string `json:"global_metastore_id,omitempty"` MetastoreId string `json:"metastore_id,omitempty"` Name string `json:"name,omitempty"` diff --git a/bundle/internal/tf/schema/data_source_mlflow_models.go b/bundle/internal/tf/schema/data_source_mlflow_models.go new file mode 100644 index 00000000..360924e5 --- /dev/null +++ b/bundle/internal/tf/schema/data_source_mlflow_models.go @@ -0,0 +1,8 @@ +// Generated from Databricks Terraform provider schema. DO NOT EDIT. + +package schema + +type DataSourceMlflowModels struct { + Id string `json:"id,omitempty"` + Names []string `json:"names,omitempty"` +} diff --git a/bundle/internal/tf/schema/data_sources.go b/bundle/internal/tf/schema/data_sources.go index 4ac78613..10829b99 100644 --- a/bundle/internal/tf/schema/data_sources.go +++ b/bundle/internal/tf/schema/data_sources.go @@ -30,6 +30,7 @@ type DataSources struct { Metastores map[string]any `json:"databricks_metastores,omitempty"` MlflowExperiment map[string]any `json:"databricks_mlflow_experiment,omitempty"` MlflowModel map[string]any `json:"databricks_mlflow_model,omitempty"` + MlflowModels map[string]any `json:"databricks_mlflow_models,omitempty"` MwsCredentials map[string]any `json:"databricks_mws_credentials,omitempty"` MwsWorkspaces map[string]any `json:"databricks_mws_workspaces,omitempty"` NodeType map[string]any `json:"databricks_node_type,omitempty"` @@ -85,6 +86,7 @@ func NewDataSources() *DataSources { Metastores: make(map[string]any), MlflowExperiment: make(map[string]any), MlflowModel: make(map[string]any), + MlflowModels: make(map[string]any), MwsCredentials: make(map[string]any), MwsWorkspaces: make(map[string]any), NodeType: make(map[string]any), diff --git a/bundle/internal/tf/schema/resource_budget.go b/bundle/internal/tf/schema/resource_budget.go new file mode 100644 index 00000000..5566eb93 --- /dev/null +++ b/bundle/internal/tf/schema/resource_budget.go @@ -0,0 +1,49 @@ +// Generated from Databricks Terraform provider schema. DO NOT EDIT. + +package schema + +type ResourceBudgetAlertConfigurationsActionConfigurations struct { + ActionConfigurationId string `json:"action_configuration_id,omitempty"` + ActionType string `json:"action_type,omitempty"` + Target string `json:"target,omitempty"` +} + +type ResourceBudgetAlertConfigurations struct { + AlertConfigurationId string `json:"alert_configuration_id,omitempty"` + QuantityThreshold string `json:"quantity_threshold,omitempty"` + QuantityType string `json:"quantity_type,omitempty"` + TimePeriod string `json:"time_period,omitempty"` + TriggerType string `json:"trigger_type,omitempty"` + ActionConfigurations []ResourceBudgetAlertConfigurationsActionConfigurations `json:"action_configurations,omitempty"` +} + +type ResourceBudgetFilterTagsValue struct { + Operator string `json:"operator,omitempty"` + Values []string `json:"values,omitempty"` +} + +type ResourceBudgetFilterTags struct { + Key string `json:"key,omitempty"` + Value *ResourceBudgetFilterTagsValue `json:"value,omitempty"` +} + +type ResourceBudgetFilterWorkspaceId struct { + Operator string `json:"operator,omitempty"` + Values []int `json:"values,omitempty"` +} + +type ResourceBudgetFilter struct { + Tags []ResourceBudgetFilterTags `json:"tags,omitempty"` + WorkspaceId *ResourceBudgetFilterWorkspaceId `json:"workspace_id,omitempty"` +} + +type ResourceBudget struct { + AccountId string `json:"account_id,omitempty"` + BudgetConfigurationId string `json:"budget_configuration_id,omitempty"` + CreateTime int `json:"create_time,omitempty"` + DisplayName string `json:"display_name,omitempty"` + Id string `json:"id,omitempty"` + UpdateTime int `json:"update_time,omitempty"` + AlertConfigurations []ResourceBudgetAlertConfigurations `json:"alert_configurations,omitempty"` + Filter *ResourceBudgetFilter `json:"filter,omitempty"` +} diff --git a/bundle/internal/tf/schema/resource_model_serving.go b/bundle/internal/tf/schema/resource_model_serving.go index 29d55cd5..71cf8925 100644 --- a/bundle/internal/tf/schema/resource_model_serving.go +++ b/bundle/internal/tf/schema/resource_model_serving.go @@ -2,6 +2,57 @@ package schema +type ResourceModelServingAiGatewayGuardrailsInputPii struct { + Behavior string `json:"behavior"` +} + +type ResourceModelServingAiGatewayGuardrailsInput struct { + InvalidKeywords []string `json:"invalid_keywords,omitempty"` + Safety bool `json:"safety,omitempty"` + ValidTopics []string `json:"valid_topics,omitempty"` + Pii *ResourceModelServingAiGatewayGuardrailsInputPii `json:"pii,omitempty"` +} + +type ResourceModelServingAiGatewayGuardrailsOutputPii struct { + Behavior string `json:"behavior"` +} + +type ResourceModelServingAiGatewayGuardrailsOutput struct { + InvalidKeywords []string `json:"invalid_keywords,omitempty"` + Safety bool `json:"safety,omitempty"` + ValidTopics []string `json:"valid_topics,omitempty"` + Pii *ResourceModelServingAiGatewayGuardrailsOutputPii `json:"pii,omitempty"` +} + +type ResourceModelServingAiGatewayGuardrails struct { + Input *ResourceModelServingAiGatewayGuardrailsInput `json:"input,omitempty"` + Output *ResourceModelServingAiGatewayGuardrailsOutput `json:"output,omitempty"` +} + +type ResourceModelServingAiGatewayInferenceTableConfig struct { + CatalogName string `json:"catalog_name,omitempty"` + Enabled bool `json:"enabled,omitempty"` + SchemaName string `json:"schema_name,omitempty"` + TableNamePrefix string `json:"table_name_prefix,omitempty"` +} + +type ResourceModelServingAiGatewayRateLimits struct { + Calls int `json:"calls"` + Key string `json:"key,omitempty"` + RenewalPeriod string `json:"renewal_period"` +} + +type ResourceModelServingAiGatewayUsageTrackingConfig struct { + Enabled bool `json:"enabled,omitempty"` +} + +type ResourceModelServingAiGateway struct { + Guardrails *ResourceModelServingAiGatewayGuardrails `json:"guardrails,omitempty"` + InferenceTableConfig *ResourceModelServingAiGatewayInferenceTableConfig `json:"inference_table_config,omitempty"` + RateLimits []ResourceModelServingAiGatewayRateLimits `json:"rate_limits,omitempty"` + UsageTrackingConfig *ResourceModelServingAiGatewayUsageTrackingConfig `json:"usage_tracking_config,omitempty"` +} + type ResourceModelServingConfigAutoCaptureConfig struct { CatalogName string `json:"catalog_name,omitempty"` Enabled bool `json:"enabled,omitempty"` @@ -139,6 +190,7 @@ type ResourceModelServing struct { Name string `json:"name"` RouteOptimized bool `json:"route_optimized,omitempty"` ServingEndpointId string `json:"serving_endpoint_id,omitempty"` + AiGateway *ResourceModelServingAiGateway `json:"ai_gateway,omitempty"` Config *ResourceModelServingConfig `json:"config,omitempty"` RateLimits []ResourceModelServingRateLimits `json:"rate_limits,omitempty"` Tags []ResourceModelServingTags `json:"tags,omitempty"` diff --git a/bundle/internal/tf/schema/resource_permissions.go b/bundle/internal/tf/schema/resource_permissions.go index ee94a1a8..0c3b90ed 100644 --- a/bundle/internal/tf/schema/resource_permissions.go +++ b/bundle/internal/tf/schema/resource_permissions.go @@ -4,7 +4,7 @@ package schema type ResourcePermissionsAccessControl struct { GroupName string `json:"group_name,omitempty"` - PermissionLevel string `json:"permission_level"` + PermissionLevel string `json:"permission_level,omitempty"` ServicePrincipalName string `json:"service_principal_name,omitempty"` UserName string `json:"user_name,omitempty"` } diff --git a/bundle/internal/tf/schema/resource_pipeline.go b/bundle/internal/tf/schema/resource_pipeline.go index 15468646..1bed91fc 100644 --- a/bundle/internal/tf/schema/resource_pipeline.go +++ b/bundle/internal/tf/schema/resource_pipeline.go @@ -238,6 +238,7 @@ type ResourcePipelineTrigger struct { type ResourcePipeline struct { AllowDuplicateNames bool `json:"allow_duplicate_names,omitempty"` + BudgetPolicyId string `json:"budget_policy_id,omitempty"` Catalog string `json:"catalog,omitempty"` Cause string `json:"cause,omitempty"` Channel string `json:"channel,omitempty"` @@ -254,6 +255,7 @@ type ResourcePipeline struct { Name string `json:"name,omitempty"` Photon bool `json:"photon,omitempty"` RunAsUserName string `json:"run_as_user_name,omitempty"` + Schema string `json:"schema,omitempty"` Serverless bool `json:"serverless,omitempty"` State string `json:"state,omitempty"` Storage string `json:"storage,omitempty"` diff --git a/bundle/internal/tf/schema/resource_sql_table.go b/bundle/internal/tf/schema/resource_sql_table.go index 4f305c52..bcf2a8e8 100644 --- a/bundle/internal/tf/schema/resource_sql_table.go +++ b/bundle/internal/tf/schema/resource_sql_table.go @@ -4,9 +4,11 @@ package schema type ResourceSqlTableColumn struct { Comment string `json:"comment,omitempty"` + Identity string `json:"identity,omitempty"` Name string `json:"name"` Nullable bool `json:"nullable,omitempty"` Type string `json:"type,omitempty"` + TypeJson string `json:"type_json,omitempty"` } type ResourceSqlTable struct { diff --git a/bundle/internal/tf/schema/resources.go b/bundle/internal/tf/schema/resources.go index 737b77a2..53f558df 100644 --- a/bundle/internal/tf/schema/resources.go +++ b/bundle/internal/tf/schema/resources.go @@ -10,6 +10,7 @@ type Resources struct { AzureAdlsGen1Mount map[string]any `json:"databricks_azure_adls_gen1_mount,omitempty"` AzureAdlsGen2Mount map[string]any `json:"databricks_azure_adls_gen2_mount,omitempty"` AzureBlobMount map[string]any `json:"databricks_azure_blob_mount,omitempty"` + Budget map[string]any `json:"databricks_budget,omitempty"` Catalog map[string]any `json:"databricks_catalog,omitempty"` CatalogWorkspaceBinding map[string]any `json:"databricks_catalog_workspace_binding,omitempty"` Cluster map[string]any `json:"databricks_cluster,omitempty"` @@ -112,6 +113,7 @@ func NewResources() *Resources { AzureAdlsGen1Mount: make(map[string]any), AzureAdlsGen2Mount: make(map[string]any), AzureBlobMount: make(map[string]any), + Budget: make(map[string]any), Catalog: make(map[string]any), CatalogWorkspaceBinding: make(map[string]any), Cluster: make(map[string]any), diff --git a/bundle/internal/tf/schema/root.go b/bundle/internal/tf/schema/root.go index 5fc34d6b..7a0cc01f 100644 --- a/bundle/internal/tf/schema/root.go +++ b/bundle/internal/tf/schema/root.go @@ -21,7 +21,7 @@ type Root struct { const ProviderHost = "registry.terraform.io" const ProviderSource = "databricks/databricks" -const ProviderVersion = "1.52.0" +const ProviderVersion = "1.53.0" func NewRoot() *Root { return &Root{ From c92c67afc9755e0d3ae58f85c1ea427a2253d195 Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Wed, 9 Oct 2024 15:06:57 +0200 Subject: [PATCH 12/24] [Release] Release v0.230.0 (#1817) Notable changes for Databricks Asset Bundles: Workspace paths are automatically prefixed with `/Workspace`. In addition, all usage of path strings such as `/Workspace/${workspace.root_path}/...` in bundle configuration is automatically replaced with `${workspace.root_path}/...` and generates a warning as part of bundle validate. More details can be find here: https://docs.databricks.com/en/release-notes/dev-tools/bundles.html#workspace-paths-will-be-automatically-prefixed Bundles: * Add an error if state files grow bigger than the export limit ([#1795](https://github.com/databricks/cli/pull/1795)). * Always prepend bundle remote paths with /Workspace ([#1724](https://github.com/databricks/cli/pull/1724)). * Add resource path field to bundle workspace configuration ([#1800](https://github.com/databricks/cli/pull/1800)). * Add validation for files with a `.(resource-name).yml` extension ([#1780](https://github.com/databricks/cli/pull/1780)). Internal: * Remove deprecated or readonly fields from the bundle schema ([#1809](https://github.com/databricks/cli/pull/1809)). API Changes: * Changed `databricks git-credentials create`, `databricks git-credentials delete`, `databricks git-credentials get`, `databricks git-credentials list`, `databricks git-credentials update` commands . * Changed `databricks repos create`, `databricks repos delete`, `databricks repos get`, `databricks repos update` command . OpenAPI commit 0c86ea6dbd9a730c24ff0d4e509603e476955ac5 (2024-10-02) Dependency updates: * Upgrade TF provider to 1.53.0 ([#1815](https://github.com/databricks/cli/pull/1815)). * Bump golang.org/x/term from 0.24.0 to 0.25.0 ([#1811](https://github.com/databricks/cli/pull/1811)). * Bump golang.org/x/text from 0.18.0 to 0.19.0 ([#1812](https://github.com/databricks/cli/pull/1812)). * Bump github.com/databricks/databricks-sdk-go from 0.47.0 to 0.48.0 ([#1810](https://github.com/databricks/cli/pull/1810)). --------- Co-authored-by: Pieter Noordhuis --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f5f68ac..f31bb10b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Version changelog +## [Release] Release v0.230.0 + +Notable changes for Databricks Asset Bundles: + +Workspace paths are automatically prefixed with `/Workspace`. In addition, all usage of path strings such as `/Workspace/${workspace.root_path}/...` in bundle configuration is automatically replaced with `${workspace.root_path}/...` and generates a warning as part of bundle validate. + +More details can be found here: https://docs.databricks.com/en/release-notes/dev-tools/bundles.html#workspace-paths + +Bundles: + * Add an error if state files grow bigger than the export limit ([#1795](https://github.com/databricks/cli/pull/1795)). + * Always prepend bundle remote paths with /Workspace ([#1724](https://github.com/databricks/cli/pull/1724)). + * Add resource path field to bundle workspace configuration ([#1800](https://github.com/databricks/cli/pull/1800)). + * Add validation for files with a `.(resource-name).yml` extension ([#1780](https://github.com/databricks/cli/pull/1780)). + +Internal: + * Remove deprecated or readonly fields from the bundle schema ([#1809](https://github.com/databricks/cli/pull/1809)). + +API Changes: + * Changed `databricks git-credentials create`, `databricks git-credentials delete`, `databricks git-credentials get`, `databricks git-credentials list`, `databricks git-credentials update` commands . + * Changed `databricks repos create`, `databricks repos delete`, `databricks repos get`, `databricks repos update` command . + +OpenAPI commit 0c86ea6dbd9a730c24ff0d4e509603e476955ac5 (2024-10-02) +Dependency updates: + * Upgrade TF provider to 1.53.0 ([#1815](https://github.com/databricks/cli/pull/1815)). + * Bump golang.org/x/term from 0.24.0 to 0.25.0 ([#1811](https://github.com/databricks/cli/pull/1811)). + * Bump golang.org/x/text from 0.18.0 to 0.19.0 ([#1812](https://github.com/databricks/cli/pull/1812)). + * Bump github.com/databricks/databricks-sdk-go from 0.47.0 to 0.48.0 ([#1810](https://github.com/databricks/cli/pull/1810)). + ## [Release] Release v0.229.0 Bundles: From e885794722793a53ae2cd05ee87659cde702a8fb Mon Sep 17 00:00:00 2001 From: "Lennart Kats (databricks)" Date: Thu, 10 Oct 2024 13:18:23 +0200 Subject: [PATCH 13/24] Show actionable errors for collaborative deployment scenarios (#1386) ## Changes This adds diagnostics for collaborative (production) deployment scenarios, including: - Bob deploys a bundle that is normally deployed by Alice, but this fails because Bob can't write to `/Users/Alice/.bundle`. - Charlie deploys a bundle that is normally deployed by Alice, but this fails because he can't create a new pipeline where Alice would be the owner. - Alice deploys a bundle where she didn't list herself as one of the CAN_MANAGE users in permissions. That can work, but is probably a mistake. ## Tests Unit tests, manual testing. --- bundle/config/mutator/run_as.go | 101 ++++++++-------- bundle/config/mutator/run_as_test.go | 9 +- bundle/deploy/files/upload.go | 6 + bundle/deploy/lock/acquire.go | 9 +- bundle/deploy/terraform/apply.go | 5 + bundle/permissions/mutator.go | 1 + bundle/permissions/permission_diagnostics.go | 110 ++++++++++++++++++ .../permission_diagnostics_test.go | 52 +++++++++ bundle/permissions/permission_report.go | 51 ++++++++ bundle/permissions/permission_report_test.go | 76 ++++++++++++ bundle/permissions/terraform_errors.go | 47 ++++++++ bundle/permissions/terraform_errors_test.go | 97 +++++++++++++++ bundle/phases/initialize.go | 2 + bundle/tests/run_as_test.go | 25 ++-- libs/diag/diagnostic.go | 15 ++- libs/diag/id.go | 16 +++ libs/filer/filer.go | 12 ++ libs/filer/workspace_files_client.go | 12 +- libs/set/set.go | 10 ++ 19 files changed, 579 insertions(+), 77 deletions(-) create mode 100644 bundle/permissions/permission_diagnostics.go create mode 100644 bundle/permissions/permission_diagnostics_test.go create mode 100644 bundle/permissions/permission_report.go create mode 100644 bundle/permissions/permission_report_test.go create mode 100644 bundle/permissions/terraform_errors.go create mode 100644 bundle/permissions/terraform_errors_test.go create mode 100644 libs/diag/id.go diff --git a/bundle/config/mutator/run_as.go b/bundle/config/mutator/run_as.go index 423bc38e..6b3069d4 100644 --- a/bundle/config/mutator/run_as.go +++ b/bundle/config/mutator/run_as.go @@ -30,50 +30,44 @@ func (m *setRunAs) Name() string { return "SetRunAs" } -type errUnsupportedResourceTypeForRunAs struct { - resourceType string - resourceLocation dyn.Location - currentUser string - runAsUser string +func reportRunAsNotSupported(resourceType string, location dyn.Location, currentUser string, runAsUser string) diag.Diagnostics { + return diag.Diagnostics{{ + Summary: fmt.Sprintf("%s do not support a setting a run_as user that is different from the owner.\n"+ + "Current identity: %s. Run as identity: %s.\n"+ + "See https://docs.databricks.com/dev-tools/bundles/run-as.html to learn more about the run_as property.", resourceType, currentUser, runAsUser), + Locations: []dyn.Location{location}, + Severity: diag.Error, + }} } -func (e errUnsupportedResourceTypeForRunAs) Error() string { - return fmt.Sprintf("%s are not supported when the current deployment user is different from the bundle's run_as identity. Please deploy as the run_as identity. Please refer to the documentation at https://docs.databricks.com/dev-tools/bundles/run-as.html for more details. Location of the unsupported resource: %s. Current identity: %s. Run as identity: %s", e.resourceType, e.resourceLocation, e.currentUser, e.runAsUser) -} +func validateRunAs(b *bundle.Bundle) diag.Diagnostics { + diags := diag.Diagnostics{} -type errBothSpAndUserSpecified struct { - spName string - spLoc dyn.Location - userName string - userLoc dyn.Location -} + neitherSpecifiedErr := diag.Diagnostics{{ + Summary: "run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified", + Locations: []dyn.Location{b.Config.GetLocation("run_as")}, + Severity: diag.Error, + }} -func (e errBothSpAndUserSpecified) Error() string { - return fmt.Sprintf("run_as section must specify exactly one identity. A service_principal_name %q is specified at %s. A user_name %q is defined at %s", e.spName, e.spLoc, e.userName, e.userLoc) -} - -func validateRunAs(b *bundle.Bundle) error { - neitherSpecifiedErr := fmt.Errorf("run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified at %s", b.Config.GetLocation("run_as")) - // Error if neither service_principal_name nor user_name are specified, but the + // Fail fast if neither service_principal_name nor user_name are specified, but the // run_as section is present. if b.Config.Value().Get("run_as").Kind() == dyn.KindNil { return neitherSpecifiedErr } - // Error if one or both of service_principal_name and user_name are specified, + + // Fail fast if one or both of service_principal_name and user_name are specified, // but with empty values. - if b.Config.RunAs.ServicePrincipalName == "" && b.Config.RunAs.UserName == "" { + runAs := b.Config.RunAs + if runAs.ServicePrincipalName == "" && runAs.UserName == "" { return neitherSpecifiedErr } - // Error if both service_principal_name and user_name are specified - runAs := b.Config.RunAs if runAs.UserName != "" && runAs.ServicePrincipalName != "" { - return errBothSpAndUserSpecified{ - spName: runAs.ServicePrincipalName, - userName: runAs.UserName, - spLoc: b.Config.GetLocation("run_as.service_principal_name"), - userLoc: b.Config.GetLocation("run_as.user_name"), - } + diags = diags.Extend(diag.Diagnostics{{ + Summary: "run_as section cannot specify both user_name and service_principal_name", + Locations: []dyn.Location{b.Config.GetLocation("run_as")}, + Severity: diag.Error, + }}) } identity := runAs.ServicePrincipalName @@ -83,40 +77,40 @@ func validateRunAs(b *bundle.Bundle) error { // All resources are supported if the run_as identity is the same as the current deployment identity. if identity == b.Config.Workspace.CurrentUser.UserName { - return nil + return diags } // DLT pipelines do not support run_as in the API. if len(b.Config.Resources.Pipelines) > 0 { - return errUnsupportedResourceTypeForRunAs{ - resourceType: "pipelines", - resourceLocation: b.Config.GetLocation("resources.pipelines"), - currentUser: b.Config.Workspace.CurrentUser.UserName, - runAsUser: identity, - } + diags = diags.Extend(reportRunAsNotSupported( + "pipelines", + b.Config.GetLocation("resources.pipelines"), + b.Config.Workspace.CurrentUser.UserName, + identity, + )) } // Model serving endpoints do not support run_as in the API. if len(b.Config.Resources.ModelServingEndpoints) > 0 { - return errUnsupportedResourceTypeForRunAs{ - resourceType: "model_serving_endpoints", - resourceLocation: b.Config.GetLocation("resources.model_serving_endpoints"), - currentUser: b.Config.Workspace.CurrentUser.UserName, - runAsUser: identity, - } + diags = diags.Extend(reportRunAsNotSupported( + "model_serving_endpoints", + b.Config.GetLocation("resources.model_serving_endpoints"), + b.Config.Workspace.CurrentUser.UserName, + identity, + )) } // Monitors do not support run_as in the API. if len(b.Config.Resources.QualityMonitors) > 0 { - return errUnsupportedResourceTypeForRunAs{ - resourceType: "quality_monitors", - resourceLocation: b.Config.GetLocation("resources.quality_monitors"), - currentUser: b.Config.Workspace.CurrentUser.UserName, - runAsUser: identity, - } + diags = diags.Extend(reportRunAsNotSupported( + "quality_monitors", + b.Config.GetLocation("resources.quality_monitors"), + b.Config.Workspace.CurrentUser.UserName, + identity, + )) } - return nil + return diags } func setRunAsForJobs(b *bundle.Bundle) { @@ -187,8 +181,9 @@ func (m *setRunAs) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics { } // Assert the run_as configuration is valid in the context of the bundle - if err := validateRunAs(b); err != nil { - return diag.FromErr(err) + diags := validateRunAs(b) + if diags.HasError() { + return diags } setRunAsForJobs(b) diff --git a/bundle/config/mutator/run_as_test.go b/bundle/config/mutator/run_as_test.go index abeea45d..8076b82f 100644 --- a/bundle/config/mutator/run_as_test.go +++ b/bundle/config/mutator/run_as_test.go @@ -188,11 +188,8 @@ func TestRunAsErrorForUnsupportedResources(t *testing.T) { Config: *r, } diags := bundle.Apply(context.Background(), b, SetRunAs()) - assert.Equal(t, diags.Error().Error(), errUnsupportedResourceTypeForRunAs{ - resourceType: rt, - resourceLocation: dyn.Location{}, - currentUser: "alice", - runAsUser: "bob", - }.Error(), "expected run_as with a different identity than the current deployment user to not supported for resources of type: %s", rt) + assert.Contains(t, diags.Error().Error(), "do not support a setting a run_as user that is different from the owner.\n"+ + "Current identity: alice. Run as identity: bob.\n"+ + "See https://docs.databricks.com/dev-tools/bundles/run-as.html to learn more about the run_as property.", rt) } } diff --git a/bundle/deploy/files/upload.go b/bundle/deploy/files/upload.go index 77b83611..bab4e176 100644 --- a/bundle/deploy/files/upload.go +++ b/bundle/deploy/files/upload.go @@ -2,9 +2,12 @@ package files import ( "context" + "errors" "fmt" + "io/fs" "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/permissions" "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/log" @@ -35,6 +38,9 @@ func (m *upload) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { b.Files, err = sync.RunOnce(ctx) if err != nil { + if errors.Is(err, fs.ErrPermission) { + return permissions.ReportPossiblePermissionDenied(ctx, b, b.Config.Workspace.FilePath) + } return diag.FromErr(err) } diff --git a/bundle/deploy/lock/acquire.go b/bundle/deploy/lock/acquire.go index 7d3d0eca..ab1f1cba 100644 --- a/bundle/deploy/lock/acquire.go +++ b/bundle/deploy/lock/acquire.go @@ -3,8 +3,10 @@ package lock import ( "context" "errors" + "io/fs" "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/permissions" "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/filer" "github.com/databricks/cli/libs/locker" @@ -51,12 +53,17 @@ func (m *acquire) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics if err != nil { log.Errorf(ctx, "Failed to acquire deployment lock: %v", err) + if errors.Is(err, fs.ErrPermission) { + return permissions.ReportPossiblePermissionDenied(ctx, b, b.Config.Workspace.StatePath) + } + notExistsError := filer.NoSuchDirectoryError{} if errors.As(err, ¬ExistsError) { // If we get a "doesn't exist" error from the API this indicates // we either don't have permissions or the path is invalid. - return diag.Errorf("cannot write to deployment root (this can indicate a previous deploy was done with a different identity): %s", b.Config.Workspace.RootPath) + return permissions.ReportPossiblePermissionDenied(ctx, b, b.Config.Workspace.StatePath) } + return diag.FromErr(err) } diff --git a/bundle/deploy/terraform/apply.go b/bundle/deploy/terraform/apply.go index e52d0ca8..5ea2effa 100644 --- a/bundle/deploy/terraform/apply.go +++ b/bundle/deploy/terraform/apply.go @@ -4,6 +4,7 @@ import ( "context" "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/permissions" "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/log" "github.com/hashicorp/terraform-exec/tfexec" @@ -34,6 +35,10 @@ func (w *apply) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { // Apply terraform according to the computed plan err := tf.Apply(ctx, tfexec.DirOrPlan(b.Plan.Path)) if err != nil { + diags := permissions.TryExtendTerraformPermissionError(ctx, b, err) + if diags != nil { + return diags + } return diag.Errorf("terraform apply: %v", err) } diff --git a/bundle/permissions/mutator.go b/bundle/permissions/mutator.go index 7787bc04..247ffcda 100644 --- a/bundle/permissions/mutator.go +++ b/bundle/permissions/mutator.go @@ -13,6 +13,7 @@ import ( const CAN_MANAGE = "CAN_MANAGE" const CAN_VIEW = "CAN_VIEW" const CAN_RUN = "CAN_RUN" +const IS_OWNER = "IS_OWNER" var allowedLevels = []string{CAN_MANAGE, CAN_VIEW, CAN_RUN} var levelsMap = map[string](map[string]string){ diff --git a/bundle/permissions/permission_diagnostics.go b/bundle/permissions/permission_diagnostics.go new file mode 100644 index 00000000..d2c24fa0 --- /dev/null +++ b/bundle/permissions/permission_diagnostics.go @@ -0,0 +1,110 @@ +package permissions + +import ( + "context" + "fmt" + "sort" + "strings" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/dyn" + "github.com/databricks/cli/libs/set" +) + +type permissionDiagnostics struct{} + +func PermissionDiagnostics() bundle.Mutator { + return &permissionDiagnostics{} +} + +func (m *permissionDiagnostics) Name() string { + return "CheckPermissions" +} + +func (m *permissionDiagnostics) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { + if len(b.Config.Permissions) == 0 { + // Only warn if there is an explicit top-level permissions section + return nil + } + + canManageBundle, _ := analyzeBundlePermissions(b) + if canManageBundle { + return nil + } + + return diag.Diagnostics{{ + Severity: diag.Warning, + Summary: fmt.Sprintf("permissions section should include %s or one of their groups with CAN_MANAGE permissions", b.Config.Workspace.CurrentUser.UserName), + Locations: []dyn.Location{b.Config.GetLocation("permissions")}, + ID: diag.PermissionNotIncluded, + }} +} + +// analyzeBundlePermissions analyzes the top-level permissions of the bundle. +// This permission set is important since it determines the permissions of the +// target workspace folder. +// +// Returns: +// - isManager: true if the current user is can manage the bundle resources. +// - assistance: advice on who to contact as to manage this project +func analyzeBundlePermissions(b *bundle.Bundle) (bool, string) { + canManageBundle := false + otherManagers := set.NewSet[string]() + if b.Config.RunAs != nil && b.Config.RunAs.UserName != "" && b.Config.RunAs.UserName != b.Config.Workspace.CurrentUser.UserName { + // The run_as user is another human that could be contacted + // about this bundle. + otherManagers.Add(b.Config.RunAs.UserName) + } + + currentUser := b.Config.Workspace.CurrentUser.UserName + targetPermissions := b.Config.Permissions + for _, p := range targetPermissions { + if p.Level != CAN_MANAGE { + continue + } + + if p.UserName == currentUser || p.ServicePrincipalName == currentUser { + canManageBundle = true + continue + } + + if isGroupOfCurrentUser(b, p.GroupName) { + canManageBundle = true + continue + } + + // Permission doesn't apply to current user; add to otherManagers + otherManager := p.UserName + if otherManager == "" { + otherManager = p.GroupName + } + if otherManager == "" { + // Skip service principals + continue + } + otherManagers.Add(otherManager) + } + + assistance := "For assistance, contact the owners of this project." + if otherManagers.Size() > 0 { + list := otherManagers.Values() + sort.Strings(list) + assistance = fmt.Sprintf( + "For assistance, users or groups with appropriate permissions may include: %s.", + strings.Join(list, ", "), + ) + } + return canManageBundle, assistance +} + +func isGroupOfCurrentUser(b *bundle.Bundle, groupName string) bool { + currentUserGroups := b.Config.Workspace.CurrentUser.User.Groups + + for _, g := range currentUserGroups { + if g.Display == groupName { + return true + } + } + return false +} diff --git a/bundle/permissions/permission_diagnostics_test.go b/bundle/permissions/permission_diagnostics_test.go new file mode 100644 index 00000000..7b0afefa --- /dev/null +++ b/bundle/permissions/permission_diagnostics_test.go @@ -0,0 +1,52 @@ +package permissions_test + +import ( + "context" + "testing" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/cli/bundle/permissions" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/databricks-sdk-go/service/iam" + "github.com/stretchr/testify/require" +) + +func TestPermissionDiagnosticsApplySuccess(t *testing.T) { + b := mockBundle([]resources.Permission{ + {Level: "CAN_MANAGE", UserName: "testuser@databricks.com"}, + }) + + diags := permissions.PermissionDiagnostics().Apply(context.Background(), b) + require.NoError(t, diags.Error()) +} + +func TestPermissionDiagnosticsApplyFail(t *testing.T) { + b := mockBundle([]resources.Permission{ + {Level: "CAN_VIEW", UserName: "testuser@databricks.com"}, + }) + + diags := permissions.PermissionDiagnostics().Apply(context.Background(), b) + require.Equal(t, diags[0].Severity, diag.Warning) + require.Contains(t, diags[0].Summary, "permissions section should include testuser@databricks.com or one of their groups with CAN_MANAGE permissions") +} + +func mockBundle(permissions []resources.Permission) *bundle.Bundle { + return &bundle.Bundle{ + Config: config.Root{ + Workspace: config.Workspace{ + CurrentUser: &config.User{ + User: &iam.User{ + UserName: "testuser@databricks.com", + DisplayName: "Test User", + Groups: []iam.ComplexValue{ + {Display: "testgroup"}, + }, + }, + }, + }, + Permissions: permissions, + }, + } +} diff --git a/bundle/permissions/permission_report.go b/bundle/permissions/permission_report.go new file mode 100644 index 00000000..a9468edb --- /dev/null +++ b/bundle/permissions/permission_report.go @@ -0,0 +1,51 @@ +package permissions + +import ( + "context" + "fmt" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/libs/auth" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/log" +) + +// ReportPossiblePermissionDenied generates a diagnostic message when a permission denied error is encountered. +// +// Note that since the workspace API doesn't always distinguish between permission denied and path errors, +// we must treat this as a "possible permission error". See acquire.go for more about this. +func ReportPossiblePermissionDenied(ctx context.Context, b *bundle.Bundle, path string) diag.Diagnostics { + log.Errorf(ctx, "Failed to update, encountered possible permission error: %v", path) + + user := b.Config.Workspace.CurrentUser.UserName + if auth.IsServicePrincipal(user) { + user = b.Config.Workspace.CurrentUser.DisplayName + } + canManageBundle, assistance := analyzeBundlePermissions(b) + + if !canManageBundle { + return diag.Diagnostics{{ + Summary: fmt.Sprintf("unable to deploy to %s as %s.\n"+ + "Please make sure the current user or one of their groups is listed under the permissions of this bundle.\n"+ + "%s\n"+ + "They may need to redeploy the bundle to apply the new permissions.\n"+ + "Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions.", + path, user, assistance), + Severity: diag.Error, + ID: diag.PathPermissionDenied, + }} + } + + // According databricks.yml, the current user has the right permissions. + // But we're still seeing permission errors. So someone else will need + // to redeploy the bundle with the right set of permissions. + return diag.Diagnostics{{ + Summary: fmt.Sprintf("unable to deploy to %s as %s. Cannot apply local deployment permissions.\n"+ + "%s\n"+ + "They can redeploy the project to apply the latest set of permissions.\n"+ + "Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions.", + path, user, assistance), + Severity: diag.Error, + ID: diag.CannotChangePathPermissions, + }} +} diff --git a/bundle/permissions/permission_report_test.go b/bundle/permissions/permission_report_test.go new file mode 100644 index 00000000..61592f7e --- /dev/null +++ b/bundle/permissions/permission_report_test.go @@ -0,0 +1,76 @@ +package permissions_test + +import ( + "context" + "testing" + + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/cli/bundle/permissions" + "github.com/stretchr/testify/require" +) + +func TestPermissionsReportPermissionDeniedWithGroup(t *testing.T) { + b := mockBundle([]resources.Permission{ + {Level: "CAN_MANAGE", GroupName: "testgroup"}, + }) + + diags := permissions.ReportPossiblePermissionDenied(context.Background(), b, "testpath") + expected := "EPERM3: unable to deploy to testpath as testuser@databricks.com. Cannot apply local deployment permissions.\n" + + "For assistance, contact the owners of this project.\n" + + "They can redeploy the project to apply the latest set of permissions.\n" + + "Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions." + require.ErrorContains(t, diags.Error(), expected) +} + +func TestPermissionsReportPermissionDeniedWithOtherGroup(t *testing.T) { + b := mockBundle([]resources.Permission{ + {Level: "CAN_MANAGE", GroupName: "othergroup"}, + }) + + diags := permissions.ReportPossiblePermissionDenied(context.Background(), b, "testpath") + expected := "EPERM1: unable to deploy to testpath as testuser@databricks.com.\n" + + "Please make sure the current user or one of their groups is listed under the permissions of this bundle.\n" + + "For assistance, users or groups with appropriate permissions may include: othergroup.\n" + + "They may need to redeploy the bundle to apply the new permissions.\n" + + "Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions." + require.ErrorContains(t, diags.Error(), expected) +} + +func TestPermissionsReportPermissionDeniedWithoutPermission(t *testing.T) { + b := mockBundle([]resources.Permission{ + {Level: "CAN_VIEW", UserName: "testuser@databricks.com"}, + }) + + diags := permissions.ReportPossiblePermissionDenied(context.Background(), b, "testpath") + expected := "EPERM1: unable to deploy to testpath as testuser@databricks.com.\n" + + "Please make sure the current user or one of their groups is listed under the permissions of this bundle.\n" + + "For assistance, contact the owners of this project.\n" + + "They may need to redeploy the bundle to apply the new permissions.\n" + + "Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions." + require.ErrorContains(t, diags.Error(), expected) +} + +func TestPermissionsReportPermissionDeniedNilPermission(t *testing.T) { + b := mockBundle(nil) + + diags := permissions.ReportPossiblePermissionDenied(context.Background(), b, "testpath") + expected := "EPERM1: unable to deploy to testpath as testuser@databricks.com.\n" + + "Please make sure the current user or one of their groups is listed under the permissions of this bundle.\n" + + "For assistance, contact the owners of this project.\n" + + "They may need to redeploy the bundle to apply the new permissions.\n" + + "Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions" + require.ErrorContains(t, diags.Error(), expected) +} + +func TestPermissionsReportFindOtherOwners(t *testing.T) { + b := mockBundle([]resources.Permission{ + {Level: "CAN_MANAGE", GroupName: "testgroup"}, + {Level: "CAN_MANAGE", UserName: "alice@databricks.com"}, + }) + + diags := permissions.ReportPossiblePermissionDenied(context.Background(), b, "testpath") + require.ErrorContains(t, diags.Error(), "EPERM3: unable to deploy to testpath as testuser@databricks.com. Cannot apply local deployment permissions.\n"+ + "For assistance, users or groups with appropriate permissions may include: alice@databricks.com.\n"+ + "They can redeploy the project to apply the latest set of permissions.\n"+ + "Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions.") +} diff --git a/bundle/permissions/terraform_errors.go b/bundle/permissions/terraform_errors.go new file mode 100644 index 00000000..cc4c9f61 --- /dev/null +++ b/bundle/permissions/terraform_errors.go @@ -0,0 +1,47 @@ +package permissions + +import ( + "context" + "fmt" + "regexp" + "strings" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/log" +) + +func TryExtendTerraformPermissionError(ctx context.Context, b *bundle.Bundle, err error) diag.Diagnostics { + _, assistance := analyzeBundlePermissions(b) + + // In a best-effort attempt to provide actionable error messages, we match + // against a few specific error messages that come from the Jobs and Pipelines API. + // For matching errors we provide a more specific error message that includes + // details on how to resolve the issue. + if !strings.Contains(err.Error(), "cannot update permissions") && + !strings.Contains(err.Error(), "permissions on pipeline") && + !strings.Contains(err.Error(), "cannot read permissions") && + !strings.Contains(err.Error(), "cannot set run_as to user") { + return nil + } + + log.Errorf(ctx, "Terraform error during deployment: %v", err.Error()) + + // Best-effort attempt to extract the resource name from the error message. + re := regexp.MustCompile(`databricks_(\w*)\.(\w*)`) + match := re.FindStringSubmatch(err.Error()) + resource := "resource" + if len(match) > 1 { + resource = match[2] + } + + return diag.Diagnostics{{ + Summary: fmt.Sprintf("permission denied creating or updating %s.\n"+ + "%s\n"+ + "They can redeploy the project to apply the latest set of permissions.\n"+ + "Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions.", + resource, assistance), + Severity: diag.Error, + ID: diag.ResourcePermissionDenied, + }} +} diff --git a/bundle/permissions/terraform_errors_test.go b/bundle/permissions/terraform_errors_test.go new file mode 100644 index 00000000..c9b5b178 --- /dev/null +++ b/bundle/permissions/terraform_errors_test.go @@ -0,0 +1,97 @@ +package permissions_test + +import ( + "context" + "errors" + "testing" + + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/cli/bundle/permissions" + "github.com/databricks/databricks-sdk-go/service/jobs" + "github.com/stretchr/testify/require" +) + +func TestTryExtendTerraformPermissionError1(t *testing.T) { + ctx := context.Background() + b := mockBundle([]resources.Permission{ + {Level: "CAN_MANAGE", UserName: "alice@databricks.com"}, + }) + err := permissions.TryExtendTerraformPermissionError(ctx, b, errors.New("Error: terraform apply: exit status 1\n"+ + "\n"+ + "Error: cannot update permissions: ...\n"+ + "\n"+ + " with databricks_pipeline.my_project_pipeline,\n"+ + " on bundle.tf.json line 39, in resource.databricks_pipeline.my_project_pipeline:\n"+ + " 39: }")).Error() + + expected := "EPERM2: permission denied creating or updating my_project_pipeline.\n" + + "For assistance, users or groups with appropriate permissions may include: alice@databricks.com.\n" + + "They can redeploy the project to apply the latest set of permissions.\n" + + "Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions" + + require.ErrorContains(t, err, expected) +} + +func TestTryExtendTerraformPermissionError2(t *testing.T) { + ctx := context.Background() + b := mockBundle([]resources.Permission{ + {Level: "CAN_MANAGE", UserName: "alice@databricks.com"}, + {Level: "CAN_MANAGE", UserName: "bob@databricks.com"}, + }) + err := permissions.TryExtendTerraformPermissionError(ctx, b, errors.New("Error: terraform apply: exit status 1\n"+ + "\n"+ + "Error: cannot read pipeline: User xyz does not have View permissions on pipeline 4521dbb6-42aa-418c-b94d-b5f4859a3454.\n"+ + "\n"+ + " with databricks_pipeline.my_project_pipeline,\n"+ + " on bundle.tf.json line 39, in resource.databricks_pipeline.my_project_pipeline:\n"+ + " 39: }")).Error() + + expected := "EPERM2: permission denied creating or updating my_project_pipeline.\n" + + "For assistance, users or groups with appropriate permissions may include: alice@databricks.com, bob@databricks.com.\n" + + "They can redeploy the project to apply the latest set of permissions.\n" + + "Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions." + require.ErrorContains(t, err, expected) +} + +func TestTryExtendTerraformPermissionError3(t *testing.T) { + ctx := context.Background() + b := mockBundle([]resources.Permission{ + {Level: "CAN_MANAGE", UserName: "testuser@databricks.com"}, + }) + err := permissions.TryExtendTerraformPermissionError(ctx, b, errors.New("Error: terraform apply: exit status 1\n"+ + "\n"+ + "Error: cannot read permissions: 1706906c-c0a2-4c25-9f57-3a7aa3cb8b90 does not have Owner permissions on Job with ID: ElasticJobId(28263044278868). Please contact the owner or an administrator for access.\n"+ + "\n"+ + " with databricks_pipeline.my_project_pipeline,\n"+ + " on bundle.tf.json line 39, in resource.databricks_pipeline.my_project_pipeline:\n"+ + " 39: }")).Error() + + expected := "EPERM2: permission denied creating or updating my_project_pipeline.\n" + + "For assistance, contact the owners of this project.\n" + + "They can redeploy the project to apply the latest set of permissions.\n" + + "Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions." + require.ErrorContains(t, err, expected) +} + +func TestTryExtendTerraformPermissionErrorNotOwner(t *testing.T) { + ctx := context.Background() + b := mockBundle([]resources.Permission{ + {Level: "CAN_MANAGE", GroupName: "data_team@databricks.com"}, + }) + b.Config.RunAs = &jobs.JobRunAs{ + UserName: "testuser@databricks.com", + } + err := permissions.TryExtendTerraformPermissionError(ctx, b, errors.New("Error: terraform apply: exit status 1\n"+ + "\n"+ + "Error: cannot read pipeline: User xyz does not have View permissions on pipeline 4521dbb6-42aa-418c-b94d-b5f4859a3454.\n"+ + "\n"+ + " with databricks_pipeline.my_project_pipeline,\n"+ + " on bundle.tf.json line 39, in resource.databricks_pipeline.my_project_pipeline:\n"+ + " 39: }")).Error() + + expected := "EPERM2: permission denied creating or updating my_project_pipeline.\n" + + "For assistance, users or groups with appropriate permissions may include: data_team@databricks.com.\n" + + "They can redeploy the project to apply the latest set of permissions.\n" + + "Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions." + require.ErrorContains(t, err, expected) +} diff --git a/bundle/phases/initialize.go b/bundle/phases/initialize.go index a41819c7..da5b2eff 100644 --- a/bundle/phases/initialize.go +++ b/bundle/phases/initialize.go @@ -62,6 +62,8 @@ func Initialize() bundle.Mutator { "workspace", "variables", ), + // Provide permission config errors & warnings after initializing all variables + permissions.PermissionDiagnostics(), mutator.SetRunAs(), mutator.OverrideCompute(), mutator.ProcessTargetMode(), diff --git a/bundle/tests/run_as_test.go b/bundle/tests/run_as_test.go index 6c07cc53..92057714 100644 --- a/bundle/tests/run_as_test.go +++ b/bundle/tests/run_as_test.go @@ -3,7 +3,6 @@ package config_tests import ( "context" "fmt" - "path/filepath" "testing" "github.com/databricks/cli/bundle" @@ -113,8 +112,9 @@ func TestRunAsErrorForPipelines(t *testing.T) { diags := bundle.Apply(ctx, b, mutator.SetRunAs()) err := diags.Error() - configPath := filepath.FromSlash("run_as/not_allowed/pipelines/databricks.yml") - assert.EqualError(t, err, fmt.Sprintf("pipelines are not supported when the current deployment user is different from the bundle's run_as identity. Please deploy as the run_as identity. Please refer to the documentation at https://docs.databricks.com/dev-tools/bundles/run-as.html for more details. Location of the unsupported resource: %s:14:5. Current identity: jane@doe.com. Run as identity: my_service_principal", configPath)) + assert.ErrorContains(t, err, "pipelines do not support a setting a run_as user that is different from the owner.\n"+ + "Current identity: jane@doe.com. Run as identity: my_service_principal.\n"+ + "See https://docs") } func TestRunAsNoErrorForPipelines(t *testing.T) { @@ -152,8 +152,9 @@ func TestRunAsErrorForModelServing(t *testing.T) { diags := bundle.Apply(ctx, b, mutator.SetRunAs()) err := diags.Error() - configPath := filepath.FromSlash("run_as/not_allowed/model_serving/databricks.yml") - assert.EqualError(t, err, fmt.Sprintf("model_serving_endpoints are not supported when the current deployment user is different from the bundle's run_as identity. Please deploy as the run_as identity. Please refer to the documentation at https://docs.databricks.com/dev-tools/bundles/run-as.html for more details. Location of the unsupported resource: %s:14:5. Current identity: jane@doe.com. Run as identity: my_service_principal", configPath)) + assert.ErrorContains(t, err, "model_serving_endpoints do not support a setting a run_as user that is different from the owner.\n"+ + "Current identity: jane@doe.com. Run as identity: my_service_principal.\n"+ + "See https://docs") } func TestRunAsNoErrorForModelServingEndpoints(t *testing.T) { @@ -191,8 +192,7 @@ func TestRunAsErrorWhenBothUserAndSpSpecified(t *testing.T) { diags := bundle.Apply(ctx, b, mutator.SetRunAs()) err := diags.Error() - configPath := filepath.FromSlash("run_as/not_allowed/both_sp_and_user/databricks.yml") - assert.EqualError(t, err, fmt.Sprintf("run_as section must specify exactly one identity. A service_principal_name \"my_service_principal\" is specified at %s:6:27. A user_name \"my_user_name\" is defined at %s:7:14", configPath, configPath)) + assert.ErrorContains(t, err, "run_as section cannot specify both user_name and service_principal_name") } func TestRunAsErrorNeitherUserOrSpSpecified(t *testing.T) { @@ -202,19 +202,19 @@ func TestRunAsErrorNeitherUserOrSpSpecified(t *testing.T) { }{ { name: "empty_run_as", - err: fmt.Sprintf("run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified at %s:4:8", filepath.FromSlash("run_as/not_allowed/neither_sp_nor_user/empty_run_as/databricks.yml")), + err: "run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified", }, { name: "empty_sp", - err: fmt.Sprintf("run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified at %s:5:3", filepath.FromSlash("run_as/not_allowed/neither_sp_nor_user/empty_sp/databricks.yml")), + err: "run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified", }, { name: "empty_user", - err: fmt.Sprintf("run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified at %s:5:3", filepath.FromSlash("run_as/not_allowed/neither_sp_nor_user/empty_user/databricks.yml")), + err: "run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified", }, { name: "empty_user_and_sp", - err: fmt.Sprintf("run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified at %s:5:3", filepath.FromSlash("run_as/not_allowed/neither_sp_nor_user/empty_user_and_sp/databricks.yml")), + err: "run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified", }, } @@ -257,8 +257,7 @@ func TestRunAsErrorNeitherUserOrSpSpecifiedAtTargetOverride(t *testing.T) { diags := bundle.Apply(ctx, b, mutator.SetRunAs()) err := diags.Error() - configPath := filepath.FromSlash("run_as/not_allowed/neither_sp_nor_user/override/override.yml") - assert.EqualError(t, err, fmt.Sprintf("run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified at %s:4:12", configPath)) + assert.EqualError(t, err, "run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified") } func TestLegacyRunAs(t *testing.T) { diff --git a/libs/diag/diagnostic.go b/libs/diag/diagnostic.go index 93334c06..254ecbd7 100644 --- a/libs/diag/diagnostic.go +++ b/libs/diag/diagnostic.go @@ -1,6 +1,7 @@ package diag import ( + "errors" "fmt" "github.com/databricks/cli/libs/dyn" @@ -24,6 +25,9 @@ type Diagnostic struct { // Paths are paths to the values in the configuration tree that the diagnostic is associated with. // It may be nil if there are no associated paths. Paths []dyn.Path + + // A diagnostic ID. Only used for select diagnostic messages. + ID ID } // Errorf creates a new error diagnostic. @@ -69,7 +73,7 @@ func Infof(format string, args ...any) Diagnostics { } } -// Diagsnostics holds zero or more instances of [Diagnostic]. +// Diagnostics holds zero or more instances of [Diagnostic]. type Diagnostics []Diagnostic // Append adds a new diagnostic to the end of the list. @@ -96,7 +100,14 @@ func (ds Diagnostics) HasError() bool { func (ds Diagnostics) Error() error { for _, d := range ds { if d.Severity == Error { - return fmt.Errorf(d.Summary) + message := d.Detail + if message == "" { + message = d.Summary + } + if d.ID != "" { + message = string(d.ID) + ": " + message + } + return errors.New(message) } } return nil diff --git a/libs/diag/id.go b/libs/diag/id.go new file mode 100644 index 00000000..44af3c93 --- /dev/null +++ b/libs/diag/id.go @@ -0,0 +1,16 @@ +package diag + +type ID string + +// For select diagnostic messages we use IDs to identify them +// for support or tooling purposes. +// It is a non-goal to have an exhaustive list of IDs. +const ( + // We have many subtly different permission errors. + // These are numbered for easy reference and tooling support. + PathPermissionDenied ID = "EPERM1" + ResourcePermissionDenied ID = "EPERM2" + CannotChangePathPermissions ID = "EPERM3" + RunAsDenied ID = "EPERM4" + PermissionNotIncluded ID = "EPERM5" +) diff --git a/libs/filer/filer.go b/libs/filer/filer.go index c1c747c5..fcfbcea0 100644 --- a/libs/filer/filer.go +++ b/libs/filer/filer.go @@ -103,6 +103,18 @@ func (err CannotDeleteRootError) Is(other error) bool { return other == fs.ErrInvalid } +type PermissionError struct { + path string +} + +func (err PermissionError) Error() string { + return fmt.Sprintf("access denied: %s", err.path) +} + +func (err PermissionError) Is(other error) bool { + return other == fs.ErrPermission +} + // Filer is used to access files in a workspace. // It has implementations for accessing files in WSFS and in DBFS. type Filer interface { diff --git a/libs/filer/workspace_files_client.go b/libs/filer/workspace_files_client.go index d8ab5a6b..4bb03aea 100644 --- a/libs/filer/workspace_files_client.go +++ b/libs/filer/workspace_files_client.go @@ -178,6 +178,9 @@ func (w *workspaceFilesClient) Write(ctx context.Context, name string, reader io // Create parent directory. err = w.workspaceClient.Workspace.MkdirsByPath(ctx, path.Dir(absPath)) if err != nil { + if errors.As(err, &aerr) && aerr.StatusCode == http.StatusForbidden { + return PermissionError{absPath} + } return fmt.Errorf("unable to mkdir to write file %s: %w", absPath, err) } @@ -203,6 +206,11 @@ func (w *workspaceFilesClient) Write(ctx context.Context, name string, reader io return FileAlreadyExistsError{absPath} } + // This API returns StatusForbidden when you have read access but don't have write access to a file + if aerr.StatusCode == http.StatusForbidden { + return PermissionError{absPath} + } + return err } @@ -295,11 +303,11 @@ func (w *workspaceFilesClient) ReadDir(ctx context.Context, name string) ([]fs.D return nil, err } - // This API returns a 404 if the specified path does not exist. + // NOTE: This API returns a 404 if the specified path does not exist, + // but can also do so if we don't have read access. if aerr.StatusCode == http.StatusNotFound { return nil, NoSuchDirectoryError{path.Dir(absPath)} } - return nil, err } diff --git a/libs/set/set.go b/libs/set/set.go index 4798ed09..4b6bc876 100644 --- a/libs/set/set.go +++ b/libs/set/set.go @@ -14,6 +14,11 @@ type Set[T any] struct { data map[string]T } +// Values returns a slice of the set's values +func (s *Set[T]) Values() []T { + return maps.Values(s.data) +} + // NewSetFromF initialise a new set with initial values and a hash function // to define uniqueness of value func NewSetFromF[T any](values []T, f hashFunc[T]) *Set[T] { @@ -69,6 +74,11 @@ func (s *Set[T]) Has(item T) bool { return ok } +// Size returns the number of elements in the set +func (s *Set[T]) Size() int { + return len(s.data) +} + // Returns an iterable slice of values from set func (s *Set[T]) Iter() []T { return maps.Values(s.data) From 3270afaff43e4ad7e7998a67060d9ce8539e7e98 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Thu, 10 Oct 2024 15:02:25 +0200 Subject: [PATCH 14/24] Move utility functions dealing with IAM to libs/iamutil (#1820) ## Changes The two functions `GetShortUserName` and `IsServicePrincipal` are unrelated to auth or the purpose of the auth package. This change moves them into their own package and updates `IsServicePrincipal` to take an `*iam.User` argument instead of a string username. ## Tests Tests pass. --- bundle/config/mutator/populate_current_user.go | 4 ++-- bundle/config/mutator/process_target_mode.go | 4 ++-- bundle/permissions/permission_report.go | 13 +++++++------ internal/init_test.go | 6 +++--- libs/{auth => iamutil}/service_principal.go | 9 +++++---- libs/{auth => iamutil}/service_principal_test.go | 15 ++++++++++----- libs/{auth => iamutil}/user.go | 4 ++-- libs/{auth => iamutil}/user_test.go | 2 +- libs/template/helpers.go | 6 +++--- 9 files changed, 35 insertions(+), 28 deletions(-) rename libs/{auth => iamutil}/service_principal.go (61%) rename libs/{auth => iamutil}/service_principal_test.go (57%) rename libs/{auth => iamutil}/user.go (87%) rename libs/{auth => iamutil}/user_test.go (99%) diff --git a/bundle/config/mutator/populate_current_user.go b/bundle/config/mutator/populate_current_user.go index 1e99b327..cab5db1b 100644 --- a/bundle/config/mutator/populate_current_user.go +++ b/bundle/config/mutator/populate_current_user.go @@ -5,8 +5,8 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config" - "github.com/databricks/cli/libs/auth" "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/iamutil" "github.com/databricks/cli/libs/tags" ) @@ -33,7 +33,7 @@ func (m *populateCurrentUser) Apply(ctx context.Context, b *bundle.Bundle) diag. } b.Config.Workspace.CurrentUser = &config.User{ - ShortName: auth.GetShortUserName(me), + ShortName: iamutil.GetShortUserName(me), User: me, } diff --git a/bundle/config/mutator/process_target_mode.go b/bundle/config/mutator/process_target_mode.go index 9944f6ff..44b53681 100644 --- a/bundle/config/mutator/process_target_mode.go +++ b/bundle/config/mutator/process_target_mode.go @@ -6,9 +6,9 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config" - "github.com/databricks/cli/libs/auth" "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/dyn" + "github.com/databricks/cli/libs/iamutil" "github.com/databricks/cli/libs/log" ) @@ -174,7 +174,7 @@ func (m *processTargetMode) Apply(ctx context.Context, b *bundle.Bundle) diag.Di transformDevelopmentMode(ctx, b) return diags case config.Production: - isPrincipal := auth.IsServicePrincipal(b.Config.Workspace.CurrentUser.UserName) + isPrincipal := iamutil.IsServicePrincipal(b.Config.Workspace.CurrentUser.User) return validateProductionMode(ctx, b, isPrincipal) case "": // No action diff --git a/bundle/permissions/permission_report.go b/bundle/permissions/permission_report.go index a9468edb..36526eee 100644 --- a/bundle/permissions/permission_report.go +++ b/bundle/permissions/permission_report.go @@ -5,8 +5,8 @@ import ( "fmt" "github.com/databricks/cli/bundle" - "github.com/databricks/cli/libs/auth" "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/iamutil" "github.com/databricks/cli/libs/log" ) @@ -17,9 +17,10 @@ import ( func ReportPossiblePermissionDenied(ctx context.Context, b *bundle.Bundle, path string) diag.Diagnostics { log.Errorf(ctx, "Failed to update, encountered possible permission error: %v", path) - user := b.Config.Workspace.CurrentUser.UserName - if auth.IsServicePrincipal(user) { - user = b.Config.Workspace.CurrentUser.DisplayName + me := b.Config.Workspace.CurrentUser.User + userName := me.UserName + if iamutil.IsServicePrincipal(me) { + userName = me.DisplayName } canManageBundle, assistance := analyzeBundlePermissions(b) @@ -30,7 +31,7 @@ func ReportPossiblePermissionDenied(ctx context.Context, b *bundle.Bundle, path "%s\n"+ "They may need to redeploy the bundle to apply the new permissions.\n"+ "Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions.", - path, user, assistance), + path, userName, assistance), Severity: diag.Error, ID: diag.PathPermissionDenied, }} @@ -44,7 +45,7 @@ func ReportPossiblePermissionDenied(ctx context.Context, b *bundle.Bundle, path "%s\n"+ "They can redeploy the project to apply the latest set of permissions.\n"+ "Please refer to https://docs.databricks.com/dev-tools/bundles/permissions.html for more on managing permissions.", - path, user, assistance), + path, userName, assistance), Severity: diag.Error, ID: diag.CannotChangePathPermissions, }} diff --git a/internal/init_test.go b/internal/init_test.go index d1a89f7b..a6241d62 100644 --- a/internal/init_test.go +++ b/internal/init_test.go @@ -12,7 +12,7 @@ import ( "github.com/databricks/cli/bundle/config" "github.com/databricks/cli/internal/testutil" - "github.com/databricks/cli/libs/auth" + "github.com/databricks/cli/libs/iamutil" "github.com/databricks/databricks-sdk-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -126,7 +126,7 @@ func TestAccBundleInitHelpers(t *testing.T) { }{ { funcName: "{{short_name}}", - expected: auth.GetShortUserName(me), + expected: iamutil.GetShortUserName(me), }, { funcName: "{{user_name}}", @@ -138,7 +138,7 @@ func TestAccBundleInitHelpers(t *testing.T) { }, { funcName: "{{is_service_principal}}", - expected: strconv.FormatBool(auth.IsServicePrincipal(me.UserName)), + expected: strconv.FormatBool(iamutil.IsServicePrincipal(me)), }, { funcName: "{{smallest_node_type}}", diff --git a/libs/auth/service_principal.go b/libs/iamutil/service_principal.go similarity index 61% rename from libs/auth/service_principal.go rename to libs/iamutil/service_principal.go index 5f1854e3..7b65f1f5 100644 --- a/libs/auth/service_principal.go +++ b/libs/iamutil/service_principal.go @@ -1,15 +1,16 @@ -package auth +package iamutil import ( + "github.com/databricks/databricks-sdk-go/service/iam" "github.com/google/uuid" ) -// Determines whether a given user name is a service principal. +// Determines whether a given user is a service principal. // This function uses a heuristic: if the user name is a UUID, then we assume // it's a service principal. Unfortunately, the service principal listing API is too // slow for our purposes. And the "users" and "service principals get" APIs // only allow access by workspace admins. -func IsServicePrincipal(userName string) bool { - _, err := uuid.Parse(userName) +func IsServicePrincipal(user *iam.User) bool { + _, err := uuid.Parse(user.UserName) return err == nil } diff --git a/libs/auth/service_principal_test.go b/libs/iamutil/service_principal_test.go similarity index 57% rename from libs/auth/service_principal_test.go rename to libs/iamutil/service_principal_test.go index 95e8ab5c..07e07669 100644 --- a/libs/auth/service_principal_test.go +++ b/libs/iamutil/service_principal_test.go @@ -1,19 +1,24 @@ -package auth +package iamutil import ( "testing" + "github.com/databricks/databricks-sdk-go/service/iam" "github.com/stretchr/testify/assert" ) func TestIsServicePrincipal_ValidUUID(t *testing.T) { - userId := "8b948b2e-d2b5-4b9e-8274-11b596f3b652" - isSP := IsServicePrincipal(userId) + user := &iam.User{ + UserName: "8b948b2e-d2b5-4b9e-8274-11b596f3b652", + } + isSP := IsServicePrincipal(user) assert.True(t, isSP, "Expected user ID to be recognized as a service principal") } func TestIsServicePrincipal_InvalidUUID(t *testing.T) { - userId := "invalid" - isSP := IsServicePrincipal(userId) + user := &iam.User{ + UserName: "invalid", + } + isSP := IsServicePrincipal(user) assert.False(t, isSP, "Expected user ID to not be recognized as a service principal") } diff --git a/libs/auth/user.go b/libs/iamutil/user.go similarity index 87% rename from libs/auth/user.go rename to libs/iamutil/user.go index c6aa974f..53704dab 100644 --- a/libs/auth/user.go +++ b/libs/iamutil/user.go @@ -1,4 +1,4 @@ -package auth +package iamutil import ( "strings" @@ -12,7 +12,7 @@ import ( // including dots, which are not supported in e.g. experiment names. func GetShortUserName(user *iam.User) string { name := user.UserName - if IsServicePrincipal(user.UserName) && user.DisplayName != "" { + if IsServicePrincipal(user) && user.DisplayName != "" { name = user.DisplayName } local, _, _ := strings.Cut(name, "@") diff --git a/libs/auth/user_test.go b/libs/iamutil/user_test.go similarity index 99% rename from libs/auth/user_test.go rename to libs/iamutil/user_test.go index 24b61464..8aa863e6 100644 --- a/libs/auth/user_test.go +++ b/libs/iamutil/user_test.go @@ -1,4 +1,4 @@ -package auth +package iamutil import ( "testing" diff --git a/libs/template/helpers.go b/libs/template/helpers.go index 88c73cc4..d00d75ed 100644 --- a/libs/template/helpers.go +++ b/libs/template/helpers.go @@ -11,7 +11,7 @@ import ( "text/template" "github.com/databricks/cli/cmd/root" - "github.com/databricks/cli/libs/auth" + "github.com/databricks/cli/libs/iamutil" "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/databricks-sdk-go/service/iam" @@ -119,7 +119,7 @@ func loadHelpers(ctx context.Context) template.FuncMap { return "", err } } - return auth.GetShortUserName(cachedUser), nil + return iamutil.GetShortUserName(cachedUser), nil }, // Get the default workspace catalog. If there is no default, or if // Unity Catalog is not enabled, return an empty string. @@ -151,7 +151,7 @@ func loadHelpers(ctx context.Context) template.FuncMap { return false, err } } - result := auth.IsServicePrincipal(cachedUser.UserName) + result := iamutil.IsServicePrincipal(cachedUser) cachedIsServicePrincipal = &result return result, nil }, From b9085de53388cffc12c325653f05eeb16c7dc526 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Thu, 10 Oct 2024 15:43:21 +0200 Subject: [PATCH 15/24] Remove unused `IS_OWNER` constant (#1823) ## Changes Leftover from #1386. ## Tests All tests pass (indicating it really wasn't used). --- bundle/permissions/mutator.go | 1 - 1 file changed, 1 deletion(-) diff --git a/bundle/permissions/mutator.go b/bundle/permissions/mutator.go index 247ffcda..7787bc04 100644 --- a/bundle/permissions/mutator.go +++ b/bundle/permissions/mutator.go @@ -13,7 +13,6 @@ import ( const CAN_MANAGE = "CAN_MANAGE" const CAN_VIEW = "CAN_VIEW" const CAN_RUN = "CAN_RUN" -const IS_OWNER = "IS_OWNER" var allowedLevels = []string{CAN_MANAGE, CAN_VIEW, CAN_RUN} var levelsMap = map[string](map[string]string){ From 845d23ac21b7a945714150ef68d48e6629a8bc6f Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Thu, 10 Oct 2024 16:10:16 +0200 Subject: [PATCH 16/24] Fixed typo in converting cluster permissions (#1826) ## Changes Fixed typo in converting cluster permissions --- bundle/deploy/terraform/tfdyn/convert_cluster.go | 2 +- bundle/deploy/terraform/tfdyn/convert_cluster_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bundle/deploy/terraform/tfdyn/convert_cluster.go b/bundle/deploy/terraform/tfdyn/convert_cluster.go index f25f09ea..18819c00 100644 --- a/bundle/deploy/terraform/tfdyn/convert_cluster.go +++ b/bundle/deploy/terraform/tfdyn/convert_cluster.go @@ -40,7 +40,7 @@ func (clusterConverter) Convert(ctx context.Context, key string, vin dyn.Value, // Configure permissions for this resource. if permissions := convertPermissionsResource(ctx, vin); permissions != nil { - permissions.JobId = fmt.Sprintf("${databricks_cluster.%s.id}", key) + permissions.ClusterId = fmt.Sprintf("${databricks_cluster.%s.id}", key) out.Permissions["cluster_"+key] = permissions } diff --git a/bundle/deploy/terraform/tfdyn/convert_cluster_test.go b/bundle/deploy/terraform/tfdyn/convert_cluster_test.go index e7d2542f..e6d2620c 100644 --- a/bundle/deploy/terraform/tfdyn/convert_cluster_test.go +++ b/bundle/deploy/terraform/tfdyn/convert_cluster_test.go @@ -81,7 +81,7 @@ func TestConvertCluster(t *testing.T) { // Assert equality on the permissions assert.Equal(t, &schema.ResourcePermissions{ - JobId: "${databricks_cluster.my_cluster.id}", + ClusterId: "${databricks_cluster.my_cluster.id}", AccessControl: []schema.ResourcePermissionsAccessControl{ { PermissionLevel: "CAN_RUN", From 08a0d083c3bafb6cd5f87f4d39f0863417c71e2b Mon Sep 17 00:00:00 2001 From: "Lennart Kats (databricks)" Date: Fri, 11 Oct 2024 14:28:56 +0200 Subject: [PATCH 17/24] Ignore metastore permission error during template generation (#1819) ## Changes This extends the `{{default_catalog}}` helper in templates to ignore any `PERMISSION_DENIED` error. We're still reviewing when exactly this error occurs, but if it does, it should not break templates. We should fall back to assuming there's no default catalog (and no UC) instead. ## Testing I have not been able to reproduce this issue, but there is a customer report about "access denied to clusters that don't have unity catalog enabled" being returned on a non-UC workspace. The error code in this PR corresponds to that message. ## Next steps We'll work together with the UC team to review if this error even makes sense for this API. If that discussion leads to a behavior change in the API we can update the CLI code again. --- libs/template/helpers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/template/helpers.go b/libs/template/helpers.go index d00d75ed..f25cbee4 100644 --- a/libs/template/helpers.go +++ b/libs/template/helpers.go @@ -128,8 +128,8 @@ func loadHelpers(ctx context.Context) template.FuncMap { metastore, err := w.Metastores.Current(ctx) if err != nil { var aerr *apierr.APIError - if errors.As(err, &aerr) && aerr.ErrorCode == "METASTORE_DOES_NOT_EXIST" { - // Workspace doesn't have a metastore assigned, ignore error + if errors.As(err, &aerr) && (aerr.ErrorCode == "PERMISSION_DENIED" || aerr.ErrorCode == "METASTORE_DOES_NOT_EXIST") { + // Ignore: access denied or workspace doesn't have a metastore assigned empty_default := "" cachedCatalog = &empty_default return "", nil From f0e2981596488af1af3fbbdbd9572a41bfaae4e8 Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Fri, 11 Oct 2024 16:39:53 +0200 Subject: [PATCH 18/24] Added JSON input validation for CLI commands (#1771) ## Changes Added JSON input validation for CLI commands. Now when invalid JSON passed as a payload to CLI commands, CLI performs input normalisation and detects if there are any mismatches such as incorrect types, unknown fields and etc. This diagnostic information is printed in standard error output and does not block command execution, so the change is backward compatible. Fixes #1769 #1764 #1625 #1560 ## Tests Added unit tests ``` andrew.nester@HFW9Y94129 ~ % databricks jobs create --json '{"seeti}' Error: error decoding JSON at (inline):1:2: unexpected EOF andrew.nester@HFW9Y94129 ~ % databricks jobs create --json '{"seeti": true}' Warning: unknown field: seeti in (inline):1:9 Error: Job settings must be specified. ``` --------- Co-authored-by: Pieter Noordhuis --- .codegen/service.go.tmpl | 13 +- bundle/render/render_text_output.go | 66 +---- cmd/account/access-control/access-control.go | 12 +- cmd/account/budgets/budgets.go | 24 +- cmd/account/credentials/credentials.go | 12 +- .../csp-enablement-account.go | 12 +- .../custom-app-integration.go | 24 +- .../disable-legacy-features.go | 12 +- .../encryption-keys/encryption-keys.go | 12 +- .../esm-enablement-account.go | 12 +- cmd/account/groups/groups.go | 36 ++- .../ip-access-lists/ip-access-lists.go | 36 ++- cmd/account/log-delivery/log-delivery.go | 24 +- .../metastore-assignments.go | 24 +- cmd/account/metastores/metastores.go | 24 +- .../network-connectivity.go | 24 +- cmd/account/networks/networks.go | 12 +- .../personal-compute/personal-compute.go | 12 +- cmd/account/private-access/private-access.go | 24 +- .../published-app-integration.go | 24 +- .../service-principals/service-principals.go | 36 ++- .../storage-credentials.go | 24 +- cmd/account/storage/storage.go | 12 +- .../usage-dashboards/usage-dashboards.go | 12 +- cmd/account/users/users.go | 36 ++- cmd/account/vpc-endpoints/vpc-endpoints.go | 12 +- .../workspace-assignment.go | 12 +- cmd/account/workspaces/workspaces.go | 24 +- cmd/api/api.go | 6 +- cmd/workspace/alerts-legacy/alerts-legacy.go | 24 +- cmd/workspace/alerts/alerts.go | 24 +- cmd/workspace/apps/apps.go | 60 +++- .../artifact-allowlists.go | 12 +- .../automatic-cluster-update.go | 12 +- cmd/workspace/catalogs/catalogs.go | 24 +- cmd/workspace/clean-rooms/clean-rooms.go | 24 +- .../cluster-policies/cluster-policies.go | 60 +++- cmd/workspace/clusters/clusters.go | 168 ++++++++--- .../compliance-security-profile.go | 12 +- cmd/workspace/connections/connections.go | 24 +- .../consumer-installations.go | 24 +- .../consumer-personalization-requests.go | 12 +- .../credentials-manager.go | 12 +- .../dashboard-widgets/dashboard-widgets.go | 24 +- cmd/workspace/dashboards/dashboards.go | 24 +- .../default-namespace/default-namespace.go | 12 +- .../disable-legacy-access.go | 12 +- .../enhanced-security-monitoring.go | 12 +- cmd/workspace/experiments/experiments.go | 264 +++++++++++++----- .../external-locations/external-locations.go | 24 +- cmd/workspace/functions/functions.go | 24 +- cmd/workspace/genie/genie.go | 24 +- .../git-credentials/git-credentials.go | 24 +- .../global-init-scripts.go | 24 +- cmd/workspace/grants/grants.go | 12 +- cmd/workspace/groups/groups.go | 36 ++- .../instance-pools/instance-pools.go | 60 +++- .../instance-profiles/instance-profiles.go | 36 ++- .../ip-access-lists/ip-access-lists.go | 36 ++- cmd/workspace/jobs/jobs.go | 144 +++++++--- cmd/workspace/lakeview/lakeview.go | 84 ++++-- cmd/workspace/libraries/libraries.go | 24 +- cmd/workspace/metastores/metastores.go | 48 +++- .../model-registry/model-registry.go | 228 +++++++++++---- .../model-versions/model-versions.go | 12 +- .../notification-destinations.go | 24 +- cmd/workspace/online-tables/online-tables.go | 12 +- .../permission-migration.go | 12 +- cmd/workspace/permissions/permissions.go | 24 +- cmd/workspace/pipelines/pipelines.go | 60 +++- .../policy-compliance-for-clusters.go | 12 +- .../policy-compliance-for-jobs.go | 12 +- .../provider-exchange-filters.go | 24 +- .../provider-exchanges/provider-exchanges.go | 36 ++- .../provider-files/provider-files.go | 24 +- .../provider-listings/provider-listings.go | 24 +- .../provider-personalization-requests.go | 12 +- .../provider-provider-analytics-dashboards.go | 12 +- .../provider-providers/provider-providers.go | 24 +- cmd/workspace/providers/providers.go | 24 +- .../quality-monitors/quality-monitors.go | 36 ++- .../queries-legacy/queries-legacy.go | 24 +- cmd/workspace/queries/queries.go | 24 +- .../query-visualizations-legacy.go | 24 +- .../query-visualizations.go | 24 +- cmd/workspace/recipients/recipients.go | 36 ++- .../registered-models/registered-models.go | 36 ++- cmd/workspace/repos/overrides.go | 26 +- cmd/workspace/repos/repos.go | 48 +++- .../restrict-workspace-admins.go | 12 +- cmd/workspace/schemas/schemas.go | 24 +- cmd/workspace/secrets/put_secret.go | 14 +- cmd/workspace/secrets/secrets.go | 60 +++- .../service-principals/service-principals.go | 36 ++- .../serving-endpoints/serving-endpoints.go | 96 +++++-- cmd/workspace/shares/shares.go | 36 ++- .../storage-credentials.go | 36 ++- .../table-constraints/table-constraints.go | 12 +- cmd/workspace/tables/tables.go | 12 +- .../temporary-table-credentials.go | 12 +- .../token-management/token-management.go | 36 ++- cmd/workspace/tokens/tokens.go | 24 +- cmd/workspace/users/users.go | 60 +++- .../vector-search-endpoints.go | 12 +- .../vector-search-indexes.go | 72 +++-- cmd/workspace/volumes/volumes.go | 24 +- cmd/workspace/warehouses/warehouses.go | 60 +++- .../workspace-bindings/workspace-bindings.go | 24 +- .../workspace-conf/workspace-conf.go | 12 +- cmd/workspace/workspace/workspace.go | 60 +++- libs/cmdio/render.go | 201 +++++++++---- libs/dyn/convert/to_typed.go | 17 +- libs/dyn/convert/to_typed_test.go | 17 +- libs/dyn/jsonloader/json.go | 107 +++++++ libs/dyn/jsonloader/json_test.go | 93 ++++++ libs/dyn/jsonloader/locations.go | 53 ++++ libs/flags/json_flag.go | 47 +++- libs/flags/json_flag_test.go | 244 +++++++++++++++- 118 files changed, 3378 insertions(+), 1042 deletions(-) create mode 100644 libs/dyn/jsonloader/json.go create mode 100644 libs/dyn/jsonloader/json_test.go create mode 100644 libs/dyn/jsonloader/locations.go diff --git a/.codegen/service.go.tmpl b/.codegen/service.go.tmpl index 281dfd6e..b489a0b0 100644 --- a/.codegen/service.go.tmpl +++ b/.codegen/service.go.tmpl @@ -5,6 +5,7 @@ package {{(.TrimPrefix "account").SnakeName}} import ( "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/flags" + "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/cmd/root" "github.com/databricks/databricks-sdk-go/service/{{.Package.Name}}" "github.com/spf13/cobra" @@ -231,9 +232,15 @@ func new{{.PascalName}}() *cobra.Command { {{- if .Request }} {{ if .CanUseJson }} if cmd.Flags().Changed("json") { - err = {{.CamelName}}Json.Unmarshal(&{{.CamelName}}Req) - if err != nil { - return err + diags := {{.CamelName}}Json.Unmarshal(&{{.CamelName}}Req) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } }{{end}}{{ if .MustUseJson }}else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/bundle/render/render_text_output.go b/bundle/render/render_text_output.go index 56387c38..3e52d5f1 100644 --- a/bundle/render/render_text_output.go +++ b/bundle/render/render_text_output.go @@ -8,6 +8,7 @@ import ( "text/template" "github.com/databricks/cli/bundle" + "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/diag" "github.com/databricks/databricks-sdk-go/service/iam" "github.com/fatih/color" @@ -28,48 +29,6 @@ var renderFuncMap = template.FuncMap{ }, } -const errorTemplate = `{{ "Error" | red }}: {{ .Summary }} -{{- range $index, $element := .Paths }} - {{ if eq $index 0 }}at {{else}} {{ end}}{{ $element.String | green }} -{{- end }} -{{- range $index, $element := .Locations }} - {{ if eq $index 0 }}in {{else}} {{ end}}{{ $element.String | cyan }} -{{- end }} -{{- if .Detail }} - -{{ .Detail }} -{{- end }} - -` - -const warningTemplate = `{{ "Warning" | yellow }}: {{ .Summary }} -{{- range $index, $element := .Paths }} - {{ if eq $index 0 }}at {{else}} {{ end}}{{ $element.String | green }} -{{- end }} -{{- range $index, $element := .Locations }} - {{ if eq $index 0 }}in {{else}} {{ end}}{{ $element.String | cyan }} -{{- end }} -{{- if .Detail }} - -{{ .Detail }} -{{- end }} - -` - -const recommendationTemplate = `{{ "Recommendation" | blue }}: {{ .Summary }} -{{- range $index, $element := .Paths }} - {{ if eq $index 0 }}at {{else}} {{ end}}{{ $element.String | green }} -{{- end }} -{{- range $index, $element := .Locations }} - {{ if eq $index 0 }}in {{else}} {{ end}}{{ $element.String | cyan }} -{{- end }} -{{- if .Detail }} - -{{ .Detail }} -{{- end }} - -` - const summaryTemplate = `{{- if .Name -}} Name: {{ .Name | bold }} {{- if .Target }} @@ -153,22 +112,7 @@ func renderSummaryTemplate(out io.Writer, b *bundle.Bundle, diags diag.Diagnosti } func renderDiagnostics(out io.Writer, b *bundle.Bundle, diags diag.Diagnostics) error { - errorT := template.Must(template.New("error").Funcs(renderFuncMap).Parse(errorTemplate)) - warningT := template.Must(template.New("warning").Funcs(renderFuncMap).Parse(warningTemplate)) - recommendationT := template.Must(template.New("recommendation").Funcs(renderFuncMap).Parse(recommendationTemplate)) - - // Print errors and warnings. for _, d := range diags { - var t *template.Template - switch d.Severity { - case diag.Error: - t = errorT - case diag.Warning: - t = warningT - case diag.Recommendation: - t = recommendationT - } - for i := range d.Locations { if b == nil { break @@ -183,15 +127,9 @@ func renderDiagnostics(out io.Writer, b *bundle.Bundle, diags diag.Diagnostics) } } } - - // Render the diagnostic with the appropriate template. - err := t.Execute(out, d) - if err != nil { - return fmt.Errorf("failed to render template: %w", err) - } } - return nil + return cmdio.RenderDiagnostics(out, diags) } // RenderOptions contains options for rendering diagnostics. diff --git a/cmd/account/access-control/access-control.go b/cmd/account/access-control/access-control.go index f6761a1b..07a7fce9 100755 --- a/cmd/account/access-control/access-control.go +++ b/cmd/account/access-control/access-control.go @@ -205,9 +205,15 @@ func newUpdateRuleSet() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = updateRuleSetJson.Unmarshal(&updateRuleSetReq) - if err != nil { - return err + diags := updateRuleSetJson.Unmarshal(&updateRuleSetReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/account/budgets/budgets.go b/cmd/account/budgets/budgets.go index 6b47bb32..87ed41d5 100755 --- a/cmd/account/budgets/budgets.go +++ b/cmd/account/budgets/budgets.go @@ -78,9 +78,15 @@ func newCreate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -316,9 +322,15 @@ func newUpdate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/account/credentials/credentials.go b/cmd/account/credentials/credentials.go index ed071cda..4ab70ef0 100755 --- a/cmd/account/credentials/credentials.go +++ b/cmd/account/credentials/credentials.go @@ -90,9 +90,15 @@ func newCreate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/account/csp-enablement-account/csp-enablement-account.go b/cmd/account/csp-enablement-account/csp-enablement-account.go index d6fce953..9fb94a61 100755 --- a/cmd/account/csp-enablement-account/csp-enablement-account.go +++ b/cmd/account/csp-enablement-account/csp-enablement-account.go @@ -129,9 +129,15 @@ func newUpdate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/account/custom-app-integration/custom-app-integration.go b/cmd/account/custom-app-integration/custom-app-integration.go index 5cdf422d..9d16a44d 100755 --- a/cmd/account/custom-app-integration/custom-app-integration.go +++ b/cmd/account/custom-app-integration/custom-app-integration.go @@ -88,9 +88,15 @@ func newCreate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -320,9 +326,15 @@ func newUpdate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updateReq.IntegrationId = args[0] diff --git a/cmd/account/disable-legacy-features/disable-legacy-features.go b/cmd/account/disable-legacy-features/disable-legacy-features.go index 6d25b943..5e732b87 100755 --- a/cmd/account/disable-legacy-features/disable-legacy-features.go +++ b/cmd/account/disable-legacy-features/disable-legacy-features.go @@ -185,9 +185,15 @@ func newUpdate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/account/encryption-keys/encryption-keys.go b/cmd/account/encryption-keys/encryption-keys.go index 44545ccf..3f7082cd 100755 --- a/cmd/account/encryption-keys/encryption-keys.go +++ b/cmd/account/encryption-keys/encryption-keys.go @@ -107,9 +107,15 @@ func newCreate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/account/esm-enablement-account/esm-enablement-account.go b/cmd/account/esm-enablement-account/esm-enablement-account.go index 71149e5a..1f820e3a 100755 --- a/cmd/account/esm-enablement-account/esm-enablement-account.go +++ b/cmd/account/esm-enablement-account/esm-enablement-account.go @@ -127,9 +127,15 @@ func newUpdate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/account/groups/groups.go b/cmd/account/groups/groups.go index a7e1ac43..7c2e8331 100755 --- a/cmd/account/groups/groups.go +++ b/cmd/account/groups/groups.go @@ -97,9 +97,15 @@ func newCreate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -358,9 +364,15 @@ func newPatch() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = patchJson.Unmarshal(&patchReq) - if err != nil { - return err + diags := patchJson.Unmarshal(&patchReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { @@ -446,9 +458,15 @@ func newUpdate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/account/ip-access-lists/ip-access-lists.go b/cmd/account/ip-access-lists/ip-access-lists.go index 5c6d27dd..61fa944a 100755 --- a/cmd/account/ip-access-lists/ip-access-lists.go +++ b/cmd/account/ip-access-lists/ip-access-lists.go @@ -132,9 +132,15 @@ func newCreate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -411,9 +417,15 @@ func newReplace() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = replaceJson.Unmarshal(&replaceReq) - if err != nil { - return err + diags := replaceJson.Unmarshal(&replaceReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } replaceReq.IpAccessListId = args[0] @@ -505,9 +517,15 @@ func newUpdate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/account/log-delivery/log-delivery.go b/cmd/account/log-delivery/log-delivery.go index 4584f4d2..cb083f3b 100755 --- a/cmd/account/log-delivery/log-delivery.go +++ b/cmd/account/log-delivery/log-delivery.go @@ -162,9 +162,15 @@ func newCreate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -369,9 +375,15 @@ func newPatchStatus() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = patchStatusJson.Unmarshal(&patchStatusReq) - if err != nil { - return err + diags := patchStatusJson.Unmarshal(&patchStatusReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } patchStatusReq.LogDeliveryConfigurationId = args[0] diff --git a/cmd/account/metastore-assignments/metastore-assignments.go b/cmd/account/metastore-assignments/metastore-assignments.go index d7f32ccb..9d249075 100755 --- a/cmd/account/metastore-assignments/metastore-assignments.go +++ b/cmd/account/metastore-assignments/metastore-assignments.go @@ -85,9 +85,15 @@ func newCreate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } _, err = fmt.Sscan(args[0], &createReq.WorkspaceId) @@ -343,9 +349,15 @@ func newUpdate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } _, err = fmt.Sscan(args[0], &updateReq.WorkspaceId) diff --git a/cmd/account/metastores/metastores.go b/cmd/account/metastores/metastores.go index 7c8e3f2c..eb575578 100755 --- a/cmd/account/metastores/metastores.go +++ b/cmd/account/metastores/metastores.go @@ -80,9 +80,15 @@ func newCreate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -304,9 +310,15 @@ func newUpdate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updateReq.MetastoreId = args[0] diff --git a/cmd/account/network-connectivity/network-connectivity.go b/cmd/account/network-connectivity/network-connectivity.go index cd8da290..168de9c1 100755 --- a/cmd/account/network-connectivity/network-connectivity.go +++ b/cmd/account/network-connectivity/network-connectivity.go @@ -96,9 +96,15 @@ func newCreateNetworkConnectivityConfiguration() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = createNetworkConnectivityConfigurationJson.Unmarshal(&createNetworkConnectivityConfigurationReq) - if err != nil { - return err + diags := createNetworkConnectivityConfigurationJson.Unmarshal(&createNetworkConnectivityConfigurationReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -187,9 +193,15 @@ func newCreatePrivateEndpointRule() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = createPrivateEndpointRuleJson.Unmarshal(&createPrivateEndpointRuleReq) - if err != nil { - return err + diags := createPrivateEndpointRuleJson.Unmarshal(&createPrivateEndpointRuleReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } createPrivateEndpointRuleReq.NetworkConnectivityConfigId = args[0] diff --git a/cmd/account/networks/networks.go b/cmd/account/networks/networks.go index 05ef0c81..086d24be 100755 --- a/cmd/account/networks/networks.go +++ b/cmd/account/networks/networks.go @@ -97,9 +97,15 @@ func newCreate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { diff --git a/cmd/account/personal-compute/personal-compute.go b/cmd/account/personal-compute/personal-compute.go index 2a14b0b3..dac7e2e3 100755 --- a/cmd/account/personal-compute/personal-compute.go +++ b/cmd/account/personal-compute/personal-compute.go @@ -189,9 +189,15 @@ func newUpdate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/account/private-access/private-access.go b/cmd/account/private-access/private-access.go index d527fa64..312f6d02 100755 --- a/cmd/account/private-access/private-access.go +++ b/cmd/account/private-access/private-access.go @@ -109,9 +109,15 @@ func newCreate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -411,9 +417,15 @@ func newReplace() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = replaceJson.Unmarshal(&replaceReq) - if err != nil { - return err + diags := replaceJson.Unmarshal(&replaceReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } replaceReq.PrivateAccessSettingsId = args[0] diff --git a/cmd/account/published-app-integration/published-app-integration.go b/cmd/account/published-app-integration/published-app-integration.go index 5143d53c..c97bcfc6 100755 --- a/cmd/account/published-app-integration/published-app-integration.go +++ b/cmd/account/published-app-integration/published-app-integration.go @@ -85,9 +85,15 @@ func newCreate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -315,9 +321,15 @@ func newUpdate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updateReq.IntegrationId = args[0] diff --git a/cmd/account/service-principals/service-principals.go b/cmd/account/service-principals/service-principals.go index c86810f1..f6ec1fa5 100755 --- a/cmd/account/service-principals/service-principals.go +++ b/cmd/account/service-principals/service-principals.go @@ -95,9 +95,15 @@ func newCreate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -358,9 +364,15 @@ func newPatch() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = patchJson.Unmarshal(&patchReq) - if err != nil { - return err + diags := patchJson.Unmarshal(&patchReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { @@ -448,9 +460,15 @@ func newUpdate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/account/storage-credentials/storage-credentials.go b/cmd/account/storage-credentials/storage-credentials.go index 4280ae8c..b3b8ad36 100755 --- a/cmd/account/storage-credentials/storage-credentials.go +++ b/cmd/account/storage-credentials/storage-credentials.go @@ -88,9 +88,15 @@ func newCreate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } createReq.MetastoreId = args[0] @@ -340,9 +346,15 @@ func newUpdate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updateReq.MetastoreId = args[0] diff --git a/cmd/account/storage/storage.go b/cmd/account/storage/storage.go index 50460ed0..24bb6d80 100755 --- a/cmd/account/storage/storage.go +++ b/cmd/account/storage/storage.go @@ -87,9 +87,15 @@ func newCreate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/account/usage-dashboards/usage-dashboards.go b/cmd/account/usage-dashboards/usage-dashboards.go index 8a1c3247..a482b466 100755 --- a/cmd/account/usage-dashboards/usage-dashboards.go +++ b/cmd/account/usage-dashboards/usage-dashboards.go @@ -80,9 +80,15 @@ func newCreate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } diff --git a/cmd/account/users/users.go b/cmd/account/users/users.go index 289d2972..10990084 100755 --- a/cmd/account/users/users.go +++ b/cmd/account/users/users.go @@ -103,9 +103,15 @@ func newCreate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -374,9 +380,15 @@ func newPatch() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = patchJson.Unmarshal(&patchReq) - if err != nil { - return err + diags := patchJson.Unmarshal(&patchReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { @@ -465,9 +477,15 @@ func newUpdate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/account/vpc-endpoints/vpc-endpoints.go b/cmd/account/vpc-endpoints/vpc-endpoints.go index e6c6c126..c1aab0d2 100755 --- a/cmd/account/vpc-endpoints/vpc-endpoints.go +++ b/cmd/account/vpc-endpoints/vpc-endpoints.go @@ -104,9 +104,15 @@ func newCreate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { diff --git a/cmd/account/workspace-assignment/workspace-assignment.go b/cmd/account/workspace-assignment/workspace-assignment.go index 58468d09..e09095d3 100755 --- a/cmd/account/workspace-assignment/workspace-assignment.go +++ b/cmd/account/workspace-assignment/workspace-assignment.go @@ -273,9 +273,15 @@ func newUpdate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } _, err = fmt.Sscan(args[0], &updateReq.WorkspaceId) diff --git a/cmd/account/workspaces/workspaces.go b/cmd/account/workspaces/workspaces.go index 1ec6230b..82d3d7db 100755 --- a/cmd/account/workspaces/workspaces.go +++ b/cmd/account/workspaces/workspaces.go @@ -133,9 +133,15 @@ func newCreate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -551,9 +557,15 @@ func newUpdate() *cobra.Command { a := root.AccountClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/api/api.go b/cmd/api/api.go index 03460f71..d33939a5 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -42,9 +42,9 @@ func makeCommand(method string) *cobra.Command { var path = args[0] var request any - err := payload.Unmarshal(&request) - if err != nil { - return err + diags := payload.Unmarshal(&request) + if diags.HasError() { + return diags.Error() } cfg := &config.Config{} diff --git a/cmd/workspace/alerts-legacy/alerts-legacy.go b/cmd/workspace/alerts-legacy/alerts-legacy.go index 1046b112..4f7d5f96 100755 --- a/cmd/workspace/alerts-legacy/alerts-legacy.go +++ b/cmd/workspace/alerts-legacy/alerts-legacy.go @@ -93,9 +93,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -357,9 +363,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/alerts/alerts.go b/cmd/workspace/alerts/alerts.go index cfaa3f55..fcf18652 100755 --- a/cmd/workspace/alerts/alerts.go +++ b/cmd/workspace/alerts/alerts.go @@ -85,9 +85,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -354,9 +360,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updateReq.Id = args[0] diff --git a/cmd/workspace/apps/apps.go b/cmd/workspace/apps/apps.go index 780f5594..4cee2f82 100755 --- a/cmd/workspace/apps/apps.go +++ b/cmd/workspace/apps/apps.go @@ -113,9 +113,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -267,9 +273,15 @@ func newDeploy() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = deployJson.Unmarshal(&deployReq) - if err != nil { - return err + diags := deployJson.Unmarshal(&deployReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } deployReq.AppName = args[0] @@ -702,9 +714,15 @@ func newSetPermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = setPermissionsJson.Unmarshal(&setPermissionsReq) - if err != nil { - return err + diags := setPermissionsJson.Unmarshal(&setPermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } setPermissionsReq.AppName = args[0] @@ -936,9 +954,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updateReq.Name = args[0] @@ -1005,9 +1029,15 @@ func newUpdatePermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updatePermissionsJson.Unmarshal(&updatePermissionsReq) - if err != nil { - return err + diags := updatePermissionsJson.Unmarshal(&updatePermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updatePermissionsReq.AppName = args[0] diff --git a/cmd/workspace/artifact-allowlists/artifact-allowlists.go b/cmd/workspace/artifact-allowlists/artifact-allowlists.go index fc25e3cb..e6683369 100755 --- a/cmd/workspace/artifact-allowlists/artifact-allowlists.go +++ b/cmd/workspace/artifact-allowlists/artifact-allowlists.go @@ -145,9 +145,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/automatic-cluster-update/automatic-cluster-update.go b/cmd/workspace/automatic-cluster-update/automatic-cluster-update.go index 2385195b..dca88f3d 100755 --- a/cmd/workspace/automatic-cluster-update/automatic-cluster-update.go +++ b/cmd/workspace/automatic-cluster-update/automatic-cluster-update.go @@ -127,9 +127,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/catalogs/catalogs.go b/cmd/workspace/catalogs/catalogs.go index a17bb007..9294c192 100755 --- a/cmd/workspace/catalogs/catalogs.go +++ b/cmd/workspace/catalogs/catalogs.go @@ -105,9 +105,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -363,9 +369,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updateReq.Name = args[0] diff --git a/cmd/workspace/clean-rooms/clean-rooms.go b/cmd/workspace/clean-rooms/clean-rooms.go index 9466c4b9..72560b84 100755 --- a/cmd/workspace/clean-rooms/clean-rooms.go +++ b/cmd/workspace/clean-rooms/clean-rooms.go @@ -85,9 +85,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -344,9 +350,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updateReq.Name = args[0] diff --git a/cmd/workspace/cluster-policies/cluster-policies.go b/cmd/workspace/cluster-policies/cluster-policies.go index 830d44ca..b34dd53d 100755 --- a/cmd/workspace/cluster-policies/cluster-policies.go +++ b/cmd/workspace/cluster-policies/cluster-policies.go @@ -113,9 +113,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -185,9 +191,15 @@ func newDelete() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = deleteJson.Unmarshal(&deleteReq) - if err != nil { - return err + diags := deleteJson.Unmarshal(&deleteReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { if len(args) == 0 { @@ -284,9 +296,15 @@ func newEdit() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = editJson.Unmarshal(&editReq) - if err != nil { - return err + diags := editJson.Unmarshal(&editReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { if len(args) == 0 { @@ -630,9 +648,15 @@ func newSetPermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = setPermissionsJson.Unmarshal(&setPermissionsReq) - if err != nil { - return err + diags := setPermissionsJson.Unmarshal(&setPermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { @@ -711,9 +735,15 @@ func newUpdatePermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updatePermissionsJson.Unmarshal(&updatePermissionsReq) - if err != nil { - return err + diags := updatePermissionsJson.Unmarshal(&updatePermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/workspace/clusters/clusters.go b/cmd/workspace/clusters/clusters.go index b36102d9..0ed454de 100755 --- a/cmd/workspace/clusters/clusters.go +++ b/cmd/workspace/clusters/clusters.go @@ -134,9 +134,15 @@ func newChangeOwner() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = changeOwnerJson.Unmarshal(&changeOwnerReq) - if err != nil { - return err + diags := changeOwnerJson.Unmarshal(&changeOwnerReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -268,9 +274,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -362,9 +374,15 @@ func newDelete() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = deleteJson.Unmarshal(&deleteReq) - if err != nil { - return err + diags := deleteJson.Unmarshal(&deleteReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { if len(args) == 0 { @@ -519,9 +537,15 @@ func newEdit() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = editJson.Unmarshal(&editReq) - if err != nil { - return err + diags := editJson.Unmarshal(&editReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -617,9 +641,15 @@ func newEvents() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = eventsJson.Unmarshal(&eventsReq) - if err != nil { - return err + diags := eventsJson.Unmarshal(&eventsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { if len(args) == 0 { @@ -1069,9 +1099,15 @@ func newPermanentDelete() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = permanentDeleteJson.Unmarshal(&permanentDeleteReq) - if err != nil { - return err + diags := permanentDeleteJson.Unmarshal(&permanentDeleteReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { if len(args) == 0 { @@ -1161,9 +1197,15 @@ func newPin() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = pinJson.Unmarshal(&pinReq) - if err != nil { - return err + diags := pinJson.Unmarshal(&pinReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { if len(args) == 0 { @@ -1260,9 +1302,15 @@ func newResize() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = resizeJson.Unmarshal(&resizeReq) - if err != nil { - return err + diags := resizeJson.Unmarshal(&resizeReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { if len(args) == 0 { @@ -1370,9 +1418,15 @@ func newRestart() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = restartJson.Unmarshal(&restartReq) - if err != nil { - return err + diags := restartJson.Unmarshal(&restartReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { if len(args) == 0 { @@ -1464,9 +1518,15 @@ func newSetPermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = setPermissionsJson.Unmarshal(&setPermissionsReq) - if err != nil { - return err + diags := setPermissionsJson.Unmarshal(&setPermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { @@ -1608,9 +1668,15 @@ func newStart() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = startJson.Unmarshal(&startReq) - if err != nil { - return err + diags := startJson.Unmarshal(&startReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { if len(args) == 0 { @@ -1712,9 +1778,15 @@ func newUnpin() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = unpinJson.Unmarshal(&unpinReq) - if err != nil { - return err + diags := unpinJson.Unmarshal(&unpinReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { if len(args) == 0 { @@ -1824,9 +1896,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -1905,9 +1983,15 @@ func newUpdatePermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updatePermissionsJson.Unmarshal(&updatePermissionsReq) - if err != nil { - return err + diags := updatePermissionsJson.Unmarshal(&updatePermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/workspace/compliance-security-profile/compliance-security-profile.go b/cmd/workspace/compliance-security-profile/compliance-security-profile.go index a7b45901..58f3edda 100755 --- a/cmd/workspace/compliance-security-profile/compliance-security-profile.go +++ b/cmd/workspace/compliance-security-profile/compliance-security-profile.go @@ -130,9 +130,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/connections/connections.go b/cmd/workspace/connections/connections.go index f76420fb..161f5ab4 100755 --- a/cmd/workspace/connections/connections.go +++ b/cmd/workspace/connections/connections.go @@ -92,9 +92,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -355,9 +361,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/consumer-installations/consumer-installations.go b/cmd/workspace/consumer-installations/consumer-installations.go index 92f61789..1848cb8f 100755 --- a/cmd/workspace/consumer-installations/consumer-installations.go +++ b/cmd/workspace/consumer-installations/consumer-installations.go @@ -86,9 +86,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } createReq.ListingId = args[0] @@ -319,9 +325,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/consumer-personalization-requests/consumer-personalization-requests.go b/cmd/workspace/consumer-personalization-requests/consumer-personalization-requests.go index 8b0af3cc..6d751c63 100755 --- a/cmd/workspace/consumer-personalization-requests/consumer-personalization-requests.go +++ b/cmd/workspace/consumer-personalization-requests/consumer-personalization-requests.go @@ -85,9 +85,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/credentials-manager/credentials-manager.go b/cmd/workspace/credentials-manager/credentials-manager.go index 5a40232b..e29bc0bd 100755 --- a/cmd/workspace/credentials-manager/credentials-manager.go +++ b/cmd/workspace/credentials-manager/credentials-manager.go @@ -75,9 +75,15 @@ func newExchangeToken() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = exchangeTokenJson.Unmarshal(&exchangeTokenReq) - if err != nil { - return err + diags := exchangeTokenJson.Unmarshal(&exchangeTokenReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/dashboard-widgets/dashboard-widgets.go b/cmd/workspace/dashboard-widgets/dashboard-widgets.go index 02b13739..e4281826 100755 --- a/cmd/workspace/dashboard-widgets/dashboard-widgets.go +++ b/cmd/workspace/dashboard-widgets/dashboard-widgets.go @@ -75,9 +75,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -196,9 +202,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/dashboards/dashboards.go b/cmd/workspace/dashboards/dashboards.go index fcab0aa2..5bdd7b13 100755 --- a/cmd/workspace/dashboards/dashboards.go +++ b/cmd/workspace/dashboards/dashboards.go @@ -78,9 +78,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -405,9 +411,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/workspace/default-namespace/default-namespace.go b/cmd/workspace/default-namespace/default-namespace.go index b15907be..e5039cba 100755 --- a/cmd/workspace/default-namespace/default-namespace.go +++ b/cmd/workspace/default-namespace/default-namespace.go @@ -199,9 +199,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/disable-legacy-access/disable-legacy-access.go b/cmd/workspace/disable-legacy-access/disable-legacy-access.go index fea2b3c4..c50de446 100755 --- a/cmd/workspace/disable-legacy-access/disable-legacy-access.go +++ b/cmd/workspace/disable-legacy-access/disable-legacy-access.go @@ -187,9 +187,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/enhanced-security-monitoring/enhanced-security-monitoring.go b/cmd/workspace/enhanced-security-monitoring/enhanced-security-monitoring.go index a8acc5cd..3d99ecef 100755 --- a/cmd/workspace/enhanced-security-monitoring/enhanced-security-monitoring.go +++ b/cmd/workspace/enhanced-security-monitoring/enhanced-security-monitoring.go @@ -132,9 +132,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/experiments/experiments.go b/cmd/workspace/experiments/experiments.go index b1af2f86..4c6b57d1 100755 --- a/cmd/workspace/experiments/experiments.go +++ b/cmd/workspace/experiments/experiments.go @@ -130,9 +130,15 @@ func newCreateExperiment() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createExperimentJson.Unmarshal(&createExperimentReq) - if err != nil { - return err + diags := createExperimentJson.Unmarshal(&createExperimentReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -203,9 +209,15 @@ func newCreateRun() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createRunJson.Unmarshal(&createRunReq) - if err != nil { - return err + diags := createRunJson.Unmarshal(&createRunReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -277,9 +289,15 @@ func newDeleteExperiment() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = deleteExperimentJson.Unmarshal(&deleteExperimentReq) - if err != nil { - return err + diags := deleteExperimentJson.Unmarshal(&deleteExperimentReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -352,9 +370,15 @@ func newDeleteRun() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = deleteRunJson.Unmarshal(&deleteRunReq) - if err != nil { - return err + diags := deleteRunJson.Unmarshal(&deleteRunReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -435,9 +459,15 @@ func newDeleteRuns() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = deleteRunsJson.Unmarshal(&deleteRunsReq) - if err != nil { - return err + diags := deleteRunsJson.Unmarshal(&deleteRunsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -518,9 +548,15 @@ func newDeleteTag() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = deleteTagJson.Unmarshal(&deleteTagReq) - if err != nil { - return err + diags := deleteTagJson.Unmarshal(&deleteTagReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -1108,9 +1144,15 @@ func newLogBatch() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = logBatchJson.Unmarshal(&logBatchReq) - if err != nil { - return err + diags := logBatchJson.Unmarshal(&logBatchReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -1174,9 +1216,15 @@ func newLogInputs() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = logInputsJson.Unmarshal(&logInputsReq) - if err != nil { - return err + diags := logInputsJson.Unmarshal(&logInputsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -1254,9 +1302,15 @@ func newLogMetric() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = logMetricJson.Unmarshal(&logMetricReq) - if err != nil { - return err + diags := logMetricJson.Unmarshal(&logMetricReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -1335,9 +1389,15 @@ func newLogModel() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = logModelJson.Unmarshal(&logModelReq) - if err != nil { - return err + diags := logModelJson.Unmarshal(&logModelReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -1414,9 +1474,15 @@ func newLogParam() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = logParamJson.Unmarshal(&logParamReq) - if err != nil { - return err + diags := logParamJson.Unmarshal(&logParamReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -1497,9 +1563,15 @@ func newRestoreExperiment() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = restoreExperimentJson.Unmarshal(&restoreExperimentReq) - if err != nil { - return err + diags := restoreExperimentJson.Unmarshal(&restoreExperimentReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -1572,9 +1644,15 @@ func newRestoreRun() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = restoreRunJson.Unmarshal(&restoreRunReq) - if err != nil { - return err + diags := restoreRunJson.Unmarshal(&restoreRunReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -1655,9 +1733,15 @@ func newRestoreRuns() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = restoreRunsJson.Unmarshal(&restoreRunsReq) - if err != nil { - return err + diags := restoreRunsJson.Unmarshal(&restoreRunsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -1732,9 +1816,15 @@ func newSearchExperiments() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = searchExperimentsJson.Unmarshal(&searchExperimentsReq) - if err != nil { - return err + diags := searchExperimentsJson.Unmarshal(&searchExperimentsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -1800,9 +1890,15 @@ func newSearchRuns() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = searchRunsJson.Unmarshal(&searchRunsReq) - if err != nil { - return err + diags := searchRunsJson.Unmarshal(&searchRunsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -1874,9 +1970,15 @@ func newSetExperimentTag() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = setExperimentTagJson.Unmarshal(&setExperimentTagReq) - if err != nil { - return err + diags := setExperimentTagJson.Unmarshal(&setExperimentTagReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -1951,9 +2053,15 @@ func newSetPermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = setPermissionsJson.Unmarshal(&setPermissionsReq) - if err != nil { - return err + diags := setPermissionsJson.Unmarshal(&setPermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } setPermissionsReq.ExperimentId = args[0] @@ -2032,9 +2140,15 @@ func newSetTag() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = setTagJson.Unmarshal(&setTagReq) - if err != nil { - return err + diags := setTagJson.Unmarshal(&setTagReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -2112,9 +2226,15 @@ func newUpdateExperiment() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateExperimentJson.Unmarshal(&updateExperimentReq) - if err != nil { - return err + diags := updateExperimentJson.Unmarshal(&updateExperimentReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -2183,9 +2303,15 @@ func newUpdatePermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updatePermissionsJson.Unmarshal(&updatePermissionsReq) - if err != nil { - return err + diags := updatePermissionsJson.Unmarshal(&updatePermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updatePermissionsReq.ExperimentId = args[0] @@ -2251,9 +2377,15 @@ func newUpdateRun() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateRunJson.Unmarshal(&updateRunReq) - if err != nil { - return err + diags := updateRunJson.Unmarshal(&updateRunReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } diff --git a/cmd/workspace/external-locations/external-locations.go b/cmd/workspace/external-locations/external-locations.go index 42493fc4..97d34df0 100755 --- a/cmd/workspace/external-locations/external-locations.go +++ b/cmd/workspace/external-locations/external-locations.go @@ -112,9 +112,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -381,9 +387,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updateReq.Name = args[0] diff --git a/cmd/workspace/functions/functions.go b/cmd/workspace/functions/functions.go index c8de4879..86b29267 100755 --- a/cmd/workspace/functions/functions.go +++ b/cmd/workspace/functions/functions.go @@ -85,9 +85,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -381,9 +387,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/workspace/genie/genie.go b/cmd/workspace/genie/genie.go index e4a05909..287bcde6 100755 --- a/cmd/workspace/genie/genie.go +++ b/cmd/workspace/genie/genie.go @@ -105,9 +105,15 @@ func newCreateMessage() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createMessageJson.Unmarshal(&createMessageReq) - if err != nil { - return err + diags := createMessageJson.Unmarshal(&createMessageReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } createMessageReq.SpaceId = args[0] @@ -392,9 +398,15 @@ func newStartConversation() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = startConversationJson.Unmarshal(&startConversationReq) - if err != nil { - return err + diags := startConversationJson.Unmarshal(&startConversationReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } startConversationReq.SpaceId = args[0] diff --git a/cmd/workspace/git-credentials/git-credentials.go b/cmd/workspace/git-credentials/git-credentials.go index b5082d31..978ca6ba 100755 --- a/cmd/workspace/git-credentials/git-credentials.go +++ b/cmd/workspace/git-credentials/git-credentials.go @@ -103,9 +103,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -371,9 +377,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } _, err = fmt.Sscan(args[0], &updateReq.CredentialId) diff --git a/cmd/workspace/global-init-scripts/global-init-scripts.go b/cmd/workspace/global-init-scripts/global-init-scripts.go index 92dcb259..52adde3f 100755 --- a/cmd/workspace/global-init-scripts/global-init-scripts.go +++ b/cmd/workspace/global-init-scripts/global-init-scripts.go @@ -101,9 +101,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -367,9 +373,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updateReq.ScriptId = args[0] diff --git a/cmd/workspace/grants/grants.go b/cmd/workspace/grants/grants.go index 876f0343..b1dd4e8c 100755 --- a/cmd/workspace/grants/grants.go +++ b/cmd/workspace/grants/grants.go @@ -223,9 +223,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } _, err = fmt.Sscan(args[0], &updateReq.SecurableType) diff --git a/cmd/workspace/groups/groups.go b/cmd/workspace/groups/groups.go index 14650d98..a2d32310 100755 --- a/cmd/workspace/groups/groups.go +++ b/cmd/workspace/groups/groups.go @@ -97,9 +97,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -358,9 +364,15 @@ func newPatch() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = patchJson.Unmarshal(&patchReq) - if err != nil { - return err + diags := patchJson.Unmarshal(&patchReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { @@ -446,9 +458,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/workspace/instance-pools/instance-pools.go b/cmd/workspace/instance-pools/instance-pools.go index db96f146..8a84df94 100755 --- a/cmd/workspace/instance-pools/instance-pools.go +++ b/cmd/workspace/instance-pools/instance-pools.go @@ -128,9 +128,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -206,9 +212,15 @@ func newDelete() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = deleteJson.Unmarshal(&deleteReq) - if err != nil { - return err + diags := deleteJson.Unmarshal(&deleteReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { if len(args) == 0 { @@ -309,9 +321,15 @@ func newEdit() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = editJson.Unmarshal(&editReq) - if err != nil { - return err + diags := editJson.Unmarshal(&editReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -631,9 +649,15 @@ func newSetPermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = setPermissionsJson.Unmarshal(&setPermissionsReq) - if err != nil { - return err + diags := setPermissionsJson.Unmarshal(&setPermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { @@ -712,9 +736,15 @@ func newUpdatePermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updatePermissionsJson.Unmarshal(&updatePermissionsReq) - if err != nil { - return err + diags := updatePermissionsJson.Unmarshal(&updatePermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/workspace/instance-profiles/instance-profiles.go b/cmd/workspace/instance-profiles/instance-profiles.go index 7134c16c..5c4bc8d9 100755 --- a/cmd/workspace/instance-profiles/instance-profiles.go +++ b/cmd/workspace/instance-profiles/instance-profiles.go @@ -99,9 +99,15 @@ func newAdd() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = addJson.Unmarshal(&addReq) - if err != nil { - return err + diags := addJson.Unmarshal(&addReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -192,9 +198,15 @@ func newEdit() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = editJson.Unmarshal(&editReq) - if err != nil { - return err + diags := editJson.Unmarshal(&editReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -311,9 +323,15 @@ func newRemove() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = removeJson.Unmarshal(&removeReq) - if err != nil { - return err + diags := removeJson.Unmarshal(&removeReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { diff --git a/cmd/workspace/ip-access-lists/ip-access-lists.go b/cmd/workspace/ip-access-lists/ip-access-lists.go index ec5958b5..070f279e 100755 --- a/cmd/workspace/ip-access-lists/ip-access-lists.go +++ b/cmd/workspace/ip-access-lists/ip-access-lists.go @@ -133,9 +133,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -414,9 +420,15 @@ func newReplace() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = replaceJson.Unmarshal(&replaceReq) - if err != nil { - return err + diags := replaceJson.Unmarshal(&replaceReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } replaceReq.IpAccessListId = args[0] @@ -510,9 +522,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/workspace/jobs/jobs.go b/cmd/workspace/jobs/jobs.go index 2d422fa8..d4ceb0c2 100755 --- a/cmd/workspace/jobs/jobs.go +++ b/cmd/workspace/jobs/jobs.go @@ -116,9 +116,15 @@ func newCancelAllRuns() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = cancelAllRunsJson.Unmarshal(&cancelAllRunsReq) - if err != nil { - return err + diags := cancelAllRunsJson.Unmarshal(&cancelAllRunsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -193,9 +199,15 @@ func newCancelRun() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = cancelRunJson.Unmarshal(&cancelRunReq) - if err != nil { - return err + diags := cancelRunJson.Unmarshal(&cancelRunReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { if len(args) == 0 { @@ -291,9 +303,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -364,9 +382,15 @@ func newDelete() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = deleteJson.Unmarshal(&deleteReq) - if err != nil { - return err + diags := deleteJson.Unmarshal(&deleteReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { if len(args) == 0 { @@ -457,9 +481,15 @@ func newDeleteRun() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = deleteRunJson.Unmarshal(&deleteRunReq) - if err != nil { - return err + diags := deleteRunJson.Unmarshal(&deleteRunReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { if len(args) == 0 { @@ -1143,9 +1173,15 @@ func newRepairRun() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = repairRunJson.Unmarshal(&repairRunReq) - if err != nil { - return err + diags := repairRunJson.Unmarshal(&repairRunReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { if len(args) == 0 { @@ -1242,9 +1278,15 @@ func newReset() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = resetJson.Unmarshal(&resetReq) - if err != nil { - return err + diags := resetJson.Unmarshal(&resetReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -1332,9 +1374,15 @@ func newRunNow() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = runNowJson.Unmarshal(&runNowReq) - if err != nil { - return err + diags := runNowJson.Unmarshal(&runNowReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { if len(args) == 0 { @@ -1436,9 +1484,15 @@ func newSetPermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = setPermissionsJson.Unmarshal(&setPermissionsReq) - if err != nil { - return err + diags := setPermissionsJson.Unmarshal(&setPermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { @@ -1538,9 +1592,15 @@ func newSubmit() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = submitJson.Unmarshal(&submitReq) - if err != nil { - return err + diags := submitJson.Unmarshal(&submitReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -1632,9 +1692,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { if len(args) == 0 { @@ -1717,9 +1783,15 @@ func newUpdatePermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updatePermissionsJson.Unmarshal(&updatePermissionsReq) - if err != nil { - return err + diags := updatePermissionsJson.Unmarshal(&updatePermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/workspace/lakeview/lakeview.go b/cmd/workspace/lakeview/lakeview.go index ef2d6845..33a45c65 100755 --- a/cmd/workspace/lakeview/lakeview.go +++ b/cmd/workspace/lakeview/lakeview.go @@ -108,9 +108,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -180,9 +186,15 @@ func newCreateSchedule() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createScheduleJson.Unmarshal(&createScheduleReq) - if err != nil { - return err + diags := createScheduleJson.Unmarshal(&createScheduleReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -250,9 +262,15 @@ func newCreateSubscription() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createSubscriptionJson.Unmarshal(&createSubscriptionReq) - if err != nil { - return err + diags := createSubscriptionJson.Unmarshal(&createSubscriptionReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -870,9 +888,15 @@ func newMigrate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = migrateJson.Unmarshal(&migrateReq) - if err != nil { - return err + diags := migrateJson.Unmarshal(&migrateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -941,9 +965,15 @@ func newPublish() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = publishJson.Unmarshal(&publishReq) - if err != nil { - return err + diags := publishJson.Unmarshal(&publishReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } publishReq.DashboardId = args[0] @@ -1128,9 +1158,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updateReq.DashboardId = args[0] @@ -1200,9 +1236,15 @@ func newUpdateSchedule() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateScheduleJson.Unmarshal(&updateScheduleReq) - if err != nil { - return err + diags := updateScheduleJson.Unmarshal(&updateScheduleReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/libraries/libraries.go b/cmd/workspace/libraries/libraries.go index 2c10d816..e6b332ae 100755 --- a/cmd/workspace/libraries/libraries.go +++ b/cmd/workspace/libraries/libraries.go @@ -190,9 +190,15 @@ func newInstall() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = installJson.Unmarshal(&installReq) - if err != nil { - return err + diags := installJson.Unmarshal(&installReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -251,9 +257,15 @@ func newUninstall() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = uninstallJson.Unmarshal(&uninstallReq) - if err != nil { - return err + diags := uninstallJson.Unmarshal(&uninstallReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/metastores/metastores.go b/cmd/workspace/metastores/metastores.go index 22bcd3dc..563beb2f 100755 --- a/cmd/workspace/metastores/metastores.go +++ b/cmd/workspace/metastores/metastores.go @@ -112,9 +112,15 @@ func newAssign() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = assignJson.Unmarshal(&assignReq) - if err != nil { - return err + diags := assignJson.Unmarshal(&assignReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } _, err = fmt.Sscan(args[0], &assignReq.WorkspaceId) @@ -201,9 +207,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -606,9 +618,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { @@ -690,9 +708,15 @@ func newUpdateAssignment() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateAssignmentJson.Unmarshal(&updateAssignmentReq) - if err != nil { - return err + diags := updateAssignmentJson.Unmarshal(&updateAssignmentReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/workspace/model-registry/model-registry.go b/cmd/workspace/model-registry/model-registry.go index 41f06ac4..b45d83e3 100755 --- a/cmd/workspace/model-registry/model-registry.go +++ b/cmd/workspace/model-registry/model-registry.go @@ -141,9 +141,15 @@ func newApproveTransitionRequest() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = approveTransitionRequestJson.Unmarshal(&approveTransitionRequestReq) - if err != nil { - return err + diags := approveTransitionRequestJson.Unmarshal(&approveTransitionRequestReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -235,9 +241,15 @@ func newCreateComment() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createCommentJson.Unmarshal(&createCommentReq) - if err != nil { - return err + diags := createCommentJson.Unmarshal(&createCommentReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -322,9 +334,15 @@ func newCreateModel() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createModelJson.Unmarshal(&createModelReq) - if err != nil { - return err + diags := createModelJson.Unmarshal(&createModelReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -403,9 +421,15 @@ func newCreateModelVersion() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createModelVersionJson.Unmarshal(&createModelVersionReq) - if err != nil { - return err + diags := createModelVersionJson.Unmarshal(&createModelVersionReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -493,9 +517,15 @@ func newCreateTransitionRequest() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createTransitionRequestJson.Unmarshal(&createTransitionRequestReq) - if err != nil { - return err + diags := createTransitionRequestJson.Unmarshal(&createTransitionRequestReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -570,9 +600,15 @@ func newCreateWebhook() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createWebhookJson.Unmarshal(&createWebhookReq) - if err != nil { - return err + diags := createWebhookJson.Unmarshal(&createWebhookReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -1079,9 +1115,15 @@ func newGetLatestVersions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = getLatestVersionsJson.Unmarshal(&getLatestVersionsReq) - if err != nil { - return err + diags := getLatestVersionsJson.Unmarshal(&getLatestVersionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -1629,9 +1671,15 @@ func newRejectTransitionRequest() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = rejectTransitionRequestJson.Unmarshal(&rejectTransitionRequestReq) - if err != nil { - return err + diags := rejectTransitionRequestJson.Unmarshal(&rejectTransitionRequestReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -1715,9 +1763,15 @@ func newRenameModel() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = renameModelJson.Unmarshal(&renameModelReq) - if err != nil { - return err + diags := renameModelJson.Unmarshal(&renameModelReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -1907,9 +1961,15 @@ func newSetModelTag() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = setModelTagJson.Unmarshal(&setModelTagReq) - if err != nil { - return err + diags := setModelTagJson.Unmarshal(&setModelTagReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -1996,9 +2056,15 @@ func newSetModelVersionTag() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = setModelVersionTagJson.Unmarshal(&setModelVersionTagReq) - if err != nil { - return err + diags := setModelVersionTagJson.Unmarshal(&setModelVersionTagReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -2076,9 +2142,15 @@ func newSetPermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = setPermissionsJson.Unmarshal(&setPermissionsReq) - if err != nil { - return err + diags := setPermissionsJson.Unmarshal(&setPermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } setPermissionsReq.RegisteredModelId = args[0] @@ -2166,9 +2238,15 @@ func newTestRegistryWebhook() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = testRegistryWebhookJson.Unmarshal(&testRegistryWebhookReq) - if err != nil { - return err + diags := testRegistryWebhookJson.Unmarshal(&testRegistryWebhookReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -2259,9 +2337,15 @@ func newTransitionStage() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = transitionStageJson.Unmarshal(&transitionStageReq) - if err != nil { - return err + diags := transitionStageJson.Unmarshal(&transitionStageReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -2350,9 +2434,15 @@ func newUpdateComment() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateCommentJson.Unmarshal(&updateCommentReq) - if err != nil { - return err + diags := updateCommentJson.Unmarshal(&updateCommentReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -2430,9 +2520,15 @@ func newUpdateModel() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateModelJson.Unmarshal(&updateModelReq) - if err != nil { - return err + diags := updateModelJson.Unmarshal(&updateModelReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -2508,9 +2604,15 @@ func newUpdateModelVersion() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateModelVersionJson.Unmarshal(&updateModelVersionReq) - if err != nil { - return err + diags := updateModelVersionJson.Unmarshal(&updateModelVersionReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -2582,9 +2684,15 @@ func newUpdatePermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updatePermissionsJson.Unmarshal(&updatePermissionsReq) - if err != nil { - return err + diags := updatePermissionsJson.Unmarshal(&updatePermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updatePermissionsReq.RegisteredModelId = args[0] @@ -2663,9 +2771,15 @@ func newUpdateWebhook() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateWebhookJson.Unmarshal(&updateWebhookReq) - if err != nil { - return err + diags := updateWebhookJson.Unmarshal(&updateWebhookReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { diff --git a/cmd/workspace/model-versions/model-versions.go b/cmd/workspace/model-versions/model-versions.go index d2f05404..439e5f65 100755 --- a/cmd/workspace/model-versions/model-versions.go +++ b/cmd/workspace/model-versions/model-versions.go @@ -377,9 +377,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updateReq.FullName = args[0] diff --git a/cmd/workspace/notification-destinations/notification-destinations.go b/cmd/workspace/notification-destinations/notification-destinations.go index 5ad47cc9..47076587 100755 --- a/cmd/workspace/notification-destinations/notification-destinations.go +++ b/cmd/workspace/notification-destinations/notification-destinations.go @@ -84,9 +84,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -313,9 +319,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updateReq.Id = args[0] diff --git a/cmd/workspace/online-tables/online-tables.go b/cmd/workspace/online-tables/online-tables.go index da2f8c04..1c25d1e2 100755 --- a/cmd/workspace/online-tables/online-tables.go +++ b/cmd/workspace/online-tables/online-tables.go @@ -79,9 +79,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } diff --git a/cmd/workspace/permission-migration/permission-migration.go b/cmd/workspace/permission-migration/permission-migration.go index 2e50b123..15ff1b75 100755 --- a/cmd/workspace/permission-migration/permission-migration.go +++ b/cmd/workspace/permission-migration/permission-migration.go @@ -92,9 +92,15 @@ func newMigratePermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = migratePermissionsJson.Unmarshal(&migratePermissionsReq) - if err != nil { - return err + diags := migratePermissionsJson.Unmarshal(&migratePermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { diff --git a/cmd/workspace/permissions/permissions.go b/cmd/workspace/permissions/permissions.go index c6033e4a..d007a425 100755 --- a/cmd/workspace/permissions/permissions.go +++ b/cmd/workspace/permissions/permissions.go @@ -265,9 +265,15 @@ func newSet() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = setJson.Unmarshal(&setReq) - if err != nil { - return err + diags := setJson.Unmarshal(&setReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } setReq.RequestObjectType = args[0] @@ -340,9 +346,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updateReq.RequestObjectType = args[0] diff --git a/cmd/workspace/pipelines/pipelines.go b/cmd/workspace/pipelines/pipelines.go index ac361e31..5bd94e0b 100755 --- a/cmd/workspace/pipelines/pipelines.go +++ b/cmd/workspace/pipelines/pipelines.go @@ -98,9 +98,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -699,9 +705,15 @@ func newSetPermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = setPermissionsJson.Unmarshal(&setPermissionsReq) - if err != nil { - return err + diags := setPermissionsJson.Unmarshal(&setPermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { @@ -788,9 +800,15 @@ func newStartUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = startUpdateJson.Unmarshal(&startUpdateReq) - if err != nil { - return err + diags := startUpdateJson.Unmarshal(&startUpdateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { @@ -977,9 +995,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { @@ -1058,9 +1082,15 @@ func newUpdatePermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updatePermissionsJson.Unmarshal(&updatePermissionsReq) - if err != nil { - return err + diags := updatePermissionsJson.Unmarshal(&updatePermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/workspace/policy-compliance-for-clusters/policy-compliance-for-clusters.go b/cmd/workspace/policy-compliance-for-clusters/policy-compliance-for-clusters.go index 1274c879..d128d80b 100755 --- a/cmd/workspace/policy-compliance-for-clusters/policy-compliance-for-clusters.go +++ b/cmd/workspace/policy-compliance-for-clusters/policy-compliance-for-clusters.go @@ -110,9 +110,15 @@ func newEnforceCompliance() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = enforceComplianceJson.Unmarshal(&enforceComplianceReq) - if err != nil { - return err + diags := enforceComplianceJson.Unmarshal(&enforceComplianceReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { diff --git a/cmd/workspace/policy-compliance-for-jobs/policy-compliance-for-jobs.go b/cmd/workspace/policy-compliance-for-jobs/policy-compliance-for-jobs.go index d74caa57..384dab2c 100755 --- a/cmd/workspace/policy-compliance-for-jobs/policy-compliance-for-jobs.go +++ b/cmd/workspace/policy-compliance-for-jobs/policy-compliance-for-jobs.go @@ -104,9 +104,15 @@ func newEnforceCompliance() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = enforceComplianceJson.Unmarshal(&enforceComplianceReq) - if err != nil { - return err + diags := enforceComplianceJson.Unmarshal(&enforceComplianceReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { diff --git a/cmd/workspace/provider-exchange-filters/provider-exchange-filters.go b/cmd/workspace/provider-exchange-filters/provider-exchange-filters.go index a3f74621..fea836d2 100755 --- a/cmd/workspace/provider-exchange-filters/provider-exchange-filters.go +++ b/cmd/workspace/provider-exchange-filters/provider-exchange-filters.go @@ -73,9 +73,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -259,9 +265,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/provider-exchanges/provider-exchanges.go b/cmd/workspace/provider-exchanges/provider-exchanges.go index b9240375..a96f0673 100755 --- a/cmd/workspace/provider-exchanges/provider-exchanges.go +++ b/cmd/workspace/provider-exchanges/provider-exchanges.go @@ -91,9 +91,15 @@ func newAddListingToExchange() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = addListingToExchangeJson.Unmarshal(&addListingToExchangeReq) - if err != nil { - return err + diags := addListingToExchangeJson.Unmarshal(&addListingToExchangeReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -154,9 +160,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -546,9 +558,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/provider-files/provider-files.go b/cmd/workspace/provider-files/provider-files.go index 62dcb6de..392ed289 100755 --- a/cmd/workspace/provider-files/provider-files.go +++ b/cmd/workspace/provider-files/provider-files.go @@ -77,9 +77,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -273,9 +279,15 @@ func newList() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = listJson.Unmarshal(&listReq) - if err != nil { - return err + diags := listJson.Unmarshal(&listReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/provider-listings/provider-listings.go b/cmd/workspace/provider-listings/provider-listings.go index 18c99c53..4c7c6c56 100755 --- a/cmd/workspace/provider-listings/provider-listings.go +++ b/cmd/workspace/provider-listings/provider-listings.go @@ -75,9 +75,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -326,9 +332,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/provider-personalization-requests/provider-personalization-requests.go b/cmd/workspace/provider-personalization-requests/provider-personalization-requests.go index d18e2e57..48c444f1 100755 --- a/cmd/workspace/provider-personalization-requests/provider-personalization-requests.go +++ b/cmd/workspace/provider-personalization-requests/provider-personalization-requests.go @@ -142,9 +142,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updateReq.ListingId = args[0] diff --git a/cmd/workspace/provider-provider-analytics-dashboards/provider-provider-analytics-dashboards.go b/cmd/workspace/provider-provider-analytics-dashboards/provider-provider-analytics-dashboards.go index bb3ca966..a8d151a2 100755 --- a/cmd/workspace/provider-provider-analytics-dashboards/provider-provider-analytics-dashboards.go +++ b/cmd/workspace/provider-provider-analytics-dashboards/provider-provider-analytics-dashboards.go @@ -208,9 +208,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updateReq.Id = args[0] diff --git a/cmd/workspace/provider-providers/provider-providers.go b/cmd/workspace/provider-providers/provider-providers.go index 94d12d6f..3c9c024e 100755 --- a/cmd/workspace/provider-providers/provider-providers.go +++ b/cmd/workspace/provider-providers/provider-providers.go @@ -74,9 +74,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -325,9 +331,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/providers/providers.go b/cmd/workspace/providers/providers.go index af2737a0..504beac5 100755 --- a/cmd/workspace/providers/providers.go +++ b/cmd/workspace/providers/providers.go @@ -97,9 +97,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -446,9 +452,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/workspace/quality-monitors/quality-monitors.go b/cmd/workspace/quality-monitors/quality-monitors.go index 1ff9b017..58075aa5 100755 --- a/cmd/workspace/quality-monitors/quality-monitors.go +++ b/cmd/workspace/quality-monitors/quality-monitors.go @@ -196,9 +196,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } createReq.TableName = args[0] @@ -559,9 +565,15 @@ func newRegenerateDashboard() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = regenerateDashboardJson.Unmarshal(®enerateDashboardReq) - if err != nil { - return err + diags := regenerateDashboardJson.Unmarshal(®enerateDashboardReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } regenerateDashboardReq.TableName = args[0] @@ -724,9 +736,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updateReq.TableName = args[0] diff --git a/cmd/workspace/queries-legacy/queries-legacy.go b/cmd/workspace/queries-legacy/queries-legacy.go index fa78bb2b..e35e1828 100755 --- a/cmd/workspace/queries-legacy/queries-legacy.go +++ b/cmd/workspace/queries-legacy/queries-legacy.go @@ -96,9 +96,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -454,9 +460,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/workspace/queries/queries.go b/cmd/workspace/queries/queries.go index fea01451..208f887d 100755 --- a/cmd/workspace/queries/queries.go +++ b/cmd/workspace/queries/queries.go @@ -85,9 +85,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -425,9 +431,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updateReq.Id = args[0] diff --git a/cmd/workspace/query-visualizations-legacy/query-visualizations-legacy.go b/cmd/workspace/query-visualizations-legacy/query-visualizations-legacy.go index 4f45ab23..f48acff1 100755 --- a/cmd/workspace/query-visualizations-legacy/query-visualizations-legacy.go +++ b/cmd/workspace/query-visualizations-legacy/query-visualizations-legacy.go @@ -87,9 +87,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -222,9 +228,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/query-visualizations/query-visualizations.go b/cmd/workspace/query-visualizations/query-visualizations.go index 04259452..62166195 100755 --- a/cmd/workspace/query-visualizations/query-visualizations.go +++ b/cmd/workspace/query-visualizations/query-visualizations.go @@ -84,9 +84,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -217,9 +223,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updateReq.Id = args[0] diff --git a/cmd/workspace/recipients/recipients.go b/cmd/workspace/recipients/recipients.go index f4472cf3..56abd201 100755 --- a/cmd/workspace/recipients/recipients.go +++ b/cmd/workspace/recipients/recipients.go @@ -118,9 +118,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -404,9 +410,15 @@ func newRotateToken() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = rotateTokenJson.Unmarshal(&rotateTokenReq) - if err != nil { - return err + diags := rotateTokenJson.Unmarshal(&rotateTokenReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } rotateTokenReq.Name = args[0] @@ -554,9 +566,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/workspace/registered-models/registered-models.go b/cmd/workspace/registered-models/registered-models.go index 5aa6cdf1..63f307a3 100755 --- a/cmd/workspace/registered-models/registered-models.go +++ b/cmd/workspace/registered-models/registered-models.go @@ -135,9 +135,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -509,9 +515,15 @@ func newSetAlias() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = setAliasJson.Unmarshal(&setAliasReq) - if err != nil { - return err + diags := setAliasJson.Unmarshal(&setAliasReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } setAliasReq.FullName = args[0] @@ -589,9 +601,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/workspace/repos/overrides.go b/cmd/workspace/repos/overrides.go index 9546d1c1..aad38ecc 100644 --- a/cmd/workspace/repos/overrides.go +++ b/cmd/workspace/repos/overrides.go @@ -7,6 +7,7 @@ import ( "github.com/databricks/cli/cmd/root" "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/flags" "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/service/workspace" @@ -35,9 +36,15 @@ func createOverride(createCmd *cobra.Command, createReq *workspace.CreateRepoReq ctx := cmd.Context() w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(createReq) - if err != nil { - return err + diags := createJson.Unmarshal(createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { createReq.Url = args[0] @@ -101,11 +108,18 @@ func updateOverride(updateCmd *cobra.Command, updateReq *workspace.UpdateRepoReq updateJson := updateCmd.Flag("json").Value.(*flags.JsonFlag) updateCmd.RunE = func(cmd *cobra.Command, args []string) (err error) { ctx := cmd.Context() + var diags diag.Diagnostics w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags = updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { updateReq.RepoId, err = repoArgumentToRepoID(ctx, w, args) diff --git a/cmd/workspace/repos/repos.go b/cmd/workspace/repos/repos.go index f11dd3ac..b77347b0 100755 --- a/cmd/workspace/repos/repos.go +++ b/cmd/workspace/repos/repos.go @@ -111,9 +111,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -521,9 +527,15 @@ func newSetPermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = setPermissionsJson.Unmarshal(&setPermissionsReq) - if err != nil { - return err + diags := setPermissionsJson.Unmarshal(&setPermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { @@ -604,9 +616,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { @@ -688,9 +706,15 @@ func newUpdatePermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updatePermissionsJson.Unmarshal(&updatePermissionsReq) - if err != nil { - return err + diags := updatePermissionsJson.Unmarshal(&updatePermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/workspace/restrict-workspace-admins/restrict-workspace-admins.go b/cmd/workspace/restrict-workspace-admins/restrict-workspace-admins.go index 5e9f59d2..5d0fba92 100755 --- a/cmd/workspace/restrict-workspace-admins/restrict-workspace-admins.go +++ b/cmd/workspace/restrict-workspace-admins/restrict-workspace-admins.go @@ -197,9 +197,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/schemas/schemas.go b/cmd/workspace/schemas/schemas.go index 3a398251..3ce573ba 100755 --- a/cmd/workspace/schemas/schemas.go +++ b/cmd/workspace/schemas/schemas.go @@ -100,9 +100,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -386,9 +392,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/workspace/secrets/put_secret.go b/cmd/workspace/secrets/put_secret.go index e323c7a1..f24814f0 100644 --- a/cmd/workspace/secrets/put_secret.go +++ b/cmd/workspace/secrets/put_secret.go @@ -8,6 +8,7 @@ import ( "github.com/databricks/cli/cmd/root" "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/flags" "github.com/databricks/databricks-sdk-go/service/workspace" "github.com/spf13/cobra" @@ -60,6 +61,7 @@ func newPutSecret() *cobra.Command { cmd.PreRunE = root.MustWorkspaceClient cmd.RunE = func(cmd *cobra.Command, args []string) (err error) { ctx := cmd.Context() + var diags diag.Diagnostics w := root.WorkspaceClient(ctx) bytesValueChanged := cmd.Flags().Changed("bytes-value") @@ -69,9 +71,15 @@ func newPutSecret() *cobra.Command { } if cmd.Flags().Changed("json") { - err = putSecretJson.Unmarshal(&putSecretReq) - if err != nil { - return err + diags = putSecretJson.Unmarshal(&putSecretReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { putSecretReq.Scope = args[0] diff --git a/cmd/workspace/secrets/secrets.go b/cmd/workspace/secrets/secrets.go index f836a267..e9547b62 100755 --- a/cmd/workspace/secrets/secrets.go +++ b/cmd/workspace/secrets/secrets.go @@ -110,9 +110,15 @@ func newCreateScope() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createScopeJson.Unmarshal(&createScopeReq) - if err != nil { - return err + diags := createScopeJson.Unmarshal(&createScopeReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -191,9 +197,15 @@ func newDeleteAcl() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = deleteAclJson.Unmarshal(&deleteAclReq) - if err != nil { - return err + diags := deleteAclJson.Unmarshal(&deleteAclReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -273,9 +285,15 @@ func newDeleteScope() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = deleteScopeJson.Unmarshal(&deleteScopeReq) - if err != nil { - return err + diags := deleteScopeJson.Unmarshal(&deleteScopeReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -354,9 +372,15 @@ func newDeleteSecret() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = deleteSecretJson.Unmarshal(&deleteSecretReq) - if err != nil { - return err + diags := deleteSecretJson.Unmarshal(&deleteSecretReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -759,9 +783,15 @@ func newPutAcl() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = putAclJson.Unmarshal(&putAclReq) - if err != nil { - return err + diags := putAclJson.Unmarshal(&putAclReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { diff --git a/cmd/workspace/service-principals/service-principals.go b/cmd/workspace/service-principals/service-principals.go index 957cb126..317779f3 100755 --- a/cmd/workspace/service-principals/service-principals.go +++ b/cmd/workspace/service-principals/service-principals.go @@ -95,9 +95,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -358,9 +364,15 @@ func newPatch() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = patchJson.Unmarshal(&patchReq) - if err != nil { - return err + diags := patchJson.Unmarshal(&patchReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { @@ -448,9 +460,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/workspace/serving-endpoints/serving-endpoints.go b/cmd/workspace/serving-endpoints/serving-endpoints.go index 0837652d..363e9ea1 100755 --- a/cmd/workspace/serving-endpoints/serving-endpoints.go +++ b/cmd/workspace/serving-endpoints/serving-endpoints.go @@ -169,9 +169,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -706,9 +712,15 @@ func newPatch() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = patchJson.Unmarshal(&patchReq) - if err != nil { - return err + diags := patchJson.Unmarshal(&patchReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } patchReq.Name = args[0] @@ -777,9 +789,15 @@ func newPut() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = putJson.Unmarshal(&putReq) - if err != nil { - return err + diags := putJson.Unmarshal(&putReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } putReq.Name = args[0] @@ -850,9 +868,15 @@ func newPutAiGateway() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = putAiGatewayJson.Unmarshal(&putAiGatewayReq) - if err != nil { - return err + diags := putAiGatewayJson.Unmarshal(&putAiGatewayReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } putAiGatewayReq.Name = args[0] @@ -928,9 +952,15 @@ func newQuery() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = queryJson.Unmarshal(&queryReq) - if err != nil { - return err + diags := queryJson.Unmarshal(&queryReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } queryReq.Name = args[0] @@ -997,9 +1027,15 @@ func newSetPermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = setPermissionsJson.Unmarshal(&setPermissionsReq) - if err != nil { - return err + diags := setPermissionsJson.Unmarshal(&setPermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } setPermissionsReq.ServingEndpointId = args[0] @@ -1076,9 +1112,15 @@ func newUpdateConfig() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateConfigJson.Unmarshal(&updateConfigReq) - if err != nil { - return err + diags := updateConfigJson.Unmarshal(&updateConfigReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updateConfigReq.Name = args[0] @@ -1158,9 +1200,15 @@ func newUpdatePermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updatePermissionsJson.Unmarshal(&updatePermissionsReq) - if err != nil { - return err + diags := updatePermissionsJson.Unmarshal(&updatePermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updatePermissionsReq.ServingEndpointId = args[0] diff --git a/cmd/workspace/shares/shares.go b/cmd/workspace/shares/shares.go index 67f87017..62c3407f 100755 --- a/cmd/workspace/shares/shares.go +++ b/cmd/workspace/shares/shares.go @@ -100,9 +100,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -427,9 +433,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updateReq.Name = args[0] @@ -501,9 +513,15 @@ func newUpdatePermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updatePermissionsJson.Unmarshal(&updatePermissionsReq) - if err != nil { - return err + diags := updatePermissionsJson.Unmarshal(&updatePermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updatePermissionsReq.Name = args[0] diff --git a/cmd/workspace/storage-credentials/storage-credentials.go b/cmd/workspace/storage-credentials/storage-credentials.go index f4ec5eb4..2caf0904 100755 --- a/cmd/workspace/storage-credentials/storage-credentials.go +++ b/cmd/workspace/storage-credentials/storage-credentials.go @@ -111,9 +111,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -377,9 +383,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { @@ -478,9 +490,15 @@ func newValidate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = validateJson.Unmarshal(&validateReq) - if err != nil { - return err + diags := validateJson.Unmarshal(&validateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } diff --git a/cmd/workspace/table-constraints/table-constraints.go b/cmd/workspace/table-constraints/table-constraints.go index 166da146..4ac7cb9f 100755 --- a/cmd/workspace/table-constraints/table-constraints.go +++ b/cmd/workspace/table-constraints/table-constraints.go @@ -92,9 +92,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/tables/tables.go b/cmd/workspace/tables/tables.go index ec297f29..35775f17 100755 --- a/cmd/workspace/tables/tables.go +++ b/cmd/workspace/tables/tables.go @@ -479,9 +479,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/workspace/temporary-table-credentials/temporary-table-credentials.go b/cmd/workspace/temporary-table-credentials/temporary-table-credentials.go index 8718f7ba..210a59f8 100755 --- a/cmd/workspace/temporary-table-credentials/temporary-table-credentials.go +++ b/cmd/workspace/temporary-table-credentials/temporary-table-credentials.go @@ -94,9 +94,15 @@ func newGenerateTemporaryTableCredentials() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = generateTemporaryTableCredentialsJson.Unmarshal(&generateTemporaryTableCredentialsReq) - if err != nil { - return err + diags := generateTemporaryTableCredentialsJson.Unmarshal(&generateTemporaryTableCredentialsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } diff --git a/cmd/workspace/token-management/token-management.go b/cmd/workspace/token-management/token-management.go index dea94edb..6deb8d12 100755 --- a/cmd/workspace/token-management/token-management.go +++ b/cmd/workspace/token-management/token-management.go @@ -96,9 +96,15 @@ func newCreateOboToken() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createOboTokenJson.Unmarshal(&createOboTokenReq) - if err != nil { - return err + diags := createOboTokenJson.Unmarshal(&createOboTokenReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { if len(args) == 0 { @@ -458,9 +464,15 @@ func newSetPermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = setPermissionsJson.Unmarshal(&setPermissionsReq) - if err != nil { - return err + diags := setPermissionsJson.Unmarshal(&setPermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -523,9 +535,15 @@ func newUpdatePermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updatePermissionsJson.Unmarshal(&updatePermissionsReq) - if err != nil { - return err + diags := updatePermissionsJson.Unmarshal(&updatePermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } diff --git a/cmd/workspace/tokens/tokens.go b/cmd/workspace/tokens/tokens.go index afe4b9a0..5c9b4994 100755 --- a/cmd/workspace/tokens/tokens.go +++ b/cmd/workspace/tokens/tokens.go @@ -84,9 +84,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -158,9 +164,15 @@ func newDelete() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = deleteJson.Unmarshal(&deleteReq) - if err != nil { - return err + diags := deleteJson.Unmarshal(&deleteReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { if len(args) == 0 { diff --git a/cmd/workspace/users/users.go b/cmd/workspace/users/users.go index 53ba2e85..b085ab41 100755 --- a/cmd/workspace/users/users.go +++ b/cmd/workspace/users/users.go @@ -107,9 +107,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -463,9 +469,15 @@ func newPatch() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = patchJson.Unmarshal(&patchReq) - if err != nil { - return err + diags := patchJson.Unmarshal(&patchReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { @@ -546,9 +558,15 @@ func newSetPermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = setPermissionsJson.Unmarshal(&setPermissionsReq) - if err != nil { - return err + diags := setPermissionsJson.Unmarshal(&setPermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -619,9 +637,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { @@ -702,9 +726,15 @@ func newUpdatePermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updatePermissionsJson.Unmarshal(&updatePermissionsReq) - if err != nil { - return err + diags := updatePermissionsJson.Unmarshal(&updatePermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } diff --git a/cmd/workspace/vector-search-endpoints/vector-search-endpoints.go b/cmd/workspace/vector-search-endpoints/vector-search-endpoints.go index dd9d5783..0cfb7617 100755 --- a/cmd/workspace/vector-search-endpoints/vector-search-endpoints.go +++ b/cmd/workspace/vector-search-endpoints/vector-search-endpoints.go @@ -95,9 +95,15 @@ func newCreateEndpoint() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createEndpointJson.Unmarshal(&createEndpointReq) - if err != nil { - return err + diags := createEndpointJson.Unmarshal(&createEndpointReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { diff --git a/cmd/workspace/vector-search-indexes/vector-search-indexes.go b/cmd/workspace/vector-search-indexes/vector-search-indexes.go index 15847477..832f4a6d 100755 --- a/cmd/workspace/vector-search-indexes/vector-search-indexes.go +++ b/cmd/workspace/vector-search-indexes/vector-search-indexes.go @@ -114,9 +114,15 @@ func newCreateIndex() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createIndexJson.Unmarshal(&createIndexReq) - if err != nil { - return err + diags := createIndexJson.Unmarshal(&createIndexReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -195,9 +201,15 @@ func newDeleteDataVectorIndex() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = deleteDataVectorIndexJson.Unmarshal(&deleteDataVectorIndexReq) - if err != nil { - return err + diags := deleteDataVectorIndexJson.Unmarshal(&deleteDataVectorIndexReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -443,9 +455,15 @@ func newQueryIndex() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = queryIndexJson.Unmarshal(&queryIndexReq) - if err != nil { - return err + diags := queryIndexJson.Unmarshal(&queryIndexReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") @@ -515,9 +533,15 @@ func newQueryNextPage() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = queryNextPageJson.Unmarshal(&queryNextPageReq) - if err != nil { - return err + diags := queryNextPageJson.Unmarshal(&queryNextPageReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } queryNextPageReq.IndexName = args[0] @@ -585,9 +609,15 @@ func newScanIndex() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = scanIndexJson.Unmarshal(&scanIndexReq) - if err != nil { - return err + diags := scanIndexJson.Unmarshal(&scanIndexReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } scanIndexReq.IndexName = args[0] @@ -718,9 +748,15 @@ func newUpsertDataVectorIndex() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = upsertDataVectorIndexJson.Unmarshal(&upsertDataVectorIndexReq) - if err != nil { - return err + diags := upsertDataVectorIndexJson.Unmarshal(&upsertDataVectorIndexReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } upsertDataVectorIndexReq.IndexName = args[0] diff --git a/cmd/workspace/volumes/volumes.go b/cmd/workspace/volumes/volumes.go index 3fc1f447..2f455573 100755 --- a/cmd/workspace/volumes/volumes.go +++ b/cmd/workspace/volumes/volumes.go @@ -119,9 +119,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -427,9 +433,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/workspace/warehouses/warehouses.go b/cmd/workspace/warehouses/warehouses.go index cdf10636..43d6c8ab 100755 --- a/cmd/workspace/warehouses/warehouses.go +++ b/cmd/workspace/warehouses/warehouses.go @@ -109,9 +109,15 @@ func newCreate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = createJson.Unmarshal(&createReq) - if err != nil { - return err + diags := createJson.Unmarshal(&createReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -277,9 +283,15 @@ func newEdit() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = editJson.Unmarshal(&editReq) - if err != nil { - return err + diags := editJson.Unmarshal(&editReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { @@ -688,9 +700,15 @@ func newSetPermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = setPermissionsJson.Unmarshal(&setPermissionsReq) - if err != nil { - return err + diags := setPermissionsJson.Unmarshal(&setPermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { @@ -779,9 +797,15 @@ func newSetWorkspaceWarehouseConfig() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = setWorkspaceWarehouseConfigJson.Unmarshal(&setWorkspaceWarehouseConfigReq) - if err != nil { - return err + diags := setWorkspaceWarehouseConfigJson.Unmarshal(&setWorkspaceWarehouseConfigReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } @@ -1030,9 +1054,15 @@ func newUpdatePermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updatePermissionsJson.Unmarshal(&updatePermissionsReq) - if err != nil { - return err + diags := updatePermissionsJson.Unmarshal(&updatePermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if len(args) == 0 { diff --git a/cmd/workspace/workspace-bindings/workspace-bindings.go b/cmd/workspace/workspace-bindings/workspace-bindings.go index 4993f1af..20f54e1d 100755 --- a/cmd/workspace/workspace-bindings/workspace-bindings.go +++ b/cmd/workspace/workspace-bindings/workspace-bindings.go @@ -226,9 +226,15 @@ func newUpdate() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateJson.Unmarshal(&updateReq) - if err != nil { - return err + diags := updateJson.Unmarshal(&updateReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updateReq.Name = args[0] @@ -297,9 +303,15 @@ func newUpdateBindings() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updateBindingsJson.Unmarshal(&updateBindingsReq) - if err != nil { - return err + diags := updateBindingsJson.Unmarshal(&updateBindingsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } _, err = fmt.Sscan(args[0], &updateBindingsReq.SecurableType) diff --git a/cmd/workspace/workspace-conf/workspace-conf.go b/cmd/workspace/workspace-conf/workspace-conf.go index 92b2f0f3..a17bc163 100755 --- a/cmd/workspace/workspace-conf/workspace-conf.go +++ b/cmd/workspace/workspace-conf/workspace-conf.go @@ -127,9 +127,15 @@ func newSetStatus() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = setStatusJson.Unmarshal(&setStatusReq) - if err != nil { - return err + diags := setStatusJson.Unmarshal(&setStatusReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { return fmt.Errorf("please provide command input in JSON format by specifying the --json flag") diff --git a/cmd/workspace/workspace/workspace.go b/cmd/workspace/workspace/workspace.go index 183cac89..21da478c 100755 --- a/cmd/workspace/workspace/workspace.go +++ b/cmd/workspace/workspace/workspace.go @@ -106,9 +106,15 @@ func newDelete() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = deleteJson.Unmarshal(&deleteReq) - if err != nil { - return err + diags := deleteJson.Unmarshal(&deleteReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { if len(args) == 0 { @@ -482,9 +488,15 @@ func newImport() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = importJson.Unmarshal(&importReq) - if err != nil { - return err + diags := importJson.Unmarshal(&importReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } if !cmd.Flags().Changed("json") { @@ -622,9 +634,15 @@ func newMkdirs() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = mkdirsJson.Unmarshal(&mkdirsReq) - if err != nil { - return err + diags := mkdirsJson.Unmarshal(&mkdirsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } else { if len(args) == 0 { @@ -710,9 +728,15 @@ func newSetPermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = setPermissionsJson.Unmarshal(&setPermissionsReq) - if err != nil { - return err + diags := setPermissionsJson.Unmarshal(&setPermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } setPermissionsReq.WorkspaceObjectType = args[0] @@ -781,9 +805,15 @@ func newUpdatePermissions() *cobra.Command { w := root.WorkspaceClient(ctx) if cmd.Flags().Changed("json") { - err = updatePermissionsJson.Unmarshal(&updatePermissionsReq) - if err != nil { - return err + diags := updatePermissionsJson.Unmarshal(&updatePermissionsReq) + if diags.HasError() { + return diags.Error() + } + if len(diags) > 0 { + err := cmdio.RenderDiagnosticsToErrorOut(ctx, diags) + if err != nil { + return err + } } } updatePermissionsReq.WorkspaceObjectType = args[0] diff --git a/libs/cmdio/render.go b/libs/cmdio/render.go index 4114db5c..72d95978 100644 --- a/libs/cmdio/render.go +++ b/libs/cmdio/render.go @@ -13,6 +13,7 @@ import ( "text/template" "time" + "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/flags" "github.com/databricks/databricks-sdk-go/listing" "github.com/fatih/color" @@ -285,70 +286,72 @@ func RenderIteratorJson[T any](ctx context.Context, i listing.Iterator[T]) error return renderWithTemplate(newIteratorRenderer(i), ctx, c.outputFormat, c.out, c.headerTemplate, c.template) } +var renderFuncMap = 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 + "header": color.BlueString, + "red": color.RedString, + "green": color.GreenString, + "blue": color.BlueString, + "yellow": color.YellowString, + "magenta": color.MagentaString, + "cyan": color.CyanString, + "bold": func(format string, a ...interface{}) string { + return color.New(color.Bold).Sprintf(format, a...) + }, + "italic": func(format string, a ...interface{}) string { + return color.New(color.Italic).Sprintf(format, a...) + }, + "replace": strings.ReplaceAll, + "join": strings.Join, + "bool": func(v bool) string { + if v { + return color.GreenString("YES") + } + return color.RedString("NO") + }, + "pretty_json": func(in string) (string, error) { + var tmp any + err := json.Unmarshal([]byte(in), &tmp) + if err != nil { + return "", err + } + b, err := fancyJSON(tmp) + if err != nil { + return "", err + } + return string(b), nil + }, + "pretty_date": func(t time.Time) string { + return t.Format("2006-01-02T15:04:05Z") + }, + "b64_encode": func(in string) (string, error) { + var out bytes.Buffer + enc := base64.NewEncoder(base64.StdEncoding, &out) + _, err := enc.Write([]byte(in)) + if err != nil { + return "", err + } + err = enc.Close() + if err != nil { + return "", err + } + return out.String(), nil + }, + "b64_decode": func(in string) (string, error) { + dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(in)) + out, err := io.ReadAll(dec) + if err != nil { + return "", err + } + return string(out), nil + }, +} + func renderUsingTemplate(ctx context.Context, r templateRenderer, w io.Writer, headerTmpl, tmpl string) error { tw := tabwriter.NewWriter(w, 0, 4, 2, ' ', 0) - 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 - "header": color.BlueString, - "red": color.RedString, - "green": color.GreenString, - "blue": color.BlueString, - "yellow": color.YellowString, - "magenta": color.MagentaString, - "cyan": color.CyanString, - "bold": func(format string, a ...interface{}) string { - return color.New(color.Bold).Sprintf(format, a...) - }, - "italic": func(format string, a ...interface{}) string { - return color.New(color.Italic).Sprintf(format, a...) - }, - "replace": strings.ReplaceAll, - "join": strings.Join, - "bool": func(v bool) string { - if v { - return color.GreenString("YES") - } - return color.RedString("NO") - }, - "pretty_json": func(in string) (string, error) { - var tmp any - err := json.Unmarshal([]byte(in), &tmp) - if err != nil { - return "", err - } - b, err := fancyJSON(tmp) - if err != nil { - return "", err - } - return string(b), nil - }, - "pretty_date": func(t time.Time) string { - return t.Format("2006-01-02T15:04:05Z") - }, - "b64_encode": func(in string) (string, error) { - var out bytes.Buffer - enc := base64.NewEncoder(base64.StdEncoding, &out) - _, err := enc.Write([]byte(in)) - if err != nil { - return "", err - } - err = enc.Close() - if err != nil { - return "", err - } - return out.String(), nil - }, - "b64_decode": func(in string) (string, error) { - dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(in)) - out, err := io.ReadAll(dec) - if err != nil { - return "", err - } - return string(out), nil - }, - }) + base := template.New("command").Funcs(renderFuncMap) if headerTmpl != "" { headerT, err := base.Parse(headerTmpl) if err != nil { @@ -388,3 +391,77 @@ func fancyJSON(v any) ([]byte, error) { return jsoncolor.MarshalIndentWithFormatter(v, "", " ", f) } + +const errorTemplate = `{{ "Error" | red }}: {{ .Summary }} +{{- range $index, $element := .Paths }} + {{ if eq $index 0 }}at {{else}} {{ end}}{{ $element.String | green }} +{{- end }} +{{- range $index, $element := .Locations }} + {{ if eq $index 0 }}in {{else}} {{ end}}{{ $element.String | cyan }} +{{- end }} +{{- if .Detail }} + +{{ .Detail }} +{{- end }} + +` + +const warningTemplate = `{{ "Warning" | yellow }}: {{ .Summary }} +{{- range $index, $element := .Paths }} + {{ if eq $index 0 }}at {{else}} {{ end}}{{ $element.String | green }} +{{- end }} +{{- range $index, $element := .Locations }} + {{ if eq $index 0 }}in {{else}} {{ end}}{{ $element.String | cyan }} +{{- end }} +{{- if .Detail }} + +{{ .Detail }} +{{- end }} + +` + +const recommendationTemplate = `{{ "Recommendation" | blue }}: {{ .Summary }} +{{- range $index, $element := .Paths }} + {{ if eq $index 0 }}at {{else}} {{ end}}{{ $element.String | green }} +{{- end }} +{{- range $index, $element := .Locations }} + {{ if eq $index 0 }}in {{else}} {{ end}}{{ $element.String | cyan }} +{{- end }} +{{- if .Detail }} + +{{ .Detail }} +{{- end }} + +` + +func RenderDiagnosticsToErrorOut(ctx context.Context, diags diag.Diagnostics) error { + c := fromContext(ctx) + return RenderDiagnostics(c.err, diags) +} + +func RenderDiagnostics(out io.Writer, diags diag.Diagnostics) error { + errorT := template.Must(template.New("error").Funcs(renderFuncMap).Parse(errorTemplate)) + warningT := template.Must(template.New("warning").Funcs(renderFuncMap).Parse(warningTemplate)) + recommendationT := template.Must(template.New("recommendation").Funcs(renderFuncMap).Parse(recommendationTemplate)) + + // Print errors and warnings. + for _, d := range diags { + var t *template.Template + switch d.Severity { + case diag.Error: + t = errorT + case diag.Warning: + t = warningT + case diag.Recommendation: + t = recommendationT + } + + // Render the diagnostic with the appropriate template. + err := t.Execute(out, d) + if err != nil { + return fmt.Errorf("failed to render template: %w", err) + } + } + + return nil +} diff --git a/libs/dyn/convert/to_typed.go b/libs/dyn/convert/to_typed.go index 839d0111..e2aa5d26 100644 --- a/libs/dyn/convert/to_typed.go +++ b/libs/dyn/convert/to_typed.go @@ -221,10 +221,10 @@ func toTypedBool(dst reflect.Value, src dyn.Value) error { case dyn.KindString: // See https://github.com/go-yaml/yaml/blob/f6f7691b1fdeb513f56608cd2c32c51f8194bf51/decode.go#L684-L693. switch src.MustString() { - case "y", "Y", "yes", "Yes", "YES", "on", "On", "ON": + case "y", "Y", "yes", "Yes", "YES", "on", "On", "ON", "true": dst.SetBool(true) return nil - case "n", "N", "no", "No", "NO", "off", "Off", "OFF": + case "n", "N", "no", "No", "NO", "off", "Off", "OFF", "false": dst.SetBool(false) return nil } @@ -246,6 +246,19 @@ func toTypedInt(dst reflect.Value, src dyn.Value) error { case dyn.KindInt: dst.SetInt(src.MustInt()) return nil + case dyn.KindFloat: + v := src.MustFloat() + if v == float64(int64(v)) { + // If the destination is smaller than int64, but the value to set is bigger + // then destination overflows and is set to -1 + dst.SetInt(int64(src.MustFloat())) + return nil + } + + return TypeError{ + value: src, + msg: fmt.Sprintf("expected an int, found a %s", src.Kind()), + } case dyn.KindString: if i64, err := strconv.ParseInt(src.MustString(), 10, 64); err == nil { dst.SetInt(i64) diff --git a/libs/dyn/convert/to_typed_test.go b/libs/dyn/convert/to_typed_test.go index 37d85539..78221c29 100644 --- a/libs/dyn/convert/to_typed_test.go +++ b/libs/dyn/convert/to_typed_test.go @@ -341,14 +341,14 @@ func TestToTypedBoolFromString(t *testing.T) { var out bool // True-ish - for _, v := range []string{"y", "yes", "on"} { + for _, v := range []string{"y", "yes", "on", "true"} { err := ToTyped(&out, dyn.V(v)) require.NoError(t, err) assert.Equal(t, true, out) } // False-ish - for _, v := range []string{"n", "no", "off"} { + for _, v := range []string{"n", "no", "off", "false"} { err := ToTyped(&out, dyn.V(v)) require.NoError(t, err) assert.Equal(t, false, out) @@ -428,6 +428,19 @@ func TestToTypedIntFromStringVariableReference(t *testing.T) { assert.Equal(t, int(0), out) } +func TestToTypedIntFromFloat(t *testing.T) { + var out int + err := ToTyped(&out, dyn.V(1.0)) + require.NoError(t, err) + assert.Equal(t, int(1), out) +} + +func TestToTypedIntFromFloatError(t *testing.T) { + var out int + err := ToTyped(&out, dyn.V(1.2)) + require.ErrorContains(t, err, "expected an int, found a float") +} + func TestToTypedFloat32(t *testing.T) { var out float32 err := ToTyped(&out, dyn.V(float32(1.0))) diff --git a/libs/dyn/jsonloader/json.go b/libs/dyn/jsonloader/json.go new file mode 100644 index 00000000..cbf53926 --- /dev/null +++ b/libs/dyn/jsonloader/json.go @@ -0,0 +1,107 @@ +package jsonloader + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + + "github.com/databricks/cli/libs/dyn" +) + +func LoadJSON(data []byte, source string) (dyn.Value, error) { + offset := BuildLineOffsets(data) + offset.SetSource(source) + + reader := bytes.NewReader(data) + decoder := json.NewDecoder(reader) + + // Start decoding from the top-level value + value, err := decodeValue(decoder, &offset) + if err != nil { + if err == io.EOF { + err = fmt.Errorf("unexpected end of JSON input") + } + return dyn.InvalidValue, fmt.Errorf("error decoding JSON at %s: %v", value.Location(), err) + } + return value, nil +} + +func decodeValue(decoder *json.Decoder, o *Offset) (dyn.Value, error) { + // Read the next JSON token + token, err := decoder.Token() + if err != nil { + return dyn.InvalidValue, err + } + + // Get the current byte offset and the location. + // We will later use this location to store the location of the value in the file + // For objects and arrays, we will store the location of the opening '{' or '[' + // For primitive types, we will store the location of the value itself (end of the value) + // We can't reliably calculate the beginning of the value for primitive types because + // the decoder doesn't provide the offset of the beginning of the value and the value might or might not be quoted. + offset := decoder.InputOffset() + location := o.GetPosition(offset) + + switch tok := token.(type) { + case json.Delim: + if tok == '{' { + location = o.GetPosition(offset - 1) + // Decode JSON object + obj := dyn.NewMapping() + for decoder.More() { + // Decode the key + keyToken, err := decoder.Token() + if err != nil { + return invalidValueWithLocation(decoder, o), err + } + key, ok := keyToken.(string) + if !ok { + return invalidValueWithLocation(decoder, o), fmt.Errorf("expected string for object key") + } + + // Get the offset of the key by subtracting the length of the key and the '"' character + keyOffset := decoder.InputOffset() - int64(len(key)+1) + keyVal := dyn.NewValue(key, []dyn.Location{o.GetPosition(keyOffset)}) + + // Decode the value recursively + val, err := decodeValue(decoder, o) + if err != nil { + return invalidValueWithLocation(decoder, o), err + } + + obj.Set(keyVal, val) + } + // Consume the closing '}' + if _, err := decoder.Token(); err != nil { + return invalidValueWithLocation(decoder, o), err + } + return dyn.NewValue(obj, []dyn.Location{location}), nil + } else if tok == '[' { + location = o.GetPosition(offset - 1) + // Decode JSON array + var arr []dyn.Value + for decoder.More() { + val, err := decodeValue(decoder, o) + if err != nil { + return invalidValueWithLocation(decoder, o), err + } + arr = append(arr, val) + } + // Consume the closing ']' + if _, err := decoder.Token(); err != nil { + return invalidValueWithLocation(decoder, o), err + } + return dyn.NewValue(arr, []dyn.Location{location}), nil + } + default: + return dyn.NewValue(tok, []dyn.Location{location}), nil + } + + return invalidValueWithLocation(decoder, o), fmt.Errorf("unexpected token: %v", token) +} + +func invalidValueWithLocation(decoder *json.Decoder, o *Offset) dyn.Value { + location := o.GetPosition(decoder.InputOffset()) + return dyn.InvalidValue.WithLocations([]dyn.Location{location}) +} diff --git a/libs/dyn/jsonloader/json_test.go b/libs/dyn/jsonloader/json_test.go new file mode 100644 index 00000000..7fdc4d7f --- /dev/null +++ b/libs/dyn/jsonloader/json_test.go @@ -0,0 +1,93 @@ +package jsonloader + +import ( + "testing" + + "github.com/databricks/cli/libs/dyn/convert" + "github.com/databricks/cli/libs/dyn/dynassert" + "github.com/databricks/databricks-sdk-go/service/jobs" +) + +const jsonData = ` +{ + "job_id": 123, + "new_settings": { + "name": "xxx", + "email_notifications": { + "on_start": [], + "on_success": [], + "on_failure": [] + }, + "webhook_notifications": { + "on_start": [], + "on_failure": [] + }, + "notification_settings": { + "no_alert_for_skipped_runs": true, + "no_alert_for_canceled_runs": true + }, + "timeout_seconds": 0, + "max_concurrent_runs": 1, + "tasks": [ + { + "task_key": "xxx", + "email_notifications": {}, + "notification_settings": {}, + "timeout_seconds": 0, + "max_retries": 0, + "min_retry_interval_millis": 0, + "retry_on_timeout": "true" + } + ] + } +} +` + +func TestJsonLoader(t *testing.T) { + v, err := LoadJSON([]byte(jsonData), "(inline)") + dynassert.NoError(t, err) + + var r jobs.ResetJob + err = convert.ToTyped(&r, v) + dynassert.NoError(t, err) +} + +const malformedMap = ` +{ + "job_id": 123, + "new_settings": { + "name": "xxx", + "wrong", + } +} +` + +func TestJsonLoaderMalformedMap(t *testing.T) { + _, err := LoadJSON([]byte(malformedMap), "(inline)") + dynassert.ErrorContains(t, err, "error decoding JSON at (inline):6:16: invalid character ',' after object key") +} + +const malformedArray = ` +{ + "job_id": 123, + "new_settings": { + "name": "xxx", + "tasks": [1, "asd",] + } +}` + +func TestJsonLoaderMalformedArray(t *testing.T) { + _, err := LoadJSON([]byte(malformedArray), "path/to/file.json") + dynassert.ErrorContains(t, err, "error decoding JSON at path/to/file.json:6:28: invalid character ']' looking for beginning of value") +} + +const eofData = ` +{ + "job_id": 123, + "new_settings": { + "name": "xxx",` + +func TestJsonLoaderEOF(t *testing.T) { + _, err := LoadJSON([]byte(eofData), "path/to/file.json") + dynassert.ErrorContains(t, err, "unexpected end of JSON input") +} diff --git a/libs/dyn/jsonloader/locations.go b/libs/dyn/jsonloader/locations.go new file mode 100644 index 00000000..a692c7d0 --- /dev/null +++ b/libs/dyn/jsonloader/locations.go @@ -0,0 +1,53 @@ +package jsonloader + +import ( + "sort" + + "github.com/databricks/cli/libs/dyn" +) + +type LineOffset struct { + Line int + Start int64 +} + +type Offset struct { + offsets []LineOffset + source string +} + +// buildLineOffsets scans the input data and records the starting byte offset of each line. +func BuildLineOffsets(data []byte) Offset { + offsets := []LineOffset{{Line: 1, Start: 0}} + line := 1 + for i, b := range data { + if b == '\n' { + line++ + offsets = append(offsets, LineOffset{Line: line, Start: int64(i + 1)}) + } + } + return Offset{offsets: offsets} +} + +// GetPosition maps a byte offset to its corresponding line and column numbers. +func (o Offset) GetPosition(offset int64) dyn.Location { + // Binary search to find the line + idx := sort.Search(len(o.offsets), func(i int) bool { + return o.offsets[i].Start > offset + }) - 1 + + if idx < 0 { + idx = 0 + } + + lineOffset := o.offsets[idx] + return dyn.Location{ + File: o.source, + Line: lineOffset.Line, + Column: int(offset-lineOffset.Start) + 1, + } +} + +func (o *Offset) SetSource(source string) { + o.source = source +} diff --git a/libs/flags/json_flag.go b/libs/flags/json_flag.go index 8dbc3b2d..0ed8be74 100644 --- a/libs/flags/json_flag.go +++ b/libs/flags/json_flag.go @@ -4,10 +4,16 @@ import ( "encoding/json" "fmt" "os" + + "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/dyn/convert" + "github.com/databricks/cli/libs/dyn/jsonloader" + "github.com/databricks/databricks-sdk-go/marshal" ) type JsonFlag struct { - raw []byte + raw []byte + source string } func (j *JsonFlag) String() string { @@ -19,21 +25,52 @@ func (j *JsonFlag) Set(v string) error { // Load request from file if it starts with '@' (like curl). if v[0] != '@' { j.raw = []byte(v) + j.source = "(inline)" return nil } - buf, err := os.ReadFile(v[1:]) + filePath := v[1:] + buf, err := os.ReadFile(filePath) + j.source = filePath if err != nil { - return fmt.Errorf("read %s: %w", v, err) + return fmt.Errorf("read %s: %w", filePath, err) } j.raw = buf return nil } -func (j *JsonFlag) Unmarshal(v any) error { +func (j *JsonFlag) Unmarshal(v any) diag.Diagnostics { if j.raw == nil { return nil } - return json.Unmarshal(j.raw, v) + + dv, err := jsonloader.LoadJSON(j.raw, j.source) + if err != nil { + return diag.FromErr(err) + } + + // First normalize the input data. + // It will convert all the values to the correct types. + // For example string literals for booleans and integers will be converted to the correct types. + nv, diags := convert.Normalize(v, dv) + if diags.HasError() { + return diags + } + + // Then marshal the normalized data to the output. + // It will serialize all set data with the correct types. + data, err := json.Marshal(nv.AsAny()) + if err != nil { + return diags.Extend(diag.FromErr(err)) + } + + // Finally unmarshal the normalized data to the output. + // It will fill in the ForceSendFields field if the struct contains it. + err = marshal.Unmarshal(data, v) + if err != nil { + return diags.Extend(diag.FromErr(err)) + } + + return diags } func (j *JsonFlag) Type() string { diff --git a/libs/flags/json_flag_test.go b/libs/flags/json_flag_test.go index 2a8170fe..16769f4e 100644 --- a/libs/flags/json_flag_test.go +++ b/libs/flags/json_flag_test.go @@ -6,18 +6,26 @@ import ( "path" "testing" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/dyn" + "github.com/databricks/databricks-sdk-go/service/jobs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +type requestType struct { + Foo string `json:"foo"` +} + func TestJsonFlagEmpty(t *testing.T) { var body JsonFlag var request any - err := body.Unmarshal(&request) + diags := body.Unmarshal(&request) assert.Equal(t, "JSON (0 bytes)", body.String()) - assert.NoError(t, err) + assert.NoError(t, diags.Error()) + assert.Empty(t, diags) assert.Nil(t, request) } @@ -27,12 +35,13 @@ func TestJsonFlagInline(t *testing.T) { err := body.Set(`{"foo": "bar"}`) assert.NoError(t, err) - var request any - err = body.Unmarshal(&request) - assert.NoError(t, err) + var request requestType + diags := body.Unmarshal(&request) + assert.NoError(t, diags.Error()) + assert.Empty(t, diags) assert.Equal(t, "JSON (14 bytes)", body.String()) - assert.Equal(t, map[string]any{"foo": "bar"}, request) + assert.Equal(t, requestType{"bar"}, request) } func TestJsonFlagError(t *testing.T) { @@ -41,18 +50,18 @@ func TestJsonFlagError(t *testing.T) { err := body.Set(`{"foo":`) assert.NoError(t, err) - var request any - err = body.Unmarshal(&request) - assert.EqualError(t, err, "unexpected end of JSON input") + var request requestType + diags := body.Unmarshal(&request) + assert.EqualError(t, diags.Error(), "error decoding JSON at (inline):1:8: unexpected end of JSON input") assert.Equal(t, "JSON (7 bytes)", body.String()) } func TestJsonFlagFile(t *testing.T) { var body JsonFlag - var request any + var request requestType var fpath string - var payload = []byte(`"hello world"`) + var payload = []byte(`{"foo": "bar"}`) { f, err := os.Create(path.Join(t.TempDir(), "file")) @@ -65,8 +74,217 @@ func TestJsonFlagFile(t *testing.T) { err := body.Set(fmt.Sprintf("@%s", fpath)) require.NoError(t, err) - err = body.Unmarshal(&request) + diags := body.Unmarshal(&request) + assert.NoError(t, diags.Error()) + assert.Empty(t, diags) + + assert.Equal(t, requestType{"bar"}, request) +} + +const jsonData = ` +{ + "job_id": 123, + "new_settings": { + "name": "new job", + "email_notifications": { + "on_start": [], + "on_success": [], + "on_failure": [] + }, + "notification_settings": { + "no_alert_for_canceled_runs": false + }, + "timeout_seconds": 0, + "max_concurrent_runs": 1, + "tasks": [ + { + "task_key": "new task", + "email_notifications": {}, + "notification_settings": {}, + "timeout_seconds": 0, + "max_retries": 0, + "min_retry_interval_millis": 0, + "retry_on_timeout": "true" + } + ] + } +} +` + +func TestJsonUnmarshalForRequest(t *testing.T) { + var body JsonFlag + + var r jobs.ResetJob + err := body.Set(jsonData) require.NoError(t, err) - assert.Equal(t, "hello world", request) + diags := body.Unmarshal(&r) + assert.NoError(t, diags.Error()) + assert.Empty(t, diags) + + assert.Equal(t, int64(123), r.JobId) + assert.Equal(t, "new job", r.NewSettings.Name) + assert.Equal(t, 0, r.NewSettings.TimeoutSeconds) + assert.Equal(t, 1, r.NewSettings.MaxConcurrentRuns) + assert.Equal(t, 1, len(r.NewSettings.Tasks)) + assert.Equal(t, "new task", r.NewSettings.Tasks[0].TaskKey) + assert.Equal(t, 0, r.NewSettings.Tasks[0].TimeoutSeconds) + assert.Equal(t, 0, r.NewSettings.Tasks[0].MaxRetries) + assert.Equal(t, 0, r.NewSettings.Tasks[0].MinRetryIntervalMillis) + assert.Equal(t, true, r.NewSettings.Tasks[0].RetryOnTimeout) +} + +const incorrectJsonData = `{ + "job_id": 123, + "settings": { + "name": "new job", + "email_notifications": { + "on_start": [], + "on_success": [], + "on_failure": [] + }, + "notification_settings": { + "no_alert_for_skipped_runs": true, + "no_alert_for_canceled_runs": true + }, + "timeout_seconds": {}, + "max_concurrent_runs": {}, + "tasks": [ + { + "task_key": "new task", + "email_notifications": {}, + "notification_settings": {}, + "timeout_seconds": 0, + "max_retries": 0, + "min_retry_interval_millis": 0, + "retry_on_timeout": "true" + } + ] + } +} +` + +func TestJsonUnmarshalRequestMismatch(t *testing.T) { + var body JsonFlag + + var r jobs.ResetJob + err := body.Set(incorrectJsonData) + require.NoError(t, err) + + diags := body.Unmarshal(&r) + assert.NoError(t, diags.Error()) + assert.NotEmpty(t, diags) + + assert.Contains(t, diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "unknown field: settings", + Locations: []dyn.Location{ + { + File: "(inline)", + Line: 3, + Column: 6, + }, + }, + Paths: []dyn.Path{{}}, + }) +} + +const wrontTypeJsonData = `{ + "job_id": 123, + "new_settings": { + "name": "new job", + "email_notifications": { + "on_start": [], + "on_success": [], + "on_failure": [] + }, + "notification_settings": { + "no_alert_for_skipped_runs": true, + "no_alert_for_canceled_runs": true + }, + "timeout_seconds": "wrong_type", + "max_concurrent_runs": {}, + "tasks": [ + { + "task_key": "new task", + "email_notifications": {}, + "notification_settings": {}, + "timeout_seconds": 0, + "max_retries": 0, + "min_retry_interval_millis": 0, + "retry_on_timeout": "true" + } + ] + } +} +` + +func TestJsonUnmarshalWrongTypeReportsCorrectLocation(t *testing.T) { + var body JsonFlag + + var r jobs.ResetJob + err := body.Set(`{ + "job_id": [1, 2, 3] +} +`) + require.NoError(t, err) + + diags := body.Unmarshal(&r) + assert.NoError(t, diags.Error()) + assert.NotEmpty(t, diags) + + assert.Contains(t, diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "expected int, found sequence", + Locations: []dyn.Location{ + { + File: "(inline)", + Line: 2, + Column: 15, + }, + }, + Paths: []dyn.Path{dyn.NewPath(dyn.Key("job_id"))}, + }) +} + +func TestJsonUnmarshalArrayInsteadOfIntReportsCorrectLocation(t *testing.T) { + var body JsonFlag + + var r jobs.ResetJob + err := body.Set(wrontTypeJsonData) + require.NoError(t, err) + + diags := body.Unmarshal(&r) + assert.NoError(t, diags.Error()) + assert.NotEmpty(t, diags) + + assert.Contains(t, diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "cannot parse \"wrong_type\" as an integer", + Locations: []dyn.Location{ + { + File: "(inline)", + Line: 14, + Column: 40, + }, + }, + Paths: []dyn.Path{dyn.NewPath(dyn.Key("new_settings"), dyn.Key("timeout_seconds"))}, + }) +} + +func TestJsonUnmarshalForRequestWithForceSendFields(t *testing.T) { + var body JsonFlag + + var r jobs.ResetJob + err := body.Set(jsonData) + require.NoError(t, err) + + diags := body.Unmarshal(&r) + assert.NoError(t, diags.Error()) + assert.Empty(t, diags) + + assert.Equal(t, false, r.NewSettings.NotificationSettings.NoAlertForSkippedRuns) + assert.Equal(t, false, r.NewSettings.NotificationSettings.NoAlertForCanceledRuns) + assert.NotContains(t, r.NewSettings.NotificationSettings.ForceSendFields, "NoAlertForSkippedRuns") + assert.Contains(t, r.NewSettings.NotificationSettings.ForceSendFields, "NoAlertForCanceledRuns") } From ab206242060a05bd7a3b5073cc3a6869973446f3 Mon Sep 17 00:00:00 2001 From: shreyas-goenka <88374338+shreyas-goenka@users.noreply.github.com> Date: Mon, 14 Oct 2024 21:49:48 +0530 Subject: [PATCH 19/24] Assert SDK version is consistent in the CLI generation process (#1814) ## Changes Followup from https://github.com/databricks/cli/pull/1809#discussion_r1790045086. User will see the following error if the SDK version does not match during CLI code generation. ``` --- FAIL: TestConsistentDatabricksSdkVersion (1.34s) info_test.go:53: Error Trace: /Users/shreyas.goenka/cli/internal/build/info_test.go:53 Error: Not equal: expected: "0c86ea6dbd9a730c24ff0d4e509603e476955ac5" actual : "6f6b1371e640f2dfeba72d365ac566368656f6b6" Diff: --- Expected +++ Actual @@ -1 +1 @@ -0c86ea6dbd9a730c24ff0d4e509603e476955ac5 +6f6b1371e640f2dfeba72d365ac566368656f6b6 Test: TestConsistentDatabricksSdkVersion Messages: please update the SDK version before generating the CLI ``` ## Tests Manually asserted that: 1. Generating the CLI without the SDK bump causes an error. 2. Generating the CLI after the SDK bump works as expected. 3. The test works even when the SDK is pinned to a specific commit like `v0.47.1-0.20241002195128-6cecc224cbf7` --- .codegen.json | 1 + internal/build/sdk_consistency_test.go | 73 ++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 internal/build/sdk_consistency_test.go diff --git a/.codegen.json b/.codegen.json index 077d072b..4524ab55 100644 --- a/.codegen.json +++ b/.codegen.json @@ -11,6 +11,7 @@ "toolchain": { "required": ["go"], "post_generate": [ + "go test -timeout 240s -run TestConsistentDatabricksSdkVersion github.com/databricks/cli/internal/build", "go run ./bundle/internal/schema/*.go ./bundle/schema/jsonschema.json", "echo 'bundle/internal/tf/schema/\\*.go linguist-generated=true' >> ./.gitattributes", "echo 'go.sum linguist-generated=true' >> ./.gitattributes", diff --git a/internal/build/sdk_consistency_test.go b/internal/build/sdk_consistency_test.go new file mode 100644 index 00000000..37f60250 --- /dev/null +++ b/internal/build/sdk_consistency_test.go @@ -0,0 +1,73 @@ +package build + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/mod/modfile" +) + +// This test ensures that the OpenAPI SHA the CLI is being generated from matches +// the OpenAPI SHA of the Go SDK version used in the CLI. We should always upgrade +// the Go SDK version before generating the CLI because downstream generated assets +// like the bundle schema depend on the Go SDK itself. +func TestConsistentDatabricksSdkVersion(t *testing.T) { + // Read the go.mod file + b, err := os.ReadFile("../../go.mod") + require.NoError(t, err) + + // Parse the go.mod file to get the databricks-sdk version + modFile, err := modfile.Parse("../../go.mod", b, nil) + require.NoError(t, err) + + modulePath := "github.com/databricks/databricks-sdk-go" + var version string + for _, r := range modFile.Require { + if r.Mod.Path == modulePath { + version = r.Mod.Version + } + } + require.NotEmpty(t, version) + + // Full path of the package. For example: github.com/databricks/databricks-sdk-go@v0.47.1-0.20241002195128-6cecc224cbf7 + fullPath := fmt.Sprintf("%s@%s", modulePath, version) + + type goListResponse struct { + Origin struct { + Hash string + } + } + + // Using the go CLI query for the git hash corresponding to the databricks-sdk-go version + cmd := exec.Command("go", "list", "-m", "-json", "-mod=readonly", fullPath) + out, err := cmd.Output() + require.NoError(t, err) + parsedOutput := new(goListResponse) + err = json.Unmarshal(out, parsedOutput) + require.NoError(t, err) + hash := parsedOutput.Origin.Hash + require.NotEmpty(t, hash) + + // Read the OpenAPI SHA from the Go SDK. + url := fmt.Sprintf("https://raw.githubusercontent.com/databricks/databricks-sdk-go/%s/.codegen/_openapi_sha", hash) + resp, err := http.Get(url) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, http.StatusOK, resp.StatusCode) + + sdkSha, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + cliSha, err := os.ReadFile("../../.codegen/_openapi_sha") + require.NoError(t, err) + + assert.Equal(t, strings.TrimSpace(string(cliSha)), strings.TrimSpace(string(sdkSha)), "please update the SDK version before generating the CLI") +} From 0753dfe2f488c834724e2bfdf1618ebe8db271ab Mon Sep 17 00:00:00 2001 From: Andrew Nester Date: Tue, 15 Oct 2024 14:10:02 +0200 Subject: [PATCH 20/24] Fixed unmarshalling json input into `interface{}` type (#1832) ## Changes Fixed unmarshalling json input into `interface{}` type Commands like `api post` support free form request input, so it should be unmarshaled correctly ## Tests Added regression test + E2E test pass --- libs/flags/json_flag.go | 24 +++++++++++++++++++----- libs/flags/json_flag_test.go | 14 +++++--------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/libs/flags/json_flag.go b/libs/flags/json_flag.go index 0ed8be74..0324f114 100644 --- a/libs/flags/json_flag.go +++ b/libs/flags/json_flag.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "reflect" "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/dyn/convert" @@ -63,11 +64,24 @@ func (j *JsonFlag) Unmarshal(v any) diag.Diagnostics { return diags.Extend(diag.FromErr(err)) } - // Finally unmarshal the normalized data to the output. - // It will fill in the ForceSendFields field if the struct contains it. - err = marshal.Unmarshal(data, v) - if err != nil { - return diags.Extend(diag.FromErr(err)) + kind := reflect.ValueOf(v).Kind() + if kind == reflect.Ptr { + kind = reflect.ValueOf(v).Elem().Kind() + } + + if kind == reflect.Struct { + // Finally unmarshal the normalized data to the output. + // It will fill in the ForceSendFields field if the struct contains it. + err = marshal.Unmarshal(data, v) + if err != nil { + return diags.Extend(diag.FromErr(err)) + } + } else { + // If the output is not a struct, just unmarshal the data to the output. + err = json.Unmarshal(data, v) + if err != nil { + return diags.Extend(diag.FromErr(err)) + } } return diags diff --git a/libs/flags/json_flag_test.go b/libs/flags/json_flag_test.go index 16769f4e..77530086 100644 --- a/libs/flags/json_flag_test.go +++ b/libs/flags/json_flag_test.go @@ -13,10 +13,6 @@ import ( "github.com/stretchr/testify/require" ) -type requestType struct { - Foo string `json:"foo"` -} - func TestJsonFlagEmpty(t *testing.T) { var body JsonFlag @@ -35,13 +31,13 @@ func TestJsonFlagInline(t *testing.T) { err := body.Set(`{"foo": "bar"}`) assert.NoError(t, err) - var request requestType + var request any diags := body.Unmarshal(&request) assert.NoError(t, diags.Error()) assert.Empty(t, diags) assert.Equal(t, "JSON (14 bytes)", body.String()) - assert.Equal(t, requestType{"bar"}, request) + assert.Equal(t, map[string]any{"foo": "bar"}, request) } func TestJsonFlagError(t *testing.T) { @@ -50,7 +46,7 @@ func TestJsonFlagError(t *testing.T) { err := body.Set(`{"foo":`) assert.NoError(t, err) - var request requestType + var request any diags := body.Unmarshal(&request) assert.EqualError(t, diags.Error(), "error decoding JSON at (inline):1:8: unexpected end of JSON input") assert.Equal(t, "JSON (7 bytes)", body.String()) @@ -58,7 +54,7 @@ func TestJsonFlagError(t *testing.T) { func TestJsonFlagFile(t *testing.T) { var body JsonFlag - var request requestType + var request any var fpath string var payload = []byte(`{"foo": "bar"}`) @@ -78,7 +74,7 @@ func TestJsonFlagFile(t *testing.T) { assert.NoError(t, diags.Error()) assert.Empty(t, diags) - assert.Equal(t, requestType{"bar"}, request) + assert.Equal(t, map[string]any{"foo": "bar"}, request) } const jsonData = ` From cc112961ce8aa3ced65635d5174aa38d5a27be8a Mon Sep 17 00:00:00 2001 From: shreyas-goenka <88374338+shreyas-goenka@users.noreply.github.com> Date: Wed, 16 Oct 2024 18:20:17 +0530 Subject: [PATCH 21/24] Fix `TestAccFsMkdirWhenFileExistsAtPath` in isolated Azure environments (#1833) ## Changes This test passes on normal `azure-prod` but started to fail on `azure-prod-is`, which is the isolated version of azure-prod. This PR patches the test to include the error returned from the cloud setup in `azure-prod-is`. ## Tests The test passes now on `azure-prod-is`. --- internal/fs_mkdir_test.go | 4 ++-- internal/helpers.go | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/internal/fs_mkdir_test.go b/internal/fs_mkdir_test.go index dd75c7c3..9191f614 100644 --- a/internal/fs_mkdir_test.go +++ b/internal/fs_mkdir_test.go @@ -112,8 +112,8 @@ func TestAccFsMkdirWhenFileExistsAtPath(t *testing.T) { // assert mkdir fails _, _, err = RequireErrorRun(t, "fs", "mkdir", path.Join(tmpDir, "hello")) - // Different cloud providers return different errors. - regex := regexp.MustCompile(`(^|: )Path is a file: .*$|(^|: )Cannot create directory .* because .* is an existing file\.$|(^|: )mkdirs\(hadoopPath: .*, permission: rwxrwxrwx\): failed$`) + // Different cloud providers or cloud configurations return different errors. + regex := regexp.MustCompile(`(^|: )Path is a file: .*$|(^|: )Cannot create directory .* because .* is an existing file\.$|(^|: )mkdirs\(hadoopPath: .*, permission: rwxrwxrwx\): failed$|(^|: )"The specified path already exists.".*$`) assert.Regexp(t, regex, err.Error()) }) diff --git a/internal/helpers.go b/internal/helpers.go index 9387706b..3bf38775 100644 --- a/internal/helpers.go +++ b/internal/helpers.go @@ -20,6 +20,7 @@ import ( "time" "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/internal/acc" "github.com/databricks/cli/libs/flags" "github.com/databricks/cli/cmd" @@ -591,13 +592,10 @@ func setupWsfsExtensionsFiler(t *testing.T) (filer.Filer, string) { } func setupDbfsFiler(t *testing.T) (filer.Filer, string) { - t.Log(GetEnvOrSkipTest(t, "CLOUD_ENV")) + _, wt := acc.WorkspaceTest(t) - w, err := databricks.NewWorkspaceClient() - require.NoError(t, err) - - tmpDir := TemporaryDbfsDir(t, w) - f, err := filer.NewDbfsClient(w, tmpDir) + tmpDir := TemporaryDbfsDir(t, wt.W) + f, err := filer.NewDbfsClient(wt.W, tmpDir) require.NoError(t, err) return f, path.Join("dbfs:/", tmpDir) From e4d039a1aa6fb7b29dd029a58aeae9b137fdb179 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Thu, 17 Oct 2024 12:00:40 +0200 Subject: [PATCH 22/24] Handle normalization of `dyn.KindTime` into an any type (#1836) ## Changes The issue reported in #1828 illustrates how using a YAML timestamp-like value (a date in this case) causes an issue during conversion to and from the typed configuration tree. We use the `AsAny()` function on the `dyn.Value` when normalizing for the `any` type. We only use the `any` type for variable values, because they can assume every type. The `AsAny()` function returns a `time.Time` for the time value during conversion **to** the typed configuration tree. Upon conversion **from** the typed configuration tree back into the dynamic configuration tree, we cannot distinguish a `time.Time` struct from any other struct. To address this, we use the underlying string value of the time value when we normalize for the `any` type. Fixes #1828. ## Tests Existing unit tests pass --- bundle/tests/issue_1828/databricks.yml | 33 +++++++++++++++++ bundle/tests/issue_1828_test.go | 48 +++++++++++++++++++++++++ libs/dyn/convert/normalize.go | 30 +++++++++++++++- libs/dyn/convert/normalize_test.go | 50 +++++++++++++++++--------- 4 files changed, 143 insertions(+), 18 deletions(-) create mode 100644 bundle/tests/issue_1828/databricks.yml create mode 100644 bundle/tests/issue_1828_test.go diff --git a/bundle/tests/issue_1828/databricks.yml b/bundle/tests/issue_1828/databricks.yml new file mode 100644 index 00000000..d5f60ce7 --- /dev/null +++ b/bundle/tests/issue_1828/databricks.yml @@ -0,0 +1,33 @@ +bundle: + name: issue_1828 + +variables: + # One entry for each of the underlying YAML (or [dyn.Kind]) types. + # The test confirms we can convert to and from the typed configuration without losing information. + + map: + default: + foo: bar + + sequence: + default: + - foo + - bar + + string: + default: foo + + bool: + default: true + + int: + default: 42 + + float: + default: 3.14 + + time: + default: 2021-01-01 + + nil: + default: diff --git a/bundle/tests/issue_1828_test.go b/bundle/tests/issue_1828_test.go new file mode 100644 index 00000000..5f2becce --- /dev/null +++ b/bundle/tests/issue_1828_test.go @@ -0,0 +1,48 @@ +package config_tests + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIssue1828(t *testing.T) { + b := load(t, "./issue_1828") + + if assert.Contains(t, b.Config.Variables, "map") { + assert.Equal(t, map[string]any{ + "foo": "bar", + }, b.Config.Variables["map"].Default) + } + + if assert.Contains(t, b.Config.Variables, "sequence") { + assert.Equal(t, []any{ + "foo", + "bar", + }, b.Config.Variables["sequence"].Default) + } + + if assert.Contains(t, b.Config.Variables, "string") { + assert.Equal(t, "foo", b.Config.Variables["string"].Default) + } + + if assert.Contains(t, b.Config.Variables, "bool") { + assert.Equal(t, true, b.Config.Variables["bool"].Default) + } + + if assert.Contains(t, b.Config.Variables, "int") { + assert.Equal(t, 42, b.Config.Variables["int"].Default) + } + + if assert.Contains(t, b.Config.Variables, "float") { + assert.Equal(t, 3.14, b.Config.Variables["float"].Default) + } + + if assert.Contains(t, b.Config.Variables, "time") { + assert.Equal(t, "2021-01-01", b.Config.Variables["time"].Default) + } + + if assert.Contains(t, b.Config.Variables, "nil") { + assert.Equal(t, nil, b.Config.Variables["nil"].Default) + } +} diff --git a/libs/dyn/convert/normalize.go b/libs/dyn/convert/normalize.go index bc80a150..106add35 100644 --- a/libs/dyn/convert/normalize.go +++ b/libs/dyn/convert/normalize.go @@ -398,6 +398,34 @@ func (n normalizeOptions) normalizeFloat(typ reflect.Type, src dyn.Value, path d return dyn.NewValue(out, src.Locations()), diags } -func (n normalizeOptions) normalizeInterface(typ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) { +func (n normalizeOptions) normalizeInterface(_ reflect.Type, src dyn.Value, path dyn.Path) (dyn.Value, diag.Diagnostics) { + // Deal with every [dyn.Kind] here to ensure completeness. + switch src.Kind() { + case dyn.KindMap: + // Fall through + case dyn.KindSequence: + // Fall through + case dyn.KindString: + // Fall through + case dyn.KindBool: + // Fall through + case dyn.KindInt: + // Fall through + case dyn.KindFloat: + // Fall through + case dyn.KindTime: + // Conversion of a time value to an interface{}. + // The [dyn.Value.AsAny] equivalent for this kind is the [time.Time] struct. + // If we convert to a typed representation and back again, we cannot distinguish + // a [time.Time] struct from any other struct. + // + // Therefore, we normalize the time value to a string. + return dyn.NewValue(src.MustTime().String(), src.Locations()), nil + case dyn.KindNil: + // Fall through + default: + return dyn.InvalidValue, diag.Errorf("unsupported kind: %s", src.Kind()) + } + return src, nil } diff --git a/libs/dyn/convert/normalize_test.go b/libs/dyn/convert/normalize_test.go index 4b2a3c18..ab0a1cec 100644 --- a/libs/dyn/convert/normalize_test.go +++ b/libs/dyn/convert/normalize_test.go @@ -858,23 +858,7 @@ func TestNormalizeAnchors(t *testing.T) { }, vout.AsAny()) } -func TestNormalizeBoolToAny(t *testing.T) { - var typ any - vin := dyn.NewValue(false, []dyn.Location{{File: "file", Line: 1, Column: 1}}) - vout, err := Normalize(&typ, vin) - assert.Len(t, err, 0) - assert.Equal(t, dyn.NewValue(false, []dyn.Location{{File: "file", Line: 1, Column: 1}}), vout) -} - -func TestNormalizeIntToAny(t *testing.T) { - var typ any - vin := dyn.NewValue(10, []dyn.Location{{File: "file", Line: 1, Column: 1}}) - vout, err := Normalize(&typ, vin) - assert.Len(t, err, 0) - assert.Equal(t, dyn.NewValue(10, []dyn.Location{{File: "file", Line: 1, Column: 1}}), vout) -} - -func TestNormalizeSliceToAny(t *testing.T) { +func TestNormalizeAnyFromSlice(t *testing.T) { var typ any v1 := dyn.NewValue(1, []dyn.Location{{File: "file", Line: 1, Column: 1}}) v2 := dyn.NewValue(2, []dyn.Location{{File: "file", Line: 1, Column: 1}}) @@ -883,3 +867,35 @@ func TestNormalizeSliceToAny(t *testing.T) { assert.Len(t, err, 0) assert.Equal(t, dyn.NewValue([]dyn.Value{v1, v2}, []dyn.Location{{File: "file", Line: 1, Column: 1}}), vout) } + +func TestNormalizeAnyFromString(t *testing.T) { + var typ any + vin := dyn.NewValue("string", []dyn.Location{{File: "file", Line: 1, Column: 1}}) + vout, err := Normalize(&typ, vin) + assert.Len(t, err, 0) + assert.Equal(t, dyn.NewValue("string", []dyn.Location{{File: "file", Line: 1, Column: 1}}), vout) +} + +func TestNormalizeAnyFromBool(t *testing.T) { + var typ any + vin := dyn.NewValue(false, []dyn.Location{{File: "file", Line: 1, Column: 1}}) + vout, err := Normalize(&typ, vin) + assert.Len(t, err, 0) + assert.Equal(t, dyn.NewValue(false, []dyn.Location{{File: "file", Line: 1, Column: 1}}), vout) +} + +func TestNormalizeAnyFromInt(t *testing.T) { + var typ any + vin := dyn.NewValue(10, []dyn.Location{{File: "file", Line: 1, Column: 1}}) + vout, err := Normalize(&typ, vin) + assert.Len(t, err, 0) + assert.Equal(t, dyn.NewValue(10, []dyn.Location{{File: "file", Line: 1, Column: 1}}), vout) +} + +func TestNormalizeAnyFromTime(t *testing.T) { + var typ any + vin := dyn.NewValue(dyn.MustTime("2024-08-29"), []dyn.Location{{File: "file", Line: 1, Column: 1}}) + vout, err := Normalize(&typ, vin) + assert.Empty(t, err) + assert.Equal(t, dyn.NewValue("2024-08-29", vin.Locations()), vout) +} From dedec58e419463f71b6e6a727d0e7ba20572b3d2 Mon Sep 17 00:00:00 2001 From: Pieter Noordhuis Date: Thu, 17 Oct 2024 15:13:30 +0200 Subject: [PATCH 23/24] Add behavioral tests for examples from the YAML spec (#1835) ## Changes I took the examples from https://yaml.org/spec/1.2.2. The required modifications to the loader are: * Correctly parse floating point infinities and NaN * Correctly parse octal numbers per the YAML 1.2 spec * Treat "null" keys in a map as valid ## Tests Existing and new unit tests pass. --- libs/dyn/dynassert/dump.go | 60 ++ libs/dyn/yamlloader/loader.go | 60 +- .../yamlloader/testdata/spec_example_2.1.yml | 5 + .../yamlloader/testdata/spec_example_2.10.yml | 10 + .../yamlloader/testdata/spec_example_2.11.yml | 10 + .../yamlloader/testdata/spec_example_2.12.yml | 10 + .../yamlloader/testdata/spec_example_2.13.yml | 6 + .../yamlloader/testdata/spec_example_2.14.yml | 6 + .../yamlloader/testdata/spec_example_2.15.yml | 10 + .../yamlloader/testdata/spec_example_2.16.yml | 9 + .../yamlloader/testdata/spec_example_2.17.yml | 9 + .../yamlloader/testdata/spec_example_2.18.yml | 8 + .../yamlloader/testdata/spec_example_2.19.yml | 15 + .../yamlloader/testdata/spec_example_2.2.yml | 5 + .../yamlloader/testdata/spec_example_2.20.yml | 7 + .../yamlloader/testdata/spec_example_2.21.yml | 5 + .../yamlloader/testdata/spec_example_2.22.yml | 6 + .../yamlloader/testdata/spec_example_2.23.yml | 15 + .../yamlloader/testdata/spec_example_2.24.yml | 16 + .../yamlloader/testdata/spec_example_2.25.yml | 9 + .../yamlloader/testdata/spec_example_2.26.yml | 9 + .../yamlloader/testdata/spec_example_2.27.yml | 31 + .../yamlloader/testdata/spec_example_2.28.yml | 28 + .../yamlloader/testdata/spec_example_2.3.yml | 10 + .../yamlloader/testdata/spec_example_2.4.yml | 10 + .../yamlloader/testdata/spec_example_2.5.yml | 5 + .../yamlloader/testdata/spec_example_2.6.yml | 7 + .../yamlloader/testdata/spec_example_2.7.yml | 12 + .../yamlloader/testdata/spec_example_2.8.yml | 12 + .../yamlloader/testdata/spec_example_2.9.yml | 10 + libs/dyn/yamlloader/yaml_spec_test.go | 821 ++++++++++++++++++ 31 files changed, 1225 insertions(+), 11 deletions(-) create mode 100644 libs/dyn/dynassert/dump.go create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.1.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.10.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.11.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.12.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.13.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.14.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.15.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.16.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.17.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.18.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.19.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.2.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.20.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.21.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.22.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.23.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.24.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.25.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.26.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.27.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.28.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.3.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.4.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.5.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.6.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.7.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.8.yml create mode 100644 libs/dyn/yamlloader/testdata/spec_example_2.9.yml create mode 100644 libs/dyn/yamlloader/yaml_spec_test.go diff --git a/libs/dyn/dynassert/dump.go b/libs/dyn/dynassert/dump.go new file mode 100644 index 00000000..be0c7242 --- /dev/null +++ b/libs/dyn/dynassert/dump.go @@ -0,0 +1,60 @@ +package dynassert + +import ( + "fmt" + "strings" + + "github.com/databricks/cli/libs/dyn" +) + +// Dump returns the Go code to recreate the given value. +func Dump(v dyn.Value) string { + var sb strings.Builder + dump(v, &sb) + return sb.String() +} + +func dump(v dyn.Value, sb *strings.Builder) { + sb.WriteString("dyn.NewValue(\n") + + switch v.Kind() { + case dyn.KindMap: + sb.WriteString("map[string]dyn.Value{") + m := v.MustMap() + for _, p := range m.Pairs() { + sb.WriteString(fmt.Sprintf("\n%q: ", p.Key.MustString())) + dump(p.Value, sb) + sb.WriteByte(',') + } + sb.WriteString("\n},\n") + case dyn.KindSequence: + sb.WriteString("[]dyn.Value{\n") + for _, e := range v.MustSequence() { + dump(e, sb) + sb.WriteByte(',') + } + sb.WriteString("},\n") + case dyn.KindString: + sb.WriteString(fmt.Sprintf("%q,\n", v.MustString())) + case dyn.KindBool: + sb.WriteString(fmt.Sprintf("%t,\n", v.MustBool())) + case dyn.KindInt: + sb.WriteString(fmt.Sprintf("%d,\n", v.MustInt())) + case dyn.KindFloat: + sb.WriteString(fmt.Sprintf("%f,\n", v.MustFloat())) + case dyn.KindTime: + sb.WriteString(fmt.Sprintf("dyn.NewTime(%q),\n", v.MustTime().String())) + case dyn.KindNil: + sb.WriteString("nil,\n") + default: + panic(fmt.Sprintf("unhandled kind: %v", v.Kind())) + } + + // Add location + sb.WriteString("[]dyn.Location{") + for _, l := range v.Locations() { + sb.WriteString(fmt.Sprintf("{File: %q, Line: %d, Column: %d},", l.File, l.Line, l.Column)) + } + sb.WriteString("},\n") + sb.WriteString(")") +} diff --git a/libs/dyn/yamlloader/loader.go b/libs/dyn/yamlloader/loader.go index c3e8d081..b4aaf0a7 100644 --- a/libs/dyn/yamlloader/loader.go +++ b/libs/dyn/yamlloader/loader.go @@ -105,6 +105,9 @@ func (d *loader) loadMapping(node *yaml.Node, loc dyn.Location) (dyn.Value, erro switch st { case "!!str": // OK + case "!!null": + // A literal unquoted "null" is treated as a null value by the YAML parser. + // However, when used as a key, it is treated as the string "null". case "!!merge": if merge != nil { panic("merge node already set") @@ -115,10 +118,11 @@ func (d *loader) loadMapping(node *yaml.Node, loc dyn.Location) (dyn.Value, erro return dyn.InvalidValue, errorf(loc, "invalid key tag: %v", st) } - k, err := d.load(key) - if err != nil { - return dyn.InvalidValue, err - } + k := dyn.NewValue(key.Value, []dyn.Location{{ + File: d.path, + Line: key.Line, + Column: key.Column, + }}) v, err := d.load(val) if err != nil { @@ -173,6 +177,14 @@ func (d *loader) loadMapping(node *yaml.Node, loc dyn.Location) (dyn.Value, erro return dyn.NewValue(out, []dyn.Location{loc}), nil } +func newIntValue(i64 int64, loc dyn.Location) dyn.Value { + // Use regular int type instead of int64 if possible. + if i64 >= math.MinInt32 && i64 <= math.MaxInt32 { + return dyn.NewValue(int(i64), []dyn.Location{loc}) + } + return dyn.NewValue(i64, []dyn.Location{loc}) +} + func (d *loader) loadScalar(node *yaml.Node, loc dyn.Location) (dyn.Value, error) { st := node.ShortTag() switch st { @@ -188,18 +200,44 @@ func (d *loader) loadScalar(node *yaml.Node, loc dyn.Location) (dyn.Value, error return dyn.InvalidValue, errorf(loc, "invalid bool value: %v", node.Value) } case "!!int": - i64, err := strconv.ParseInt(node.Value, 10, 64) - if err != nil { - return dyn.InvalidValue, errorf(loc, "invalid int value: %v", node.Value) + // Try to parse the an integer value in base 10. + // We trim leading zeros to avoid octal parsing of the "0" prefix. + // See "testdata/spec_example_2.19.yml" for background. + i64, err := strconv.ParseInt(strings.TrimLeft(node.Value, "0"), 10, 64) + if err == nil { + return newIntValue(i64, loc), nil } - // Use regular int type instead of int64 if possible. - if i64 >= math.MinInt32 && i64 <= math.MaxInt32 { - return dyn.NewValue(int(i64), []dyn.Location{loc}), nil + // Let the [ParseInt] function figure out the base. + i64, err = strconv.ParseInt(node.Value, 0, 64) + if err == nil { + return newIntValue(i64, loc), nil } - return dyn.NewValue(i64, []dyn.Location{loc}), nil + return dyn.InvalidValue, errorf(loc, "invalid int value: %v", node.Value) case "!!float": f64, err := strconv.ParseFloat(node.Value, 64) if err != nil { + // Deal with infinity prefixes. + v := strings.ToLower(node.Value) + switch { + case strings.HasPrefix(v, "+"): + v = strings.TrimPrefix(v, "+") + f64 = math.Inf(1) + case strings.HasPrefix(v, "-"): + v = strings.TrimPrefix(v, "-") + f64 = math.Inf(-1) + default: + // No prefix. + f64 = math.Inf(1) + } + + // Deal with infinity and NaN values. + switch v { + case ".inf": + return dyn.NewValue(f64, []dyn.Location{loc}), nil + case ".nan": + return dyn.NewValue(math.NaN(), []dyn.Location{loc}), nil + } + return dyn.InvalidValue, errorf(loc, "invalid float value: %v", node.Value) } return dyn.NewValue(f64, []dyn.Location{loc}), nil diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.1.yml b/libs/dyn/yamlloader/testdata/spec_example_2.1.yml new file mode 100644 index 00000000..c9e26274 --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.1.yml @@ -0,0 +1,5 @@ +# Example 2.1 Sequence of Scalars (ball players) + +- Mark McGwire +- Sammy Sosa +- Ken Griffey diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.10.yml b/libs/dyn/yamlloader/testdata/spec_example_2.10.yml new file mode 100644 index 00000000..a3459ded --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.10.yml @@ -0,0 +1,10 @@ +# Example 2.10 Node for “Sammy Sosa” appears twice in this document + +--- +hr: +- Mark McGwire +# Following node labeled SS +- &SS Sammy Sosa +rbi: +- *SS # Subsequent occurrence +- Ken Griffey diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.11.yml b/libs/dyn/yamlloader/testdata/spec_example_2.11.yml new file mode 100644 index 00000000..e3c5c115 --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.11.yml @@ -0,0 +1,10 @@ +# Example 2.11 Mapping between Sequences + +? - Detroit Tigers + - Chicago cubs +: - 2001-07-23 + +? [ New York Yankees, + Atlanta Braves ] +: [ 2001-07-02, 2001-08-12, + 2001-08-14 ] diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.12.yml b/libs/dyn/yamlloader/testdata/spec_example_2.12.yml new file mode 100644 index 00000000..eb4a526f --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.12.yml @@ -0,0 +1,10 @@ +# Example 2.12 Compact Nested Mapping + +--- +# Products purchased +- item : Super Hoop + quantity: 1 +- item : Basketball + quantity: 4 +- item : Big Shoes + quantity: 1 diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.13.yml b/libs/dyn/yamlloader/testdata/spec_example_2.13.yml new file mode 100644 index 00000000..e55abff1 --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.13.yml @@ -0,0 +1,6 @@ +# Example 2.13 In literals, newlines are preserved + +# ASCII Art +--- | + \//||\/|| + // || ||__ diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.14.yml b/libs/dyn/yamlloader/testdata/spec_example_2.14.yml new file mode 100644 index 00000000..439fca30 --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.14.yml @@ -0,0 +1,6 @@ +# Example 2.14 In the folded scalars, newlines become spaces + +--- > + Mark McGwire's + year was crippled + by a knee injury. diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.15.yml b/libs/dyn/yamlloader/testdata/spec_example_2.15.yml new file mode 100644 index 00000000..266e7ce4 --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.15.yml @@ -0,0 +1,10 @@ +# Example 2.15 Folded newlines are preserved for “more indented” and blank lines + +--- > + Sammy Sosa completed another + fine season with great stats. + + 63 Home Runs + 0.288 Batting Average + + What a year! diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.16.yml b/libs/dyn/yamlloader/testdata/spec_example_2.16.yml new file mode 100644 index 00000000..6db6b087 --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.16.yml @@ -0,0 +1,9 @@ +# Example 2.16 Indentation determines scope + +name: Mark McGwire +accomplishment: > + Mark set a major league + home run record in 1998. +stats: | + 65 Home Runs + 0.278 Batting Average diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.17.yml b/libs/dyn/yamlloader/testdata/spec_example_2.17.yml new file mode 100644 index 00000000..af0777ab --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.17.yml @@ -0,0 +1,9 @@ +# Example 2.17 Quoted Scalars + +unicode: "Sosa did fine.\u263A" +control: "\b1998\t1999\t2000\n" +hex esc: "\x0d\x0a is \r\n" + +single: '"Howdy!" he cried.' +quoted: ' # Not a ''comment''.' +tie-fighter: '|\-*-/|' diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.18.yml b/libs/dyn/yamlloader/testdata/spec_example_2.18.yml new file mode 100644 index 00000000..741bcd8c --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.18.yml @@ -0,0 +1,8 @@ +# Example 2.18 Multi-line Flow Scalars + +plain: + This unquoted scalar + spans many lines. + +quoted: "So does this + quoted scalar.\n" diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.19.yml b/libs/dyn/yamlloader/testdata/spec_example_2.19.yml new file mode 100644 index 00000000..6ed95e09 --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.19.yml @@ -0,0 +1,15 @@ +# Example 2.19 Integers + +canonical: 12345 +decimal: +12345 +octal: 0o14 +hexadecimal: 0xC + +# Note: this example is not part of the spec but added for completeness. +# +# Octal numbers: +# - YAML 1.1: prefix is "0" +# - YAML 1.2: prefix is "0o" +# The "gopkg.in/yaml.v3" package accepts both for backwards compat. +# We accept only the YAML 1.2 prefix "0o". +octal11: 012345 diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.2.yml b/libs/dyn/yamlloader/testdata/spec_example_2.2.yml new file mode 100644 index 00000000..29c16105 --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.2.yml @@ -0,0 +1,5 @@ +# Example 2.2 Mapping Scalars to Scalars (player statistics) + +hr: 65 # Home runs +avg: 0.278 # Batting average +rbi: 147 # Runs Batted In diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.20.yml b/libs/dyn/yamlloader/testdata/spec_example_2.20.yml new file mode 100644 index 00000000..77a79a0c --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.20.yml @@ -0,0 +1,7 @@ +# Example 2.20 Floating Point + +canonical: 1.23015e+3 +exponential: 12.3015e+02 +fixed: 1230.15 +negative infinity: -.inf +not a number: .nan diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.21.yml b/libs/dyn/yamlloader/testdata/spec_example_2.21.yml new file mode 100644 index 00000000..cdb423c5 --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.21.yml @@ -0,0 +1,5 @@ +# Example 2.21 Miscellaneous + +null: +booleans: [ true, false ] +string: '012345' diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.22.yml b/libs/dyn/yamlloader/testdata/spec_example_2.22.yml new file mode 100644 index 00000000..bef2addf --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.22.yml @@ -0,0 +1,6 @@ +# Example 2.22 Timestamps + +canonical: 2001-12-15T02:59:43.1Z +iso8601: 2001-12-14t21:59:43.10-05:00 +spaced: 2001-12-14 21:59:43.10 -5 +date: 2002-12-14 diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.23.yml b/libs/dyn/yamlloader/testdata/spec_example_2.23.yml new file mode 100644 index 00000000..56e9898e --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.23.yml @@ -0,0 +1,15 @@ +# Example 2.23 Various Explicit Tags + +--- +not-date: !!str 2002-04-28 + +picture: !!binary | + R0lGODlhDAAMAIQAAP//9/X + 17unp5WZmZgAAAOfn515eXv + Pz7Y6OjuDg4J+fn5OTk6enp + 56enmleECcgggoBADs= + +application specific tag: !something | + The semantics of the tag + above may be different for + different documents. diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.24.yml b/libs/dyn/yamlloader/testdata/spec_example_2.24.yml new file mode 100644 index 00000000..f7c11d0f --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.24.yml @@ -0,0 +1,16 @@ +# Example 2.24 Global Tags + +%TAG ! tag:clarkevans.com,2002: +--- !shape + # Use the ! handle for presenting + # tag:clarkevans.com,2002:circle +- !circle + center: &ORIGIN {x: 73, y: 129} + radius: 7 +- !line + start: *ORIGIN + finish: { x: 89, y: 102 } +- !label + start: *ORIGIN + color: 0xFFEEBB + text: Pretty vector drawing. diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.25.yml b/libs/dyn/yamlloader/testdata/spec_example_2.25.yml new file mode 100644 index 00000000..73bac862 --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.25.yml @@ -0,0 +1,9 @@ +# Example 2.25 Unordered Sets + +# Sets are represented as a +# Mapping where each key is +# associated with a null value +--- !!set +? Mark McGwire +? Sammy Sosa +? Ken Griffey diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.26.yml b/libs/dyn/yamlloader/testdata/spec_example_2.26.yml new file mode 100644 index 00000000..00863a6b --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.26.yml @@ -0,0 +1,9 @@ +# Example 2.26 Ordered Mappings + +# Ordered maps are represented as +# A sequence of mappings, with +# each mapping having one key +--- !!omap +- Mark McGwire: 65 +- Sammy Sosa: 63 +- Ken Griffey: 58 diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.27.yml b/libs/dyn/yamlloader/testdata/spec_example_2.27.yml new file mode 100644 index 00000000..fc9b460c --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.27.yml @@ -0,0 +1,31 @@ +# Example 2.27 Invoice + +--- ! +invoice: 34843 +date : 2001-01-23 +bill-to: &id001 + given : Chris + family : Dumars + address: + lines: | + 458 Walkman Dr. + Suite #292 + city : Royal Oak + state : MI + postal : 48046 +ship-to: *id001 +product: +- sku : BL394D + quantity : 4 + description : Basketball + price : 450.00 +- sku : BL4438H + quantity : 1 + description : Super Hoop + price : 2392.00 +tax : 251.42 +total: 4443.52 +comments: + Late afternoon is best. + Backup contact is Nancy + Billsmer @ 338-4338. diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.28.yml b/libs/dyn/yamlloader/testdata/spec_example_2.28.yml new file mode 100644 index 00000000..35369472 --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.28.yml @@ -0,0 +1,28 @@ +# Example 2.28 Log File + +--- +Time: 2001-11-23 15:01:42 -5 +User: ed +Warning: + This is an error message + for the log file +--- +Time: 2001-11-23 15:02:31 -5 +User: ed +Warning: + A slightly different error + message. +--- +Date: 2001-11-23 15:03:17 -5 +User: ed +Fatal: + Unknown variable "bar" +Stack: +- file: TopClass.py + line: 23 + code: | + x = MoreObject("345\n") +- file: MoreClass.py + line: 58 + code: |- + foo = bar diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.3.yml b/libs/dyn/yamlloader/testdata/spec_example_2.3.yml new file mode 100644 index 00000000..70cbe07d --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.3.yml @@ -0,0 +1,10 @@ +# Example 2.3 Mapping Scalars to Sequences (ball clubs in each league) + +american: +- Boston Red Sox +- Detroit Tigers +- New York Yankees +national: +- New York Mets +- Chicago Cubs +- Atlanta Braves diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.4.yml b/libs/dyn/yamlloader/testdata/spec_example_2.4.yml new file mode 100644 index 00000000..cce28625 --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.4.yml @@ -0,0 +1,10 @@ +# Example 2.4 Sequence of Mappings (players’ statistics) + +- + name: Mark McGwire + hr: 65 + avg: 0.278 +- + name: Sammy Sosa + hr: 63 + avg: 0.288 diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.5.yml b/libs/dyn/yamlloader/testdata/spec_example_2.5.yml new file mode 100644 index 00000000..a585faee --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.5.yml @@ -0,0 +1,5 @@ +# Example 2.5 Sequence of Sequences + +- [name , hr, avg ] +- [Mark McGwire, 65, 0.278] +- [Sammy Sosa , 63, 0.288] diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.6.yml b/libs/dyn/yamlloader/testdata/spec_example_2.6.yml new file mode 100644 index 00000000..cc137e5d --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.6.yml @@ -0,0 +1,7 @@ +# Example 2.6 Mapping of Mappings + +Mark McGwire: {hr: 65, avg: 0.278} +Sammy Sosa: { + hr: 63, + avg: 0.288, + } diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.7.yml b/libs/dyn/yamlloader/testdata/spec_example_2.7.yml new file mode 100644 index 00000000..35c2541d --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.7.yml @@ -0,0 +1,12 @@ +# Example 2.7 Two Documents in a Stream (each with a leading comment) + +# Ranking of 1998 home runs +--- +- Mark McGwire +- Sammy Sosa +- Ken Griffey + +# Team ranking +--- +- Chicago Cubs +- St Louis Cardinals diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.8.yml b/libs/dyn/yamlloader/testdata/spec_example_2.8.yml new file mode 100644 index 00000000..ae6e8bf2 --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.8.yml @@ -0,0 +1,12 @@ +# Example 2.8 Play by Play Feed from a Game + +--- +time: 20:03:20 +player: Sammy Sosa +action: strike (miss) +... +--- +time: 20:03:47 +player: Sammy Sosa +action: grand slam +... diff --git a/libs/dyn/yamlloader/testdata/spec_example_2.9.yml b/libs/dyn/yamlloader/testdata/spec_example_2.9.yml new file mode 100644 index 00000000..75217b25 --- /dev/null +++ b/libs/dyn/yamlloader/testdata/spec_example_2.9.yml @@ -0,0 +1,10 @@ +# Example 2.9 Single Document with Two Comments + +--- +hr: # 1998 hr ranking +- Mark McGwire +- Sammy Sosa +# 1998 rbi ranking +rbi: +- Sammy Sosa +- Ken Griffey diff --git a/libs/dyn/yamlloader/yaml_spec_test.go b/libs/dyn/yamlloader/yaml_spec_test.go new file mode 100644 index 00000000..2a5ae817 --- /dev/null +++ b/libs/dyn/yamlloader/yaml_spec_test.go @@ -0,0 +1,821 @@ +package yamlloader_test + +import ( + "bytes" + "math" + "os" + "testing" + + "github.com/databricks/cli/libs/dyn" + assert "github.com/databricks/cli/libs/dyn/dynassert" + "github.com/databricks/cli/libs/dyn/yamlloader" + "github.com/stretchr/testify/require" +) + +const NL = "\n" + +func loadExample(t *testing.T, file string) dyn.Value { + input, err := os.ReadFile(file) + require.NoError(t, err) + self, err := yamlloader.LoadYAML(file, bytes.NewBuffer(input)) + require.NoError(t, err) + return self +} + +func TestYAMLSpecExample_2_1(t *testing.T) { + file := "testdata/spec_example_2.1.yml" + self := loadExample(t, file) + + assert.Equal(t, dyn.NewValue( + []dyn.Value{ + dyn.NewValue("Mark McGwire", []dyn.Location{{File: file, Line: 3, Column: 3}}), + dyn.NewValue("Sammy Sosa", []dyn.Location{{File: file, Line: 4, Column: 3}}), + dyn.NewValue("Ken Griffey", []dyn.Location{{File: file, Line: 5, Column: 3}}), + }, + []dyn.Location{{File: file, Line: 3, Column: 1}}, + ), self) +} + +func TestYAMLSpecExample_2_2(t *testing.T) { + file := "testdata/spec_example_2.2.yml" + self := loadExample(t, file) + + assert.Equal(t, dyn.NewValue( + map[string]dyn.Value{ + "hr": dyn.NewValue(65, []dyn.Location{{File: file, Line: 3, Column: 6}}), + "avg": dyn.NewValue(0.278, []dyn.Location{{File: file, Line: 4, Column: 6}}), + "rbi": dyn.NewValue(147, []dyn.Location{{File: file, Line: 5, Column: 6}}), + }, + []dyn.Location{{File: file, Line: 3, Column: 1}}, + ), self) +} + +func TestYAMLSpecExample_2_3(t *testing.T) { + file := "testdata/spec_example_2.3.yml" + self := loadExample(t, file) + + assert.Equal(t, dyn.NewValue( + map[string]dyn.Value{ + "american": dyn.NewValue( + []dyn.Value{ + dyn.NewValue("Boston Red Sox", []dyn.Location{{File: file, Line: 4, Column: 3}}), + dyn.NewValue("Detroit Tigers", []dyn.Location{{File: file, Line: 5, Column: 3}}), + dyn.NewValue("New York Yankees", []dyn.Location{{File: file, Line: 6, Column: 3}}), + }, + []dyn.Location{{File: file, Line: 4, Column: 1}}, + ), + "national": dyn.NewValue( + []dyn.Value{ + dyn.NewValue("New York Mets", []dyn.Location{{File: file, Line: 8, Column: 3}}), + dyn.NewValue("Chicago Cubs", []dyn.Location{{File: file, Line: 9, Column: 3}}), + dyn.NewValue("Atlanta Braves", []dyn.Location{{File: file, Line: 10, Column: 3}}), + }, + []dyn.Location{{File: file, Line: 8, Column: 1}}, + ), + }, + []dyn.Location{{File: file, Line: 3, Column: 1}}, + ), self) +} + +func TestYAMLSpecExample_2_4(t *testing.T) { + file := "testdata/spec_example_2.4.yml" + self := loadExample(t, file) + + assert.Equal(t, dyn.NewValue( + []dyn.Value{ + dyn.NewValue( + map[string]dyn.Value{ + "name": dyn.NewValue("Mark McGwire", []dyn.Location{{File: file, Line: 4, Column: 9}}), + "hr": dyn.NewValue(65, []dyn.Location{{File: file, Line: 5, Column: 9}}), + "avg": dyn.NewValue(0.278, []dyn.Location{{File: file, Line: 6, Column: 9}}), + }, + []dyn.Location{{File: file, Line: 4, Column: 3}}, + ), + dyn.NewValue( + map[string]dyn.Value{ + "name": dyn.NewValue("Sammy Sosa", []dyn.Location{{File: file, Line: 8, Column: 9}}), + "hr": dyn.NewValue(63, []dyn.Location{{File: file, Line: 9, Column: 9}}), + "avg": dyn.NewValue(0.288, []dyn.Location{{File: file, Line: 10, Column: 9}}), + }, + []dyn.Location{{File: file, Line: 8, Column: 3}}, + ), + }, + []dyn.Location{{File: file, Line: 3, Column: 1}}, + ), self) +} + +func TestYAMLSpecExample_2_5(t *testing.T) { + file := "testdata/spec_example_2.5.yml" + self := loadExample(t, file) + + assert.Equal(t, dyn.NewValue( + []dyn.Value{ + dyn.NewValue( + []dyn.Value{ + dyn.NewValue("name", []dyn.Location{{File: file, Line: 3, Column: 4}}), + dyn.NewValue("hr", []dyn.Location{{File: file, Line: 3, Column: 18}}), + dyn.NewValue("avg", []dyn.Location{{File: file, Line: 3, Column: 22}}), + }, + []dyn.Location{{File: file, Line: 3, Column: 3}}, + ), + dyn.NewValue( + []dyn.Value{ + dyn.NewValue("Mark McGwire", []dyn.Location{{File: file, Line: 4, Column: 4}}), + dyn.NewValue(65, []dyn.Location{{File: file, Line: 4, Column: 18}}), + dyn.NewValue(0.278, []dyn.Location{{File: file, Line: 4, Column: 22}}), + }, + []dyn.Location{{File: file, Line: 4, Column: 3}}, + ), + dyn.NewValue( + []dyn.Value{ + dyn.NewValue("Sammy Sosa", []dyn.Location{{File: file, Line: 5, Column: 4}}), + dyn.NewValue(63, []dyn.Location{{File: file, Line: 5, Column: 18}}), + dyn.NewValue(0.288, []dyn.Location{{File: file, Line: 5, Column: 22}}), + }, + []dyn.Location{{File: file, Line: 5, Column: 3}}, + ), + }, + []dyn.Location{{File: file, Line: 3, Column: 1}}, + ), self) +} + +func TestYAMLSpecExample_2_6(t *testing.T) { + file := "testdata/spec_example_2.6.yml" + self := loadExample(t, file) + + assert.Equal(t, dyn.NewValue( + map[string]dyn.Value{ + "Mark McGwire": dyn.NewValue( + map[string]dyn.Value{ + "hr": dyn.NewValue(65, []dyn.Location{{File: file, Line: 3, Column: 20}}), + "avg": dyn.NewValue(0.278, []dyn.Location{{File: file, Line: 3, Column: 29}}), + }, + []dyn.Location{{File: file, Line: 3, Column: 15}}, + ), + "Sammy Sosa": dyn.NewValue( + map[string]dyn.Value{ + "hr": dyn.NewValue(63, []dyn.Location{{File: file, Line: 5, Column: 9}}), + "avg": dyn.NewValue(0.288, []dyn.Location{{File: file, Line: 6, Column: 10}}), + }, + []dyn.Location{{File: file, Line: 4, Column: 13}}, + ), + }, + []dyn.Location{{File: file, Line: 3, Column: 1}}, + ), self) +} + +func TestYAMLSpecExample_2_7(t *testing.T) { + file := "testdata/spec_example_2.7.yml" + self := loadExample(t, file) + + // Note: we do not support multiple documents in a single YAML file. + + assert.Equal(t, dyn.NewValue( + []dyn.Value{ + dyn.NewValue( + "Mark McGwire", + []dyn.Location{{File: file, Line: 5, Column: 3}}, + ), + dyn.NewValue( + "Sammy Sosa", + []dyn.Location{{File: file, Line: 6, Column: 3}}, + ), + dyn.NewValue( + "Ken Griffey", + []dyn.Location{{File: file, Line: 7, Column: 3}}, + ), + }, + []dyn.Location{{File: file, Line: 5, Column: 1}}, + ), self) +} + +func TestYAMLSpecExample_2_8(t *testing.T) { + file := "testdata/spec_example_2.8.yml" + self := loadExample(t, file) + + // Note: we do not support multiple documents in a single YAML file. + + assert.Equal(t, dyn.NewValue( + map[string]dyn.Value{ + "time": dyn.NewValue("20:03:20", []dyn.Location{{File: file, Line: 4, Column: 7}}), + "player": dyn.NewValue("Sammy Sosa", []dyn.Location{{File: file, Line: 5, Column: 9}}), + "action": dyn.NewValue("strike (miss)", []dyn.Location{{File: file, Line: 6, Column: 9}}), + }, + []dyn.Location{{File: file, Line: 4, Column: 1}}, + ), self) +} + +func TestYAMLSpecExample_2_9(t *testing.T) { + file := "testdata/spec_example_2.9.yml" + self := loadExample(t, file) + + // Note: we do not support multiple documents in a single YAML file. + + assert.Equal(t, dyn.NewValue( + map[string]dyn.Value{ + "hr": dyn.NewValue( + []dyn.Value{ + dyn.NewValue("Mark McGwire", []dyn.Location{{File: file, Line: 5, Column: 3}}), + dyn.NewValue("Sammy Sosa", []dyn.Location{{File: file, Line: 6, Column: 3}}), + }, + []dyn.Location{{File: file, Line: 5, Column: 1}}, + ), + "rbi": dyn.NewValue( + []dyn.Value{ + dyn.NewValue("Sammy Sosa", []dyn.Location{{File: file, Line: 9, Column: 3}}), + dyn.NewValue("Ken Griffey", []dyn.Location{{File: file, Line: 10, Column: 3}}), + }, + []dyn.Location{{File: file, Line: 9, Column: 1}}, + ), + }, + []dyn.Location{{File: file, Line: 4, Column: 1}}, + ), self) +} + +func TestYAMLSpecExample_2_10(t *testing.T) { + file := "testdata/spec_example_2.10.yml" + self := loadExample(t, file) + + assert.Equal(t, dyn.NewValue( + map[string]dyn.Value{ + "hr": dyn.NewValue( + []dyn.Value{ + dyn.NewValue("Mark McGwire", []dyn.Location{{File: file, Line: 5, Column: 3}}), + dyn.NewValue("Sammy Sosa", []dyn.Location{{File: file, Line: 7, Column: 3}}), + }, + []dyn.Location{{File: file, Line: 5, Column: 1}}, + ), + "rbi": dyn.NewValue( + []dyn.Value{ + // The location for an anchored value refers to the anchor, not the reference. + // This is the same location as the anchor that appears in the "hr" mapping. + dyn.NewValue("Sammy Sosa", []dyn.Location{{File: file, Line: 7, Column: 3}}), + dyn.NewValue("Ken Griffey", []dyn.Location{{File: file, Line: 10, Column: 3}}), + }, + []dyn.Location{{File: file, Line: 9, Column: 1}}, + ), + }, + []dyn.Location{{File: file, Line: 4, Column: 1}}, + ), self) +} + +func TestYAMLSpecExample_2_11(t *testing.T) { + file := "testdata/spec_example_2.11.yml" + input, err := os.ReadFile(file) + require.NoError(t, err) + + // Note: non-string mapping keys are not supported by "gopkg.in/yaml.v3". + _, err = yamlloader.LoadYAML(file, bytes.NewBuffer(input)) + assert.ErrorContains(t, err, `: key is not a scalar`) +} + +func TestYAMLSpecExample_2_12(t *testing.T) { + file := "testdata/spec_example_2.12.yml" + self := loadExample(t, file) + + assert.Equal(t, dyn.NewValue( + []dyn.Value{ + dyn.NewValue( + map[string]dyn.Value{ + "item": dyn.NewValue("Super Hoop", []dyn.Location{{File: file, Line: 5, Column: 13}}), + "quantity": dyn.NewValue(1, []dyn.Location{{File: file, Line: 6, Column: 13}}), + }, + []dyn.Location{{File: file, Line: 5, Column: 3}}, + ), + dyn.NewValue( + map[string]dyn.Value{ + "item": dyn.NewValue("Basketball", []dyn.Location{{File: file, Line: 7, Column: 13}}), + "quantity": dyn.NewValue(4, []dyn.Location{{File: file, Line: 8, Column: 13}}), + }, + []dyn.Location{{File: file, Line: 7, Column: 3}}, + ), + dyn.NewValue( + map[string]dyn.Value{ + "item": dyn.NewValue("Big Shoes", []dyn.Location{{File: file, Line: 9, Column: 13}}), + "quantity": dyn.NewValue(1, []dyn.Location{{File: file, Line: 10, Column: 13}}), + }, + []dyn.Location{{File: file, Line: 9, Column: 3}}, + ), + }, + []dyn.Location{{File: file, Line: 5, Column: 1}}, + ), self) +} + +func TestYAMLSpecExample_2_13(t *testing.T) { + file := "testdata/spec_example_2.13.yml" + self := loadExample(t, file) + + assert.Equal(t, dyn.NewValue( + ``+ + `\//||\/||`+NL+ + "// || ||__"+NL, + []dyn.Location{{File: file, Line: 4, Column: 5}}, + ), self) +} + +func TestYAMLSpecExample_2_14(t *testing.T) { + file := "testdata/spec_example_2.14.yml" + self := loadExample(t, file) + + assert.Equal(t, dyn.NewValue( + `Mark McGwire's year was crippled by a knee injury.`+NL, + []dyn.Location{{File: file, Line: 3, Column: 5}}, + ), self) +} + +func TestYAMLSpecExample_2_15(t *testing.T) { + file := "testdata/spec_example_2.15.yml" + self := loadExample(t, file) + + assert.Equal(t, dyn.NewValue( + ``+ + `Sammy Sosa completed another fine season with great stats.`+NL+ + NL+ + ` 63 Home Runs`+NL+ + ` 0.288 Batting Average`+NL+ + NL+ + `What a year!`+NL, + []dyn.Location{{File: file, Line: 3, Column: 5}}, + ), self) +} + +func TestYAMLSpecExample_2_16(t *testing.T) { + file := "testdata/spec_example_2.16.yml" + self := loadExample(t, file) + + assert.Equal(t, dyn.NewValue( + map[string]dyn.Value{ + "name": dyn.NewValue( + "Mark McGwire", + []dyn.Location{{File: file, Line: 3, Column: 7}}, + ), + "accomplishment": dyn.NewValue( + `Mark set a major league home run record in 1998.`+NL, + []dyn.Location{{File: file, Line: 4, Column: 17}}, + ), + "stats": dyn.NewValue( + ``+ + `65 Home Runs`+NL+ + `0.278 Batting Average`+NL, + []dyn.Location{{File: file, Line: 7, Column: 8}}, + ), + }, + []dyn.Location{{File: file, Line: 3, Column: 1}}, + ), self) +} + +func TestYAMLSpecExample_2_17(t *testing.T) { + file := "testdata/spec_example_2.17.yml" + self := loadExample(t, file) + + assert.Equal(t, dyn.NewValue( + map[string]dyn.Value{ + "unicode": dyn.NewValue( + `Sosa did fine.`+"\u263A", + []dyn.Location{{File: file, Line: 3, Column: 10}}, + ), + "control": dyn.NewValue( + "\b1998\t1999\t2000\n", + []dyn.Location{{File: file, Line: 4, Column: 10}}, + ), + "hex esc": dyn.NewValue( + "\x0d\x0a is \r\n", + []dyn.Location{{File: file, Line: 5, Column: 10}}, + ), + "single": dyn.NewValue( + `"Howdy!" he cried.`, + []dyn.Location{{File: file, Line: 7, Column: 9}}, + ), + "quoted": dyn.NewValue( + ` # Not a 'comment'.`, + []dyn.Location{{File: file, Line: 8, Column: 9}}, + ), + "tie-fighter": dyn.NewValue( + `|\-*-/|`, + []dyn.Location{{File: file, Line: 9, Column: 14}}, + ), + }, + []dyn.Location{{File: file, Line: 3, Column: 1}}, + ), self) +} + +func TestYAMLSpecExample_2_18(t *testing.T) { + file := "testdata/spec_example_2.18.yml" + self := loadExample(t, file) + + assert.Equal(t, dyn.NewValue( + map[string]dyn.Value{ + "plain": dyn.NewValue( + `This unquoted scalar spans many lines.`, + []dyn.Location{{File: file, Line: 4, Column: 3}}, + ), + "quoted": dyn.NewValue( + `So does this quoted scalar.`+NL, + []dyn.Location{{File: file, Line: 7, Column: 9}}, + ), + }, + []dyn.Location{{File: file, Line: 3, Column: 1}}, + ), self) +} + +func TestYAMLSpecExample_2_19(t *testing.T) { + file := "testdata/spec_example_2.19.yml" + self := loadExample(t, file) + + assert.Equal(t, dyn.NewValue( + map[string]dyn.Value{ + "canonical": dyn.NewValue( + 12345, + []dyn.Location{{File: file, Line: 3, Column: 12}}, + ), + "decimal": dyn.NewValue( + 12345, + []dyn.Location{{File: file, Line: 4, Column: 10}}, + ), + "octal": dyn.NewValue( + 12, + []dyn.Location{{File: file, Line: 5, Column: 8}}, + ), + "hexadecimal": dyn.NewValue( + 12, + []dyn.Location{{File: file, Line: 6, Column: 14}}, + ), + "octal11": dyn.NewValue( + 12345, + []dyn.Location{{File: file, Line: 15, Column: 10}}, + ), + }, + []dyn.Location{{File: file, Line: 3, Column: 1}}, + ), self) +} + +func TestYAMLSpecExample_2_20(t *testing.T) { + file := "testdata/spec_example_2.20.yml" + self := loadExample(t, file) + + // Equality assertion doesn't work with NaNs. + // See https://github.com/stretchr/testify/issues/624. + // + // Remove the NaN entry. + self, _ = dyn.Walk(self, func(p dyn.Path, v dyn.Value) (dyn.Value, error) { + if f, ok := v.AsFloat(); ok && math.IsNaN(f) { + return dyn.InvalidValue, dyn.ErrDrop + } + return v, nil + }) + + assert.Equal(t, dyn.NewValue( + map[string]dyn.Value{ + "canonical": dyn.NewValue( + 1230.15, + []dyn.Location{{File: file, Line: 3, Column: 12}}, + ), + "exponential": dyn.NewValue( + 1230.15, + []dyn.Location{{File: file, Line: 4, Column: 14}}, + ), + "fixed": dyn.NewValue( + 1230.15, + []dyn.Location{{File: file, Line: 5, Column: 8}}, + ), + "negative infinity": dyn.NewValue( + math.Inf(-1), + []dyn.Location{{File: file, Line: 6, Column: 20}}, + ), + }, + []dyn.Location{{File: file, Line: 3, Column: 1}}, + ), self) +} + +func TestYAMLSpecExample_2_21(t *testing.T) { + file := "testdata/spec_example_2.21.yml" + self := loadExample(t, file) + + assert.Equal(t, dyn.NewValue( + map[string]dyn.Value{ + "null": dyn.NewValue( + nil, + []dyn.Location{{File: file, Line: 3, Column: 6}}, + ), + "booleans": dyn.NewValue( + []dyn.Value{ + dyn.NewValue(true, []dyn.Location{{File: file, Line: 4, Column: 13}}), + dyn.NewValue(false, []dyn.Location{{File: file, Line: 4, Column: 19}}), + }, + []dyn.Location{{File: file, Line: 4, Column: 11}}, + ), + "string": dyn.NewValue( + "012345", + []dyn.Location{{File: file, Line: 5, Column: 9}}, + ), + }, + []dyn.Location{{File: file, Line: 3, Column: 1}}, + ), self) +} + +func TestYAMLSpecExample_2_22(t *testing.T) { + file := "testdata/spec_example_2.22.yml" + self := loadExample(t, file) + + assert.Equal(t, dyn.NewValue( + map[string]dyn.Value{ + "canonical": dyn.NewValue( + dyn.MustTime("2001-12-15T02:59:43.1Z"), + []dyn.Location{{File: file, Line: 3, Column: 12}}, + ), + "iso8601": dyn.NewValue( + dyn.MustTime("2001-12-14t21:59:43.10-05:00"), + []dyn.Location{{File: file, Line: 4, Column: 10}}, + ), + "spaced": dyn.NewValue( + // This is parsed as a string, not a timestamp, + // both by "gopkg.in/yaml.v3" and by our implementation. + "2001-12-14 21:59:43.10 -5", + []dyn.Location{{File: file, Line: 5, Column: 9}}, + ), + "date": dyn.NewValue( + dyn.MustTime("2002-12-14"), + []dyn.Location{{File: file, Line: 6, Column: 7}}, + ), + }, + []dyn.Location{{File: file, Line: 3, Column: 1}}, + ), self) +} + +func TestYAMLSpecExample_2_23(t *testing.T) { + file := "testdata/spec_example_2.23.yml" + input, err := os.ReadFile(file) + require.NoError(t, err) + + // Note: the !!binary tag is not supported by us. + + _, err = yamlloader.LoadYAML(file, bytes.NewBuffer(input)) + assert.ErrorContains(t, err, `: unknown tag: !!binary`) +} + +func TestYAMLSpecExample_2_24(t *testing.T) { + file := "testdata/spec_example_2.24.yml" + self := loadExample(t, file) + + assert.Equal(t, dyn.NewValue( + []dyn.Value{ + dyn.NewValue( + map[string]dyn.Value{ + "center": dyn.NewValue( + map[string]dyn.Value{ + "x": dyn.NewValue(73, []dyn.Location{{File: file, Line: 8, Column: 23}}), + "y": dyn.NewValue(129, []dyn.Location{{File: file, Line: 8, Column: 30}}), + }, + []dyn.Location{{File: file, Line: 8, Column: 11}}, + ), + "radius": dyn.NewValue(7, []dyn.Location{{File: file, Line: 9, Column: 11}}), + }, + []dyn.Location{{File: file, Line: 7, Column: 3}}, + ), + dyn.NewValue( + map[string]dyn.Value{ + "start": dyn.NewValue( + map[string]dyn.Value{ + "x": dyn.NewValue(73, []dyn.Location{{File: file, Line: 8, Column: 23}}), + "y": dyn.NewValue(129, []dyn.Location{{File: file, Line: 8, Column: 30}}), + }, + []dyn.Location{{File: file, Line: 8, Column: 11}}, + ), + "finish": dyn.NewValue( + map[string]dyn.Value{ + "x": dyn.NewValue(89, []dyn.Location{{File: file, Line: 12, Column: 16}}), + "y": dyn.NewValue(102, []dyn.Location{{File: file, Line: 12, Column: 23}}), + }, + []dyn.Location{{File: file, Line: 12, Column: 11}}, + ), + }, + []dyn.Location{{File: file, Line: 10, Column: 3}}, + ), + dyn.NewValue( + map[string]dyn.Value{ + "start": dyn.NewValue( + map[string]dyn.Value{ + "x": dyn.NewValue(73, []dyn.Location{{File: file, Line: 8, Column: 23}}), + "y": dyn.NewValue(129, []dyn.Location{{File: file, Line: 8, Column: 30}}), + }, + []dyn.Location{{File: file, Line: 8, Column: 11}}, + ), + "color": dyn.NewValue(16772795, []dyn.Location{{File: file, Line: 15, Column: 10}}), + "text": dyn.NewValue("Pretty vector drawing.", []dyn.Location{{File: file, Line: 16, Column: 9}}), + }, + []dyn.Location{{File: file, Line: 13, Column: 3}}, + ), + }, + []dyn.Location{{File: file, Line: 4, Column: 5}}, + ), self) +} + +func TestYAMLSpecExample_2_25(t *testing.T) { + file := "testdata/spec_example_2.25.yml" + self := loadExample(t, file) + + assert.Equal(t, dyn.NewValue( + map[string]dyn.Value{ + "Mark McGwire": dyn.NewValue(nil, []dyn.Location{{File: file, Line: 8, Column: 1}}), + "Sammy Sosa": dyn.NewValue(nil, []dyn.Location{{File: file, Line: 9, Column: 1}}), + "Ken Griffey": dyn.NewValue(nil, []dyn.Location{{File: file, Line: 10, Column: 1}}), + }, + []dyn.Location{{File: file, Line: 6, Column: 5}}, + ), self) +} + +func TestYAMLSpecExample_2_26(t *testing.T) { + file := "testdata/spec_example_2.26.yml" + self := loadExample(t, file) + + assert.Equal(t, dyn.NewValue( + []dyn.Value{ + dyn.NewValue( + map[string]dyn.Value{ + "Mark McGwire": dyn.NewValue(65, []dyn.Location{{File: file, Line: 7, Column: 17}}), + }, + []dyn.Location{{File: file, Line: 7, Column: 3}}, + ), + dyn.NewValue( + map[string]dyn.Value{ + "Sammy Sosa": dyn.NewValue(63, []dyn.Location{{File: file, Line: 8, Column: 15}}), + }, + []dyn.Location{{File: file, Line: 8, Column: 3}}, + ), + dyn.NewValue( + map[string]dyn.Value{ + "Ken Griffey": dyn.NewValue(58, []dyn.Location{{File: file, Line: 9, Column: 16}}), + }, + []dyn.Location{{File: file, Line: 9, Column: 3}}, + ), + }, + []dyn.Location{{File: file, Line: 6, Column: 5}}, + ), self) +} + +func TestYAMLSpecExample_2_27(t *testing.T) { + file := "testdata/spec_example_2.27.yml" + self := loadExample(t, file) + + assert.Equal(t, dyn.NewValue( + map[string]dyn.Value{ + "invoice": dyn.NewValue( + 34843, + []dyn.Location{{File: file, Line: 4, Column: 10}}, + ), + "date": dyn.NewValue( + dyn.MustTime("2001-01-23"), + []dyn.Location{{File: file, Line: 5, Column: 10}}, + ), + "bill-to": dyn.NewValue( + map[string]dyn.Value{ + "given": dyn.NewValue( + "Chris", + []dyn.Location{{File: file, Line: 7, Column: 12}}, + ), + "family": dyn.NewValue( + "Dumars", + []dyn.Location{{File: file, Line: 8, Column: 12}}, + ), + "address": dyn.NewValue( + map[string]dyn.Value{ + "lines": dyn.NewValue( + "458 Walkman Dr.\nSuite #292\n", + []dyn.Location{{File: file, Line: 10, Column: 12}}, + ), + "city": dyn.NewValue( + "Royal Oak", + []dyn.Location{{File: file, Line: 13, Column: 15}}, + ), + "state": dyn.NewValue( + "MI", + []dyn.Location{{File: file, Line: 14, Column: 15}}, + ), + "postal": dyn.NewValue( + 48046, + []dyn.Location{{File: file, Line: 15, Column: 15}}, + ), + }, + []dyn.Location{{File: file, Line: 10, Column: 5}}, + ), + }, + []dyn.Location{{File: file, Line: 6, Column: 10}}, + ), + "ship-to": dyn.NewValue( + map[string]dyn.Value{ + "given": dyn.NewValue( + "Chris", + []dyn.Location{{File: file, Line: 7, Column: 12}}, + ), + "family": dyn.NewValue( + "Dumars", + []dyn.Location{{File: file, Line: 8, Column: 12}}, + ), + "address": dyn.NewValue( + map[string]dyn.Value{ + "lines": dyn.NewValue( + "458 Walkman Dr.\nSuite #292\n", + []dyn.Location{{File: file, Line: 10, Column: 12}}, + ), + "city": dyn.NewValue( + "Royal Oak", + []dyn.Location{{File: file, Line: 13, Column: 15}}, + ), + "state": dyn.NewValue( + "MI", + []dyn.Location{{File: file, Line: 14, Column: 15}}, + ), + "postal": dyn.NewValue( + 48046, + []dyn.Location{{File: file, Line: 15, Column: 15}}, + ), + }, + []dyn.Location{{File: file, Line: 10, Column: 5}}, + ), + }, + []dyn.Location{{File: file, Line: 6, Column: 10}}, + ), + "product": dyn.NewValue( + []dyn.Value{ + dyn.NewValue( + map[string]dyn.Value{ + "sku": dyn.NewValue( + "BL394D", + []dyn.Location{{File: file, Line: 18, Column: 17}}, + ), + "quantity": dyn.NewValue( + 4, + []dyn.Location{{File: file, Line: 19, Column: 17}}, + ), + "description": dyn.NewValue( + "Basketball", + []dyn.Location{{File: file, Line: 20, Column: 17}}, + ), + "price": dyn.NewValue( + 450.0, + []dyn.Location{{File: file, Line: 21, Column: 17}}, + ), + }, + []dyn.Location{{File: file, Line: 18, Column: 3}}, + ), dyn.NewValue( + map[string]dyn.Value{ + "sku": dyn.NewValue( + "BL4438H", + []dyn.Location{{File: file, Line: 22, Column: 17}}, + ), + "quantity": dyn.NewValue( + 1, + []dyn.Location{{File: file, Line: 23, Column: 17}}, + ), + "description": dyn.NewValue( + "Super Hoop", + []dyn.Location{{File: file, Line: 24, Column: 17}}, + ), + "price": dyn.NewValue( + 2392.0, + []dyn.Location{{File: file, Line: 25, Column: 17}}, + ), + }, + []dyn.Location{{File: file, Line: 22, Column: 3}}, + )}, + []dyn.Location{{File: file, Line: 18, Column: 1}}, + ), + "tax": dyn.NewValue( + 251.42, + []dyn.Location{{File: file, Line: 26, Column: 8}}, + ), + "total": dyn.NewValue( + 4443.52, + []dyn.Location{{File: file, Line: 27, Column: 8}}, + ), + "comments": dyn.NewValue( + "Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338.", + []dyn.Location{{File: file, Line: 29, Column: 3}}, + ), + }, + []dyn.Location{{File: file, Line: 3, Column: 5}}, + ), self) +} + +func TestYAMLSpecExample_2_28(t *testing.T) { + file := "testdata/spec_example_2.28.yml" + self := loadExample(t, file) + + assert.Equal(t, dyn.NewValue( + map[string]dyn.Value{ + "Time": dyn.NewValue( + "2001-11-23 15:01:42 -5", + []dyn.Location{{File: file, Line: 4, Column: 7}}, + ), + "User": dyn.NewValue( + "ed", + []dyn.Location{{File: file, Line: 5, Column: 7}}, + ), + "Warning": dyn.NewValue( + "This is an error message for the log file", + []dyn.Location{{File: file, Line: 7, Column: 3}}, + ), + }, + []dyn.Location{{File: file, Line: 4, Column: 1}}, + ), self) +} From c5043c3d9df8c6422f325c8ce30fa0456115d707 Mon Sep 17 00:00:00 2001 From: "Lennart Kats (databricks)" Date: Fri, 18 Oct 2024 08:45:47 +0200 Subject: [PATCH 24/24] Add `bundle summary` to display URLs for deployed resources (#1731) ## Changes Adds a textual output to the `databricks bundle summary` command, which includes URLs of deployed resources. Example usage: ``` $ databricks bundle summary Name: my_pipeline Target: dev Workspace: Host: https://domain.databricks.com User: user@databricks.com Path: /Users/user@databricks.com/.bundle/my_pipeline/dev Resources: Jobs: my_project_job: Name: [dev lennart] my_project_job URL: https://domain.databricks.com/jobs/206899209187287?o=6051921418418893 Pipelines: my_project_pipeline: Name: [dev lennart] my_project_pipeline URL: https://domain.databricks.com/pipelines/3f849fd5-ba7d-47fa-a34c-c6bf034b4f58?o=6051921418418893 ``` Notes: * The top headers of the output are the same as those from the existing `bundle validate` command * URLs are colored light blue in the output * For resources that haven't been deployed yet, we show `(not deployed)` in place of the URL --------- Co-authored-by: Pieter Noordhuis Co-authored-by: Pieter Noordhuis --- bundle/config/mutator/initialize_urls.go | 65 +++++++++ bundle/config/mutator/initialize_urls_test.go | 130 ++++++++++++++++++ bundle/config/resources.go | 117 ++++++++++++++-- bundle/config/resources/clusters.go | 19 +++ bundle/config/resources/job.go | 19 +++ bundle/config/resources/mlflow_experiment.go | 19 +++ bundle/config/resources/mlflow_model.go | 19 +++ .../resources/model_serving_endpoint.go | 19 +++ bundle/config/resources/pipeline.go | 19 +++ bundle/config/resources/quality_monitor.go | 20 +++ bundle/config/resources/registered_model.go | 20 +++ bundle/config/resources/schema.go | 31 +++++ bundle/config/resources_test.go | 34 ++++- bundle/render/render_text_output.go | 121 ++++++++++++---- bundle/render/render_text_output_test.go | 112 ++++++++++++++- cmd/bundle/deploy.go | 2 +- cmd/bundle/summary.go | 12 +- cmd/bundle/validate.go | 2 +- 18 files changed, 728 insertions(+), 52 deletions(-) create mode 100644 bundle/config/mutator/initialize_urls.go create mode 100644 bundle/config/mutator/initialize_urls_test.go diff --git a/bundle/config/mutator/initialize_urls.go b/bundle/config/mutator/initialize_urls.go new file mode 100644 index 00000000..31930591 --- /dev/null +++ b/bundle/config/mutator/initialize_urls.go @@ -0,0 +1,65 @@ +package mutator + +import ( + "context" + "net/url" + "strconv" + "strings" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/libs/diag" +) + +type initializeURLs struct { +} + +// InitializeURLs makes sure the URL field of each resource is configured. +// NOTE: since this depends on an extra API call, this mutator adds some extra +// latency. As such, it should only be used when needed. +// This URL field is used for the output of the 'bundle summary' CLI command. +func InitializeURLs() bundle.Mutator { + return &initializeURLs{} +} + +func (m *initializeURLs) Name() string { + return "InitializeURLs" +} + +func (m *initializeURLs) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { + workspaceId, err := b.WorkspaceClient().CurrentWorkspaceID(ctx) + if err != nil { + return diag.FromErr(err) + } + orgId := strconv.FormatInt(workspaceId, 10) + host := b.WorkspaceClient().Config.CanonicalHostName() + initializeForWorkspace(b, orgId, host) + return nil +} + +func initializeForWorkspace(b *bundle.Bundle, orgId string, host string) error { + baseURL, err := url.Parse(host) + if err != nil { + return err + } + + // Add ?o= only if wasn't in the subdomain already. + // The ?o= is needed when vanity URLs / legacy workspace URLs are used. + // If it's not needed we prefer to leave it out since these URLs are rather + // long for most terminals. + // + // See https://docs.databricks.com/en/workspace/workspace-details.html for + // further reading about the '?o=' suffix. + if !strings.Contains(baseURL.Hostname(), orgId) { + values := baseURL.Query() + values.Add("o", orgId) + baseURL.RawQuery = values.Encode() + } + + for _, group := range b.Config.Resources.AllResources() { + for _, r := range group.Resources { + r.InitializeURL(*baseURL) + } + } + + return nil +} diff --git a/bundle/config/mutator/initialize_urls_test.go b/bundle/config/mutator/initialize_urls_test.go new file mode 100644 index 00000000..71cc153a --- /dev/null +++ b/bundle/config/mutator/initialize_urls_test.go @@ -0,0 +1,130 @@ +package mutator + +import ( + "testing" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/databricks-sdk-go/service/catalog" + "github.com/databricks/databricks-sdk-go/service/compute" + "github.com/databricks/databricks-sdk-go/service/jobs" + "github.com/databricks/databricks-sdk-go/service/ml" + "github.com/databricks/databricks-sdk-go/service/pipelines" + "github.com/databricks/databricks-sdk-go/service/serving" + "github.com/stretchr/testify/require" +) + +func TestInitializeURLs(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Workspace: config.Workspace{ + Host: "https://mycompany.databricks.com/", + }, + Resources: config.Resources{ + Jobs: map[string]*resources.Job{ + "job1": { + ID: "1", + JobSettings: &jobs.JobSettings{Name: "job1"}, + }, + }, + Pipelines: map[string]*resources.Pipeline{ + "pipeline1": { + ID: "3", + PipelineSpec: &pipelines.PipelineSpec{Name: "pipeline1"}, + }, + }, + Experiments: map[string]*resources.MlflowExperiment{ + "experiment1": { + ID: "4", + Experiment: &ml.Experiment{Name: "experiment1"}, + }, + }, + Models: map[string]*resources.MlflowModel{ + "model1": { + ID: "a model uses its name for identifier", + Model: &ml.Model{Name: "a model uses its name for identifier"}, + }, + }, + ModelServingEndpoints: map[string]*resources.ModelServingEndpoint{ + "servingendpoint1": { + ID: "my_serving_endpoint", + CreateServingEndpoint: &serving.CreateServingEndpoint{ + Name: "my_serving_endpoint", + }, + }, + }, + RegisteredModels: map[string]*resources.RegisteredModel{ + "registeredmodel1": { + ID: "8", + CreateRegisteredModelRequest: &catalog.CreateRegisteredModelRequest{ + Name: "my_registered_model", + }, + }, + }, + QualityMonitors: map[string]*resources.QualityMonitor{ + "qualityMonitor1": { + CreateMonitor: &catalog.CreateMonitor{ + TableName: "catalog.schema.qualityMonitor1", + }, + }, + }, + Schemas: map[string]*resources.Schema{ + "schema1": { + ID: "catalog.schema", + CreateSchema: &catalog.CreateSchema{ + Name: "schema", + }, + }, + }, + Clusters: map[string]*resources.Cluster{ + "cluster1": { + ID: "1017-103929-vlr7jzcf", + ClusterSpec: &compute.ClusterSpec{ + ClusterName: "cluster1", + }, + }, + }, + }, + }, + } + + expectedURLs := map[string]string{ + "job1": "https://mycompany.databricks.com/jobs/1?o=123456", + "pipeline1": "https://mycompany.databricks.com/pipelines/3?o=123456", + "experiment1": "https://mycompany.databricks.com/ml/experiments/4?o=123456", + "model1": "https://mycompany.databricks.com/ml/models/a%20model%20uses%20its%20name%20for%20identifier?o=123456", + "servingendpoint1": "https://mycompany.databricks.com/ml/endpoints/my_serving_endpoint?o=123456", + "registeredmodel1": "https://mycompany.databricks.com/explore/data/models/8?o=123456", + "qualityMonitor1": "https://mycompany.databricks.com/explore/data/catalog/schema/qualityMonitor1?o=123456", + "schema1": "https://mycompany.databricks.com/explore/data/catalog/schema?o=123456", + "cluster1": "https://mycompany.databricks.com/compute/clusters/1017-103929-vlr7jzcf?o=123456", + } + + initializeForWorkspace(b, "123456", "https://mycompany.databricks.com/") + + for _, group := range b.Config.Resources.AllResources() { + for key, r := range group.Resources { + require.Equal(t, expectedURLs[key], r.GetURL(), "Unexpected URL for "+key) + } + } +} + +func TestInitializeURLsWithoutOrgId(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Jobs: map[string]*resources.Job{ + "job1": { + ID: "1", + JobSettings: &jobs.JobSettings{Name: "job1"}, + }, + }, + }, + }, + } + + initializeForWorkspace(b, "123456", "https://adb-123456.azuredatabricks.net/") + + require.Equal(t, "https://adb-123456.azuredatabricks.net/jobs/1", b.Config.Resources.Jobs["job1"].URL) +} diff --git a/bundle/config/resources.go b/bundle/config/resources.go index dc51a7ca..9513369e 100644 --- a/bundle/config/resources.go +++ b/bundle/config/resources.go @@ -3,6 +3,7 @@ package config import ( "context" "fmt" + "net/url" "github.com/databricks/cli/bundle/config/resources" "github.com/databricks/databricks-sdk-go" @@ -30,6 +31,53 @@ type ConfigResource interface { // Terraform equivalent name of the resource. For example "databricks_job" // for jobs and "databricks_pipeline" for pipelines. TerraformResourceName() string + + // GetName returns the in-product name of the resource. + GetName() string + + // GetURL returns the URL of the resource. + GetURL() string + + // InitializeURL initializes the URL field of the resource. + InitializeURL(baseURL url.URL) +} + +// ResourceGroup represents a group of resources of the same type. +// It includes a description of the resource type and a map of resources. +type ResourceGroup struct { + Description ResourceDescription + Resources map[string]ConfigResource +} + +// collectResourceMap collects resources of a specific type into a ResourceGroup. +func collectResourceMap[T ConfigResource]( + description ResourceDescription, + input map[string]T, +) ResourceGroup { + resources := make(map[string]ConfigResource) + for key, resource := range input { + resources[key] = resource + } + return ResourceGroup{ + Description: description, + Resources: resources, + } +} + +// AllResources returns all resources in the bundle grouped by their resource type. +func (r *Resources) AllResources() []ResourceGroup { + descriptions := SupportedResources() + return []ResourceGroup{ + collectResourceMap(descriptions["jobs"], r.Jobs), + collectResourceMap(descriptions["pipelines"], r.Pipelines), + collectResourceMap(descriptions["models"], r.Models), + collectResourceMap(descriptions["experiments"], r.Experiments), + collectResourceMap(descriptions["model_serving_endpoints"], r.ModelServingEndpoints), + collectResourceMap(descriptions["registered_models"], r.RegisteredModels), + collectResourceMap(descriptions["quality_monitors"], r.QualityMonitors), + collectResourceMap(descriptions["schemas"], r.Schemas), + collectResourceMap(descriptions["clusters"], r.Clusters), + } } func (r *Resources) FindResourceByConfigKey(key string) (ConfigResource, error) { @@ -61,20 +109,71 @@ func (r *Resources) FindResourceByConfigKey(key string) (ConfigResource, error) } type ResourceDescription struct { + // Singular and plural name when used to refer to the configuration. SingularName string + PluralName string + + // Singular and plural title when used in summaries / terminal UI. + SingularTitle string + PluralTitle string } // The keys of the map corresponds to the resource key in the bundle configuration. func SupportedResources() map[string]ResourceDescription { return map[string]ResourceDescription{ - "jobs": {SingularName: "job"}, - "pipelines": {SingularName: "pipeline"}, - "models": {SingularName: "model"}, - "experiments": {SingularName: "experiment"}, - "model_serving_endpoints": {SingularName: "model_serving_endpoint"}, - "registered_models": {SingularName: "registered_model"}, - "quality_monitors": {SingularName: "quality_monitor"}, - "schemas": {SingularName: "schema"}, - "clusters": {SingularName: "cluster"}, + "jobs": { + SingularName: "job", + PluralName: "jobs", + SingularTitle: "Job", + PluralTitle: "Jobs", + }, + "pipelines": { + SingularName: "pipeline", + PluralName: "pipelines", + SingularTitle: "Pipeline", + PluralTitle: "Pipelines", + }, + "models": { + SingularName: "model", + PluralName: "models", + SingularTitle: "Model", + PluralTitle: "Models", + }, + "experiments": { + SingularName: "experiment", + PluralName: "experiments", + SingularTitle: "Experiment", + PluralTitle: "Experiments", + }, + "model_serving_endpoints": { + SingularName: "model_serving_endpoint", + PluralName: "model_serving_endpoints", + SingularTitle: "Model Serving Endpoint", + PluralTitle: "Model Serving Endpoints", + }, + "registered_models": { + SingularName: "registered_model", + PluralName: "registered_models", + SingularTitle: "Registered Model", + PluralTitle: "Registered Models", + }, + "quality_monitors": { + SingularName: "quality_monitor", + PluralName: "quality_monitors", + SingularTitle: "Quality Monitor", + PluralTitle: "Quality Monitors", + }, + "schemas": { + SingularName: "schema", + PluralName: "schemas", + SingularTitle: "Schema", + PluralTitle: "Schemas", + }, + "clusters": { + SingularName: "cluster", + PluralName: "clusters", + SingularTitle: "Cluster", + PluralTitle: "Clusters", + }, } } diff --git a/bundle/config/resources/clusters.go b/bundle/config/resources/clusters.go index 63234566..eb0247c6 100644 --- a/bundle/config/resources/clusters.go +++ b/bundle/config/resources/clusters.go @@ -2,6 +2,8 @@ package resources import ( "context" + "fmt" + "net/url" "github.com/databricks/cli/libs/log" "github.com/databricks/databricks-sdk-go" @@ -13,6 +15,7 @@ type Cluster struct { ID string `json:"id,omitempty" bundle:"readonly"` Permissions []Permission `json:"permissions,omitempty"` ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"` + URL string `json:"url,omitempty" bundle:"internal"` *compute.ClusterSpec } @@ -37,3 +40,19 @@ func (s *Cluster) Exists(ctx context.Context, w *databricks.WorkspaceClient, id func (s *Cluster) TerraformResourceName() string { return "databricks_cluster" } + +func (s *Cluster) InitializeURL(baseURL url.URL) { + if s.ID == "" { + return + } + baseURL.Path = fmt.Sprintf("compute/clusters/%s", s.ID) + s.URL = baseURL.String() +} + +func (s *Cluster) GetName() string { + return s.ClusterName +} + +func (s *Cluster) GetURL() string { + return s.URL +} diff --git a/bundle/config/resources/job.go b/bundle/config/resources/job.go index d8f97a2d..98db1ec5 100644 --- a/bundle/config/resources/job.go +++ b/bundle/config/resources/job.go @@ -2,6 +2,8 @@ package resources import ( "context" + "fmt" + "net/url" "strconv" "github.com/databricks/cli/libs/log" @@ -14,6 +16,7 @@ type Job struct { ID string `json:"id,omitempty" bundle:"readonly"` Permissions []Permission `json:"permissions,omitempty"` ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"` + URL string `json:"url,omitempty" bundle:"internal"` *jobs.JobSettings } @@ -44,3 +47,19 @@ func (j *Job) Exists(ctx context.Context, w *databricks.WorkspaceClient, id stri func (j *Job) TerraformResourceName() string { return "databricks_job" } + +func (j *Job) InitializeURL(baseURL url.URL) { + if j.ID == "" { + return + } + baseURL.Path = fmt.Sprintf("jobs/%s", j.ID) + j.URL = baseURL.String() +} + +func (j *Job) GetName() string { + return j.Name +} + +func (j *Job) GetURL() string { + return j.URL +} diff --git a/bundle/config/resources/mlflow_experiment.go b/bundle/config/resources/mlflow_experiment.go index 0ab48643..a5871468 100644 --- a/bundle/config/resources/mlflow_experiment.go +++ b/bundle/config/resources/mlflow_experiment.go @@ -2,6 +2,8 @@ package resources import ( "context" + "fmt" + "net/url" "github.com/databricks/cli/libs/log" "github.com/databricks/databricks-sdk-go" @@ -13,6 +15,7 @@ type MlflowExperiment struct { ID string `json:"id,omitempty" bundle:"readonly"` Permissions []Permission `json:"permissions,omitempty"` ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"` + URL string `json:"url,omitempty" bundle:"internal"` *ml.Experiment } @@ -39,3 +42,19 @@ func (s *MlflowExperiment) Exists(ctx context.Context, w *databricks.WorkspaceCl func (s *MlflowExperiment) TerraformResourceName() string { return "databricks_mlflow_experiment" } + +func (s *MlflowExperiment) InitializeURL(baseURL url.URL) { + if s.ID == "" { + return + } + baseURL.Path = fmt.Sprintf("ml/experiments/%s", s.ID) + s.URL = baseURL.String() +} + +func (s *MlflowExperiment) GetName() string { + return s.Name +} + +func (s *MlflowExperiment) GetURL() string { + return s.URL +} diff --git a/bundle/config/resources/mlflow_model.go b/bundle/config/resources/mlflow_model.go index 300474e3..9ead254d 100644 --- a/bundle/config/resources/mlflow_model.go +++ b/bundle/config/resources/mlflow_model.go @@ -2,6 +2,8 @@ package resources import ( "context" + "fmt" + "net/url" "github.com/databricks/cli/libs/log" "github.com/databricks/databricks-sdk-go" @@ -13,6 +15,7 @@ type MlflowModel struct { ID string `json:"id,omitempty" bundle:"readonly"` Permissions []Permission `json:"permissions,omitempty"` ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"` + URL string `json:"url,omitempty" bundle:"internal"` *ml.Model } @@ -39,3 +42,19 @@ func (s *MlflowModel) Exists(ctx context.Context, w *databricks.WorkspaceClient, func (s *MlflowModel) TerraformResourceName() string { return "databricks_mlflow_model" } + +func (s *MlflowModel) InitializeURL(baseURL url.URL) { + if s.ID == "" { + return + } + baseURL.Path = fmt.Sprintf("ml/models/%s", s.ID) + s.URL = baseURL.String() +} + +func (s *MlflowModel) GetName() string { + return s.Name +} + +func (s *MlflowModel) GetURL() string { + return s.URL +} diff --git a/bundle/config/resources/model_serving_endpoint.go b/bundle/config/resources/model_serving_endpoint.go index 5efb7ea2..7f3ae00c 100644 --- a/bundle/config/resources/model_serving_endpoint.go +++ b/bundle/config/resources/model_serving_endpoint.go @@ -2,6 +2,8 @@ package resources import ( "context" + "fmt" + "net/url" "github.com/databricks/cli/libs/log" "github.com/databricks/databricks-sdk-go" @@ -23,6 +25,7 @@ type ModelServingEndpoint struct { Permissions []Permission `json:"permissions,omitempty"` ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"` + URL string `json:"url,omitempty" bundle:"internal"` } func (s *ModelServingEndpoint) UnmarshalJSON(b []byte) error { @@ -47,3 +50,19 @@ func (s *ModelServingEndpoint) Exists(ctx context.Context, w *databricks.Workspa func (s *ModelServingEndpoint) TerraformResourceName() string { return "databricks_model_serving" } + +func (s *ModelServingEndpoint) InitializeURL(baseURL url.URL) { + if s.ID == "" { + return + } + baseURL.Path = fmt.Sprintf("ml/endpoints/%s", s.ID) + s.URL = baseURL.String() +} + +func (s *ModelServingEndpoint) GetName() string { + return s.Name +} + +func (s *ModelServingEndpoint) GetURL() string { + return s.URL +} diff --git a/bundle/config/resources/pipeline.go b/bundle/config/resources/pipeline.go index 55270be6..b3311d8e 100644 --- a/bundle/config/resources/pipeline.go +++ b/bundle/config/resources/pipeline.go @@ -2,6 +2,8 @@ package resources import ( "context" + "fmt" + "net/url" "github.com/databricks/cli/libs/log" "github.com/databricks/databricks-sdk-go" @@ -13,6 +15,7 @@ type Pipeline struct { ID string `json:"id,omitempty" bundle:"readonly"` Permissions []Permission `json:"permissions,omitempty"` ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"` + URL string `json:"url,omitempty" bundle:"internal"` *pipelines.PipelineSpec } @@ -39,3 +42,19 @@ func (p *Pipeline) Exists(ctx context.Context, w *databricks.WorkspaceClient, id func (p *Pipeline) TerraformResourceName() string { return "databricks_pipeline" } + +func (p *Pipeline) InitializeURL(baseURL url.URL) { + if p.ID == "" { + return + } + baseURL.Path = fmt.Sprintf("pipelines/%s", p.ID) + p.URL = baseURL.String() +} + +func (p *Pipeline) GetName() string { + return p.Name +} + +func (s *Pipeline) GetURL() string { + return s.URL +} diff --git a/bundle/config/resources/quality_monitor.go b/bundle/config/resources/quality_monitor.go index 9160782c..3c823e62 100644 --- a/bundle/config/resources/quality_monitor.go +++ b/bundle/config/resources/quality_monitor.go @@ -2,6 +2,9 @@ package resources import ( "context" + "fmt" + "net/url" + "strings" "github.com/databricks/cli/libs/log" "github.com/databricks/databricks-sdk-go" @@ -20,6 +23,7 @@ type QualityMonitor struct { ID string `json:"id,omitempty" bundle:"readonly"` ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"` + URL string `json:"url,omitempty" bundle:"internal"` } func (s *QualityMonitor) UnmarshalJSON(b []byte) error { @@ -44,3 +48,19 @@ func (s *QualityMonitor) Exists(ctx context.Context, w *databricks.WorkspaceClie func (s *QualityMonitor) TerraformResourceName() string { return "databricks_quality_monitor" } + +func (s *QualityMonitor) InitializeURL(baseURL url.URL) { + if s.TableName == "" { + return + } + baseURL.Path = fmt.Sprintf("explore/data/%s", strings.ReplaceAll(s.TableName, ".", "/")) + s.URL = baseURL.String() +} + +func (s *QualityMonitor) GetName() string { + return s.TableName +} + +func (s *QualityMonitor) GetURL() string { + return s.URL +} diff --git a/bundle/config/resources/registered_model.go b/bundle/config/resources/registered_model.go index 6033ffdf..c44526d0 100644 --- a/bundle/config/resources/registered_model.go +++ b/bundle/config/resources/registered_model.go @@ -2,6 +2,9 @@ package resources import ( "context" + "fmt" + "net/url" + "strings" "github.com/databricks/cli/libs/log" "github.com/databricks/databricks-sdk-go" @@ -24,6 +27,7 @@ type RegisteredModel struct { *catalog.CreateRegisteredModelRequest ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"` + URL string `json:"url,omitempty" bundle:"internal"` } func (s *RegisteredModel) UnmarshalJSON(b []byte) error { @@ -48,3 +52,19 @@ func (s *RegisteredModel) Exists(ctx context.Context, w *databricks.WorkspaceCli func (s *RegisteredModel) TerraformResourceName() string { return "databricks_registered_model" } + +func (s *RegisteredModel) InitializeURL(baseURL url.URL) { + if s.ID == "" { + return + } + baseURL.Path = fmt.Sprintf("explore/data/models/%s", strings.ReplaceAll(s.ID, ".", "/")) + s.URL = baseURL.String() +} + +func (s *RegisteredModel) GetName() string { + return s.Name +} + +func (s *RegisteredModel) GetURL() string { + return s.URL +} diff --git a/bundle/config/resources/schema.go b/bundle/config/resources/schema.go index 7ab00495..a9f905cf 100644 --- a/bundle/config/resources/schema.go +++ b/bundle/config/resources/schema.go @@ -1,6 +1,12 @@ package resources import ( + "context" + "fmt" + "net/url" + "strings" + + "github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go/marshal" "github.com/databricks/databricks-sdk-go/service/catalog" ) @@ -16,6 +22,31 @@ type Schema struct { *catalog.CreateSchema ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"` + URL string `json:"url,omitempty" bundle:"internal"` +} + +func (s *Schema) Exists(ctx context.Context, w *databricks.WorkspaceClient, id string) (bool, error) { + return false, fmt.Errorf("schema.Exists() is not supported") +} + +func (s *Schema) TerraformResourceName() string { + return "databricks_schema" +} + +func (s *Schema) InitializeURL(baseURL url.URL) { + if s.ID == "" { + return + } + baseURL.Path = fmt.Sprintf("explore/data/%s", strings.ReplaceAll(s.ID, ".", "/")) + s.URL = baseURL.String() +} + +func (s *Schema) GetURL() string { + return s.URL +} + +func (s *Schema) GetName() string { + return s.Name } func (s *Schema) UnmarshalJSON(b []byte) error { diff --git a/bundle/config/resources_test.go b/bundle/config/resources_test.go index c1b76118..9ae73b22 100644 --- a/bundle/config/resources_test.go +++ b/bundle/config/resources_test.go @@ -63,17 +63,37 @@ func TestCustomMarshallerIsImplemented(t *testing.T) { } } +func TestResourcesAllResourcesCompleteness(t *testing.T) { + r := Resources{} + rt := reflect.TypeOf(r) + + // Collect set of includes resource types + var types []string + for _, group := range r.AllResources() { + types = append(types, group.Description.PluralName) + } + + for i := 0; i < rt.NumField(); i++ { + field := rt.Field(i) + jsonTag := field.Tag.Get("json") + + if idx := strings.Index(jsonTag, ","); idx != -1 { + jsonTag = jsonTag[:idx] + } + + assert.Contains(t, types, jsonTag, "Field %s is missing in AllResources", field.Name) + } +} + func TestSupportedResources(t *testing.T) { - expected := map[string]ResourceDescription{} + // Please add your resource to the SupportedResources() function in resources.go if you add a new resource. + actual := SupportedResources() + typ := reflect.TypeOf(Resources{}) for i := 0; i < typ.NumField(); i++ { field := typ.Field(i) jsonTags := strings.Split(field.Tag.Get("json"), ",") - singularName := strings.TrimSuffix(jsonTags[0], "s") - expected[jsonTags[0]] = ResourceDescription{SingularName: singularName} + pluralName := jsonTags[0] + assert.Equal(t, actual[pluralName].PluralName, pluralName) } - - // Please add your resource to the SupportedResources() function in resources.go - // if you are adding a new resource. - assert.Equal(t, expected, SupportedResources()) } diff --git a/bundle/render/render_text_output.go b/bundle/render/render_text_output.go index 3e52d5f1..2f7affbf 100644 --- a/bundle/render/render_text_output.go +++ b/bundle/render/render_text_output.go @@ -1,9 +1,11 @@ package render import ( + "context" "fmt" "io" "path/filepath" + "sort" "strings" "text/template" @@ -29,7 +31,7 @@ var renderFuncMap = template.FuncMap{ }, } -const summaryTemplate = `{{- if .Name -}} +const summaryHeaderTemplate = `{{- if .Name -}} Name: {{ .Name | bold }} {{- if .Target }} Target: {{ .Target | bold }} @@ -46,12 +48,30 @@ Workspace: Path: {{ .Path | bold }} {{- end }} {{- end }} +{{ end -}}` -{{ end -}} - -{{ .Trailer }} +const resourcesTemplate = `Resources: +{{- range . }} + {{ .GroupName }}: + {{- range .Resources }} + {{ .Key | bold }}: + Name: {{ .Name }} + URL: {{ if .URL }}{{ .URL | cyan }}{{ else }}{{ "(not deployed)" | cyan }}{{ end }} + {{- end }} +{{- end }} ` +type ResourceGroup struct { + GroupName string + Resources []ResourceInfo +} + +type ResourceInfo struct { + Key string + Name string + URL string +} + func pluralize(n int, singular, plural string) string { if n == 1 { return fmt.Sprintf("%d %s", n, singular) @@ -74,20 +94,20 @@ func buildTrailer(diags diag.Diagnostics) string { case len(parts) >= 3: first := strings.Join(parts[:len(parts)-1], ", ") last := parts[len(parts)-1] - return fmt.Sprintf("Found %s, and %s", first, last) + return fmt.Sprintf("Found %s, and %s\n", first, last) case len(parts) == 2: - return fmt.Sprintf("Found %s and %s", parts[0], parts[1]) + return fmt.Sprintf("Found %s and %s\n", parts[0], parts[1]) case len(parts) == 1: - return fmt.Sprintf("Found %s", parts[0]) + return fmt.Sprintf("Found %s\n", parts[0]) default: // No diagnostics to print. - return color.GreenString("Validation OK!") + return color.GreenString("Validation OK!\n") } } -func renderSummaryTemplate(out io.Writer, b *bundle.Bundle, diags diag.Diagnostics) error { +func renderSummaryHeaderTemplate(out io.Writer, b *bundle.Bundle) error { if b == nil { - return renderSummaryTemplate(out, &bundle.Bundle{}, diags) + return renderSummaryHeaderTemplate(out, &bundle.Bundle{}) } var currentUser = &iam.User{} @@ -98,20 +118,19 @@ func renderSummaryTemplate(out io.Writer, b *bundle.Bundle, diags diag.Diagnosti } } - t := template.Must(template.New("summary").Funcs(renderFuncMap).Parse(summaryTemplate)) + t := template.Must(template.New("summary").Funcs(renderFuncMap).Parse(summaryHeaderTemplate)) err := t.Execute(out, map[string]any{ - "Name": b.Config.Bundle.Name, - "Target": b.Config.Bundle.Target, - "User": currentUser.UserName, - "Path": b.Config.Workspace.RootPath, - "Host": b.Config.Workspace.Host, - "Trailer": buildTrailer(diags), + "Name": b.Config.Bundle.Name, + "Target": b.Config.Bundle.Target, + "User": currentUser.UserName, + "Path": b.Config.Workspace.RootPath, + "Host": b.Config.Workspace.Host, }) return err } -func renderDiagnostics(out io.Writer, b *bundle.Bundle, diags diag.Diagnostics) error { +func renderDiagnosticsOnly(out io.Writer, b *bundle.Bundle, diags diag.Diagnostics) error { for _, d := range diags { for i := range d.Locations { if b == nil { @@ -139,19 +158,73 @@ type RenderOptions struct { RenderSummaryTable bool } -// RenderTextOutput renders the diagnostics in a human-readable format. -func RenderTextOutput(out io.Writer, b *bundle.Bundle, diags diag.Diagnostics, opts RenderOptions) error { - err := renderDiagnostics(out, b, diags) +// RenderDiagnostics renders the diagnostics in a human-readable format. +func RenderDiagnostics(out io.Writer, b *bundle.Bundle, diags diag.Diagnostics, opts RenderOptions) error { + err := renderDiagnosticsOnly(out, b, diags) if err != nil { return fmt.Errorf("failed to render diagnostics: %w", err) } if opts.RenderSummaryTable { - err = renderSummaryTemplate(out, b, diags) - if err != nil { - return fmt.Errorf("failed to render summary: %w", err) + if b != nil { + err = renderSummaryHeaderTemplate(out, b) + if err != nil { + return fmt.Errorf("failed to render summary: %w", err) + } + io.WriteString(out, "\n") } + trailer := buildTrailer(diags) + io.WriteString(out, trailer) } return nil } + +func RenderSummary(ctx context.Context, out io.Writer, b *bundle.Bundle) error { + if err := renderSummaryHeaderTemplate(out, b); err != nil { + return err + } + + var resourceGroups []ResourceGroup + + for _, group := range b.Config.Resources.AllResources() { + resources := make([]ResourceInfo, 0, len(group.Resources)) + for key, resource := range group.Resources { + resources = append(resources, ResourceInfo{ + Key: key, + Name: resource.GetName(), + URL: resource.GetURL(), + }) + } + + if len(resources) > 0 { + resourceGroups = append(resourceGroups, ResourceGroup{ + GroupName: group.Description.PluralTitle, + Resources: resources, + }) + } + } + + if err := renderResourcesTemplate(out, resourceGroups); err != nil { + return fmt.Errorf("failed to render resources template: %w", err) + } + + return nil +} + +// Helper function to sort and render resource groups using the template +func renderResourcesTemplate(out io.Writer, resourceGroups []ResourceGroup) error { + // Sort everything to ensure consistent output + sort.Slice(resourceGroups, func(i, j int) bool { + return resourceGroups[i].GroupName < resourceGroups[j].GroupName + }) + for _, group := range resourceGroups { + sort.Slice(group.Resources, func(i, j int) bool { + return group.Resources[i].Key < group.Resources[j].Key + }) + } + + t := template.Must(template.New("resources").Funcs(renderFuncMap).Parse(resourcesTemplate)) + + return t.Execute(out, resourceGroups) +} diff --git a/bundle/render/render_text_output_test.go b/bundle/render/render_text_output_test.go index 1a41fa01..cd9e7723 100644 --- a/bundle/render/render_text_output_test.go +++ b/bundle/render/render_text_output_test.go @@ -2,14 +2,21 @@ package render import ( "bytes" + "context" + "io" "testing" "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/config/resources" "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/dyn" - assert "github.com/databricks/cli/libs/dyn/dynassert" + "github.com/databricks/databricks-sdk-go/service/catalog" "github.com/databricks/databricks-sdk-go/service/iam" + "github.com/databricks/databricks-sdk-go/service/jobs" + "github.com/databricks/databricks-sdk-go/service/pipelines" + "github.com/databricks/databricks-sdk-go/service/serving" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -326,7 +333,7 @@ func TestRenderTextOutput(t *testing.T) { t.Run(tc.name, func(t *testing.T) { writer := &bytes.Buffer{} - err := RenderTextOutput(writer, tc.bundle, tc.diags, tc.opts) + err := RenderDiagnostics(writer, tc.bundle, tc.diags, tc.opts) require.NoError(t, err) assert.Equal(t, tc.expected, writer.String()) @@ -468,7 +475,7 @@ func TestRenderDiagnostics(t *testing.T) { t.Run(tc.name, func(t *testing.T) { writer := &bytes.Buffer{} - err := renderDiagnostics(writer, bundle, tc.diags) + err := renderDiagnosticsOnly(writer, bundle, tc.diags) require.NoError(t, err) assert.Equal(t, tc.expected, writer.String()) @@ -479,8 +486,105 @@ func TestRenderDiagnostics(t *testing.T) { func TestRenderSummaryTemplate_nilBundle(t *testing.T) { writer := &bytes.Buffer{} - err := renderSummaryTemplate(writer, nil, nil) + err := renderSummaryHeaderTemplate(writer, nil) require.NoError(t, err) + io.WriteString(writer, buildTrailer(nil)) + assert.Equal(t, "Validation OK!\n", writer.String()) } + +func TestRenderSummary(t *testing.T) { + ctx := context.Background() + + // Create a mock bundle with various resources + b := &bundle.Bundle{ + Config: config.Root{ + Bundle: config.Bundle{ + Name: "test-bundle", + Target: "test-target", + }, + Workspace: config.Workspace{ + Host: "https://mycompany.databricks.com/", + }, + Resources: config.Resources{ + Jobs: map[string]*resources.Job{ + "job1": { + ID: "1", + URL: "https://url1", + JobSettings: &jobs.JobSettings{Name: "job1-name"}, + }, + "job2": { + ID: "2", + URL: "https://url2", + JobSettings: &jobs.JobSettings{Name: "job2-name"}, + }, + }, + Pipelines: map[string]*resources.Pipeline{ + "pipeline2": { + ID: "4", + // no URL + PipelineSpec: &pipelines.PipelineSpec{Name: "pipeline2-name"}, + }, + "pipeline1": { + ID: "3", + URL: "https://url3", + PipelineSpec: &pipelines.PipelineSpec{Name: "pipeline1-name"}, + }, + }, + Schemas: map[string]*resources.Schema{ + "schema1": { + ID: "catalog.schema", + CreateSchema: &catalog.CreateSchema{ + Name: "schema", + }, + // no URL + }, + }, + ModelServingEndpoints: map[string]*resources.ModelServingEndpoint{ + "endpoint1": { + ID: "7", + CreateServingEndpoint: &serving.CreateServingEndpoint{ + Name: "my_serving_endpoint", + }, + URL: "https://url4", + }, + }, + }, + }, + } + + writer := &bytes.Buffer{} + err := RenderSummary(ctx, writer, b) + require.NoError(t, err) + + expectedSummary := `Name: test-bundle +Target: test-target +Workspace: + Host: https://mycompany.databricks.com/ +Resources: + Jobs: + job1: + Name: job1-name + URL: https://url1 + job2: + Name: job2-name + URL: https://url2 + Model Serving Endpoints: + endpoint1: + Name: my_serving_endpoint + URL: https://url4 + Pipelines: + pipeline1: + Name: pipeline1-name + URL: https://url3 + pipeline2: + Name: pipeline2-name + URL: (not deployed) + Schemas: + schema1: + Name: schema + URL: (not deployed) +` + assert.Equal(t, expectedSummary, writer.String()) +} diff --git a/cmd/bundle/deploy.go b/cmd/bundle/deploy.go index f1c85cb3..a25e02f6 100644 --- a/cmd/bundle/deploy.go +++ b/cmd/bundle/deploy.go @@ -78,7 +78,7 @@ func newDeployCommand() *cobra.Command { } renderOpts := render.RenderOptions{RenderSummaryTable: false} - err := render.RenderTextOutput(cmd.OutOrStdout(), b, diags, renderOpts) + err := render.RenderDiagnostics(cmd.OutOrStdout(), b, diags, renderOpts) if err != nil { return fmt.Errorf("failed to render output: %w", err) } diff --git a/cmd/bundle/summary.go b/cmd/bundle/summary.go index 5a64b46c..8c34dd61 100644 --- a/cmd/bundle/summary.go +++ b/cmd/bundle/summary.go @@ -8,8 +8,10 @@ import ( "path/filepath" "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config/mutator" "github.com/databricks/cli/bundle/deploy/terraform" "github.com/databricks/cli/bundle/phases" + "github.com/databricks/cli/bundle/render" "github.com/databricks/cli/cmd/bundle/utils" "github.com/databricks/cli/cmd/root" "github.com/databricks/cli/libs/flags" @@ -19,11 +21,8 @@ import ( func newSummaryCommand() *cobra.Command { cmd := &cobra.Command{ Use: "summary", - Short: "Describe the bundle resources and their deployment states", + Short: "Summarize resources deployed by this bundle", Args: root.NoArgs, - - // This command is currently intended for the Databricks VSCode extension only - Hidden: true, } var forcePull bool @@ -60,14 +59,15 @@ func newSummaryCommand() *cobra.Command { } } - diags = bundle.Apply(ctx, b, terraform.Load()) + diags = bundle.Apply(ctx, b, + bundle.Seq(terraform.Load(), mutator.InitializeURLs())) if err := diags.Error(); err != nil { return err } switch root.OutputType(cmd) { case flags.OutputText: - return fmt.Errorf("%w, only json output is supported", errors.ErrUnsupported) + return render.RenderSummary(ctx, cmd.OutOrStdout(), b) case flags.OutputJSON: buf, err := json.MarshalIndent(b.Config, "", " ") if err != nil { diff --git a/cmd/bundle/validate.go b/cmd/bundle/validate.go index 496d5d2b..5331e7e7 100644 --- a/cmd/bundle/validate.go +++ b/cmd/bundle/validate.go @@ -54,7 +54,7 @@ func newValidateCommand() *cobra.Command { switch root.OutputType(cmd) { case flags.OutputText: renderOpts := render.RenderOptions{RenderSummaryTable: true} - err := render.RenderTextOutput(cmd.OutOrStdout(), b, diags, renderOpts) + err := render.RenderDiagnostics(cmd.OutOrStdout(), b, diags, renderOpts) if err != nil { return fmt.Errorf("failed to render output: %w", err) }